diff --git a/susconecta/app/laudos-editor/page.tsx b/susconecta/app/laudos-editor/page.tsx new file mode 100644 index 0000000..04629ec --- /dev/null +++ b/susconecta/app/laudos-editor/page.tsx @@ -0,0 +1,970 @@ +'use client'; + +import React, { useState, useRef, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import ProtectedRoute from '@/components/shared/ProtectedRoute'; +import { useAuth } from '@/hooks/useAuth'; +import { useToast } from '@/hooks/use-toast'; +import { listarPacientes, buscarMedicos } from '@/lib/api'; +import { useReports } from '@/hooks/useReports'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { FileText, Upload, Settings, Eye, ArrowLeft, BookOpen } from 'lucide-react'; + +// Helpers para normalizar dados +const getPatientName = (p: any) => p?.full_name ?? p?.nome ?? ''; +const getPatientCpf = (p: any) => p?.cpf ?? ''; +const getPatientSex = (p: any) => p?.sex ?? p?.sexo ?? ''; +const getPatientAge = (p: any) => { + if (!p) return ''; + const bd = p?.birth_date ?? p?.data_nascimento ?? p?.birthDate; + if (bd) { + const d = new Date(bd); + if (!isNaN(d.getTime())) { + const age = Math.floor((Date.now() - d.getTime()) / (1000 * 60 * 60 * 24 * 365.25)); + return `${age}`; + } + } + return p?.idade ?? p?.age ?? ''; +}; + +export default function LaudosEditorPage() { + const router = useRouter(); + const { user, token } = useAuth(); + const { toast } = useToast(); + const { createNewReport } = useReports(); + + // Estados principais + const [pacienteSelecionado, setPacienteSelecionado] = useState(null); + const [listaPacientes, setListaPacientes] = useState([]); + const [content, setContent] = useState(''); + const [activeTab, setActiveTab] = useState('editor'); + const [showPreview, setShowPreview] = useState(false); + + // Estados para solicitante e prazo + const [solicitanteId, setSolicitanteId] = useState(user?.id || ''); + // Nome exibido do solicitante (preferir nome do médico vindo da API) + const [solicitanteNome, setSolicitanteNome] = useState(user?.name || ''); + const [prazoDate, setPrazoDate] = useState(''); + const [prazoTime, setPrazoTime] = useState(''); + + // Campos do laudo + const [campos, setCampos] = useState({ + cid: '', + diagnostico: '', + conclusao: '', + exame: '', + especialidade: '', + mostrarData: true, + mostrarAssinatura: true, + }); + + // Imagens + const [imagens, setImagens] = useState([]); + const [templates] = useState([ + 'Exame normal, sem alterações significativas', + 'Paciente em acompanhamento ambulatorial', + 'Recomenda-se retorno em 30 dias', + 'Alterações compatíveis com processo inflamatório', + 'Resultado dentro dos parâmetros de normalidade', + 'Recomendo seguimento com especialista', + ]); + + // Frases prontas + const [frasesProntas] = useState([ + 'Paciente apresenta bom estado geral.', + 'Recomenda-se seguimento clínico periódico.', + 'Encaminhar para especialista.', + 'Realizar novos exames em 30 dias.', + 'Retorno em 15 dias para reavaliação.', + 'Suspender medicamento em caso de efeitos colaterais.', + 'Manter repouso relativo por 7 dias.', + 'Seguir orientações prescritas rigorosamente.', + 'Compatível com os achados clínicos.', + 'Sem alterações significativas detectadas.', + ]); + + // Histórico + const [history, setHistory] = useState([]); + const [historyIndex, setHistoryIndex] = useState(-1); + + // Editor ref + const editorRef = useRef(null); + + // Estado para rastrear formatações ativas + const [activeFormats, setActiveFormats] = useState({ + bold: false, + italic: false, + underline: false, + strikethrough: false, + }); + + // Estado para controlar modal de confirmação de rascunho + const [showDraftConfirm, setShowDraftConfirm] = useState(false); + + // Atualizar formatações ativas ao mudar seleção + useEffect(() => { + const updateFormats = () => { + setActiveFormats({ + bold: document.queryCommandState('bold'), + italic: document.queryCommandState('italic'), + underline: document.queryCommandState('underline'), + strikethrough: document.queryCommandState('strikeThrough'), + }); + }; + + editorRef.current?.addEventListener('mouseup', updateFormats); + editorRef.current?.addEventListener('keyup', updateFormats); + + return () => { + editorRef.current?.removeEventListener('mouseup', updateFormats); + editorRef.current?.removeEventListener('keyup', updateFormats); + }; + }, []); + + // Carregar pacientes ao montar + useEffect(() => { + async function fetchPacientes() { + try { + if (!token) { + setListaPacientes([]); + return; + } + const pacientes = await listarPacientes(); + setListaPacientes(pacientes || []); + } catch (err) { + console.warn('Erro ao carregar pacientes:', err); + setListaPacientes([]); + } + } + fetchPacientes(); + + // Carregar rascunho salvo ao montar + const savedDraft = localStorage.getItem('laudoDraft'); + if (savedDraft) { + try { + const draft = JSON.parse(savedDraft); + setPacienteSelecionado(draft.pacienteSelecionado); + setContent(draft.content); + setCampos(draft.campos); + setSolicitanteId(draft.solicitanteId); + setPrazoDate(draft.prazoDate); + setPrazoTime(draft.prazoTime); + setImagens(draft.imagens || []); + + // Sincronizar editor com conteúdo carregado + if (editorRef.current) { + editorRef.current.innerHTML = draft.content; + } + } catch (err) { + console.warn('Erro ao carregar rascunho:', err); + } + } + }, [token]); + + // Import Quill CSS on client side only + useEffect(() => { + // No CSS needed for native contenteditable + }, []); + + // Sincronizar conteúdo inicial com editor ao montar + useEffect(() => { + if (editorRef.current && !editorRef.current.innerHTML) { + editorRef.current.innerHTML = content; + } + }, []); + + // Tentar obter o registro de médico correspondente ao usuário autenticado + useEffect(() => { + let mounted = true; + async function fetchDoctorName() { + try { + // Se já temos um nome razoável, não sobrescrever + if (solicitanteNome && solicitanteNome.trim().length > 1) return; + if (!user) return; + // Buscar médicos por email (buscarMedicos aceita termos com @ e faz a busca por email) + if (user.email && user.email.includes('@')) { + const docs = await buscarMedicos(user.email).catch(() => []); + if (!mounted) return; + if (Array.isArray(docs) && docs.length > 0) { + const d = docs[0]; + // Preferir full_name do médico quando disponível + if (d && (d.full_name || (d as any).nome)) { + setSolicitanteNome((d.full_name as string) || ((d as any).nome as string) || user.name || user.email || ''); + return; + } + } + } + + // Fallbacks: usar user.name se existir; caso contrário, email completo + setSolicitanteNome(user.name || user.email || ''); + } catch (err) { + // em caso de erro, manter o fallback + setSolicitanteNome(user?.name || user?.email || ''); + } + } + + fetchDoctorName(); + return () => { + mounted = false; + }; + }, [user]); + + // Atualizar histórico + useEffect(() => { + if (history[historyIndex] !== content) { + const newHistory = history.slice(0, historyIndex + 1); + setHistory([...newHistory, content]); + setHistoryIndex(newHistory.length); + } + }, [content]); + + // Desfazer + const handleUndo = () => { + if (historyIndex > 0) { + const newIndex = historyIndex - 1; + setContent(history[newIndex]); + setHistoryIndex(newIndex); + + // Atualizar editor com conteúdo anterior + setTimeout(() => { + if (editorRef.current) { + editorRef.current.innerHTML = history[newIndex]; + editorRef.current.focus(); + } + }, 0); + } + }; + + // Formatação com contenteditable (document.execCommand) + const applyFormat = (command: string, value?: string) => { + document.execCommand(command, false, value || undefined); + editorRef.current?.focus(); + }; + + const makeBold = () => applyFormat('bold'); + const makeItalic = () => applyFormat('italic'); + const makeUnderline = () => applyFormat('underline'); + const makeStrikethrough = () => applyFormat('strikeThrough'); + + const insertUnorderedList = () => { + document.execCommand('insertUnorderedList', false); + editorRef.current?.focus(); + }; + + const insertOrderedList = () => { + document.execCommand('insertOrderedList', false); + editorRef.current?.focus(); + }; + + const alignLeft = () => applyFormat('justifyLeft'); + const alignCenter = () => applyFormat('justifyCenter'); + const alignRight = () => applyFormat('justifyRight'); + const alignJustify = () => applyFormat('justifyFull'); + + const insertTemplate = (template: string) => { + setContent((prev: string) => (prev ? `${prev}\n\n${template}` : template)); + }; + + const insertFraseProta = (frase: string) => { + editorRef.current?.focus(); + document.execCommand('insertText', false, frase + ' '); + setContent(editorRef.current?.innerHTML || ''); + }; + + const handleImageUpload = (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + files.forEach((file) => { + const reader = new FileReader(); + reader.onload = (e) => { + setImagens((prev) => [ + ...prev, + { + id: Date.now() + Math.random(), + name: file.name, + url: e.target?.result, + type: file.type, + }, + ]); + }; + reader.readAsDataURL(file); + }); + }; + + // Salvar rascunho no localStorage + const saveDraft = () => { + const draft = { + pacienteSelecionado, + content, + campos, + solicitanteId, + prazoDate, + prazoTime, + imagens, + }; + localStorage.setItem('laudoDraft', JSON.stringify(draft)); + toast({ + title: 'Rascunho salvo!', + description: 'As informações do laudo foram salvas. Você pode continuar depois.', + variant: 'default', + }); + + // Redirecionar para profissional após 1 segundo + setTimeout(() => { + router.push('/profissional'); + }, 1000); + }; + + // Descartar rascunho + const discardDraft = () => { + localStorage.removeItem('laudoDraft'); + router.push('/profissional'); + }; + + // Processar cancelamento com confirmação + const handleCancel = () => { + // Verificar se há dados para salvar + const hasData = content || campos.cid || campos.diagnostico || campos.conclusao || campos.exame || imagens.length > 0; + + if (hasData) { + setShowDraftConfirm(true); + } else { + router.push('/profissional'); + } + }; + + const processContent = (content: string) => { + return content + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/__(.*?)__/g, '$1') + .replace(/\[left\]([\s\S]*?)\[\/left\]/g, '
$1
') + .replace(/\[center\]([\s\S]*?)\[\/center\]/g, '
$1
') + .replace(/\[right\]([\s\S]*?)\[\/right\]/g, '
$1
') + .replace(/\[justify\]([\s\S]*?)\[\/justify\]/g, '
$1
') + .replace(/\[size=(\d+)\]([\s\S]*?)\[\/size\]/g, '$2') + .replace(/\[font=([^\]]+)\]([\s\S]*?)\[\/font\]/g, '$2') + .replace(/\[color=([^\]]+)\]([\s\S]*?)\[\/color\]/g, '$2') + .replace(/{{diagnostico}}/g, campos.diagnostico || '[DIAGNÓSTICO]') + .replace(/{{conclusao}}/g, campos.conclusao || '[CONCLUSÃO]') + .replace(/\n/g, '
'); + }; + + const handleSave = async () => { + try { + if (!pacienteSelecionado?.id) { + toast({ + title: 'Erro', + description: 'Selecione um paciente para continuar.', + variant: 'destructive', + }); + return; + } + + const userId = user?.id || '00000000-0000-0000-0000-000000000001'; + + let composedDueAt = undefined; + if (prazoDate) { + const t = prazoTime || '23:59'; + composedDueAt = new Date(`${prazoDate}T${t}:00`).toISOString(); + } + + const payload = { + patient_id: pacienteSelecionado?.id, + order_number: '', + exam: campos.exame || '', + diagnosis: campos.diagnostico || '', + conclusion: campos.conclusao || '', + cid_code: campos.cid || '', + content_html: content, + content_json: {}, + requested_by: solicitanteId || userId, + due_at: composedDueAt ?? new Date().toISOString(), + hide_date: !campos.mostrarData, + hide_signature: !campos.mostrarAssinatura, + }; + + if (createNewReport) { + await createNewReport(payload as any); + toast({ + title: 'Laudo criado com sucesso!', + description: 'O laudo foi liberado e salvo.', + variant: 'default', + }); + // Redirecionar para profissional + router.push('/profissional'); + } + } catch (err) { + toast({ + title: 'Erro ao criar laudo', + description: (err && typeof err === 'object' && 'message' in err) ? (err as any).message : String(err) || 'Tente novamente.', + variant: 'destructive', + }); + } + }; + + return ( + +
+ {/* Header */} +
+
+
+ +
+

Novo Laudo Médico

+

Crie um novo laudo selecionando um paciente

+
+
+
+
+ + {/* Main Content */} +
+ {/* Seleção de Paciente */} +
+ {!pacienteSelecionado ? ( +
+ + +
+ ) : ( +
+
+
{getPatientName(pacienteSelecionado)}
+
+ {getPatientCpf(pacienteSelecionado) ? `CPF: ${getPatientCpf(pacienteSelecionado)} | ` : ''} + {pacienteSelecionado?.birth_date ? `Nascimento: ${pacienteSelecionado.birth_date}` : getPatientAge(pacienteSelecionado) ? `Idade: ${getPatientAge(pacienteSelecionado)} anos` : ''} + {getPatientSex(pacienteSelecionado) ? ` | Sexo: ${getPatientSex(pacienteSelecionado)}` : ''} +
+
+ +
+ )} + + {/* Solicitante e Prazo */} + {pacienteSelecionado && ( +
+
+ + +
+
+ +
+ setPrazoDate(e.target.value)} + className="text-xs sm:text-sm h-8 sm:h-10 flex-1" + /> + setPrazoTime(e.target.value)} + className="text-xs sm:text-sm h-8 sm:h-10 flex-1" + /> +
+

Defina a data e hora (opcional).

+
+
+ )} +
+ + {/* Tabs */} +
+ + + + +
+ + {/* Content */} +
+ {/* Left Panel */} +
+ {/* Editor Tab */} + {activeTab === 'editor' && ( +
+ {/* Toolbar */} +
+
+ {/* Font Family */} + + + + {/* Font Size */} + + + +
+ + + + + +
+ + +
+ + + + +
+ + + + + + {frasesProntas.map((frase, index) => ( + insertFraseProta(frase)} + className="text-xs cursor-pointer" + > + {frase} + + ))} + + +
+
+ + {/* Editor contenteditable */} +
+
setContent(e.currentTarget.innerHTML)} + onPaste={(e) => { + e.preventDefault(); + const text = e.clipboardData.getData('text/plain'); + document.execCommand('insertText', false, text); + }} + className="w-full h-full overflow-auto p-3 text-sm border border-border rounded bg-background text-foreground outline-none empty:before:content-['Digite_aqui...'] empty:before:text-muted-foreground" + style={{ caretColor: 'currentColor' }} + suppressContentEditableWarning + /> +
+
+ )} + + {/* Imagens Tab */} + {activeTab === 'imagens' && ( +
+
+ + +
+ +
+ {imagens.map((img) => ( +
+ {img.type.startsWith('image/') ? ( + {img.name} + ) : ( +
+ +
+ )} +

{img.name}

+ +
+ ))} +
+
+ )} + + {/* Campos Tab */} + {activeTab === 'campos' && ( +
+
+ + setCampos((prev) => ({ ...prev, cid: e.target.value }))} + placeholder="Ex: M25.5, I10, etc." + className="text-xs sm:text-sm mt-1 h-8 sm:h-10" + /> +
+
+ + setCampos((prev) => ({ ...prev, exame: e.target.value }))} + placeholder="Exame realizado" + className="text-xs sm:text-sm mt-1 h-8 sm:h-10" + /> +
+
+ +