diff --git a/src/App.jsx b/src/App.jsx index 348b3d1..fdc85e5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,9 +1,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react' -import { authRepository } from './repositories/authRepository.js' - import './App.css' import { AppShell } from './components/AppShell.jsx' +import { canAccess } from './config/permissions.js' +import { useAuth } from './hooks/useAuth.js' import { AgendaPage } from './pages/AgendaPage.jsx' import { AnalyticsPage } from './pages/AnalyticsPage.jsx' import { ForgotPasswordPage, LoginPage, RegisterPage } from './pages/AuthPages.jsx' @@ -16,11 +16,13 @@ import { ProfilePage } from './pages/ProfilePage.jsx' import { ReportsPage } from './pages/ReportsPage.jsx' import { SettingsPage } from './pages/SettingsPage.jsx' import { TeamPage } from './pages/TeamPage.jsx' +import { UsersPage } from './pages/UsersPage.jsx' import { VisitsPage } from './pages/VisitsPage.jsx' import { patientRepository } from './repositories/patientRepository.js' function App() { const [location, setLocation] = useState(() => readLocation()) + const { isAuthenticated, role, loading: authLoading } = useAuth() const navigate = useCallback((to, options = {}) => { if (options.replace) { @@ -49,25 +51,47 @@ function App() { return () => window.removeEventListener('popstate', handlePopState) }, []) - const route = useMemo(() => resolveRoute(location.pathname, navigate), [location.pathname, navigate]) - const isAuthenticated = authRepository.isAuthenticated() + const route = useMemo( + () => resolveRoute(location.pathname, navigate, role), + [location.pathname, navigate, role], + ) + // Tela de carregamento enquanto busca o role do usuário + if (authLoading) { + return ( +
+

Carregando...

