@omersx/bemms 0.0.0-stage → 0.1.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,178 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ createMaintenanceTaskSchema,
4
+ assignMaintenanceTaskSchema,
5
+ updateMaintenanceTaskStageSchema,
6
+ completeMaintenanceTaskSchema,
7
+ cancelMaintenanceTaskSchema,
8
+ createMaintenancePlanSchema,
9
+ } from '../maintenance';
10
+
11
+ describe('Maintenance Validators', () => {
12
+ const validUUID1 = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11';
13
+ const validUUID2 = 'b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22';
14
+ const validUUID3 = 'c0eebc99-9c0b-4ef8-bb6d-6bb9bd380a33';
15
+
16
+ describe('createMaintenanceTaskSchema', () => {
17
+ it('validates a valid corrective maintenance task', () => {
18
+ const data = {
19
+ organizationId: validUUID1,
20
+ hospitalId: validUUID2,
21
+ deviceId: validUUID3,
22
+ maintenanceType: 'corrective_maintenance',
23
+ technicalProblemCategory: 'electrical_power',
24
+ title: 'Repair ventilator power supply',
25
+ description: 'Blown capacitor on secondary DC rail',
26
+ priorityCode: 'p1_critical',
27
+ };
28
+
29
+ const result = createMaintenanceTaskSchema.safeParse(data);
30
+ expect(result.success).toBe(true);
31
+ if (result.success) {
32
+ expect(result.data.title).toBe('Repair ventilator power supply');
33
+ expect(result.data.priorityCode).toBe('p1_critical');
34
+ }
35
+ });
36
+
37
+ it('validates a preventive maintenance task with default priority', () => {
38
+ const data = {
39
+ organizationId: validUUID1,
40
+ hospitalId: validUUID2,
41
+ deviceId: validUUID3,
42
+ maintenanceType: 'preventive_maintenance',
43
+ title: '6-Month Anesthesia PM Routine',
44
+ };
45
+
46
+ const result = createMaintenanceTaskSchema.safeParse(data);
47
+ expect(result.success).toBe(true);
48
+ if (result.success) {
49
+ expect(result.data.priorityCode).toBe('p3_normal');
50
+ }
51
+ });
52
+
53
+ it('rejects title shorter than 5 characters', () => {
54
+ const data = {
55
+ organizationId: validUUID1,
56
+ hospitalId: validUUID2,
57
+ deviceId: validUUID3,
58
+ maintenanceType: 'corrective_maintenance',
59
+ title: 'Fix',
60
+ };
61
+
62
+ const result = createMaintenanceTaskSchema.safeParse(data);
63
+ expect(result.success).toBe(false);
64
+ });
65
+
66
+ it('rejects invalid maintenance type', () => {
67
+ const data = {
68
+ organizationId: validUUID1,
69
+ hospitalId: validUUID2,
70
+ deviceId: validUUID3,
71
+ maintenanceType: 'magical_repair',
72
+ title: 'Fix defibrillator',
73
+ };
74
+
75
+ const result = createMaintenanceTaskSchema.safeParse(data);
76
+ expect(result.success).toBe(false);
77
+ });
78
+ });
79
+
80
+ describe('assignMaintenanceTaskSchema', () => {
81
+ it('validates engineer assignment with optional handoff note', () => {
82
+ const result = assignMaintenanceTaskSchema.safeParse({
83
+ assignedEngineerUserId: validUUID1,
84
+ handoffNote: 'Device is located in ICU Room 4',
85
+ });
86
+ expect(result.success).toBe(true);
87
+ });
88
+
89
+ it('fails if assignedEngineerUserId is missing or not a UUID', () => {
90
+ const result = assignMaintenanceTaskSchema.safeParse({
91
+ assignedEngineerUserId: 'not-a-uuid',
92
+ });
93
+ expect(result.success).toBe(false);
94
+ });
95
+ });
96
+
97
+ describe('updateMaintenanceTaskStageSchema', () => {
98
+ it('validates waiting dependency details', () => {
99
+ const result = updateMaintenanceTaskStageSchema.safeParse({
100
+ waitingDependencyType: 'parts',
101
+ waitingReason: 'Awaiting OEM pressure sensor delivery',
102
+ });
103
+ expect(result.success).toBe(true);
104
+ });
105
+
106
+ it('rejects waitingReason shorter than 5 chars if provided', () => {
107
+ const result = updateMaintenanceTaskStageSchema.safeParse({
108
+ waitingDependencyType: 'vendor',
109
+ waitingReason: 'Wait',
110
+ });
111
+ expect(result.success).toBe(false);
112
+ });
113
+ });
114
+
115
+ describe('completeMaintenanceTaskSchema', () => {
116
+ it('validates completion payload with final result and device release status', () => {
117
+ const payload = {
118
+ diagnosis: 'Defective power supply board',
119
+ rootCause: 'Electrical mains surge',
120
+ workPerformed: 'Replaced main PCB, verified voltage rails, passed electrical safety testing',
121
+ finalResultCode: 'passed',
122
+ finalDeviceStatusCode: 'operational',
123
+ };
124
+
125
+ const result = completeMaintenanceTaskSchema.safeParse(payload);
126
+ expect(result.success).toBe(true);
127
+ });
128
+
129
+ it('rejects workPerformed if shorter than 10 characters', () => {
130
+ const payload = {
131
+ workPerformed: 'Done fix',
132
+ finalResultCode: 'passed',
133
+ finalDeviceStatusCode: 'operational',
134
+ };
135
+
136
+ const result = completeMaintenanceTaskSchema.safeParse(payload);
137
+ expect(result.success).toBe(false);
138
+ });
139
+ });
140
+
141
+ describe('cancelMaintenanceTaskSchema', () => {
142
+ it('requires a cancellation reason with at least 10 chars', () => {
143
+ const valid = cancelMaintenanceTaskSchema.safeParse({
144
+ cancellationReason: 'Device decommissioned before scheduled repair',
145
+ });
146
+ expect(valid.success).toBe(true);
147
+
148
+ const invalid = cancelMaintenanceTaskSchema.safeParse({
149
+ cancellationReason: 'Too short',
150
+ });
151
+ expect(invalid.success).toBe(false);
152
+ });
153
+ });
154
+
155
+ describe('createMaintenancePlanSchema', () => {
156
+ it('validates PM plan creation parameters', () => {
157
+ const plan = {
158
+ organizationId: validUUID1,
159
+ hospitalId: validUUID2,
160
+ deviceId: validUUID3,
161
+ title: 'Quarterly Defibrillator Safety & Calibration',
162
+ planTypeCode: 'preventive_maintenance' as const,
163
+ calculationMethod: 'fixed_calendar' as const,
164
+ frequencyInterval: 3,
165
+ frequencyUnit: 'months' as const,
166
+ startBaselineDate: '2026-11-01T00:00:00.000Z',
167
+ leadTimeDays: 7,
168
+ };
169
+
170
+ const result = createMaintenancePlanSchema.safeParse(plan);
171
+ expect(result.success).toBe(true);
172
+ if (result.success) {
173
+ expect(result.data.frequencyInterval).toBe(3);
174
+ expect(result.data.frequencyUnit).toBe('months');
175
+ }
176
+ });
177
+ });
178
+ });
@@ -0,0 +1,154 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+
3
+ vi.mock('@/lib/auth', () => ({
4
+ auth: vi.fn(),
5
+ }));
6
+
7
+ vi.mock('@/lib/db', () => ({
8
+ db: {
9
+ query: {},
10
+ select: vi.fn(),
11
+ insert: vi.fn(),
12
+ update: vi.fn(),
13
+ transaction: vi.fn(),
14
+ },
15
+ }));
16
+
17
+ import {
18
+ createSignaturePolicySchema,
19
+ signActionSchema,
20
+ signatureRequirementSchema,
21
+ } from '../signatures';
22
+ import { getAttestationText } from '@/lib/utils/attestations';
23
+
24
+ describe('Signature Validators', () => {
25
+ describe('signatureRequirementSchema', () => {
26
+ it('validates a correct signature requirement', () => {
27
+ const valid = {
28
+ purpose: 'perform',
29
+ allowedRoles: ['BME_TECHNICIAN', 'BME_ENGINEER'],
30
+ required: true,
31
+ };
32
+ const result = signatureRequirementSchema.safeParse(valid);
33
+ expect(result.success).toBe(true);
34
+ });
35
+
36
+ it('rejects an empty allowedRoles array', () => {
37
+ const invalid = {
38
+ purpose: 'perform',
39
+ allowedRoles: [],
40
+ required: true,
41
+ };
42
+ const result = signatureRequirementSchema.safeParse(invalid);
43
+ expect(result.success).toBe(false);
44
+ });
45
+
46
+ it('rejects an invalid purpose', () => {
47
+ const invalid = {
48
+ purpose: 'invalid_purpose',
49
+ allowedRoles: ['BME_ENGINEER'],
50
+ required: true,
51
+ };
52
+ const result = signatureRequirementSchema.safeParse(invalid);
53
+ expect(result.success).toBe(false);
54
+ });
55
+ });
56
+
57
+ describe('createSignaturePolicySchema', () => {
58
+ it('validates a complete signature policy input', () => {
59
+ const valid = {
60
+ actionType: 'complete_corrective',
61
+ criticalityLevel: 'high',
62
+ requiredSignatures: [
63
+ { purpose: 'perform', allowedRoles: ['BME_TECHNICIAN'], required: true },
64
+ { purpose: 'review', allowedRoles: ['BME_ENGINEER', 'BME_MANAGER'], required: true },
65
+ ],
66
+ requireIndependentReview: true,
67
+ performerCanRelease: false,
68
+ requireReauth: true,
69
+ };
70
+ const result = createSignaturePolicySchema.safeParse(valid);
71
+ expect(result.success).toBe(true);
72
+ });
73
+
74
+ it('applies defaults for optional flags', () => {
75
+ const minimal = {
76
+ actionType: 'complete_pm',
77
+ requiredSignatures: [
78
+ { purpose: 'perform', allowedRoles: ['BME_ENGINEER'], required: true },
79
+ ],
80
+ };
81
+ const parsed = createSignaturePolicySchema.parse(minimal);
82
+ expect(parsed.requireIndependentReview).toBe(false);
83
+ expect(parsed.performerCanRelease).toBe(true);
84
+ expect(parsed.requireReauth).toBe(true);
85
+ expect(parsed.policyVersion).toBe('1.0');
86
+ });
87
+
88
+ it('rejects an invalid actionType', () => {
89
+ const invalid = {
90
+ actionType: 'non_existent_action',
91
+ requiredSignatures: [
92
+ { purpose: 'perform', allowedRoles: ['BME_ENGINEER'], required: true },
93
+ ],
94
+ };
95
+ const result = createSignaturePolicySchema.safeParse(invalid);
96
+ expect(result.success).toBe(false);
97
+ });
98
+ });
99
+
100
+ describe('signActionSchema', () => {
101
+ it('validates a valid sign action payload with password and comments', () => {
102
+ const valid = {
103
+ signaturePurpose: 'perform',
104
+ password: 'SecurePassword123!',
105
+ comments: 'Checklist completed with no anomalies.',
106
+ };
107
+ const result = signActionSchema.safeParse(valid);
108
+ expect(result.success).toBe(true);
109
+ });
110
+
111
+ it('rejects an empty password', () => {
112
+ const invalid = {
113
+ signaturePurpose: 'perform',
114
+ password: '',
115
+ };
116
+ const result = signActionSchema.safeParse(invalid);
117
+ expect(result.success).toBe(false);
118
+ if (!result.success) {
119
+ expect(result.error.issues[0].message).toContain('Password is required');
120
+ }
121
+ });
122
+
123
+ it('accepts all standard signature purposes', () => {
124
+ const purposes = [
125
+ 'accept', 'perform', 'review', 'approve',
126
+ 'release', 'resolve', 'close', 'reject',
127
+ 'amend', 'void',
128
+ ];
129
+ for (const purpose of purposes) {
130
+ const result = signActionSchema.safeParse({
131
+ signaturePurpose: purpose,
132
+ password: 'secret',
133
+ });
134
+ expect(result.success).toBe(true);
135
+ }
136
+ });
137
+ });
138
+ });
139
+
140
+ describe('getAttestationText', () => {
141
+ it('returns specific attestation text for known purposes', () => {
142
+ expect(getAttestationText('perform')).toContain('performed the recorded maintenance');
143
+ expect(getAttestationText('review')).toContain('reviewed the performed work');
144
+ expect(getAttestationText('release')).toContain('authorize the release of this medical device');
145
+ expect(getAttestationText('approve')).toContain('approve the recorded work');
146
+ expect(getAttestationText('reject')).toContain('returning this work for correction');
147
+ });
148
+
149
+ it('returns fallback attestation for unknown purpose', () => {
150
+ const text = getAttestationText('custom_action');
151
+ expect(text).toContain('I confirm this custom_action action');
152
+ });
153
+ });
154
+
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ localizationSettingsSchema,
4
+ securitySettingsSchema,
5
+ retentionSettingsSchema,
6
+ systemSettingsSchema,
7
+ } from '../system-settings';
8
+
9
+ describe('localizationSettingsSchema', () => {
10
+ it('accepts valid localization settings', () => {
11
+ const result = localizationSettingsSchema.safeParse({
12
+ timezone: 'Africa/Cairo',
13
+ dateFormat: 'YYYY-MM-DD',
14
+ timeFormat: '24h',
15
+ language: 'en',
16
+ });
17
+ expect(result.success).toBe(true);
18
+ });
19
+
20
+ it('accepts Arabic language setting per Section 14.1', () => {
21
+ const result = localizationSettingsSchema.safeParse({
22
+ timezone: 'Asia/Riyadh',
23
+ dateFormat: 'DD/MM/YYYY',
24
+ timeFormat: '12h',
25
+ language: 'ar',
26
+ });
27
+ expect(result.success).toBe(true);
28
+ });
29
+
30
+ it('rejects unsupported language', () => {
31
+ const result = localizationSettingsSchema.safeParse({
32
+ timezone: 'UTC',
33
+ dateFormat: 'YYYY-MM-DD',
34
+ timeFormat: '24h',
35
+ language: 'es',
36
+ });
37
+ expect(result.success).toBe(false);
38
+ });
39
+
40
+ it('rejects invalid date format', () => {
41
+ const result = localizationSettingsSchema.safeParse({
42
+ timezone: 'UTC',
43
+ dateFormat: 'DD-MM-YYYY', // unsupported delimiter
44
+ timeFormat: '24h',
45
+ language: 'en',
46
+ });
47
+ expect(result.success).toBe(false);
48
+ });
49
+ });
50
+
51
+ describe('securitySettingsSchema', () => {
52
+ it('accepts standard 21 CFR Part 11 security settings', () => {
53
+ const result = securitySettingsSchema.safeParse({
54
+ sessionTimeoutMinutes: 15,
55
+ passwordExpiryDays: 90,
56
+ requireReAuthForSignatures: true,
57
+ maxLoginAttempts: 5,
58
+ });
59
+ expect(result.success).toBe(true);
60
+ });
61
+
62
+ it('rejects timeout below minimum threshold', () => {
63
+ const result = securitySettingsSchema.safeParse({
64
+ sessionTimeoutMinutes: 2, // min is 5
65
+ passwordExpiryDays: 90,
66
+ requireReAuthForSignatures: true,
67
+ maxLoginAttempts: 5,
68
+ });
69
+ expect(result.success).toBe(false);
70
+ });
71
+
72
+ it('coerces string inputs to numbers gracefully', () => {
73
+ const result = securitySettingsSchema.safeParse({
74
+ sessionTimeoutMinutes: '30',
75
+ passwordExpiryDays: '60',
76
+ requireReAuthForSignatures: false,
77
+ maxLoginAttempts: '3',
78
+ });
79
+ expect(result.success).toBe(true);
80
+ if (result.success) {
81
+ expect(result.data.sessionTimeoutMinutes).toBe(30);
82
+ expect(result.data.passwordExpiryDays).toBe(60);
83
+ expect(result.data.maxLoginAttempts).toBe(3);
84
+ }
85
+ });
86
+ });
87
+
88
+ describe('retentionSettingsSchema', () => {
89
+ it('accepts valid retention policies', () => {
90
+ const result = retentionSettingsSchema.safeParse({
91
+ auditLogRetentionDays: 365,
92
+ pmAlertWindowDays: 14,
93
+ attachmentQuotaMb: 10000,
94
+ });
95
+ expect(result.success).toBe(true);
96
+ });
97
+
98
+ it('rejects audit log retention less than 30 days', () => {
99
+ const result = retentionSettingsSchema.safeParse({
100
+ auditLogRetentionDays: 10,
101
+ pmAlertWindowDays: 14,
102
+ attachmentQuotaMb: 5000,
103
+ });
104
+ expect(result.success).toBe(false);
105
+ });
106
+ });
107
+
108
+ describe('systemSettingsSchema', () => {
109
+ it('provides complete defaults when given empty object', () => {
110
+ const result = systemSettingsSchema.safeParse({});
111
+ expect(result.success).toBe(true);
112
+ if (result.success) {
113
+ expect(result.data.localization.timezone).toBe('UTC');
114
+ expect(result.data.security.requireReAuthForSignatures).toBe(true);
115
+ expect(result.data.retention.pmAlertWindowDays).toBe(14);
116
+ }
117
+ });
118
+ });
119
+
@@ -0,0 +1,176 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ createTicketSchema,
4
+ triageTicketSchema,
5
+ resolveTicketSchema,
6
+ updateTicketDeviceStatusSchema,
7
+ } from '../tickets';
8
+
9
+ describe('createTicketSchema', () => {
10
+ const validUuid = '123e4567-e89b-12d3-a456-426614174000';
11
+
12
+ it('validates a valid ticket creation request', () => {
13
+ const result = createTicketSchema.safeParse({
14
+ organizationId: validUuid,
15
+ hospitalId: validUuid,
16
+ departmentId: validUuid,
17
+ source: 'qr_scan',
18
+ ticketType: 'device_problem',
19
+ title: 'Display screen flickering constantly',
20
+ description: 'The unit screen flickers on startup and powers off unexpectedly after 5 minutes.',
21
+ reportedImpact: 'device_not_usable',
22
+ reportedProblemCategory: 'display_interface',
23
+ });
24
+ expect(result.success).toBe(true);
25
+ });
26
+
27
+ it('rejects title shorter than 5 characters', () => {
28
+ const result = createTicketSchema.safeParse({
29
+ organizationId: validUuid,
30
+ hospitalId: validUuid,
31
+ departmentId: validUuid,
32
+ source: 'manual_entry',
33
+ title: 'Bad',
34
+ description: 'The device is completely broken and unusable.',
35
+ reportedImpact: 'device_not_usable',
36
+ });
37
+ expect(result.success).toBe(false);
38
+ });
39
+
40
+ it('rejects description shorter than 10 characters', () => {
41
+ const result = createTicketSchema.safeParse({
42
+ organizationId: validUuid,
43
+ hospitalId: validUuid,
44
+ departmentId: validUuid,
45
+ source: 'manual_entry',
46
+ title: 'Broken button on front panel',
47
+ description: 'Broken',
48
+ reportedImpact: 'device_usable',
49
+ });
50
+ expect(result.success).toBe(false);
51
+ });
52
+ });
53
+
54
+ describe('triageTicketSchema', () => {
55
+ it('validates a triage assignment', () => {
56
+ const result = triageTicketSchema.safeParse({
57
+ priorityCode: 'p1_critical',
58
+ priorityReason: 'Life support equipment non-functional in ICU',
59
+ selectedMaintenanceType: 'corrective_maintenance',
60
+ selectedTechnicalCategory: 'electrical_power',
61
+ });
62
+ expect(result.success).toBe(true);
63
+ });
64
+
65
+ it('rejects invalid priority code', () => {
66
+ const result = triageTicketSchema.safeParse({
67
+ priorityCode: 'urgent',
68
+ });
69
+ expect(result.success).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe('resolveTicketSchema', () => {
74
+ it('accepts valid resolution with operational device status', () => {
75
+ const result = resolveTicketSchema.safeParse({
76
+ resolutionSummary: 'Power supply board replaced and tested according to manufacturer calibration spec.',
77
+ finalDeviceStatusCode: 'operational',
78
+ });
79
+ expect(result.success).toBe(true);
80
+ });
81
+
82
+ it('accepts valid resolution with operational_with_limitations', () => {
83
+ const result = resolveTicketSchema.safeParse({
84
+ resolutionSummary: 'Primary channel calibrated; secondary probe port disabled pending replacement part.',
85
+ finalDeviceStatusCode: 'operational_with_limitations',
86
+ });
87
+ expect(result.success).toBe(true);
88
+ });
89
+
90
+ it('accepts valid resolution with operational_with_limitations and limitationsNote', () => {
91
+ const result = resolveTicketSchema.safeParse({
92
+ resolutionSummary: 'Primary channel calibrated; secondary probe port disabled pending replacement part.',
93
+ finalDeviceStatusCode: 'operational_with_limitations',
94
+ limitationsNote: 'Secondary probe port disabled. Use Channel 1 only.',
95
+ });
96
+ expect(result.success).toBe(true);
97
+ });
98
+
99
+ it('accepts valid resolution with awaiting_release, standby, and decommissioned', () => {
100
+ for (const status of ['awaiting_release', 'standby', 'decommissioned'] as const) {
101
+ const result = resolveTicketSchema.safeParse({
102
+ resolutionSummary: 'Maintenance procedure completed in full compliance with protocol.',
103
+ finalDeviceStatusCode: status,
104
+ });
105
+ expect(result.success).toBe(true);
106
+ }
107
+ });
108
+
109
+ it('accepts valid resolution with out_of_service (unrepairable / condemned)', () => {
110
+ const result = resolveTicketSchema.safeParse({
111
+ resolutionSummary: 'Main motherboard cracked beyond repair. Equipment condemned and recommended for decommissioning.',
112
+ finalDeviceStatusCode: 'out_of_service',
113
+ });
114
+ expect(result.success).toBe(true);
115
+ });
116
+
117
+ it('rejects resolving a ticket with under_repair device status', () => {
118
+ const result = resolveTicketSchema.safeParse({
119
+ resolutionSummary: 'Device is still being worked on by external biomedical service engineer.',
120
+ finalDeviceStatusCode: 'under_repair',
121
+ });
122
+ expect(result.success).toBe(false);
123
+ });
124
+
125
+ it('rejects resolving a ticket with waiting_for_parts device status', () => {
126
+ const result = resolveTicketSchema.safeParse({
127
+ resolutionSummary: 'Part ordered from Philips Medical, expected next Tuesday.',
128
+ finalDeviceStatusCode: 'waiting_for_parts',
129
+ });
130
+ expect(result.success).toBe(false);
131
+ });
132
+
133
+ it('rejects resolution summary shorter than 10 characters', () => {
134
+ const result = resolveTicketSchema.safeParse({
135
+ resolutionSummary: 'Fixed it',
136
+ finalDeviceStatusCode: 'operational',
137
+ });
138
+ expect(result.success).toBe(false);
139
+ });
140
+ });
141
+
142
+ describe('updateTicketDeviceStatusSchema', () => {
143
+ it('accepts interim under_repair status with progress note', () => {
144
+ const result = updateTicketDeviceStatusSchema.safeParse({
145
+ deviceStatusCode: 'under_repair',
146
+ notes: 'Disassembled unit on bench; isolating display inverter circuit.',
147
+ });
148
+ expect(result.success).toBe(true);
149
+ });
150
+
151
+ it('accepts waiting_for_parts status with note', () => {
152
+ const result = updateTicketDeviceStatusSchema.safeParse({
153
+ deviceStatusCode: 'waiting_for_parts',
154
+ notes: 'Waiting for replacement oxygen cell sensor from vendor.',
155
+ });
156
+ expect(result.success).toBe(true);
157
+ });
158
+
159
+ it('accepts operational_with_limitations with limitationsNote', () => {
160
+ const result = updateTicketDeviceStatusSchema.safeParse({
161
+ deviceStatusCode: 'operational_with_limitations',
162
+ notes: 'Calibrated main unit; SpO2 module pending replacement.',
163
+ limitationsNote: 'SpO2 sensor offline; NIBP and ECG operational.',
164
+ });
165
+ expect(result.success).toBe(true);
166
+ });
167
+
168
+ it('rejects notes shorter than 3 characters', () => {
169
+ const result = updateTicketDeviceStatusSchema.safeParse({
170
+ deviceStatusCode: 'under_repair',
171
+ notes: 'ok',
172
+ });
173
+ expect(result.success).toBe(false);
174
+ });
175
+ });
176
+