diff --git a/susconecta/app/laudos-editor/page.tsx b/susconecta/app/laudos-editor/page.tsx new file mode 100644 index 0000000..380de9b --- /dev/null +++ b/susconecta/app/laudos-editor/page.tsx @@ -0,0 +1,733 @@ +'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 } 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 { FileText, Upload, Settings, Eye, ArrowLeft } 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 || ''); + const displaySolicitante = 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', + ]); + + // Histórico + const [history, setHistory] = useState([]); + const [historyIndex, setHistoryIndex] = useState(-1); + + // 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(); + }, [token]); + + // 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) { + setContent(history[historyIndex - 1]); + setHistoryIndex(historyIndex - 1); + } + }; + + // Formatação de texto + const formatText = (type: string, value?: any) => { + const textarea = document.querySelector('textarea') as HTMLTextAreaElement; + if (!textarea) return; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const selectedText = textarea.value.substring(start, end); + let formattedText = ''; + switch (type) { + case 'bold': + formattedText = selectedText ? `**${selectedText}**` : '**texto em negrito**'; + break; + case 'italic': + formattedText = selectedText ? `*${selectedText}*` : '*texto em itálico*'; + break; + case 'underline': + formattedText = selectedText ? `__${selectedText}__` : '__texto sublinhado__'; + break; + case 'list-ul': + formattedText = selectedText ? selectedText.split('\n').map((l) => `• ${l}`).join('\n') : '• item da lista'; + break; + case 'list-ol': + formattedText = selectedText ? selectedText.split('\n').map((l, i) => `${i + 1}. ${l}`).join('\n') : '1. item da lista'; + break; + case 'indent': + formattedText = selectedText ? selectedText.split('\n').map((l) => ` ${l}`).join('\n') : ' '; + break; + case 'outdent': + formattedText = selectedText ? selectedText.split('\n').map((l) => l.replace(/^\s{1,4}/, '')).join('\n') : ''; + break; + case 'align-left': + formattedText = selectedText ? `[left]${selectedText}[/left]` : '[left]Texto à esquerda[/left]'; + break; + case 'align-center': + formattedText = selectedText ? `[center]${selectedText}[/center]` : '[center]Texto centralizado[/center]'; + break; + case 'align-right': + formattedText = selectedText ? `[right]${selectedText}[/right]` : '[right]Texto à direita[/right]'; + break; + case 'align-justify': + formattedText = selectedText ? `[justify]${selectedText}[/justify]` : '[justify]Texto justificado[/justify]'; + break; + case 'font-size': + formattedText = selectedText ? `[size=${value}]${selectedText}[/size]` : `[size=${value}]Texto tamanho ${value}[/size]`; + break; + case 'font-family': + formattedText = selectedText ? `[font=${value}]${selectedText}[/font]` : `[font=${value}]${value}[/font]`; + break; + case 'font-color': + formattedText = selectedText ? `[color=${value}]${selectedText}[/color]` : `[color=${value}]${value}[/color]`; + break; + default: + return; + } + const newText = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end); + setContent(newText); + }; + + const insertTemplate = (template: string) => { + setContent((prev: string) => (prev ? `${prev}\n\n${template}` : template)); + }; + + 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); + }); + }; + + 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 */} +
+
+ + formatText('font-size', e.target.value)} + className="w-10 sm:w-12 border rounded px-1 py-0.5 text-xs" + title="Tamanho da fonte" + /> + + + + formatText('font-color', e.target.value)} + className="w-7 sm:w-8 h-7 sm:h-8 border rounded hidden sm:block" + title="Cor da fonte" + /> + + + + + + +
+ {templates.map((template, idx) => ( + + ))} +
+
+
+ + {/* Editor Textarea */} +
+