+
+ ) + } + + // Rotas públicas (sem shell) if (!route.withShell) { return route.element } + // Usuário não autenticado if (!isAuthenticated) { return } + // Usuário autenticado mas sem permissão para a rota + if (!role || !canAccess(role, location.pathname)) { + return ( + + + + ) + } + return ( - + {route.element} ) } -function resolveRoute(pathname, navigate) { +function resolveRoute(pathname, navigate, role) { if (pathname === '/' || pathname === '/login') { return { element: , @@ -102,7 +126,7 @@ function resolveRoute(pathname, navigate) { if (pathname === '/agenda') { return { - element: , + element: , title: 'Agenda', withShell: true, } @@ -110,7 +134,7 @@ function resolveRoute(pathname, navigate) { if (pathname === '/pacientes') { return { - element: , + element: , title: 'Pacientes', withShell: true, } @@ -126,7 +150,6 @@ function resolveRoute(pathname, navigate) { if (pathname.startsWith('/pacientes/')) { const patientId = pathname.split('/')[2] - return { element: , title: 'Paciente', @@ -145,7 +168,7 @@ function resolveRoute(pathname, navigate) { if (pathname === '/laudos') { return { element: , - title: 'Relatorios medicos', + title: 'Relatórios médicos', withShell: true, } } @@ -158,9 +181,18 @@ function resolveRoute(pathname, navigate) { } } - if (pathname === '/camunicacao' || pathname === '/comunicacao' || pathname === '/mensagens') { + if (pathname === '/camunicacao') { + navigate('/comunicacao', { replace: true }) return { - element: , + element: , + title: 'Comunicação', + withShell: true, + } + } + + if (pathname === '/comunicacao' || pathname === '/mensagens') { + return { + element: , title: 'Comunicação', withShell: true, } @@ -174,6 +206,14 @@ function resolveRoute(pathname, navigate) { } } + if (pathname === '/usuarios') { + return { + element: , + title: 'Usuários', + withShell: true, + } + } + if (pathname === '/perfil') { return { element: , @@ -192,7 +232,7 @@ function resolveRoute(pathname, navigate) { return { element: , - title: 'Tela nao encontrada', + title: 'Página não encontrada', withShell: true, } } @@ -204,7 +244,8 @@ function PatientDetailRoute({ navigate, patientId }) { useEffect(() => { let active = true - patientRepository.getById(patientId) + patientRepository + .getById(patientId) .then((data) => { if (active) setPatient(data) }) @@ -221,7 +262,30 @@ function PatientDetailRoute({ navigate, patientId }) { return
Carregando paciente...
} - return patient ? : + return patient ? ( + + ) : ( + + ) +} + +function UnauthorizedPage({ navigate }) { + return ( +
+

🔒

+

Acesso não permitido

+

+ Você não tem permissão para acessar esta página. +

+ +
+ ) } function readLocation() { diff --git a/src/components/AppShell.jsx b/src/components/AppShell.jsx index f44c0c7..22b2036 100644 --- a/src/components/AppShell.jsx +++ b/src/components/AppShell.jsx @@ -1,22 +1,26 @@ import { useEffect, useMemo, useState } from 'react' +import { ROLE_LABELS, ROLE_NAV_ITEMS } from '../config/permissions.js' import { profileRepository } from '../repositories/profileRepository.js' import { BrandLogo } from './Brand.jsx' -const navItems = [ +// Todos os itens de navegação com seus ícones e metadados +const ALL_NAV_ITEMS = [ { href: '/inicio', label: 'Painel', icon: 'pulse', activePaths: ['/inicio', '/home', '/dashboard'] }, { href: '/agenda', label: 'Agenda', icon: 'calendar' }, { href: '/pacientes', label: 'Pacientes', icon: 'users', exact: true }, - { href: '/prontuario', label: 'Prontuario', icon: 'file' }, - { href: '/laudos', label: 'Relatorios medicos', icon: 'clipboard' }, + { href: '/prontuario', label: 'Prontuário', icon: 'file' }, + { href: '/laudos', label: 'Relatórios médicos', icon: 'clipboard' }, { - href: '/camunicacao', - label: 'Comunicacao', + href: '/comunicacao', + label: 'Comunicação', icon: 'message', - activePaths: ['/camunicacao', '/comunicacao', '/mensagens'], + activePaths: ['/comunicacao', '/mensagens'], }, - { href: '/relatorios', label: 'Relatorios', icon: 'chart' }, - { href: '/configuracoes', label: 'Configuracoes', icon: 'settings', activePaths: ['/configuracoes', '/config'] }, + { href: '/relatorios', label: 'Relatórios', icon: 'chart' }, + { href: '/profissionais', label: 'Profissionais', icon: 'users' }, + { href: '/usuarios', label: 'Usuários', icon: 'shield' }, + { href: '/configuracoes', label: 'Configurações', icon: 'settings', activePaths: ['/configuracoes', '/config'] }, ] const titles = { @@ -25,23 +29,23 @@ const titles = { '/dashboard': 'Painel', '/agenda': 'Agenda', '/consultas': 'Consultas', - '/laudos': 'Relatorios medicos', + '/laudos': 'Relatórios médicos', '/pacientes': 'Pacientes', - '/prontuario': 'Prontuario', - '/camunicacao': 'Comunicacao', - '/comunicacao': 'Comunicacao', - '/mensagens': 'Comunicacao', - '/relatorios': 'Relatorios', + '/prontuario': 'Prontuário', + '/comunicacao': 'Comunicação', + '/mensagens': 'Comunicação', + '/relatorios': 'Relatórios', '/profissionais': 'Profissionais', '/perfil': 'Perfil', - '/configuracoes': 'Configuracoes', - '/config': 'Configuracoes', + '/configuracoes': 'Configurações', + '/config': 'Configurações', + '/usuarios': 'Usuários', } -export function AppShell({ children, currentPath, navigate, routeTitle }) { +export function AppShell({ children, currentPath, navigate, role, routeTitle }) { const [menuOpen, setMenuOpen] = useState(false) const [quickSearch, setQuickSearch] = useState('') - const [viewerProfile, setViewerProfile] = useState({ name: 'Usuario', role: 'Usuario do Sistema' }) + const [viewerProfile, setViewerProfile] = useState({ name: 'Usuário', role: 'Usuário do Sistema' }) const pageTitle = useMemo(() => { if (currentPath.startsWith('/pacientes/') && routeTitle) { @@ -51,24 +55,46 @@ export function AppShell({ children, currentPath, navigate, routeTitle }) { return routeTitle || titles[currentPath] || 'MediConnect' }, [currentPath, routeTitle]) + // Filtra os itens de navegação com base no role do usuário + const navItems = useMemo(() => { + if (!role) return [] + + const allowedPaths = ROLE_NAV_ITEMS[role]?.map((item) => item.path) ?? [] + + return ALL_NAV_ITEMS.filter((item) => + allowedPaths.some( + (allowed) => item.href === allowed || item.activePaths?.includes(allowed), + ), + ) + }, [role]) + useEffect(() => { let active = true - profileRepository.getCurrentUserProfile() + profileRepository + .getCurrentUserProfile() .then((profile) => { if (!active || !profile) return setViewerProfile({ - name: profile.name || 'Usuario', - role: profile.role || 'Usuario do Sistema', + name: profile.name || 'Usuário', + role: ROLE_LABELS[role] || profile.role || 'Usuário do Sistema', }) }) - .catch(() => {}) + .catch(() => { + // Fallback: usa o label do role diretamente + if (active && role) { + setViewerProfile((prev) => ({ + ...prev, + role: ROLE_LABELS[role] || 'Usuário do Sistema', + })) + } + }) return () => { active = false } - }, []) + }, [role]) function goTo(path) { setMenuOpen(false) @@ -81,7 +107,7 @@ export function AppShell({ children, currentPath, navigate, routeTitle }) { className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:rounded-md focus:bg-[#262626] focus:px-4 focus:py-2 focus:text-sm focus:font-semibold focus:text-[#3b82f6]" href="#app-content" > - Pular para conteudo + Pular para conteúdo