@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,454 @@
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, Trash2, AlertTriangle, Database, Skull, CheckCircle, X, Server, ArrowLeft, Paintbrush, BrainCircuit, Link2, HardDrive, Users, Shield } from "lucide-react"
6
+ import { useRouter } from "next/navigation"
7
+ import { UserManagementPanel } from "@/components/dashboard/UserManagementPanel"
8
+ import { AppearancePanel } from "@/components/dashboard/AppearancePanel"
9
+ import { AIConfigPanel } from "@/components/dashboard/AIConfigPanel"
10
+ import { CopilotConfigPanel } from "@/components/dashboard/CopilotConfigPanel"
11
+ import { SegmentationConfigPanel } from "@/components/dashboard/SegmentationConfigPanel"
12
+ import { FhirIntegrationPanel } from "@/components/dashboard/FhirIntegrationPanel"
13
+ import { SupabaseIntegrationPanel } from "@/components/dashboard/SupabaseIntegrationPanel"
14
+ import { SecurityPanel } from "@/components/settings/SecurityPanel"
15
+ import { resetSupabaseClient } from "@/lib/supabase"
16
+
17
+ type SettingsSection = "appearance" | "users" | "security" | "ai" | "integrations" | "storage"
18
+
19
+ const navItems: { id: SettingsSection; label: string; icon: React.ElementType }[] = [
20
+ { id: "appearance", label: "Appearance", icon: Paintbrush },
21
+ { id: "users", label: "User Management", icon: Users },
22
+ { id: "security", label: "Security", icon: Shield },
23
+ { id: "ai", label: "AI Configurations", icon: BrainCircuit },
24
+ { id: "integrations", label: "Integrations", icon: Link2 },
25
+ { id: "storage", label: "Storage Management", icon: HardDrive },
26
+ ]
27
+
28
+ export default function SettingsPage() {
29
+ const router = useRouter();
30
+ const [activeSection, setActiveSection] = React.useState<SettingsSection>("appearance")
31
+ const [config, setConfig] = React.useState({
32
+ n8nWebhookUrl: "",
33
+ supabaseUrl: "",
34
+ supabaseAnonKey: "",
35
+ pacsOrthancUrl: "",
36
+ pacsAuthType: "none",
37
+ pacsUsername: "",
38
+ pacsPassword: "",
39
+ pacsBearerToken: "",
40
+ pacsAeTitle: "",
41
+ });
42
+ const [isSaved, setIsSaved] = React.useState(false);
43
+ const [isLoading, setIsLoading] = React.useState(true);
44
+
45
+ // Modal & toast state
46
+ const [showClearModal, setShowClearModal] = React.useState(false);
47
+ const [showWipeModal, setShowWipeModal] = React.useState(false);
48
+ const [successMessage, setSuccessMessage] = React.useState("");
49
+ const [isProcessing, setIsProcessing] = React.useState(false);
50
+
51
+ React.useEffect(() => {
52
+ // Load from SQLite via API
53
+ fetch('/api/settings?type=config')
54
+ .then(res => res.json())
55
+ .then(data => {
56
+ setConfig({
57
+ n8nWebhookUrl: data.n8nWebhookUrl || "",
58
+ supabaseUrl: data.supabaseUrl || "",
59
+ supabaseAnonKey: data.supabaseAnonKey || "",
60
+ pacsOrthancUrl: data.pacsOrthancUrl || "",
61
+ pacsAuthType: data.pacsAuthType || "none",
62
+ pacsUsername: data.pacsUsername || "",
63
+ pacsPassword: data.pacsPassword || "",
64
+ pacsBearerToken: data.pacsBearerToken || "",
65
+ pacsAeTitle: data.pacsAeTitle || "",
66
+ });
67
+ })
68
+ .catch(e => console.error("Error loading config:", e))
69
+ .finally(() => setIsLoading(false));
70
+ }, []);
71
+
72
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
73
+ const { id, value } = e.target;
74
+ setConfig(prev => ({ ...prev, [id]: value }));
75
+ setIsSaved(false);
76
+ };
77
+
78
+ const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
79
+ const { id, value } = e.target;
80
+ setConfig(prev => ({ ...prev, [id]: value }));
81
+ setIsSaved(false);
82
+ };
83
+
84
+ const handleSave = async () => {
85
+ await fetch('/api/settings', {
86
+ method: 'PUT',
87
+ headers: { 'Content-Type': 'application/json' },
88
+ body: JSON.stringify({ type: 'config', data: config }),
89
+ });
90
+ // Reset cached Supabase client so new credentials take effect immediately
91
+ resetSupabaseClient();
92
+ setIsSaved(true);
93
+ setTimeout(() => setIsSaved(false), 2000);
94
+ };
95
+
96
+ const handleClearData = async () => {
97
+ setIsProcessing(true);
98
+ try {
99
+ const res = await fetch('/api/reports/clear', { method: 'DELETE' });
100
+ const data = await res.json();
101
+ if (res.ok && data.success) {
102
+ setShowClearModal(false);
103
+ setSuccessMessage("All local reports and patients have been cleared successfully!");
104
+ setTimeout(() => setSuccessMessage(""), 4000);
105
+ } else {
106
+ throw new Error(data.error || "Failed");
107
+ }
108
+ } catch (e) {
109
+ console.error(e);
110
+ setShowClearModal(false);
111
+ setSuccessMessage("");
112
+ } finally {
113
+ setIsProcessing(false);
114
+ }
115
+ };
116
+
117
+ const handleWipeData = async () => {
118
+ setIsProcessing(true);
119
+ try {
120
+ const res = await fetch('/api/auth/wipe', { method: 'DELETE' });
121
+ const data = await res.json();
122
+ if (res.ok && data.success) {
123
+ router.push('/setup');
124
+ } else {
125
+ throw new Error(data.error || "Failed");
126
+ }
127
+ } catch (e) {
128
+ console.error(e);
129
+ setShowWipeModal(false);
130
+ } finally {
131
+ setIsProcessing(false);
132
+ }
133
+ };
134
+
135
+ if (isLoading) {
136
+ return <div className="p-6 max-w-5xl mx-auto"><p className="text-text-muted">Loading settings...</p></div>;
137
+ }
138
+
139
+ return (
140
+ <div className="flex h-full">
141
+ {/* Settings Sidebar */}
142
+ <aside className="w-[260px] min-w-[260px] h-full bg-bg-surface border-r border-border-primary flex flex-col overflow-y-auto shrink-0">
143
+ {/* Back to app */}
144
+ <button
145
+ onClick={() => router.push('/')}
146
+ className="flex items-center gap-2 px-5 py-4 text-sm text-text-muted hover:text-text-primary transition-colors group"
147
+ >
148
+ <ArrowLeft size={16} className="transition-transform group-hover:-translate-x-0.5" />
149
+ <span>Back to app</span>
150
+ </button>
151
+
152
+ {/* Navigation Items */}
153
+ <nav className="flex flex-col px-3 pb-6 gap-0.5">
154
+ {navItems.map((item) => {
155
+ const Icon = item.icon
156
+ const isActive = activeSection === item.id
157
+ return (
158
+ <button
159
+ key={item.id}
160
+ onClick={() => setActiveSection(item.id)}
161
+ className={`
162
+ flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 text-left w-full
163
+ ${isActive
164
+ ? 'bg-white/10 text-text-heading shadow-sm'
165
+ : 'text-text-secondary hover:text-text-primary hover:bg-white/5'
166
+ }
167
+ `}
168
+ >
169
+ <Icon size={18} className={isActive ? 'text-primary' : 'text-text-muted'} />
170
+ <span>{item.label}</span>
171
+ </button>
172
+ )
173
+ })}
174
+ </nav>
175
+ </aside>
176
+
177
+ {/* Main Content Area */}
178
+ <main className="flex-1 overflow-y-auto">
179
+ <div className="max-w-4xl mx-auto p-8 space-y-6">
180
+ {/* Section Header */}
181
+ <div className="mb-2">
182
+ <h2 className="text-2xl font-semibold text-text-heading">
183
+ {navItems.find(n => n.id === activeSection)?.label}
184
+ </h2>
185
+ </div>
186
+
187
+ {/* Appearance Section */}
188
+ {activeSection === "appearance" && (
189
+ <div className="space-y-6">
190
+ <AppearancePanel />
191
+ </div>
192
+ )}
193
+
194
+ {/* User Management Section */}
195
+ {activeSection === "users" && (
196
+ <div className="space-y-6">
197
+ <UserManagementPanel />
198
+ </div>
199
+ )}
200
+
201
+ {/* Security Section */}
202
+ {activeSection === "security" && (
203
+ <div className="space-y-6">
204
+ <SecurityPanel />
205
+ </div>
206
+ )}
207
+
208
+ {/* AI Configurations Section */}
209
+ {activeSection === "ai" && (
210
+ <div className="space-y-6">
211
+ <AIConfigPanel />
212
+ <CopilotConfigPanel />
213
+ <SegmentationConfigPanel />
214
+ </div>
215
+ )}
216
+
217
+ {/* Integrations Section */}
218
+ {activeSection === "integrations" && (
219
+ <div className="space-y-6">
220
+ {/* Supabase Cloud Sync */}
221
+ <SupabaseIntegrationPanel
222
+ supabaseUrl={config.supabaseUrl}
223
+ supabaseAnonKey={config.supabaseAnonKey}
224
+ onConfigChange={(field, value) => {
225
+ setConfig(prev => ({ ...prev, [field]: value }));
226
+ setIsSaved(false);
227
+ }}
228
+ onSave={handleSave}
229
+ />
230
+
231
+ {/* PACS / Orthanc Configuration */}
232
+ <Card className="bg-bg-surface border-border-primary">
233
+ <CardHeader>
234
+ <CardTitle className="text-text-heading flex items-center gap-2">
235
+ <Server size={20} className="text-blue-500" />
236
+ PACS & Orthanc Configuration
237
+ </CardTitle>
238
+ <p className="text-sm text-text-secondary">Connect to a DICOMweb-compliant PACS server (e.g. Orthanc).</p>
239
+ </CardHeader>
240
+ <CardContent className="space-y-4">
241
+ <div>
242
+ <Label htmlFor="pacsOrthancUrl" className="text-text-primary">DICOMweb Base URL *</Label>
243
+ <Input
244
+ id="pacsOrthancUrl"
245
+ type="url"
246
+ placeholder="http://localhost:8042/dicom-web"
247
+ value={config.pacsOrthancUrl}
248
+ onChange={handleChange}
249
+ className="mt-1 bg-bg-panel border-border-primary text-text-primary placeholder-text-muted"
250
+ />
251
+ </div>
252
+
253
+ <div className="grid grid-cols-2 gap-4">
254
+ <div>
255
+ <Label htmlFor="pacsAuthType" className="text-text-primary">Authentication Type</Label>
256
+ <select
257
+ id="pacsAuthType"
258
+ value={config.pacsAuthType}
259
+ onChange={handleSelectChange}
260
+ 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-primary"
261
+ >
262
+ <option value="none">None</option>
263
+ <option value="basic">Basic (Username/Password)</option>
264
+ <option value="bearer">Bearer Token</option>
265
+ </select>
266
+ </div>
267
+ <div>
268
+ <Label htmlFor="pacsAeTitle" className="text-text-primary">Local AE Title (Optional)</Label>
269
+ <Input
270
+ id="pacsAeTitle"
271
+ type="text"
272
+ placeholder="OPENRAD"
273
+ value={config.pacsAeTitle}
274
+ onChange={handleChange}
275
+ className="mt-1 bg-bg-panel border-border-primary text-text-primary placeholder-text-muted"
276
+ />
277
+ </div>
278
+ </div>
279
+
280
+ {config.pacsAuthType === "basic" && (
281
+ <div className="grid grid-cols-2 gap-4">
282
+ <div>
283
+ <Label htmlFor="pacsUsername" className="text-text-primary">Username</Label>
284
+ <Input
285
+ id="pacsUsername"
286
+ type="text"
287
+ placeholder="orthanc"
288
+ value={config.pacsUsername}
289
+ onChange={handleChange}
290
+ className="mt-1 bg-bg-panel border-border-primary text-text-primary"
291
+ />
292
+ </div>
293
+ <div>
294
+ <Label htmlFor="pacsPassword" className="text-text-primary">Password</Label>
295
+ <Input
296
+ id="pacsPassword"
297
+ type="password"
298
+ placeholder="••••••••"
299
+ value={config.pacsPassword}
300
+ onChange={handleChange}
301
+ className="mt-1 bg-bg-panel border-border-primary text-text-primary"
302
+ />
303
+ </div>
304
+ </div>
305
+ )}
306
+
307
+ {config.pacsAuthType === "bearer" && (
308
+ <div>
309
+ <Label htmlFor="pacsBearerToken" className="text-text-primary">Bearer Token</Label>
310
+ <Input
311
+ id="pacsBearerToken"
312
+ type="password"
313
+ placeholder="eyJhbGciOiJIUzI1NiIsInR..."
314
+ value={config.pacsBearerToken}
315
+ onChange={handleChange}
316
+ className="mt-1 bg-bg-panel border-border-primary text-text-primary"
317
+ />
318
+ </div>
319
+ )}
320
+
321
+ <Button
322
+ onClick={handleSave}
323
+ className="w-full mt-4 bg-primary-main hover:bg-primary-hover text-white gap-2"
324
+ >
325
+ <Save size={16} />
326
+ {isSaved ? "Saved!" : "Save PACS Configuration"}
327
+ </Button>
328
+ </CardContent>
329
+ </Card>
330
+
331
+ <FhirIntegrationPanel />
332
+ </div>
333
+ )}
334
+
335
+ {/* Storage Management Section */}
336
+ {activeSection === "storage" && (
337
+ <div className="space-y-6">
338
+ {/* Database Management */}
339
+ <Card className="bg-bg-surface border-border-primary border-t-4 border-t-red-500">
340
+ <CardHeader>
341
+ <CardTitle className="text-text-heading flex items-center gap-2">
342
+ <Database size={20} className="text-red-500" />
343
+ Database Management
344
+ </CardTitle>
345
+ <p className="text-sm text-text-secondary">Manage your report storage and history.</p>
346
+ </CardHeader>
347
+ <CardContent className="space-y-4">
348
+ <div className="p-4 bg-red-50 dark:bg-red-950/20 border border-red-100 dark:border-red-500/20 rounded-lg flex gap-3 text-red-800 dark:text-red-400">
349
+ <AlertTriangle className="shrink-0 w-5 h-5" />
350
+ <div className="space-y-1">
351
+ <p className="font-semibold text-sm">Clear Local History</p>
352
+ <p className="text-xs opacity-90">This will remove all reports AND patients from your Local Database ONLY. Cloud data will remain safe.</p>
353
+ </div>
354
+ </div>
355
+
356
+ <Button
357
+ onClick={() => setShowClearModal(true)}
358
+ className="w-full bg-red-600 hover:bg-red-700 text-white font-semibold flex items-center justify-center gap-2"
359
+ >
360
+ <Trash2 size={16} />
361
+ Clear Local Reports & Patients
362
+ </Button>
363
+
364
+ {/* Divider */}
365
+ <div className="border-t border-red-200 dark:border-red-900/50 my-2"></div>
366
+
367
+ {/* Delete Account / Factory Reset */}
368
+ <div className="p-4 bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-500/40 rounded-lg flex gap-3 text-red-800 dark:text-red-400">
369
+ <Skull className="shrink-0 w-5 h-5 mt-0.5" />
370
+ <div className="space-y-1">
371
+ <p className="font-semibold text-sm text-red-800 dark:text-red-400">Factory Reset — Delete Everything</p>
372
+ <p className="text-xs opacity-80">This will permanently delete ALL users, reports, settings, and data. The app will restart from the initial setup screen. This action is irreversible.</p>
373
+ </div>
374
+ </div>
375
+
376
+ <Button
377
+ onClick={() => setShowWipeModal(true)}
378
+ className="w-full bg-red-900 hover:bg-red-800 text-red-100 font-semibold flex items-center justify-center gap-2 border border-red-700"
379
+ >
380
+ <Skull size={16} />
381
+ Delete Everything & Reset Application
382
+ </Button>
383
+ </CardContent>
384
+ </Card>
385
+ </div>
386
+ )}
387
+ </div>
388
+ </main>
389
+
390
+ {/* Success Toast */}
391
+ {successMessage && (
392
+ <div className="fixed top-6 right-6 z-[100] animate-in fade-in slide-in-from-top-4 duration-300">
393
+ <div className="bg-green-600 text-white px-5 py-3 rounded-xl shadow-2xl flex items-center gap-3">
394
+ <CheckCircle size={20} />
395
+ <span className="font-medium text-sm">{successMessage}</span>
396
+ <button onClick={() => setSuccessMessage("")} className="ml-2 hover:opacity-70"><X size={16} /></button>
397
+ </div>
398
+ </div>
399
+ )}
400
+
401
+ {/* Clear Reports Confirmation Modal */}
402
+ {showClearModal && (
403
+ <div className="fixed inset-0 z-[90] flex items-center justify-center bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
404
+ <div className="bg-bg-surface border border-border-primary rounded-2xl shadow-2xl max-w-md w-full mx-4 p-6 space-y-5 animate-in zoom-in-95 duration-200">
405
+ <div className="flex items-start gap-4">
406
+ <div className="p-3 bg-red-500/10 rounded-full shrink-0">
407
+ <AlertTriangle className="w-6 h-6 text-red-500" />
408
+ </div>
409
+ <div className="space-y-2">
410
+ <h3 className="text-lg font-semibold text-text-heading">Clear Local Report & Patient History?</h3>
411
+ <p className="text-sm text-text-muted">This will permanently delete <strong>all reports and patients</strong> from your local database. Your Supabase/Cloud data will remain safe.</p>
412
+ </div>
413
+ </div>
414
+ <div className="flex gap-3 justify-end pt-2">
415
+ <Button variant="outline" className="border-border-card text-text-primary hover:bg-slate-100 dark:hover:bg-white/10 hover:border-slate-300 dark:hover:border-white/20 hover:text-slate-900 dark:hover:text-white transition-all focus:ring-0" onClick={() => setShowClearModal(false)} disabled={isProcessing}>Cancel</Button>
416
+ <Button onClick={handleClearData} className="bg-red-600 hover:bg-red-700 text-white" disabled={isProcessing}>
417
+ {isProcessing ? "Clearing..." : "Yes, Clear Local Data"}
418
+ </Button>
419
+ </div>
420
+ </div>
421
+ </div>
422
+ )}
423
+
424
+ {/* Factory Reset Confirmation Modal */}
425
+ {showWipeModal && (
426
+ <div className="fixed inset-0 z-[90] flex items-center justify-center bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
427
+ <div className="bg-bg-surface border border-red-500/40 rounded-2xl shadow-2xl max-w-md w-full mx-4 p-6 space-y-5 animate-in zoom-in-95 duration-200">
428
+ <div className="flex items-start gap-4">
429
+ <div className="p-3 bg-red-500/10 rounded-full shrink-0">
430
+ <Skull className="w-6 h-6 text-red-500" />
431
+ </div>
432
+ <div className="space-y-2">
433
+ <h3 className="text-lg font-semibold text-red-400">Factory Reset — Delete Everything?</h3>
434
+ <p className="text-sm text-text-muted">This will <strong>permanently delete</strong>:</p>
435
+ <ul className="text-sm text-text-muted list-disc list-inside space-y-1">
436
+ <li>All user accounts</li>
437
+ <li>All reports & history</li>
438
+ <li>All settings & configuration</li>
439
+ </ul>
440
+ <p className="text-sm text-red-400 font-medium mt-2">The application will be completely reset. This action is irreversible.</p>
441
+ </div>
442
+ </div>
443
+ <div className="flex gap-3 justify-end pt-2">
444
+ <Button variant="outline" className="border-border-card text-text-primary hover:bg-slate-100 dark:hover:bg-white/10 hover:border-slate-300 dark:hover:border-white/20 hover:text-slate-900 dark:hover:text-white transition-all focus:ring-0" onClick={() => setShowWipeModal(false)} disabled={isProcessing}>Cancel</Button>
445
+ <Button onClick={handleWipeData} className="bg-red-900 hover:bg-red-800 text-red-100 border border-red-700" disabled={isProcessing}>
446
+ {isProcessing ? "Wiping..." : "Yes, Delete Everything"}
447
+ </Button>
448
+ </div>
449
+ </div>
450
+ </div>
451
+ )}
452
+ </div>
453
+ );
454
+ }
@@ -0,0 +1,199 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import Image from "next/image";
6
+ import { UserPlus, Loader2, Server, ArrowRight, Eye, EyeOff } from "lucide-react";
7
+
8
+ export default function SetupPage() {
9
+ const router = useRouter();
10
+
11
+ // Track visual steps (welcome vs form)
12
+ const [step, setStep] = useState<"welcome" | "form">("welcome");
13
+
14
+ const [loading, setLoading] = useState(false);
15
+ const [error, setError] = useState("");
16
+ const [showPassword, setShowPassword] = useState(false);
17
+
18
+ const [formData, setFormData] = useState({
19
+ fullName: "",
20
+ username: "",
21
+ email: "",
22
+ password: "",
23
+ });
24
+
25
+ const handleSubmit = async (e: React.FormEvent) => {
26
+ e.preventDefault();
27
+ setLoading(true);
28
+ setError("");
29
+
30
+ if (formData.password.length < 8) {
31
+ setError("Password must be at least 8 characters");
32
+ setLoading(false);
33
+ return;
34
+ }
35
+
36
+ try {
37
+ const res = await fetch("/api/auth/setup", {
38
+ method: "POST",
39
+ headers: { "Content-Type": "application/json" },
40
+ body: JSON.stringify(formData),
41
+ });
42
+
43
+ const data = await res.json();
44
+
45
+ if (!res.ok) {
46
+ throw new Error(data.error || "Failed to create admin account");
47
+ }
48
+
49
+ router.push("/");
50
+ router.refresh();
51
+ } catch (err: any) {
52
+ setError(err.message);
53
+ } finally {
54
+ setLoading(false);
55
+ }
56
+ };
57
+
58
+ return (
59
+ <div className="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-bg-secondary to-bg-primary">
60
+ <div className="w-full max-w-md bg-bg-surface border border-border-primary rounded-2xl shadow-2xl overflow-hidden relative">
61
+
62
+ <div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 via-primary to-blue-500" />
63
+
64
+ {step === "welcome" && (
65
+ <div className="p-10 text-center animate-in fade-in slide-in-from-bottom-4 duration-500">
66
+ <div className="flex justify-center mb-8 mt-2">
67
+ <div className="relative group">
68
+ <div className="absolute -inset-1 bg-gradient-to-r from-primary to-blue-500 rounded-full blur opacity-30 group-hover:opacity-60 transition duration-1000"></div>
69
+ <div className="relative bg-bg-panel border border-border-primary rounded-full p-2 flex flex-col items-center justify-center shadow-lg">
70
+ <Image src="/logo.svg" alt="OmniRad Logo" width={180} height={180} priority className="object-contain" />
71
+ </div>
72
+ </div>
73
+ </div>
74
+
75
+ <h1 className="text-3xl font-bold text-text-heading mb-4 tracking-tight">
76
+ Welcome to OmniRad
77
+ </h1>
78
+
79
+ <p className="text-text-secondary leading-relaxed mb-10">
80
+ Your intelligent AI-powered radiology workspace. Fast, secure, and designed to seamlessly enhance your diagnostic workflows. Let's get your environment set up.
81
+ </p>
82
+
83
+ <button
84
+ onClick={() => setStep("form")}
85
+ className="w-full group flex items-center justify-center space-x-2 bg-primary hover:bg-primary-hover text-white font-medium py-3 px-6 rounded-xl transition-all shadow-lg hover:shadow-primary/25 active:scale-[0.98]"
86
+ >
87
+ <span>Start Now</span>
88
+ <ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
89
+ </button>
90
+ </div>
91
+ )}
92
+
93
+ {step === "form" && (
94
+ <div className="p-8 animate-in fade-in slide-in-from-right-8 duration-500">
95
+ <div className="mb-6">
96
+ <h1 className="text-2xl font-bold text-text-heading mb-2">
97
+ Create Admin Account
98
+ </h1>
99
+ <p className="text-text-secondary text-sm">
100
+ Set up the initial Master Administrator account to secure your installation.
101
+ </p>
102
+ </div>
103
+
104
+ <form onSubmit={handleSubmit} className="space-y-4">
105
+ <div>
106
+ <label className="block text-sm font-medium text-text-secondary mb-1.5 ml-1">
107
+ Full Name
108
+ </label>
109
+ <input
110
+ type="text"
111
+ required
112
+ value={formData.fullName}
113
+ onChange={(e) => setFormData({ ...formData, fullName: e.target.value })}
114
+ className="w-full px-4 py-2.5 bg-bg-panel border border-border-primary rounded-xl focus:outline-none focus:ring-2 focus:ring-primary/50 text-text-primary transition-all shadow-sm"
115
+ placeholder="Dr. John Doe"
116
+ />
117
+ </div>
118
+
119
+ <div>
120
+ <label className="block text-sm font-medium text-text-secondary mb-1.5 ml-1">
121
+ Username
122
+ </label>
123
+ <input
124
+ type="text"
125
+ required
126
+ value={formData.username}
127
+ onChange={(e) => setFormData({ ...formData, username: e.target.value.toLowerCase().replace(/\s/g, '') })}
128
+ className="w-full px-4 py-2.5 bg-bg-panel border border-border-primary rounded-xl focus:outline-none focus:ring-2 focus:ring-primary/50 text-text-primary transition-all shadow-sm"
129
+ placeholder="johndoe"
130
+ />
131
+ </div>
132
+
133
+ <div>
134
+ <label className="block text-sm font-medium text-text-secondary mb-1.5 ml-1">
135
+ Email Address
136
+ </label>
137
+ <input
138
+ type="email"
139
+ required
140
+ value={formData.email}
141
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
142
+ className="w-full px-4 py-2.5 bg-bg-panel border border-border-primary rounded-xl focus:outline-none focus:ring-2 focus:ring-primary/50 text-text-primary transition-all shadow-sm"
143
+ placeholder="john@example.com"
144
+ />
145
+ </div>
146
+
147
+ <div>
148
+ <label className="block text-sm font-medium text-text-secondary mb-1.5 ml-1">
149
+ Secure Password
150
+ </label>
151
+ <div className="relative">
152
+ <input
153
+ type={showPassword ? "text" : "password"}
154
+ required
155
+ value={formData.password}
156
+ onChange={(e) => setFormData({ ...formData, password: e.target.value })}
157
+ className="w-full px-4 py-2.5 bg-bg-panel border border-border-primary rounded-xl focus:outline-none focus:ring-2 focus:ring-primary/50 text-text-primary transition-all shadow-sm pr-12"
158
+ placeholder="Minimum 8 characters"
159
+ />
160
+ <button
161
+ type="button"
162
+ onClick={() => setShowPassword(!showPassword)}
163
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-primary focus:outline-none transition-colors"
164
+ >
165
+ {showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
166
+ </button>
167
+ </div>
168
+ </div>
169
+
170
+ {error && (
171
+ <div className="p-3 bg-red-500/10 border border-red-500/30 rounded-xl text-red-500 text-sm animate-in zoom-in-95 duration-200">
172
+ {error}
173
+ </div>
174
+ )}
175
+
176
+ <button
177
+ type="submit"
178
+ disabled={loading}
179
+ className="w-full flex items-center justify-center space-x-2 bg-primary hover:bg-primary-hover text-white font-medium py-3 px-4 rounded-xl transition-all shadow-lg hover:shadow-primary/25 mt-8 disabled:opacity-70 disabled:cursor-not-allowed active:scale-[0.98]"
180
+ >
181
+ {loading ? (
182
+ <>
183
+ <Loader2 className="w-5 h-5 animate-spin" />
184
+ <span>Processing...</span>
185
+ </>
186
+ ) : (
187
+ <>
188
+ <UserPlus className="w-5 h-5" />
189
+ <span>Create System Admin</span>
190
+ </>
191
+ )}
192
+ </button>
193
+ </form>
194
+ </div>
195
+ )}
196
+ </div>
197
+ </div>
198
+ );
199
+ }