Files
riseup_squad_03/src/App.jsx
Squad03_Leticia_Lacerda bd337349e1 modified: .env.example
new file:   .env.local
modified:   .gitignore
new file:   docs/mock-audit.md
modified:   eslint.config.js
modified:   package-lock.json
modified:   package.json
deleted:    src/App.css
modified:   src/App.jsx
deleted:    src/assets/react.svg
deleted:    src/assets/vite.svg
new file:   src/components/RichTextEditor.jsx
modified:   src/components/calendar/AgendaMonthlyView.jsx
modified:   src/components/calendar/AgendaWeeklyView.jsx
modified:   src/components/ui.jsx
modified:   src/config/api.js
modified:   src/data/mockData.js
new file:   src/data/reportTemplates.js
modified:   src/hooks/useAgenda.js
modified:   src/mappers/appointmentMapper.js
modified:   src/pages/AgendaPage.jsx
modified:   src/pages/MedicalRecordsPage.jsx
modified:   src/pages/MessagesPage.jsx
modified:   src/pages/PatientsPage.jsx
modified:   src/pages/ProfilePage.jsx
modified:   src/pages/ReportsPage.jsx
modified:   src/pages/UsersPage.jsx
modified:   src/pages/VisitsPage.jsx
modified:   src/repositories/patientRepository.js
modified:   src/repositories/profileRepository.js
modified:   src/repositories/userRepository.js
deleted:    test.mjs
deleted:    test2.mjs
deleted:    test3.mjs
deleted:    test4.mjs
deleted:    test5.mjs
new file:   tests/mappers.test.mjs
new file:   tests/patientRepository.test.mjs
new file:   tests/permissions.test.mjs
new file:   tests/repositoryUtils.test.mjs
2026-05-12 04:48:25 -03:00

