@omersx/bemms 0.0.0-stage → 1.0.0

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 (335) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +203 -2
  3. package/drizzle.config.ts +11 -0
  4. package/next.config.ts +51 -0
  5. package/package.json +99 -3
  6. package/postcss.config.mjs +7 -0
  7. package/public/hospital_demo_devices.csv +32 -0
  8. package/public/icons/icon-192.png +0 -0
  9. package/public/icons/icon-512.png +0 -0
  10. package/public/icons/icon-maskable-512.png +0 -0
  11. package/public/manifest.webmanifest +31 -0
  12. package/public/sw.js +177 -0
  13. package/src/app/(auth)/layout.tsx +20 -0
  14. package/src/app/(auth)/login/page.tsx +135 -0
  15. package/src/app/(dashboard)/admin/audit-logs/page.tsx +178 -0
  16. package/src/app/(dashboard)/admin/data/page.tsx +37 -0
  17. package/src/app/(dashboard)/admin/departments/[deptId]/edit/page.tsx +206 -0
  18. package/src/app/(dashboard)/admin/departments/[deptId]/page.tsx +71 -0
  19. package/src/app/(dashboard)/admin/departments/new/page.tsx +6 -0
  20. package/src/app/(dashboard)/admin/departments/page.tsx +6 -0
  21. package/src/app/(dashboard)/admin/general/page.tsx +374 -0
  22. package/src/app/(dashboard)/admin/hospitals/[hospitalId]/edit/page.tsx +304 -0
  23. package/src/app/(dashboard)/admin/hospitals/[hospitalId]/page.tsx +94 -0
  24. package/src/app/(dashboard)/admin/hospitals/new/page.tsx +302 -0
  25. package/src/app/(dashboard)/admin/hospitals/page.tsx +6 -0
  26. package/src/app/(dashboard)/admin/layout.tsx +49 -0
  27. package/src/app/(dashboard)/admin/organizations/[orgId]/edit/page.tsx +172 -0
  28. package/src/app/(dashboard)/admin/organizations/[orgId]/page.tsx +82 -0
  29. package/src/app/(dashboard)/admin/organizations/new/page.tsx +150 -0
  30. package/src/app/(dashboard)/admin/organizations/page.tsx +6 -0
  31. package/src/app/(dashboard)/admin/page.tsx +258 -0
  32. package/src/app/(dashboard)/admin/roles/page.tsx +153 -0
  33. package/src/app/(dashboard)/admin/settings/page.tsx +69 -0
  34. package/src/app/(dashboard)/admin/users/[userId]/edit/page.tsx +153 -0
  35. package/src/app/(dashboard)/admin/users/[userId]/page.tsx +317 -0
  36. package/src/app/(dashboard)/admin/users/invite/page.tsx +3 -0
  37. package/src/app/(dashboard)/admin/users/new/page.tsx +5 -0
  38. package/src/app/(dashboard)/admin/users/page.tsx +23 -0
  39. package/src/app/(dashboard)/dashboard/page.tsx +462 -0
  40. package/src/app/(dashboard)/departments/[deptId]/page.tsx +39 -0
  41. package/src/app/(dashboard)/departments/new/page.tsx +330 -0
  42. package/src/app/(dashboard)/departments/page.tsx +52 -0
  43. package/src/app/(dashboard)/devices/[id]/edit/page.tsx +131 -0
  44. package/src/app/(dashboard)/devices/[id]/page.tsx +408 -0
  45. package/src/app/(dashboard)/devices/[id]/qr/page.tsx +54 -0
  46. package/src/app/(dashboard)/devices/new/page.tsx +382 -0
  47. package/src/app/(dashboard)/devices/page.tsx +80 -0
  48. package/src/app/(dashboard)/engineering-team/page.tsx +41 -0
  49. package/src/app/(dashboard)/layout.tsx +43 -0
  50. package/src/app/(dashboard)/maintenance/checklists/page.tsx +57 -0
  51. package/src/app/(dashboard)/maintenance/page.tsx +231 -0
  52. package/src/app/(dashboard)/maintenance/plans/page.tsx +142 -0
  53. package/src/app/(dashboard)/maintenance/tasks/[id]/page.tsx +997 -0
  54. package/src/app/(dashboard)/maintenance/tasks/page.tsx +119 -0
  55. package/src/app/(dashboard)/more/page.tsx +310 -0
  56. package/src/app/(dashboard)/my-department/page.tsx +151 -0
  57. package/src/app/(dashboard)/notifications/notification-list.tsx +103 -0
  58. package/src/app/(dashboard)/notifications/page.tsx +59 -0
  59. package/src/app/(dashboard)/profile/page.tsx +27 -0
  60. package/src/app/(dashboard)/reports/costs/page.tsx +127 -0
  61. package/src/app/(dashboard)/reports/history/page.tsx +105 -0
  62. package/src/app/(dashboard)/reports/inventory/page.tsx +116 -0
  63. package/src/app/(dashboard)/reports/maintenance/page.tsx +136 -0
  64. package/src/app/(dashboard)/reports/page.tsx +120 -0
  65. package/src/app/(dashboard)/reports/tickets/page.tsx +139 -0
  66. package/src/app/(dashboard)/reports/workload/page.tsx +135 -0
  67. package/src/app/(dashboard)/rounds/[id]/page.tsx +197 -0
  68. package/src/app/(dashboard)/rounds/history/page.tsx +82 -0
  69. package/src/app/(dashboard)/rounds/page.tsx +107 -0
  70. package/src/app/(dashboard)/scan/[code]/page.tsx +154 -0
  71. package/src/app/(dashboard)/scan/page.tsx +73 -0
  72. package/src/app/(dashboard)/team/page.tsx +42 -0
  73. package/src/app/(dashboard)/tickets/[id]/page.tsx +465 -0
  74. package/src/app/(dashboard)/tickets/create/page.tsx +722 -0
  75. package/src/app/(dashboard)/tickets/page.tsx +55 -0
  76. package/src/app/(dashboard)/tickets/triage/page.tsx +161 -0
  77. package/src/app/api/admin/data/export/route.ts +316 -0
  78. package/src/app/api/auth/[...nextauth]/route.ts +3 -0
  79. package/src/app/api/health/route.ts +46 -0
  80. package/src/app/api/notifications/sse/route.ts +91 -0
  81. package/src/app/api/notifications/subscribe/route.ts +54 -0
  82. package/src/app/api/notifications/vapid-key/route.ts +7 -0
  83. package/src/app/api/reports/export/route.ts +102 -0
  84. package/src/app/api/tickets/[id]/attachments/route.ts +129 -0
  85. package/src/app/api/tickets/[id]/live-sync/route.ts +52 -0
  86. package/src/app/error.tsx +54 -0
  87. package/src/app/globals.css +89 -0
  88. package/src/app/layout.tsx +59 -0
  89. package/src/app/not-found.tsx +32 -0
  90. package/src/app/page.tsx +5 -0
  91. package/src/components/admin/add-department-user-dialog.tsx +759 -0
  92. package/src/components/admin/add-engineer-dialog.tsx +773 -0
  93. package/src/components/admin/add-user-type-dialog.tsx +89 -0
  94. package/src/components/admin/audit-log-export.tsx +113 -0
  95. package/src/components/admin/audit-log-filters.tsx +255 -0
  96. package/src/components/admin/audit-log-table.tsx +383 -0
  97. package/src/components/admin/data-management/danger-zone.tsx +301 -0
  98. package/src/components/admin/data-management/data-management-tabs.tsx +61 -0
  99. package/src/components/admin/data-management/export-center.tsx +301 -0
  100. package/src/components/admin/data-management/import-wizard.tsx +558 -0
  101. package/src/components/admin/settings-forms/features-form.tsx +115 -0
  102. package/src/components/admin/settings-forms/health-panel.tsx +182 -0
  103. package/src/components/admin/settings-forms/localization-form.tsx +203 -0
  104. package/src/components/admin/settings-forms/retention-form.tsx +164 -0
  105. package/src/components/admin/settings-forms/security-form.tsx +186 -0
  106. package/src/components/admin/system-settings-tabs.tsx +88 -0
  107. package/src/components/admin/user-role-dialog.tsx +293 -0
  108. package/src/components/admin/user-status-dialog.tsx +166 -0
  109. package/src/components/admin/users-page-client.tsx +514 -0
  110. package/src/components/dashboard/action-buttons.tsx +93 -0
  111. package/src/components/dashboard/admin-alerts.tsx +86 -0
  112. package/src/components/dashboard/availability-alerts.tsx +65 -0
  113. package/src/components/dashboard/maintenance-due-list.tsx +87 -0
  114. package/src/components/dashboard/round-widget.tsx +110 -0
  115. package/src/components/dashboard/stat-card.tsx +38 -0
  116. package/src/components/dashboard/ticket-list-compact.tsx +86 -0
  117. package/src/components/departments/department-cards-view.tsx +244 -0
  118. package/src/components/departments/department-detail-view.tsx +467 -0
  119. package/src/components/departments/department-visuals.tsx +190 -0
  120. package/src/components/departments/my-department-view.tsx +333 -0
  121. package/src/components/devices/change-status-dialog.tsx +146 -0
  122. package/src/components/devices/device-action-buttons.tsx +125 -0
  123. package/src/components/devices/device-availability-banner.tsx +59 -0
  124. package/src/components/devices/device-badges.tsx +55 -0
  125. package/src/components/devices/device-documents-tab.tsx +93 -0
  126. package/src/components/devices/device-maintenance-tab.tsx +113 -0
  127. package/src/components/devices/device-mini-qr.tsx +109 -0
  128. package/src/components/devices/device-qr-card.tsx +187 -0
  129. package/src/components/devices/device-signatures-tab.tsx +607 -0
  130. package/src/components/devices/device-tickets-tab.tsx +139 -0
  131. package/src/components/devices/device-timelines.tsx +124 -0
  132. package/src/components/devices/qr-camera-scanner.tsx +167 -0
  133. package/src/components/devices/qr-code-display.tsx +139 -0
  134. package/src/components/devices/quick-add-category-dialog.tsx +135 -0
  135. package/src/components/devices/quick-add-manufacturer-dialog.tsx +212 -0
  136. package/src/components/devices/transfer-device-dialog.tsx +190 -0
  137. package/src/components/layout/header.tsx +80 -0
  138. package/src/components/layout/mobile-nav.tsx +91 -0
  139. package/src/components/layout/more-item-badge.tsx +15 -0
  140. package/src/components/layout/nav-badge.tsx +72 -0
  141. package/src/components/layout/sidebar.tsx +297 -0
  142. package/src/components/maintenance/maintenance-badges.tsx +579 -0
  143. package/src/components/maintenance/maintenance-nav.tsx +107 -0
  144. package/src/components/maintenance/parts-costs-manager.tsx +909 -0
  145. package/src/components/maintenance/signature-card.tsx +413 -0
  146. package/src/components/maintenance/task-action-bar.tsx +955 -0
  147. package/src/components/notifications/notification-bell.tsx +367 -0
  148. package/src/components/notifications/notification-item.tsx +93 -0
  149. package/src/components/notifications/notification-preferences.tsx +182 -0
  150. package/src/components/profile/user-profile-view.tsx +626 -0
  151. package/src/components/providers/nav-counts-provider.tsx +235 -0
  152. package/src/components/providers/session-provider.tsx +7 -0
  153. package/src/components/providers/theme-provider.tsx +124 -0
  154. package/src/components/pwa/install-prompt.tsx +82 -0
  155. package/src/components/pwa/sw-register.tsx +43 -0
  156. package/src/components/reports/export-button.tsx +62 -0
  157. package/src/components/reports/filter-chips.tsx +47 -0
  158. package/src/components/reports/summary-cards.tsx +40 -0
  159. package/src/components/reports/transparency-header.tsx +54 -0
  160. package/src/components/rounds/department-visit-sheet.tsx +199 -0
  161. package/src/components/rounds/mention-textarea.tsx +187 -0
  162. package/src/components/rounds/round-calendar.tsx +233 -0
  163. package/src/components/rounds/round-department-list.tsx +281 -0
  164. package/src/components/rounds/round-history-client.tsx +218 -0
  165. package/src/components/rounds/start-round-card.tsx +184 -0
  166. package/src/components/shared/confirm-dialog.tsx +67 -0
  167. package/src/components/shared/data-table.tsx +104 -0
  168. package/src/components/shared/page-header.tsx +20 -0
  169. package/src/components/shared/sign-out-button.tsx +52 -0
  170. package/src/components/shared/status-badge.tsx +42 -0
  171. package/src/components/shared/table-search.tsx +55 -0
  172. package/src/components/signatures/sign-action-dialog.tsx +253 -0
  173. package/src/components/signatures/signature-history.tsx +121 -0
  174. package/src/components/signatures/signature-status-panel.tsx +164 -0
  175. package/src/components/team/department-coverage.tsx +141 -0
  176. package/src/components/team/edit-engineer-dialog.tsx +242 -0
  177. package/src/components/team/engineer-card.tsx +180 -0
  178. package/src/components/team/engineer-details-dialog.tsx +432 -0
  179. package/src/components/team/schedule-calendar.tsx +204 -0
  180. package/src/components/team/staff-engineer-team-view.tsx +368 -0
  181. package/src/components/team/team-header-stats.tsx +88 -0
  182. package/src/components/team/team-page-client.tsx +216 -0
  183. package/src/components/theme/theme-selector-card.tsx +63 -0
  184. package/src/components/theme/theme-toggle.tsx +65 -0
  185. package/src/components/tickets/staff-status-label.tsx +58 -0
  186. package/src/components/tickets/staff-ticket-card.tsx +109 -0
  187. package/src/components/tickets/staff-ticket-progress-tracker.tsx +405 -0
  188. package/src/components/tickets/staff-tickets-view.tsx +182 -0
  189. package/src/components/tickets/ticket-action-buttons.tsx +873 -0
  190. package/src/components/tickets/ticket-auto-refresh.tsx +197 -0
  191. package/src/components/tickets/ticket-badges.tsx +43 -0
  192. package/src/components/tickets/ticket-card.tsx +197 -0
  193. package/src/components/tickets/ticket-cards-view.tsx +395 -0
  194. package/src/components/tickets/ticket-comment-section.tsx +684 -0
  195. package/src/components/tickets/ticket-dispatch-status.tsx +184 -0
  196. package/src/components/tickets/ticket-list-auto-refresh.tsx +55 -0
  197. package/src/components/tickets/ticket-reporter-profile.tsx +235 -0
  198. package/src/components/tickets/ticket-signatures-panel.tsx +32 -0
  199. package/src/components/tickets/ticket-timeline.tsx +131 -0
  200. package/src/components/tickets/ticket-triage-panel.tsx +124 -0
  201. package/src/components/tickets/tickets-nav.tsx +107 -0
  202. package/src/components/ui/avatar.tsx +43 -0
  203. package/src/components/ui/badge.tsx +34 -0
  204. package/src/components/ui/button.tsx +58 -0
  205. package/src/components/ui/card.tsx +77 -0
  206. package/src/components/ui/checkbox.tsx +31 -0
  207. package/src/components/ui/dialog.tsx +123 -0
  208. package/src/components/ui/dropdown-menu.tsx +126 -0
  209. package/src/components/ui/form.tsx +179 -0
  210. package/src/components/ui/input.tsx +24 -0
  211. package/src/components/ui/label.tsx +27 -0
  212. package/src/components/ui/popover.tsx +34 -0
  213. package/src/components/ui/progress.tsx +36 -0
  214. package/src/components/ui/scroll-area.tsx +47 -0
  215. package/src/components/ui/select.tsx +160 -0
  216. package/src/components/ui/separator.tsx +25 -0
  217. package/src/components/ui/sheet.tsx +131 -0
  218. package/src/components/ui/switch.tsx +30 -0
  219. package/src/components/ui/table.tsx +121 -0
  220. package/src/components/ui/tabs.tsx +62 -0
  221. package/src/components/ui/textarea.tsx +23 -0
  222. package/src/components/ui/tooltip.tsx +27 -0
  223. package/src/lib/actions/__tests__/devices.test.ts +176 -0
  224. package/src/lib/actions/__tests__/signatures.test.ts +303 -0
  225. package/src/lib/actions/__tests__/tickets.test.ts +243 -0
  226. package/src/lib/actions/audit-logs.ts +298 -0
  227. package/src/lib/actions/checklists.ts +429 -0
  228. package/src/lib/actions/dashboard.ts +358 -0
  229. package/src/lib/actions/data-management.ts +723 -0
  230. package/src/lib/actions/departments.ts +369 -0
  231. package/src/lib/actions/device-categories.ts +117 -0
  232. package/src/lib/actions/devices.ts +748 -0
  233. package/src/lib/actions/engineer-rounds.ts +973 -0
  234. package/src/lib/actions/engineers.ts +285 -0
  235. package/src/lib/actions/hospitals.ts +148 -0
  236. package/src/lib/actions/locations.ts +115 -0
  237. package/src/lib/actions/maintenance-plans.ts +316 -0
  238. package/src/lib/actions/maintenance-tasks.ts +415 -0
  239. package/src/lib/actions/manufacturers.ts +132 -0
  240. package/src/lib/actions/nav-counts.ts +144 -0
  241. package/src/lib/actions/notifications.ts +136 -0
  242. package/src/lib/actions/organizations.ts +99 -0
  243. package/src/lib/actions/qr.ts +147 -0
  244. package/src/lib/actions/report-history.ts +77 -0
  245. package/src/lib/actions/reports.ts +192 -0
  246. package/src/lib/actions/roles.ts +32 -0
  247. package/src/lib/actions/signature-auth.ts +377 -0
  248. package/src/lib/actions/signatures.ts +532 -0
  249. package/src/lib/actions/system-settings.ts +274 -0
  250. package/src/lib/actions/tickets.ts +1445 -0
  251. package/src/lib/actions/users.ts +740 -0
  252. package/src/lib/audit/index.ts +103 -0
  253. package/src/lib/auth/__tests__/rbac.test.ts +33 -0
  254. package/src/lib/auth/index.ts +338 -0
  255. package/src/lib/auth/password.ts +10 -0
  256. package/src/lib/auth/rbac.ts +103 -0
  257. package/src/lib/auth/types.ts +57 -0
  258. package/src/lib/constants/departments.ts +16 -0
  259. package/src/lib/constants/manufacturers.ts +32 -0
  260. package/src/lib/db/index.ts +23 -0
  261. package/src/lib/db/schema/audit-logs.ts +42 -0
  262. package/src/lib/db/schema/departments.ts +39 -0
  263. package/src/lib/db/schema/device-categories.ts +38 -0
  264. package/src/lib/db/schema/devices.ts +195 -0
  265. package/src/lib/db/schema/engineer-profiles.ts +63 -0
  266. package/src/lib/db/schema/engineer-rounds.ts +115 -0
  267. package/src/lib/db/schema/enums.ts +148 -0
  268. package/src/lib/db/schema/hospitals.ts +34 -0
  269. package/src/lib/db/schema/index.ts +24 -0
  270. package/src/lib/db/schema/locations.ts +46 -0
  271. package/src/lib/db/schema/maintenance-plans.ts +99 -0
  272. package/src/lib/db/schema/maintenance-records.ts +161 -0
  273. package/src/lib/db/schema/maintenance-tasks.ts +196 -0
  274. package/src/lib/db/schema/manufacturers.ts +60 -0
  275. package/src/lib/db/schema/notifications.ts +48 -0
  276. package/src/lib/db/schema/organizations.ts +24 -0
  277. package/src/lib/db/schema/report-history.ts +28 -0
  278. package/src/lib/db/schema/roles.ts +59 -0
  279. package/src/lib/db/schema/sessions.ts +46 -0
  280. package/src/lib/db/schema/signature-events.ts +48 -0
  281. package/src/lib/db/schema/signature-policies.ts +60 -0
  282. package/src/lib/db/schema/signatures.ts +87 -0
  283. package/src/lib/db/schema/tickets.ts +169 -0
  284. package/src/lib/db/schema/user-assignments.ts +85 -0
  285. package/src/lib/db/schema/users.ts +39 -0
  286. package/src/lib/db/seed.ts +131 -0
  287. package/src/lib/logger/__tests__/logger.test.ts +154 -0
  288. package/src/lib/logger/formatters.ts +159 -0
  289. package/src/lib/logger/index.ts +184 -0
  290. package/src/lib/logger/types.ts +48 -0
  291. package/src/lib/notifications/create.ts +71 -0
  292. package/src/lib/notifications/dispatcher.ts +490 -0
  293. package/src/lib/notifications/index.ts +7 -0
  294. package/src/lib/notifications/push-sender.ts +128 -0
  295. package/src/lib/notifications/push.ts +64 -0
  296. package/src/lib/notifications/triggers.ts +166 -0
  297. package/src/lib/utils/__tests__/crypto.test.ts +30 -0
  298. package/src/lib/utils/__tests__/export.test.ts +55 -0
  299. package/src/lib/utils/__tests__/nav-badge.test.ts +147 -0
  300. package/src/lib/utils/__tests__/staff-status.test.ts +50 -0
  301. package/src/lib/utils/attestations.ts +19 -0
  302. package/src/lib/utils/crypto.ts +18 -0
  303. package/src/lib/utils/csv-parser.ts +181 -0
  304. package/src/lib/utils/export.ts +45 -0
  305. package/src/lib/utils/nav-badge.ts +225 -0
  306. package/src/lib/utils/sound.ts +152 -0
  307. package/src/lib/utils/staff-status.ts +115 -0
  308. package/src/lib/utils.ts +6 -0
  309. package/src/lib/validators/__tests__/auth.test.ts +127 -0
  310. package/src/lib/validators/__tests__/data-management.test.ts +182 -0
  311. package/src/lib/validators/__tests__/devices-profile.test.ts +129 -0
  312. package/src/lib/validators/__tests__/devices.test.ts +98 -0
  313. package/src/lib/validators/__tests__/engineer-rounds.test.ts +170 -0
  314. package/src/lib/validators/__tests__/maintenance.test.ts +178 -0
  315. package/src/lib/validators/__tests__/signatures.test.ts +154 -0
  316. package/src/lib/validators/__tests__/system-settings.test.ts +119 -0
  317. package/src/lib/validators/__tests__/tickets.test.ts +176 -0
  318. package/src/lib/validators/__tests__/users.test.ts +92 -0
  319. package/src/lib/validators/auth.ts +32 -0
  320. package/src/lib/validators/data-management.ts +67 -0
  321. package/src/lib/validators/departments.ts +14 -0
  322. package/src/lib/validators/devices.ts +94 -0
  323. package/src/lib/validators/engineer-rounds.ts +70 -0
  324. package/src/lib/validators/hospitals.ts +17 -0
  325. package/src/lib/validators/locations.ts +15 -0
  326. package/src/lib/validators/maintenance.ts +211 -0
  327. package/src/lib/validators/organizations.ts +11 -0
  328. package/src/lib/validators/reports.ts +31 -0
  329. package/src/lib/validators/signatures.ts +49 -0
  330. package/src/lib/validators/system-settings.ts +55 -0
  331. package/src/lib/validators/tickets.ts +125 -0
  332. package/src/lib/validators/users.ts +43 -0
  333. package/src/middleware.ts +45 -0
  334. package/tailwind.config.ts +76 -0
  335. package/tsconfig.json +28 -0
