@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,331 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Card, CardContent, CardHeader, CardTitle, Input, Label, Button } from "@/components/ui/basic"
5
+ import { Save, Link, CheckCircle, AlertCircle, RefreshCw } from "lucide-react"
6
+
7
+ export function FhirIntegrationPanel() {
8
+ const [loading, setLoading] = React.useState(true)
9
+ const [saveStatus, setSaveStatus] = React.useState<"idle" | "saving" | "saved" | "error">("idle")
10
+ const [errorMsg, setErrorMsg] = React.useState("")
11
+ const [connStatus, setConnStatus] = React.useState<"idle" | "checking" | "verified" | "invalid">("idle")
12
+ const [connError, setConnError] = React.useState("")
13
+ const [serverInfo, setServerInfo] = React.useState({ fhirVersion: "", softwareName: "" })
14
+
15
+ const [formData, setFormData] = React.useState({
16
+ enabled: false,
17
+ publicBaseUrl: "",
18
+ authMode: "bearer_token",
19
+ inboundServiceRequestEnabled: false,
20
+ outboundReadEnabled: true,
21
+ externalFhirBaseUrl: "",
22
+ externalFhirAuthType: "none",
23
+ externalFhirClientId: "",
24
+ externalFhirClientSecret: "",
25
+ externalFhirBearerToken: "",
26
+ })
27
+
28
+ React.useEffect(() => {
29
+ fetch("/api/fhir/config")
30
+ .then(res => res.json())
31
+ .then(data => {
32
+ if (data && !data.error) {
33
+ setFormData(prev => ({ ...prev, ...data }))
34
+ }
35
+ })
36
+ .catch(console.error)
37
+ .finally(() => setLoading(false))
38
+ }, [])
39
+
40
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
41
+ const { id, value, type } = e.target
42
+ const val = type === "checkbox" ? (e.target as HTMLInputElement).checked : value
43
+ setFormData(prev => ({ ...prev, [id]: val }))
44
+
45
+ if (id === "externalFhirBaseUrl" || id === "externalFhirAuthType" || id === "externalFhirBearerToken" || id === "externalFhirClientId" || id === "externalFhirClientSecret") {
46
+ setConnStatus("idle")
47
+ }
48
+ setSaveStatus("idle")
49
+ }
50
+
51
+ const checkConnection = async () => {
52
+ if (!formData.externalFhirBaseUrl) {
53
+ setConnStatus("invalid")
54
+ setConnError("FHIR Base URL is required.")
55
+ return
56
+ }
57
+
58
+ setConnStatus("checking")
59
+ setConnError("")
60
+ try {
61
+ const res = await fetch("/api/fhir/config/test-connection", {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify(formData)
65
+ })
66
+ const data = await res.json()
67
+ if (res.ok && data.success) {
68
+ setServerInfo({ fhirVersion: data.fhirVersion, softwareName: data.softwareName })
69
+ setConnStatus("verified")
70
+ } else {
71
+ setConnStatus("invalid")
72
+ setConnError(data.error || "Failed to connect to FHIR server.")
73
+ }
74
+ } catch(e) {
75
+ setConnStatus("invalid")
76
+ setConnError("Network error while trying to reach the test connection endpoint.")
77
+ }
78
+ }
79
+
80
+ const handleSave = async () => {
81
+ if (formData.enabled && !formData.externalFhirBaseUrl) {
82
+ setErrorMsg("External FHIR Base URL is required when enabled.")
83
+ setSaveStatus("error")
84
+ return
85
+ }
86
+
87
+ setSaveStatus("saving")
88
+ setErrorMsg("")
89
+ try {
90
+ const res = await fetch("/api/fhir/config", {
91
+ method: "POST",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify(formData),
94
+ })
95
+ const result = await res.json()
96
+ if (res.ok && result.success) {
97
+ setSaveStatus("saved")
98
+ setTimeout(() => setSaveStatus("idle"), 3000)
99
+ } else {
100
+ setErrorMsg(result.error || "Failed to save FHIR configuration")
101
+ setSaveStatus("error")
102
+ }
103
+ } catch (e) {
104
+ setErrorMsg(String(e))
105
+ setSaveStatus("error")
106
+ }
107
+ }
108
+
109
+ if (loading) return null
110
+
111
+ return (
112
+ <Card className="bg-bg-surface border-border-primary border-t-4 border-t-emerald-500">
113
+ <CardHeader>
114
+ <div className="flex items-center justify-between">
115
+ <div>
116
+ <CardTitle className="text-text-heading flex items-center gap-2">
117
+ <Link size={20} className="text-emerald-500" />
118
+ FHIR API Integration
119
+ </CardTitle>
120
+ <p className="text-sm text-text-secondary mt-1">Connect to external EMR/EHR systems and enable standard FHIR workflows</p>
121
+ </div>
122
+ <div className="flex items-center gap-2">
123
+ <Label htmlFor="enabled" className="text-sm font-medium text-text-primary cursor-pointer">Enable FHIR Integration</Label>
124
+ <label className="relative inline-flex items-center cursor-pointer">
125
+ <input
126
+ type="checkbox"
127
+ id="enabled"
128
+ className="sr-only peer"
129
+ checked={formData.enabled}
130
+ onChange={handleChange}
131
+ />
132
+ <div className="w-11 h-6 bg-slate-200 dark:bg-slate-700 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-emerald-500 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-emerald-500"></div>
133
+ </label>
134
+ </div>
135
+ </div>
136
+ </CardHeader>
137
+ <CardContent className={`space-y-5 transition-opacity duration-300 ${formData.enabled ? 'opacity-100' : 'opacity-50 pointer-events-none'}`}>
138
+
139
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
140
+ {/* External FHIR Base URL */}
141
+ <div className="col-span-1 md:col-span-2">
142
+ <Label htmlFor="externalFhirBaseUrl" className="text-text-primary">External FHIR Base URL *</Label>
143
+ <Input
144
+ id="externalFhirBaseUrl"
145
+ value={formData.externalFhirBaseUrl}
146
+ onChange={handleChange}
147
+ placeholder="https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4/"
148
+ className="mt-1 bg-bg-panel border-border-primary text-text-primary"
149
+ />
150
+ </div>
151
+
152
+ {/* Authentication Type */}
153
+ <div>
154
+ <Label htmlFor="externalFhirAuthType" className="text-text-primary">Authentication Type</Label>
155
+ <select
156
+ id="externalFhirAuthType"
157
+ value={formData.externalFhirAuthType}
158
+ onChange={handleChange}
159
+ className="flex h-10 w-full rounded-md border border-border-primary bg-bg-panel px-3 py-2 text-sm text-text-primary mt-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
160
+ >
161
+ <option value="none">None / Open Endpoint</option>
162
+ <option value="basic">Basic (Client ID / Secret)</option>
163
+ <option value="bearer">Bearer Token</option>
164
+ </select>
165
+ </div>
166
+
167
+ {/* Authentication Details based on type */}
168
+ {formData.externalFhirAuthType === "basic" && (
169
+ <div className="col-span-1 md:col-span-2 grid grid-cols-2 gap-4">
170
+ <div>
171
+ <Label htmlFor="externalFhirClientId" className="text-text-primary">Client ID (Username)</Label>
172
+ <Input
173
+ id="externalFhirClientId"
174
+ value={formData.externalFhirClientId}
175
+ onChange={handleChange}
176
+ className="mt-1 bg-bg-panel border-border-primary text-text-primary"
177
+ />
178
+ </div>
179
+ <div>
180
+ <Label htmlFor="externalFhirClientSecret" className="text-text-primary">Client Secret (Password)</Label>
181
+ <Input
182
+ id="externalFhirClientSecret"
183
+ type="password"
184
+ value={formData.externalFhirClientSecret}
185
+ onChange={handleChange}
186
+ className="mt-1 bg-bg-panel border-border-primary text-text-primary"
187
+ />
188
+ </div>
189
+ </div>
190
+ )}
191
+
192
+ {formData.externalFhirAuthType === "bearer" && (
193
+ <div className="col-span-1 md:col-span-2">
194
+ <Label htmlFor="externalFhirBearerToken" className="text-text-primary">Bearer Token</Label>
195
+ <Input
196
+ id="externalFhirBearerToken"
197
+ type="password"
198
+ value={formData.externalFhirBearerToken}
199
+ onChange={handleChange}
200
+ placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
201
+ className="mt-1 bg-bg-panel border-border-primary text-text-primary"
202
+ />
203
+ </div>
204
+ )}
205
+
206
+ {/* Test Connection Button */}
207
+ <div className="col-span-1 md:col-span-2 mt-2">
208
+ <Button
209
+ variant="outline"
210
+ onClick={(e) => { e.preventDefault(); checkConnection(); }}
211
+ disabled={connStatus === "checking"}
212
+ className="w-full border-emerald-500 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-950/30 gap-2 disabled:opacity-50"
213
+ >
214
+ <RefreshCw size={16} className={connStatus === "checking" ? "animate-spin" : ""} />
215
+ {connStatus === "checking" ? "Connecting to FHIR Server..." : "Test FHIR Connection"}
216
+ </Button>
217
+ </div>
218
+
219
+ {connStatus === "invalid" && (
220
+ <div className="col-span-1 md:col-span-2 text-xs text-red-500 bg-red-500/10 p-3 rounded-md border border-red-500/20">
221
+ <strong>Connection Failed:</strong> {connError}
222
+ </div>
223
+ )}
224
+
225
+ {connStatus === "verified" && (
226
+ <div className="col-span-1 md:col-span-2 text-xs text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 p-3 rounded-md border border-emerald-500/20 flex flex-col gap-1">
227
+ <div className="flex items-center gap-1.5 font-medium">
228
+ <CheckCircle size={14} /> Connection Verified Successfully!
229
+ </div>
230
+ <div className="pl-5 opacity-90">
231
+ Server: <strong>{serverInfo.softwareName}</strong> (FHIR Version: <strong>{serverInfo.fhirVersion}</strong>)
232
+ </div>
233
+ </div>
234
+ )}
235
+
236
+ <div className="col-span-1 md:col-span-2 pt-2 pb-2">
237
+ <div className="border-t border-border-primary"></div>
238
+ </div>
239
+
240
+ {/* Feature Toggles */}
241
+ <div className="col-span-1 md:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-4">
242
+ <div className="flex items-start space-x-3 p-3 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-border-primary">
243
+ <div className="flex items-center h-5 mt-0.5">
244
+ <input
245
+ id="inboundServiceRequestEnabled"
246
+ type="checkbox"
247
+ checked={formData.inboundServiceRequestEnabled}
248
+ onChange={handleChange}
249
+ className="w-4 h-4 text-emerald-600 bg-slate-100 border-slate-300 rounded focus:ring-emerald-500 dark:focus:ring-emerald-600 dark:ring-offset-slate-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600"
250
+ />
251
+ </div>
252
+ <div className="text-sm">
253
+ <label htmlFor="inboundServiceRequestEnabled" className="font-medium text-text-primary">Enable Inbound Service Requests</label>
254
+ <p className="text-text-muted mt-1 text-xs">Allow external systems to create ServiceRequest orders in OmniRad via API.</p>
255
+ </div>
256
+ </div>
257
+
258
+ <div className="flex items-start space-x-3 p-3 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-border-primary">
259
+ <div className="flex items-center h-5 mt-0.5">
260
+ <input
261
+ id="outboundReadEnabled"
262
+ type="checkbox"
263
+ checked={formData.outboundReadEnabled}
264
+ onChange={handleChange}
265
+ className="w-4 h-4 text-emerald-600 bg-slate-100 border-slate-300 rounded focus:ring-emerald-500 dark:focus:ring-emerald-600 dark:ring-offset-slate-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600"
266
+ />
267
+ </div>
268
+ <div className="text-sm">
269
+ <label htmlFor="outboundReadEnabled" className="font-medium text-text-primary">Enable Outbound Lookups</label>
270
+ <p className="text-text-muted mt-1 text-xs">Allow OmniRad to fetch Patient demographics and previous DiagnosticReports from the external server.</p>
271
+ </div>
272
+ </div>
273
+ </div>
274
+
275
+ {/* Additional Local FHIR Auth Config (Inbound) */}
276
+ <div className="col-span-1 md:col-span-2 mt-4 space-y-4">
277
+ <h4 className="text-sm font-medium text-text-primary mb-2">Local API Authentication (Inbound)</h4>
278
+
279
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
280
+ <div>
281
+ <Label htmlFor="authMode" className="text-text-primary">Inbound Authentication Mode</Label>
282
+ <select
283
+ id="authMode"
284
+ value={formData.authMode}
285
+ onChange={handleChange}
286
+ className="flex h-10 w-full rounded-md border border-border-primary bg-bg-panel px-3 py-2 text-sm text-text-primary mt-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
287
+ >
288
+ <option value="none">No Auth (Not Recommended)</option>
289
+ <option value="bearer_token">Bearer Token</option>
290
+ </select>
291
+ </div>
292
+
293
+ <div>
294
+ <Label htmlFor="publicBaseUrl" className="text-text-primary">Public Base URL</Label>
295
+ <Input
296
+ id="publicBaseUrl"
297
+ value={formData.publicBaseUrl}
298
+ onChange={handleChange}
299
+ placeholder="https://omnirad.yourdomain.com"
300
+ className="mt-1 bg-bg-panel border-border-primary text-text-primary"
301
+ />
302
+ <p className="text-[11px] text-text-muted mt-1">Used for webhook callbacks and self-referencing URLs.</p>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ </div>
307
+
308
+ {saveStatus === "error" && errorMsg && (
309
+ <div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-center gap-2 text-red-500 dark:text-red-400 text-sm">
310
+ <AlertCircle size={16} /> {errorMsg}
311
+ </div>
312
+ )}
313
+
314
+ {saveStatus === "saved" && (
315
+ <div className="p-3 bg-emerald-500/10 border border-emerald-500/30 rounded-lg flex items-center gap-2 text-emerald-600 dark:text-emerald-400 text-sm">
316
+ <CheckCircle size={16} /> FHIR Configuration saved successfully!
317
+ </div>
318
+ )}
319
+
320
+ <Button
321
+ onClick={handleSave}
322
+ disabled={saveStatus === "saving"}
323
+ className="w-full mt-4 bg-emerald-600 hover:bg-emerald-700 text-white gap-2 disabled:opacity-50"
324
+ >
325
+ <Save size={16} />
326
+ {saveStatus === "saving" ? "Saving..." : "Save FHIR Configuration"}
327
+ </Button>
328
+ </CardContent>
329
+ </Card>
330
+ )
331
+ }
@@ -0,0 +1,269 @@
1
+ import * as React from "react";
2
+ import { ReportData, Comment, AuditLog } from "@/types";
3
+ import { Button } from "@/components/ui/basic";
4
+ import { Download, Printer, Edit, XCircle, CheckCircle, ChevronUp, ChevronDown, X } from "lucide-react";
5
+ import { ImageViewer } from "@/components/dashboard/ImageViewer";
6
+ import { CollaborationPanel } from "@/components/dashboard/CollaborationPanel";
7
+ import { StandardTemplate, ModernTemplate, MinimalTemplate } from "@/components/dashboard/ReportTemplates";
8
+
9
+ interface FullReportOverlayProps {
10
+ report: ReportData;
11
+ onClose: () => void;
12
+ onNewPatient: () => void;
13
+ onPrint: () => void;
14
+ onDownloadPDF: () => void;
15
+ onEdit: () => void;
16
+ onReject: () => void;
17
+ onApprove: () => void;
18
+ onUnreject: () => void;
19
+ onAddComment: (text: string) => void;
20
+ currentUser: { name: string; role: string };
21
+ reportId?: string;
22
+ imageSrc?: string | null;
23
+ images?: string[];
24
+ }
25
+
26
+ export function FullReportOverlay({
27
+ report,
28
+ onClose,
29
+ onNewPatient,
30
+ onPrint,
31
+ onDownloadPDF,
32
+ onEdit,
33
+ onReject,
34
+ onApprove,
35
+ onUnreject,
36
+ onAddComment,
37
+ currentUser,
38
+ reportId,
39
+ imageSrc,
40
+ images = []
41
+ }: FullReportOverlayProps) {
42
+ const [isImageCollapsed, setIsImageCollapsed] = React.useState(false);
43
+ const [selectedTemplate, setSelectedTemplate] = React.useState<string>('standard');
44
+ const [logoUrl, setLogoUrl] = React.useState<string>('');
45
+
46
+ // Local state for collaboration to ensure immediate UI updates
47
+ const [localComments, setLocalComments] = React.useState<Comment[]>(report.collaboration?.comments || []);
48
+ const [localLogs, setLocalLogs] = React.useState<AuditLog[]>(report.collaboration?.logs || []);
49
+
50
+ React.useEffect(() => {
51
+ setLocalComments(report.collaboration?.comments || []);
52
+ setLocalLogs(report.collaboration?.logs || []);
53
+
54
+ // Load template preference
55
+ fetch('/api/settings?type=appearance')
56
+ .then(res => res.json())
57
+ .then(config => {
58
+ if (config.template) {
59
+ setSelectedTemplate(config.template);
60
+ }
61
+ if (config.logo) {
62
+ setLogoUrl(config.logo);
63
+ }
64
+ })
65
+ .catch(e => console.error("Error loading template preference:", e));
66
+ }, [report.collaboration]);
67
+
68
+ const handleLocalAddComment = (text: string) => {
69
+ // Optimistic update
70
+ const newComment: Comment = {
71
+ id: Date.now().toString(),
72
+ author: currentUser.name,
73
+ role: currentUser.role,
74
+ text,
75
+ timestamp: new Date().toISOString()
76
+ };
77
+ const newLog: AuditLog = {
78
+ id: Date.now().toString() + "_log",
79
+ action: "Comment Added",
80
+ user: currentUser.name,
81
+ timestamp: new Date().toISOString(),
82
+ details: text.substring(0, 50) + (text.length > 50 ? "..." : "")
83
+ };
84
+ setLocalComments(prev => [...prev, newComment]);
85
+ setLocalLogs(prev => [...prev, newLog]);
86
+
87
+ // Call parent handler (which updates backend)
88
+ onAddComment(text);
89
+ };
90
+
91
+ const urgencyColor = report.urgency === 'Critical' ? 'text-red-600' :
92
+ report.urgency === 'Urgent' ? 'text-orange-600' : 'text-green-600';
93
+
94
+ const statusColor = report.report_footer.report_status === 'Approved' ? 'bg-green-100 text-green-800' :
95
+ report.report_footer.report_status === 'Rejected' ? 'bg-red-100 text-red-800' : 'bg-blue-100 text-blue-800';
96
+
97
+ // --- TEMPLATE RENDERERS ---
98
+
99
+ const getSelectedTemplateId = () => {
100
+ return selectedTemplate === 'modern' ? 'report-full-content-modern' :
101
+ selectedTemplate === 'minimal' ? 'report-full-content-minimal' :
102
+ 'report-full-content-standard';
103
+ };
104
+
105
+ return (
106
+ <div className="fixed inset-0 z-50 bg-bg-surface flex flex-col animate-in fade-in duration-200">
107
+ {/* Global Header */}
108
+ <div className="h-16 px-4 border-b border-border-primary bg-bg-panel flex items-center justify-between shrink-0 shadow-sm z-50">
109
+ <div className="flex items-center gap-4">
110
+ <div className="flex flex-col">
111
+ <h1 className="text-xl font-bold text-text-heading">{report.patient.name}</h1>
112
+ <span className="text-xs text-text-muted">
113
+ {report.patient.patient_id && <>{`ID: ${report.patient.patient_id}`} • </>}
114
+ {report.study.modality} • {report.study.examination}
115
+ </span>
116
+ </div>
117
+ <div className={`px-2 py-0.5 rounded-full ${statusColor} text-xs font-bold uppercase`}>
118
+ {report.report_footer.report_status}
119
+ </div>
120
+ </div>
121
+
122
+ <div className="flex items-center gap-2">
123
+ <div className="h-8 w-px bg-border-primary mx-2" />
124
+
125
+ {report.report_footer.report_status === 'Pending' && (
126
+ <>
127
+ <Button
128
+ variant="danger"
129
+ size="sm"
130
+ onClick={onReject}
131
+ className="bg-red-100 text-red-700 hover:bg-red-200 border-red-200"
132
+ >
133
+ Reject
134
+ </Button>
135
+ <Button
136
+ variant="success"
137
+ size="sm"
138
+ onClick={onApprove}
139
+ className="bg-green-100 text-green-700 hover:bg-green-200 border-green-200"
140
+ >
141
+ Approve
142
+ </Button>
143
+ <Button variant="outline" size="sm" onClick={onEdit} className="text-blue-600 border-blue-200 hover:bg-blue-50">
144
+ Edit
145
+ </Button>
146
+ </>
147
+ )}
148
+
149
+ {report.report_footer.report_status === 'Rejected' && (
150
+ <Button
151
+ variant="danger"
152
+ size="sm"
153
+ onClick={onUnreject}
154
+ className="bg-red-600 text-white hover:bg-red-700 border-red-600"
155
+ title="Click to Unreject"
156
+ >
157
+ Rejected
158
+ </Button>
159
+ )}
160
+
161
+ {report.report_footer.report_status === 'Approved' && (
162
+ <Button
163
+ variant="success"
164
+ size="sm"
165
+ className="bg-green-600 text-white hover:bg-green-700 border-green-600 cursor-default"
166
+ >
167
+ Approved
168
+ </Button>
169
+ )}
170
+
171
+ <div className="h-8 w-px bg-border-primary mx-2" />
172
+
173
+ <Button
174
+ variant="outline"
175
+ size="icon"
176
+ onClick={onPrint}
177
+ title="Print"
178
+ className="transition-all hover:bg-blue-50 hover:text-blue-600 hover:scale-105 active:scale-95"
179
+ >
180
+ <Printer size={18} />
181
+ </Button>
182
+ <Button
183
+ variant="outline"
184
+ size="icon"
185
+ onClick={onDownloadPDF}
186
+ title="Download PDF"
187
+ className="transition-all hover:bg-blue-50 hover:text-blue-600 hover:scale-105 active:scale-95"
188
+ >
189
+ <Download size={18} />
190
+ </Button>
191
+
192
+ <div className="h-8 w-px bg-border-primary mx-2" />
193
+
194
+ <Button
195
+ variant="ghost"
196
+ size="icon"
197
+ onClick={onClose}
198
+ className="hover:bg-red-100 hover:text-red-600 transition-all hover:rotate-90 active:scale-90"
199
+ >
200
+ <X size={24} />
201
+ </Button>
202
+ </div>
203
+ </div>
204
+
205
+ {/* Top Section: Collapsible Image Viewer */}
206
+ <div className={`relative transition-all duration-300 ease-in-out border-b border-border-primary bg-black ${isImageCollapsed ? 'h-[0px] border-b-0' : 'h-[45vh]'} shrink-0 group`}>
207
+ <div className={`absolute top-0 left-0 w-full h-full ${isImageCollapsed ? 'invisible' : 'visible'}`}>
208
+ <ImageViewer
209
+ imageSrc={imageSrc || report.image_data || null}
210
+ images={
211
+ images.length > 0 ? images :
212
+ report.images_data && report.images_data.length > 0 ? report.images_data :
213
+ report.image_data ? [report.image_data] : []
214
+ }
215
+ className="w-full h-full"
216
+ isCollapsed={isImageCollapsed}
217
+ onToggleCollapse={() => { }}
218
+ />
219
+ </div>
220
+
221
+ {/* Redesigned Collapse/Expand Toggle - Floating Pill */}
222
+ <div className={`absolute ${isImageCollapsed ? '-bottom-8' : 'bottom-4'} left-1/2 -translate-x-1/2 z-50 transition-all duration-300`}>
223
+ <button
224
+ onClick={() => setIsImageCollapsed(!isImageCollapsed)}
225
+ className="flex items-center gap-2 bg-bg-panel/90 backdrop-blur border border-border-primary px-4 py-1.5 rounded-full shadow-lg text-xs font-semibold text-text-primary hover:bg-bg-panel transition-all transform hover:scale-105"
226
+ >
227
+ {isImageCollapsed ? (
228
+ <>
229
+ <ChevronDown size={14} /> Show Image Viewer
230
+ </>
231
+ ) : (
232
+ <>
233
+ <ChevronUp size={14} /> Hide Image Viewer
234
+ </>
235
+ )}
236
+ </button>
237
+ </div>
238
+ </div>
239
+
240
+ {/* Bottom Section: Split Pane */}
241
+ <div className="flex-1 overflow-hidden flex flex-row">
242
+ {/* Left Column: Collaboration (30%) */}
243
+ <div className="w-[30%] min-w-[300px] border-r border-border-primary bg-bg-panel/50 flex flex-col overflow-hidden">
244
+ <div className="p-4 border-b border-border-primary bg-bg-surface">
245
+ <h3 className="font-bold text-text-heading">Collaboration & Audit</h3>
246
+ </div>
247
+ <div className="flex-1 overflow-y-auto p-4">
248
+ <CollaborationPanel
249
+ comments={localComments}
250
+ logs={localLogs}
251
+ onAddComment={handleLocalAddComment}
252
+ currentUser={currentUser}
253
+ />
254
+ </div>
255
+ </div>
256
+
257
+ {/* Right Column: Report (70%) */}
258
+ <div className="flex-1 bg-gray-100 overflow-y-auto p-8 relative">
259
+ {/* Render selected template */}
260
+ <div className="animate-in fade-in duration-300">
261
+ {selectedTemplate === 'modern' ? <ModernTemplate report={report} logoUrl={logoUrl} /> :
262
+ selectedTemplate === 'minimal' ? <MinimalTemplate report={report} logoUrl={logoUrl} /> :
263
+ <StandardTemplate report={report} logoUrl={logoUrl} />}
264
+ </div>
265
+ </div>
266
+ </div>
267
+ </div>
268
+ );
269
+ }