import { useCallback, useEffect, useMemo, useState } from 'react' import { patientRepository } from '../repositories/patientRepository.js' import { professionalRepository } from '../repositories/professionalRepository.js' import { reportRepository } from '../repositories/reportRepository.js' const ITEMS_PER_PAGE = 25 const statusConfig = { draft: { label: 'Rascunho', pill: 'bg-amber-500/20 text-amber-400', stat: 'text-amber-400', }, } const orderOptions = [ { label: 'Criação mais recente', value: 'created_at.desc' }, { label: 'Criação mais antiga', value: 'created_at.asc' }, { label: 'Prazo mais proximo', value: 'due_at.asc' }, { label: 'Prazo mais distante', value: 'due_at.desc' }, ] const inputClass = 'h-10 w-full rounded-lg border border-[#404040] bg-[#1a1a1a] px-3 text-sm text-[#e5e5e5] outline-none transition placeholder:text-[#a3a3a3] focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6]' const textareaClass = 'min-h-24 w-full rounded-lg border border-[#404040] bg-[#1a1a1a] px-3 py-2 text-sm text-[#e5e5e5] outline-none transition placeholder:text-[#a3a3a3] focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6]' const labelClass = 'mb-1.5 block text-xs font-medium text-[#e5e5e5]' const cardClass = 'rounded-2xl border border-[#404040] bg-[#262626] shadow-sm' const emptyEditor = { id: null, patientId: '', status: 'draft', exam: '', requestedBy: '', cidCode: '', diagnosis: '', conclusion: '', contentHtml: '', contentJson: undefined, hideDate: false, hideSignature: false, dueAt: '', } export function ReportsPage() { const [reports, setReports] = useState([]) const [patients, setPatients] = useState([]) const [professionals, setProfessionals] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [error, setError] = useState('') const [filterPatientId, setFilterPatientId] = useState('') const [filterStatus, setFilterStatus] = useState('') const [filterCreatedBy, setFilterCreatedBy] = useState('') const [filterOrder, setFilterOrder] = useState('created_at.desc') const [editorOpen, setEditorOpen] = useState(false) const [viewerReport, setViewerReport] = useState(null) const [editor, setEditor] = useState(emptyEditor) const [page, setPage] = useState(1) const patientOptions = useMemo( () => patients.map((patient) => ({ id: String(patient.id || ''), name: patient.name || patient.full_name || patient.nome || 'Paciente', })), [patients], ) const professionalOptions = useMemo(() => { const seen = new Set() return professionals .map((professional) => { const createdByValue = String(professional.userId || professional.id || '') return { id: String(professional.id || ''), createdByValue, name: professional.name || 'Médico(a)', } }) .filter((professional) => { if (!professional.createdByValue || seen.has(professional.createdByValue)) { return false } seen.add(professional.createdByValue) return true }) }, [professionals]) const patientNameById = useMemo( () => Object.fromEntries(patientOptions.map((patient) => [patient.id, patient.name])), [patientOptions], ) const professionalNameByCreatedBy = useMemo( () => Object.fromEntries(professionalOptions.map((professional) => [professional.createdByValue, professional.name])), [professionalOptions], ) const enrichedReports = useMemo( () => reports.map((report) => ({ ...report, patientName: patientNameById[String(report.patientId || '')] || 'Paciente não encontrado', createdByName: professionalNameByCreatedBy[String(report.createdBy || '')] || report.createdBy || 'Sistema', })), [patientNameById, professionalNameByCreatedBy, reports], ) const stats = useMemo( () => [ { label: 'Total', value: enrichedReports.length, className: 'text-[#e5e5e5]' }, { label: 'Rascunhos', value: enrichedReports.filter((report) => report.status === 'draft').length, className: statusConfig.draft.stat, }, ], [enrichedReports], ) const totalPages = Math.max(1, Math.ceil(enrichedReports.length / ITEMS_PER_PAGE)) const currentPage = Math.min(page, totalPages) const startIndex = (currentPage - 1) * ITEMS_PER_PAGE const paginatedReports = enrichedReports.slice(startIndex, startIndex + ITEMS_PER_PAGE) const loadReports = useCallback(async () => { setLoading(true) setError('') try { const data = await reportRepository.getInitialReports({ patientId: filterPatientId || undefined, status: filterStatus || undefined, createdBy: filterCreatedBy || undefined, order: filterOrder, }) setReports(data) setPage(1) } catch (loadError) { console.error(loadError) setError(loadError.message || 'Erro ao carregar relatórios médicos.') setReports([]) setPage(1) } finally { setLoading(false) } }, [filterCreatedBy, filterOrder, filterPatientId, filterStatus]) useEffect(() => { let active = true Promise.all([ patientRepository.getAll(), professionalRepository.getAll(), ]) .then(([patientData, professionalData]) => { if (!active) return setPatients(patientData || []) setProfessionals(professionalData || []) }) .catch((loadError) => { if (!active) return console.error(loadError) setError(loadError.message || 'Erro ao carregar dados auxiliares.') }) return () => { active = false } }, []) useEffect(() => { loadReports() }, [loadReports]) function openNew() { setEditor({ ...emptyEditor, patientId: patientOptions[0]?.id || '', }) setEditorOpen(true) } function openEdit(report) { setEditor({ id: report.id, patientId: String(report.patientId || ''), status: report.status, exam: report.exam, requestedBy: report.requestedBy, cidCode: report.cidCode, diagnosis: report.diagnosis, conclusion: report.conclusion, contentHtml: report.contentHtml, contentJson: report.contentJson, hideDate: report.hideDate, hideSignature: report.hideSignature, dueAt: toDateTimeLocal(report.dueAt), }) setEditorOpen(true) } async function handleSave() { if (!editor.patientId) return setSaving(true) const payload = { patientId: editor.patientId, status: editor.status, exam: editor.exam, requestedBy: editor.requestedBy, cidCode: editor.cidCode, diagnosis: editor.diagnosis, conclusion: editor.conclusion, contentHtml: editor.contentHtml, contentJson: editor.contentJson, hideDate: editor.hideDate, hideSignature: editor.hideSignature, dueAt: editor.dueAt ? new Date(editor.dueAt).toISOString() : '', } try { if (editor.id) { await reportRepository.update(editor.id, payload) } else { await reportRepository.create(payload) } setEditorOpen(false) await loadReports() } catch (saveError) { alert(saveError.message || 'Erro ao salvar relatório médico.') } finally { setSaving(false) } } return (

Relatórios médicos

Consulta, criação e edição de relatórios médicos.

{stats.map((stat) => (

{stat.label}

{stat.value}

))}
{error ? (
{error}
) : null}
{loading ? ( ) : paginatedReports.length ? ( paginatedReports.map((report) => ( openEdit(report)} onView={() => setViewerReport(report)} report={report} /> )) ) : ( )}
Numero Exame Paciente Solicitante Criado em Status Ações
Carregando relatórios médicos...
Nenhum relatório encontrado com os filtros atuais.

Mostrando {enrichedReports.length ? startIndex + 1 : 0}-{Math.min(startIndex + ITEMS_PER_PAGE, enrichedReports.length)} de{' '} {enrichedReports.length} relatórios

setPage(currentPage - 1)}> {Array.from({ length: totalPages }, (_, index) => index + 1).map((pageNumber) => ( ))} setPage(currentPage + 1)}>
{editorOpen ? ( setEditorOpen(false)} onSave={handleSave} patientOptions={patientOptions} professionalOptions={professionalOptions} saving={saving} /> ) : null} {viewerReport ? ( setViewerReport(null)} report={viewerReport} /> ) : null}
) } function ReportRow({ onEdit, onView, report }) { return ( {report.orderNumber || '-'}
{report.exam || 'Sem exame'}
{report.patientName} {report.requestedBy || '-'} {formatDate(report.createdAt)} {statusConfig[report.status].label}
) } function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions, professionalOptions, saving }) { const isValid = Boolean(editor.patientId) function updateField(field, value) { onChange((current) => ({ ...current, [field]: value })) } return (
event.stopPropagation()} >

{editor.id ? 'Editar relatório médico' : 'Novo relatório médico'}

updateField('exam', event.target.value)} placeholder="Nome do exame" value={editor.exam} />
updateField('requestedBy', event.target.value)} placeholder="Nome do solicitante" value={editor.requestedBy} /> {professionalOptions.map((professional) => (
updateField('cidCode', event.target.value)} placeholder="Ex: Z01.7" value={editor.cidCode} /> updateField('dueAt', event.target.value)} type="datetime-local" value={editor.dueAt} />