@@ -0,0 +1,186 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { Label } from '@/components/ui/label';
6
+ import { Input } from '@/components/ui/input';
7
+ import { Button } from '@/components/ui/button';
8
+ import { Switch } from '@/components/ui/switch';
9
+ import { ShieldCheck, Lock, KeyRound, AlertTriangle, Loader2, Save } from 'lucide-react';
10
+ import { toast } from 'sonner';
11
+ import { updateOrganizationSettings } from '@/lib/actions/system-settings';
12
+ import type { SecuritySettings } from '@/lib/validators/system-settings';
13
+
14
+ interface SecurityFormProps {
15
+ initialData: SecuritySettings;
16
+ organizationId: string;
17
+ }
18
+
19
+ export function SecurityForm({ initialData, organizationId }: SecurityFormProps) {
20
+ const [formData, setFormData] = useState<SecuritySettings>(initialData);
21
+ const [changeReason, setChangeReason] = useState('');
22
+ const [saving, setSaving] = useState(false);
23
+
24
+ const handleSubmit = async (e: React.FormEvent) => {
25
+ e.preventDefault();
26
+ setSaving(true);
27
+
28
+ try {
29
+ const res = await updateOrganizationSettings({
30
+ organizationId,
31
+ section: 'security',
32
+ settings: formData,
33
+ changeReason: changeReason.trim() || 'Updated security and compliance policies',
34
+ });
35
+
36
+ if (res.success) {
37
+ toast.success('Security settings saved successfully');
38
+ setChangeReason('');
39
+ } else {
40
+ toast.error(res.error || 'Failed to update security settings');
41
+ }
42
+ } catch {
43
+ toast.error('An unexpected error occurred');
44
+ } finally {
45
+ setSaving(false);
46
+ }
47
+ };
48
+
49
+ return (
50
+ <form onSubmit={handleSubmit}>
51
+ <Card>
52
+ <CardHeader>
53
+ <CardTitle className="flex items-center gap-2 text-xl">
54
+ <ShieldCheck className="h-5 w-5 text-primary" />
55
+ Security & 21 CFR Part 11 Compliance
56
+ </CardTitle>
57
+ <CardDescription>
58
+ Configure authentication thresholds, inactivity timeouts, and electronic signature safeguards.
59
+ </CardDescription>
60
+ </CardHeader>
61
+
62
+ <CardContent className="space-y-6">
63
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
64
+ {/* Session Timeout */}
65
+ <div className="space-y-2">
66
+ <Label htmlFor="sessionTimeout" className="flex items-center gap-2">
67
+ <Lock className="h-4 w-4 text-muted-foreground" />
68
+ Session Inactivity Timeout (Minutes)
69
+ </Label>
70
+ <Input
71
+ id="sessionTimeout"
72
+ type="number"
73
+ min={5}
74
+ max={1440}
75
+ value={formData.sessionTimeoutMinutes}
76
+ onChange={(e) =>
77
+ setFormData((prev) => ({ ...prev, sessionTimeoutMinutes: parseInt(e.target.value) || 30 }))
78
+ }
79
+ />
80
+ <p className="text-xs text-muted-foreground">
81
+ Inactivity duration before active user sessions are terminated automatically (standard: 15-30m).
82
+ </p>
83
+ </div>
84
+
85
+ {/* Password Expiry */}
86
+ <div className="space-y-2">
87
+ <Label htmlFor="passwordExpiry" className="flex items-center gap-2">
88
+ <KeyRound className="h-4 w-4 text-muted-foreground" />
89
+ Password Expiration Policy (Days)
90
+ </Label>
91
+ <Input
92
+ id="passwordExpiry"
93
+ type="number"
94
+ min={0}
95
+ max={365}
96
+ value={formData.passwordExpiryDays}
97
+ onChange={(e) =>
98
+ setFormData((prev) => ({ ...prev, passwordExpiryDays: parseInt(e.target.value) || 0 }))
99
+ }
100
+ />
101
+ <p className="text-xs text-muted-foreground">
102
+ Set to 0 to disable automated expiry, or specify hospital policy interval (e.g., 90 days).
103
+ </p>
104
+ </div>
105
+
106
+ {/* Max Login Attempts */}
107
+ <div className="space-y-2">
108
+ <Label htmlFor="maxAttempts" className="flex items-center gap-2">
109
+ <AlertTriangle className="h-4 w-4 text-muted-foreground" />
110
+ Max Failed Login Attempts
111
+ </Label>
112
+ <Input
113
+ id="maxAttempts"
114
+ type="number"
115
+ min={3}
116
+ max={10}
117
+ value={formData.maxLoginAttempts}
118
+ onChange={(e) =>
119
+ setFormData((prev) => ({ ...prev, maxLoginAttempts: parseInt(e.target.value) || 5 }))
120
+ }
121
+ />
122
+ <p className="text-xs text-muted-foreground">
123
+ Account lockout threshold to prevent brute-force attacks.
124
+ </p>
125
+ </div>
126
+
127
+ {/* Re-auth for Signatures */}
128
+ <div className="flex flex-col justify-between space-y-2 p-4 border rounded-lg bg-muted/20">
129
+ <div className="flex items-center justify-between">
130
+ <div className="space-y-0.5 pr-4">
131
+ <Label htmlFor="requireReAuth" className="font-medium text-sm">
132
+ Re-Authenticate for Electronic Signatures
133
+ </Label>
134
+ <p className="text-xs text-muted-foreground">
135
+ Enforces password/PIN re-entry when signing maintenance release and calibration records (FDA 21 CFR Part 11).
136
+ </p>
137
+ </div>
138
+ <Switch
139
+ id="requireReAuth"
140
+ checked={formData.requireReAuthForSignatures}
141
+ onCheckedChange={(val) =>
142
+ setFormData((prev) => ({ ...prev, requireReAuthForSignatures: val }))
143
+ }
144
+ />
145
+ </div>
146
+ </div>
147
+ </div>
148
+
149
+ {/* Audit Trace Reason */}
150
+ <div className="space-y-2 pt-2 border-t">
151
+ <Label htmlFor="changeReason" className="text-sm font-medium">
152
+ Change Reason (21 CFR Part 11 Audit Trace)
153
+ </Label>
154
+ <Input
155
+ id="changeReason"
156
+ placeholder="e.g., Hospital IT security policy annual review"
157
+ value={changeReason}
158
+ onChange={(e) => setChangeReason(e.target.value)}
159
+ className="max-w-xl"
160
+ />
161
+ <p className="text-xs text-muted-foreground">
162
+ Required reason for modifying clinical and infrastructure security parameters.
163
+ </p>
164
+ </div>
165
+ </CardContent>
166
+
167
+ <CardFooter className="flex justify-end border-t pt-4">
168
+ <Button type="submit" disabled={saving} className="gap-2">
169
+ {saving ? (
170
+ <>
171
+ <Loader2 className="h-4 w-4 animate-spin" />
172
+ Saving...
173
+ </>
174
+ ) : (
175
+ <>
176
+ <Save className="h-4 w-4" />
177
+ Save Security Settings
178
+ </>
179
+ )}
180
+ </Button>
181
+ </CardFooter>
182
+ </Card>
183
+ </form>
184
+ );
185
+ }
186
+
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+
3
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
4
+ import { Globe, ShieldCheck, Database, Activity, Sliders } from 'lucide-react';
5
+ import { LocalizationForm } from './settings-forms/localization-form';
6
+ import { SecurityForm } from './settings-forms/security-form';
7
+ import { RetentionForm } from './settings-forms/retention-form';
8
+ import { HealthPanel } from './settings-forms/health-panel';
9
+ import { FeaturesForm } from './settings-forms/features-form';
10
+ import type { SystemSettings } from '@/lib/validators/system-settings';
11
+ import type { SystemHealthData } from '@/lib/actions/system-settings';
12
+
13
+ interface SystemSettingsTabsProps {
14
+ settings: SystemSettings;
15
+ organizationId: string;
16
+ organizationName?: string;
17
+ healthData: SystemHealthData;
18
+ }
19
+
20
+ export function SystemSettingsTabs({
21
+ settings,
22
+ organizationId,
23
+ organizationName,
24
+ healthData,
25
+ }: SystemSettingsTabsProps) {
26
+ return (
27
+ <div className="space-y-6">
28
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
29
+ <div>
30
+ <h1 className="text-2xl font-bold tracking-tight">System Settings</h1>
31
+ <p className="text-sm text-muted-foreground">
32
+ Configure system policies, localization, security rules, and data retention
33
+ {organizationName ? ` for ${organizationName}` : ''}.
34
+ </p>
35
+ </div>
36
+ </div>
37
+
38
+ <Tabs defaultValue="localization" className="space-y-6">
39
+ <TabsList className="grid grid-cols-2 md:grid-cols-5 w-full h-auto p-1 gap-1">
40
+ <TabsTrigger value="localization" className="flex items-center gap-2 py-2">
41
+ <Globe className="h-4 w-4" />
42
+ <span className="hidden sm:inline">Localization</span>
43
+ <span className="sm:hidden">Locale</span>
44
+ </TabsTrigger>
45
+ <TabsTrigger value="security" className="flex items-center gap-2 py-2">
46
+ <ShieldCheck className="h-4 w-4" />
47
+ <span>Security</span>
48
+ </TabsTrigger>
49
+ <TabsTrigger value="retention" className="flex items-center gap-2 py-2">
50
+ <Database className="h-4 w-4" />
51
+ <span className="hidden sm:inline">Data Retention</span>
52
+ <span className="sm:hidden">Retention</span>
53
+ </TabsTrigger>
54
+ <TabsTrigger value="features" className="flex items-center gap-2 py-2">
55
+ <Sliders className="h-4 w-4" />
56
+ <span>Features</span>
57
+ </TabsTrigger>
58
+ <TabsTrigger value="health" className="flex items-center gap-2 py-2">
59
+ <Activity className="h-4 w-4" />
60
+ <span className="hidden sm:inline">System Health</span>
61
+ <span className="sm:hidden">Health</span>
62
+ </TabsTrigger>
63
+ </TabsList>
64
+
65
+ <TabsContent value="localization" className="space-y-4">
66
+ <LocalizationForm initialData={settings.localization} organizationId={organizationId} />
67
+ </TabsContent>
68
+
69
+ <TabsContent value="security" className="space-y-4">
70
+ <SecurityForm initialData={settings.security} organizationId={organizationId} />
71
+ </TabsContent>
72
+
73
+ <TabsContent value="retention" className="space-y-4">
74
+ <RetentionForm initialData={settings.retention} organizationId={organizationId} />
75
+ </TabsContent>
76
+
77
+ <TabsContent value="features" className="space-y-4">
78
+ <FeaturesForm initialData={settings.features} organizationId={organizationId} />
79
+ </TabsContent>
80
+
81
+ <TabsContent value="health" className="space-y-4">
82
+ <HealthPanel initialData={healthData} />
83
+ </TabsContent>
84
+ </Tabs>
85
+ </div>
86
+ );
87
+ }
88
+
@@ -0,0 +1,293 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ DialogTrigger,
11
+ DialogFooter,
12
+ DialogDescription,
13
+ } from "@/components/ui/dialog";
14
+ import { Button } from "@/components/ui/button";
15
+ import { Label } from "@/components/ui/label";
16
+ import { Plus, Trash2, Loader2, ShieldCheck, AlertCircle } from "lucide-react";
17
+ import { assignUserRole, revokeUserRole } from "@/lib/actions/users";
18
+
19
+ interface RoleOption {
20
+ id: string;
21
+ code: string;
22
+ name: string;
23
+ description?: string | null;
24
+ }
25
+
26
+ interface AssignedRole {
27
+ id: string;
28
+ assignmentId: string;
29
+ code: string;
30
+ name: string;
31
+ description?: string | null;
32
+ }
33
+
34
+ interface UserRoleDialogProps {
35
+ userId: string;
36
+ assignedRoles: AssignedRole[];
37
+ availableRoles: RoleOption[];
38
+ }
39
+
40
+ export function UserRoleDialog({
41
+ userId,
42
+ assignedRoles,
43
+ availableRoles,
44
+ }: UserRoleDialogProps) {
45
+ const router = useRouter();
46
+
47
+ // Assign Role State
48
+ const [assignOpen, setAssignOpen] = useState(false);
49
+ const [selectedRoleId, setSelectedRoleId] = useState("");
50
+ const [assignLoading, setAssignLoading] = useState(false);
51
+ const [assignError, setAssignError] = useState("");
52
+
53
+ // Revoke Role State
54
+ const [revokeOpen, setRevokeOpen] = useState(false);
55
+ const [roleToRevoke, setRoleToRevoke] = useState<AssignedRole | null>(null);
56
+ const [revokeLoading, setRevokeLoading] = useState(false);
57
+ const [revokeError, setRevokeError] = useState("");
58
+
59
+ // Filter out roles already assigned
60
+ const unassignedRoles = availableRoles.filter(
61
+ (role) => !assignedRoles.some((ar) => ar.id === role.id || ar.code === role.code)
62
+ );
63
+
64
+ const handleAssignRole = async (e: React.FormEvent) => {
65
+ e.preventDefault();
66
+ if (!selectedRoleId) {
67
+ setAssignError("Please select a role to assign.");
68
+ return;
69
+ }
70
+
71
+ setAssignLoading(true);
72
+ setAssignError("");
73
+
74
+ try {
75
+ const result = await assignUserRole({
76
+ userId,
77
+ roleId: selectedRoleId,
78
+ });
79
+
80
+ if (result.success) {
81
+ setAssignOpen(false);
82
+ setSelectedRoleId("");
83
+ router.refresh();
84
+ } else {
85
+ setAssignError((result as any)?.error || "Failed to assign role");
86
+ }
87
+ } catch (err: any) {
88
+ setAssignError(err?.message || "An unexpected error occurred");
89
+ } finally {
90
+ setAssignLoading(false);
91
+ }
92
+ };
93
+
94
+ const handleRevokeRole = async () => {
95
+ if (!roleToRevoke) return;
96
+
97
+ setRevokeLoading(true);
98
+ setRevokeError("");
99
+
100
+ try {
101
+ const result = await revokeUserRole(roleToRevoke.assignmentId);
102
+
103
+ if (result.success) {
104
+ setRevokeOpen(false);
105
+ setRoleToRevoke(null);
106
+ router.refresh();
107
+ } else {
108
+ setRevokeError((result as any)?.error || "Failed to revoke role");
109
+ }
110
+ } catch (err: any) {
111
+ setRevokeError(err?.message || "An unexpected error occurred");
112
+ } finally {
113
+ setRevokeLoading(false);
114
+ }
115
+ };
116
+
117
+ return (
118
+ <div className="space-y-4">
119
+ {/* ── Assign Role Trigger ── */}
120
+ <div className="flex justify-end">
121
+ <Dialog open={assignOpen} onOpenChange={setAssignOpen}>
122
+ <DialogTrigger asChild>
123
+ <Button size="sm" className="gap-1.5 min-h-[36px]">
124
+ <Plus className="w-4 h-4" />
125
+ Assign Role
126
+ </Button>
127
+ </DialogTrigger>
128
+ <DialogContent className="sm:max-w-[450px]">
129
+ <form onSubmit={handleAssignRole} className="space-y-4">
130
+ <DialogHeader>
131
+ <DialogTitle className="flex items-center gap-2">
132
+ <ShieldCheck className="w-5 h-5 text-primary" />
133
+ Assign System Role
134
+ </DialogTitle>
135
+ <DialogDescription className="text-xs">
136
+ Grant operational or administrative access permissions to this user.
137
+ </DialogDescription>
138
+ </DialogHeader>
139
+
140
+ {assignError && (
141
+ <div className="bg-destructive/10 text-destructive text-xs p-2.5 rounded-md border border-destructive/20">
142
+ {assignError}
143
+ </div>
144
+ )}
145
+
146
+ <div className="space-y-2">
147
+ <Label htmlFor="role-select" className="text-xs font-medium">
148
+ Select Role *
149
+ </Label>
150
+ {unassignedRoles.length > 0 ? (
151
+ <select
152
+ id="role-select"
153
+ value={selectedRoleId}
154
+ onChange={(e) => setSelectedRoleId(e.target.value)}
155
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
156
+ required
157
+ >
158
+ <option value="">Choose a role...</option>
159
+ {unassignedRoles.map((role) => (
160
+ <option key={role.id} value={role.id}>
161
+ {role.name} ({role.code})
162
+ </option>
163
+ ))}
164
+ </select>
165
+ ) : (
166
+ <p className="text-xs text-muted-foreground italic p-2 border rounded bg-muted/20">
167
+ All available system roles have already been assigned to this user.
168
+ </p>
169
+ )}
170
+
171
+ {selectedRoleId && (
172
+ <p className="text-xs text-muted-foreground pt-1">
173
+ {unassignedRoles.find((r) => r.id === selectedRoleId)?.description}
174
+ </p>
175
+ )}
176
+ </div>
177
+
178
+ <DialogFooter className="gap-2 sm:gap-0 pt-2">
179
+ <Button
180
+ type="button"
181
+ variant="outline"
182
+ size="sm"
183
+ onClick={() => setAssignOpen(false)}
184
+ disabled={assignLoading}
185
+ >
186
+ Cancel
187
+ </Button>
188
+ <Button
189
+ type="submit"
190
+ size="sm"
191
+ disabled={assignLoading || unassignedRoles.length === 0}
192
+ >
193
+ {assignLoading && <Loader2 className="w-3.5 h-3.5 mr-1.5 animate-spin" />}
194
+ Confirm Assignment
195
+ </Button>
196
+ </DialogFooter>
197
+ </form>
198
+ </DialogContent>
199
+ </Dialog>
200
+ </div>
201
+
202
+ {/* ── Assigned Roles List ── */}
203
+ {assignedRoles.length > 0 ? (
204
+ <div className="space-y-3">
205
+ {assignedRoles.map((role) => (
206
+ <div
207
+ key={role.assignmentId || role.id}
208
+ className="flex items-center justify-between p-3.5 rounded-lg border bg-card hover:bg-muted/20 transition-colors"
209
+ >
210
+ <div className="space-y-0.5 min-w-0 pr-4">
211
+ <div className="flex items-center gap-2">
212
+ <span className="font-semibold text-sm">{role.name}</span>
213
+ <span className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded bg-muted text-muted-foreground border">
214
+ {role.code}
215
+ </span>
216
+ </div>
217
+ {role.description && (
218
+ <p className="text-xs text-muted-foreground truncate">{role.description}</p>
219
+ )}
220
+ </div>
221
+
222
+ <Button
223
+ type="button"
224
+ variant="ghost"
225
+ size="sm"
226
+ className="text-red-500 hover:text-red-700 hover:bg-red-50 shrink-0 h-8 gap-1 text-xs"
227
+ onClick={() => {
228
+ setRoleToRevoke(role);
229
+ setRevokeOpen(true);
230
+ }}
231
+ >
232
+ <Trash2 className="w-3.5 h-3.5" />
233
+ Revoke
234
+ </Button>
235
+ </div>
236
+ ))}
237
+ </div>
238
+ ) : (
239
+ <div className="text-center py-8 text-sm text-muted-foreground border border-dashed rounded-lg">
240
+ No roles assigned yet. Click &quot;Assign Role&quot; above to grant permissions.
241
+ </div>
242
+ )}
243
+
244
+ {/* ── Revoke Confirmation Dialog ── */}
245
+ <Dialog open={revokeOpen} onOpenChange={setRevokeOpen}>
246
+ <DialogContent className="sm:max-w-[420px]">
247
+ <DialogHeader>
248
+ <div className="flex items-center gap-2">
249
+ <div className="p-2 rounded-full bg-red-100 text-red-600">
250
+ <AlertCircle className="w-5 h-5" />
251
+ </div>
252
+ <DialogTitle>Revoke Role</DialogTitle>
253
+ </div>
254
+ <DialogDescription className="text-xs pt-1">
255
+ Are you sure you want to revoke the{" "}
256
+ <strong className="text-foreground">{roleToRevoke?.name}</strong> role? The user will
257
+ immediately lose the privileges associated with this role.
258
+ </DialogDescription>
259
+ </DialogHeader>
260
+
261
+ {revokeError && (
262
+ <div className="bg-destructive/10 text-destructive text-xs p-2.5 rounded-md border border-destructive/20">
263
+ {revokeError}
264
+ </div>
265
+ )}
266
+
267
+ <DialogFooter className="gap-2 sm:gap-0 pt-2">
268
+ <Button
269
+ type="button"
270
+ variant="outline"
271
+ size="sm"
272
+ onClick={() => setRevokeOpen(false)}
273
+ disabled={revokeLoading}
274
+ >
275
+ Cancel
276
+ </Button>
277
+ <Button
278
+ type="button"
279
+ variant="destructive"
280
+ size="sm"
281
+ onClick={handleRevokeRole}
282
+ disabled={revokeLoading}
283
+ >
284
+ {revokeLoading && <Loader2 className="w-3.5 h-3.5 mr-1.5 animate-spin" />}
285
+ Confirm Revocation
286
+ </Button>
287
+ </DialogFooter>
288
+ </DialogContent>
289
+ </Dialog>
290
+ </div>
291
+ );
292
+ }
293
+