@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,418 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Card, CardContent, CardHeader, CardTitle, Button } from "@/components/ui/basic"
5
+ import { Shield, ShieldCheck, ShieldOff, Lock, Unlock, AlertTriangle, Clock, User } from "lucide-react"
6
+
7
+ interface SecuritySettings {
8
+ appLockEnabled: boolean
9
+ defaultUserId: string | null
10
+ updatedBy: string | null
11
+ updatedAt: string | null
12
+ updatedByName: string | null
13
+ }
14
+
15
+ interface CurrentUser {
16
+ id: string
17
+ fullName: string
18
+ username?: string
19
+ role: string
20
+ }
21
+
22
+ export function SecurityPanel() {
23
+ const [settings, setSettings] = React.useState<SecuritySettings>({
24
+ appLockEnabled: true,
25
+ defaultUserId: null,
26
+ updatedBy: null,
27
+ updatedAt: null,
28
+ updatedByName: null,
29
+ })
30
+ const [usersList, setUsersList] = React.useState<CurrentUser[]>([])
31
+ const [currentUser, setCurrentUser] = React.useState<CurrentUser | null>(null)
32
+ const [isLoading, setIsLoading] = React.useState(true)
33
+ const [isSaving, setIsSaving] = React.useState(false)
34
+ const [showConfirmModal, setShowConfirmModal] = React.useState(false)
35
+ const [pendingLockState, setPendingLockState] = React.useState<boolean>(true)
36
+ const [saveSuccess, setSaveSuccess] = React.useState(false)
37
+
38
+ // Load current user and security settings
39
+ React.useEffect(() => {
40
+ Promise.all([
41
+ fetch('/api/auth/me').then(r => r.json()),
42
+ fetch('/api/settings?type=security').then(r => r.json()),
43
+ fetch('/api/auth/users').then(r => r.json()),
44
+ ])
45
+ .then(([userData, secData, usersData]) => {
46
+ if (userData && !userData.error) {
47
+ setCurrentUser(userData)
48
+ }
49
+ setSettings({
50
+ appLockEnabled: secData.appLockEnabled ?? true,
51
+ defaultUserId: secData.defaultUserId || null,
52
+ updatedBy: secData.updatedBy || null,
53
+ updatedAt: secData.updatedAt || null,
54
+ updatedByName: secData.updatedByName || null,
55
+ })
56
+ if (usersData && Array.isArray(usersData)) {
57
+ setUsersList(usersData)
58
+ }
59
+ })
60
+ .catch(e => console.error("Error loading security settings:", e))
61
+ .finally(() => setIsLoading(false))
62
+ }, [])
63
+
64
+ const isAdmin = currentUser?.role === "Admin"
65
+
66
+ const handleToggleClick = (newState: boolean) => {
67
+ setPendingLockState(newState)
68
+ setShowConfirmModal(true)
69
+ }
70
+
71
+ const handleConfirm = async () => {
72
+ setShowConfirmModal(false)
73
+ setIsSaving(true)
74
+ try {
75
+ const res = await fetch('/api/settings', {
76
+ method: 'PUT',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({
79
+ type: 'security',
80
+ data: { appLockEnabled: pendingLockState, defaultUserId: settings.defaultUserId },
81
+ }),
82
+ })
83
+ if (res.ok) {
84
+ setSettings(prev => ({
85
+ ...prev,
86
+ appLockEnabled: pendingLockState,
87
+ updatedByName: currentUser?.fullName || null,
88
+ updatedAt: new Date().toISOString(),
89
+ }))
90
+ setSaveSuccess(true)
91
+ setTimeout(() => setSaveSuccess(false), 3000)
92
+ } else {
93
+ const data = await res.json()
94
+ console.error("Failed to save:", data.error)
95
+ }
96
+ } catch (e) {
97
+ console.error("Error saving security settings:", e)
98
+ } finally {
99
+ setIsSaving(false)
100
+ }
101
+ }
102
+
103
+ const handleUserChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
104
+ const newUserId = e.target.value
105
+ setSettings(prev => ({ ...prev, defaultUserId: newUserId }))
106
+ setIsSaving(true)
107
+ try {
108
+ const res = await fetch('/api/settings', {
109
+ method: 'PUT',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify({
112
+ type: 'security',
113
+ data: { appLockEnabled: settings.appLockEnabled, defaultUserId: newUserId },
114
+ }),
115
+ })
116
+ if (res.ok) {
117
+ setSettings(prev => ({
118
+ ...prev,
119
+ updatedByName: currentUser?.fullName || null,
120
+ updatedAt: new Date().toISOString(),
121
+ }))
122
+ setSaveSuccess(true)
123
+ setTimeout(() => setSaveSuccess(false), 3000)
124
+ } else {
125
+ console.error("Failed to save default user")
126
+ }
127
+ } catch (e) {
128
+ console.error("Error saving security settings:", e)
129
+ } finally {
130
+ setIsSaving(false)
131
+ }
132
+ }
133
+
134
+ const formatDate = (iso: string | null) => {
135
+ if (!iso) return null
136
+ try {
137
+ return new Date(iso).toLocaleDateString('en-US', {
138
+ year: 'numeric',
139
+ month: 'short',
140
+ day: 'numeric',
141
+ hour: '2-digit',
142
+ minute: '2-digit',
143
+ })
144
+ } catch {
145
+ return iso
146
+ }
147
+ }
148
+
149
+ if (isLoading) {
150
+ return (
151
+ <Card className="bg-bg-surface border-border-primary">
152
+ <CardContent className="py-12 text-center">
153
+ <div className="inline-flex items-center gap-2 text-text-muted">
154
+ <div className="w-4 h-4 border-2 border-text-muted border-t-transparent rounded-full animate-spin" />
155
+ Loading security settings...
156
+ </div>
157
+ </CardContent>
158
+ </Card>
159
+ )
160
+ }
161
+
162
+ // Non-admin users see a restricted message
163
+ if (!isAdmin) {
164
+ return (
165
+ <Card className="bg-bg-surface border-border-primary">
166
+ <CardHeader>
167
+ <CardTitle className="text-text-heading flex items-center gap-2">
168
+ <Shield size={20} className="text-amber-500" />
169
+ Security
170
+ </CardTitle>
171
+ </CardHeader>
172
+ <CardContent>
173
+ <div className="flex items-center gap-3 p-4 bg-amber-500/10 border border-amber-500/20 rounded-xl">
174
+ <Lock size={20} className="text-amber-500 shrink-0" />
175
+ <div>
176
+ <p className="text-sm font-medium text-text-primary">Admin Access Required</p>
177
+ <p className="text-xs text-text-muted mt-0.5">Only administrators can view and change security settings.</p>
178
+ </div>
179
+ </div>
180
+ </CardContent>
181
+ </Card>
182
+ )
183
+ }
184
+
185
+ return (
186
+ <>
187
+ {/* Main Security Card */}
188
+ <Card className="bg-bg-surface border-border-primary overflow-hidden">
189
+ <CardHeader className="pb-4">
190
+ <CardTitle className="text-text-heading flex items-center gap-2">
191
+ <Shield size={20} className="text-blue-500" />
192
+ App Lock Mode
193
+ </CardTitle>
194
+ <p className="text-sm text-text-secondary">
195
+ Control whether users need to log in when opening the application.
196
+ </p>
197
+ </CardHeader>
198
+ <CardContent className="space-y-5">
199
+ {/* Current Status */}
200
+ <div className={`
201
+ flex items-center justify-between p-4 rounded-xl border transition-all duration-500
202
+ ${settings.appLockEnabled
203
+ ? 'bg-emerald-500/5 border-emerald-500/20'
204
+ : 'bg-amber-500/5 border-amber-500/20'
205
+ }
206
+ `}>
207
+ <div className="flex items-center gap-3">
208
+ <div className={`
209
+ p-2.5 rounded-xl transition-all duration-500
210
+ ${settings.appLockEnabled
211
+ ? 'bg-emerald-500/15 text-emerald-500'
212
+ : 'bg-amber-500/15 text-amber-500'
213
+ }
214
+ `}>
215
+ {settings.appLockEnabled
216
+ ? <ShieldCheck size={22} />
217
+ : <ShieldOff size={22} />
218
+ }
219
+ </div>
220
+ <div>
221
+ <p className={`text-sm font-semibold ${settings.appLockEnabled ? 'text-emerald-400' : 'text-amber-400'}`}>
222
+ {settings.appLockEnabled ? "App is Locked" : "App is Unlocked"}
223
+ </p>
224
+ <p className="text-xs text-text-muted mt-0.5">
225
+ {settings.appLockEnabled
226
+ ? "Login with username & password is required to access the app."
227
+ : "The app opens directly without login — auto-logged in as admin."
228
+ }
229
+ </p>
230
+ </div>
231
+ </div>
232
+
233
+ {/* Toggle Button */}
234
+ <button
235
+ onClick={() => handleToggleClick(!settings.appLockEnabled)}
236
+ disabled={isSaving}
237
+ className={`
238
+ relative inline-flex h-7 w-[52px] shrink-0 cursor-pointer items-center rounded-full border-2 transition-all duration-300
239
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-bg-surface
240
+ disabled:cursor-not-allowed disabled:opacity-50
241
+ ${settings.appLockEnabled
242
+ ? 'bg-emerald-500 border-emerald-500'
243
+ : 'bg-zinc-600 border-zinc-600'
244
+ }
245
+ `}
246
+ role="switch"
247
+ aria-checked={settings.appLockEnabled}
248
+ aria-label="Toggle app lock"
249
+ >
250
+ <span className={`
251
+ pointer-events-none flex items-center justify-center h-5 w-5 rounded-full bg-white shadow-lg transition-all duration-300
252
+ ${settings.appLockEnabled ? 'translate-x-[26px]' : 'translate-x-[3px]'}
253
+ `}>
254
+ {settings.appLockEnabled
255
+ ? <Lock size={11} className="text-emerald-600" />
256
+ : <Unlock size={11} className="text-zinc-500" />
257
+ }
258
+ </span>
259
+ </button>
260
+ </div>
261
+
262
+ {/* Unlocked Warning */}
263
+ {!settings.appLockEnabled && (
264
+ <div className="flex gap-3 p-3.5 bg-amber-500/8 border border-amber-500/20 rounded-xl animate-in fade-in slide-in-from-top-2 duration-300">
265
+ <AlertTriangle size={18} className="text-amber-500 shrink-0 mt-0.5" />
266
+ <div>
267
+ <p className="text-sm font-medium text-amber-400">Security Notice</p>
268
+ <p className="text-xs text-text-muted mt-1">
269
+ Anyone with physical access to this workstation can use the application without credentials.
270
+ Only use this mode on trusted, single-user workstations.
271
+ </p>
272
+ </div>
273
+ </div>
274
+ )}
275
+
276
+ {/* Auto-login User Selection */}
277
+ {!settings.appLockEnabled && (
278
+ <div className="flex flex-col gap-2 p-4 bg-bg-panel border border-border-primary rounded-xl animate-in fade-in slide-in-from-top-2 duration-300">
279
+ <label htmlFor="defaultUserId" className="text-sm font-medium text-text-primary">
280
+ Auto-Login User
281
+ </label>
282
+ <p className="text-xs text-text-muted mb-2">
283
+ Select which user account the application will automatically log into when opened.
284
+ </p>
285
+ <select
286
+ id="defaultUserId"
287
+ value={settings.defaultUserId || ""}
288
+ onChange={handleUserChange}
289
+ disabled={isSaving}
290
+ className="flex h-10 w-full rounded-md border border-border-primary bg-bg-surface px-3 py-2 text-sm text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:opacity-50"
291
+ >
292
+ <option value="" disabled>Select a user</option>
293
+ {usersList.map(user => (
294
+ <option key={user.id} value={user.id}>
295
+ {user.fullName} (@{user.username || 'user'}) - {user.role}
296
+ </option>
297
+ ))}
298
+ </select>
299
+ </div>
300
+ )}
301
+
302
+ {/* Last Changed Info */}
303
+ {settings.updatedAt && (
304
+ <div className="flex items-center gap-4 pt-2 border-t border-border-primary">
305
+ <div className="flex items-center gap-1.5 text-xs text-text-muted">
306
+ <User size={12} />
307
+ <span>Changed by <span className="text-text-secondary font-medium">{settings.updatedByName || 'Unknown'}</span></span>
308
+ </div>
309
+ <div className="flex items-center gap-1.5 text-xs text-text-muted">
310
+ <Clock size={12} />
311
+ <span>{formatDate(settings.updatedAt)}</span>
312
+ </div>
313
+ </div>
314
+ )}
315
+ </CardContent>
316
+ </Card>
317
+
318
+ {/* How It Works Card */}
319
+ <Card className="bg-bg-surface border-border-primary">
320
+ <CardHeader className="pb-3">
321
+ <CardTitle className="text-text-heading text-base flex items-center gap-2">
322
+ How It Works
323
+ </CardTitle>
324
+ </CardHeader>
325
+ <CardContent>
326
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
327
+ <div className="flex gap-3 p-3 rounded-lg bg-white/[0.02]">
328
+ <div className="p-2 rounded-lg bg-emerald-500/10 h-fit">
329
+ <Lock size={16} className="text-emerald-500" />
330
+ </div>
331
+ <div>
332
+ <p className="text-sm font-medium text-text-primary">Locked Mode</p>
333
+ <p className="text-xs text-text-muted mt-1">
334
+ Users must enter their username and password each time they open the app. Best for shared workstations.
335
+ </p>
336
+ </div>
337
+ </div>
338
+ <div className="flex gap-3 p-3 rounded-lg bg-white/[0.02]">
339
+ <div className="p-2 rounded-lg bg-amber-500/10 h-fit">
340
+ <Unlock size={16} className="text-amber-500" />
341
+ </div>
342
+ <div>
343
+ <p className="text-sm font-medium text-text-primary">Unlocked Mode</p>
344
+ <p className="text-xs text-text-muted mt-1">
345
+ The app opens directly as the admin user — no login screen. Best for personal, single-user workstations.
346
+ </p>
347
+ </div>
348
+ </div>
349
+ </div>
350
+ </CardContent>
351
+ </Card>
352
+
353
+ {/* Save Success Toast */}
354
+ {saveSuccess && (
355
+ <div className="fixed top-6 right-6 z-[100] animate-in fade-in slide-in-from-top-4 duration-300">
356
+ <div className="bg-emerald-600 text-white px-5 py-3 rounded-xl shadow-2xl flex items-center gap-3">
357
+ <ShieldCheck size={20} />
358
+ <span className="font-medium text-sm">
359
+ {settings.appLockEnabled ? "App locked — login required" : "App unlocked — direct access enabled"}
360
+ </span>
361
+ </div>
362
+ </div>
363
+ )}
364
+
365
+ {/* Confirmation Modal */}
366
+ {showConfirmModal && (
367
+ <div className="fixed inset-0 z-[90] flex items-center justify-center bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
368
+ <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">
369
+ <div className="flex items-start gap-4">
370
+ <div className={`p-3 rounded-full shrink-0 ${pendingLockState ? 'bg-emerald-500/10' : 'bg-amber-500/10'}`}>
371
+ {pendingLockState
372
+ ? <Lock className="w-6 h-6 text-emerald-500" />
373
+ : <Unlock className="w-6 h-6 text-amber-500" />
374
+ }
375
+ </div>
376
+ <div className="space-y-2">
377
+ <h3 className="text-lg font-semibold text-text-heading">
378
+ {pendingLockState ? "Lock the Application?" : "Unlock the Application?"}
379
+ </h3>
380
+ <p className="text-sm text-text-muted">
381
+ {pendingLockState
382
+ ? "All users will need to enter their username and password to access the application."
383
+ : "The application will open directly without a login screen. Anyone with access to this workstation can use the app."
384
+ }
385
+ </p>
386
+ </div>
387
+ </div>
388
+ <div className="flex gap-3 justify-end pt-2">
389
+ <Button
390
+ variant="outline"
391
+ 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"
392
+ onClick={() => setShowConfirmModal(false)}
393
+ disabled={isSaving}
394
+ >
395
+ Cancel
396
+ </Button>
397
+ <Button
398
+ onClick={handleConfirm}
399
+ className={`text-white ${pendingLockState
400
+ ? 'bg-emerald-600 hover:bg-emerald-700'
401
+ : 'bg-amber-600 hover:bg-amber-700'
402
+ }`}
403
+ disabled={isSaving}
404
+ >
405
+ {isSaving
406
+ ? "Saving..."
407
+ : pendingLockState
408
+ ? "Yes, Lock App"
409
+ : "Yes, Unlock App"
410
+ }
411
+ </Button>
412
+ </div>
413
+ </div>
414
+ </div>
415
+ )}
416
+ </>
417
+ )
418
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ export const Badge = React.forwardRef<
5
+ HTMLDivElement,
6
+ React.HTMLAttributes<HTMLDivElement> & { variant?: "default" | "secondary" | "destructive" | "outline" }
7
+ >(({ className, variant = "default", ...props }, ref) => {
8
+ return (
9
+ <div ref={ref} className={cn(
10
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
11
+ variant === "default" && "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
12
+ variant === "secondary" && "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
13
+ variant === "destructive" && "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
14
+ variant === "outline" && "text-foreground",
15
+ className
16
+ )} {...props} />
17
+ )
18
+ });
19
+ Badge.displayName = "Badge";
@@ -0,0 +1,156 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ // Button Component
5
+ export interface ButtonProps
6
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
7
+ variant?: "default" | "outline" | "ghost" | "danger" | "success"
8
+ size?: "default" | "sm" | "lg" | "icon"
9
+ }
10
+
11
+ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
12
+ ({ className, variant = "default", size = "default", ...props }, ref) => {
13
+ return (
14
+ <button
15
+ className={cn(
16
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
17
+ {
18
+ "bg-primary text-text-heading hover:bg-primary-hover": variant === "default",
19
+ "border border-border-primary bg-transparent hover:bg-bg-panel text-text-primary": variant === "outline",
20
+ "hover:bg-bg-panel hover:text-text-heading": variant === "ghost",
21
+ "bg-danger text-text-heading hover:bg-danger/90": variant === "danger",
22
+ "bg-success text-text-heading hover:bg-success/90": variant === "success",
23
+ "h-10 px-4 py-2": size === "default",
24
+ "h-9 rounded-md px-3": size === "sm",
25
+ "h-11 rounded-md px-8": size === "lg",
26
+ "h-10 w-10": size === "icon",
27
+ },
28
+ className
29
+ )}
30
+ ref={ref}
31
+ {...props}
32
+ />
33
+ )
34
+ }
35
+ )
36
+ Button.displayName = "Button"
37
+
38
+ // Input Component
39
+ export interface InputProps
40
+ extends React.InputHTMLAttributes<HTMLInputElement> { }
41
+
42
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
43
+ ({ className, type, ...props }, ref) => {
44
+ return (
45
+ <input
46
+ type={type}
47
+ className={cn(
48
+ "flex h-10 w-full rounded-md border border-border-primary bg-bg-panel px-3 py-2 text-sm text-text-primary ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50",
49
+ className
50
+ )}
51
+ ref={ref}
52
+ {...props}
53
+ />
54
+ )
55
+ }
56
+ )
57
+ Input.displayName = "Input"
58
+
59
+ // Textarea Component
60
+ export interface TextareaProps
61
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { }
62
+
63
+ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
64
+ ({ className, ...props }, ref) => {
65
+ return (
66
+ <textarea
67
+ className={cn(
68
+ "flex min-h-[80px] w-full rounded-md border border-border-primary bg-bg-panel px-3 py-2 text-sm text-text-primary ring-offset-background placeholder:text-text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50",
69
+ className
70
+ )}
71
+ ref={ref}
72
+ {...props}
73
+ />
74
+ )
75
+ }
76
+ )
77
+ Textarea.displayName = "Textarea"
78
+
79
+ // Label Component
80
+ export const Label = React.forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
81
+ ({ className, ...props }, ref) => (
82
+ <label
83
+ ref={ref}
84
+ className={cn(
85
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-text-secondary mb-2 block",
86
+ className
87
+ )}
88
+ {...props}
89
+ />
90
+ )
91
+ )
92
+ Label.displayName = "Label"
93
+
94
+ // Card Component
95
+ export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
96
+ ({ className, ...props }, ref) => (
97
+ <div
98
+ ref={ref}
99
+ className={cn(
100
+ "rounded-lg border border-border-primary bg-bg-surface text-text-primary shadow-sm",
101
+ className
102
+ )}
103
+ {...props}
104
+ />
105
+ )
106
+ )
107
+ Card.displayName = "Card"
108
+
109
+ export const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
110
+ ({ className, ...props }, ref) => (
111
+ <div
112
+ ref={ref}
113
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
114
+ {...props}
115
+ />
116
+ )
117
+ )
118
+ CardHeader.displayName = "CardHeader"
119
+
120
+ export const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
121
+ ({ className, ...props }, ref) => (
122
+ <h3
123
+ ref={ref}
124
+ className={cn(
125
+ "text-lg font-semibold leading-none tracking-tight text-text-heading",
126
+ className
127
+ )}
128
+ {...props}
129
+ />
130
+ )
131
+ )
132
+ CardTitle.displayName = "CardTitle"
133
+
134
+ export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
135
+ ({ className, ...props }, ref) => (
136
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
137
+ )
138
+ )
139
+ CardContent.displayName = "CardContent"
140
+
141
+ export const Badge = React.forwardRef<
142
+ HTMLDivElement,
143
+ React.HTMLAttributes<HTMLDivElement> & { variant?: "default" | "secondary" | "destructive" | "outline" }
144
+ >(({ className, variant = "default", ...props }, ref) => {
145
+ return (
146
+ <div ref={ref} className={cn(
147
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
148
+ variant === "default" && "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
149
+ variant === "secondary" && "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
150
+ variant === "destructive" && "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
151
+ variant === "outline" && "text-foreground",
152
+ className
153
+ )} {...props} />
154
+ )
155
+ });
156
+ Badge.displayName = "Badge";