modified: index.html

modified:   src/App.jsx
modified:   src/components/AppShell.jsx
modified:   src/components/featureStateStyles.js
modified:   src/config/permissions.js
modified:   src/hooks/useAgenda.js
modified:   src/mappers/reportMapper.js
modified:   src/pages/AgendaPage.jsx
modified:   src/pages/AnalyticsPage.jsx
modified:   src/pages/AuthPages.jsx
modified:   src/pages/HomePage.jsx
modified:   src/pages/MedicalRecordsPage.jsx
modified:   src/pages/MessagesPage.jsx
modified:   src/pages/PatientsPage.jsx
modified:   src/pages/ReportsPage.jsx
modified:   src/pages/SettingsPage.jsx
deleted:    src/pages/TeamPage.jsx
modified:   src/pages/UsersPage.jsx
modified:   src/repositories/availabilityRepository.js
modified:   src/repositories/patientRepository.js
modified:   src/repositories/professionalRepository.js
modified:   src/repositories/reportRepository.js
modified:   src/repositories/settingsRepository.js
This commit is contained in:
2026-05-07 01:11:10 -03:00
parent 9335e974eb
commit efb942d5aa
23 changed files with 1461 additions and 591 deletions

View File

@@ -1,7 +1,9 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { normalizeRole } from '../config/permissions.js'
import { patientRepository } from '../repositories/patientRepository.js'
import { professionalRepository } from '../repositories/professionalRepository.js'
import { profileRepository } from '../repositories/profileRepository.js'
import { reportRepository } from '../repositories/reportRepository.js'
const ITEMS_PER_PAGE = 25
@@ -12,6 +14,11 @@ const statusConfig = {
pill: 'bg-amber-500/20 text-amber-400',
stat: 'text-amber-400',
},
finalized: {
label: 'Finalizado',
pill: 'bg-emerald-500/20 text-emerald-400',
stat: 'text-emerald-400',
},
}
const orderOptions = [
@@ -44,13 +51,17 @@ const emptyEditor = {
dueAt: '',
}
export function ReportsPage() {
export function ReportsPage({ role }) {
const [reports, setReports] = useState([])
const [patients, setPatients] = useState([])
const [professionals, setProfessionals] = useState([])
const [viewerProfile, setViewerProfile] = useState(null)
const [currentProfessional, setCurrentProfessional] = useState(null)
const [loading, setLoading] = useState(true)
const [scopeLoading, setScopeLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const isDoctorRole = normalizeRole(role) === 'medico'
const [filterPatientId, setFilterPatientId] = useState('')
const [filterStatus, setFilterStatus] = useState('')
@@ -121,6 +132,11 @@ export function ReportsPage() {
value: enrichedReports.filter((report) => report.status === 'draft').length,
className: statusConfig.draft.stat,
},
{
label: 'Finalizados',
value: enrichedReports.filter((report) => report.status === 'finalized').length,
className: statusConfig.finalized.stat,
},
],
[enrichedReports],
)
@@ -131,14 +147,30 @@ export function ReportsPage() {
const paginatedReports = enrichedReports.slice(startIndex, startIndex + ITEMS_PER_PAGE)
const loadReports = useCallback(async () => {
if (scopeLoading) return
setLoading(true)
setError('')
try {
const doctorPatientIds = isDoctorRole
? patientOptions.map((patient) => patient.id).filter(Boolean)
: []
const createdByValues = isDoctorRole
? uniqueValues([
viewerProfile?.id,
viewerProfile?.doctorId,
currentProfessional?.userId,
currentProfessional?.id,
])
: []
const data = await reportRepository.getInitialReports({
patientId: filterPatientId || undefined,
patientIds: !filterPatientId && doctorPatientIds.length ? doctorPatientIds : undefined,
status: filterStatus || undefined,
createdBy: filterCreatedBy || undefined,
createdBy: !isDoctorRole ? filterCreatedBy || undefined : undefined,
createdByValues: isDoctorRole && !doctorPatientIds.length ? createdByValues : undefined,
order: filterOrder,
})
@@ -146,36 +178,54 @@ export function ReportsPage() {
setPage(1)
} catch (loadError) {
console.error(loadError)
setError(loadError.message || 'Erro ao carregar relatórios médicos.')
setError(loadError.message || 'Erro ao carregar relatórios.')
setReports([])
setPage(1)
} finally {
setLoading(false)
}
}, [filterCreatedBy, filterOrder, filterPatientId, filterStatus])
}, [currentProfessional, filterCreatedBy, filterOrder, filterPatientId, filterStatus, isDoctorRole, patientOptions, scopeLoading, viewerProfile])
useEffect(() => {
let active = true
Promise.all([
patientRepository.getAll(),
professionalRepository.getAll(),
])
.then(([patientData, professionalData]) => {
async function loadAuxiliaryData() {
setScopeLoading(true)
try {
const [professionalData, currentProfile] = await Promise.all([
professionalRepository.getAll(),
profileRepository.getCurrentUserProfile(),
])
if (!active) return
const resolvedProfessional = professionalRepository.resolveCurrentProfessional(currentProfile, professionalData || [])
const patientData = isDoctorRole && resolvedProfessional?.id
? await patientRepository.getDirectoryRows({ doctorId: resolvedProfessional.id })
: await patientRepository.getAll()
if (!active) return
setViewerProfile(currentProfile)
setCurrentProfessional(resolvedProfessional)
setPatients(patientData || [])
setProfessionals(professionalData || [])
})
.catch((loadError) => {
} catch (loadError) {
if (!active) return
console.error(loadError)
setError(loadError.message || 'Erro ao carregar dados auxiliares.')
})
} finally {
if (active) setScopeLoading(false)
}
}
loadAuxiliaryData()
return () => {
active = false
}
}, [])
}, [isDoctorRole])
useEffect(() => {
loadReports()
@@ -238,7 +288,7 @@ export function ReportsPage() {
setEditorOpen(false)
await loadReports()
} catch (saveError) {
alert(saveError.message || 'Erro ao salvar relatório médico.')
alert(saveError.message || 'Erro ao salvar relatório.')
} finally {
setSaving(false)
}
@@ -248,8 +298,8 @@ export function ReportsPage() {
<div className="mx-auto max-w-7xl space-y-6 text-[#e5e5e5]">
<div className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
<div>
<h1 className="text-2xl font-bold tracking-tight text-[#e5e5e5]">Relatórios médicos</h1>
<p className="mt-1 text-sm text-[#a3a3a3]">Consulta, criação e edição de relatórios médicos.</p>
<h1 className="text-2xl font-bold tracking-tight text-[#e5e5e5]">Relatórios</h1>
<p className="mt-1 text-sm text-[#a3a3a3]">Consulta, criação e edição de relatórios.</p>
</div>
<button
className="inline-flex h-10 items-center justify-center gap-2 rounded-lg bg-[#3b82f6] px-4 text-sm font-medium text-white transition hover:bg-[#2563eb]"
@@ -303,6 +353,7 @@ export function ReportsPage() {
>
<option value="">Todos os status</option>
<option value="draft">Rascunho</option>
<option value="finalized">Finalizado</option>
</select>
</FilterField>
@@ -365,7 +416,7 @@ export function ReportsPage() {
{loading ? (
<tr>
<td className="px-4 py-8 text-center text-sm text-[#a3a3a3]" colSpan={7}>
Carregando relatórios médicos...
Carregando relatórios...
</td>
</tr>
) : paginatedReports.length ? (
@@ -438,6 +489,8 @@ export function ReportsPage() {
}
function ReportRow({ onEdit, onView, report }) {
const currentStatus = statusConfig[report.status] || statusConfig.draft
return (
<tr className="transition hover:bg-[#303030]">
<td className="px-4 py-3 align-top text-[#a3a3a3]">{report.orderNumber || '-'}</td>
@@ -451,8 +504,8 @@ function ReportRow({ onEdit, onView, report }) {
<td className="px-4 py-3 align-top whitespace-normal break-words text-[#a3a3a3]">{report.requestedBy || '-'}</td>
<td className="px-4 py-3 align-top text-[#a3a3a3]">{formatDate(report.createdAt)}</td>
<td className="px-4 py-3 align-top">
<span className={`rounded px-2 py-1 text-[10px] font-bold ${statusConfig[report.status].pill}`}>
{statusConfig[report.status].label}
<span className={`rounded px-2 py-1 text-[10px] font-bold ${currentStatus.pill}`}>
{currentStatus.label}
</span>
</td>
<td className="sticky right-0 bg-[#262626] px-4 py-3 text-right shadow-[-10px_0_12px_-12px_rgba(0,0,0,0.75)]">
@@ -480,7 +533,7 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,
>
<div className="flex items-center justify-between border-b border-[#404040] px-6 py-4">
<h2 className="text-lg font-bold text-[#e5e5e5]">
{editor.id ? 'Editar relatório médico' : 'Novo relatório médico'}
{editor.id ? 'Editar relatório' : 'Novo relatório'}
</h2>
<button className="rounded-lg p-1.5 transition hover:bg-[#2a2a2a]" onClick={onClose} type="button">
<ReportIcon className="size-4 text-[#a3a3a3]" name="x" />
@@ -504,6 +557,7 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,
<DarkField label="Status">
<select className={inputClass} onChange={(event) => updateField('status', event.target.value)} value={editor.status}>
<option value="draft">Rascunho</option>
<option value="finalized">Finalizado</option>
</select>
</DarkField>
</div>
@@ -574,11 +628,11 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,
/>
</DarkField>
<DarkField label="Conteúdo HTML">
<DarkField label="Complemento">
<textarea
className={`${textareaClass} min-h-72`}
onChange={(event) => updateField('contentHtml', event.target.value)}
placeholder="<p>Conteúdo do relatório</p>"
placeholder="Complemento em texto simples"
value={editor.contentHtml}
/>
</DarkField>
@@ -631,6 +685,8 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,
}
function ReportViewModal({ onClose, report }) {
const currentStatus = statusConfig[report.status] || statusConfig.draft
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" onClick={onClose}>
<div
@@ -639,7 +695,7 @@ function ReportViewModal({ onClose, report }) {
>
<div className="flex items-center justify-between border-b border-[#404040] px-6 py-4">
<div>
<h2 className="text-lg font-bold text-[#e5e5e5]">Relatório médico</h2>
<h2 className="text-lg font-bold text-[#e5e5e5]">Relatório</h2>
<p className="mt-1 text-xs text-[#a3a3a3]">{report.orderNumber || 'Sem número'} </p>
</div>
<button className="rounded-lg p-1.5 transition hover:bg-[#2a2a2a]" onClick={onClose} type="button">
@@ -653,7 +709,7 @@ function ReportViewModal({ onClose, report }) {
<DetailCard label="Solicitante" value={report.requestedBy || '-'} />
<DetailCard label="Criado em" value={formatDate(report.createdAt)} />
<DetailCard label="Criado por" value={report.createdByName} />
<DetailCard label="Status" value={statusConfig[report.status].label} />
<DetailCard label="Status" value={currentStatus.label} />
<DetailCard label="Prazo" value={formatDateTime(report.dueAt)} />
</div>
@@ -677,14 +733,11 @@ function ReportViewModal({ onClose, report }) {
</div>
<div className="mt-6 rounded-xl border border-[#404040] bg-[#1a1a1a] p-5">
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-[#a3a3a3]">Conteúdo HTML</p>
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-[#a3a3a3]">Complemento</p>
{report.contentHtml ? (
<div
className="prose prose-invert max-w-none text-sm text-[#e5e5e5]"
dangerouslySetInnerHTML={{ __html: report.contentHtml }}
/>
<p className="whitespace-pre-wrap text-sm leading-6 text-[#e5e5e5]">{report.contentHtml}</p>
) : (
<p className="text-sm text-[#a3a3a3]">Nenhum conteúdo HTML informado.</p>
<p className="text-sm text-[#a3a3a3]">Nenhum complemento informado.</p>
)}
</div>
</div>
@@ -789,6 +842,10 @@ function toDateTimeLocal(value) {
return `${year}-${month}-${day}T${hours}:${minutes}`
}
function uniqueValues(values) {
return [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))]
}
function ReportIcon({ className = 'size-4', name }) {
const common = {
className,