import { useCallback, useEffect, useMemo, useRef, 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 reportTemplates = [ { id: 'consulta-medica', category: 'Relatórios', title: 'Relatório de Consulta Médica', description: 'Resumo clínico com queixa, exame físico, hipótese diagnóstica e conduta.', popular: true, tags: ['consulta', 'clínico', 'conduta'], exam: 'Consulta médica', cidCode: 'Z00.0', diagnosis: 'Paciente avaliado(a) em consulta médica, com hipótese diagnóstica em investigação conforme quadro clínico.', conclusion: 'Paciente orientado(a) quanto à conduta proposta, sinais de alerta e necessidade de seguimento.', contentHtml: '

Relatório de Consulta Médica

Queixa principal:

História clínica:

Exame físico:

Hipóteses diagnósticas:

Conduta:

', }, { id: 'evolucao-clinica', category: 'Relatórios', title: 'Evolução Clínica', description: 'Registro de evolução diária para acompanhamento de internação.', tags: ['internação', 'evolução', 'diário'], exam: 'Evolução clínica', cidCode: 'Z51.9', diagnosis: 'Paciente em acompanhamento clínico durante internação, com evolução registrada em prontuário.', conclusion: 'Manter acompanhamento multiprofissional e reavaliar conduta conforme evolução.', contentHtml: '

Evolução Clínica

Data e hora:

Estado geral:

Sinais vitais:

Evolução:

Conduta do dia:

Profissional:

', }, { id: 'hemograma', category: 'Laudos', title: 'Laudo de Hemograma', description: 'Interpretação clínica de hemograma com correlação diagnóstica.', tags: ['laboratorial', 'sangue', 'hemograma'], exam: 'Hemograma completo', cidCode: 'Z01.7', diagnosis: 'Exame laboratorial avaliado em conjunto com quadro clínico e exames complementares.', conclusion: 'Resultado analisado e correlacionado com a hipótese diagnóstica descrita.', contentHtml: '

Laudo de Hemograma

Material: Sangue periférico.

Achados principais:

Interpretação:

Conclusão:

', }, { id: 'imagem', category: 'Laudos', title: 'Laudo de Imagem', description: 'Modelo para exames de imagem com descrição técnica e impressão diagnóstica.', popular: true, tags: ['imagem', 'radiologia', 'exame'], exam: 'Exame de imagem', cidCode: 'Z01.6', diagnosis: 'Achados de imagem descritos conforme exame realizado e indicação clínica.', conclusion: 'Impressão diagnóstica registrada conforme achados do exame.', contentHtml: '

Laudo de Imagem

Técnica:

Achados:

Impressão diagnóstica:

Recomendação:

', }, { id: 'pre-operatorio', category: 'Relatórios', title: 'Avaliação Pré-operatória', description: 'Avaliação clínica para estratificação de risco e liberação cirúrgica.', tags: ['pré-op', 'cirurgia', 'risco'], exam: 'Avaliação pré-operatória', cidCode: 'Z01.8', diagnosis: 'Paciente em avaliação pré-operatória, com risco definido conforme dados clínicos disponíveis.', conclusion: 'Conduta pré-operatória orientada conforme avaliação clínica e exames apresentados.', contentHtml: '

Avaliação Pré-operatória

Procedimento proposto:

Comorbidades:

Medicamentos em uso:

Estratificação de risco:

Orientações:

', }, { id: 'encaminhamento', category: 'Encaminhamentos', title: 'Encaminhamento Especializado', description: 'Encaminhamento com justificativa clínica e resumo do caso.', tags: ['encaminhamento', 'especialista', 'conduta'], exam: 'Encaminhamento médico', cidCode: 'Z75.8', diagnosis: 'Paciente encaminhado(a) para avaliação especializada por necessidade clínica descrita.', conclusion: 'Solicitada avaliação especializada e continuidade do cuidado compartilhado.', contentHtml: '

Encaminhamento Especializado

Especialidade solicitada:

Resumo clínico:

Motivo do encaminhamento:

Exames anexos:

', }, ] const templateCategories = ['Todos', ...Array.from(new Set(reportTemplates.map((template) => template.category)))] const emptyEditor = { id: null, orderNumber: '', patientId: '', status: 'draft', exam: '', requestedBy: '', cidCode: '', diagnosis: '', conclusion: '', contentHtml: '', contentJson: undefined, 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, dueAt: toDateTimeLocal(report.dueAt), }) setEditorOpen(true) } async function handleSave() { if (!isReportEditorValid(editor)) { alert('Preencha todos os campos obrigatórios antes de salvar o relatório.') 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, 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 ReportEditorModalV2({ editor, onChange, onClose, onSave, patientOptions, professionalOptions, saving }) { const editorRef = useRef(null) const [requesterSearch, setRequesterSearch] = useState(editor.requestedBy || '') const [patientSearch, setPatientSearch] = useState('') const [templateSearch, setTemplateSearch] = useState('') const [templateCategory, setTemplateCategory] = useState('Todos') const [selectedTemplateId, setSelectedTemplateId] = useState('') const [previewOpen, setPreviewOpen] = useState(false) const isValid = isReportEditorValid(editor) const selectedPatient = patientOptions.find((patient) => patient.id === String(editor.patientId)) const filteredPatients = patientOptions .filter((patient) => normalizeSearch(patient.name).includes(normalizeSearch(patientSearch))) .slice(0, 5) const filteredRequesterOptions = professionalOptions .filter((professional) => normalizeSearch(professional.name).includes(normalizeSearch(requesterSearch))) .slice(0, 5) const filteredTemplates = reportTemplates.filter((template) => { const matchesCategory = templateCategory === 'Todos' || template.category === templateCategory const query = normalizeSearch(templateSearch) const matchesSearch = !query || normalizeSearch([template.title, template.description, template.tags.join(' ')].join(' ')).includes(query) return matchesCategory && matchesSearch }) const selectedTemplate = reportTemplates.find((template) => template.id === selectedTemplateId) function updateField(field, value) { onChange((current) => ({ ...current, [field]: value })) } function applyTemplate(template) { setSelectedTemplateId(template.id) setPreviewOpen(true) onChange((current) => ({ ...current, exam: template.exam, cidCode: template.cidCode, diagnosis: template.diagnosis, conclusion: template.conclusion, contentHtml: template.contentHtml, contentJson: { templateId: template.id, templateTitle: template.title, appliedAt: new Date().toISOString(), }, })) } function runCommand(command, value = null) { editorRef.current?.focus() document.execCommand(command, false, value) updateField('contentHtml', editorRef.current?.innerHTML || '') } function insertToken(token) { const values = { patient: selectedPatient?.name || '[Paciente]', date: new Date().toLocaleDateString('pt-BR'), doctor: editor.requestedBy || '[Médico]', } runCommand('insertText', values[token] || '') } return (
event.stopPropagation()} >

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

Selecione um template e finalize o conteúdo no editor rico.

setTemplateSearch(event.target.value)} placeholder="Buscar templates..." value={templateSearch} />
{filteredTemplates.map((template) => ( ))}
updateField('exam', event.target.value)} placeholder="Ex: Relatório de consulta médica" value={editor.exam} />
setPatientSearch(event.target.value)} placeholder="Digite o nome do paciente..." value={patientSearch || selectedPatient?.name || ''} /> { updateField('patientId', patient.id) setPatientSearch(patient.name) }} selectedValue={editor.patientId} valueKey="id" />
{ setRequesterSearch(event.target.value) updateField('requestedBy', event.target.value) }} placeholder="Pesquisar médico" value={requesterSearch} /> { setRequesterSearch(professional.name) updateField('requestedBy', professional.name) }} selectedValue={editor.requestedBy} valueKey="name" />
updateField('cidCode', event.target.value)} placeholder="Ex: Z01.7" value={editor.cidCode} /> updateField('dueAt', event.target.value)} type="datetime-local" value={editor.dueAt} />