diff --git a/src/components/featureStateStyles.js b/src/components/featureStateStyles.js index 57cb266..631e834 100644 --- a/src/components/featureStateStyles.js +++ b/src/components/featureStateStyles.js @@ -6,21 +6,21 @@ export const featureStateStyles = { label: '', }, partial: { - badge: 'border-sky-500/40 bg-sky-500/15 text-sky-300', - panel: 'border-sky-500/35 bg-sky-500/8', - title: 'text-sky-300', + badge: 'feature-badge-partial border-sky-500/40 bg-sky-500/15 text-sky-300', + panel: 'feature-panel-partial border-sky-500/35 bg-sky-500/8', + title: 'feature-title-partial text-sky-300', label: 'Parcial', }, mock: { - badge: 'border-amber-500/40 bg-amber-500/15 text-amber-300', - panel: 'border-amber-500/35 bg-amber-500/8', - title: 'text-amber-300', + badge: 'feature-badge-mock border-amber-500/40 bg-amber-500/15 text-amber-300', + panel: 'feature-panel-mock border-amber-500/35 bg-amber-500/8', + title: 'feature-title-mock text-amber-300', label: 'Mockado', }, wip: { - badge: 'border-rose-500/40 bg-rose-500/15 text-rose-300', - panel: 'border-rose-500/35 bg-rose-500/8', - title: 'text-rose-300', + badge: 'feature-badge-wip border-rose-500/40 bg-rose-500/15 text-rose-300', + panel: 'feature-panel-wip border-rose-500/35 bg-rose-500/8', + title: 'feature-title-wip text-rose-300', label: 'WIP', }, } diff --git a/src/index.css b/src/index.css index 2d99747..34504f9 100644 --- a/src/index.css +++ b/src/index.css @@ -46,12 +46,12 @@ button:disabled { :root[data-theme='light'] { color: #333333; - background: #cfd7e0; + background: #d9e4f0; color-scheme: light; } [data-theme='light'] body { - background: #cfd7e0; + background: #d9e4f0; color: #333333; } @@ -67,7 +67,7 @@ button:disabled { [data-theme='light'] .bg-\[\#0a0a0a\], [data-theme='light'] .bg-\[\#171717\] { - background-color: #cfd7e0; + background-color: #d9e4f0; } [data-theme='light'] .bg-\[\#1a1a1a\] { @@ -106,7 +106,7 @@ button:disabled { } [data-theme='light'] .disabled\:bg-\[\#303030\]:disabled { - background-color: #cfd7e0; + background-color: #d9e4f0; } [data-theme='light'] .border-\[\#404040\], @@ -254,6 +254,54 @@ button:disabled { background-color: #404040; } +[data-theme='light'] .feature-badge-partial { + border-color: #0284c7; + background: #dff3ff; + color: #075985; + box-shadow: inset 0 0 0 1px rgba(2, 132, 199, 0.12); +} + +[data-theme='light'] .feature-panel-partial { + border-color: #38bdf8; + background: #eef9ff; +} + +[data-theme='light'] .feature-title-partial { + color: #075985; +} + +[data-theme='light'] .feature-badge-mock { + border-color: #d97706; + background: #fff2c2; + color: #92400e; + box-shadow: inset 0 0 0 1px rgba(217, 119, 6, 0.14); +} + +[data-theme='light'] .feature-panel-mock { + border-color: #f59e0b; + background: #fff8db; +} + +[data-theme='light'] .feature-title-mock { + color: #92400e; +} + +[data-theme='light'] .feature-badge-wip { + border-color: #e11d48; + background: #ffe4e8; + color: #9f1239; + box-shadow: inset 0 0 0 1px rgba(225, 29, 72, 0.12); +} + +[data-theme='light'] .feature-panel-wip { + border-color: #fb7185; + background: #fff1f3; +} + +[data-theme='light'] .feature-title-wip { + color: #9f1239; +} + .agenda-calendar-shell { border-color: #3b3b3b; background: #202020; diff --git a/src/pages/ReportsPage.jsx b/src/pages/ReportsPage.jsx index f42f761..2b73edd 100644 --- a/src/pages/ReportsPage.jsx +++ b/src/pages/ReportsPage.jsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { normalizeRole } from '../config/permissions.js' import { patientRepository } from '../repositories/patientRepository.js' @@ -35,6 +35,91 @@ const textareaClass = 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: '', @@ -472,7 +557,7 @@ export function ReportsPage({ role }) { {editorOpen ? ( - setEditorOpen(false)} @@ -520,6 +605,310 @@ function ReportRow({ onEdit, onView, report }) { ) } +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} /> + +
+ +
+ +