import React, { useState, useEffect } from 'react'; import { HybridBackend } from './services/backend.ts'; import { Sidebar } from './components/Sidebar.tsx'; import { Menu } from 'lucide-react'; import { ToastContainer } from './components/Toast.tsx'; import { Dashboard } from './views/Dashboard.tsx'; import { LeadsView } from './views/LeadsView.tsx'; import { PublicContactForm } from './views/PublicContactForm.tsx'; import { PatientsView } from './views/PatientsView.tsx'; import { AgendaView } from './views/AgendaView.tsx'; import { OrthoView } from './views/OrthoView.tsx'; import { FinanceiroView } from './views/FinanceiroView.tsx'; import { MeusTratamentos } from './views/MeusTratamentos'; import { LancarGTO } from './views/LancarGTO'; import { LoginView } from './views/LoginView.tsx'; import { LandingPage } from './views/LandingPage.tsx'; import { UpdateDbView } from './views/UpdateDbView.tsx'; import { ClinicasView } from './views/ClinicasView.tsx'; import { DentistRegisterView } from './views/DentistRegisterView.tsx'; import { DentistasView, EspecialidadesView, PlanosView, SyncView, ProcedimentosView } from './views/AdminViews.tsx'; import { NotificationsView } from './views/NotificationsView.tsx'; import { ReportsView } from './views/ReportsView.tsx'; export type ViewKey = | 'landing' | 'dashboard' | 'leads' | 'public' | 'pacientes' | 'agenda' | 'ortodontia' | 'financeiro' | 'login' | 'update' | 'dentistas' | 'especialidades' | 'procedimentos' | 'planos' | 'tratamentos' | 'lancar-gto' | 'notificacoes' | 'clinicas' | 'sync' | 'relatorios' | 'cadastro-dentista'; // ------- URL <-> VIEW mapping ------- const ROUTE_MAP: Record = { '/': 'landing', '/dashboard': 'dashboard', '/landing': 'landing', '/leads': 'leads', '/contato': 'public', '/pacientes': 'pacientes', '/agenda': 'agenda', '/ortodontia': 'ortodontia', '/financeiro': 'financeiro', '/login': 'login', '/update': 'update', '/clinicas': 'clinicas', '/admin/dentistas': 'dentistas', '/admin/especialidades': 'especialidades', '/admin/procedimentos': 'procedimentos', '/admin/planos': 'planos', '/admin/sync': 'sync', '/tratamentos': 'tratamentos', '/lancar-gto': 'lancar-gto', '/notificacoes': 'notificacoes', '/relatorios': 'relatorios', '/cadastro-dentista': 'cadastro-dentista', }; const VIEW_TO_ROUTE: Record = { landing: '/', dashboard: '/dashboard', leads: '/leads', public: '/contato', pacientes: '/pacientes', agenda: '/agenda', ortodontia: '/ortodontia', financeiro: '/financeiro', login: '/login', update: '/update', clinicas: '/clinicas', dentistas: '/admin/dentistas', especialidades: '/admin/especialidades', procedimentos: '/admin/procedimentos', planos: '/admin/planos', tratamentos: '/tratamentos', 'lancar-gto': '/lancar-gto', notificacoes: '/notificacoes', relatorios: '/relatorios', sync: '/admin/sync', 'cadastro-dentista': '/cadastro-dentista' }; /** Get the hash path, e.g. "/#/pacientes" → "/pacientes" */ function getHashPath(): string { const hash = window.location.hash; if (hash.startsWith('#/')) return hash.slice(1); // "#/pacientes" → "/pacientes" if (hash === '#' || hash === '') return '/'; return '/'; } /** Resolve current URL to a ViewKey */ function resolveViewFromUrl(): ViewKey { // Legacy support: ?p=contato if (window.location.search.includes('p=contato')) return 'public'; // Legacy support: #update_db (old non-slash hash) if (window.location.hash === '#update_db') return 'update'; const path = getHashPath(); return ROUTE_MAP[path] ?? 'dashboard'; } /** Push a new hash-based URL without reloading the page */ function navigate(view: ViewKey) { const route = VIEW_TO_ROUTE[view]; window.location.hash = route; } // ------- App ------- const App: React.FC = () => { const [isSidebarOpen, setSidebarOpen] = useState(false); 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; const permissions: Record = { paciente: ['tratamentos', 'notificacoes'], dentista: ['dashboard', 'pacientes', 'agenda', 'ortodontia', 'tratamentos', 'notificacoes', 'clinicas'], funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas'], }; return permissions[role]?.includes(view) || false; }; const getDefaultViewForRole = (role: string): ViewKey => { if (role === 'paciente') return 'tratamentos'; return 'dashboard'; }; const getInitialView = (): ViewKey => { const fromUrl = resolveViewFromUrl(); 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); // If URL points to a protected view but user is not auth'd → landing (or login) const isProtected = !['landing', 'login', 'public'].includes(fromUrl); if (isProtected && !HybridBackend.isAuthenticated()) return 'landing'; // Verify permission for the view if (HybridBackend.isAuthenticated() && !isViewAllowed(fromUrl, role)) { return getDefaultViewForRole(role); } return fromUrl; }; const [currentView, setCurrentView] = useState(getInitialView); // Keep URL in sync when view changes programmatically via sidebar const handleNavigate = (view: ViewKey) => { if (isViewAllowed(view, currentRole)) { setCurrentView(view); navigate(view); setSidebarOpen(false); // Close sidebar on mobile after navigation } }; // Sync view when user uses browser back/forward buttons useEffect(() => { const onHashChange = () => { const fromUrl = resolveViewFromUrl(); const role = HybridBackend.getCurrentRole(); const isProtected = !['landing', 'login', 'public'].includes(fromUrl); if (isProtected && !HybridBackend.isAuthenticated()) { setCurrentView('landing'); navigate('landing'); return; } if (HybridBackend.isAuthenticated() && !isViewAllowed(fromUrl, role)) { const defView = getDefaultViewForRole(role); setCurrentView(defView); navigate(defView); return; } setCurrentView(fromUrl); }; window.addEventListener('hashchange', onHashChange); return () => window.removeEventListener('hashchange', 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: if there's no hash yet, write the current view into the URL useEffect(() => { if (!window.location.hash || window.location.hash === '#update_db') { navigate(currentView); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const renderView = () => { switch (currentView) { case 'landing': return handleNavigate('login')} />; case 'dashboard': return ; case 'leads': return ; case 'public': return ; case 'pacientes': return ; case 'agenda': return ; case 'ortodontia': return ; case 'financeiro': return ; case 'dentistas': return ; case 'especialidades': return ; case 'procedimentos': return ; case 'planos': return ; case 'sync': return ; case 'tratamentos': return ; case 'lancar-gto': return ; case 'update': return ; case 'clinicas': return ; case 'notificacoes': return ; case 'relatorios': return ; case 'cadastro-dentista': return ; case 'login': default: return { const role = HybridBackend.getCurrentRole(); handleNavigate(getDefaultViewForRole(role)); }} />; } }; const isStandaloneView = ['landing', 'login', 'public', 'update', 'cadastro-dentista'].includes(currentView); return ( <>
{!isStandaloneView && ( <> {/* Mobile Hamburger Overlay */} {isSidebarOpen && (
setSidebarOpen(false)} >
)} {/* Sidebar with mobile toggle logic */}
handleNavigate(t as ViewKey)} />
)}
{!isStandaloneView && ( <> {/* Mobile Navbar Header */}
S
SCOREODONTO
)}
{renderView()}
); }; export default App;