fix(login): render standalone views edge-to-edge e esconde scrollbar do carrossel de onboarding
- App.tsx: wrappers de padding (p-4/md:p-8) e max-w-7xl/mx-auto agora só se aplicam quando não é tela standalone; login/landing/public renderizam full-bleed - OnboardingChat.tsx: classe ob-noscroll esconde a barra de rolagem do slide intro (mantém scroll), substituindo a custom-scrollbar indefinida no fluxo de login Inclui também demais alterações pendentes do working tree (snapshot do estado atual de dev). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+255
-30
@@ -2,7 +2,10 @@ import React, { useState, useEffect } from 'react';
|
||||
import { HybridBackend } from './services/backend.ts';
|
||||
|
||||
import { Sidebar } from './components/Sidebar.tsx';
|
||||
import { Menu } from 'lucide-react';
|
||||
import {
|
||||
Menu, LayoutDashboard, Users, Calendar as CalendarIcon,
|
||||
DollarSign, Activity, ClipboardList, Bell, Settings, MoreHorizontal, Eye, X
|
||||
} from 'lucide-react';
|
||||
|
||||
import { ToastContainer } from './components/Toast.tsx';
|
||||
|
||||
@@ -16,6 +19,7 @@ import { FinanceiroView } from './views/FinanceiroView.tsx';
|
||||
import { MeusTratamentos } from './views/MeusTratamentos';
|
||||
import { LancarGTO } from './views/LancarGTO';
|
||||
import { LoginView } from './views/LoginView.tsx';
|
||||
import { ChangePasswordModal } from './components/ChangePasswordModal.tsx';
|
||||
import { LandingPage } from './views/LandingPage.tsx';
|
||||
import { UpdateDbView } from './views/UpdateDbView.tsx';
|
||||
import { ClinicasView } from './views/ClinicasView.tsx';
|
||||
@@ -25,6 +29,20 @@ import { NotificationsView } from './views/NotificationsView.tsx';
|
||||
import { ReportsView } from './views/ReportsView.tsx';
|
||||
import { ConfiguracoesView } from './views/ConfiguracoesView.tsx';
|
||||
import { ContratosView } from './views/ContratosView.tsx';
|
||||
import { PluginsView } from './views/PluginsView.tsx';
|
||||
import { RXPlugin } from './views/plugins/RXPlugin.tsx';
|
||||
import { SalasPlugin } from './views/plugins/SalasPlugin.tsx';
|
||||
import { ProfissionaisPlugin } from './views/plugins/ProfissionaisPlugin.tsx';
|
||||
import { isPluginActive } from './views/plugins/pluginRegistry.ts';
|
||||
import { CreateClinicView } from './views/CreateClinicView.tsx';
|
||||
import { RxConfigView } from './views/RxConfigView.tsx';
|
||||
import { GestaoEquipeView } from './views/GestaoEquipeView.tsx';
|
||||
import { TutorPainelView } from './views/TutorPainelView.tsx';
|
||||
import { TutorCandidaturaView } from './views/TutorCandidaturaView.tsx';
|
||||
import { AdminGestaoTutoresView } from './views/AdminGestaoTutoresView.tsx';
|
||||
import { WaitingInviteView } from './views/WaitingInviteView.tsx';
|
||||
import { SuperAdminView } from './views/SuperAdminView.tsx';
|
||||
import { DiretorioProfissionaisView } from './views/DiretorioProfissionaisView.tsx';
|
||||
|
||||
export type ViewKey =
|
||||
| 'landing'
|
||||
@@ -49,7 +67,26 @@ export type ViewKey =
|
||||
| 'relatorios'
|
||||
| 'cadastro-dentista'
|
||||
| 'configuracoes'
|
||||
| 'contratos';
|
||||
| 'contratos'
|
||||
| 'plugins'
|
||||
| 'rx-radiografias'
|
||||
| 'salas'
|
||||
| 'marketplace-profissionais'
|
||||
| 'criar-clinica'
|
||||
| 'rx-config'
|
||||
| 'gestao-equipe'
|
||||
| 'tutoria-painel'
|
||||
| 'aguardando-convite'
|
||||
| 'candidatura-tutor'
|
||||
| 'admin-gestao-tutores'
|
||||
| 'superadmin'
|
||||
| 'superadmin-overview'
|
||||
| 'superadmin-users'
|
||||
| 'superadmin-clinicas'
|
||||
| 'superadmin-logs'
|
||||
| 'superadmin-financeiro'
|
||||
| 'superadmin-notificacoes'
|
||||
| 'diretorio-profissionais';
|
||||
|
||||
// ------- URL <-> VIEW mapping -------
|
||||
const ROUTE_MAP: Record<string, ViewKey> = {
|
||||
@@ -77,6 +114,25 @@ const ROUTE_MAP: Record<string, ViewKey> = {
|
||||
'/cadastro-dentista': 'cadastro-dentista',
|
||||
'/configuracoes': 'configuracoes',
|
||||
'/contratos': 'contratos',
|
||||
'/plugins': 'plugins',
|
||||
'/rx-radiografias': 'rx-radiografias',
|
||||
'/salas': 'salas',
|
||||
'/marketplace-profissionais': 'marketplace-profissionais',
|
||||
'/criar-clinica': 'criar-clinica',
|
||||
'/rx-config': 'rx-config',
|
||||
'/gestao-equipe': 'gestao-equipe',
|
||||
'/tutoria-painel': 'tutoria-painel',
|
||||
'/candidatura-tutor': 'candidatura-tutor',
|
||||
'/admin-gestao-tutores': 'admin-gestao-tutores',
|
||||
'/superadmin': 'superadmin-overview',
|
||||
'/superadmin/overview': 'superadmin-overview',
|
||||
'/superadmin/users': 'superadmin-users',
|
||||
'/superadmin/clinicas': 'superadmin-clinicas',
|
||||
'/superadmin/logs': 'superadmin-logs',
|
||||
'/superadmin/financeiro': 'superadmin-financeiro',
|
||||
'/superadmin/notificacoes': 'superadmin-notificacoes',
|
||||
'/aguardando-convite': 'aguardando-convite',
|
||||
'/diretorio-profissionais': 'diretorio-profissionais',
|
||||
};
|
||||
|
||||
const VIEW_TO_ROUTE: Record<ViewKey, string> = {
|
||||
@@ -103,6 +159,25 @@ const VIEW_TO_ROUTE: Record<ViewKey, string> = {
|
||||
'cadastro-dentista': '/cadastro-dentista',
|
||||
configuracoes: '/configuracoes',
|
||||
contratos: '/contratos',
|
||||
plugins: '/plugins',
|
||||
'rx-radiografias': '/rx-radiografias',
|
||||
salas: '/salas',
|
||||
'marketplace-profissionais': '/marketplace-profissionais',
|
||||
'criar-clinica': '/criar-clinica',
|
||||
'rx-config': '/rx-config',
|
||||
'gestao-equipe': '/gestao-equipe',
|
||||
'tutoria-painel': '/tutoria-painel',
|
||||
'candidatura-tutor': '/candidatura-tutor',
|
||||
'admin-gestao-tutores': '/admin-gestao-tutores',
|
||||
'aguardando-convite': '/aguardando-convite',
|
||||
superadmin: '/superadmin',
|
||||
'superadmin-overview': '/superadmin/overview',
|
||||
'superadmin-users': '/superadmin/users',
|
||||
'superadmin-clinicas': '/superadmin/clinicas',
|
||||
'superadmin-logs': '/superadmin/logs',
|
||||
'superadmin-financeiro': '/superadmin/financeiro',
|
||||
'superadmin-notificacoes': '/superadmin/notificacoes',
|
||||
'diretorio-profissionais': '/diretorio-profissionais',
|
||||
};
|
||||
|
||||
/** Resolve current URL to a ViewKey */
|
||||
@@ -117,63 +192,145 @@ function navigate(view: ViewKey) {
|
||||
history.pushState({}, '', route);
|
||||
}
|
||||
|
||||
// ------- Mobile Bottom Nav -------
|
||||
const BOTTOM_TABS: Record<string, Array<{ id: ViewKey; icon: React.ElementType; label: string } | { more: true }>> = {
|
||||
admin: [{ id: 'dashboard', icon: LayoutDashboard, label: 'Início' }, { id: 'pacientes', icon: Users, label: 'Pacientes' }, { id: 'agenda', icon: CalendarIcon, label: 'Agenda' }, { id: 'financeiro', icon: DollarSign, label: 'Financeiro' }, { more: true }],
|
||||
donoclinica: [{ id: 'dashboard', icon: LayoutDashboard, label: 'Início' }, { id: 'pacientes', icon: Users, label: 'Pacientes' }, { id: 'agenda', icon: CalendarIcon, label: 'Agenda' }, { id: 'financeiro', icon: DollarSign, label: 'Financeiro' }, { more: true }],
|
||||
donoconsultorio: [{ id: 'dashboard', icon: LayoutDashboard, label: 'Início' }, { id: 'pacientes', icon: Users, label: 'Pacientes' }, { id: 'agenda', icon: CalendarIcon, label: 'Agenda' }, { id: 'financeiro', icon: DollarSign, label: 'Financeiro' }, { more: true }],
|
||||
funcionario: [{ id: 'dashboard', icon: LayoutDashboard, label: 'Início' }, { id: 'pacientes', icon: Users, label: 'Pacientes' }, { id: 'agenda', icon: CalendarIcon, label: 'Agenda' }, { id: 'financeiro', icon: DollarSign, label: 'Financeiro' }, { more: true }],
|
||||
dentista: [{ id: 'dashboard', icon: LayoutDashboard, label: 'Início' }, { id: 'agenda', icon: CalendarIcon, label: 'Agenda' }, { id: 'ortodontia', icon: Activity, label: 'Orto' }, { more: true }],
|
||||
biomedico: [{ id: 'dashboard', icon: LayoutDashboard, label: 'Início' }, { id: 'agenda', icon: CalendarIcon, label: 'Agenda' }, { id: 'ortodontia', icon: Activity, label: 'Orto' }, { more: true }],
|
||||
paciente: [{ id: 'tratamentos', icon: ClipboardList, label: 'Tratamentos' }, { id: 'notificacoes', icon: Bell, label: 'Notif.' }, { id: 'configuracoes', icon: Settings, label: 'Config.' }],
|
||||
};
|
||||
|
||||
const MobileBottomNav: React.FC<{
|
||||
activeTab: ViewKey;
|
||||
role: string;
|
||||
clinColor: string;
|
||||
onNavigate: (v: ViewKey) => void;
|
||||
onMore: () => void;
|
||||
}> = ({ activeTab, role, clinColor, onNavigate, onMore }) => {
|
||||
const tabs = BOTTOM_TABS[role] || BOTTOM_TABS['funcionario'];
|
||||
return (
|
||||
<nav className="md:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 z-40">
|
||||
<div className="flex items-stretch justify-around" style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}>
|
||||
{tabs.map((tab, i) => {
|
||||
if ('more' in tab) return (
|
||||
<button key="more" onClick={onMore}
|
||||
className="flex flex-col items-center justify-center gap-0.5 py-2 px-3 flex-1 text-gray-400 active:bg-gray-50">
|
||||
<MoreHorizontal size={22} />
|
||||
<span className="text-[9px] font-black uppercase">Mais</span>
|
||||
</button>
|
||||
);
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === (tab as any).id;
|
||||
return (
|
||||
<button key={i} onClick={() => onNavigate((tab as any).id)}
|
||||
className="flex flex-col items-center justify-center gap-0.5 py-2 px-3 flex-1 active:bg-gray-50 transition-colors"
|
||||
style={{ color: isActive ? clinColor : undefined }}>
|
||||
<Icon size={22} className={isActive ? '' : 'text-gray-400'} />
|
||||
<span className={`text-[9px] font-black uppercase ${isActive ? '' : 'text-gray-400'}`}>{(tab as any).label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
// ------- App -------
|
||||
const App: React.FC = () => {
|
||||
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [, forcePwRerender] = useState(0); // re-avalia o modal de troca obrigatória de senha
|
||||
const currentRole = HybridBackend.getCurrentRole();
|
||||
const activeWorkspace = HybridBackend.getActiveWorkspace();
|
||||
const clinColor = activeWorkspace?.cor || '#2563eb';
|
||||
|
||||
const isViewAllowed = (view: ViewKey, role: string): boolean => {
|
||||
if (['landing', 'login', 'public', 'update'].includes(view)) return true;
|
||||
if (role === 'admin' || role === 'donoclinica') return true;
|
||||
if (role === 'superadmin') return true;
|
||||
// Sync GLOBAL removido do app (substituído pelo backup por clínica em Configurações).
|
||||
if (view === 'sync') return false;
|
||||
// Admin/donos têm acesso amplo, MAS as telas de superadmin são exclusivas do superadmin.
|
||||
if (role === 'admin' || role === 'donoclinica' || role === 'donoconsultorio') return !view.startsWith('superadmin');
|
||||
|
||||
// Equipe: acesso por CARGO (RBAC) no workspace ativo.
|
||||
const ws: any = HybridBackend.getActiveWorkspace();
|
||||
if (ws && ['donoclinica', 'donoconsultorio', 'admin'].includes(ws.role)) return !view.startsWith('superadmin');
|
||||
// Páginas sempre disponíveis a qualquer membro (navegação/conta).
|
||||
if (['configuracoes', 'notificacoes', 'clinicas'].includes(view)) return true;
|
||||
// Só gateia por cargo quando há permissões EXPLÍCITAS. Cargo sem permissões
|
||||
// (ou membro sem cargo) herda o menu padrão do role (fallback abaixo).
|
||||
if (Array.isArray(ws?.permissoes) && ws.permissoes.length > 0) return (ws.permissoes as string[]).includes(view);
|
||||
|
||||
const permissions: Record<string, ViewKey[]> = {
|
||||
paciente: ['tratamentos', 'notificacoes', 'configuracoes'],
|
||||
dentista: ['dashboard', 'pacientes', 'agenda', 'ortodontia', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'contratos'],
|
||||
funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes', 'contratos'],
|
||||
dentista: ['dashboard', 'agenda', 'ortodontia', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'tutoria-painel', 'diretorio-profissionais'],
|
||||
biomedico: ['dashboard', 'agenda', 'ortodontia', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'],
|
||||
funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes', 'contratos', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'diretorio-profissionais'],
|
||||
};
|
||||
|
||||
return permissions[role]?.includes(view) || false;
|
||||
};
|
||||
|
||||
const getDefaultViewForRole = (role: string): ViewKey => {
|
||||
if (role === 'paciente') return 'tratamentos';
|
||||
if (role === 'superadmin') return 'superadmin-overview';
|
||||
if (role === 'paciente') return 'tratamentos';
|
||||
return 'dashboard';
|
||||
};
|
||||
|
||||
const getStoredUserRole = (): string => {
|
||||
try {
|
||||
const raw = localStorage.getItem('SCOREODONTO_USER_DATA');
|
||||
return raw ? (JSON.parse(raw).role ?? 'paciente') : 'paciente';
|
||||
} catch { return 'paciente'; }
|
||||
};
|
||||
|
||||
const getInitialView = (): ViewKey => {
|
||||
const fromUrl = resolveViewFromUrl();
|
||||
const isAuthed = HybridBackend.isAuthenticated();
|
||||
|
||||
// Superadmin pode acessar páginas públicas (landing / contato) diretamente,
|
||||
// sem ser rebotado de volta para o painel.
|
||||
if (isAuthed && getStoredUserRole() === 'superadmin' && ['landing', 'public'].includes(fromUrl)) {
|
||||
return fromUrl;
|
||||
}
|
||||
|
||||
// Authenticated but no workspace → route to the right post-registration screen
|
||||
if (isAuthed && !HybridBackend.getActiveWorkspace()) {
|
||||
const storedRole = getStoredUserRole();
|
||||
if (storedRole === 'superadmin') return 'superadmin-overview';
|
||||
if (storedRole === 'admin' || storedRole === 'donoclinica' || storedRole === 'donoconsultorio') return 'criar-clinica';
|
||||
if (storedRole === 'dentista' || storedRole === 'biomedico' || storedRole === 'protetico') return 'aguardando-convite';
|
||||
return 'aguardando-convite';
|
||||
}
|
||||
|
||||
const role = HybridBackend.getCurrentRole();
|
||||
|
||||
// If user is authenticated, and trying to go to landing or login, send to default for role
|
||||
if (HybridBackend.isAuthenticated() && ['landing', 'login'].includes(fromUrl)) return getDefaultViewForRole(role);
|
||||
// Authenticated visiting landing/login → default for role
|
||||
if (isAuthed && ['landing', 'login'].includes(fromUrl)) return getDefaultViewForRole(role);
|
||||
|
||||
// If URL points to a protected view but user is not auth'd → landing (or login)
|
||||
// Protected route without auth → landing
|
||||
const isProtected = !['landing', 'login', 'public'].includes(fromUrl);
|
||||
if (isProtected && !HybridBackend.isAuthenticated()) return 'landing';
|
||||
if (isProtected && !isAuthed) return 'landing';
|
||||
|
||||
// Verify permission for the view
|
||||
if (HybridBackend.isAuthenticated() && !isViewAllowed(fromUrl, role)) {
|
||||
return getDefaultViewForRole(role);
|
||||
}
|
||||
// Authenticated but lacking permission for the requested view
|
||||
if (isAuthed && !isViewAllowed(fromUrl, role)) return getDefaultViewForRole(role);
|
||||
|
||||
return fromUrl;
|
||||
};
|
||||
|
||||
const [currentView, setCurrentView] = useState<ViewKey>(getInitialView);
|
||||
|
||||
// Keep URL in sync when view changes programmatically via sidebar
|
||||
const handleNavigate = (view: ViewKey) => {
|
||||
const freshRole = HybridBackend.getCurrentRole();
|
||||
if (isViewAllowed(view, freshRole)) {
|
||||
setCurrentView(view);
|
||||
navigate(view);
|
||||
setSidebarOpen(false); // Close sidebar on mobile after navigation
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Sync view when user uses browser back/forward buttons
|
||||
useEffect(() => {
|
||||
const onHashChange = () => {
|
||||
const fromUrl = resolveViewFromUrl();
|
||||
@@ -199,13 +356,11 @@ const App: React.FC = () => {
|
||||
return () => window.removeEventListener('popstate', onHashChange);
|
||||
}, []);
|
||||
|
||||
// Inject dynamic brand color
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty('--brand-color', clinColor);
|
||||
document.documentElement.style.setProperty('--brand-color-soft', `${clinColor}11`);
|
||||
}, [clinColor]);
|
||||
|
||||
// On mount: always sync URL to match the actual current view (handles auth redirects).
|
||||
useEffect(() => {
|
||||
navigate(currentView);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -213,7 +368,12 @@ const App: React.FC = () => {
|
||||
|
||||
const renderView = () => {
|
||||
switch (currentView) {
|
||||
case 'landing': return <LandingPage onGetStarted={() => handleNavigate('login')} />;
|
||||
case 'landing': return <LandingPage
|
||||
onGetStarted={() => handleNavigate('login')}
|
||||
isAuthenticated={HybridBackend.isAuthenticated()}
|
||||
userName={HybridBackend.getCurrentUserName()}
|
||||
onGoToArea={() => handleNavigate(getDefaultViewForRole(HybridBackend.getCurrentRole()))}
|
||||
/>;
|
||||
case 'dashboard': return <Dashboard />;
|
||||
case 'leads': return <LeadsView />;
|
||||
case 'public': return <PublicContactForm />;
|
||||
@@ -235,9 +395,54 @@ const App: React.FC = () => {
|
||||
case 'cadastro-dentista': return <DentistRegisterView />;
|
||||
case 'configuracoes': return <ConfiguracoesView />;
|
||||
case 'contratos': return <ContratosView />;
|
||||
case 'plugins': return <PluginsView />;
|
||||
case 'rx-radiografias': return <RXPlugin />;
|
||||
case 'salas': return <SalasPlugin />;
|
||||
case 'marketplace-profissionais': return <ProfissionaisPlugin />;
|
||||
case 'criar-clinica': return <CreateClinicView onCreated={() => { window.location.reload(); }} />;
|
||||
case 'rx-config': return <RxConfigView />;
|
||||
case 'gestao-equipe': return <GestaoEquipeView />;
|
||||
case 'tutoria-painel': return <TutorPainelView />;
|
||||
case 'candidatura-tutor': return <TutorCandidaturaView />;
|
||||
case 'admin-gestao-tutores': return <AdminGestaoTutoresView />;
|
||||
case 'aguardando-convite': return <WaitingInviteView />;
|
||||
case 'superadmin': return <SuperAdminView tab="overview" />;
|
||||
case 'superadmin-overview': return <SuperAdminView tab="overview" />;
|
||||
case 'superadmin-users': return <SuperAdminView tab="users" />;
|
||||
case 'superadmin-clinicas': return <SuperAdminView tab="clinicas" />;
|
||||
case 'superadmin-logs': return <SuperAdminView tab="logs" />;
|
||||
case 'superadmin-financeiro': return <SuperAdminView tab="financeiro" />;
|
||||
case 'superadmin-notificacoes': return <SuperAdminView tab="notificacoes" />;
|
||||
// Plugin Profissionais ativo absorve o diretório antigo (mesma rota, view nova).
|
||||
case 'diretorio-profissionais': return isPluginActive('profissionais-marketplace') ? <ProfissionaisPlugin /> : <DiretorioProfissionaisView />;
|
||||
case 'login':
|
||||
default:
|
||||
return <LoginView onLoginSuccess={() => {
|
||||
return <LoginView onLoginSuccess={(result) => {
|
||||
if (result?.noWorkspace) {
|
||||
const userRole = result.userRole || 'dentista';
|
||||
if (userRole === 'superadmin') {
|
||||
setCurrentView('superadmin');
|
||||
navigate('superadmin');
|
||||
} else if (userRole === 'admin' || userRole === 'donoclinica' || userRole === 'donoconsultorio') {
|
||||
setCurrentView('criar-clinica');
|
||||
navigate('criar-clinica');
|
||||
} else {
|
||||
setCurrentView('aguardando-convite');
|
||||
navigate('aguardando-convite');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const storedRole = getStoredUserRole();
|
||||
if (storedRole === 'superadmin') {
|
||||
setCurrentView('superadmin-overview');
|
||||
navigate('superadmin-overview');
|
||||
return;
|
||||
}
|
||||
if (storedRole === 'donoconsultorio') {
|
||||
setCurrentView('dashboard');
|
||||
navigate('dashboard');
|
||||
return;
|
||||
}
|
||||
const role = HybridBackend.getCurrentRole();
|
||||
const view = getDefaultViewForRole(role);
|
||||
setCurrentView(view);
|
||||
@@ -246,10 +451,13 @@ const App: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const isStandaloneView = ['landing', 'login', 'public', 'update', 'cadastro-dentista'].includes(currentView);
|
||||
const isStandaloneView = ['landing', 'login', 'public', 'update', 'cadastro-dentista', 'criar-clinica', 'aguardando-convite'].includes(currentView);
|
||||
|
||||
return (
|
||||
<>
|
||||
{HybridBackend.isAuthenticated() && HybridBackend.mustChangePassword() && (
|
||||
<ChangePasswordModal forced onDone={() => forcePwRerender(t => t + 1)} />
|
||||
)}
|
||||
<div className="flex h-screen bg-gray-50 text-gray-800 overflow-hidden relative">
|
||||
<style>{`
|
||||
:root { --brand-color: ${clinColor}; }
|
||||
@@ -262,7 +470,6 @@ const App: React.FC = () => {
|
||||
|
||||
{!isStandaloneView && (
|
||||
<>
|
||||
{/* Mobile Hamburger Overlay */}
|
||||
{isSidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-20 md:hidden backdrop-blur-sm transition-opacity"
|
||||
@@ -270,7 +477,6 @@ const App: React.FC = () => {
|
||||
></div>
|
||||
)}
|
||||
|
||||
{/* Sidebar with mobile toggle logic */}
|
||||
<div className={`fixed inset-y-0 left-0 z-30 transform transition-transform duration-300 md:relative md:translate-x-0 ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`}>
|
||||
<Sidebar
|
||||
activeTab={currentView}
|
||||
@@ -281,9 +487,24 @@ const App: React.FC = () => {
|
||||
)}
|
||||
|
||||
<main className={`flex-1 h-full relative flex flex-col min-w-0`}>
|
||||
{HybridBackend.isPreviewMode() && (
|
||||
<div className="bg-red-600 text-white px-4 py-2 flex items-center justify-between gap-3 shadow-md z-30">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Eye size={15} className="shrink-0" />
|
||||
<span className="text-[11px] font-black uppercase tracking-wide truncate">
|
||||
Modo visualização — vendo a área como {HybridBackend.getPreviewRole()}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => HybridBackend.exitPreview()}
|
||||
className="shrink-0 flex items-center gap-1.5 bg-white/15 hover:bg-white/25 px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all"
|
||||
>
|
||||
<X size={13} /> Sair da visualização
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!isStandaloneView && (
|
||||
<>
|
||||
{/* Mobile Navbar Header */}
|
||||
<div className="md:hidden flex items-center justify-between px-4 py-3 bg-white border-b border-gray-200">
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
@@ -302,10 +523,14 @@ const App: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 md:p-8 pb-20 scroll-smooth">
|
||||
<div className="max-w-7xl mx-auto h-full">
|
||||
{renderView()}
|
||||
</div>
|
||||
<div className={`flex-1 overflow-y-auto scroll-smooth ${isStandaloneView ? '' : 'p-4 md:p-8'}`}>
|
||||
{isStandaloneView ? (
|
||||
renderView()
|
||||
) : (
|
||||
<div className="max-w-7xl mx-auto h-full">
|
||||
{renderView()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -314,4 +539,4 @@ const App: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
||||
Reference in New Issue
Block a user