@omniradiology/omnirad 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +438 -0
- package/app/api/ai-config/route.ts +131 -0
- package/app/api/ai-config/test/route.ts +49 -0
- package/app/api/auth/auto-login/route.ts +66 -0
- package/app/api/auth/check/route.ts +17 -0
- package/app/api/auth/login/route.ts +72 -0
- package/app/api/auth/logout/route.ts +25 -0
- package/app/api/auth/me/route.ts +75 -0
- package/app/api/auth/password/route.ts +49 -0
- package/app/api/auth/setup/route.ts +63 -0
- package/app/api/auth/users/route.ts +100 -0
- package/app/api/auth/wipe/route.ts +27 -0
- package/app/api/compliance/anonymize/patient/[id]/route.ts +104 -0
- package/app/api/compliance/audit/route.ts +110 -0
- package/app/api/compliance/export/patient/[id]/route.ts +108 -0
- package/app/api/compliance/restrict/patient/[id]/route.ts +59 -0
- package/app/api/compliance/settings/route.ts +93 -0
- package/app/api/copilot/annotate/route.ts +94 -0
- package/app/api/copilot/chat/route.ts +238 -0
- package/app/api/copilot/history/route.ts +95 -0
- package/app/api/copilot/reports/route.ts +81 -0
- package/app/api/fhir/Bundle/report/[id]/route.ts +85 -0
- package/app/api/fhir/DiagnosticReport/[id]/route.ts +45 -0
- package/app/api/fhir/ImagingStudy/[id]/route.ts +57 -0
- package/app/api/fhir/Patient/[id]/route.ts +26 -0
- package/app/api/fhir/ServiceRequest/route.ts +85 -0
- package/app/api/fhir/config/route.ts +102 -0
- package/app/api/fhir/config/test-connection/route.ts +49 -0
- package/app/api/fhir/metadata/route.ts +51 -0
- package/app/api/pacs/metadata/route.ts +32 -0
- package/app/api/pacs/qido/instances/route.ts +39 -0
- package/app/api/pacs/qido/series/route.ts +38 -0
- package/app/api/pacs/qido/studies/route.ts +37 -0
- package/app/api/pacs/test/route.ts +30 -0
- package/app/api/pacs/wado/render/route.ts +51 -0
- package/app/api/patients/[id]/reports/route.ts +18 -0
- package/app/api/patients/[id]/route.ts +43 -0
- package/app/api/patients/merge/route.ts +57 -0
- package/app/api/patients/route.ts +67 -0
- package/app/api/patients/search/route.ts +25 -0
- package/app/api/reports/[id]/route.ts +84 -0
- package/app/api/reports/[id]/status/route.ts +87 -0
- package/app/api/reports/clear/route.ts +16 -0
- package/app/api/reports/route.ts +112 -0
- package/app/api/segmentation-config/route.ts +238 -0
- package/app/api/settings/route.ts +245 -0
- package/app/api/settings/test-supabase/route.ts +103 -0
- package/app/api/upload/route.ts +48 -0
- package/app/copilot/page.tsx +30 -0
- package/app/globals.css +141 -0
- package/app/history/page.tsx +242 -0
- package/app/icon.svg +3 -0
- package/app/layout.tsx +47 -0
- package/app/login/page.tsx +175 -0
- package/app/pacs/page.tsx +78 -0
- package/app/page.tsx +125 -0
- package/app/patients/[id]/page.tsx +315 -0
- package/app/patients/page.tsx +110 -0
- package/app/profile/page.tsx +208 -0
- package/app/reports/page.tsx +432 -0
- package/app/settings/page.tsx +454 -0
- package/app/setup/page.tsx +199 -0
- package/components/admin/AuditLogTable.tsx +293 -0
- package/components/copilot/ActivityIndicator.tsx +215 -0
- package/components/copilot/ChatHistoryPanel.tsx +140 -0
- package/components/copilot/ChatMessage.tsx +251 -0
- package/components/copilot/ClickableReference.tsx +40 -0
- package/components/copilot/CopilotCornerstoneViewer.tsx +562 -0
- package/components/copilot/CopilotPanel.tsx +311 -0
- package/components/copilot/FindingsList.tsx +75 -0
- package/components/copilot/ViewerPanel.tsx +460 -0
- package/components/copilot/WorkspaceLayout.tsx +398 -0
- package/components/dashboard/AIConfigPanel.tsx +339 -0
- package/components/dashboard/AppearancePanel.tsx +491 -0
- package/components/dashboard/ApprovalModal.tsx +163 -0
- package/components/dashboard/CollaborationPanel.tsx +134 -0
- package/components/dashboard/CopilotConfigPanel.tsx +337 -0
- package/components/dashboard/DicomViewer.tsx +645 -0
- package/components/dashboard/FhirIntegrationPanel.tsx +331 -0
- package/components/dashboard/FullReportOverlay.tsx +269 -0
- package/components/dashboard/ImageViewer.tsx +541 -0
- package/components/dashboard/PatientForm.tsx +597 -0
- package/components/dashboard/RejectionModal.tsx +74 -0
- package/components/dashboard/ReportEditor.tsx +160 -0
- package/components/dashboard/ReportTemplates.tsx +729 -0
- package/components/dashboard/ReportView.tsx +539 -0
- package/components/dashboard/SegmentationConfigPanel.tsx +490 -0
- package/components/dashboard/StudyPlaceholder.tsx +17 -0
- package/components/dashboard/SupabaseIntegrationPanel.tsx +345 -0
- package/components/dashboard/UserManagementPanel.tsx +272 -0
- package/components/layout/ClientLayout.tsx +39 -0
- package/components/layout/Header.tsx +20 -0
- package/components/layout/Sidebar.tsx +119 -0
- package/components/pacs/PacsImageViewerModal.tsx +121 -0
- package/components/pacs/PacsSearchFilters.tsx +117 -0
- package/components/pacs/PacsSeriesViewer.tsx +190 -0
- package/components/pacs/PacsStudyTable.tsx +113 -0
- package/components/patients/patient-card.tsx +117 -0
- package/components/patients/patient-header.tsx +122 -0
- package/components/patients/patient-search.tsx +137 -0
- package/components/patients/patient-timeline.tsx +153 -0
- package/components/settings/ComplianceSettingsPanel.tsx +278 -0
- package/components/settings/SecurityPanel.tsx +418 -0
- package/components/ui/badge.tsx +19 -0
- package/components/ui/basic.tsx +156 -0
- package/db/index.ts +350 -0
- package/db/migrations/0000_odd_quasimodo.sql +117 -0
- package/db/migrations/meta/0000_snapshot.json +778 -0
- package/db/migrations/meta/_journal.json +13 -0
- package/db/schema.ts +239 -0
- package/drizzle.config.ts +10 -0
- package/lib/api.ts +689 -0
- package/lib/auth.ts +22 -0
- package/lib/copilot/action-executor.ts +94 -0
- package/lib/copilot/action-types.ts +72 -0
- package/lib/copilot/coordinate-mapper.ts +84 -0
- package/lib/dicomImageExtractor.ts +103 -0
- package/lib/dicomMetadataParser.ts +111 -0
- package/lib/fhir/client.ts +25 -0
- package/lib/fhir/constants.ts +21 -0
- package/lib/fhir/diagnostic-report.ts +88 -0
- package/lib/fhir/helpers.ts +73 -0
- package/lib/fhir/imaging-study.ts +49 -0
- package/lib/fhir/patient.ts +55 -0
- package/lib/fhir/service-request.ts +85 -0
- package/lib/fhir.ts +6 -0
- package/lib/pacs/dicom-utils.ts +72 -0
- package/lib/pacs/dicomweb.ts +72 -0
- package/lib/pacs/server-utils.ts +37 -0
- package/lib/patients.ts +25 -0
- package/lib/pdfHelper.ts +119 -0
- package/lib/reportHtmlGenerator.ts +581 -0
- package/lib/security/audit.ts +180 -0
- package/lib/security/authz.ts +246 -0
- package/lib/security/phi-redaction.ts +156 -0
- package/lib/security/rate-limit.ts +106 -0
- package/lib/security/secrets.ts +179 -0
- package/lib/supabase.ts +72 -0
- package/lib/utils.ts +6 -0
- package/next.config.ts +35 -0
- package/package.json +76 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo.svg +8 -0
- package/public/next.svg +1 -0
- package/public/omnirad-favicon.svg +8 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +34 -0
- package/types/copilot-viewer.ts +155 -0
- package/types/copilot.ts +105 -0
- package/types/fhir.ts +21 -0
- package/types/html2pdf.d.ts +20 -0
- package/types/index.ts +139 -0
- package/types/pacs.ts +41 -0
|
@@ -0,0 +1,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
|
+
}
|