fix-report-page

This commit is contained in:
João Gustavo 2025-11-07 00:21:01 -03:00
parent e22ad305c4
commit 4d02b55ce7
4 changed files with 216 additions and 13 deletions

View File

@ -96,6 +96,136 @@ export default function LaudoPage() {
window.print()
}
const handleDownloadPDF = async () => {
if (!report) return
try {
// Para simplificar, vamos usar jsPDF com html2canvas para capturar o conteúdo
const { jsPDF } = await import('jspdf')
const html2canvas = await import('html2canvas').then((m) => m.default)
// Criar um elemento temporário com o conteúdo
const element = document.createElement('div')
element.style.position = 'absolute'
element.style.left = '-9999px'
element.style.width = '210mm' // A4 width
element.style.padding = '20mm'
element.style.backgroundColor = 'white'
element.style.fontFamily = 'Arial, sans-serif'
// Extrair informações
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 notesText = report.content ?? report.body ?? report.conteudo ?? report.notes ?? report.observacoes ?? ''
// Extrair nome do médico
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
}
}
}
// Montar HTML do documento
element.innerHTML = `
<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>
<p style="text-align: center; font-size: 10px; color: #6b7280; margin: 5px 0;">Data: ${reportDate}</p>
${doctorName ? `<p style="text-align: center; font-size: 10px; color: #6b7280; margin: 5px 0;">Profissional: ${doctorName}</p>` : ''}
</div>
<div style="background-color: #f0f9ff; border: 1px solid #bfdbfe; padding: 10px; margin-bottom: 15px;">
<div style="display: flex; gap: 20px;">
${cid ? `<div><p style="font-size: 9px; font-weight: bold; color: #475569; margin: 0 0 5px 0;">CID</p><p style="font-size: 11px; font-weight: bold; color: #1f2937; margin: 0;">${cid}</p></div>` : ''}
${exam ? `<div><p style="font-size: 9px; font-weight: bold; color: #475569; margin: 0 0 5px 0;">EXAME / TIPO</p><p style="font-size: 11px; font-weight: bold; color: #1f2937; margin: 0;">${exam}</p></div>` : ''}
</div>
</div>
${diagnosis ? `
<div style="margin-bottom: 20px;">
<h2 style="font-size: 14px; font-weight: bold; color: #1e40af; margin: 0 0 10px 0;">DIAGNÓSTICO</h2>
<p style="margin-left: 10px; padding-left: 10px; border-left: 2px solid #3b82f6; background-color: #f3f4f6; font-size: 10px; line-height: 1.5; margin: 0;">${diagnosis}</p>
</div>
` : ''}
${conclusion ? `
<div style="margin-bottom: 20px;">
<h2 style="font-size: 14px; font-weight: bold; color: #1e40af; margin: 0 0 10px 0;">CONCLUSÃO</h2>
<p style="margin-left: 10px; padding-left: 10px; border-left: 2px solid #3b82f6; background-color: #f3f4f6; font-size: 10px; line-height: 1.5; margin: 0;">${conclusion}</p>
</div>
` : ''}
${notesText ? `
<div style="margin-bottom: 20px;">
<h2 style="font-size: 14px; font-weight: bold; color: #1e40af; margin: 0 0 10px 0;">NOTAS DO PROFISSIONAL</h2>
<p style="margin-left: 10px; padding-left: 10px; border-left: 2px solid #3b82f6; background-color: #f3f4f6; font-size: 10px; line-height: 1.5; margin: 0;">${notesText}</p>
</div>
` : ''}
<div style="margin-top: 30px; padding-top: 10px; border-top: 1px solid #e5e7eb; font-size: 8px; text-align: center; color: #9ca3af;">
Documento gerado em ${new Date().toLocaleString('pt-BR')}
</div>
`
document.body.appendChild(element)
// Capturar como canvas
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
backgroundColor: '#ffffff',
})
document.body.removeChild(element)
// Converter para PDF
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4',
})
const imgWidth = 210 // A4 width in mm
const pageHeight = 297 // A4 height in mm
const imgHeight = (canvas.height * imgWidth) / canvas.width
let heightLeft = imgHeight
let position = 0
while (heightLeft >= 0) {
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight)
heightLeft -= pageHeight
position -= pageHeight
if (heightLeft > 0) {
pdf.addPage()
}
}
// Download
pdf.save(`laudo-${reportDate}-${doctorName || 'profissional'}.pdf`)
} catch (error) {
console.error('Erro ao gerar PDF:', error)
alert('Erro ao gerar PDF. Tente novamente.')
}
}
if (loading) {
return (
<ProtectedRoute>
@ -158,7 +288,7 @@ export default function LaudoPage() {
: 'bg-gradient-to-br from-slate-50 to-slate-100'
}`}>
{/* Header Toolbar */}
<div className={`sticky top-0 z-40 transition-colors duration-300 ${
<div className={`sticky top-0 z-40 transition-colors duration-300 print:hidden ${
isDark
? 'bg-slate-800 border-slate-700'
: 'bg-white border-slate-200'
@ -221,13 +351,13 @@ export default function LaudoPage() {
</div>
{/* Main Content Area */}
<div className="flex justify-center py-12 px-4 min-h-[calc(100vh-80px)]">
<div className="flex justify-center py-12 px-4 print:py-0 print:px-0 min-h-[calc(100vh-80px)] print:min-h-screen">
{/* Document Container */}
<div className={`w-full max-w-4xl transition-colors duration-300 shadow-2xl rounded-xl overflow-hidden ${
<div className={`w-full max-w-4xl transition-colors duration-300 shadow-2xl rounded-xl overflow-hidden print:shadow-none print:rounded-none print:max-w-full ${
isDark ? 'bg-slate-800' : 'bg-white'
}`}>
{/* Document Content */}
<div className="p-16 space-y-8 print:p-0 print:shadow-none">
<div className="p-16 space-y-8 print:p-12 print:space-y-6">
{/* Title */}
<div className={`text-center mb-12 pb-8 border-b-2 ${

View File

@ -960,8 +960,10 @@ export default function PacientePage() {
const [searchTerm, setSearchTerm] = useState<string>('')
const [remoteMatch, setRemoteMatch] = useState<any | null>(null)
const [searchingRemote, setSearchingRemote] = useState<boolean>(false)
const [sortOrder, setSortOrder] = useState<'newest' | 'oldest' | 'custom'>('newest')
const [filterDate, setFilterDate] = useState<string>('')
// derived filtered list based on search term
// derived filtered list based on search term and date filters
const filteredReports = useMemo(() => {
if (!reports || !Array.isArray(reports)) return []
const qRaw = String(searchTerm || '').trim()
@ -980,8 +982,8 @@ export default function PacientePage() {
return [remoteMatch]
}
if (!q) return reports
return reports.filter((r: any) => {
// Start with all reports or filtered by search
let filtered = !q ? reports : reports.filter((r: any) => {
try {
const id = r.id ? String(r.id).toLowerCase() : ''
const title = String(reportTitle(r) || '').toLowerCase()
@ -1013,8 +1015,38 @@ export default function PacientePage() {
return false
}
})
// Apply date filter if specified
if (filterDate) {
const filterDateObj = new Date(filterDate)
filterDateObj.setHours(0, 0, 0, 0)
filtered = filtered.filter((r: any) => {
const reportDateObj = new Date(r.report_date || r.created_at || Date.now())
reportDateObj.setHours(0, 0, 0, 0)
return reportDateObj.getTime() === filterDateObj.getTime()
})
}
// Apply sorting
const sorted = [...filtered]
if (sortOrder === 'newest') {
sorted.sort((a: any, b: any) => {
const dateA = new Date(a.report_date || a.created_at || 0).getTime()
const dateB = new Date(b.report_date || b.created_at || 0).getTime()
return dateB - dateA // Newest first
})
} else if (sortOrder === 'oldest') {
sorted.sort((a: any, b: any) => {
const dateA = new Date(a.report_date || a.created_at || 0).getTime()
const dateB = new Date(b.report_date || b.created_at || 0).getTime()
return dateA - dateB // Oldest first
})
}
return sorted
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reports, searchTerm, doctorsMap, remoteMatch])
}, [reports, searchTerm, doctorsMap, remoteMatch, sortOrder, filterDate])
// When the search term looks like an id, attempt a direct fetch using the reports API
useEffect(() => {
@ -1404,6 +1436,50 @@ export default function PacientePage() {
<Button variant="ghost" onClick={() => { setSearchTerm(''); setReportsPage(1) }} className="text-xs sm:text-sm w-full sm:w-auto">Limpar</Button>
)}
</div>
{/* Date filter and sort controls */}
<div className="flex flex-col sm:flex-row gap-2 items-stretch sm:items-center flex-wrap">
{/* Sort buttons */}
<div className="flex gap-2 flex-wrap">
<Button
size="sm"
variant={sortOrder === 'newest' ? 'default' : 'outline'}
onClick={() => { setSortOrder('newest'); setReportsPage(1) }}
className="text-xs sm:text-sm"
>
Mais Recente
</Button>
<Button
size="sm"
variant={sortOrder === 'oldest' ? 'default' : 'outline'}
onClick={() => { setSortOrder('oldest'); setReportsPage(1) }}
className="text-xs sm:text-sm"
>
Mais Antigo
</Button>
</div>
{/* Date picker */}
<div className="flex gap-2 items-center">
<input
type="date"
value={filterDate}
onChange={(e) => { setFilterDate(e.target.value); setReportsPage(1) }}
className="text-xs sm:text-sm px-2 sm:px-3 py-1.5 sm:py-2 border border-border rounded bg-background"
/>
{filterDate && (
<Button
size="sm"
variant="ghost"
onClick={() => { setFilterDate(''); setReportsPage(1) }}
className="text-xs sm:text-sm"
>
</Button>
)}
</div>
</div>
{loadingReports ? (
<div className="text-center py-6 sm:py-8 text-xs sm:text-sm text-muted-foreground">{strings.carregando}</div>
) : reportsError ? (

View File

@ -51,6 +51,7 @@
"embla-carousel-react": "latest",
"framer-motion": "^12.23.24",
"geist": "^1.3.1",
"html2canvas": "^1.4.1",
"input-otp": "latest",
"jspdf": "^3.0.3",
"lucide-react": "^0.454.0",
@ -4017,7 +4018,6 @@
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
@ -4386,7 +4386,6 @@
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
@ -6066,7 +6065,6 @@
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
@ -8802,7 +8800,6 @@
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
@ -9214,7 +9211,6 @@
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}

View File

@ -53,6 +53,7 @@
"embla-carousel-react": "latest",
"framer-motion": "^12.23.24",
"geist": "^1.3.1",
"html2canvas": "^1.4.1",
"input-otp": "latest",
"jspdf": "^3.0.3",
"lucide-react": "^0.454.0",