fix-new-laudo-editor

This commit is contained in:
João Gustavo 2025-11-10 22:27:59 -03:00
parent f6cbfad75b
commit 3c636ff537
4 changed files with 1081 additions and 138 deletions

View File

@ -12,7 +12,8 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'; import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select';
import { FileText, Upload, Settings, Eye, ArrowLeft } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { FileText, Upload, Settings, Eye, ArrowLeft, BookOpen } from 'lucide-react';
// Helpers para normalizar dados // Helpers para normalizar dados
const getPatientName = (p: any) => p?.full_name ?? p?.nome ?? ''; const getPatientName = (p: any) => p?.full_name ?? p?.nome ?? '';
@ -73,10 +74,58 @@ export default function LaudosEditorPage() {
'Recomendo seguimento com especialista', '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 // Histórico
const [history, setHistory] = useState<string[]>([]); const [history, setHistory] = useState<string[]>([]);
const [historyIndex, setHistoryIndex] = useState(-1); const [historyIndex, setHistoryIndex] = useState(-1);
// Editor ref
const editorRef = useRef<HTMLDivElement>(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 // Carregar pacientes ao montar
useEffect(() => { useEffect(() => {
async function fetchPacientes() { async function fetchPacientes() {
@ -93,8 +142,42 @@ export default function LaudosEditorPage() {
} }
} }
fetchPacientes(); 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]); }, [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 // Tentar obter o registro de médico correspondente ao usuário autenticado
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
@ -143,73 +226,56 @@ export default function LaudosEditorPage() {
// Desfazer // Desfazer
const handleUndo = () => { const handleUndo = () => {
if (historyIndex > 0) { if (historyIndex > 0) {
setContent(history[historyIndex - 1]); const newIndex = historyIndex - 1;
setHistoryIndex(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 de texto // Formatação com contenteditable (document.execCommand)
const formatText = (type: string, value?: any) => { const applyFormat = (command: string, value?: string) => {
const textarea = document.querySelector('textarea') as HTMLTextAreaElement; document.execCommand(command, false, value || undefined);
if (!textarea) return; editorRef.current?.focus();
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 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) => { const insertTemplate = (template: string) => {
setContent((prev: string) => (prev ? `${prev}\n\n${template}` : template)); 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<HTMLInputElement>) => { const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []); const files = Array.from(e.target.files || []);
files.forEach((file) => { files.forEach((file) => {
@ -229,6 +295,48 @@ export default function LaudosEditorPage() {
}); });
}; };
// 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) => { const processContent = (content: string) => {
return content return content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
@ -449,8 +557,7 @@ export default function LaudosEditorPage() {
}`} }`}
> >
<Eye className="w-3 sm:w-4 h-3 sm:h-4 inline mr-1" /> <Eye className="w-3 sm:w-4 h-3 sm:h-4 inline mr-1" />
<span className="hidden sm:inline">{showPreview ? 'Ocultar' : 'Pré-visualização'}</span> <span>{showPreview ? 'Ocultar' : 'Pré-visualização'}</span>
<span className="sm:hidden">{showPreview ? 'Ocultar' : 'Preview'}</span>
</button> </button>
</div> </div>
@ -462,24 +569,14 @@ export default function LaudosEditorPage() {
{activeTab === 'editor' && ( {activeTab === 'editor' && (
<div className="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
{/* Toolbar */} {/* Toolbar */}
<div className="p-1.5 sm:p-2 md:p-3 border-b border-border overflow-x-auto bg-card flex-shrink-0"> <div className="p-2 border-b border-border bg-card flex-shrink-0 overflow-x-auto">
<div className="flex flex-wrap gap-1 sm:gap-2 items-center"> <div className="flex flex-wrap gap-2 items-center">
<label className="text-xs mr-0.5 sm:mr-1 whitespace-nowrap">Tam</label> {/* Font Family */}
<input <label className="text-xs font-medium text-foreground whitespace-nowrap">Fonte:</label>
type="number"
min={8}
max={32}
defaultValue={14}
onBlur={(e) => 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"
/>
<label className="text-xs mr-0.5 sm:mr-1 whitespace-nowrap hidden sm:inline">Fonte</label>
<select <select
defaultValue={'Arial'} defaultValue="Arial"
onBlur={(e) => formatText('font-family', e.target.value)} onChange={(e) => applyFormat('fontName', e.target.value)}
className="border rounded px-1 py-0.5 text-xs bg-background text-foreground hidden sm:block" className="border border-border rounded px-2 py-1 text-xs bg-background text-foreground"
style={{ minWidth: 80 }}
> >
<option value="Arial">Arial</option> <option value="Arial">Arial</option>
<option value="Helvetica">Helvetica</option> <option value="Helvetica">Helvetica</option>
@ -488,56 +585,117 @@ export default function LaudosEditorPage() {
<option value="Verdana">Verdana</option> <option value="Verdana">Verdana</option>
<option value="Georgia">Georgia</option> <option value="Georgia">Georgia</option>
</select> </select>
<label className="text-xs mr-1 hidden sm:inline">Cor</label>
<input {/* Font Size */}
type="color" <label className="text-xs font-medium text-foreground whitespace-nowrap">Tamanho:</label>
defaultValue="#222222" <select
onBlur={(e) => formatText('font-color', e.target.value)} defaultValue="3"
className="w-7 sm:w-8 h-7 sm:h-8 border rounded hidden sm:block" onChange={(e) => applyFormat('fontSize', e.target.value)}
title="Cor da fonte" className="border border-border rounded px-2 py-1 text-xs bg-background text-foreground"
/> >
<Button variant="outline" size="sm" onClick={() => formatText('align-left')} title="Alinhar à esquerda" className="px-1.5 text-xs h-8"> <option value="1">8px</option>
<option value="2">10px</option>
<option value="3">12px</option>
<option value="4">14px</option>
<option value="5">18px</option>
<option value="6">24px</option>
<option value="7">32px</option>
</select>
<div className="w-px h-6 bg-border mx-1" />
<Button
variant={activeFormats.bold ? "default" : "outline"}
size="sm"
onMouseDown={(e) => { e.preventDefault(); makeBold(); }}
title="Negrito (Ctrl+B)"
className="text-xs h-8 px-2"
>
<strong>B</strong>
</Button> </Button>
<Button variant="outline" size="sm" onClick={() => formatText('align-center')} title="Centralizar" className="px-1.5 text-xs h-8"> <Button
· variant={activeFormats.italic ? "default" : "outline"}
size="sm"
onMouseDown={(e) => { e.preventDefault(); makeItalic(); }}
title="Itálico (Ctrl+I)"
className="text-xs h-8 px-2"
>
<em>I</em>
</Button> </Button>
<Button variant="outline" size="sm" onClick={() => formatText('align-right')} title="Alinhar à direita" className="px-1.5 text-xs h-8"> <Button
variant={activeFormats.underline ? "default" : "outline"}
size="sm"
onMouseDown={(e) => { e.preventDefault(); makeUnderline(); }}
title="Sublinhado (Ctrl+U)"
className="text-xs h-8 px-2"
>
<u>U</u>
</Button> </Button>
<Button variant="outline" size="sm" onClick={() => formatText('list-ol')} title="Lista numerada" className="px-1.5 text-xs h-8"> <Button
1. variant={activeFormats.strikethrough ? "default" : "outline"}
size="sm"
onMouseDown={(e) => { e.preventDefault(); makeStrikethrough(); }}
title="Tachado"
className="text-xs h-8 px-2"
>
<del>S</del>
</Button> </Button>
<Button variant="outline" size="sm" onClick={() => formatText('list-ul')} title="Lista com marcadores" className="px-1.5 text-xs h-8"> <div className="w-px h-6 bg-border mx-1" />
<Button variant="outline" size="sm" onMouseDown={(e) => { e.preventDefault(); insertUnorderedList(); }} title="Lista com marcadores" className="text-xs h-8 px-2 hover:bg-muted">
</Button> </Button>
<Button variant="outline" size="sm" onClick={handleUndo} title="Desfazer" className="px-1.5 text-xs h-8"> <Button variant="outline" size="sm" onMouseDown={(e) => { e.preventDefault(); insertOrderedList(); }} title="Lista numerada" className="text-xs h-8 px-2 hover:bg-muted">
1.
</Button> </Button>
<div className="hidden md:flex flex-wrap gap-1"> <div className="w-px h-6 bg-border mx-1" />
{templates.map((template, idx) => ( <Button variant="outline" size="sm" onMouseDown={(e) => { e.preventDefault(); alignLeft(); }} title="Alinhar à esquerda" className="text-xs h-8 px-2 hover:bg-muted">
<Button
key={idx} </Button>
variant="ghost" <Button variant="outline" size="sm" onMouseDown={(e) => { e.preventDefault(); alignCenter(); }} title="Centralizar" className="text-xs h-8 px-2 hover:bg-muted">
size="sm" ·
className="text-xs h-auto p-1 px-1.5" </Button>
onClick={() => insertTemplate(template)} <Button variant="outline" size="sm" onMouseDown={(e) => { e.preventDefault(); alignRight(); }} title="Alinhar à direita" className="text-xs h-8 px-2 hover:bg-muted">
title={template}
> </Button>
{template.substring(0, 15)}... <Button variant="outline" size="sm" onMouseDown={(e) => { e.preventDefault(); alignJustify(); }} title="Justificar" className="text-xs h-8 px-2 hover:bg-muted">
</Button>
<div className="w-px h-6 bg-border mx-1" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" title="Frases prontas" className="text-xs h-8 px-2 hover:bg-muted">
<BookOpen className="w-4 h-4" />
</Button> </Button>
))} </DropdownMenuTrigger>
</div> <DropdownMenuContent align="start" className="w-64">
{frasesProntas.map((frase, index) => (
<DropdownMenuItem
key={index}
onSelect={() => insertFraseProta(frase)}
className="text-xs cursor-pointer"
>
{frase}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
{/* Editor Textarea */} {/* Editor contenteditable */}
<div className="flex-1 overflow-hidden p-2 sm:p-3 md:p-4"> <div className="flex-1 overflow-hidden p-2 sm:p-3 md:p-4">
<Textarea <div
value={content} ref={editorRef}
onChange={(e) => setContent(e.target.value)} contentEditable
placeholder="Digite o conteúdo do laudo aqui. Use ** para negrito, * para itálico." onInput={(e) => setContent(e.currentTarget.innerHTML)}
className="w-full h-full resize-none text-xs sm:text-sm" 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
/> />
</div> </div>
</div> </div>
@ -675,7 +833,7 @@ export default function LaudosEditorPage() {
<h3 className="font-semibold text-xs sm:text-sm text-foreground truncate">Pré-visualização</h3> <h3 className="font-semibold text-xs sm:text-sm text-foreground truncate">Pré-visualização</h3>
</div> </div>
<div className="flex-1 overflow-y-auto p-2 sm:p-2.5 md:p-3"> <div className="flex-1 overflow-y-auto p-2 sm:p-2.5 md:p-3">
<div className="bg-background border border-border rounded p-2 sm:p-2.5 md:p-3 text-xs space-y-1.5 sm:space-y-2"> <div className="bg-background border border-border rounded p-2 sm:p-2.5 md:p-3 text-xs space-y-1.5 sm:space-y-2 max-w-full">
{/* Header */} {/* Header */}
<div className="text-center mb-2 pb-2 border-b border-border/40"> <div className="text-center mb-2 pb-2 border-b border-border/40">
<h2 className="text-xs sm:text-sm font-bold leading-tight whitespace-normal"> <h2 className="text-xs sm:text-sm font-bold leading-tight whitespace-normal">
@ -726,7 +884,7 @@ export default function LaudosEditorPage() {
<div className="mb-1.5 pb-1.5 border-b border-border/40"> <div className="mb-1.5 pb-1.5 border-b border-border/40">
<div className="text-xs font-semibold mb-0.5">Conteúdo:</div> <div className="text-xs font-semibold mb-0.5">Conteúdo:</div>
<div <div
className="text-xs leading-tight whitespace-pre-wrap text-muted-foreground" className="text-xs leading-tight whitespace-pre-wrap text-muted-foreground break-words overflow-hidden"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: processContent(content), __html: processContent(content),
}} }}
@ -757,7 +915,7 @@ export default function LaudosEditorPage() {
Editor de relatórios com formatação de texto rica. Editor de relatórios com formatação de texto rica.
</div> </div>
<div className="flex gap-2 w-full sm:w-auto"> <div className="flex gap-2 w-full sm:w-auto">
<Button variant="outline" onClick={() => router.push('/profissional')} className="flex-1 sm:flex-none text-xs sm:text-sm h-8 sm:h-10"> <Button variant="outline" onClick={handleCancel} className="flex-1 sm:flex-none text-xs sm:text-sm h-8 sm:h-10">
Cancelar Cancelar
</Button> </Button>
<Button onClick={handleSave} className="flex-1 sm:flex-none text-xs sm:text-sm h-8 sm:h-10"> <Button onClick={handleSave} className="flex-1 sm:flex-none text-xs sm:text-sm h-8 sm:h-10">
@ -766,6 +924,46 @@ export default function LaudosEditorPage() {
</div> </div>
</div> </div>
</div> </div>
{/* Modal de Confirmação de Rascunho */}
{showDraftConfirm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-card rounded-lg shadow-lg p-4 sm:p-6 max-w-sm w-full">
<h2 className="text-lg sm:text-xl font-bold mb-2 text-foreground">Salvar Rascunho?</h2>
<p className="text-sm text-muted-foreground mb-6">
Você tem informações não salvas. Deseja salvar como rascunho para continuar depois?
</p>
<div className="flex gap-2 sm:gap-3 flex-col sm:flex-row">
<Button
variant="outline"
onClick={() => {
setShowDraftConfirm(false);
discardDraft();
}}
className="text-xs sm:text-sm h-9 sm:h-10"
>
Descartar
</Button>
<Button
variant="outline"
onClick={() => setShowDraftConfirm(false)}
className="text-xs sm:text-sm h-9 sm:h-10"
>
Voltar
</Button>
<Button
onClick={() => {
setShowDraftConfirm(false);
saveDraft();
}}
className="text-xs sm:text-sm h-9 sm:h-10"
>
Salvar Rascunho
</Button>
</div>
</div>
</div>
)}
</div> </div>
</ProtectedRoute> </ProtectedRoute>
); );

View File

@ -0,0 +1,726 @@
'use client';
import React, { useState, useRef, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import ProtectedRoute from '@/components/shared/ProtectedRoute';
import { useAuth } from '@/hooks/useAuth';
import { useToast } from '@/hooks/use-toast';
import { buscarRelatorioPorId, buscarPacientePorId } 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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { FileText, Settings, Eye, ArrowLeft, BookOpen } from 'lucide-react';
export default function EditarLaudoPage() {
const router = useRouter();
const params = useParams();
const { user, token } = useAuth();
const { toast } = useToast();
const { updateExistingReport } = useReports();
const laudoId = params.id as string;
// Estados principais
const [reportData, setReportData] = useState<any>(null);
const [patient, setPatient] = useState<any>(null);
const [content, setContent] = useState('');
const [activeTab, setActiveTab] = useState('editor');
const [showPreview, setShowPreview] = useState(false);
const [loading, setLoading] = useState(true);
// Campos do laudo
const [campos, setCampos] = useState({
cid: '',
diagnostico: '',
conclusao: '',
exame: '',
especialidade: '',
mostrarData: true,
mostrarAssinatura: true,
});
// Editor ref
const editorRef = useRef<HTMLDivElement>(null);
// Frases prontas
const frasesProntas = [
'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.',
];
// Estado para rastrear formatações ativas
const [activeFormats, setActiveFormats] = useState({
bold: false,
italic: false,
underline: false,
strikethrough: false,
});
// Estado para rastrear alinhamento ativo
const [activeAlignment, setActiveAlignment] = useState('left');
// Salvar conteúdo no localStorage sempre que muda
useEffect(() => {
if (content && laudoId) {
localStorage.setItem(`laudo-draft-${laudoId}`, content);
}
}, [content, laudoId]);
// Sincronizar conteúdo com o editor
useEffect(() => {
if (editorRef.current && content) {
if (editorRef.current.innerHTML !== content) {
editorRef.current.innerHTML = content;
}
}
}, [content]);
// Restaurar conteúdo quando volta para a aba editor
useEffect(() => {
if (activeTab === 'editor' && editorRef.current && content) {
editorRef.current.focus();
const range = document.createRange();
const sel = window.getSelection();
range.setStart(editorRef.current, editorRef.current.childNodes.length);
range.collapse(true);
sel?.removeAllRanges();
sel?.addRange(range);
}
}, [activeTab]);
// 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'),
});
// Detectar alinhamento ativo
if (document.queryCommandState('justifyCenter')) {
setActiveAlignment('center');
} else if (document.queryCommandState('justifyRight')) {
setActiveAlignment('right');
} else if (document.queryCommandState('justifyFull')) {
setActiveAlignment('justify');
} else {
setActiveAlignment('left');
}
};
editorRef.current?.addEventListener('mouseup', updateFormats);
editorRef.current?.addEventListener('keyup', updateFormats);
return () => {
editorRef.current?.removeEventListener('mouseup', updateFormats);
editorRef.current?.removeEventListener('keyup', updateFormats);
};
}, []);
// Carregar laudo ao montar
useEffect(() => {
async function fetchLaudo() {
try {
if (!laudoId || !token) {
setLoading(false);
return;
}
const report = await buscarRelatorioPorId(laudoId);
setReportData(report);
// Carregar paciente se existir patient_id
const r = report as any;
if (r.patient_id) {
try {
const patientData = await buscarPacientePorId(r.patient_id);
setPatient(patientData);
} catch (err) {
console.warn('Erro ao carregar paciente:', err);
}
}
// Preencher campos
setCampos({
cid: r.cid_code || r.cid || '',
diagnostico: r.diagnosis || r.diagnostico || '',
conclusao: r.conclusion || r.conclusao || '',
exame: r.exam || r.exame || '',
especialidade: r.especialidade || '',
mostrarData: !r.hide_date,
mostrarAssinatura: !r.hide_signature,
});
// Preencher conteúdo
const contentHtml = r.content_html || r.conteudo_html || '';
// Verificar se existe rascunho salvo no localStorage
const draftContent = typeof window !== 'undefined' ? localStorage.getItem(`laudo-draft-${laudoId}`) : null;
const finalContent = draftContent || contentHtml;
setContent(finalContent);
if (editorRef.current) {
editorRef.current.innerHTML = finalContent;
// Colocar cursor no final do texto
editorRef.current.focus();
const range = document.createRange();
const sel = window.getSelection();
range.setStart(editorRef.current, editorRef.current.childNodes.length);
range.collapse(true);
sel?.removeAllRanges();
sel?.addRange(range);
}
} catch (err) {
console.warn('Erro ao carregar laudo:', err);
toast({
title: 'Erro',
description: 'Erro ao carregar o laudo.',
variant: 'destructive',
});
} finally {
setLoading(false);
}
}
fetchLaudo();
}, [laudoId, token, toast]);
// Formatação com contenteditable
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 alignText = (alignment: 'left' | 'center' | 'right' | 'justify') => {
editorRef.current?.focus();
const alignCommands: { [key: string]: string } = {
left: 'justifyLeft',
center: 'justifyCenter',
right: 'justifyRight',
justify: 'justifyFull',
};
document.execCommand(alignCommands[alignment], false, undefined);
if (editorRef.current) {
setContent(editorRef.current.innerHTML);
}
};
const alignLeft = () => alignText('left');
const alignCenter = () => alignText('center');
const alignRight = () => alignText('right');
const alignJustify = () => alignText('justify');
const insertFraseProta = (frase: string) => {
editorRef.current?.focus();
document.execCommand('insertText', false, frase + ' ');
if (editorRef.current) {
setContent(editorRef.current.innerHTML);
}
};
const processContent = (content: string) => {
return content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/__(.*?)__/g, '<u>$1</u>')
.replace(/\[left\]([\s\S]*?)\[\/left\]/g, '<div style="text-align:left">$1</div>')
.replace(/\[center\]([\s\S]*?)\[\/center\]/g, '<div style="text-align:center">$1</div>')
.replace(/\[right\]([\s\S]*?)\[\/right\]/g, '<div style="text-align:right">$1</div>')
.replace(/\[justify\]([\s\S]*?)\[\/justify\]/g, '<div style="text-align:justify">$1</div>')
.replace(/\[size=(\d+)\]([\s\S]*?)\[\/size\]/g, '<span style="font-size:$1px">$2</span>')
.replace(/\[font=([^\]]+)\]([\s\S]*?)\[\/font\]/g, '<span style="font-family:$1">$2</span>')
.replace(/\[color=([^\]]+)\]([\s\S]*?)\[\/color\]/g, '<span style="color:$1">$2</span>')
.replace(/{{diagnostico}}/g, campos.diagnostico || '[DIAGNÓSTICO]')
.replace(/{{conclusao}}/g, campos.conclusao || '[CONCLUSÃO]')
.replace(/\n/g, '<br>');
};
const handleSave = async () => {
try {
if (!reportData?.id) {
toast({
title: 'Erro',
description: 'ID do laudo não encontrado.',
variant: 'destructive',
});
return;
}
// Pegar conteúdo diretamente do DOM para garantir que está atualizado
const currentContent = editorRef.current?.innerHTML || content;
const payload = {
exam: campos.exame || '',
diagnosis: campos.diagnostico || '',
conclusion: campos.conclusao || '',
cid_code: campos.cid || '',
content_html: currentContent,
content_json: {},
hide_date: !campos.mostrarData,
hide_signature: !campos.mostrarAssinatura,
};
if (updateExistingReport) {
await updateExistingReport(reportData.id, payload as any);
// Limpar rascunho do localStorage após salvar
if (typeof window !== 'undefined') {
localStorage.removeItem(`laudo-draft-${reportData.id}`);
}
toast({
title: 'Laudo atualizado com sucesso!',
description: 'As alterações foram salvas.',
variant: 'default',
});
router.push(`/laudos/${reportData.id}`);
}
} catch (err) {
toast({
title: 'Erro ao atualizar laudo',
description: (err && typeof err === 'object' && 'message' in err) ? (err as any).message : String(err) || 'Tente novamente.',
variant: 'destructive',
});
}
};
if (loading) {
return (
<ProtectedRoute>
<div className="flex items-center justify-center min-h-screen bg-background">
<div className="text-lg text-muted-foreground">Carregando laudo...</div>
</div>
</ProtectedRoute>
);
}
return (
<ProtectedRoute>
<div className="min-h-screen bg-background flex flex-col">
{/* Header */}
<div className="border-b border-border bg-card shadow-sm sticky top-0 z-10">
<div className="px-2 sm:px-4 md:px-6 py-3 sm:py-4 flex items-center justify-between gap-2 sm:gap-4">
<div className="flex items-center gap-2 sm:gap-4 flex-1 min-w-0">
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
className="p-0 h-auto flex-shrink-0"
>
<ArrowLeft className="w-4 sm:w-5 h-4 sm:h-5" />
</Button>
<div className="min-w-0">
<h1 className="text-lg sm:text-2xl font-bold truncate">Editar Laudo Médico</h1>
<div className="flex flex-col gap-0.5">
<p className="text-xs sm:text-sm text-muted-foreground truncate">Atualize as informações do laudo</p>
{patient && (
<p className="text-xs sm:text-sm font-semibold text-blue-600 dark:text-blue-400 truncate">
Paciente: {patient.full_name || patient.name || 'N/A'}
</p>
)}
</div>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="flex-1 overflow-hidden flex flex-col">
{/* Tabs */}
<div className="flex border-b border-border bg-card overflow-x-auto flex-shrink-0">
<button
onClick={() => setActiveTab('editor')}
className={`px-2 sm:px-4 py-2 text-xs sm:text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
activeTab === 'editor'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-600 dark:text-muted-foreground'
}`}
>
<FileText className="w-3 sm:w-4 h-3 sm:h-4 inline mr-1" />
Editor
</button>
<button
onClick={() => setActiveTab('campos')}
className={`px-2 sm:px-4 py-2 text-xs sm:text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
activeTab === 'campos'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-600 dark:text-muted-foreground'
}`}
>
<Settings className="w-3 sm:w-4 h-3 sm:h-4 inline mr-1" />
Campos
</button>
<button
onClick={() => setShowPreview(!showPreview)}
className={`px-2 sm:px-4 py-2 text-xs sm:text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
showPreview ? 'border-green-500 text-green-600' : 'border-transparent text-gray-600 dark:text-muted-foreground'
}`}
>
<Eye className="w-3 sm:w-4 h-3 sm:h-4 inline mr-1" />
<span>{showPreview ? 'Ocultar' : 'Pré-visualização'}</span>
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-hidden flex flex-col md:flex-row bg-background">
{/* Left Panel */}
<div className={`flex flex-col overflow-hidden transition-all ${showPreview ? 'w-full md:w-3/5 h-auto md:h-full' : 'w-full'}`}>
{/* Editor Tab */}
{activeTab === 'editor' && (
<div className="flex-1 flex flex-col overflow-hidden">
{/* Toolbar */}
<div className="p-2 border-b border-border bg-card flex-shrink-0 overflow-x-auto">
<div className="flex flex-wrap gap-2 items-center">
{/* Font Family */}
<label className="text-xs font-medium text-foreground whitespace-nowrap">Fonte:</label>
<select
defaultValue="Arial"
onChange={(e) => applyFormat('fontName', e.target.value)}
className="border border-border rounded px-2 py-1 text-xs bg-background text-foreground"
>
<option value="Arial">Arial</option>
<option value="Helvetica">Helvetica</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Courier New">Courier New</option>
<option value="Verdana">Verdana</option>
<option value="Georgia">Georgia</option>
</select>
{/* Font Size */}
<label className="text-xs font-medium text-foreground whitespace-nowrap">Tamanho:</label>
<select
defaultValue="3"
onChange={(e) => applyFormat('fontSize', e.target.value)}
className="border border-border rounded px-2 py-1 text-xs bg-background text-foreground"
>
<option value="1">8px</option>
<option value="2">10px</option>
<option value="3">12px</option>
<option value="4">14px</option>
<option value="5">18px</option>
<option value="6">24px</option>
<option value="7">32px</option>
</select>
<div className="w-px h-6 bg-border mx-1" />
<Button
variant={activeFormats.bold ? "default" : "outline"}
size="sm"
onMouseDown={(e) => { e.preventDefault(); makeBold(); }}
title="Negrito (Ctrl+B)"
className="text-xs h-8 px-2"
>
<strong>B</strong>
</Button>
<Button
variant={activeFormats.italic ? "default" : "outline"}
size="sm"
onMouseDown={(e) => { e.preventDefault(); makeItalic(); }}
title="Itálico (Ctrl+I)"
className="text-xs h-8 px-2"
>
<em>I</em>
</Button>
<Button
variant={activeFormats.underline ? "default" : "outline"}
size="sm"
onMouseDown={(e) => { e.preventDefault(); makeUnderline(); }}
title="Sublinhado (Ctrl+U)"
className="text-xs h-8 px-2"
>
<u>U</u>
</Button>
<Button
variant={activeFormats.strikethrough ? "default" : "outline"}
size="sm"
onMouseDown={(e) => { e.preventDefault(); makeStrikethrough(); }}
title="Tachado"
className="text-xs h-8 px-2"
>
<del>S</del>
</Button>
<div className="w-px h-6 bg-border mx-1" />
<Button variant="outline" size="sm" onMouseDown={(e) => { e.preventDefault(); insertUnorderedList(); }} title="Lista com marcadores" className="text-xs h-8 px-2 hover:bg-muted">
</Button>
<Button variant="outline" size="sm" onMouseDown={(e) => { e.preventDefault(); insertOrderedList(); }} title="Lista numerada" className="text-xs h-8 px-2 hover:bg-muted">
1.
</Button>
<div className="w-px h-6 bg-border mx-1" />
<Button
variant={activeAlignment === 'left' ? "default" : "outline"}
size="sm"
onMouseDown={(e) => { e.preventDefault(); alignLeft(); }}
title="Alinhar à esquerda"
className="text-xs h-8 px-2"
>
</Button>
<Button
variant={activeAlignment === 'center' ? "default" : "outline"}
size="sm"
onMouseDown={(e) => { e.preventDefault(); alignCenter(); }}
title="Centralizar"
className="text-xs h-8 px-2"
>
·
</Button>
<Button
variant={activeAlignment === 'right' ? "default" : "outline"}
size="sm"
onMouseDown={(e) => { e.preventDefault(); alignRight(); }}
title="Alinhar à direita"
className="text-xs h-8 px-2"
>
</Button>
<Button
variant={activeAlignment === 'justify' ? "default" : "outline"}
size="sm"
onMouseDown={(e) => { e.preventDefault(); alignJustify(); }}
title="Justificar"
className="text-xs h-8 px-2"
>
</Button>
<div className="w-px h-6 bg-border mx-1" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" title="Frases prontas" className="text-xs h-8 px-2 hover:bg-muted">
<BookOpen className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-64">
{frasesProntas.map((frase, index) => (
<DropdownMenuItem
key={index}
onSelect={() => insertFraseProta(frase)}
className="text-xs cursor-pointer"
>
{frase}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* Editor contenteditable */}
<div className="flex-1 overflow-hidden p-2 sm:p-3 md:p-4">
<div
ref={editorRef}
contentEditable
onInput={(e) => 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
/>
</div>
</div>
)}
{/* Campos Tab */}
{activeTab === 'campos' && (
<div className="flex-1 p-2 sm:p-3 md:p-4 space-y-2 sm:space-y-3 md:space-y-4 overflow-y-auto">
<div>
<Label htmlFor="cid" className="text-xs sm:text-sm">
CID
</Label>
<Input
id="cid"
value={campos.cid}
onChange={(e) => 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"
/>
</div>
<div>
<Label htmlFor="exame" className="text-xs sm:text-sm">
Exame
</Label>
<Input
id="exame"
value={campos.exame}
onChange={(e) => setCampos((prev) => ({ ...prev, exame: e.target.value }))}
placeholder="Exame realizado"
className="text-xs sm:text-sm mt-1 h-8 sm:h-10"
/>
</div>
<div>
<Label htmlFor="diagnostico" className="text-xs sm:text-sm">
Diagnóstico
</Label>
<Textarea
id="diagnostico"
value={campos.diagnostico}
onChange={(e) => setCampos((prev) => ({ ...prev, diagnostico: e.target.value }))}
placeholder="Diagnóstico principal"
rows={2}
className="text-xs sm:text-sm mt-1"
/>
</div>
<div>
<Label htmlFor="conclusao" className="text-xs sm:text-sm">
Conclusão
</Label>
<Textarea
id="conclusao"
value={campos.conclusao}
onChange={(e) => setCampos((prev) => ({ ...prev, conclusao: e.target.value }))}
placeholder="Conclusão do laudo"
rows={2}
className="text-xs sm:text-sm mt-1"
/>
</div>
<div className="space-y-1 sm:space-y-2">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="mostrar-data"
checked={campos.mostrarData}
onChange={(e) => setCampos((prev) => ({ ...prev, mostrarData: e.target.checked }))}
className="w-4 h-4"
/>
<Label htmlFor="mostrar-data" className="text-xs sm:text-sm">
Mostrar data no laudo
</Label>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="mostrar-assinatura"
checked={campos.mostrarAssinatura}
onChange={(e) => setCampos((prev) => ({ ...prev, mostrarAssinatura: e.target.checked }))}
className="w-4 h-4"
/>
<Label htmlFor="mostrar-assinatura" className="text-xs sm:text-sm">
Mostrar assinatura no laudo
</Label>
</div>
</div>
</div>
)}
</div>
{/* Preview Panel */}
{showPreview && (
<div className="w-full md:w-2/5 h-auto md:h-full border-t md:border-l md:border-t-0 border-border bg-muted/20 flex flex-col overflow-hidden">
<div className="p-2 sm:p-2.5 md:p-3 border-b border-border flex-shrink-0 bg-card">
<h3 className="font-semibold text-xs sm:text-sm text-foreground truncate">Pré-visualização</h3>
</div>
<div className="flex-1 overflow-y-auto p-2 sm:p-2.5 md:p-3">
<div className="bg-background border border-border rounded p-2 sm:p-2.5 md:p-3 text-xs space-y-1.5 sm:space-y-2 max-w-full">
{/* Header */}
<div className="text-center mb-2 pb-2 border-b border-border/40">
<h2 className="text-xs sm:text-sm font-bold leading-tight whitespace-normal">
LAUDO {campos.especialidade ? `- ${campos.especialidade.toUpperCase().substring(0, 12)}` : ''}
</h2>
{campos.exame && <p className="text-xs font-semibold mt-1 whitespace-pre-wrap break-words">{campos.exame}</p>}
{campos.mostrarData && (
<p className="text-xs text-muted-foreground mt-1">{new Date().toLocaleDateString('pt-BR')}</p>
)}
</div>
{/* Informações Clínicas */}
<div className="mb-1.5 pb-1.5 border-b border-border/40 space-y-0.5">
{campos.cid && (
<div className="text-xs whitespace-normal break-words">
<div className="font-semibold">CID:</div>
<div className="mt-0.5 text-blue-600 dark:text-blue-400 font-semibold">{campos.cid}</div>
</div>
)}
</div>
{/* Diagnóstico */}
{campos.diagnostico && (
<div className="mb-1.5 pb-1.5 border-b border-border/40">
<div className="text-xs font-semibold mb-0.5">Diagnóstico:</div>
<div className="text-xs leading-tight whitespace-pre-wrap text-muted-foreground break-words">
{campos.diagnostico}
</div>
</div>
)}
{/* Conteúdo */}
{content && (
<div className="mb-1.5 pb-1.5 border-b border-border/40">
<div className="text-xs font-semibold mb-0.5">Conteúdo:</div>
<div
className="text-xs leading-tight whitespace-pre-wrap text-muted-foreground break-words overflow-hidden"
dangerouslySetInnerHTML={{
__html: processContent(content),
}}
/>
</div>
)}
{/* Conclusão */}
{campos.conclusao && (
<div className="mb-1.5 pb-1.5 border-b border-border/40">
<div className="text-xs font-semibold mb-0.5">Conclusão:</div>
<div className="text-xs leading-tight whitespace-pre-wrap text-muted-foreground break-words">
{campos.conclusao}
</div>
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
{/* Footer */}
<div className="p-2 sm:p-3 md:p-4 border-t border-border bg-card flex-shrink-0">
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-2 sm:gap-4">
<div className="text-xs text-muted-foreground hidden md:block">
Edite as informações do laudo e salve as alterações.
</div>
<div className="flex gap-2 w-full sm:w-auto">
<Button variant="outline" onClick={() => router.back()} className="flex-1 sm:flex-none text-xs sm:text-sm h-8 sm:h-10">
Cancelar
</Button>
<Button onClick={handleSave} className="flex-1 sm:flex-none text-xs sm:text-sm h-8 sm:h-10">
Salvar Alterações
</Button>
</div>
</div>
</div>
</div>
</ProtectedRoute>
);
}

View File

@ -6,7 +6,7 @@ import { useTheme } from 'next-themes'
import Image from 'next/image' import Image from 'next/image'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ArrowLeft, Printer, Download, MoreVertical } from 'lucide-react' import { ArrowLeft, Printer, Download, MoreVertical } from 'lucide-react'
import { buscarRelatorioPorId, getDoctorById, buscarMedicosPorIds } from '@/lib/api' import { buscarRelatorioPorId, getDoctorById, buscarMedicosPorIds, buscarPacientePorId } from '@/lib/api'
import { ENV_CONFIG } from '@/lib/env-config' import { ENV_CONFIG } from '@/lib/env-config'
import ProtectedRoute from '@/components/shared/ProtectedRoute' import ProtectedRoute from '@/components/shared/ProtectedRoute'
import { useAuth } from '@/hooks/useAuth' import { useAuth } from '@/hooks/useAuth'
@ -21,6 +21,7 @@ export default function LaudoPage() {
const [report, setReport] = useState<any | null>(null) const [report, setReport] = useState<any | null>(null)
const [doctor, setDoctor] = useState<any | null>(null) const [doctor, setDoctor] = useState<any | null>(null)
const [patient, setPatient] = useState<any | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState('') const [error, setError] = useState('')
@ -37,8 +38,21 @@ export default function LaudoPage() {
if (!mounted) return if (!mounted) return
setReport(reportData) setReport(reportData)
// Load doctor info using the same strategy as paciente/page.tsx // Load patient info if patient_id exists
const rd = reportData as any const rd = reportData as any
const patientId = rd?.patient_id
if (patientId) {
try {
const patientData = await buscarPacientePorId(patientId).catch(() => null)
if (mounted && patientData) {
setPatient(patientData)
}
} catch (e) {
console.warn('Erro ao carregar dados do paciente:', e)
}
}
// Load doctor info using the same strategy as paciente/page.tsx
const maybeId = rd?.doctor_id ?? rd?.created_by ?? rd?.doctor ?? null const maybeId = rd?.doctor_id ?? rd?.created_by ?? rd?.doctor ?? null
if (maybeId) { if (maybeId) {
@ -142,11 +156,18 @@ export default function LaudoPage() {
} }
} }
// Extrair nome do paciente
let patientName = ''
if (patient) {
patientName = patient.full_name || patient.name || ''
}
// Montar HTML do documento // Montar HTML do documento
element.innerHTML = ` element.innerHTML = `
<div style="border-bottom: 2px solid #3b82f6; padding-bottom: 10px; margin-bottom: 20px;"> <div style="border-bottom: 2px solid #3b82f6; padding-bottom: 10px; margin-bottom: 20px;">
<h1 style="text-align: center; font-size: 24px; font-weight: bold; color: #1f2937; margin: 0;">RELATÓRIO MÉDICO</h1> <h1 style="text-align: center; font-size: 24px; font-weight: bold; color: #1f2937; margin: 0;">RELATÓRIO MÉDICO</h1>
<p style="text-align: center; font-size: 10px; color: #6b7280; margin: 5px 0;">Data: ${reportDate}</p> <p style="text-align: center; font-size: 10px; color: #6b7280; margin: 5px 0;">Data: ${reportDate}</p>
${patientName ? `<p style="text-align: center; font-size: 10px; color: #6b7280; margin: 5px 0;">Paciente: ${patientName}</p>` : ''}
${doctorName ? `<p style="text-align: center; font-size: 10px; color: #6b7280; margin: 5px 0;">Profissional: ${doctorName}</p>` : ''} ${doctorName ? `<p style="text-align: center; font-size: 10px; color: #6b7280; margin: 5px 0;">Profissional: ${doctorName}</p>` : ''}
</div> </div>
@ -386,6 +407,16 @@ export default function LaudoPage() {
: 'bg-slate-50 border-slate-200' : 'bg-slate-50 border-slate-200'
}`}> }`}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 md:gap-6 text-xs sm:text-sm"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 md:gap-6 text-xs sm:text-sm">
{patient && (
<div>
<label className={`text-xs uppercase font-semibold tracking-wide block mb-1.5 sm:mb-2 ${
isDark ? 'text-slate-400' : 'text-slate-600'
}`}>Paciente</label>
<p className={`text-base sm:text-lg font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
{patient.full_name || patient.name || 'N/A'}
</p>
</div>
)}
{cid && ( {cid && (
<div> <div>
<label className={`text-xs uppercase font-semibold tracking-wide block mb-1.5 sm:mb-2 ${ <label className={`text-xs uppercase font-semibold tracking-wide block mb-1.5 sm:mb-2 ${

View File

@ -1500,17 +1500,8 @@ const ProfissionalPage = () => {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={async () => { onClick={() => {
try { router.push(`/laudos/${laudo.id}`);
const full = (laudo?.id || laudo?.order_number) ? await loadReportById(String(laudo?.id ?? laudo?.order_number)) : laudo;
await ensurePaciente(full);
setLaudoSelecionado(full);
setIsViewing(true);
} catch (e) {
// fallback
setLaudoSelecionado(laudo);
setIsViewing(true);
}
}} }}
className="flex items-center gap-1 hover:bg-primary! hover:text-white! transition-colors" className="flex items-center gap-1 hover:bg-primary! hover:text-white! transition-colors"
> >
@ -1521,8 +1512,7 @@ const ProfissionalPage = () => {
variant="default" variant="default"
size="sm" size="sm"
onClick={() => { onClick={() => {
setPatientForLaudo(laudo); router.push(`/laudos/${laudo.id}/editar`);
setIsEditingLaudoForPatient(true);
}} }}
className="flex items-center gap-1 bg-green-600 hover:bg-green-700 text-white" className="flex items-center gap-1 bg-green-600 hover:bg-green-700 text-white"
title="Editar laudo para este paciente" title="Editar laudo para este paciente"
@ -1571,8 +1561,7 @@ const ProfissionalPage = () => {
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => { onClick={() => {
setLaudoSelecionado(laudo); router.push(`/laudos/${laudo.id}`);
setIsViewing(true);
}} }}
className="flex items-center gap-1" className="flex items-center gap-1"
> >
@ -1582,8 +1571,7 @@ const ProfissionalPage = () => {
variant="default" variant="default"
size="sm" size="sm"
onClick={() => { onClick={() => {
setPatientForLaudo(laudo); router.push(`/laudos/${laudo.id}/editar`);
setIsEditingLaudoForPatient(true);
}} }}
className="flex items-center gap-1 bg-green-600 hover:bg-green-700 text-white" className="flex items-center gap-1 bg-green-600 hover:bg-green-700 text-white"
title="Editar laudo" title="Editar laudo"