399 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Component, lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react'
import { AppShell } from './components/AppShell.jsx'
import { canAccess } from './config/permissions.js'
import { useAuth } from './hooks/useAuth.js'
import { ForgotPasswordPage, LoginPage, RegisterPage } from './pages/AuthPages.jsx'
import { NotFoundPage } from './pages/NotFoundPage.jsx'
import { patientRepository } from './repositories/patientRepository.js'
const AgendaPage = lazyPage(() => import('./pages/AgendaPage.jsx'), 'AgendaPage')
const AnalyticsPage = lazyPage(() => import('./pages/AnalyticsPage.jsx'), 'AnalyticsPage')
const HomePage = lazyPage(() => import('./pages/HomePage.jsx'), 'HomePage')
const MedicalRecordsPage = lazyPage(() => import('./pages/MedicalRecordsPage.jsx'), 'MedicalRecordsPage')
const MessagesPage = lazyPage(() => import('./pages/MessagesPage.jsx'), 'MessagesPage')
const PatientDetailPage = lazyPage(() => import('./pages/PatientsPage.jsx'), 'PatientDetailPage')
const PatientsPage = lazyPage(() => import('./pages/PatientsPage.jsx'), 'PatientsPage')
const ProfilePage = lazyPage(() => import('./pages/ProfilePage.jsx'), 'ProfilePage')
const ReportsPage = lazyPage(() => import('./pages/ReportsPage.jsx'), 'ReportsPage')
const SettingsPage = lazyPage(() => import('./pages/SettingsPage.jsx'), 'SettingsPage')
const UsersPage = lazyPage(() => import('./pages/UsersPage.jsx'), 'UsersPage')
const VisitsPage = lazyPage(() => import('./pages/VisitsPage.jsx'), 'VisitsPage')
const PANEL_PATHS = ['/inicio', '/home', '/dashboard']
const ROLE_HOME_PATHS = {
medico: '/agenda',
secretaria: '/agenda',
}
function lazyPage(loader, exportName) {
return lazy(() => loader().then((module) => ({ default: module[exportName] })))
}
function App() {
const [location, setLocation] = useState(() => readLocation())
const { isAuthenticated, role, loading: authLoading } = useAuth()
const navigate = useCallback((to, options = {}) => {
if (options.replace) {
window.history.replaceState({}, '', to)
} else {
window.history.pushState({}, '', to)
}
setLocation(readLocation())
const hash = to.split('#')[1]
window.requestAnimationFrame(() => {
if (hash) {
document.getElementById(hash)?.scrollIntoView({ block: 'start' })
} else {
window.scrollTo({ left: 0, top: 0 })
}
})
}, [])
useEffect(() => {
function handlePopState() {
setLocation(readLocation())
}
window.addEventListener('popstate', handlePopState)
return () => window.removeEventListener('popstate', handlePopState)
}, [])
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 (
<div className="flex min-h-screen items-center justify-center bg-[#0a0a0a]">
<p className="text-sm text-[#a3a3a3]">Carregando...</p>
</div>
)
}
// Rotas públicas (sem shell)
if (!route.withShell) {
return <RouteSuspense resetKey={location.pathname}>{route.element}</RouteSuspense>
}
// Usuário não autenticado
if (!isAuthenticated) {
return <LoginPage navigate={navigate} />
}
// Usuário autenticado mas sem permissão para a rota
if (!role || !canAccess(role, location.pathname)) {
const roleHomePath = ROLE_HOME_PATHS[role]
if (roleHomePath && PANEL_PATHS.includes(location.pathname)) {
navigate(roleHomePath, { replace: true })
return null
}
return (
<AppShell currentPath={location.pathname} navigate={navigate} role={role} routeTitle="Sem acesso">
<UnauthorizedPage navigate={navigate} />
</AppShell>
)
}
return (
<AppShell currentPath={location.pathname} navigate={navigate} role={role} routeTitle={route.title}>
<RouteSuspense resetKey={location.pathname}>{route.element}</RouteSuspense>
</AppShell>
)
}
class RouteErrorBoundary extends Component {
state = { error: null }
static getDerivedStateFromError(error) {
return { error }
}
componentDidUpdate(previousProps) {
if (previousProps.resetKey !== this.props.resetKey && this.state.error) {
this.setState({ error: null })
}
}
render() {
if (this.state.error) {
return <RouteErrorFallback />
}
return this.props.children
}
}
function RouteSuspense({ children, resetKey }) {
return (
<RouteErrorBoundary resetKey={resetKey}>
<Suspense fallback={<RouteFallback />}>
{children}
</Suspense>
</RouteErrorBoundary>
)
}
function RouteFallback() {
return (
<div className="flex min-h-[40vh] items-center justify-center px-4">
<div className="w-full max-w-xl rounded-2xl border border-[#404040] bg-[#262626] p-5 shadow-sm">
<div className="h-4 w-36 animate-pulse rounded bg-[#404040]" />
<div className="mt-4 grid gap-3">
<div className="h-20 animate-pulse rounded-xl bg-[#1a1a1a]" />
<div className="h-20 animate-pulse rounded-xl bg-[#1a1a1a]" />
</div>
<p className="mt-4 text-sm text-[#a3a3a3]">Carregando modulo...</p>
</div>
</div>
)
}
function RouteErrorFallback() {
return (
<div className="flex min-h-[40vh] items-center justify-center px-4">
<div className="max-w-xl rounded-2xl border border-red-500/40 bg-[#262626] p-6 text-center shadow-sm">
<h2 className="text-lg font-bold text-[#e5e5e5]">£o foi possível carregar esta tela</h2>
<p className="mt-2 text-sm leading-6 text-[#a3a3a3]">
Ocorreu um erro ao abrir o modulo. Recarregue a pagina e tente novamente.
</p>
<button
className="mt-5 rounded-lg bg-[#3b82f6] px-4 py-2 text-sm font-semibold text-white transition hover:bg-[#2563eb]"
onClick={() => window.location.reload()}
type="button"
>
Recarregar
</button>
</div>
</div>
)
}
function resolveRoute(pathname, navigate, role) {
if (pathname === '/' || pathname === '/login') {
return {
element: <LoginPage navigate={navigate} />,
title: 'Login',
withShell: false,
}
}
if (pathname === '/cadastro') {
return {
element: <RegisterPage navigate={navigate} />,
title: 'Cadastro',
withShell: false,
}
}
if (pathname === '/recuperar-senha') {
return {
element: <ForgotPasswordPage navigate={navigate} />,
title: 'Recuperar senha',
withShell: false,
}
}
if (pathname === '/inicio' || pathname === '/home' || pathname === '/dashboard') {
return {
element: <HomePage navigate={navigate} />,
title: 'Painel',
withShell: true,
}
}
if (pathname === '/agenda') {
return {
element: <AgendaPage navigate={navigate} role={role} />,
title: 'Agenda',
withShell: true,
}
}
if (pathname === '/pacientes') {
return {
element: <PatientsPage navigate={navigate} role={role} />,
title: 'Pacientes',
withShell: true,
}
}
if (pathname === '/prontuario') {
return {
element: <MedicalRecordsPage navigate={navigate} />,
title: 'Prontuário',
withShell: true,
}
}
if (pathname === '/prontuario/novo') {
return {
element: <MedicalRecordsPage mode="new" navigate={navigate} />,
title: 'Novo prontuário',
withShell: true,
}
}
if (pathname.startsWith('/prontuario/')) {
const [, , recordId, action] = pathname.split('/')
return {
element: <MedicalRecordsPage mode={action === 'editar' ? 'edit' : 'detail'} navigate={navigate} recordId={recordId} />,
title: action === 'editar' ? 'Editar prontuário' : 'Prontuário',
withShell: true,
}
}
if (pathname.startsWith('/pacientes/')) {
const patientId = pathname.split('/')[2]
return {
element: <PatientDetailRoute navigate={navigate} patientId={patientId} role={role} />,
title: 'Paciente',
withShell: true,
}
}
if (pathname === '/consultas') {
return {
element: <VisitsPage navigate={navigate} />,
title: 'Consultas',
withShell: true,
}
}
if (pathname === '/laudos') {
return {
element: <ReportsPage navigate={navigate} role={role} />,
title: 'Relatórios',
withShell: true,
}
}
if (pathname === '/relatorios') {
return {
element: <AnalyticsPage />,
title: 'Analytics',
withShell: true,
}
}
if (pathname === '/camunicacao') {
navigate('/comunicacao', { replace: true })
return {
element: <MessagesPage navigate={navigate} role={role} />,
title: 'Comunicação',
withShell: true,
}
}
if (pathname === '/comunicacao' || pathname === '/mensagens') {
return {
element: <MessagesPage navigate={navigate} role={role} />,
title: 'Comunicação',
withShell: true,
}
}
if (pathname === '/usuarios') {
return {
element: <UsersPage role={role} />,
title: 'Usuários',
withShell: true,
}
}
if (pathname === '/perfil') {
return {
element: <ProfilePage navigate={navigate} />,
title: 'Perfil',
withShell: true,
}
}
if (pathname === '/configuracoes' || pathname === '/config') {
return {
element: <SettingsPage navigate={navigate} />,
title: 'Configurações',
withShell: true,
}
}
return {
element: <NotFoundPage navigate={navigate} />,
title: 'Página não encontrada',
withShell: true,
}
}
function PatientDetailRoute({ navigate, patientId, role }) {
const [patient, setPatient] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
let active = true
patientRepository
.getById(patientId)
.then((data) => {
if (active) setPatient(data)
})
.finally(() => {
if (active) setLoading(false)
})
return () => {
active = false
}
}, [patientId])
if (loading) {
return <div className="pt-10 text-sm text-[#a3a3a3]">Carregando paciente...</div>
}
return patient ? (
<PatientDetailPage navigate={navigate} patient={patient} role={role} />
) : (
<NotFoundPage navigate={navigate} />
)
}
function UnauthorizedPage({ navigate }) {
return (
<div className="flex flex-col items-center justify-center py-20 text-center">
<p className="text-5xl">🔒</p>
<h1 className="mt-4 text-2xl font-bold text-[#e5e5e5]">Acesso não permitido</h1>
<p className="mt-2 text-sm text-[#a3a3a3]">
Você não tem permissão para acessar esta página.
</p>
<button
className="mt-6 rounded-lg bg-[#3b82f6] px-5 py-2.5 text-sm font-medium text-white transition hover:bg-[#2563eb]"
onClick={() => navigate('/inicio')}
type="button"
>
Voltar ao painel
</button>
</div>
)
}
function readLocation() {
return {
pathname: normalizePath(window.location.pathname),
search: window.location.search,
}
}
function normalizePath(pathname) {
if (!pathname || pathname === '/') {
return '/'
}
return pathname.replace(/\/+$/, '')
}
export default App