@omniradiology/omnirad 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +438 -0
- package/app/api/ai-config/route.ts +131 -0
- package/app/api/ai-config/test/route.ts +49 -0
- package/app/api/auth/auto-login/route.ts +66 -0
- package/app/api/auth/check/route.ts +17 -0
- package/app/api/auth/login/route.ts +72 -0
- package/app/api/auth/logout/route.ts +25 -0
- package/app/api/auth/me/route.ts +75 -0
- package/app/api/auth/password/route.ts +49 -0
- package/app/api/auth/setup/route.ts +63 -0
- package/app/api/auth/users/route.ts +100 -0
- package/app/api/auth/wipe/route.ts +27 -0
- package/app/api/compliance/anonymize/patient/[id]/route.ts +104 -0
- package/app/api/compliance/audit/route.ts +110 -0
- package/app/api/compliance/export/patient/[id]/route.ts +108 -0
- package/app/api/compliance/restrict/patient/[id]/route.ts +59 -0
- package/app/api/compliance/settings/route.ts +93 -0
- package/app/api/copilot/annotate/route.ts +94 -0
- package/app/api/copilot/chat/route.ts +238 -0
- package/app/api/copilot/history/route.ts +95 -0
- package/app/api/copilot/reports/route.ts +81 -0
- package/app/api/fhir/Bundle/report/[id]/route.ts +85 -0
- package/app/api/fhir/DiagnosticReport/[id]/route.ts +45 -0
- package/app/api/fhir/ImagingStudy/[id]/route.ts +57 -0
- package/app/api/fhir/Patient/[id]/route.ts +26 -0
- package/app/api/fhir/ServiceRequest/route.ts +85 -0
- package/app/api/fhir/config/route.ts +102 -0
- package/app/api/fhir/config/test-connection/route.ts +49 -0
- package/app/api/fhir/metadata/route.ts +51 -0
- package/app/api/pacs/metadata/route.ts +32 -0
- package/app/api/pacs/qido/instances/route.ts +39 -0
- package/app/api/pacs/qido/series/route.ts +38 -0
- package/app/api/pacs/qido/studies/route.ts +37 -0
- package/app/api/pacs/test/route.ts +30 -0
- package/app/api/pacs/wado/render/route.ts +51 -0
- package/app/api/patients/[id]/reports/route.ts +18 -0
- package/app/api/patients/[id]/route.ts +43 -0
- package/app/api/patients/merge/route.ts +57 -0
- package/app/api/patients/route.ts +67 -0
- package/app/api/patients/search/route.ts +25 -0
- package/app/api/reports/[id]/route.ts +84 -0
- package/app/api/reports/[id]/status/route.ts +87 -0
- package/app/api/reports/clear/route.ts +16 -0
- package/app/api/reports/route.ts +112 -0
- package/app/api/segmentation-config/route.ts +238 -0
- package/app/api/settings/route.ts +245 -0
- package/app/api/settings/test-supabase/route.ts +103 -0
- package/app/api/upload/route.ts +48 -0
- package/app/copilot/page.tsx +30 -0
- package/app/globals.css +141 -0
- package/app/history/page.tsx +242 -0
- package/app/icon.svg +3 -0
- package/app/layout.tsx +47 -0
- package/app/login/page.tsx +175 -0
- package/app/pacs/page.tsx +78 -0
- package/app/page.tsx +125 -0
- package/app/patients/[id]/page.tsx +315 -0
- package/app/patients/page.tsx +110 -0
- package/app/profile/page.tsx +208 -0
- package/app/reports/page.tsx +432 -0
- package/app/settings/page.tsx +454 -0
- package/app/setup/page.tsx +199 -0
- package/components/admin/AuditLogTable.tsx +293 -0
- package/components/copilot/ActivityIndicator.tsx +215 -0
- package/components/copilot/ChatHistoryPanel.tsx +140 -0
- package/components/copilot/ChatMessage.tsx +251 -0
- package/components/copilot/ClickableReference.tsx +40 -0
- package/components/copilot/CopilotCornerstoneViewer.tsx +562 -0
- package/components/copilot/CopilotPanel.tsx +311 -0
- package/components/copilot/FindingsList.tsx +75 -0
- package/components/copilot/ViewerPanel.tsx +460 -0
- package/components/copilot/WorkspaceLayout.tsx +398 -0
- package/components/dashboard/AIConfigPanel.tsx +339 -0
- package/components/dashboard/AppearancePanel.tsx +491 -0
- package/components/dashboard/ApprovalModal.tsx +163 -0
- package/components/dashboard/CollaborationPanel.tsx +134 -0
- package/components/dashboard/CopilotConfigPanel.tsx +337 -0
- package/components/dashboard/DicomViewer.tsx +645 -0
- package/components/dashboard/FhirIntegrationPanel.tsx +331 -0
- package/components/dashboard/FullReportOverlay.tsx +269 -0
- package/components/dashboard/ImageViewer.tsx +541 -0
- package/components/dashboard/PatientForm.tsx +597 -0
- package/components/dashboard/RejectionModal.tsx +74 -0
- package/components/dashboard/ReportEditor.tsx +160 -0
- package/components/dashboard/ReportTemplates.tsx +729 -0
- package/components/dashboard/ReportView.tsx +539 -0
- package/components/dashboard/SegmentationConfigPanel.tsx +490 -0
- package/components/dashboard/StudyPlaceholder.tsx +17 -0
- package/components/dashboard/SupabaseIntegrationPanel.tsx +345 -0
- package/components/dashboard/UserManagementPanel.tsx +272 -0
- package/components/layout/ClientLayout.tsx +39 -0
- package/components/layout/Header.tsx +20 -0
- package/components/layout/Sidebar.tsx +119 -0
- package/components/pacs/PacsImageViewerModal.tsx +121 -0
- package/components/pacs/PacsSearchFilters.tsx +117 -0
- package/components/pacs/PacsSeriesViewer.tsx +190 -0
- package/components/pacs/PacsStudyTable.tsx +113 -0
- package/components/patients/patient-card.tsx +117 -0
- package/components/patients/patient-header.tsx +122 -0
- package/components/patients/patient-search.tsx +137 -0
- package/components/patients/patient-timeline.tsx +153 -0
- package/components/settings/ComplianceSettingsPanel.tsx +278 -0
- package/components/settings/SecurityPanel.tsx +418 -0
- package/components/ui/badge.tsx +19 -0
- package/components/ui/basic.tsx +156 -0
- package/db/index.ts +350 -0
- package/db/migrations/0000_odd_quasimodo.sql +117 -0
- package/db/migrations/meta/0000_snapshot.json +778 -0
- package/db/migrations/meta/_journal.json +13 -0
- package/db/schema.ts +239 -0
- package/drizzle.config.ts +10 -0
- package/lib/api.ts +689 -0
- package/lib/auth.ts +22 -0
- package/lib/copilot/action-executor.ts +94 -0
- package/lib/copilot/action-types.ts +72 -0
- package/lib/copilot/coordinate-mapper.ts +84 -0
- package/lib/dicomImageExtractor.ts +103 -0
- package/lib/dicomMetadataParser.ts +111 -0
- package/lib/fhir/client.ts +25 -0
- package/lib/fhir/constants.ts +21 -0
- package/lib/fhir/diagnostic-report.ts +88 -0
- package/lib/fhir/helpers.ts +73 -0
- package/lib/fhir/imaging-study.ts +49 -0
- package/lib/fhir/patient.ts +55 -0
- package/lib/fhir/service-request.ts +85 -0
- package/lib/fhir.ts +6 -0
- package/lib/pacs/dicom-utils.ts +72 -0
- package/lib/pacs/dicomweb.ts +72 -0
- package/lib/pacs/server-utils.ts +37 -0
- package/lib/patients.ts +25 -0
- package/lib/pdfHelper.ts +119 -0
- package/lib/reportHtmlGenerator.ts +581 -0
- package/lib/security/audit.ts +180 -0
- package/lib/security/authz.ts +246 -0
- package/lib/security/phi-redaction.ts +156 -0
- package/lib/security/rate-limit.ts +106 -0
- package/lib/security/secrets.ts +179 -0
- package/lib/supabase.ts +72 -0
- package/lib/utils.ts +6 -0
- package/next.config.ts +35 -0
- package/package.json +76 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo.svg +8 -0
- package/public/next.svg +1 -0
- package/public/omnirad-favicon.svg +8 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +34 -0
- package/types/copilot-viewer.ts +155 -0
- package/types/copilot.ts +105 -0
- package/types/fhir.ts +21 -0
- package/types/html2pdf.d.ts +20 -0
- package/types/index.ts +139 -0
- package/types/pacs.ts +41 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ReportData } from "@/types";
|
|
3
|
+
import { CheckCircle, XCircle } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
interface ReportTemplateProps {
|
|
6
|
+
report: ReportData;
|
|
7
|
+
logoUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* PDF-Optimized Standard Template
|
|
12
|
+
*
|
|
13
|
+
* This component uses a strict legacy HTML/CSS approach (Tables, inline styles)
|
|
14
|
+
* to ensure 100% compatibility with html2pdf.js / html2canvas.
|
|
15
|
+
* It avoids CSS Grid, Flexbox, and modern CSS variables.
|
|
16
|
+
*/
|
|
17
|
+
export const PdfStandardTemplate: React.FC<ReportTemplateProps> = ({ report, logoUrl }) => {
|
|
18
|
+
// Helper for safe styles
|
|
19
|
+
const styles = {
|
|
20
|
+
container: {
|
|
21
|
+
width: '210mm',
|
|
22
|
+
minHeight: '297mm',
|
|
23
|
+
backgroundColor: '#ffffff',
|
|
24
|
+
color: '#000000',
|
|
25
|
+
fontFamily: 'Arial, sans-serif',
|
|
26
|
+
fontSize: '12px',
|
|
27
|
+
lineHeight: '1.5',
|
|
28
|
+
padding: '40px',
|
|
29
|
+
boxSizing: 'border-box' as const,
|
|
30
|
+
position: 'relative' as const
|
|
31
|
+
},
|
|
32
|
+
headerTable: {
|
|
33
|
+
width: '100%',
|
|
34
|
+
marginBottom: '20px',
|
|
35
|
+
borderBottom: '4px solid #333333',
|
|
36
|
+
paddingBottom: '20px'
|
|
37
|
+
},
|
|
38
|
+
title: {
|
|
39
|
+
fontSize: '24px',
|
|
40
|
+
fontWeight: 'bold',
|
|
41
|
+
color: '#333333',
|
|
42
|
+
marginBottom: '4px',
|
|
43
|
+
textTransform: 'uppercase' as const
|
|
44
|
+
},
|
|
45
|
+
subtitle: {
|
|
46
|
+
fontSize: '14px',
|
|
47
|
+
color: '#666666',
|
|
48
|
+
fontStyle: 'italic'
|
|
49
|
+
},
|
|
50
|
+
infoLabel: {
|
|
51
|
+
fontSize: '10px',
|
|
52
|
+
fontWeight: 'bold',
|
|
53
|
+
color: '#888888',
|
|
54
|
+
textTransform: 'uppercase' as const
|
|
55
|
+
},
|
|
56
|
+
infoValue: {
|
|
57
|
+
fontSize: '14px',
|
|
58
|
+
fontWeight: 'bold',
|
|
59
|
+
color: '#333333'
|
|
60
|
+
},
|
|
61
|
+
sectionHeader: {
|
|
62
|
+
fontSize: '14px',
|
|
63
|
+
fontWeight: 'bold',
|
|
64
|
+
textTransform: 'uppercase' as const,
|
|
65
|
+
color: '#555555',
|
|
66
|
+
borderBottom: '1px solid #cccccc',
|
|
67
|
+
paddingBottom: '4px',
|
|
68
|
+
marginTop: '20px',
|
|
69
|
+
marginBottom: '10px'
|
|
70
|
+
},
|
|
71
|
+
detailsTable: {
|
|
72
|
+
width: '100%',
|
|
73
|
+
borderCollapse: 'collapse' as const,
|
|
74
|
+
marginBottom: '20px',
|
|
75
|
+
border: '1px solid #eeeeee'
|
|
76
|
+
},
|
|
77
|
+
detailsCell: {
|
|
78
|
+
padding: '8px',
|
|
79
|
+
border: '1px solid #eeeeee',
|
|
80
|
+
verticalAlign: 'top'
|
|
81
|
+
},
|
|
82
|
+
detailsLabel: {
|
|
83
|
+
fontWeight: 'bold',
|
|
84
|
+
color: '#555555',
|
|
85
|
+
width: '120px'
|
|
86
|
+
},
|
|
87
|
+
findingRow: {
|
|
88
|
+
marginBottom: '15px',
|
|
89
|
+
pageBreakInside: 'avoid' as const
|
|
90
|
+
},
|
|
91
|
+
findingRegion: {
|
|
92
|
+
fontWeight: 'bold',
|
|
93
|
+
fontSize: '13px',
|
|
94
|
+
borderBottom: '1px solid #eeeeee',
|
|
95
|
+
display: 'inline-block',
|
|
96
|
+
marginBottom: '4px'
|
|
97
|
+
},
|
|
98
|
+
tag: {
|
|
99
|
+
display: 'inline-block',
|
|
100
|
+
padding: '2px 6px',
|
|
101
|
+
fontSize: '10px',
|
|
102
|
+
fontWeight: 'bold',
|
|
103
|
+
borderRadius: '4px',
|
|
104
|
+
marginLeft: '10px',
|
|
105
|
+
verticalAlign: 'middle'
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div id="report-pdf-standard" style={styles.container}>
|
|
111
|
+
{/* Header */}
|
|
112
|
+
<table style={styles.headerTable}>
|
|
113
|
+
<tbody>
|
|
114
|
+
<tr>
|
|
115
|
+
{logoUrl && (
|
|
116
|
+
<td style={{ verticalAlign: 'top', paddingRight: '12px', width: '60px' }}>
|
|
117
|
+
<img src={logoUrl} alt="Hospital Logo" style={{ height: '50px', width: 'auto', objectFit: 'contain' }} />
|
|
118
|
+
</td>
|
|
119
|
+
)}
|
|
120
|
+
<td style={{ verticalAlign: 'top' }}>
|
|
121
|
+
<div style={styles.title}>{report.report_header.hospital_name}</div>
|
|
122
|
+
<div style={styles.subtitle}>{report.report_header.department}</div>
|
|
123
|
+
</td>
|
|
124
|
+
<td style={{ textAlign: 'right', verticalAlign: 'top' }}>
|
|
125
|
+
<div style={styles.infoLabel}>Report ID</div>
|
|
126
|
+
<div style={styles.infoValue}>{report.report_header.report_id}</div>
|
|
127
|
+
<div style={{ ...styles.infoLabel, marginTop: '8px' }}>Date</div>
|
|
128
|
+
<div style={{ fontSize: '12px', color: '#666666' }}>
|
|
129
|
+
{new Date(report.report_header.report_date).toLocaleString()}
|
|
130
|
+
</div>
|
|
131
|
+
</td>
|
|
132
|
+
</tr>
|
|
133
|
+
</tbody>
|
|
134
|
+
</table>
|
|
135
|
+
|
|
136
|
+
{/* Patient & Exam Details */}
|
|
137
|
+
<table style={styles.detailsTable}>
|
|
138
|
+
<tbody>
|
|
139
|
+
<tr>
|
|
140
|
+
<td style={{ ...styles.detailsCell, width: '50%', backgroundColor: '#f9f9f9' }}>
|
|
141
|
+
<table style={{ width: '100%' }}>
|
|
142
|
+
<tbody>
|
|
143
|
+
<tr>
|
|
144
|
+
<td style={styles.detailsLabel}>Patient Name:</td>
|
|
145
|
+
<td style={{ fontWeight: 'bold' }}>{report.patient.name}</td>
|
|
146
|
+
</tr>
|
|
147
|
+
{report.patient.patient_id && (
|
|
148
|
+
<tr>
|
|
149
|
+
<td style={styles.detailsLabel}>Patient ID:</td>
|
|
150
|
+
<td>{report.patient.patient_id}</td>
|
|
151
|
+
</tr>
|
|
152
|
+
)}
|
|
153
|
+
<tr>
|
|
154
|
+
<td style={styles.detailsLabel}>Age / Gender:</td>
|
|
155
|
+
<td>{report.patient.age} / {report.patient.gender}</td>
|
|
156
|
+
</tr>
|
|
157
|
+
</tbody>
|
|
158
|
+
</table>
|
|
159
|
+
</td>
|
|
160
|
+
<td style={{ ...styles.detailsCell, width: '50%', backgroundColor: '#ffffff' }}>
|
|
161
|
+
<table style={{ width: '100%' }}>
|
|
162
|
+
<tbody>
|
|
163
|
+
<tr>
|
|
164
|
+
<td style={styles.detailsLabel}>Modality:</td>
|
|
165
|
+
<td>{report.study.modality}</td>
|
|
166
|
+
</tr>
|
|
167
|
+
<tr>
|
|
168
|
+
<td style={styles.detailsLabel}>Exam:</td>
|
|
169
|
+
<td>{report.study.examination}</td>
|
|
170
|
+
</tr>
|
|
171
|
+
</tbody>
|
|
172
|
+
</table>
|
|
173
|
+
</td>
|
|
174
|
+
</tr>
|
|
175
|
+
</tbody>
|
|
176
|
+
</table>
|
|
177
|
+
|
|
178
|
+
{/* Clinical History */}
|
|
179
|
+
<div style={styles.sectionHeader}>Clinical History</div>
|
|
180
|
+
<div style={{ marginBottom: '20px' }}>
|
|
181
|
+
<div style={{ marginBottom: '5px' }}>{report.clinical_information.history}</div>
|
|
182
|
+
<div><span style={{ fontWeight: 'bold' }}>Indication: </span>{report.clinical_information.indication}</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Findings */}
|
|
186
|
+
<div style={styles.sectionHeader}>Findings</div>
|
|
187
|
+
<div style={{ marginBottom: '20px' }}>
|
|
188
|
+
{report.findings.map((finding, idx) => {
|
|
189
|
+
const status = finding.status?.toLowerCase() || 'normal';
|
|
190
|
+
let tagStyle = { ...styles.tag, backgroundColor: '#dcfce7', color: '#166534', border: '1px solid #166534' }; // Green
|
|
191
|
+
let label = 'NORMAL';
|
|
192
|
+
|
|
193
|
+
if (status === 'abnormal') {
|
|
194
|
+
tagStyle = { ...styles.tag, backgroundColor: '#fee2e2', color: '#991b1b', border: '1px solid #991b1b' }; // Red
|
|
195
|
+
label = 'ABNORMAL';
|
|
196
|
+
} else if (status === 'indeterminate') {
|
|
197
|
+
tagStyle = { ...styles.tag, backgroundColor: '#fef9c3', color: '#854d0e', border: '1px solid #854d0e' }; // Yellow
|
|
198
|
+
label = 'INDETERMINATE';
|
|
199
|
+
} else if (status.includes('post')) {
|
|
200
|
+
tagStyle = { ...styles.tag, backgroundColor: '#dbeafe', color: '#1e40af', border: '1px solid #1e40af' }; // Blue
|
|
201
|
+
label = 'POST-PROCEDURAL';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div key={idx} style={styles.findingRow}>
|
|
206
|
+
<div>
|
|
207
|
+
<span style={styles.findingRegion}>{finding.anatomical_region}</span>
|
|
208
|
+
<span style={tagStyle}>{label}</span>
|
|
209
|
+
</div>
|
|
210
|
+
<div style={{ marginTop: '5px', paddingLeft: '5px' }}>
|
|
211
|
+
{finding.observation}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
})}
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* Impression */}
|
|
219
|
+
<div style={styles.sectionHeader}>Impression</div>
|
|
220
|
+
<div style={{ padding: '15px', backgroundColor: '#f8f9fa', borderLeft: '4px solid #333333', marginBottom: '30px' }}>
|
|
221
|
+
<ul style={{ margin: 0, paddingLeft: '20px' }}>
|
|
222
|
+
{report.impression.map((imp, idx) => (
|
|
223
|
+
<li key={idx} style={{ marginBottom: '8px', fontWeight: 'bold', fontSize: '13px' }}>{imp}</li>
|
|
224
|
+
))}
|
|
225
|
+
</ul>
|
|
226
|
+
<div style={{ marginTop: '15px', fontSize: '12px' }}>
|
|
227
|
+
<span style={{ fontWeight: 'bold' }}>Urgency: </span>
|
|
228
|
+
<span style={{ fontWeight: 'bold', color: report.urgency === 'Critical' ? 'red' : 'green' }}>{report.urgency}</span>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Footer / Signatures */}
|
|
233
|
+
<div style={{ marginTop: 'auto', borderTop: '2px solid #333333', paddingTop: '20px' }}>
|
|
234
|
+
<table style={{ width: '100%' }}>
|
|
235
|
+
<tbody>
|
|
236
|
+
<tr>
|
|
237
|
+
<td style={{ width: '60%', verticalAlign: 'bottom' }}>
|
|
238
|
+
<div style={{ fontSize: '10px', color: '#666666', marginBottom: '4px' }}>Prepared by:</div>
|
|
239
|
+
<div style={{ fontSize: '14px', fontWeight: 'bold' }}>{report.report_footer.prepared_by}</div>
|
|
240
|
+
<div style={{ fontSize: '11px', color: '#666666' }}>{report.report_footer.department}</div>
|
|
241
|
+
</td>
|
|
242
|
+
{report.report_footer.report_status === 'Approved' && (
|
|
243
|
+
<td style={{ width: '40%', textAlign: 'right', verticalAlign: 'bottom' }}>
|
|
244
|
+
{report.report_footer.signature && (
|
|
245
|
+
<img src={report.report_footer.signature} style={{ height: '50px', marginBottom: '5px', display: 'inline-block' }} alt="Sig" />
|
|
246
|
+
)}
|
|
247
|
+
<div style={{ fontSize: '10px', color: '#666666', marginBottom: '4px', borderTop: '1px solid #999', paddingTop: '4px', display: 'inline-block', width: '200px' }}>
|
|
248
|
+
Electronically Approved by<br />
|
|
249
|
+
<span style={{ fontSize: '12px', fontWeight: 'bold', color: '#000' }}>{report.report_footer.approved_by}</span>
|
|
250
|
+
</div>
|
|
251
|
+
</td>
|
|
252
|
+
)}
|
|
253
|
+
</tr>
|
|
254
|
+
</tbody>
|
|
255
|
+
</table>
|
|
256
|
+
<div style={{ textAlign: 'center', marginTop: '30px', fontSize: '9px', color: '#999999' }}>
|
|
257
|
+
{report.disclaimer}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export const StandardTemplate: React.FC<ReportTemplateProps> = ({ report, logoUrl }) => {
|
|
265
|
+
// Keep existing StandardTemplate for web view to ensure it looks good on screen
|
|
266
|
+
const urgencyColor = report.urgency === 'Critical' ? '#dc2626' :
|
|
267
|
+
report.urgency === 'Urgent' ? '#ea580c' : '#16a34a';
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div className="max-w-4xl mx-auto bg-white shadow-lg min-h-full border border-gray-200 html2pdf__page-break" id="report-full-content-standard">
|
|
271
|
+
{/* 1. Header Section */}
|
|
272
|
+
<div className="p-10 pb-6 border-b-2 border-gray-800">
|
|
273
|
+
<div className="flex justify-between items-start">
|
|
274
|
+
<div className="flex items-start gap-4">
|
|
275
|
+
{logoUrl && (
|
|
276
|
+
<img src={logoUrl} alt="Hospital Logo" className="h-14 w-auto object-contain" />
|
|
277
|
+
)}
|
|
278
|
+
<div>
|
|
279
|
+
<h1 className="text-4xl font-serif font-bold text-gray-900 tracking-tight uppercase mb-1">
|
|
280
|
+
{report.report_header.hospital_name}
|
|
281
|
+
</h1>
|
|
282
|
+
<p className="text-gray-600 font-medium text-lg font-serif italic">
|
|
283
|
+
{report.report_header.department}
|
|
284
|
+
</p>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
<div className="text-right text-sm text-gray-500 font-medium">
|
|
288
|
+
<p><span className="font-bold text-gray-700">Report ID:</span> {report.report_header.report_id}</p>
|
|
289
|
+
<p><span className="font-bold text-gray-700">Date:</span> {new Date(report.report_header.report_date).toLocaleString()}</p>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{/* 2. Patient & Study Details Box */}
|
|
295
|
+
<div className="px-10 py-8">
|
|
296
|
+
<div className="flex border border-gray-200 rounded-sm overflow-hidden">
|
|
297
|
+
{/* Left Column: Patient */}
|
|
298
|
+
<div className="w-1/2 bg-gray-50 p-4 border-r border-gray-200">
|
|
299
|
+
<table className="w-full text-sm">
|
|
300
|
+
<tbody>
|
|
301
|
+
<tr>
|
|
302
|
+
<td className="font-bold w-[110px] pb-2 align-top" style={{ color: '#374151' }}>Patient Name:</td>
|
|
303
|
+
<td className="font-bold uppercase pb-2 align-top" style={{ color: '#111827' }}>{report.patient.name}</td>
|
|
304
|
+
</tr>
|
|
305
|
+
{report.patient.patient_id && (
|
|
306
|
+
<tr>
|
|
307
|
+
<td className="font-bold pb-2 align-top" style={{ color: '#374151' }}>Patient ID:</td>
|
|
308
|
+
<td className="font-medium pb-2 align-top" style={{ color: '#111827' }}>{report.patient.patient_id}</td>
|
|
309
|
+
</tr>
|
|
310
|
+
)}
|
|
311
|
+
<tr>
|
|
312
|
+
<td className="font-bold pb-2 align-top" style={{ color: '#374151' }}>Age / Gender:</td>
|
|
313
|
+
<td className="font-medium pb-2 align-top" style={{ color: '#111827' }}>{report.patient.age} / {report.patient.gender === 'M' ? 'Male' : 'Female'}</td>
|
|
314
|
+
</tr>
|
|
315
|
+
</tbody>
|
|
316
|
+
</table>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
{/* Right Column: Study */}
|
|
320
|
+
<div className="w-1/2 bg-gray-50 p-4">
|
|
321
|
+
<table className="w-full text-sm">
|
|
322
|
+
<tbody>
|
|
323
|
+
<tr>
|
|
324
|
+
<td className="font-bold w-[100px] pb-2 align-top" style={{ color: '#374151' }}>Modality:</td>
|
|
325
|
+
<td className="font-medium pb-2 align-top" style={{ color: '#111827' }}>{report.study.modality}</td>
|
|
326
|
+
</tr>
|
|
327
|
+
<tr>
|
|
328
|
+
<td className="font-bold pb-2 align-top" style={{ color: '#374151' }}>Indication:</td>
|
|
329
|
+
<td className="font-medium pb-2 align-top" style={{ color: '#111827' }}>{report.clinical_information.indication}</td>
|
|
330
|
+
</tr>
|
|
331
|
+
<tr>
|
|
332
|
+
<td className="font-bold pb-2 align-top" style={{ color: '#374151' }}>Examination:</td>
|
|
333
|
+
<td className="font-medium pb-2 align-top" style={{ color: '#111827' }}>{report.study.examination} {report.study.views ? `- ${report.study.views}` : ''}</td>
|
|
334
|
+
</tr>
|
|
335
|
+
</tbody>
|
|
336
|
+
</table>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
{/* 3. Main Content Body */}
|
|
342
|
+
<div className="px-10 pb-10 space-y-8" style={{ display: 'block' }}>
|
|
343
|
+
|
|
344
|
+
{/* Clinical History */}
|
|
345
|
+
<section>
|
|
346
|
+
<h3 className="text-lg font-serif font-bold border-b pb-1 mb-3 uppercase tracking-wider" style={{ color: '#111827', borderColor: '#d1d5db' }}>
|
|
347
|
+
Clinical History
|
|
348
|
+
</h3>
|
|
349
|
+
<div className="text-base space-y-1" style={{ color: '#1f2937' }}>
|
|
350
|
+
<p>{report.clinical_information.history}</p>
|
|
351
|
+
<p><span className="font-bold">Symptoms: </span>{report.clinical_information.symptoms}</p>
|
|
352
|
+
</div>
|
|
353
|
+
</section>
|
|
354
|
+
|
|
355
|
+
{/* Findings */}
|
|
356
|
+
<section>
|
|
357
|
+
<h3 className="text-lg font-serif font-bold border-b pb-1 mb-4 uppercase tracking-wider" style={{ color: '#111827', borderColor: '#d1d5db' }}>
|
|
358
|
+
Findings
|
|
359
|
+
</h3>
|
|
360
|
+
<div className="space-y-5">
|
|
361
|
+
{report.findings.map((finding, idx) => {
|
|
362
|
+
const status = finding.status?.toLowerCase() || 'normal';
|
|
363
|
+
|
|
364
|
+
// Use simpler inline styles for PDF safety
|
|
365
|
+
let statusColor = '#15803d'; // green-700
|
|
366
|
+
let bgColor = '#dcfce7'; // green-100
|
|
367
|
+
let label = 'NORMAL';
|
|
368
|
+
|
|
369
|
+
if (status === 'abnormal') {
|
|
370
|
+
statusColor = '#b91c1c'; // red-700
|
|
371
|
+
bgColor = '#fee2e2'; // red-100
|
|
372
|
+
label = 'ABNORMAL';
|
|
373
|
+
} else if (status === 'indeterminate') {
|
|
374
|
+
statusColor = '#854d0e'; // yellow-800
|
|
375
|
+
bgColor = '#fef9c3'; // yellow-100
|
|
376
|
+
label = 'INDETERMINATE';
|
|
377
|
+
} else if (status === 'post_procedural' || status === 'post-procedural') {
|
|
378
|
+
statusColor = '#1d4ed8'; // blue-700
|
|
379
|
+
bgColor = '#dbeafe'; // blue-100
|
|
380
|
+
label = 'POST-PROCEDURAL';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
<div key={idx} className="mb-4">
|
|
385
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '4px' }}>
|
|
386
|
+
<span className="font-bold text-base" style={{ color: '#111827', paddingBottom: '2px', lineHeight: '1.2' }}>
|
|
387
|
+
{finding.anatomical_region}
|
|
388
|
+
</span>
|
|
389
|
+
<span style={{
|
|
390
|
+
display: 'inline-flex',
|
|
391
|
+
alignItems: 'center',
|
|
392
|
+
justifyContent: 'center',
|
|
393
|
+
height: '16px',
|
|
394
|
+
padding: '0 8px',
|
|
395
|
+
fontSize: '10px',
|
|
396
|
+
borderRadius: '4px',
|
|
397
|
+
border: `1px solid ${statusColor}`,
|
|
398
|
+
backgroundColor: bgColor,
|
|
399
|
+
color: statusColor,
|
|
400
|
+
fontWeight: 'bold',
|
|
401
|
+
textTransform: 'uppercase',
|
|
402
|
+
letterSpacing: '0.05em',
|
|
403
|
+
lineHeight: '1'
|
|
404
|
+
}}>
|
|
405
|
+
{label}
|
|
406
|
+
</span>
|
|
407
|
+
</div>
|
|
408
|
+
<div className="text-base leading-relaxed pl-1" style={{ color: '#1f2937' }}>
|
|
409
|
+
{finding.observation}
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
);
|
|
413
|
+
})}
|
|
414
|
+
</div>
|
|
415
|
+
</section>
|
|
416
|
+
|
|
417
|
+
{/* Impression */}
|
|
418
|
+
<section>
|
|
419
|
+
<h3 className="text-lg font-serif font-bold border-b pb-1 mb-3 uppercase tracking-wider" style={{ color: '#111827', borderColor: '#d1d5db' }}>
|
|
420
|
+
Impression
|
|
421
|
+
</h3>
|
|
422
|
+
<ul className="list-disc list-outside ml-4 space-y-2">
|
|
423
|
+
{report.impression.map((imp, idx) => (
|
|
424
|
+
<li key={idx} className="font-bold text-lg leading-relaxed pl-2" style={{ color: '#111827' }}>
|
|
425
|
+
{imp}
|
|
426
|
+
</li>
|
|
427
|
+
))}
|
|
428
|
+
</ul>
|
|
429
|
+
|
|
430
|
+
{/* Urgency Inline */}
|
|
431
|
+
<div className="mt-4 flex items-center gap-2">
|
|
432
|
+
<span className="font-bold uppercase tracking-wider" style={{ color: '#1f2937' }}>Urgency:</span>
|
|
433
|
+
<span className="text-lg font-bold italic" style={{ color: urgencyColor }}>
|
|
434
|
+
{report.urgency}
|
|
435
|
+
</span>
|
|
436
|
+
</div>
|
|
437
|
+
</section>
|
|
438
|
+
|
|
439
|
+
{/* Recommendations */}
|
|
440
|
+
{report.recommendations && report.recommendations.length > 0 && (
|
|
441
|
+
<section>
|
|
442
|
+
<h3 className="text-lg font-serif font-bold border-b pb-1 mb-3 uppercase tracking-wider" style={{ color: '#111827', borderColor: '#d1d5db' }}>
|
|
443
|
+
Recommendations
|
|
444
|
+
</h3>
|
|
445
|
+
<ul className="list-disc list-outside ml-4 space-y-1">
|
|
446
|
+
{report.recommendations.map((rec, idx) => (
|
|
447
|
+
<li key={idx} className="text-base pl-2" style={{ color: '#1f2937' }}>{rec}</li>
|
|
448
|
+
))}
|
|
449
|
+
</ul>
|
|
450
|
+
</section>
|
|
451
|
+
)}
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
{/* 4. Footer Section */}
|
|
455
|
+
<div className="mt-auto px-10 pb-10 pt-6 border-t-2" style={{ borderColor: '#1f2937' }}>
|
|
456
|
+
<div className="flex justify-between items-end">
|
|
457
|
+
{/* Left: Prepared By */}
|
|
458
|
+
<div>
|
|
459
|
+
<p className="text-sm font-medium mb-1" style={{ color: '#111827' }}>Prepared by :</p>
|
|
460
|
+
<p className="font-bold text-lg capitalize" style={{ color: '#111827' }}>{report.report_footer.prepared_by}</p>
|
|
461
|
+
<p className="text-sm text-gray-500" style={{ color: '#6b7280' }}>{report.report_footer.department}</p>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
{/* Right: Approved By + Signature */}
|
|
465
|
+
{report.report_footer.report_status === 'Approved' && report.report_footer.approved_by && (
|
|
466
|
+
<div className="text-right">
|
|
467
|
+
<p className="text-sm font-medium mb-1" style={{ color: '#111827' }}>Approved by:</p>
|
|
468
|
+
<p className="font-bold text-lg capitalize mb-2" style={{ color: '#111827' }}>{report.report_footer.approved_by}</p>
|
|
469
|
+
{report.report_footer.signature ? (
|
|
470
|
+
<img
|
|
471
|
+
src={report.report_footer.signature}
|
|
472
|
+
alt="Signature"
|
|
473
|
+
className="h-16 ml-auto object-contain"
|
|
474
|
+
/>
|
|
475
|
+
) : (
|
|
476
|
+
<div className="h-16 w-32 ml-auto border border-dashed flex items-center justify-center text-xs text-gray-400" style={{ borderColor: '#d1d5db', color: '#9ca3af' }}>
|
|
477
|
+
(Signed)
|
|
478
|
+
</div>
|
|
479
|
+
)}
|
|
480
|
+
</div>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
{/* Disclaimer */}
|
|
485
|
+
<div className="mt-8 text-center border-t pt-4" style={{ borderColor: '#e5e7eb' }}>
|
|
486
|
+
<p className="text-xs font-medium" style={{ color: '#6b7280' }}>
|
|
487
|
+
{report.disclaimer}
|
|
488
|
+
</p>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
export const ModernTemplate: React.FC<ReportTemplateProps> = ({ report, logoUrl }) => {
|
|
496
|
+
const statusColor = report.report_footer.report_status === 'Approved' ? 'bg-green-100 text-green-800' :
|
|
497
|
+
report.report_footer.report_status === 'Rejected' ? 'bg-red-100 text-red-800' : 'bg-blue-100 text-blue-800';
|
|
498
|
+
|
|
499
|
+
return (
|
|
500
|
+
<div className="max-w-4xl mx-auto bg-white shadow-lg min-h-full font-sans html2pdf__page-break" id="report-full-content-modern">
|
|
501
|
+
{/* Header - Blue Bar */}
|
|
502
|
+
<div className="bg-slate-900 text-white p-8">
|
|
503
|
+
<div className="flex justify-between items-center">
|
|
504
|
+
<div className="flex items-center gap-4">
|
|
505
|
+
{logoUrl && (
|
|
506
|
+
<img src={logoUrl} alt="Hospital Logo" className="h-12 w-auto object-contain brightness-0 invert" />
|
|
507
|
+
)}
|
|
508
|
+
<div>
|
|
509
|
+
<h1 className="text-2xl font-bold tracking-wide uppercase">{report.report_header.hospital_name}</h1>
|
|
510
|
+
<p className="text-slate-400 font-medium">{report.report_header.department}</p>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
<div className="text-right">
|
|
514
|
+
<div className="inline-block px-3 py-1 bg-slate-800 rounded border border-slate-700 text-xs font-mono mb-1">
|
|
515
|
+
ID: {report.report_header.report_id}
|
|
516
|
+
</div>
|
|
517
|
+
<p className="text-sm text-slate-400">{new Date(report.report_header.report_date).toLocaleDateString()}</p>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
{/* Patient Info Bar */}
|
|
523
|
+
<div className="bg-slate-100 border-b border-slate-200 px-8 py-4">
|
|
524
|
+
<div className="grid grid-cols-3 gap-6 text-sm">
|
|
525
|
+
<div>
|
|
526
|
+
<p className="text-xs font-bold text-slate-500 uppercase">Patient</p>
|
|
527
|
+
<p className="font-bold text-slate-900 text-lg">{report.patient.name}</p>
|
|
528
|
+
<p className="text-slate-600">{report.patient.age}Y • {report.patient.gender === 'M' ? 'Male' : 'Female'}</p>
|
|
529
|
+
{report.patient.patient_id && <p className="text-xs text-slate-500 font-mono">ID: {report.patient.patient_id}</p>}
|
|
530
|
+
</div>
|
|
531
|
+
<div>
|
|
532
|
+
<p className="text-xs font-bold text-slate-500 uppercase">Exam</p>
|
|
533
|
+
<p className="font-semibold text-slate-900">{report.study.modality}</p>
|
|
534
|
+
<p className="text-slate-600">{report.study.examination}</p>
|
|
535
|
+
</div>
|
|
536
|
+
<div>
|
|
537
|
+
<p className="text-xs font-bold text-slate-500 uppercase">Indication</p>
|
|
538
|
+
<p className="font-medium text-slate-900 line-clamp-2">{report.clinical_information.indication}</p>
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<div className="p-8 space-y-8 text-gray-800">
|
|
544
|
+
{/* Findings with Modern Styling */}
|
|
545
|
+
<section>
|
|
546
|
+
<h3 className="text-sm font-bold text-blue-600 uppercase tracking-widest mb-4 flex items-center gap-2">
|
|
547
|
+
<span className="w-8 h-0.5 bg-blue-600"></span>
|
|
548
|
+
Findings
|
|
549
|
+
</h3>
|
|
550
|
+
<div className="grid gap-4">
|
|
551
|
+
{report.findings.map((finding, idx) => {
|
|
552
|
+
const status = finding.status?.toLowerCase() || 'normal';
|
|
553
|
+
let cardStyle = 'bg-white border-slate-100 shadow-sm';
|
|
554
|
+
let textColor = 'text-slate-600';
|
|
555
|
+
let badge = null;
|
|
556
|
+
|
|
557
|
+
if (status === 'abnormal') {
|
|
558
|
+
cardStyle = 'bg-red-50 border-red-100';
|
|
559
|
+
textColor = 'text-red-900';
|
|
560
|
+
badge = <span className="text-[10px] font-bold bg-red-100 text-red-600 px-2 py-0.5 rounded-full uppercase">Abnormal</span>;
|
|
561
|
+
} else if (status === 'indeterminate') {
|
|
562
|
+
cardStyle = 'bg-yellow-50 border-yellow-100';
|
|
563
|
+
textColor = 'text-yellow-900';
|
|
564
|
+
badge = <span className="text-[10px] font-bold bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded-full uppercase">Indeterminate</span>;
|
|
565
|
+
} else if (status === 'post_procedural' || status === 'post-procedural') {
|
|
566
|
+
cardStyle = 'bg-blue-50 border-blue-100';
|
|
567
|
+
textColor = 'text-blue-900';
|
|
568
|
+
badge = <span className="text-[10px] font-bold bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full uppercase">Post-Procedural</span>;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return (
|
|
572
|
+
<div key={idx} className={`p-4 rounded-lg border ${cardStyle}`}>
|
|
573
|
+
<div className="flex justify-between items-start mb-2">
|
|
574
|
+
<span className="font-bold text-slate-900">{finding.anatomical_region}</span>
|
|
575
|
+
{badge}
|
|
576
|
+
</div>
|
|
577
|
+
<p className={`text-sm leading-relaxed ${textColor}`}>{finding.observation}</p>
|
|
578
|
+
</div>
|
|
579
|
+
);
|
|
580
|
+
})}
|
|
581
|
+
</div>
|
|
582
|
+
</section>
|
|
583
|
+
|
|
584
|
+
{/* Impression Box */}
|
|
585
|
+
<section className="bg-slate-50 border-l-4 border-blue-600 p-6 rounded-r-lg">
|
|
586
|
+
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-widest mb-3">Impression</h3>
|
|
587
|
+
<ul className="space-y-3">
|
|
588
|
+
{report.impression.map((imp, idx) => (
|
|
589
|
+
<li key={idx} className="flex gap-3 text-slate-900 font-semibold text-base">
|
|
590
|
+
<span className="text-blue-500 mt-1.5">•</span>
|
|
591
|
+
{imp}
|
|
592
|
+
</li>
|
|
593
|
+
))}
|
|
594
|
+
</ul>
|
|
595
|
+
</section>
|
|
596
|
+
|
|
597
|
+
{/* Signatures Modern */}
|
|
598
|
+
<div className="flex justify-between items-end pt-12 mt-4 border-t border-slate-100">
|
|
599
|
+
<div>
|
|
600
|
+
<p className="text-xs uppercase text-slate-400 font-bold tracking-wider">Report Status</p>
|
|
601
|
+
<span className={`inline-block mt-1 px-3 py-1 rounded-full text-xs font-bold ${statusColor}`}>
|
|
602
|
+
{report.report_footer.report_status}
|
|
603
|
+
</span>
|
|
604
|
+
</div>
|
|
605
|
+
{report.report_footer.approved_by && (
|
|
606
|
+
<div className="text-right">
|
|
607
|
+
{report.report_footer.signature && (
|
|
608
|
+
<img src={report.report_footer.signature} className="h-12 ml-auto mb-2 opacity-80" alt="Signed" />
|
|
609
|
+
)}
|
|
610
|
+
<p className="font-bold text-slate-900">{report.report_footer.approved_by}</p>
|
|
611
|
+
<p className="text-xs text-slate-500 uppercase tracking-wider">Approved Radiologist</p>
|
|
612
|
+
</div>
|
|
613
|
+
)}
|
|
614
|
+
</div>
|
|
615
|
+
</div>
|
|
616
|
+
{/* Footer Strip */}
|
|
617
|
+
<div className="bg-slate-50 py-3 text-center border-t border-slate-200">
|
|
618
|
+
<p className="text-[10px] text-slate-400">{report.disclaimer}</p>
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
export const MinimalTemplate: React.FC<ReportTemplateProps> = ({ report, logoUrl }) => {
|
|
625
|
+
return (
|
|
626
|
+
<div className="max-w-4xl mx-auto bg-white min-h-full font-sans text-sm p-8 html2pdf__page-break" id="report-full-content-minimal">
|
|
627
|
+
{/* Minimal Header */}
|
|
628
|
+
<div className="flex justify-between items-center border-b-2 border-black pb-4 mb-2">
|
|
629
|
+
<div className="flex items-center gap-3">
|
|
630
|
+
{logoUrl && (
|
|
631
|
+
<img src={logoUrl} alt="Hospital Logo" className="h-8 w-auto object-contain" />
|
|
632
|
+
)}
|
|
633
|
+
<div>
|
|
634
|
+
<h1 className="font-bold text-xl uppercase tracking-tighter">{report.report_header.hospital_name}</h1>
|
|
635
|
+
<p className="text-xs uppercase tracking-widest">{report.report_header.department}</p>
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
<div className="text-right text-xs font-mono">
|
|
639
|
+
{report.report_header.report_id} | {new Date(report.report_header.report_date).toLocaleDateString()}
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
|
|
643
|
+
{/* Compact Details Grid */}
|
|
644
|
+
<div className="grid grid-cols-4 gap-4 py-3 border-b border-black mb-6">
|
|
645
|
+
<div>
|
|
646
|
+
<span className="block text-[10px] font-bold uppercase text-gray-500">Patient</span>
|
|
647
|
+
<span className="font-bold block truncate">{report.patient.name}</span>
|
|
648
|
+
{report.patient.patient_id && <span className="block text-[10px] font-mono text-gray-400">ID: {report.patient.patient_id}</span>}
|
|
649
|
+
</div>
|
|
650
|
+
<div>
|
|
651
|
+
<span className="block text-[10px] font-bold uppercase text-gray-500">Details</span>
|
|
652
|
+
<span className="block">{report.patient.age} / {report.patient.gender}</span>
|
|
653
|
+
</div>
|
|
654
|
+
<div>
|
|
655
|
+
<span className="block text-[10px] font-bold uppercase text-gray-500">Exam</span>
|
|
656
|
+
<span className="block truncate">{report.study.modality}</span>
|
|
657
|
+
</div>
|
|
658
|
+
<div>
|
|
659
|
+
<span className="block text-[10px] font-bold uppercase text-gray-500">Indication</span>
|
|
660
|
+
<span className="block truncate">{report.clinical_information.indication}</span>
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
|
|
664
|
+
{/* High Density Content */}
|
|
665
|
+
<div className="space-y-4">
|
|
666
|
+
<div className="grid grid-cols-[120px_1fr] gap-4">
|
|
667
|
+
<h3 className="font-bold text-xs uppercase text-gray-500 pt-1">History</h3>
|
|
668
|
+
<p className="leading-snug">{report.clinical_information.history}. <span className="italic">Symptoms: {report.clinical_information.symptoms}</span></p>
|
|
669
|
+
</div>
|
|
670
|
+
|
|
671
|
+
<div className="grid grid-cols-[120px_1fr] gap-4">
|
|
672
|
+
<h3 className="font-bold text-xs uppercase text-gray-500 pt-1">Findings</h3>
|
|
673
|
+
<div className="space-y-2">
|
|
674
|
+
{report.findings.map((finding, idx) => {
|
|
675
|
+
const status = finding.status?.toLowerCase() || 'normal';
|
|
676
|
+
let badge = null;
|
|
677
|
+
|
|
678
|
+
if (status === 'abnormal') {
|
|
679
|
+
badge = <span className="ml-2 text-[10px] font-bold border border-black px-1">ABNORMAL</span>;
|
|
680
|
+
} else if (status === 'indeterminate') {
|
|
681
|
+
badge = <span className="ml-2 text-[10px] font-bold border border-black px-1 bg-gray-100">INDETERMINATE</span>;
|
|
682
|
+
} else if (status === 'post_procedural' || status === 'post-procedural') {
|
|
683
|
+
badge = <span className="ml-2 text-[10px] font-bold border border-black px-1">POST-PROCEDURAL</span>;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return (
|
|
687
|
+
<div key={idx} className="border-b border-gray-100 last:border-0 pb-1">
|
|
688
|
+
<span className="font-bold mr-2 uppercase text-xs">{finding.anatomical_region}:</span>
|
|
689
|
+
<span className={status === 'abnormal' ? 'font-semibold' : ''}>{finding.observation}</span>
|
|
690
|
+
{badge}
|
|
691
|
+
</div>
|
|
692
|
+
);
|
|
693
|
+
})}
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
|
|
697
|
+
<div className="grid grid-cols-[120px_1fr] gap-4 pt-2">
|
|
698
|
+
<h3 className="font-bold text-xs uppercase text-gray-500 pt-1">Impression</h3>
|
|
699
|
+
<div className="bg-gray-50 p-3">
|
|
700
|
+
<ul className="list-decimal list-inside space-y-1">
|
|
701
|
+
{report.impression.map((imp, idx) => (
|
|
702
|
+
<li key={idx} className="font-bold leading-snug">{imp}</li>
|
|
703
|
+
))}
|
|
704
|
+
</ul>
|
|
705
|
+
<div className="mt-2 pt-2 border-t border-gray-200 text-xs flex justify-between">
|
|
706
|
+
<span>Urgency: <span className="font-bold uppercase">{report.urgency}</span></span>
|
|
707
|
+
</div>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
711
|
+
|
|
712
|
+
{/* Compact Footer */}
|
|
713
|
+
<div className="mt-12 flex justify-between items-end border-t border-black pt-4">
|
|
714
|
+
<div className="text-xs">
|
|
715
|
+
<p className="font-bold uppercase">Prepared By: {report.report_footer.prepared_by}</p>
|
|
716
|
+
</div>
|
|
717
|
+
{report.report_footer.approved_by && (
|
|
718
|
+
<div className="text-right">
|
|
719
|
+
{report.report_footer.signature && <img src={report.report_footer.signature} className="h-8 ml-auto mb-1" />}
|
|
720
|
+
<p className="text-xs font-bold uppercase border-t border-black inline-block min-w-[150px] pt-1 mt-1">
|
|
721
|
+
Approved: {report.report_footer.approved_by}
|
|
722
|
+
</p>
|
|
723
|
+
</div>
|
|
724
|
+
)}
|
|
725
|
+
</div>
|
|
726
|
+
<p className="mt-8 text-[9px] text-gray-400 text-center">{report.disclaimer}</p>
|
|
727
|
+
</div>
|
|
728
|
+
);
|
|
729
|
+
};
|