@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.
Files changed (155) hide show
  1. package/README.md +438 -0
  2. package/app/api/ai-config/route.ts +131 -0
  3. package/app/api/ai-config/test/route.ts +49 -0
  4. package/app/api/auth/auto-login/route.ts +66 -0
  5. package/app/api/auth/check/route.ts +17 -0
  6. package/app/api/auth/login/route.ts +72 -0
  7. package/app/api/auth/logout/route.ts +25 -0
  8. package/app/api/auth/me/route.ts +75 -0
  9. package/app/api/auth/password/route.ts +49 -0
  10. package/app/api/auth/setup/route.ts +63 -0
  11. package/app/api/auth/users/route.ts +100 -0
  12. package/app/api/auth/wipe/route.ts +27 -0
  13. package/app/api/compliance/anonymize/patient/[id]/route.ts +104 -0
  14. package/app/api/compliance/audit/route.ts +110 -0
  15. package/app/api/compliance/export/patient/[id]/route.ts +108 -0
  16. package/app/api/compliance/restrict/patient/[id]/route.ts +59 -0
  17. package/app/api/compliance/settings/route.ts +93 -0
  18. package/app/api/copilot/annotate/route.ts +94 -0
  19. package/app/api/copilot/chat/route.ts +238 -0
  20. package/app/api/copilot/history/route.ts +95 -0
  21. package/app/api/copilot/reports/route.ts +81 -0
  22. package/app/api/fhir/Bundle/report/[id]/route.ts +85 -0
  23. package/app/api/fhir/DiagnosticReport/[id]/route.ts +45 -0
  24. package/app/api/fhir/ImagingStudy/[id]/route.ts +57 -0
  25. package/app/api/fhir/Patient/[id]/route.ts +26 -0
  26. package/app/api/fhir/ServiceRequest/route.ts +85 -0
  27. package/app/api/fhir/config/route.ts +102 -0
  28. package/app/api/fhir/config/test-connection/route.ts +49 -0
  29. package/app/api/fhir/metadata/route.ts +51 -0
  30. package/app/api/pacs/metadata/route.ts +32 -0
  31. package/app/api/pacs/qido/instances/route.ts +39 -0
  32. package/app/api/pacs/qido/series/route.ts +38 -0
  33. package/app/api/pacs/qido/studies/route.ts +37 -0
  34. package/app/api/pacs/test/route.ts +30 -0
  35. package/app/api/pacs/wado/render/route.ts +51 -0
  36. package/app/api/patients/[id]/reports/route.ts +18 -0
  37. package/app/api/patients/[id]/route.ts +43 -0
  38. package/app/api/patients/merge/route.ts +57 -0
  39. package/app/api/patients/route.ts +67 -0
  40. package/app/api/patients/search/route.ts +25 -0
  41. package/app/api/reports/[id]/route.ts +84 -0
  42. package/app/api/reports/[id]/status/route.ts +87 -0
  43. package/app/api/reports/clear/route.ts +16 -0
  44. package/app/api/reports/route.ts +112 -0
  45. package/app/api/segmentation-config/route.ts +238 -0
  46. package/app/api/settings/route.ts +245 -0
  47. package/app/api/settings/test-supabase/route.ts +103 -0
  48. package/app/api/upload/route.ts +48 -0
  49. package/app/copilot/page.tsx +30 -0
  50. package/app/globals.css +141 -0
  51. package/app/history/page.tsx +242 -0
  52. package/app/icon.svg +3 -0
  53. package/app/layout.tsx +47 -0
  54. package/app/login/page.tsx +175 -0
  55. package/app/pacs/page.tsx +78 -0
  56. package/app/page.tsx +125 -0
  57. package/app/patients/[id]/page.tsx +315 -0
  58. package/app/patients/page.tsx +110 -0
  59. package/app/profile/page.tsx +208 -0
  60. package/app/reports/page.tsx +432 -0
  61. package/app/settings/page.tsx +454 -0
  62. package/app/setup/page.tsx +199 -0
  63. package/components/admin/AuditLogTable.tsx +293 -0
  64. package/components/copilot/ActivityIndicator.tsx +215 -0
  65. package/components/copilot/ChatHistoryPanel.tsx +140 -0
  66. package/components/copilot/ChatMessage.tsx +251 -0
  67. package/components/copilot/ClickableReference.tsx +40 -0
  68. package/components/copilot/CopilotCornerstoneViewer.tsx +562 -0
  69. package/components/copilot/CopilotPanel.tsx +311 -0
  70. package/components/copilot/FindingsList.tsx +75 -0
  71. package/components/copilot/ViewerPanel.tsx +460 -0
  72. package/components/copilot/WorkspaceLayout.tsx +398 -0
  73. package/components/dashboard/AIConfigPanel.tsx +339 -0
  74. package/components/dashboard/AppearancePanel.tsx +491 -0
  75. package/components/dashboard/ApprovalModal.tsx +163 -0
  76. package/components/dashboard/CollaborationPanel.tsx +134 -0
  77. package/components/dashboard/CopilotConfigPanel.tsx +337 -0
  78. package/components/dashboard/DicomViewer.tsx +645 -0
  79. package/components/dashboard/FhirIntegrationPanel.tsx +331 -0
  80. package/components/dashboard/FullReportOverlay.tsx +269 -0
  81. package/components/dashboard/ImageViewer.tsx +541 -0
  82. package/components/dashboard/PatientForm.tsx +597 -0
  83. package/components/dashboard/RejectionModal.tsx +74 -0
  84. package/components/dashboard/ReportEditor.tsx +160 -0
  85. package/components/dashboard/ReportTemplates.tsx +729 -0
  86. package/components/dashboard/ReportView.tsx +539 -0
  87. package/components/dashboard/SegmentationConfigPanel.tsx +490 -0
  88. package/components/dashboard/StudyPlaceholder.tsx +17 -0
  89. package/components/dashboard/SupabaseIntegrationPanel.tsx +345 -0
  90. package/components/dashboard/UserManagementPanel.tsx +272 -0
  91. package/components/layout/ClientLayout.tsx +39 -0
  92. package/components/layout/Header.tsx +20 -0
  93. package/components/layout/Sidebar.tsx +119 -0
  94. package/components/pacs/PacsImageViewerModal.tsx +121 -0
  95. package/components/pacs/PacsSearchFilters.tsx +117 -0
  96. package/components/pacs/PacsSeriesViewer.tsx +190 -0
  97. package/components/pacs/PacsStudyTable.tsx +113 -0
  98. package/components/patients/patient-card.tsx +117 -0
  99. package/components/patients/patient-header.tsx +122 -0
  100. package/components/patients/patient-search.tsx +137 -0
  101. package/components/patients/patient-timeline.tsx +153 -0
  102. package/components/settings/ComplianceSettingsPanel.tsx +278 -0
  103. package/components/settings/SecurityPanel.tsx +418 -0
  104. package/components/ui/badge.tsx +19 -0
  105. package/components/ui/basic.tsx +156 -0
  106. package/db/index.ts +350 -0
  107. package/db/migrations/0000_odd_quasimodo.sql +117 -0
  108. package/db/migrations/meta/0000_snapshot.json +778 -0
  109. package/db/migrations/meta/_journal.json +13 -0
  110. package/db/schema.ts +239 -0
  111. package/drizzle.config.ts +10 -0
  112. package/lib/api.ts +689 -0
  113. package/lib/auth.ts +22 -0
  114. package/lib/copilot/action-executor.ts +94 -0
  115. package/lib/copilot/action-types.ts +72 -0
  116. package/lib/copilot/coordinate-mapper.ts +84 -0
  117. package/lib/dicomImageExtractor.ts +103 -0
  118. package/lib/dicomMetadataParser.ts +111 -0
  119. package/lib/fhir/client.ts +25 -0
  120. package/lib/fhir/constants.ts +21 -0
  121. package/lib/fhir/diagnostic-report.ts +88 -0
  122. package/lib/fhir/helpers.ts +73 -0
  123. package/lib/fhir/imaging-study.ts +49 -0
  124. package/lib/fhir/patient.ts +55 -0
  125. package/lib/fhir/service-request.ts +85 -0
  126. package/lib/fhir.ts +6 -0
  127. package/lib/pacs/dicom-utils.ts +72 -0
  128. package/lib/pacs/dicomweb.ts +72 -0
  129. package/lib/pacs/server-utils.ts +37 -0
  130. package/lib/patients.ts +25 -0
  131. package/lib/pdfHelper.ts +119 -0
  132. package/lib/reportHtmlGenerator.ts +581 -0
  133. package/lib/security/audit.ts +180 -0
  134. package/lib/security/authz.ts +246 -0
  135. package/lib/security/phi-redaction.ts +156 -0
  136. package/lib/security/rate-limit.ts +106 -0
  137. package/lib/security/secrets.ts +179 -0
  138. package/lib/supabase.ts +72 -0
  139. package/lib/utils.ts +6 -0
  140. package/next.config.ts +35 -0
  141. package/package.json +76 -0
  142. package/public/file.svg +1 -0
  143. package/public/globe.svg +1 -0
  144. package/public/logo.svg +8 -0
  145. package/public/next.svg +1 -0
  146. package/public/omnirad-favicon.svg +8 -0
  147. package/public/vercel.svg +1 -0
  148. package/public/window.svg +1 -0
  149. package/tsconfig.json +34 -0
  150. package/types/copilot-viewer.ts +155 -0
  151. package/types/copilot.ts +105 -0
  152. package/types/fhir.ts +21 -0
  153. package/types/html2pdf.d.ts +20 -0
  154. package/types/index.ts +139 -0
  155. package/types/pacs.ts +41 -0
