@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,539 @@
1
+ import * as React from "react";
2
+ import { ReportData } from "@/types";
3
+ import { Card, CardContent } from "@/components/ui/basic";
4
+ import { Download, Printer, Edit, XCircle, CheckCircle, FileText, Share2 } from "lucide-react";
5
+ import { Button } from "@/components/ui/basic";
6
+ import { CollaborationPanel } from "@/components/dashboard/CollaborationPanel";
7
+ import { FullReportOverlay } from "@/components/dashboard/FullReportOverlay";
8
+ import { RejectionModal } from "@/components/dashboard/RejectionModal";
9
+ import { ApprovalModal } from "@/components/dashboard/ApprovalModal";
10
+ import { ReportEditor } from "@/components/dashboard/ReportEditor";
11
+ import { Comment, AuditLog } from "@/types";
12
+ import { updateReportData, updateReportStatus } from "@/lib/api";
13
+ import { StandardTemplate, PdfStandardTemplate } from "@/components/dashboard/ReportTemplates";
14
+
15
+ interface ReportViewProps {
16
+ report: ReportData;
17
+ onNewPatient: () => void;
18
+ reportId?: string;
19
+ imagePreview?: string | null;
20
+ imagesPreviews?: string[];
21
+ onStatusChange?: () => void;
22
+ }
23
+
24
+ export function ReportView({ report, onNewPatient, reportId, imagePreview, imagesPreviews = [], onStatusChange }: ReportViewProps) {
25
+ const [currentUser, setCurrentUser] = React.useState({ name: "Dr. User", role: "Doctor" });
26
+ const [logoUrl, setLogoUrl] = React.useState<string>("");
27
+
28
+ // Local state for the report footer and collaboration so UI re-renders immediately
29
+ const [footer, setFooter] = React.useState({ ...report.report_footer });
30
+ const [collaboration, setCollaboration] = React.useState({
31
+ comments: report.collaboration?.comments || [],
32
+ logs: report.collaboration?.logs || []
33
+ });
34
+
35
+ // Keep state in sync if a completely new report is passed in
36
+ React.useEffect(() => {
37
+ setFooter({ ...report.report_footer });
38
+ setCollaboration({
39
+ comments: report.collaboration?.comments || [],
40
+ logs: report.collaboration?.logs || []
41
+ });
42
+ }, [report]);
43
+
44
+ React.useEffect(() => {
45
+ // Load current authenticated user instead of global profile
46
+ fetch('/api/auth/me')
47
+ .then(res => res.json())
48
+ .then(data => {
49
+ if (data && !data.error) {
50
+ setCurrentUser({
51
+ name: data.fullName || "Dr. User",
52
+ role: data.position || data.role || "Doctor"
53
+ });
54
+ }
55
+ })
56
+ .catch(e => console.error("Error fetching current user:", e));
57
+
58
+ // Load appearance settings (logo)
59
+ fetch('/api/settings?type=appearance')
60
+ .then(res => res.json())
61
+ .then(data => {
62
+ if (data && data.logo) {
63
+ setLogoUrl(data.logo);
64
+ }
65
+ })
66
+ .catch(e => console.error("Error fetching appearance settings:", e));
67
+ }, []);
68
+
69
+ const handleAddComment = async (text: string) => {
70
+ if (!reportId) return;
71
+
72
+ const newComment: Comment = {
73
+ id: Date.now().toString(),
74
+ author: currentUser.name,
75
+ role: currentUser.role,
76
+ text,
77
+ timestamp: new Date().toISOString()
78
+ };
79
+
80
+ const updatedComments = [...collaboration.comments, newComment];
81
+ const updatedLogs = [...collaboration.logs, {
82
+ id: Date.now().toString() + "_log",
83
+ action: "Comment Added",
84
+ user: currentUser.name,
85
+ timestamp: new Date().toISOString(),
86
+ details: text.substring(0, 50) + (text.length > 50 ? "..." : "")
87
+ } as AuditLog];
88
+
89
+ await updateReportData(reportId, {
90
+ collaboration: {
91
+ comments: updatedComments,
92
+ logs: updatedLogs
93
+ }
94
+ });
95
+
96
+ // Optimistically update local state
97
+ setCollaboration({ comments: updatedComments, logs: updatedLogs });
98
+
99
+ // Notify parent
100
+ if (onStatusChange) onStatusChange();
101
+ };
102
+
103
+ const handleUnreject = async () => {
104
+ if (!reportId) return;
105
+
106
+ const success = await updateReportStatus(reportId, 'Pending');
107
+ if (success) {
108
+ setFooter(prev => ({ ...prev, report_status: 'Pending', rejection_reason: undefined }));
109
+
110
+ // Add audit log optimistically
111
+ setCollaboration(prev => ({
112
+ ...prev,
113
+ logs: [...prev.logs, {
114
+ id: `log_${Date.now()}`,
115
+ action: "Status Changed to Pending",
116
+ user: currentUser.name,
117
+ timestamp: new Date().toISOString(),
118
+ details: "Status reset"
119
+ }]
120
+ }));
121
+
122
+ if (onStatusChange) onStatusChange();
123
+ }
124
+ };
125
+
126
+ const handlePrint = () => window.print();
127
+
128
+ const handleDownloadPDF = async () => {
129
+ // Use the Clean Slate PDF generator which creates a fresh HTML structure
130
+ // This bypasses any UI state or visibility issues with the React components.
131
+
132
+ const filename = `${report.patient.name.replace(/\s+/g, '_')}_Report.pdf`;
133
+
134
+ // Determine template preference
135
+ let template: 'standard' | 'modern' | 'minimal' = 'standard';
136
+ let logoToUse = logoUrl;
137
+
138
+ try {
139
+ const res = await fetch('/api/settings?type=appearance');
140
+ const config = await res.json();
141
+ if (config.template) {
142
+ template = config.template;
143
+ }
144
+ if (config.logo) {
145
+ logoToUse = config.logo;
146
+ }
147
+ } catch (e) {
148
+ console.error("Error reading template preference", e);
149
+ }
150
+
151
+ const { generatePDF } = await import('@/lib/pdfHelper');
152
+ await generatePDF(report, filename, template, logoToUse);
153
+ };
154
+ const urgencyColor = report.urgency === 'Critical' ? 'text-red-600' :
155
+ report.urgency === 'Urgent' ? 'text-orange-600' : 'text-green-600';
156
+
157
+ // Use local footer state for all status-driven UI
158
+ const statusColor = footer.report_status === 'Approved' ? 'bg-green-100 text-green-800' :
159
+ footer.report_status === 'Rejected' ? 'bg-red-100 text-red-800' : 'bg-blue-100 text-blue-800';
160
+
161
+
162
+
163
+ // ... imports
164
+
165
+ const [isFullReport, setIsFullReport] = React.useState(false);
166
+ const [isRejectModalOpen, setIsRejectModalOpen] = React.useState(false);
167
+ const [isApproveModalOpen, setIsApproveModalOpen] = React.useState(false);
168
+ const [isEditing, setIsEditing] = React.useState(false);
169
+
170
+ const handleSaveReport = async (updatedReport: ReportData) => {
171
+ if (!reportId) return;
172
+
173
+ // Optimistic update locally
174
+ Object.assign(report, updatedReport);
175
+ setIsEditing(false);
176
+
177
+ // Update backend
178
+ await updateReportData(reportId, {
179
+ findings: updatedReport.findings,
180
+ impression: updatedReport.impression,
181
+ recommendations: updatedReport.recommendations,
182
+ urgency: updatedReport.urgency
183
+ });
184
+
185
+ // Reload or update callback
186
+ if (onStatusChange) onStatusChange();
187
+ };
188
+
189
+ const handleRejectConfirm = async (reason: string, comment: string) => {
190
+ if (!reportId) return;
191
+ const success = await updateReportStatus(reportId, 'Rejected', { rejectionReason: reason, notes: comment });
192
+ if (success) {
193
+ setFooter(prev => ({ ...prev, report_status: 'Rejected', rejection_reason: reason }));
194
+
195
+ // Optmistic logs and comments updates
196
+ const timestamp = new Date().toISOString();
197
+ const newLogs = [...collaboration.logs, {
198
+ id: `log_${Date.now()}`,
199
+ action: "Status Changed to Rejected",
200
+ user: currentUser.name,
201
+ timestamp,
202
+ details: `Reason: ${reason}`
203
+ }];
204
+ let newComments = [...collaboration.comments];
205
+ if (comment) {
206
+ newComments.push({
207
+ id: `comment_${Date.now()}`,
208
+ author: currentUser.name,
209
+ role: currentUser.role,
210
+ text: comment,
211
+ timestamp
212
+ });
213
+ }
214
+ setCollaboration({ logs: newLogs, comments: newComments });
215
+
216
+ setIsRejectModalOpen(false);
217
+ if (onStatusChange) onStatusChange();
218
+ }
219
+ };
220
+
221
+ const handleApproveConfirm = async (signature: string, comment: string) => {
222
+ if (!reportId) return;
223
+ const success = await updateReportStatus(reportId, 'Approved', { signature, notes: comment });
224
+ if (success) {
225
+ const timestamp = new Date().toISOString();
226
+ setFooter(prev => ({
227
+ ...prev,
228
+ report_status: 'Approved',
229
+ signature,
230
+ approved_by: currentUser.name,
231
+ approved_at: timestamp,
232
+ }));
233
+
234
+ // Optmistic logs and comments updates
235
+ const newLogs = [...collaboration.logs, {
236
+ id: `log_${Date.now()}`,
237
+ action: "Status Changed to Approved",
238
+ user: currentUser.name,
239
+ timestamp,
240
+ details: "Report Approved"
241
+ }];
242
+ let newComments = [...collaboration.comments];
243
+ if (comment) {
244
+ newComments.push({
245
+ id: `comment_${Date.now()}`,
246
+ author: currentUser.name,
247
+ role: currentUser.role,
248
+ text: comment,
249
+ timestamp
250
+ });
251
+ }
252
+ setCollaboration({ logs: newLogs, comments: newComments });
253
+
254
+ setIsApproveModalOpen(false);
255
+ if (onStatusChange) onStatusChange();
256
+ }
257
+ };
258
+
259
+ return (
260
+ <div className="h-full relative">
261
+ <RejectionModal
262
+ isOpen={isRejectModalOpen}
263
+ onClose={() => setIsRejectModalOpen(false)}
264
+ onConfirm={handleRejectConfirm}
265
+ />
266
+ <ApprovalModal
267
+ isOpen={isApproveModalOpen}
268
+ onClose={() => setIsApproveModalOpen(false)}
269
+ onConfirm={handleApproveConfirm}
270
+ currentUser={currentUser}
271
+ />
272
+
273
+ {isFullReport && (
274
+ <FullReportOverlay
275
+ report={{ ...report, report_footer: footer, collaboration }}
276
+ reportId={reportId}
277
+ imageSrc={imagePreview || report.image_data}
278
+ images={
279
+ imagesPreviews.length > 0 ? imagesPreviews :
280
+ report.images_data && report.images_data.length > 0 ? report.images_data :
281
+ []
282
+ }
283
+ onClose={() => setIsFullReport(false)}
284
+ onNewPatient={onNewPatient}
285
+ onPrint={handlePrint}
286
+ onDownloadPDF={handleDownloadPDF}
287
+ onEdit={() => {
288
+ setIsFullReport(false);
289
+ setIsEditing(true);
290
+ }}
291
+ onReject={() => setIsRejectModalOpen(true)}
292
+ onApprove={() => setIsApproveModalOpen(true)}
293
+ onUnreject={handleUnreject}
294
+ onAddComment={handleAddComment}
295
+ currentUser={currentUser}
296
+ />
297
+ )}
298
+
299
+ {isEditing ? (
300
+ <div className="h-full bg-bg-surface rounded-xl shadow-sm border border-border-primary overflow-hidden">
301
+ <ReportEditor
302
+ report={report}
303
+ onSave={handleSaveReport}
304
+ onCancel={() => setIsEditing(false)}
305
+ />
306
+ </div>
307
+ ) : (
308
+ <div className="h-full flex flex-col overflow-hidden bg-bg-surface rounded-xl shadow-sm border border-border-primary">
309
+ <div className="flex flex-col border-b border-border-primary bg-bg-panel shrink-0">
310
+ {/* Top Row: Patient Info */}
311
+ <div className="p-4 pb-2 flex justify-between items-start">
312
+ <div className="flex flex-col gap-1">
313
+ <h1 className="text-2xl font-bold text-text-heading">{report.patient.name}</h1>
314
+ <div className="flex items-center gap-2 text-sm text-text-muted">
315
+ {report.patient.patient_id && (
316
+ <>
317
+ <span className="font-mono text-xs bg-white/5 px-1.5 py-0.5 rounded border border-white/10">ID: {report.patient.patient_id}</span>
318
+ <span>•</span>
319
+ </>
320
+ )}
321
+ <span>{report.study.modality}</span>
322
+ <span>•</span>
323
+ <span>{report.study.examination}</span>
324
+ </div>
325
+ </div>
326
+ <div className={`px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider ${statusColor}`}>
327
+ {footer.report_status}
328
+ </div>
329
+ </div>
330
+
331
+ {/* Bottom Row: Action Toolbar */}
332
+ <div className="px-4 pb-4 pt-2 flex items-center justify-between gap-4">
333
+ {/* Left Group: View & Export */}
334
+ <div className="flex items-center gap-2">
335
+ <Button
336
+ variant={isFullReport ? "default" : "outline"}
337
+ size="sm"
338
+ onClick={() => setIsFullReport(!isFullReport)}
339
+ className="transition-all hover:shadow-md active:scale-95 hover:bg-blue-50 text-blue-700 border-blue-200"
340
+ >
341
+ {isFullReport ? "Exit Full View" : "Full Report"}
342
+ </Button>
343
+ <div className="h-6 w-px bg-border-primary mx-1" />
344
+ <Button
345
+ variant="outline"
346
+ size="icon"
347
+ onClick={handleDownloadPDF}
348
+ title="Download PDF"
349
+ className="w-8 h-8 transition-all hover:bg-gray-100 hover:text-blue-600 active:scale-95"
350
+ >
351
+ <Download size={16} />
352
+ </Button>
353
+ <Button
354
+ variant="outline"
355
+ size="icon"
356
+ onClick={handlePrint}
357
+ title="Print"
358
+ className="w-8 h-8 transition-all hover:bg-gray-100 hover:text-blue-600 active:scale-95"
359
+ >
360
+ <Printer size={16} />
361
+ </Button>
362
+ </div>
363
+
364
+ {/* Right Group: Workflow Actions */}
365
+ <div className="flex items-center gap-2">
366
+ {footer.report_status === 'Pending' && (
367
+ <>
368
+ <Button
369
+ variant="outline"
370
+ size="sm"
371
+ className="text-blue-600 border-blue-200 hover:bg-blue-50"
372
+ onClick={() => setIsEditing(true)}
373
+ >
374
+ <Edit size={14} className="mr-1.5" /> Edit
375
+ </Button>
376
+ <Button
377
+ variant="danger"
378
+ size="sm"
379
+ onClick={() => setIsRejectModalOpen(true)}
380
+ className="bg-red-50 text-red-600 border-red-200 hover:bg-red-100"
381
+ >
382
+ <XCircle size={14} className="mr-1.5" /> Reject
383
+ </Button>
384
+ <Button
385
+ variant="success"
386
+ size="sm"
387
+ onClick={() => setIsApproveModalOpen(true)}
388
+ className="bg-green-600 text-white hover:bg-green-700 hover:shadow-md border-transparent"
389
+ >
390
+ <CheckCircle size={14} className="mr-1.5" /> Approve
391
+ </Button>
392
+ </>
393
+ )}
394
+
395
+ {footer.report_status === 'Rejected' && (
396
+ <Button
397
+ variant="danger"
398
+ size="sm"
399
+ onClick={handleUnreject}
400
+ className="bg-red-100 text-red-700 hover:bg-red-200 border-red-200"
401
+ title="Click to Unreject"
402
+ >
403
+ <XCircle size={14} className="mr-1.5" /> Unreject
404
+ </Button>
405
+ )}
406
+
407
+ {footer.report_status === 'Approved' && (
408
+ <Button
409
+ variant="success"
410
+ size="sm"
411
+ className="bg-green-100 text-green-800 border-green-200 cursor-default"
412
+ >
413
+ <CheckCircle size={14} className="mr-1.5" /> Approved
414
+ </Button>
415
+ )}
416
+
417
+ <div className="h-6 w-px bg-border-primary mx-1" />
418
+
419
+ <Button variant="default" size="sm" onClick={onNewPatient} className="bg-blue-600 hover:bg-blue-700 shadow-sm active:scale-95">
420
+ New Patient
421
+ </Button>
422
+ </div>
423
+ </div>
424
+ </div>
425
+
426
+ {/* Scrollable Report Content */}
427
+ <div className="flex-1 overflow-y-auto p-6 space-y-8" id="report-container">
428
+
429
+ {/* Findings */}
430
+ <section>
431
+ <h3 className="text-sm font-bold text-text-muted uppercase tracking-wider mb-3">Findings</h3>
432
+ <div className="space-y-4">
433
+ {report.findings.map((finding, idx) => {
434
+ const status = finding.status?.toLowerCase() || 'normal';
435
+
436
+ let badgeColor = 'bg-gray-100 text-gray-700 border-gray-200';
437
+ let statusLabel = 'NORMAL';
438
+
439
+ if (status === 'abnormal') {
440
+ badgeColor = 'bg-red-100 text-red-700 border-red-200';
441
+ statusLabel = 'ABNORMAL';
442
+ } else if (status === 'normal') {
443
+ badgeColor = 'bg-green-100 text-green-700 border-green-200';
444
+ statusLabel = 'NORMAL';
445
+ } else if (status === 'indeterminate') {
446
+ badgeColor = 'bg-yellow-100 text-yellow-800 border-yellow-200';
447
+ statusLabel = 'INDETERMINATE';
448
+ } else if (status === 'post_procedural' || status === 'post-procedural') {
449
+ badgeColor = 'bg-blue-100 text-blue-700 border-blue-200';
450
+ statusLabel = 'POST-PROCEDURAL';
451
+ }
452
+
453
+ return (
454
+ <div key={idx} className="pb-3 border-b border-border-primary/30 last:border-0">
455
+ <div className="flex items-center gap-2 mb-1">
456
+ <span className="font-bold text-text-heading underline decoration-gray-300 underline-offset-4">
457
+ {finding.anatomical_region}
458
+ </span>
459
+ <span className={`text-[10px] px-2 py-0.5 rounded border font-bold uppercase tracking-wider ${badgeColor}`}>
460
+ {statusLabel}
461
+ </span>
462
+ </div>
463
+ <p className="text-text-primary leading-relaxed">
464
+ {finding.observation}
465
+ </p>
466
+ </div>
467
+ );
468
+ })}
469
+ </div>
470
+ </section>
471
+
472
+ {/* Impression */}
473
+ <section className="bg-blue-50/50 p-4 rounded-lg border border-blue-100/50">
474
+ <h3 className="text-sm font-bold text-blue-900 uppercase tracking-wider mb-2">Impression</h3>
475
+ <div className="space-y-2">
476
+ {report.impression.map((imp, idx) => (
477
+ <p key={idx} className="text-text-heading font-medium leading-relaxed">{imp}</p>
478
+ ))}
479
+ </div>
480
+ </section>
481
+
482
+ {/* Urgency Level - Moved here per request */}
483
+ <div className="p-4 rounded-lg border border-border-primary bg-bg-panel/50">
484
+ <span className="block text-xs font-bold text-text-muted uppercase tracking-wider mb-1">Urgency Level</span>
485
+ <span className={`text-lg font-bold ${urgencyColor}`}>{report.urgency}</span>
486
+ </div>
487
+
488
+ {/* Recommendations */}
489
+ {report.recommendations && report.recommendations.length > 0 && (
490
+ <section>
491
+ <h3 className="text-sm font-bold text-text-muted uppercase tracking-wider mb-2">Recommendations</h3>
492
+ <ul className="list-disc list-inside space-y-1 text-text-primary">
493
+ {report.recommendations.map((rec, idx) => (
494
+ <li key={idx}>{rec}</li>
495
+ ))}
496
+ </ul>
497
+ </section>
498
+ )}
499
+
500
+ {/* Footer Info */}
501
+ <div className="pt-8 border-t border-border-primary text-sm text-text-muted grid grid-cols-2 gap-4">
502
+ <div className="text-left">
503
+ <p>Prepared by: {footer.prepared_by}</p>
504
+ <p>{new Date(report.report_header.report_date).toLocaleDateString()}</p>
505
+ {footer.report_status === 'Rejected' && footer.rejection_reason && (
506
+ <div className="mt-2 text-red-600 font-medium">
507
+ Rejection Reason: {footer.rejection_reason}
508
+ </div>
509
+ )}
510
+ </div>
511
+ {/* Approval Sig in Footer */}
512
+ {footer.report_status === 'Approved' && footer.approved_by && (
513
+ <div className="text-right">
514
+ <p className="text-xs uppercase font-bold mb-2">Electronically Signed By</p>
515
+ <p className="font-bold text-lg">{footer.approved_by}</p>
516
+ {footer.signature && (
517
+ <img src={footer.signature} alt="Signature" className="h-12 ml-auto opacity-80 mt-1 dark:invert" />
518
+ )}
519
+ <p className="text-xs mt-1">{new Date(footer.approved_at || "").toLocaleString()}</p>
520
+ </div>
521
+ )}
522
+ </div>
523
+
524
+ {/* Collaboration (Included at bottom for utility) */}
525
+ <div className="pt-8 border-t border-border-primary">
526
+ <h3 className="text-sm font-bold text-text-muted uppercase tracking-wider mb-4">Collaboration & Logs</h3>
527
+ <CollaborationPanel
528
+ comments={collaboration.comments}
529
+ logs={collaboration.logs}
530
+ onAddComment={handleAddComment}
531
+ currentUser={currentUser}
532
+ />
533
+ </div>
534
+ </div>
535
+ </div>
536
+ )}
537
+ </div>
538
+ );
539
+ }