import { useEffect, useMemo, useState } from 'react'
import { FeatureCallout } from '../components/FeatureState.jsx'
import { medicalRecordRepository } from '../repositories/medicalRecordRepository.js'
import { patientRepository } from '../repositories/patientRepository.js'
import { reportRepository } from '../repositories/reportRepository.js'
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-28 w-full rounded-lg border border-[#404040] bg-[#1a1a1a] px-3 py-2 text-sm leading-6 text-[#e5e5e5] outline-none transition placeholder:text-[#a3a3a3] focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6]'
const labelClass = 'mb-1 block text-xs font-medium text-[#e5e5e5]'
const cardClass = 'rounded-2xl border border-[#404040] bg-[#262626] shadow-sm'
const emptyRecord = {
patientId: '',
patient: '',
patientDocument: '',
patientEmail: '',
patientPhone: '',
dateTime: '',
doctor: '',
type: 'Primeira Consulta',
cid: '',
status: 'completo',
diagnosticReasoning: '',
diagnosticHypotheses: '',
definitiveDiagnosis: '',
prescriptions: '',
procedures: '',
surgeries: '',
orientations: '',
labResults: '',
imageResults: '',
multiprofessionalNotes: '',
signature: '',
professionalStamp: '',
}
const requiredFields = [
['patient', 'paciente'],
['dateTime', 'data e hora'],
['doctor', 'profissional'],
['cid', 'CID ou referência diagnóstica'],
['diagnosticReasoning', 'raciocínio médico'],
['diagnosticHypotheses', 'hipóteses diagnósticas'],
['definitiveDiagnosis', 'diagnóstico definitivo'],
['prescriptions', 'prescrição médica'],
['procedures', 'procedimentos realizados'],
['surgeries', 'cirurgias'],
['orientations', 'orientações'],
['labResults', 'laudos laboratoriais'],
['imageResults', 'laudos de imagem'],
['multiprofessionalNotes', 'notas multiprofissionais'],
['signature', 'assinatura/carimbo'],
['professionalStamp', 'carimbo profissional'],
]
export function MedicalRecordsPage({ navigate, mode = 'list', recordId = '' }) {
const recordTypes = medicalRecordRepository.getRecordTypes()
const [records, setRecords] = useState(() => medicalRecordRepository.getAll())
const [patients, setPatients] = useState([])
const [search, setSearch] = useState('')
useEffect(() => {
let active = true
patientRepository
.getDirectoryRows()
.then((data) => {
if (active) setPatients(data || [])
})
.catch(() => {
if (active) setPatients([])
})
return () => {
active = false
}
}, [])
const filteredRecords = useMemo(() => {
const query = normalizeSearch(search)
return records.filter((record) => {
if (!query) return true
return normalizeSearch([record.patient, record.cid, record.doctor, record.type, record.summary].join(' ')).includes(query)
})
}, [records, search])
const selectedRecord = records.find((record) => String(record.id) === String(recordId)) || null
const selectedPatient = selectedRecord ? findPatient(patients, selectedRecord) : null
function refreshRecords() {
setRecords(medicalRecordRepository.getAll())
}
function handleCreateRecord(data) {
const created = medicalRecordRepository.create(data)
refreshRecords()
navigate(`/prontuario/${created.id}`)
}
function handleUpdateRecord(data) {
const updated = medicalRecordRepository.update(recordId, data)
refreshRecords()
navigate(`/prontuario/${updated?.id || recordId}`)
}
if (mode === 'new') {
return (
)
}
if (mode === 'edit') {
return selectedRecord ? (
) : (
)
}
if (mode === 'detail') {
return selectedRecord ? (
) : (
)
}
return (
Prontuário Médico
Registros cronológicos, diagnósticos, condutas e resultados de exames
{filteredRecords.length ? (
filteredRecords.map((record) => (
navigate(`/prontuario/${record.id}/editar`)}
onOpen={() => navigate(`/prontuario/${record.id}`)}
onPrint={() => printRecordAsPdf(record)}
record={record}
/>
))
) : (
Nenhum registro encontrado nos dados locais.
)}
)
}
function RecordCard({ onEdit, onOpen, onPrint, record }) {
const statusClass =
record.status === 'completo'
? 'bg-emerald-500/20 text-emerald-400'
: 'bg-amber-500/20 text-amber-400'
return (
{record.patient}
{record.status === 'completo' ? 'Completo' : 'Rascunho'}
{formatDateTime(record.dateTime)}
{record.doctor}
{record.type}
{record.cid}
{record.summary}
)
}
function RecordDetailPage({ navigate, patient, record }) {
const [reports, setReports] = useState(() => medicalRecordRepository.getMockReportHistory(record.patientId, record.patient))
useEffect(() => {
let active = true
loadReportsForPatient(record.patientId, record.patient).then((data) => {
if (active) setReports(data)
})
return () => {
active = false
}
}, [record.patientId, record.patient])
const chronology = buildChronology(record, reports)
return (
Prontuário de {record.patient}
Registro cronológico inverso com assinatura profissional e histórico contínuo.
)
}
function RecordEditorPage({ navigate, onSave, patients, record, recordTypes }) {
const [patientSearch, setPatientSearch] = useState(record?.patient || '')
const [formData, setFormData] = useState(() => ({
...emptyRecord,
dateTime: toDateTimeLocal(new Date()),
doctor: 'Dr. Henrique Cardoso',
signature: 'Dr. Henrique Cardoso - CRM não informado',
professionalStamp: 'Assinado digitalmente por Dr. Henrique Cardoso',
...record,
}))
const filteredPatients = useMemo(() => {
const query = normalizeSearch(patientSearch)
if (!query) return patients.slice(0, 8)
return patients
.filter((patient) =>
normalizeSearch([patient.name, patient.full_name, patient.nome, patient.cpf, patient.document, patient.email].filter(Boolean).join(' ')).includes(query),
)
.slice(0, 8)
}, [patientSearch, patients])
function updateField(event) {
const { name, value } = event.target
setFormData((currentData) => ({ ...currentData, [name]: value }))
}
function selectPatient(patient) {
const patientName = getPatientName(patient)
setPatientSearch(patientName)
setFormData((currentData) => ({
...currentData,
patientId: String(patient.id || ''),
patient: patientName,
patientDocument: patient.document || patient.cpf || '',
patientEmail: patient.email || '',
patientPhone: patient.phone || patient.phone_mobile || patient.telefone || '',
}))
}
function handleSubmit(event) {
event.preventDefault()
const missing = requiredFields.filter(([field]) => !String(formData[field] || '').trim())
if (missing.length) {
alert(`Preencha os campos obrigatórios: ${missing.map(([, label]) => label).join(', ')}.`)
return
}
const summary = formData.definitiveDiagnosis || formData.diagnosticReasoning
onSave({
...formData,
summary,
date: formatDateTime(formData.dateTime),
})
}
return (
{record ? 'Editar prontuário' : 'Novo prontuário'}
Preencha os dados clínicos, legais e multiprofissionais obrigatórios do registro.
)
}
function PatientPickList({ items, onSelect, selectedId }) {
return (
{items.length ? (
items.map((patient) => {
const selected = String(patient.id || '') === String(selectedId || '')
return (
)
})
) : (
Nenhum paciente encontrado.
)}
)
}
function PatientReportHistory({ fallbackReports = [], patientId, patientName }) {
const [reportState, setReportState] = useState({ patientId: '', reports: [] })
const reports = patientId && reportState.patientId === patientId ? reportState.reports : fallbackReports
const loading = Boolean(patientId && reportState.patientId !== patientId)
useEffect(() => {
if (!patientId) return undefined
let active = true
loadReportsForPatient(patientId, patientName).then((data) => {
if (!active) return
setReportState({ patientId, reports: data.length ? data : fallbackReports })
})
return () => {
active = false
}
}, [fallbackReports, patientId, patientName])
return (
Histórico contínuo de relatórios
Ordem cronológica inversa do paciente selecionado.
{loading ? (
Carregando relatórios...
) : reports.length ? (
reports.map((report) => (
{report.title}
{report.status}
{formatDateTime(report.createdAt)} • {report.author}
{report.summary}
))
) : (
Nenhum relatório encontrado para este paciente.
)}
)
}
function ChronologyPanel({ entries }) {
return (
Cronologia clínica
{entries.map((entry) => (
{entry.kind}
{entry.title}
{formatDateTime(entry.createdAt)} • {entry.author}
{entry.summary}
))}
)
}
function ClinicalSection({ items, title }) {
return (
{title}
{items.map(([label, value]) => (
{label}
{value || `${label} não informado`}
))}
)
}
function FormSection({ children, title }) {
return (
)
}
function DetailCard({ label, value }) {
return (
{label}
{value || `${label} não informado`}
)
}
function IconButton({ label, name, onClick }) {
return (
)
}
function DarkField({ children, label }) {
return (
{label}
{children}
)
}
function RecordNotFound({ navigate }) {
return (
Prontuário não encontrado
O registro solicitado não existe nos dados locais.
)
}
async function loadReportsForPatient(patientId, patientName) {
if (!patientId) return medicalRecordRepository.getMockReportHistory(patientId, patientName)
try {
const reports = await reportRepository.getInitialReports({ patientId, order: 'created_at.desc' })
const mapped = reports.map((report) => ({
id: report.id,
title: report.exam || report.orderNumber || 'Relatório médico',
status: report.status === 'finalized' ? 'Finalizado' : 'Rascunho',
createdAt: report.createdAt || report.dueAt,
author: report.requestedBy || report.createdByName || 'Profissional não informado',
summary: stripHtml(report.contentHtml) || report.diagnosis || report.conclusion || 'Relatório sem complemento.',
}))
return mapped.length ? mapped : medicalRecordRepository.getMockReportHistory(patientId, patientName)
} catch {
return medicalRecordRepository.getMockReportHistory(patientId, patientName)
}
}
function buildChronology(record, reports) {
return [
{
id: `${record.id}-record`,
kind: 'Prontuário',
title: `${record.type} • ${record.cid}`,
createdAt: record.dateTime || record.createdAt,
author: record.signature || record.doctor,
summary: record.summary,
},
...reports.map((report) => ({
id: `report-${report.id}`,
kind: 'Relatório',
title: report.title,
createdAt: report.createdAt,
author: report.author,
summary: report.summary,
})),
].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
}
function findPatient(patients, record) {
return patients.find((patient) => String(patient.id || '') === String(record.patientId || '')) ||
patients.find((patient) => normalizeSearch(getPatientName(patient)) === normalizeSearch(record.patient)) ||
null
}
function getPatientName(patient) {
return patient?.name || patient?.full_name || patient?.nome || ''
}
function formatDateTime(value) {
const parsed = new Date(value)
if (Number.isNaN(parsed.getTime())) return value || 'Data não informada'
return parsed.toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' })
}
function toDateTimeLocal(value) {
const parsed = value instanceof Date ? value : new Date(value)
const safeDate = Number.isNaN(parsed.getTime()) ? new Date() : parsed
const year = safeDate.getFullYear()
const month = String(safeDate.getMonth() + 1).padStart(2, '0')
const day = String(safeDate.getDate()).padStart(2, '0')
const hours = String(safeDate.getHours()).padStart(2, '0')
const minutes = String(safeDate.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day}T${hours}:${minutes}`
}
function normalizeSearch(value) {
return String(value || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.trim()
.toLowerCase()
}
function stripHtml(value) {
return String(value || '')
.replace(/<[^>]+>/g, ' ')
.replace(/ /g, ' ')
.replace(/\s+/g, ' ')
.trim()
}
function printRecordAsPdf(record, patient, reports = []) {
const printWindow = window.open('', '_blank', 'noopener,noreferrer,width=900,height=1100')
if (!printWindow) {
window.print()
return
}
const reportItems = reports.map((report) => `${escapeHtml(report.title)} - ${escapeHtml(formatDateTime(report.createdAt))}: ${escapeHtml(report.summary)}`).join('')
printWindow.document.write(`
Prontuário - ${escapeHtml(record.patient)}
Prontuário Médico
Paciente: ${escapeHtml(record.patient)}
${printDetail('Documento', patient?.document || record.patientDocument || '-')}
${printDetail('Data e hora', formatDateTime(record.dateTime))}
${printDetail('Profissional', record.doctor)}
${printDetail('Assinatura/Carimbo', record.signature)}
Diagnóstico
Raciocínio médico: ${escapeHtml(record.diagnosticReasoning)}
Hipóteses: ${escapeHtml(record.diagnosticHypotheses)}
Diagnóstico definitivo: ${escapeHtml(record.definitiveDiagnosis)}
Conduta
Prescrição: ${escapeHtml(record.prescriptions)}
Procedimentos: ${escapeHtml(record.procedures)}
Cirurgias: ${escapeHtml(record.surgeries)}
Orientações: ${escapeHtml(record.orientations)}
Resultados de Exames
Laboratoriais: ${escapeHtml(record.labResults)}
Imagem: ${escapeHtml(record.imageResults)}
Multiprofissional
${escapeHtml(record.multiprofessionalNotes)}
Histórico de Relatórios
${reportItems || '- Nenhum relatório vinculado.
'}
`)
printWindow.document.close()
printWindow.focus()
printWindow.print()
}
function printDetail(label, value) {
return `${escapeHtml(label)}
${escapeHtml(value || '-')}
`
}
function sendRecordByEmail(record, patient) {
const email = patient?.email || record.patientEmail
if (!email) {
alert('E-mail do paciente não informado.')
return
}
const subject = encodeURIComponent(`Cópia do prontuário - ${record.patient}`)
const body = encodeURIComponent('Segue orientação para emissão da cópia em PDF do prontuário solicitado pelo paciente.')
window.location.href = `mailto:${email}?subject=${subject}&body=${body}`
}
function sendRecordByWhatsapp(record, patient) {
const phone = onlyDigits(patient?.phone || record.patientPhone)
if (!phone) {
alert('WhatsApp/celular do paciente não informado.')
return
}
const message = encodeURIComponent(`Olá, ${record.patient}. A cópia do seu prontuário está disponível para envio em PDF pela clínica.`)
window.open(`https://wa.me/55${phone}?text=${message}`, '_blank', 'noopener,noreferrer')
}
function onlyDigits(value) {
return String(value || '').replace(/\D/g, '')
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
function RecordIcon({ className = 'size-4', name }) {
const common = {
className,
fill: 'none',
stroke: 'currentColor',
strokeLinecap: 'round',
strokeLinejoin: 'round',
strokeWidth: 1.8,
viewBox: '0 0 24 24',
}
if (name === 'search') {
return (
)
}
if (name === 'plus') {
return (
)
}
if (name === 'file') {
return (
)
}
if (name === 'calendar') {
return (
)
}
if (name === 'user') {
return (
)
}
if (name === 'activity') {
return (
)
}
if (name === 'eye') {
return (
)
}
if (name === 'edit') {
return (
)
}
if (name === 'printer') {
return (
)
}
return (
)
}