@@ -0,0 +1,581 @@
1
+ import { ReportData } from "@/types";
2
+
3
+ /**
4
+ * Report HTML Generator - PDF Compatible
5
+ *
6
+ * Generates a pure inline-styled HTML string that matches the selected template.
7
+ *
8
+ * Rules for html2pdf.js compatibility:
9
+ * - NO Tailwind classes, NO CSS variables, NO oklab colors
10
+ * - ALL styles inline with hex colors
11
+ * - Uses tables/flexbox with inline styles for layouts
12
+ */
13
+
14
+ export function generateReportHtml(report: ReportData, template: 'standard' | 'modern' | 'minimal' = 'standard', logoUrl?: string): string {
15
+ switch (template) {
16
+ case 'modern':
17
+ return generateModernHtml(report, logoUrl);
18
+ case 'minimal':
19
+ return generateMinimalHtml(report, logoUrl);
20
+ case 'standard':
21
+ default:
22
+ return generateStandardHtml(report, logoUrl);
23
+ }
24
+ }
25
+
26
+ function generateStandardHtml(report: ReportData, logoUrl?: string): string {
27
+ const formattedDate = new Date(report.report_header.report_date).toLocaleString();
28
+
29
+ // Urgency color
30
+ const urgencyColor = report.urgency === 'Critical' ? '#dc2626' :
31
+ report.urgency === 'Urgent' ? '#ea580c' : '#16a34a';
32
+
33
+ // Build findings HTML
34
+ const findingsHtml = report.findings.map(finding => {
35
+ const status = (finding.status || 'normal').toLowerCase();
36
+ let statusColor = '#15803d';
37
+ let bgColor = '#dcfce7';
38
+ let label = 'NORMAL';
39
+
40
+ if (status === 'abnormal') {
41
+ statusColor = '#b91c1c';
42
+ bgColor = '#fee2e2';
43
+ label = 'ABNORMAL';
44
+ } else if (status === 'indeterminate') {
45
+ statusColor = '#854d0e';
46
+ bgColor = '#fef9c3';
47
+ label = 'INDETERMINATE';
48
+ } else if (status === 'post_procedural' || status === 'post-procedural' || status.includes('post')) {
49
+ statusColor = '#1d4ed8';
50
+ bgColor = '#dbeafe';
51
+ label = 'POST-PROCEDURAL';
52
+ }
53
+
54
+ // Calculate SVG badge width based on text length (approx 6px per char + 12px padding)
55
+ const charWidth = 6;
56
+ const padding = 12;
57
+ const svgWidth = (label.length * charWidth) + padding;
58
+ const svgHeight = 18;
59
+
60
+ // SVG Badge Construction
61
+ const badgeSvg = `
62
+ <svg xmlns="http://www.w3.org/2000/svg" width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}">
63
+ <rect x="0.5" y="0.5" width="${svgWidth - 1}" height="${svgHeight - 1}" rx="4" ry="4" fill="${bgColor}" stroke="${statusColor}" stroke-width="1"/>
64
+ <text x="50%" y="55%" dominant-baseline="middle" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" font-weight="bold" fill="${statusColor}">${label}</text>
65
+ </svg>
66
+ `;
67
+ // Encode SVG for data URI to ensure html2canvas compatibility
68
+ const badgeDataUri = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(badgeSvg)}`;
69
+
70
+ return `
71
+ <div style="margin-bottom: 12px; page-break-inside: avoid;">
72
+ <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 4px;">
73
+ <span style="font-weight: bold; font-size: 13px; color: #111827; line-height: 1.3;">
74
+ ${finding.anatomical_region}
75
+ </span>
76
+ <img src="${badgeDataUri}" alt="${label}" style="display: block; vertical-align: middle; height: ${svgHeight}px; width: ${svgWidth}px;" />
77
+ </div>
78
+ <div style="font-size: 12px; line-height: 1.5; padding-left: 2px; color: #1f2937;">
79
+ ${finding.observation}
80
+ </div>
81
+ </div>
82
+ `;
83
+ }).join('');
84
+
85
+ // Build impressions HTML
86
+ const impressionsHtml = report.impression.map(imp =>
87
+ `<li style="margin-bottom: 4px; font-weight: bold; font-size: 13px; line-height: 1.5; color: #111827; padding-left: 4px;">${imp}</li>`
88
+ ).join('');
89
+
90
+ // Build recommendations HTML
91
+ let recommendationsHtml = '';
92
+ if (report.recommendations && report.recommendations.length > 0) {
93
+ const recItems = report.recommendations.map(rec =>
94
+ `<li style="margin-bottom: 2px; font-size: 12px; padding-left: 4px; color: #1f2937;">${rec}</li>`
95
+ ).join('');
96
+ recommendationsHtml = `
97
+ <div style="margin-top: 16px;">
98
+ <h3 style="font-size: 14px; font-family: Georgia, 'Times New Roman', serif; font-weight: bold; border-bottom: 1px solid #d1d5db; padding-bottom: 2px; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 1px; color: #111827;">
99
+ Recommendations
100
+ </h3>
101
+ <ul style="margin: 0; padding-left: 20px; list-style-type: disc;">
102
+ ${recItems}
103
+ </ul>
104
+ </div>
105
+ `;
106
+ }
107
+
108
+ // Signature / Approval footer
109
+ let approvalHtml = '';
110
+ if (report.report_footer.report_status === 'Approved' && report.report_footer.approved_by) {
111
+ let signatureImg = '';
112
+ if (report.report_footer.signature) {
113
+ signatureImg = `<img src="${report.report_footer.signature}" alt="Signature" style="height: 40px; display: block; margin-left: auto; object-fit: contain; background: #ffffff;" />`;
114
+ } else {
115
+ signatureImg = `<div style="height: 40px; width: 100px; margin-left: auto; border: 1px dashed #d1d5db; text-align: center; line-height: 40px; font-size: 10px; color: #9ca3af;">(Signed)</div>`;
116
+ }
117
+ approvalHtml = `
118
+ <td style="width: 50%; text-align: right; vertical-align: bottom;">
119
+ <p style="font-size: 11px; font-weight: 500; margin: 0 0 2px 0; color: #111827;">Approved by:</p>
120
+ <p style="font-weight: bold; font-size: 14px; text-transform: capitalize; margin: 0 0 4px 0; color: #111827;">${report.report_footer.approved_by}</p>
121
+ ${signatureImg}
122
+ </td>
123
+ `;
124
+ }
125
+
126
+ // Rejection info
127
+ let rejectionHtml = '';
128
+ if (report.report_footer.report_status === 'Rejected' && report.report_footer.rejection_reason) {
129
+ rejectionHtml = `
130
+ <div style="margin-top: 10px; padding: 8px 12px; background-color: #fee2e2; border: 1px solid #dc2626; border-radius: 3px;">
131
+ <span style="font-weight: bold; color: #dc2626; font-size: 11px;">REJECTED: </span>
132
+ <span style="color: #991b1b; font-size: 11px;">${report.report_footer.rejection_reason}</span>
133
+ </div>
134
+ `;
135
+ }
136
+
137
+ // Gender display
138
+ const genderDisplay = report.patient.gender === 'M' ? 'Male' : report.patient.gender === 'F' ? 'Female' : report.patient.gender;
139
+
140
+ // === MAIN TEMPLATE ===
141
+ return `
142
+ <div style="
143
+ max-width: 210mm;
144
+ margin: 0 auto;
145
+ background-color: #ffffff;
146
+ color: #000000;
147
+ font-family: Arial, Helvetica, sans-serif;
148
+ font-size: 12px;
149
+ line-height: 1.4;
150
+ border: 1px solid #e5e7eb;
151
+ ">
152
+ <!-- 1. Header Section -->
153
+ <div style="padding: 24px 28px 16px 28px; border-bottom: 2px solid #1f2937;">
154
+ <table style="width: 100%; border-collapse: collapse;">
155
+ <tr>
156
+ ${logoUrl ? `
157
+ <td style="vertical-align: top; padding-right: 12px; width: 60px;">
158
+ <img src="${logoUrl}" alt="Hospital Logo" style="height: 50px; width: auto; object-fit: contain;" />
159
+ </td>
160
+ ` : ''}
161
+ <td style="vertical-align: top;">
162
+ <h1 style="font-size: 22px; font-family: Georgia, 'Times New Roman', serif; font-weight: bold; color: #111827; text-transform: uppercase; margin: 0 0 2px 0; letter-spacing: -0.3px;">
163
+ ${report.report_header.hospital_name}
164
+ </h1>
165
+ <p style="color: #4b5563; font-weight: 500; font-size: 13px; font-family: Georgia, 'Times New Roman', serif; font-style: italic; margin: 0;">
166
+ ${report.report_header.department}
167
+ </p>
168
+ </td>
169
+ <td style="text-align: right; vertical-align: top; font-size: 11px; color: #6b7280; font-weight: 500;">
170
+ <p style="margin: 0 0 2px 0;"><span style="font-weight: bold; color: #374151;">Report ID:</span> ${report.report_header.report_id}</p>
171
+ <p style="margin: 0;"><span style="font-weight: bold; color: #374151;">Date:</span> ${formattedDate}</p>
172
+ </td>
173
+ </tr>
174
+ </table>
175
+ </div>
176
+
177
+ <!-- 2. Patient & Study Details Box -->
178
+ <div style="padding: 16px 28px;">
179
+ <table style="width: 100%; border-collapse: collapse; border: 1px solid #e5e7eb;">
180
+ <tr>
181
+ <!-- Left Column: Patient -->
182
+ <td style="width: 50%; background-color: #f9fafb; padding: 10px 14px; border-right: 1px solid #e5e7eb; vertical-align: top;">
183
+ <table style="width: 100%; font-size: 11px; border-collapse: collapse;">
184
+ <tr>
185
+ <td style="font-weight: bold; color: #374151; width: 90px; padding-bottom: 4px; vertical-align: top;">Patient Name:</td>
186
+ <td style="font-weight: bold; text-transform: uppercase; color: #111827; padding-bottom: 4px; vertical-align: top;">${report.patient.name}</td>
187
+ </tr>
188
+ <tr>
189
+ <td style="font-weight: bold; color: #374151; padding-bottom: 4px; vertical-align: top;">Age / Gender:</td>
190
+ <td style="font-weight: 500; color: #111827; padding-bottom: 4px; vertical-align: top;">${report.patient.age} / ${genderDisplay}</td>
191
+ </tr>
192
+ </table>
193
+ </td>
194
+ <!-- Right Column: Study -->
195
+ <td style="width: 50%; background-color: #f9fafb; padding: 10px 14px; vertical-align: top;">
196
+ <table style="width: 100%; font-size: 11px; border-collapse: collapse;">
197
+ <tr>
198
+ <td style="font-weight: bold; color: #374151; width: 80px; padding-bottom: 4px; vertical-align: top;">Modality:</td>
199
+ <td style="font-weight: 500; color: #111827; padding-bottom: 4px; vertical-align: top;">${report.study.modality}</td>
200
+ </tr>
201
+ <tr>
202
+ <td style="font-weight: bold; color: #374151; padding-bottom: 4px; vertical-align: top;">Indication:</td>
203
+ <td style="font-weight: 500; color: #111827; padding-bottom: 4px; vertical-align: top;">${report.clinical_information.indication}</td>
204
+ </tr>
205
+ <tr>
206
+ <td style="font-weight: bold; color: #374151; padding-bottom: 4px; vertical-align: top;">Examination:</td>
207
+ <td style="font-weight: 500; color: #111827; padding-bottom: 4px; vertical-align: top;">${report.study.examination}${report.study.views ? ` - ${report.study.views}` : ''}</td>
208
+ </tr>
209
+ </table>
210
+ </td>
211
+ </tr>
212
+ </table>
213
+ </div>
214
+
215
+ <!-- 3. Main Content Body -->
216
+ <div style="padding: 0 28px 20px 28px;">
217
+
218
+ <!-- Clinical History -->
219
+ <div style="margin-bottom: 16px;">
220
+ <h3 style="font-size: 14px; font-family: Georgia, 'Times New Roman', serif; font-weight: bold; border-bottom: 1px solid #d1d5db; padding-bottom: 2px; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 1px; color: #111827;">
221
+ Clinical History
222
+ </h3>
223
+ <div style="font-size: 12px; color: #1f2937;">
224
+ <p style="margin: 0 0 2px 0;">${report.clinical_information.history}</p>
225
+ <p style="margin: 0;"><span style="font-weight: bold;">Symptoms: </span>${report.clinical_information.symptoms}</p>
226
+ </div>
227
+ </div>
228
+
229
+ <!-- Findings -->
230
+ <div style="margin-bottom: 16px;">
231
+ <h3 style="font-size: 14px; font-family: Georgia, 'Times New Roman', serif; font-weight: bold; border-bottom: 1px solid #d1d5db; padding-bottom: 2px; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; color: #111827;">
232
+ Findings
233
+ </h3>
234
+ ${findingsHtml}
235
+ </div>
236
+
237
+ <!-- Impression -->
238
+ <div style="margin-bottom: 16px;">
239
+ <h3 style="font-size: 14px; font-family: Georgia, 'Times New Roman', serif; font-weight: bold; border-bottom: 1px solid #d1d5db; padding-bottom: 2px; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 1px; color: #111827;">
240
+ Impression
241
+ </h3>
242
+ <ul style="margin: 0; padding-left: 20px; list-style-type: disc;">
243
+ ${impressionsHtml}
244
+ </ul>
245
+ <div style="margin-top: 8px;">
246
+ <span style="font-weight: bold; text-transform: uppercase; letter-spacing: 1px; font-size: 12px; color: #1f2937;">Urgency:</span>
247
+ <span style="font-size: 14px; font-weight: bold; font-style: italic; color: ${urgencyColor}; margin-left: 6px;">
248
+ ${report.urgency}
249
+ </span>
250
+ </div>
251
+ </div>
252
+
253
+ <!-- Recommendations -->
254
+ ${recommendationsHtml}
255
+
256
+ ${rejectionHtml}
257
+ </div>
258
+
259
+ <!-- 4. Footer Section -->
260
+ <div style="padding: 14px 28px 20px 28px; border-top: 2px solid #1f2937;">
261
+ <table style="width: 100%; border-collapse: collapse;">
262
+ <tr>
263
+ <!-- Left: Prepared By -->
264
+ <td style="width: 50%; vertical-align: bottom;">
265
+ <p style="font-size: 11px; font-weight: 500; margin: 0 0 2px 0; color: #111827;">Prepared by :</p>
266
+ <p style="font-weight: bold; font-size: 14px; text-transform: capitalize; margin: 0; color: #111827;">${report.report_footer.prepared_by}</p>
267
+ <p style="font-size: 11px; color: #6b7280; margin: 0;">${report.report_footer.department}</p>
268
+ </td>
269
+ <!-- Right: Approved By + Signature -->
270
+ ${approvalHtml}
271
+ </tr>
272
+ </table>
273
+
274
+ <!-- Disclaimer -->
275
+ <div style="margin-top: 16px; text-align: center; border-top: 1px solid #e5e7eb; padding-top: 8px;">
276
+ <p style="font-size: 10px; font-weight: 500; color: #6b7280; margin: 0;">
277
+ ${report.disclaimer}
278
+ </p>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ `;
283
+ }
284
+
285
+ function generateModernHtml(report: ReportData, logoUrl?: string): string {
286
+ const formattedDate = new Date(report.report_header.report_date).toLocaleString();
287
+ const genderDisplay = report.patient.gender === 'M' ? 'Male' : report.patient.gender === 'F' ? 'Female' : report.patient.gender;
288
+
289
+ // Status color (header badge)
290
+ const status = report.report_footer.report_status;
291
+ let statusBg = '#dbeafe';
292
+ let statusText = '#1e40af';
293
+ if (status === 'Approved') { statusBg = '#dcfce7'; statusText = '#166534'; }
294
+ if (status === 'Rejected') { statusBg = '#fee2e2'; statusText = '#991b1b'; }
295
+
296
+ // Findings
297
+ const findingsHtml = report.findings.map(finding => {
298
+ const fStatus = (finding.status || 'normal').toLowerCase();
299
+ let borderClass = 'border: 1px solid #f1f5f9; background-color: #ffffff;';
300
+ let badge = '';
301
+
302
+ if (fStatus === 'abnormal') {
303
+ borderClass = 'border: 1px solid #fee2e2; background-color: #fef2f2;';
304
+ badge = `<span style="font-size: 9px; font-weight: bold; background-color: #fee2e2; color: #dc2626; padding: 2px 6px; border-radius: 999px; text-transform: uppercase;">ABNORMAL</span>`;
305
+ } else if (fStatus === 'indeterminate') {
306
+ borderClass = 'border: 1px solid #fef9c3; background-color: #fffbeb;';
307
+ badge = `<span style="font-size: 9px; font-weight: bold; background-color: #fef9c3; color: #b45309; padding: 2px 6px; border-radius: 999px; text-transform: uppercase;">INDETERMINATE</span>`;
308
+ } else if (fStatus === 'post_procedural' || fStatus.includes('post')) {
309
+ borderClass = 'border: 1px solid #dbeafe; background-color: #eff6ff;';
310
+ badge = `<span style="font-size: 9px; font-weight: bold; background-color: #dbeafe; color: #1d4ed8; padding: 2px 6px; border-radius: 999px; text-transform: uppercase;">POST-PROCEDURAL</span>`;
311
+ }
312
+
313
+ return `
314
+ <div style="padding: 10px; border-radius: 6px; margin-bottom: 10px; ${borderClass} page-break-inside: avoid;">
315
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
316
+ <span style="font-weight: bold; color: #0f172a; font-size: 13px;">${finding.anatomical_region}</span>
317
+ ${badge}
318
+ </div>
319
+ <p style="margin: 0; font-size: 12px; color: #475569; line-height: 1.4;">${finding.observation}</p>
320
+ </div>`;
321
+ }).join('');
322
+
323
+ // Impressions
324
+ const impressionHtml = report.impression.map(imp =>
325
+ `<li style="margin-bottom: 6px; font-weight: 600; color: #0f172a; font-size: 13px; display: flex; align-items: flex-start;">
326
+ <span style="color: #3b82f6; margin-right: 6px;">•</span>
327
+ <span style="flex: 1;">${imp}</span>
328
+ </li>`
329
+ ).join('');
330
+
331
+ // Signature
332
+ let sigHtml = '';
333
+ if (report.report_footer.approved_by && report.report_footer.report_status === 'Approved') {
334
+ const sigImg = report.report_footer.signature ?
335
+ `<img src="${report.report_footer.signature}" style="height: 40px; opacity: 0.8; margin-bottom: 4px; display: block; margin-left: auto;" />` : '';
336
+ sigHtml = `
337
+ <div style="text-align: right;">
338
+ ${sigImg}
339
+ <div style="font-weight: bold; color: #0f172a; font-size: 14px;">${report.report_footer.approved_by}</div>
340
+ <div style="font-size: 10px; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px;">Approved Radiologist</div>
341
+ </div>
342
+ `;
343
+ }
344
+
345
+ // Rejection info with modern style
346
+ let rejectionHtml = '';
347
+ if (report.report_footer.report_status === 'Rejected' && report.report_footer.rejection_reason) {
348
+ rejectionHtml = `
349
+ <div style="margin-top: 10px; padding: 8px 12px; background-color: #fef2f2; border: 1px solid #ef4444; border-radius: 4px; margin-bottom: 16px;">
350
+ <span style="font-weight: bold; color: #dc2626; font-size: 11px;">REJECTED: </span>
351
+ <span style="color: #991b1b; font-size: 11px;">${report.report_footer.rejection_reason}</span>
352
+ </div>
353
+ `;
354
+ }
355
+
356
+ return `
357
+ <div style="max-width: 210mm; margin: 0 auto; background: #fff; font-family: sans-serif; font-size: 12px; color: #334155; line-height: 1.4;">
358
+ <!-- Header -->
359
+ <div style="background-color: #0f172a; color: white; padding: 24px 32px;">
360
+ <table style="width: 100%;">
361
+ <tr>
362
+ ${logoUrl ? `
363
+ <td style="width: 60px; vertical-align: middle; padding-right: 16px;">
364
+ <img src="${logoUrl}" alt="Hospital Logo" style="height: 48px; width: auto; object-fit: contain; filter: brightness(0) invert(1);" />
365
+ </td>
366
+ ` : ''}
367
+ <td style="vertical-align: middle;">
368
+ <h1 style="margin: 0; font-size: 20px; text-transform: uppercase; letter-spacing: 1px; font-weight: bold;">${report.report_header.hospital_name}</h1>
369
+ <p style="margin: 4px 0 0 0; color: #94a3b8; font-weight: 500;">${report.report_header.department}</p>
370
+ </td>
371
+ <td style="text-align: right; vertical-align: top;">
372
+ <div style="display: inline-block; background: #1e293b; border: 1px solid #334155; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 11px; margin-bottom: 4px;">
373
+ ID: ${report.report_header.report_id}
374
+ </div>
375
+ <div style="font-size: 11px; color: #94a3b8;">${formattedDate}</div>
376
+ </td>
377
+ </tr>
378
+ </table>
379
+ </div>
380
+
381
+ <!-- Patient Info -->
382
+ <div style="background-color: #f1f5f9; border-bottom: 1px solid #e2e8f0; padding: 16px 32px;">
383
+ <table style="width: 100%; font-size: 12px;">
384
+ <tr>
385
+ <td style="width: 33%; vertical-align: top;">
386
+ <div style="font-size: 10px; font-weight: bold; color: #64748b; text-transform: uppercase; margin-bottom: 2px;">Patient</div>
387
+ <div style="font-weight: bold; color: #0f172a; font-size: 14px;">${report.patient.name}</div>
388
+ <div style="color: #475569;">${report.patient.age}Y • ${genderDisplay}</div>
389
+ </td>
390
+ <td style="width: 33%; vertical-align: top;">
391
+ <div style="font-size: 10px; font-weight: bold; color: #64748b; text-transform: uppercase; margin-bottom: 2px;">Exam</div>
392
+ <div style="font-weight: 600; color: #0f172a;">${report.study.modality}</div>
393
+ <div style="color: #475569;">${report.study.examination}</div>
394
+ </td>
395
+ <td style="width: 33%; vertical-align: top;">
396
+ <div style="font-size: 10px; font-weight: bold; color: #64748b; text-transform: uppercase; margin-bottom: 2px;">Indication</div>
397
+ <div style="font-weight: 500; color: #0f172a;">${report.clinical_information.indication}</div>
398
+ </td>
399
+ </tr>
400
+ </table>
401
+ </div>
402
+
403
+ <div style="padding: 24px 32px;">
404
+ ${rejectionHtml}
405
+
406
+ <!-- Content -->
407
+ <div style="margin-bottom: 16px;">
408
+ <h3 style="font-size: 11px; font-weight: bold; color: #475569; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px;">History</h3>
409
+ <p style="font-size: 12px; color: #334155; margin: 0;">${report.clinical_information.history} <span style="color: #64748b; font-style: italic;">(${report.clinical_information.symptoms})</span></p>
410
+ </div>
411
+
412
+ <!-- Findings -->
413
+ <div style="margin-bottom: 24px;">
414
+ <h3 style="font-size: 11px; font-weight: bold; color: #2563eb; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; display: flex; align-items: center;">
415
+ <span style="width: 20px; height: 2px; background: #2563eb; margin-right: 8px; display: inline-block;"></span>
416
+ Findings
417
+ </h3>
418
+ <div>${findingsHtml}</div>
419
+ </div>
420
+
421
+ <!-- Impression -->
422
+ <div style="background: #f8fafc; border-left: 4px solid #2563eb; padding: 20px; border-radius: 0 6px 6px 0; margin-bottom: 24px;">
423
+ <h3 style="margin: 0 0 12px 0; font-size: 11px; font-weight: bold; color: #0f172a; text-transform: uppercase; letter-spacing: 1px;">Impression</h3>
424
+ <ul style="list-style: none; padding: 0; margin: 0;">${impressionHtml}</ul>
425
+ </div>
426
+
427
+ <!-- Recommendations (if any) -->
428
+ ${report.recommendations?.length ? `
429
+ <div style="margin-bottom: 24px;">
430
+ <h3 style="font-size: 11px; font-weight: bold; color: #64748b; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px;">Recommendations</h3>
431
+ <ul style="margin: 0; padding-left: 20px; list-style-type: disc;">
432
+ ${report.recommendations.map(r => `<li style="font-size: 12px; color: #334155; margin-bottom: 4px;">${r}</li>`).join('')}
433
+ </ul>
434
+ </div>
435
+ ` : ''}
436
+
437
+ <!-- Footer / Signatures -->
438
+ <div style="border-top: 1px solid #f1f5f9; padding-top: 16px; margin-top: 32px; display: flex; justify-content: space-between; align-items: flex-end;">
439
+ <div>
440
+ <div style="font-size: 10px; text-transform: uppercase; color: #94a3b8; font-weight: bold; margin-bottom: 4px;">Report Status</div>
441
+ <div style="display: inline-block; padding: 3px 10px; border-radius: 999px; background: ${statusBg}; color: ${statusText}; font-size: 10px; font-weight: bold; text-transform: uppercase;">
442
+ ${status}
443
+ </div>
444
+ </div>
445
+ ${sigHtml}
446
+ </div>
447
+
448
+ <!-- Disclaimer -->
449
+ <div style="margin-top: 24px; padding-top: 12px; border-top: 1px solid #e2e8f0; text-align: center;">
450
+ <p style="font-size: 9px; color: #cbd5e1; margin: 0;">${report.disclaimer}</p>
451
+ </div>
452
+ </div>
453
+ </div>`;
454
+ }
455
+
456
+ function generateMinimalHtml(report: ReportData, logoUrl?: string): string {
457
+ const formattedDate = new Date(report.report_header.report_date).toLocaleString();
458
+
459
+ // Helper for rows
460
+ function row(label: string, content: string) {
461
+ return `
462
+ <div style="display: flex; margin-bottom: 8px; page-break-inside: avoid;">
463
+ <div style="width: 100px; font-weight: bold; font-size: 10px; color: #6b7280; text-transform: uppercase; padding-top: 2px;">${label}</div>
464
+ <div style="flex: 1; font-size: 11px; color: #111827;">${content}</div>
465
+ </div>`;
466
+ }
467
+
468
+ const findingsHtml = report.findings.map(finding => {
469
+ const status = (finding.status || 'normal').toLowerCase();
470
+ let badge = '';
471
+ if (status === 'abnormal') badge = `<span style="display: inline-block; vertical-align: middle; margin-left: 8px; font-size: 9px; line-height: 1; font-weight: bold; border: 1px solid #000; padding: 2px 4px 1px 4px; border-radius: 2px;">ABNORMAL</span>`;
472
+ if (status === 'post_procedural' || status.includes('post')) badge = `<span style="display: inline-block; vertical-align: middle; margin-left: 8px; font-size: 9px; line-height: 1; font-weight: bold; border: 1px solid #000; padding: 2px 4px 1px 4px; border-radius: 2px;">POST-PROCEDURAL</span>`;
473
+
474
+ return `
475
+ <div style="border-bottom: 1px solid #f3f4f6; padding-bottom: 4px; margin-bottom: 4px;">
476
+ <span style="font-weight: bold; text-transform: uppercase; font-size: 11px; margin-right: 6px;">${finding.anatomical_region}:</span>
477
+ <span style="font-size: 11px;">${finding.observation}</span>
478
+ ${badge}
479
+ </div>`;
480
+ }).join('');
481
+
482
+ const impressionsHtml = report.impression.map(imp =>
483
+ `<li style="font-weight: bold; margin-bottom: 2px;">${imp}</li>`
484
+ ).join('');
485
+
486
+ // Rejection info minimal
487
+ let rejectionHtml = '';
488
+ if (report.report_footer.report_status === 'Rejected' && report.report_footer.rejection_reason) {
489
+ rejectionHtml = `
490
+ <div style="margin-bottom: 16px; border: 1px solid #000; padding: 8px;">
491
+ <span style="font-weight: bold; color: #dc2626; font-size: 10px;">REJECTED: </span>
492
+ <span style="font-size: 10px;">${report.report_footer.rejection_reason}</span>
493
+ </div>
494
+ `;
495
+ }
496
+
497
+ return `
498
+ <div style="max-width: 210mm; margin: 0 auto; background: #fff; font-family: sans-serif; font-size: 11px; color: #000; padding: 30px;">
499
+ <!-- Header -->
500
+ <div style="border-bottom: 2px solid #000; padding-bottom: 10px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: flex-end;">
501
+ <div style="display: flex; align-items: center;">
502
+ ${logoUrl ? `<img src="${logoUrl}" alt="Hospital Logo" style="height: 32px; width: auto; object-fit: contain; margin-right: 12px;" />` : ''}
503
+ <div>
504
+ <h1 style="margin: 0; font-size: 18px; font-weight: bold; text-transform: uppercase; letter-spacing: -0.5px;">${report.report_header.hospital_name}</h1>
505
+ <p style="margin: 0; font-size: 10px; text-transform: uppercase; letter-spacing: 2px;">${report.report_header.department}</p>
506
+ </div>
507
+ </div>
508
+ <div style="text-align: right; font-family: monospace; font-size: 10px;">
509
+ ${report.report_header.report_id} <br/> ${formattedDate}
510
+ </div>
511
+ </div>
512
+
513
+ <!-- Grid Info -->
514
+ <div style="display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 16px; border-bottom: 1px solid #000; padding-bottom: 16px; margin-bottom: 20px;">
515
+ <div>
516
+ <div style="font-size: 9px; font-weight: bold; color: #6b7280; text-transform: uppercase;">Patient</div>
517
+ <div style="font-weight: bold; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${report.patient.name}</div>
518
+ </div>
519
+ <div>
520
+ <div style="font-size: 9px; font-weight: bold; color: #6b7280; text-transform: uppercase;">Details</div>
521
+ <div>${report.patient.age} / ${report.patient.gender}</div>
522
+ </div>
523
+ <div>
524
+ <div style="font-size: 9px; font-weight: bold; color: #6b7280; text-transform: uppercase;">Exam</div>
525
+ <div>${report.study.modality}</div>
526
+ </div>
527
+ <div>
528
+ <div style="font-size: 9px; font-weight: bold; color: #6b7280; text-transform: uppercase;">Indication</div>
529
+ <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${report.clinical_information.indication}</div>
530
+ </div>
531
+ </div>
532
+
533
+ ${rejectionHtml}
534
+
535
+ <!-- Content -->
536
+ ${row('History', `${report.clinical_information.history} <span style="font-style: italic;">(Symptoms: ${report.clinical_information.symptoms})</span>`)}
537
+
538
+ <div style="display: flex; margin-bottom: 12px; page-break-inside: avoid;">
539
+ <div style="width: 100px; font-weight: bold; font-size: 10px; color: #6b7280; text-transform: uppercase; padding-top: 2px;">Findings</div>
540
+ <div style="flex: 1;">${findingsHtml}</div>
541
+ </div>
542
+
543
+ <div style="display: flex; margin-bottom: 12px; page-break-inside: avoid;">
544
+ <div style="width: 100px; font-weight: bold; font-size: 10px; color: #6b7280; text-transform: uppercase; padding-top: 2px;">Impression</div>
545
+ <div style="flex: 1; background: #f9fafb; padding: 10px;">
546
+ <ul style="margin: 0; padding-left: 16px; list-style-type: decimal;">${impressionsHtml}</ul>
547
+ <div style="margin-top: 8px; border-top: 1px solid #e5e7eb; padding-top: 6px; font-size: 10px; display: flex; justify-content: space-between;">
548
+ <span>URGENCY: <span style="font-weight: bold; text-transform: uppercase;">${report.urgency}</span></span>
549
+ </div>
550
+ </div>
551
+ </div>
552
+
553
+ <!-- Recommendations (if any) -->
554
+ ${report.recommendations?.length ? `
555
+ <div style="display: flex; margin-bottom: 12px; page-break-inside: avoid;">
556
+ <div style="width: 100px; font-weight: bold; font-size: 10px; color: #6b7280; text-transform: uppercase; padding-top: 2px;">Recs</div>
557
+ <div style="flex: 1;">
558
+ <ul style="margin: 0; padding-left: 16px; list-style-type: disc;">
559
+ ${report.recommendations.map(r => `<li style="font-size: 11px;">${r}</li>`).join('')}
560
+ </ul>
561
+ </div>
562
+ </div>
563
+ ` : ''}
564
+
565
+ <!-- Footer -->
566
+ <div style="margin-top: 40px; border-top: 1px solid #000; padding-top: 10px; display: flex; justify-content: space-between; align-items: flex-end;">
567
+ <div style="font-size: 10px;">
568
+ <span style="font-weight: bold; text-transform: uppercase;">Prepared By:</span> ${report.report_footer.prepared_by}
569
+ </div>
570
+ ${report.report_footer.approved_by && report.report_footer.report_status === 'Approved' ? `
571
+ <div style="text-align: right;">
572
+ ${report.report_footer.signature ? `<img src="${report.report_footer.signature}" style="height: 30px; margin-bottom: 2px;" />` : ''}
573
+ <div style="font-size: 10px; font-weight: bold; text-transform: uppercase; border-top: 1px solid #000; padding-top: 2px;">
574
+ Approved: ${report.report_footer.approved_by}
575
+ </div>
576
+ </div>
577
+ ` : ''}
578
+ </div>
579
+ <p style="margin-top: 20px; font-size: 8px; color: #9ca3af; text-align: center;">${report.disclaimer}</p>
580
+ </div>`;
581
+ }