develop #83

Merged
M-Gabrielly merged 426 commits from develop into main 2025-12-04 04:13:15 +00:00
2 changed files with 390 additions and 90 deletions
Showing only changes of commit 5f902e0899 - Show all commits

View File

@ -0,0 +1,387 @@
'use client'
import { useState, useEffect } from 'react'
import { useRouter, useParams } from 'next/navigation'
import { useTheme } from 'next-themes'
import Image from 'next/image'
import { Button } from '@/components/ui/button'
import { ArrowLeft, Printer, Download, MoreVertical } from 'lucide-react'
import { buscarRelatorioPorId, getDoctorById, buscarMedicosPorIds } from '@/lib/api'
import { ENV_CONFIG } from '@/lib/env-config'
import ProtectedRoute from '@/components/shared/ProtectedRoute'
import { useAuth } from '@/hooks/useAuth'
export default function LaudoPage() {
const router = useRouter()
const params = useParams()
const { user } = useAuth()
const { theme } = useTheme()
const reportId = params.id as string
const isDark = theme === 'dark'
const [report, setReport] = useState<any | null>(null)
const [doctor, setDoctor] = useState<any | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
useEffect(() => {
if (!reportId) return
let mounted = true
async function loadReport() {
try {
setLoading(true)
const reportData = await buscarRelatorioPorId(reportId)
if (!mounted) return
setReport(reportData)
// Load doctor info using the same strategy as paciente/page.tsx
const rd = reportData as any
const maybeId = rd?.doctor_id ?? rd?.created_by ?? rd?.doctor ?? null
if (maybeId) {
try {
// First try: buscarMedicosPorIds
let doctors = await buscarMedicosPorIds([maybeId]).catch(() => [])
if (!doctors || doctors.length === 0) {
// Second try: getDoctorById
const doc = await getDoctorById(String(maybeId)).catch(() => null)
if (doc) doctors = [doc]
}
if (!doctors || doctors.length === 0) {
// Third try: direct REST with user_id filter
const token = (typeof window !== 'undefined')
? (localStorage.getItem('auth_token') || localStorage.getItem('token') ||
sessionStorage.getItem('auth_token') || sessionStorage.getItem('token'))
: null
const headers: Record<string,string> = {
apikey: (ENV_CONFIG as any).SUPABASE_ANON_KEY,
Accept: 'application/json'
}
if (token) headers.Authorization = `Bearer ${token}`
const url = `${(ENV_CONFIG as any).SUPABASE_URL}/rest/v1/doctors?user_id=eq.${encodeURIComponent(String(maybeId))}&limit=1`
const res = await fetch(url, { method: 'GET', headers })
if (res && res.status < 400) {
const rows = await res.json().catch(() => [])
if (rows && Array.isArray(rows) && rows.length) {
doctors = rows
}
}
}
if (mounted && doctors && doctors.length > 0) {
setDoctor(doctors[0])
}
} catch (e) {
console.warn('Erro ao carregar dados do profissional:', e)
}
}
} catch (err) {
if (mounted) setError('Erro ao carregar o laudo.')
console.error(err)
} finally {
if (mounted) setLoading(false)
}
}
loadReport()
return () => { mounted = false }
}, [reportId])
const handlePrint = () => {
window.print()
}
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>
)
}
if (error || !report) {
return (
<ProtectedRoute>
<div className="flex flex-col items-center justify-center min-h-screen bg-background">
<div className="text-lg text-red-500 mb-4">{error || 'Laudo não encontrado.'}</div>
<Button onClick={() => router.back()} variant="outline">
<ArrowLeft className="w-4 h-4 mr-2" />
Voltar
</Button>
</div>
</ProtectedRoute>
)
}
// Extract fields with fallbacks
const reportDate = new Date(report.report_date || report.created_at || Date.now()).toLocaleDateString('pt-BR')
const cid = report.cid ?? report.cid_code ?? report.cidCode ?? report.cie ?? ''
const exam = report.exam ?? report.exame ?? report.especialidade ?? report.report_type ?? ''
const diagnosis = report.diagnosis ?? report.diagnostico ?? report.diagnosis_text ?? report.diagnostico_text ?? ''
const conclusion = report.conclusion ?? report.conclusao ?? report.conclusion_text ?? report.conclusao_text ?? ''
const notesHtml = report.content_html ?? report.conteudo_html ?? report.contentHtml ?? null
const notesText = report.content ?? report.body ?? report.conteudo ?? report.notes ?? report.observacoes ?? ''
// Extract doctor name with multiple fallbacks
let doctorName = ''
if (doctor) {
doctorName = doctor.full_name || doctor.name || doctor.fullName || doctor.doctor_name || ''
}
if (!doctorName) {
const rd = report as any
const tryKeys = [
'doctor_name', 'doctor_full_name', 'doctorFullName', 'doctorName',
'requested_by_name', 'requested_by', 'requester_name', 'requester',
'created_by_name', 'created_by', 'executante', 'executante_name',
]
for (const k of tryKeys) {
const v = rd[k]
if (v !== undefined && v !== null && String(v).trim() !== '') {
doctorName = String(v)
break
}
}
}
return (
<ProtectedRoute>
<div className={`min-h-screen transition-colors duration-300 ${
isDark
? 'bg-gradient-to-br from-slate-950 to-slate-900'
: 'bg-gradient-to-br from-slate-50 to-slate-100'
}`}>
{/* Header Toolbar */}
<div className={`sticky top-0 z-40 transition-colors duration-300 ${
isDark
? 'bg-slate-800 border-slate-700'
: 'bg-white border-slate-200'
} border-b shadow-md`}>
<div className="flex items-center justify-between px-6 py-4">
{/* Left Section */}
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="icon"
onClick={() => router.back()}
className={`${
isDark
? 'text-slate-300 hover:bg-slate-700 hover:text-white'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<ArrowLeft className="w-5 h-5" />
</Button>
<div className={`h-8 w-px ${isDark ? 'bg-slate-600' : 'bg-slate-300'}`} />
<div>
<p className={`text-xs font-semibold uppercase tracking-wide ${
isDark ? 'text-slate-400' : 'text-slate-500'
}`}>Laudo Médico</p>
<p className={`text-lg font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>
{doctorName || 'Profissional'}
</p>
</div>
</div>
{/* Right Section */}
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={handlePrint}
title="Imprimir"
className={`${
isDark
? 'text-slate-300 hover:bg-slate-700 hover:text-white'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<Printer className="w-5 h-5" />
</Button>
<Button
variant="ghost"
size="icon"
title="Mais opções"
className={`${
isDark
? 'text-slate-300 hover:bg-slate-700 hover:text-white'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<MoreVertical className="w-5 h-5" />
</Button>
</div>
</div>
</div>
{/* Main Content Area */}
<div className="flex justify-center py-12 px-4 min-h-[calc(100vh-80px)]">
{/* Document Container */}
<div className={`w-full max-w-4xl transition-colors duration-300 shadow-2xl rounded-xl overflow-hidden ${
isDark ? 'bg-slate-800' : 'bg-white'
}`}>
{/* Document Content */}
<div className="p-16 space-y-8 print:p-0 print:shadow-none">
{/* Title */}
<div className={`text-center mb-12 pb-8 border-b-2 ${
isDark ? 'border-blue-900' : 'border-blue-200'
}`}>
<h1 className={`text-4xl font-bold mb-4 ${isDark ? 'text-white' : 'text-slate-900'}`}>
RELATÓRIO MÉDICO
</h1>
<div className={`text-sm space-y-1 ${isDark ? 'text-slate-300' : 'text-slate-700'}`}>
<p className="font-medium">
<span className={isDark ? 'text-slate-400' : 'text-slate-500'}>Data:</span> {reportDate}
</p>
{doctorName && (
<p className="font-medium">
<span className={isDark ? 'text-slate-400' : 'text-slate-500'}>Profissional:</span>{' '}
<strong className={isDark ? 'text-blue-400' : 'text-blue-700'}>{doctorName}</strong>
</p>
)}
</div>
</div>
{/* Patient/Header Info */}
<div className={`rounded-lg p-6 border transition-colors duration-300 ${
isDark
? 'bg-slate-900 border-slate-700'
: 'bg-slate-50 border-slate-200'
}`}>
<div className="grid grid-cols-2 gap-6 text-sm">
{cid && (
<div>
<label className={`text-xs uppercase font-semibold tracking-wide block mb-2 ${
isDark ? 'text-slate-400' : 'text-slate-600'
}`}>CID</label>
<p className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
{cid}
</p>
</div>
)}
{exam && (
<div>
<label className={`text-xs uppercase font-semibold tracking-wide block mb-2 ${
isDark ? 'text-slate-400' : 'text-slate-600'
}`}>Exame / Tipo</label>
<p className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
{exam}
</p>
</div>
)}
</div>
</div>
{/* Diagnosis Section */}
{diagnosis && (
<div className="space-y-3">
<h2 className={`text-xl font-bold uppercase tracking-wide ${
isDark ? 'text-blue-400' : 'text-blue-700'
}`}>Diagnóstico</h2>
<div className={`whitespace-pre-wrap text-base leading-relaxed rounded-lg p-4 border-l-4 border-blue-500 transition-colors duration-300 ${
isDark
? 'bg-slate-900 text-slate-200'
: 'bg-blue-50 text-slate-800'
}`}>
{diagnosis}
</div>
</div>
)}
{/* Conclusion Section */}
{conclusion && (
<div className="space-y-3">
<h2 className={`text-xl font-bold uppercase tracking-wide ${
isDark ? 'text-blue-400' : 'text-blue-700'
}`}>Conclusão</h2>
<div className={`whitespace-pre-wrap text-base leading-relaxed rounded-lg p-4 border-l-4 border-blue-500 transition-colors duration-300 ${
isDark
? 'bg-slate-900 text-slate-200'
: 'bg-blue-50 text-slate-800'
}`}>
{conclusion}
</div>
</div>
)}
{/* Notes/Content Section */}
{(notesHtml || notesText) && (
<div className="space-y-3">
<h2 className={`text-xl font-bold uppercase tracking-wide ${
isDark ? 'text-blue-400' : 'text-blue-700'
}`}>Notas do Profissional</h2>
{notesHtml ? (
<div
className={`prose prose-sm max-w-none rounded-lg p-4 border-l-4 border-blue-500 transition-colors duration-300 ${
isDark
? 'prose-invert bg-slate-900 text-slate-200'
: 'bg-blue-50 text-slate-800'
}`}
dangerouslySetInnerHTML={{ __html: String(notesHtml) }}
/>
) : (
<div className={`whitespace-pre-wrap text-base leading-relaxed rounded-lg p-4 border-l-4 border-blue-500 transition-colors duration-300 ${
isDark
? 'bg-slate-900 text-slate-200'
: 'bg-blue-50 text-slate-800'
}`}>
{notesText}
</div>
)}
</div>
)}
{/* Signature Section */}
{report.doctor_signature && (
<div className={`pt-8 border-t-2 ${isDark ? 'border-slate-600' : 'border-slate-300'}`}>
<div className="flex flex-col items-center gap-4">
<div className={`rounded-lg p-4 border transition-colors duration-300 ${
isDark
? 'bg-slate-900 border-slate-600'
: 'bg-slate-100 border-slate-300'
}`}>
<Image
src={report.doctor_signature}
alt="Assinatura do profissional"
width={150}
height={100}
className="h-20 w-auto"
/>
</div>
{doctorName && (
<div className="text-center">
<p className={`text-sm font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>
{doctorName}
</p>
{doctor?.crm && (
<p className={`text-xs mt-1 ${isDark ? 'text-slate-400' : 'text-slate-600'}`}>
CRM: {doctor.crm}
</p>
)}
</div>
)}
</div>
</div>
)}
{/* Footer */}
<div className={`pt-8 border-t-2 text-center space-y-2 ${isDark ? 'border-slate-600' : 'border-slate-300'}`}>
<p className={`text-xs ${isDark ? 'text-slate-400' : 'text-slate-600'}`}>
Documento gerado em {new Date().toLocaleString('pt-BR')}
</p>
</div>
</div>
</div>
</div>
</div>
</ProtectedRoute>
)
}

View File

@ -932,6 +932,7 @@ export default function PacientePage() {
const [selectedReport, setSelectedReport] = useState<any | null>(null) const [selectedReport, setSelectedReport] = useState<any | null>(null)
function ExamesLaudos() { function ExamesLaudos() {
const router = useRouter()
const [reports, setReports] = useState<any[] | null>(null) const [reports, setReports] = useState<any[] | null>(null)
const [loadingReports, setLoadingReports] = useState(false) const [loadingReports, setLoadingReports] = useState(false)
const [reportsError, setReportsError] = useState<string | null>(null) const [reportsError, setReportsError] = useState<string | null>(null)
@ -1426,7 +1427,7 @@ export default function PacientePage() {
<div className="text-base md:text-base text-muted-foreground mt-1">Data: {new Date(r.report_date || r.created_at || Date.now()).toLocaleDateString('pt-BR')}</div> <div className="text-base md:text-base text-muted-foreground mt-1">Data: {new Date(r.report_date || r.created_at || Date.now()).toLocaleDateString('pt-BR')}</div>
</div> </div>
<div className="flex gap-2 mt-2 md:mt-0"> <div className="flex gap-2 mt-2 md:mt-0">
<Button variant="outline" className="hover:bg-primary! hover:text-white! transition-colors" onClick={async () => { setSelectedReport(r); }}>{strings.visualizarLaudo}</Button> <Button variant="outline" className="hover:bg-primary! hover:text-white! transition-colors" onClick={async () => { router.push(`/laudos/${r.id}`); }}>{strings.visualizarLaudo}</Button>
<Button variant="secondary" className="hover:bg-primary! hover:text-white! transition-colors" onClick={async () => { try { await navigator.clipboard.writeText(JSON.stringify(r)); setToast({ type: 'success', msg: 'Laudo copiado.' }) } catch { setToast({ type: 'error', msg: 'Falha ao copiar.' }) } }}>{strings.compartilhar}</Button> <Button variant="secondary" className="hover:bg-primary! hover:text-white! transition-colors" onClick={async () => { try { await navigator.clipboard.writeText(JSON.stringify(r)); setToast({ type: 'success', msg: 'Laudo copiado.' }) } catch { setToast({ type: 'error', msg: 'Falha ao copiar.' }) } }}>{strings.compartilhar}</Button>
</div> </div>
</div> </div>
@ -1449,95 +1450,7 @@ export default function PacientePage() {
</section> </section>
{/* Modal removed - now using dedicated page /app/laudos/[id] */}
<Dialog open={!!selectedReport} onOpenChange={open => !open && setSelectedReport(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{selectedReport && (
(() => {
const looksLikeIdStr = (s: any) => {
try {
const hexOnly = String(s || '').replace(/[^0-9a-fA-F]/g, '');
const len = (typeof hexOnly === 'string') ? hexOnly.length : (Number(hexOnly) || 0);
return len >= 8;
} catch { return false; }
};
const maybeId = selectedReport?.doctor_id || selectedReport?.created_by || selectedReport?.doctor || null;
const derived = reportDoctorName ? reportTitle(selectedReport, reportDoctorName) : reportTitle(selectedReport);
if (looksLikeIdStr(derived)) {
return <span className="font-semibold text-xl md:text-2xl text-muted-foreground">{strings.carregando}</span>;
}
if (resolvingDoctors && maybeId && !doctorsMap[String(maybeId)]) {
return <span className="font-semibold text-xl md:text-2xl text-muted-foreground">{strings.carregando}</span>;
}
return <span className="font-semibold text-xl md:text-2xl">{derived}</span>;
})()
)}
</DialogTitle>
<DialogDescription className="sr-only">Detalhes do laudo</DialogDescription>
<div className="mt-4 space-y-3 max-h-96 overflow-y-auto">
{selectedReport && (
<>
<div className="text-sm text-muted-foreground">Data: {new Date(selectedReport.report_date || selectedReport.created_at || Date.now()).toLocaleDateString('pt-BR')}</div>
{reportDoctorName && <div className="text-sm text-muted-foreground">Profissional: <strong className="text-foreground">{reportDoctorName}</strong></div>}
{/* Standardized laudo sections */}
{(() => {
const cid = selectedReport.cid ?? selectedReport.cid_code ?? selectedReport.cidCode ?? selectedReport.cie ?? '-';
const exam = selectedReport.exam ?? selectedReport.exame ?? selectedReport.especialidade ?? selectedReport.report_type ?? '-';
const diagnosis = selectedReport.diagnosis ?? selectedReport.diagnostico ?? selectedReport.diagnosis_text ?? selectedReport.diagnostico_text ?? '';
const conclusion = selectedReport.conclusion ?? selectedReport.conclusao ?? selectedReport.conclusion_text ?? selectedReport.conclusao_text ?? '';
const notesHtml = selectedReport.content_html ?? selectedReport.conteudo_html ?? selectedReport.contentHtml ?? null;
const notesText = selectedReport.content ?? selectedReport.body ?? selectedReport.conteudo ?? selectedReport.notes ?? selectedReport.observacoes ?? '';
return (
<div className="space-y-3">
<div>
<div className="text-xs text-muted-foreground">CID</div>
<div className="text-foreground">{cid || '-'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Exame</div>
<div className="text-foreground">{exam || '-'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Diagnóstico</div>
<div className="whitespace-pre-line text-foreground">{diagnosis || '-'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Conclusão</div>
<div className="whitespace-pre-line text-foreground">{conclusion || '-'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Notas do Profissional</div>
{notesHtml ? (
<div className="prose max-w-none p-2 bg-muted rounded" dangerouslySetInnerHTML={{ __html: String(notesHtml) }} />
) : (
<div className="whitespace-pre-line text-foreground p-2 bg-muted rounded">{notesText || '-'}</div>
)}
</div>
</div>
);
})()}
{selectedReport.doctor_signature && (
<div className="mt-4 text-sm text-muted-foreground">Assinatura: <Image src={selectedReport.doctor_signature} alt="assinatura" width={40} height={40} className="inline-block h-10 w-auto" /></div>
)}
</>
)}
</div>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setSelectedReport(null)}
className="transition duration-200 hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground"
>
Fechar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</> </>
) )
} }