@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,558 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useCallback } from 'react';
4
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Badge } from '@/components/ui/badge';
7
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
8
+ import {
9
+ Download,
10
+ UploadCloud,
11
+ FileText,
12
+ FileSpreadsheet,
13
+ CheckCircle2,
14
+ AlertTriangle,
15
+ XCircle,
16
+ Loader2,
17
+ ArrowRight,
18
+ ArrowLeft,
19
+ RotateCcw,
20
+ Layers,
21
+ Building2,
22
+ Info
23
+ } from 'lucide-react';
24
+ import { toast } from 'sonner';
25
+
26
+ // Next lines are ignored if they don't exist yet, per instructions they will be added later
27
+ import {
28
+ validateDeviceImport,
29
+ executeDeviceImport,
30
+ validateDepartmentImport,
31
+ executeDepartmentImport
32
+ } from '@/lib/actions/data-management';
33
+
34
+ import {
35
+ getDeviceImportTemplateCSV,
36
+ getDepartmentImportTemplateCSV
37
+ } from '@/lib/utils/csv-parser';
38
+
39
+ import type { DeviceImportValidationResult } from '@/lib/validators/data-management';
40
+
41
+ type EntityType = 'devices' | 'departments';
42
+
43
+ interface ValidationData {
44
+ totalRows: number;
45
+ validCount: number;
46
+ warningCount: number;
47
+ errorCount: number;
48
+ rows: any[]; // any to accommodate both devices and departments
49
+ }
50
+
51
+ export function ImportWizard() {
52
+ const [step, setStep] = useState<1 | 2 | 3>(1);
53
+ const [entity, setEntity] = useState<EntityType>('devices');
54
+ const [file, setFile] = useState<File | null>(null);
55
+ const [isDragging, setIsDragging] = useState(false);
56
+ const [validating, setValidating] = useState(false);
57
+ const [importing, setImporting] = useState(false);
58
+ const [validationData, setValidationData] = useState<ValidationData | null>(null);
59
+ const [importedCount, setImportedCount] = useState<number>(0);
60
+
61
+ const fileInputRef = useRef<HTMLInputElement>(null);
62
+
63
+ const handleDownloadTemplate = async (format: 'csv' | 'excel') => {
64
+ try {
65
+ const csvContent = entity === 'devices'
66
+ ? getDeviceImportTemplateCSV()
67
+ : getDepartmentImportTemplateCSV();
68
+
69
+ if (format === 'excel') {
70
+ const XLSX = await import('xlsx');
71
+ const wb = XLSX.read(csvContent, { type: 'string' });
72
+ const excelBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
73
+ const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
74
+ const url = URL.createObjectURL(blob);
75
+ const link = document.createElement('a');
76
+ link.href = url;
77
+ link.setAttribute('download', `bemms_${entity}_template.xlsx`);
78
+ document.body.appendChild(link);
79
+ link.click();
80
+ document.body.removeChild(link);
81
+ URL.revokeObjectURL(url);
82
+ toast.success(`Downloaded ${entity} Excel template (.xlsx)`);
83
+ } else {
84
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
85
+ const url = URL.createObjectURL(blob);
86
+ const link = document.createElement('a');
87
+ link.href = url;
88
+ link.setAttribute('download', `bemms_${entity}_template.csv`);
89
+ document.body.appendChild(link);
90
+ link.click();
91
+ document.body.removeChild(link);
92
+ URL.revokeObjectURL(url);
93
+ toast.success(`Downloaded ${entity} CSV template (.csv)`);
94
+ }
95
+ } catch (error) {
96
+ toast.error('Failed to download template');
97
+ }
98
+ };
99
+
100
+ const parseFileToCsv = async (fileToParse: File): Promise<string> => {
101
+ if (fileToParse.name.endsWith('.csv')) {
102
+ return await fileToParse.text();
103
+ } else if (fileToParse.name.endsWith('.xlsx') || fileToParse.name.endsWith('.xls')) {
104
+ const XLSX = await import('xlsx');
105
+ const data = await fileToParse.arrayBuffer();
106
+ const workbook = XLSX.read(data, { type: 'array' });
107
+ const sheet = workbook.Sheets[workbook.SheetNames[0]];
108
+ return XLSX.utils.sheet_to_csv(sheet);
109
+ }
110
+ throw new Error('Unsupported file format');
111
+ };
112
+
113
+ const handleFileSet = (selectedFile: File) => {
114
+ if (selectedFile.name.endsWith('.csv') || selectedFile.name.endsWith('.xlsx') || selectedFile.name.endsWith('.xls')) {
115
+ setFile(selectedFile);
116
+ } else {
117
+ toast.error('Please upload a .csv, .xlsx, or .xls file');
118
+ }
119
+ };
120
+
121
+ const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
122
+ e.preventDefault();
123
+ setIsDragging(true);
124
+ }, []);
125
+
126
+ const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
127
+ e.preventDefault();
128
+ setIsDragging(true);
129
+ }, []);
130
+
131
+ const handleDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
132
+ e.preventDefault();
133
+ setIsDragging(false);
134
+ }, []);
135
+
136
+ const handleDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
137
+ e.preventDefault();
138
+ setIsDragging(false);
139
+
140
+ const droppedFile = e.dataTransfer.files[0];
141
+ if (droppedFile) {
142
+ handleFileSet(droppedFile);
143
+ }
144
+ }, []);
145
+
146
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
147
+ const selectedFile = e.target.files?.[0];
148
+ if (selectedFile) {
149
+ handleFileSet(selectedFile);
150
+ }
151
+ };
152
+
153
+ const handleValidate = async () => {
154
+ if (!file) return;
155
+
156
+ setValidating(true);
157
+ setValidationData(null);
158
+
159
+ try {
160
+ const csvText = await parseFileToCsv(file);
161
+
162
+ let res;
163
+ if (entity === 'devices') {
164
+ res = await validateDeviceImport(csvText);
165
+ } else {
166
+ res = await validateDepartmentImport(csvText);
167
+ }
168
+
169
+ if (res.success) {
170
+ setValidationData({
171
+ totalRows: res.totalRows,
172
+ validCount: res.validCount,
173
+ warningCount: res.warningCount,
174
+ errorCount: res.errorCount,
175
+ rows: res.rows,
176
+ });
177
+ toast.info(`Validated ${res.totalRows} rows`);
178
+ setStep(2);
179
+ } else {
180
+ toast.error(res.error || 'Failed to validate file');
181
+ }
182
+ } catch (error) {
183
+ toast.error('Could not read or parse the selected file');
184
+ } finally {
185
+ setValidating(false);
186
+ }
187
+ };
188
+
189
+ const handleExecuteImport = async () => {
190
+ if (!validationData) return;
191
+ setImporting(true);
192
+
193
+ try {
194
+ let res;
195
+ if (entity === 'devices') {
196
+ res = await executeDeviceImport(validationData.rows);
197
+ } else {
198
+ res = await executeDepartmentImport(validationData.rows);
199
+ }
200
+
201
+ if (res.success) {
202
+ toast.success(`Successfully imported ${res.importedCount} records!`);
203
+ setImportedCount(res.importedCount);
204
+ setStep(3);
205
+ } else {
206
+ toast.error(res.error || 'Failed to complete import');
207
+ }
208
+ } catch {
209
+ toast.error('An unexpected error occurred during import execution');
210
+ } finally {
211
+ setImporting(false);
212
+ }
213
+ };
214
+
215
+ const handleReset = () => {
216
+ setFile(null);
217
+ setValidationData(null);
218
+ setStep(1);
219
+ if (fileInputRef.current) fileInputRef.current.value = '';
220
+ };
221
+
222
+ const importableCount = validationData ? validationData.validCount + validationData.warningCount : 0;
223
+
224
+ return (
225
+ <div className="space-y-6">
226
+ {/* 1. Entity Type Selector */}
227
+ {step === 1 && (
228
+ <>
229
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
230
+ <Card
231
+ className={`cursor-pointer transition-all hover:border-primary/50 ${entity === 'devices' ? 'border-primary ring-1 ring-primary' : ''}`}
232
+ onClick={() => setEntity('devices')}
233
+ >
234
+ <CardContent className="flex items-center gap-4 p-4">
235
+ <div className={`p-2 rounded-full ${entity === 'devices' ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}>
236
+ <Layers className="h-6 w-6" />
237
+ </div>
238
+ <div>
239
+ <h3 className="font-medium text-base">Medical Devices</h3>
240
+ <p className="text-sm text-muted-foreground">Import equipment and assets</p>
241
+ </div>
242
+ </CardContent>
243
+ </Card>
244
+
245
+ <Card
246
+ className={`cursor-pointer transition-all hover:border-primary/50 ${entity === 'departments' ? 'border-primary ring-1 ring-primary' : ''}`}
247
+ onClick={() => setEntity('departments')}
248
+ >
249
+ <CardContent className="flex items-center gap-4 p-4">
250
+ <div className={`p-2 rounded-full ${entity === 'departments' ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}>
251
+ <Building2 className="h-6 w-6" />
252
+ </div>
253
+ <div>
254
+ <h3 className="font-medium text-base">Departments</h3>
255
+ <p className="text-sm text-muted-foreground">Import hospital departments</p>
256
+ </div>
257
+ </CardContent>
258
+ </Card>
259
+ </div>
260
+
261
+ {/* Relationship Workflow Guidance */}
262
+ {entity === 'devices' ? (
263
+ <div className="flex items-start gap-3 rounded-lg border border-blue-500/20 bg-blue-500/10 p-3 text-sm text-blue-900 dark:text-blue-200">
264
+ <Info className="h-5 w-5 shrink-0 text-blue-600 dark:text-blue-400 mt-0.5" />
265
+ <div className="space-y-1">
266
+ <span className="font-semibold text-xs uppercase tracking-wider text-blue-700 dark:text-blue-300">Recommended Sequence</span>
267
+ <p className="text-xs text-blue-800/90 dark:text-blue-200">
268
+ Medical devices are associated with hospital departments. If your facility has new or unlisted wards,{' '}
269
+ <button
270
+ type="button"
271
+ onClick={() => setEntity('departments')}
272
+ className="font-semibold underline underline-offset-2 hover:text-blue-600 dark:hover:text-blue-100"
273
+ >
274
+ import departments first
275
+ </button>
276
+ , then import devices referencing those department names.
277
+ </p>
278
+ </div>
279
+ </div>
280
+ ) : (
281
+ <div className="flex items-start gap-3 rounded-lg border border-emerald-500/20 bg-emerald-500/10 p-3 text-sm text-emerald-900 dark:text-emerald-200">
282
+ <Info className="h-5 w-5 shrink-0 text-emerald-600 dark:text-emerald-400 mt-0.5" />
283
+ <div className="space-y-1">
284
+ <span className="font-semibold text-xs uppercase tracking-wider text-emerald-700 dark:text-emerald-300">Organizational Hierarchy</span>
285
+ <p className="text-xs text-emerald-800/90 dark:text-emerald-200">
286
+ Importing departments registers hospital clinical areas (e.g. ICU, Radiology, Emergency). Once imported, you can immediately switch to{' '}
287
+ <button
288
+ type="button"
289
+ onClick={() => setEntity('devices')}
290
+ className="font-semibold underline underline-offset-2 hover:text-emerald-600 dark:hover:text-emerald-100"
291
+ >
292
+ importing medical devices
293
+ </button>{' '}
294
+ to assign assets to them.
295
+ </p>
296
+ </div>
297
+ </div>
298
+ )}
299
+ </>
300
+ )}
301
+
302
+ <Card>
303
+ <CardHeader>
304
+ <div className="flex flex-col space-y-4">
305
+ <div>
306
+ <CardTitle className="text-xl flex items-center gap-2">
307
+ <UploadCloud className="h-5 w-5 text-primary" />
308
+ Data Import Wizard
309
+ </CardTitle>
310
+ <CardDescription>
311
+ Follow the steps to import data into the system
312
+ </CardDescription>
313
+ </div>
314
+
315
+ {/* 2. Three-Step Visual Stepper */}
316
+ <div className="relative py-4">
317
+ <div className="absolute top-1/2 left-0 w-full h-0.5 bg-muted -translate-y-1/2"></div>
318
+ <div className="relative flex justify-between">
319
+ <div className="flex flex-col items-center gap-2 bg-card px-2">
320
+ <div className={`flex h-8 w-8 items-center justify-center rounded-full border-2 ${step >= 1 ? 'border-primary bg-primary text-primary-foreground' : 'border-muted bg-muted text-muted-foreground'}`}>
321
+ {step > 1 ? <CheckCircle2 className="h-5 w-5" /> : '1'}
322
+ </div>
323
+ <span className="text-xs font-medium">Upload</span>
324
+ </div>
325
+ <div className="flex flex-col items-center gap-2 bg-card px-2">
326
+ <div className={`flex h-8 w-8 items-center justify-center rounded-full border-2 ${step >= 2 ? 'border-primary bg-primary text-primary-foreground' : 'border-muted bg-muted text-muted-foreground'}`}>
327
+ {step > 2 ? <CheckCircle2 className="h-5 w-5" /> : '2'}
328
+ </div>
329
+ <span className="text-xs font-medium">Preview</span>
330
+ </div>
331
+ <div className="flex flex-col items-center gap-2 bg-card px-2">
332
+ <div className={`flex h-8 w-8 items-center justify-center rounded-full border-2 ${step >= 3 ? 'border-primary bg-primary text-primary-foreground' : 'border-muted bg-muted text-muted-foreground'}`}>
333
+ {step > 3 ? <CheckCircle2 className="h-5 w-5" /> : '3'}
334
+ </div>
335
+ <span className="text-xs font-medium">Results</span>
336
+ </div>
337
+ </div>
338
+ </div>
339
+ </div>
340
+ </CardHeader>
341
+
342
+ <CardContent className="space-y-6 pt-4">
343
+ {/* Step 1: Upload Zone */}
344
+ {step === 1 && (
345
+ <div className="space-y-6">
346
+ <div className="flex flex-col sm:flex-row gap-3">
347
+ <Button variant="outline" onClick={() => handleDownloadTemplate('csv')} className="flex-1 gap-2">
348
+ <FileText className="h-4 w-4" />
349
+ Download CSV Template
350
+ </Button>
351
+ <Button variant="outline" onClick={() => handleDownloadTemplate('excel')} className="flex-1 gap-2">
352
+ <FileSpreadsheet className="h-4 w-4" />
353
+ Download Excel Template
354
+ </Button>
355
+ </div>
356
+
357
+ <div
358
+ onDragOver={handleDragOver}
359
+ onDragEnter={handleDragEnter}
360
+ onDragLeave={handleDragLeave}
361
+ onDrop={handleDrop}
362
+ className={`border-2 border-dashed rounded-xl min-h-[200px] p-8 text-center transition-all flex flex-col items-center justify-center space-y-4 ${
363
+ isDragging
364
+ ? 'border-primary bg-primary/5'
365
+ : 'border-muted-foreground/25 hover:border-primary/50'
366
+ }`}
367
+ >
368
+ {!file ? (
369
+ <>
370
+ <UploadCloud className="h-12 w-12 text-muted-foreground" />
371
+ <div className="space-y-1">
372
+ <p className="font-bold text-lg">Drop your file here</p>
373
+ <p className="text-sm text-muted-foreground">
374
+ Supports CSV and Excel (.xlsx, .xls) files
375
+ </p>
376
+ </div>
377
+ <input
378
+ ref={fileInputRef}
379
+ type="file"
380
+ accept=".csv, .xlsx, .xls"
381
+ className="hidden"
382
+ onChange={handleFileChange}
383
+ />
384
+ <Button onClick={() => fileInputRef.current?.click()}>
385
+ Browse Files
386
+ </Button>
387
+ </>
388
+ ) : (
389
+ <div className="flex flex-col items-center space-y-4">
390
+ <div className="flex items-center gap-3 bg-muted p-4 rounded-lg">
391
+ {file.name.endsWith('.csv') ? (
392
+ <FileText className="h-8 w-8 text-primary" />
393
+ ) : (
394
+ <FileSpreadsheet className="h-8 w-8 text-emerald-600" />
395
+ )}
396
+ <div className="text-left text-sm">
397
+ <p className="font-medium truncate max-w-[200px]">{file.name}</p>
398
+ <p className="text-muted-foreground">
399
+ {(file.size / 1024).toFixed(1)} KB
400
+ </p>
401
+ </div>
402
+ <Button variant="ghost" size="icon" onClick={() => setFile(null)} className="ml-2">
403
+ <XCircle className="h-5 w-5 text-destructive" />
404
+ </Button>
405
+ </div>
406
+ </div>
407
+ )}
408
+ </div>
409
+
410
+ {file && (
411
+ <div className="flex justify-end">
412
+ <Button onClick={handleValidate} disabled={validating} className="w-full sm:w-auto">
413
+ {validating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
414
+ Validate & Preview
415
+ </Button>
416
+ </div>
417
+ )}
418
+ </div>
419
+ )}
420
+
421
+ {/* Step 2: Preview (Validation Results) */}
422
+ {step === 2 && validationData && (
423
+ <div className="space-y-4">
424
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 p-4 bg-muted/40 rounded-lg border">
425
+ <div>
426
+ <span className="text-xs text-muted-foreground">Total Rows</span>
427
+ <div className="text-xl font-bold">{validationData.totalRows}</div>
428
+ </div>
429
+ <div>
430
+ <span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium">Ready to Import</span>
431
+ <div className="text-xl font-bold text-emerald-600 dark:text-emerald-400">
432
+ {importableCount}
433
+ </div>
434
+ </div>
435
+ <div>
436
+ <span className="text-xs text-amber-600 dark:text-amber-400 font-medium">Auto-Create Notices</span>
437
+ <div className="text-xl font-bold text-amber-600 dark:text-amber-400">
438
+ {validationData.warningCount}
439
+ </div>
440
+ </div>
441
+ <div>
442
+ <span className="text-xs text-destructive font-medium">Blocking Errors</span>
443
+ <div className="text-xl font-bold text-destructive">{validationData.errorCount}</div>
444
+ </div>
445
+ </div>
446
+
447
+ <div className="border rounded-lg overflow-x-auto max-h-[380px]">
448
+ <Table>
449
+ <TableHeader>
450
+ <TableRow>
451
+ <TableHead className="w-16">Row</TableHead>
452
+ <TableHead>Status</TableHead>
453
+ {entity === 'devices' ? (
454
+ <>
455
+ <TableHead>Asset Tag</TableHead>
456
+ <TableHead>Device Name</TableHead>
457
+ <TableHead>Category</TableHead>
458
+ <TableHead>Department</TableHead>
459
+ </>
460
+ ) : (
461
+ <>
462
+ <TableHead>Department Name</TableHead>
463
+ <TableHead>Code</TableHead>
464
+ <TableHead>Type</TableHead>
465
+ <TableHead>Hospital</TableHead>
466
+ </>
467
+ )}
468
+ <TableHead>Notes</TableHead>
469
+ </TableRow>
470
+ </TableHeader>
471
+ <TableBody>
472
+ {validationData.rows.map((row: any) => (
473
+ <TableRow key={row.rowNumber} className={row.status === 'error' ? 'bg-destructive/5' : ''}>
474
+ <TableCell className="font-mono text-xs text-muted-foreground">{row.rowNumber}</TableCell>
475
+ <TableCell>
476
+ {row.status === 'valid' && (
477
+ <Badge variant="outline" className="text-emerald-600 border-emerald-500/30 bg-emerald-500/10 gap-1">
478
+ <CheckCircle2 className="h-3 w-3" />
479
+ Valid
480
+ </Badge>
481
+ )}
482
+ {row.status === 'warning' && (
483
+ <Badge variant="outline" className="text-amber-600 border-amber-500/30 bg-amber-500/10 gap-1">
484
+ <AlertTriangle className="h-3 w-3" />
485
+ Notice
486
+ </Badge>
487
+ )}
488
+ {row.status === 'error' && (
489
+ <Badge variant="destructive" className="gap-1">
490
+ <XCircle className="h-3 w-3" />
491
+ Invalid
492
+ </Badge>
493
+ )}
494
+ </TableCell>
495
+ {entity === 'devices' ? (
496
+ <>
497
+ <TableCell className="font-mono text-xs font-semibold">{row.data.assetNumber || '—'}</TableCell>
498
+ <TableCell className="font-medium text-xs">{row.data.name || '—'}</TableCell>
499
+ <TableCell className="text-xs">{row.data.categoryName || '—'}</TableCell>
500
+ <TableCell className="text-xs">{row.data.departmentName || '—'}</TableCell>
501
+ </>
502
+ ) : (
503
+ <>
504
+ <TableCell className="font-medium text-xs">{row.data.name || '—'}</TableCell>
505
+ <TableCell className="font-mono text-xs">{row.data.code || '—'}</TableCell>
506
+ <TableCell className="text-xs">{row.data.departmentType || '—'}</TableCell>
507
+ <TableCell className="text-xs">{row.data.hospitalName || '—'}</TableCell>
508
+ </>
509
+ )}
510
+ <TableCell className="text-xs text-muted-foreground max-w-xs">
511
+ {row.messages?.length > 0 ? row.messages.join(' • ') : 'Ready to import'}
512
+ </TableCell>
513
+ </TableRow>
514
+ ))}
515
+ </TableBody>
516
+ </Table>
517
+ </div>
518
+ </div>
519
+ )}
520
+
521
+ {/* Step 3: Results */}
522
+ {step === 3 && (
523
+ <div className="flex flex-col items-center justify-center py-8 space-y-4 text-center">
524
+ <CheckCircle2 className="h-16 w-16 text-emerald-500" />
525
+ <h2 className="text-2xl font-bold">Import Completed Successfully</h2>
526
+ <p className="text-muted-foreground">
527
+ {importedCount} records imported into the system
528
+ </p>
529
+ <Button onClick={handleReset} variant="outline" className="mt-4 gap-2">
530
+ <RotateCcw className="h-4 w-4" />
531
+ Import Another File
532
+ </Button>
533
+ </div>
534
+ )}
535
+ </CardContent>
536
+
537
+ {step === 2 && (
538
+ <CardFooter className="flex flex-col sm:flex-row items-center justify-between border-t pt-4 gap-3">
539
+ <Button variant="ghost" size="sm" onClick={() => setStep(1)} className="gap-2">
540
+ <ArrowLeft className="h-4 w-4" />
541
+ Back to Upload
542
+ </Button>
543
+ <Button
544
+ onClick={handleExecuteImport}
545
+ disabled={importing || importableCount === 0}
546
+ className="gap-2 w-full sm:w-auto"
547
+ >
548
+ {importing && <Loader2 className="h-4 w-4 animate-spin" />}
549
+ {!importing && <ArrowRight className="h-4 w-4" />}
550
+ Confirm Import ({importableCount} Records)
551
+ </Button>
552
+ </CardFooter>
553
+ )}
554
+ </Card>
555
+ </div>
556
+ );
557
+ }
558
+
@@ -0,0 +1,115 @@
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 { Sliders, Route, Loader2, Save } from 'lucide-react';
10
+ import { toast } from 'sonner';
11
+ import { updateOrganizationSettings } from '@/lib/actions/system-settings';
12
+ import type { FeaturesSettings } from '@/lib/validators/system-settings';
13
+
14
+ interface FeaturesFormProps {
15
+ initialData?: FeaturesSettings;
16
+ organizationId: string;
17
+ }
18
+
19
+ export function FeaturesForm({ initialData, organizationId }: FeaturesFormProps) {
20
+ const [formData, setFormData] = useState<FeaturesSettings>(
21
+ initialData || { engineerRoundEnabled: false }
22
+ );
23
+ const [changeReason, setChangeReason] = useState('');
24
+ const [saving, setSaving] = useState(false);
25
+
26
+ const handleSubmit = async (e: React.FormEvent) => {
27
+ e.preventDefault();
28
+ setSaving(true);
29
+
30
+ try {
31
+ const res = await updateOrganizationSettings({
32
+ organizationId,
33
+ section: 'features',
34
+ settings: formData,
35
+ changeReason: changeReason.trim() || 'Updated optional feature toggles',
36
+ });
37
+
38
+ if (res.success) {
39
+ toast.success('Feature settings saved successfully');
40
+ setChangeReason('');
41
+ } else {
42
+ toast.error(res.error || 'Failed to update feature settings');
43
+ }
44
+ } catch {
45
+ toast.error('An unexpected error occurred');
46
+ } finally {
47
+ setSaving(false);
48
+ }
49
+ };
50
+
51
+ return (
52
+ <form onSubmit={handleSubmit}>
53
+ <Card>
54
+ <CardHeader>
55
+ <CardTitle className="flex items-center gap-2 text-xl">
56
+ <Sliders className="h-5 w-5 text-primary" />
57
+ Optional Module & Feature Toggles
58
+ </CardTitle>
59
+ <CardDescription>
60
+ Enable or deactivate optional modules and workflows to tailor BEMMS to your hospital&apos;s operations.
61
+ </CardDescription>
62
+ </CardHeader>
63
+
64
+ <CardContent className="space-y-6">
65
+ <div className="rounded-lg border p-4 transition-colors hover:bg-muted/30">
66
+ <div className="flex items-start justify-between gap-4">
67
+ <div className="flex gap-3">
68
+ <div className="mt-1 rounded-md bg-primary/10 p-2 text-primary">
69
+ <Route className="h-5 w-5" />
70
+ </div>
71
+ <div className="space-y-1">
72
+ <Label htmlFor="engineerRoundEnabled" className="text-base font-semibold cursor-pointer">
73
+ Engineer&apos;s Daily Round
74
+ </Label>
75
+ <p className="text-sm text-muted-foreground">
76
+ Digitize the daily physical walk-through performed by biomedical engineers across hospital
77
+ departments with electronic signatures, to-do checklists, notes with @device mentions, and round history tracking.
78
+ </p>
79
+ <p className="text-xs text-muted-foreground">
80
+ When disabled, the Daily Round menu items and scheduling will be hidden.
81
+ </p>
82
+ </div>
83
+ </div>
84
+
85
+ <Switch
86
+ id="engineerRoundEnabled"
87
+ checked={formData.engineerRoundEnabled}
88
+ onCheckedChange={(checked) =>
89
+ setFormData((prev) => ({ ...prev, engineerRoundEnabled: checked }))
90
+ }
91
+ />
92
+ </div>
93
+ </div>
94
+
95
+ <div className="space-y-2 pt-2 border-t">
96
+ <Label htmlFor="changeReasonFeatures">Change Reason (for compliance audit trail)</Label>
97
+ <Input
98
+ id="changeReasonFeatures"
99
+ placeholder="e.g., Enabled Daily Rounds for biomedical engineering staff"
100
+ value={changeReason}
101
+ onChange={(e) => setChangeReason(e.target.value)}
102
+ />
103
+ </div>
104
+ </CardContent>
105
+
106
+ <CardFooter className="flex justify-end border-t pt-4">
107
+ <Button type="submit" disabled={saving} className="gap-2">
108
+ {saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
109
+ Save Changes
110
+ </Button>
111
+ </CardFooter>
112
+ </Card>
113
+ </form>
114
+ );
115
+ }