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 const statusConfig = { draft: { label: 'Rascunho', 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 = [ { 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, orderNumber: '', patientId: '', status: 'draft', exam: '', requestedBy: '', cidCode: '', diagnosis: '', conclusion: '', contentHtml: '', contentJson: undefined, hideDate: false, hideSignature: false, dueAt: '', } 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('') 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, }, { label: 'Finalizados', value: enrichedReports.filter((report) => report.status === 'finalized').length, className: statusConfig.finalized.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 () => { 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: !isDoctorRole ? filterCreatedBy || undefined : undefined, createdByValues: isDoctorRole && !doctorPatientIds.length ? createdByValues : undefined, order: filterOrder, }) setReports(data) setPage(1) } catch (loadError) { console.error(loadError) setError(loadError.message || 'Erro ao carregar relatórios.') setReports([]) setPage(1) } finally { setLoading(false) } }, [currentProfessional, filterCreatedBy, filterOrder, filterPatientId, filterStatus, isDoctorRole, patientOptions, scopeLoading, viewerProfile]) useEffect(() => { let active = true 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) { 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() }, [loadReports]) function openNew() { setEditor({ ...emptyEditor, patientId: patientOptions[0]?.id || '', }) setEditorOpen(true) } function openEdit(report) { setEditor({ id: report.id, orderNumber: report.orderNumber, 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 = { orderNumber: editor.id ? editor.orderNumber : `REL-${Date.now()}`, 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() : '', createdBy: editor.id ? undefined : viewerProfile?.id || currentProfessional?.userId || currentProfessional?.id || undefined, updatedBy: viewerProfile?.id || currentProfessional?.userId || currentProfessional?.id || undefined, } 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.') } finally { setSaving(false) } } return (

Relatórios

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

{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...
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 }) { const currentStatus = statusConfig[report.status] || statusConfig.draft return ( {report.orderNumber || '-'}
{report.exam || 'Sem exame'}
{report.patientName} {report.requestedBy || '-'} {formatDate(report.createdAt)} {currentStatus.label}
) } function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions, professionalOptions, saving }) { const [requesterSearch, setRequesterSearch] = useState(editor.requestedBy || '') const isValid = Boolean(editor.patientId) const filteredRequesterOptions = professionalOptions .filter((professional) => normalizeSearch(professional.name).includes(normalizeSearch(requesterSearch))) .slice(0, 6) function updateField(field, value) { onChange((current) => ({ ...current, [field]: value })) } return (
event.stopPropagation()} >

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

updateField('exam', event.target.value)} placeholder="Nome do exame" value={editor.exam} />
{ setRequesterSearch(event.target.value) updateField('requestedBy', event.target.value) }} placeholder="Pesquisar médico" type="search" value={requesterSearch} />
{filteredRequesterOptions.length ? ( filteredRequesterOptions.map((professional) => ( )) ) : (

Nenhum médico encontrado.

)}
updateField('cidCode', event.target.value)} placeholder="Ex: Z01.7" value={editor.cidCode} /> updateField('dueAt', event.target.value)} type="datetime-local" value={editor.dueAt} />