@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,92 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { updateUserProfileSchema, inviteUserSchema } from '../users';
3
+
4
+ describe('updateUserProfileSchema', () => {
5
+ it('validates profile updates with valid clinical title and phone', () => {
6
+ const result = updateUserProfileSchema.safeParse({
7
+ fullName: 'Dr. Sarah Al-Mansoor',
8
+ jobTitle: 'Charge Nurse - Intensive Care Unit',
9
+ phone: '+966 50 123 4567',
10
+ employeeIdentifier: 'Ext. 3120 - Bed 4',
11
+ });
12
+ expect(result.success).toBe(true);
13
+ });
14
+
15
+ it('accepts partial updates', () => {
16
+ const result = updateUserProfileSchema.safeParse({
17
+ phone: '+966 55 987 6543',
18
+ });
19
+ expect(result.success).toBe(true);
20
+ });
21
+
22
+ it('rejects fullName that is too short', () => {
23
+ const result = updateUserProfileSchema.safeParse({
24
+ fullName: 'A',
25
+ });
26
+ expect(result.success).toBe(false);
27
+ });
28
+ });
29
+
30
+ describe('inviteUserSchema', () => {
31
+ it('validates user invitation payload with required fields', () => {
32
+ const result = inviteUserSchema.safeParse({
33
+ fullName: 'Nurse Layla',
34
+ email: 'layla@hospital.org',
35
+ jobTitle: 'Staff Nurse',
36
+ organizationId: '123e4567-e89b-12d3-a456-426614174000',
37
+ roleIds: ['123e4567-e89b-12d3-a456-426614174001'],
38
+ });
39
+ expect(result.success).toBe(true);
40
+ });
41
+
42
+ it('validates user invitation payload with optional hospital, department and specialization', () => {
43
+ const result = inviteUserSchema.safeParse({
44
+ fullName: 'Eng. Khalid Al-Otaibi',
45
+ email: 'khalid@hospital.org',
46
+ jobTitle: 'Senior Biomedical Engineer',
47
+ organizationId: '123e4567-e89b-12d3-a456-426614174000',
48
+ roleIds: ['123e4567-e89b-12d3-a456-426614174001'],
49
+ hospitalId: '123e4567-e89b-12d3-a456-426614174002',
50
+ departmentId: '123e4567-e89b-12d3-a456-426614174003',
51
+ specialization: 'ICU & Ventilators',
52
+ });
53
+ expect(result.success).toBe(true);
54
+ });
55
+
56
+ it('validates addUser with initial password and dutyStatus', () => {
57
+ const result = inviteUserSchema.safeParse({
58
+ fullName: 'Eng. Omar',
59
+ email: 'omar@hospital.org',
60
+ organizationId: '123e4567-e89b-12d3-a456-426614174000',
61
+ roleIds: ['123e4567-e89b-12d3-a456-426614174001'],
62
+ password: 'SecurePassword123!',
63
+ dutyStatus: 'on_duty',
64
+ });
65
+ expect(result.success).toBe(true);
66
+ });
67
+
68
+ it('validates addUser with explicit username', () => {
69
+ const result = inviteUserSchema.safeParse({
70
+ fullName: 'Eng. Omar',
71
+ email: 'omar@hospital.org',
72
+ username: 'eng.omar',
73
+ organizationId: '123e4567-e89b-12d3-a456-426614174000',
74
+ roleIds: ['123e4567-e89b-12d3-a456-426614174001'],
75
+ password: 'SecurePassword123!',
76
+ });
77
+ expect(result.success).toBe(true);
78
+ });
79
+
80
+ it('rejects password shorter than 6 characters if provided', () => {
81
+ const result = inviteUserSchema.safeParse({
82
+ fullName: 'Eng. Omar',
83
+ email: 'omar@hospital.org',
84
+ organizationId: '123e4567-e89b-12d3-a456-426614174000',
85
+ roleIds: ['123e4567-e89b-12d3-a456-426614174001'],
86
+ password: '123',
87
+ });
88
+ expect(result.success).toBe(false);
89
+ });
90
+ });
91
+
92
+
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+
3
+ export const loginSchema = z.object({
4
+ email: z.string().min(1, 'Username or email is required'),
5
+ password: z.string().min(6, 'Password must be at least 6 characters'),
6
+ });
7
+
8
+ export const registerSchema = loginSchema.extend({
9
+ fullName: z.string().min(2, 'Full name must be at least 2 characters'),
10
+ phone: z.string().optional(),
11
+ jobTitle: z.string().optional(),
12
+ });
13
+
14
+ export const passwordResetSchema = z.object({
15
+ email: z.string().email('Invalid email address'),
16
+ });
17
+
18
+ export const changePasswordSchema = z.object({
19
+ currentPassword: z.string(),
20
+ newPassword: z.string()
21
+ .min(8, 'Password must be at least 8 characters')
22
+ .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
23
+ .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
24
+ .regex(/[0-9]/, 'Password must contain at least one number')
25
+ .regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
26
+ });
27
+
28
+ export type LoginInput = z.infer<typeof loginSchema>;
29
+ export type RegisterInput = z.infer<typeof registerSchema>;
30
+ export type PasswordResetInput = z.infer<typeof passwordResetSchema>;
31
+ export type ChangePasswordInput = z.infer<typeof changePasswordSchema>;
32
+
@@ -0,0 +1,67 @@
1
+ import { z } from 'zod';
2
+
3
+ export const deviceImportRowSchema = z.object({
4
+ assetNumber: z.string().min(1, 'Asset number is required').trim(),
5
+ name: z.string().min(1, 'Device name is required').trim(),
6
+ model: z.string().optional().default(''),
7
+ serialNumber: z.string().optional().default(''),
8
+ categoryName: z.string().min(1, 'Category is required').trim(),
9
+ manufacturerName: z.string().min(1, 'Manufacturer is required').trim(),
10
+ departmentName: z.string().min(1, 'Department is required').trim(),
11
+ locationDescription: z.string().optional().default(''),
12
+ riskClassification: z.enum(['class_i', 'class_iia', 'class_iib', 'class_iii']).default('class_i'),
13
+ criticalityLevel: z.enum(['low', 'medium', 'high']).default('medium'),
14
+ purchaseDate: z.string().optional().default(''),
15
+ purchaseCost: z.string().optional().default(''),
16
+ installationDate: z.string().optional().default(''),
17
+ warrantyStartDate: z.string().optional().default(''),
18
+ warrantyEndDate: z.string().optional().default(''),
19
+ });
20
+
21
+ export type DeviceImportRow = z.infer<typeof deviceImportRowSchema>;
22
+
23
+ export interface DeviceImportValidationResult {
24
+ rowNumber: number;
25
+ data: DeviceImportRow;
26
+ status: 'valid' | 'warning' | 'error';
27
+ messages: string[];
28
+ resolvedCategoryId?: string;
29
+ resolvedManufacturerId?: string;
30
+ resolvedDepartmentId?: string;
31
+ willCreateCategory?: boolean;
32
+ willCreateManufacturer?: boolean;
33
+ }
34
+
35
+ export const departmentImportRowSchema = z.object({
36
+ name: z.string().min(1, 'Department name is required').trim(),
37
+ code: z.string().min(1, 'Department code is required').trim(),
38
+ departmentType: z.string().optional().default(''),
39
+ hospitalName: z.string().min(1, 'Hospital name is required').trim(),
40
+ });
41
+
42
+ export type DepartmentImportRow = z.infer<typeof departmentImportRowSchema>;
43
+
44
+ export interface DepartmentImportValidationResult {
45
+ rowNumber: number;
46
+ data: DepartmentImportRow;
47
+ status: 'valid' | 'warning' | 'error';
48
+ messages: string[];
49
+ resolvedHospitalId?: string;
50
+ }
51
+
52
+ export const purgeDataSchema = z.object({
53
+ scope: z.enum(['test_transactions', 'factory_reset']),
54
+ confirmText: z.string().min(1, 'Confirmation phrase is required'),
55
+ password: z.string().min(1, 'Admin password is required'),
56
+ reason: z.string().optional(),
57
+ });
58
+
59
+ export type PurgeDataPayload = z.infer<typeof purgeDataSchema>;
60
+
61
+ export const exportRequestSchema = z.object({
62
+ entities: z.array(z.enum(['devices', 'departments', 'catalogs', 'tickets', 'audit_logs', 'rounds'])).min(1, 'Select at least one entity'),
63
+ format: z.enum(['csv', 'json', 'xlsx']).default('csv'),
64
+ });
65
+
66
+ export type ExportRequest = z.infer<typeof exportRequestSchema>;
67
+
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod';
2
+
3
+ export const createDepartmentSchema = z.object({
4
+ organizationId: z.string().uuid(),
5
+ hospitalId: z.string().uuid(),
6
+ name: z.string().min(2).max(100),
7
+ code: z.string().min(2).max(20),
8
+ departmentType: z.string().optional().nullable(),
9
+ managerUserId: z.string().uuid().optional().nullable().or(z.literal('')),
10
+ status: z.enum(['active', 'inactive', 'archived']).default('active'),
11
+ });
12
+
13
+ export const updateDepartmentSchema = createDepartmentSchema.partial().omit({ organizationId: true, hospitalId: true });
14
+
@@ -0,0 +1,94 @@
1
+ import { z } from 'zod';
2
+
3
+ export const createDeviceCategorySchema = z.object({
4
+ organizationId: z.string().uuid(),
5
+ name: z.string().min(2).max(100),
6
+ code: z.string().min(1).max(50),
7
+ description: z.string().max(500).optional(),
8
+ riskClassification: z.enum(['class_i', 'class_iia', 'class_iib', 'class_iii']).optional(),
9
+ criticalityLevel: z.enum(['critical', 'high', 'medium', 'low']).optional(),
10
+ defaultPmIntervalDays: z.number().int().positive().optional(),
11
+ });
12
+
13
+ export const updateDeviceCategorySchema = createDeviceCategorySchema.partial().omit({ organizationId: true });
14
+
15
+ export const createManufacturerSchema = z.object({
16
+ organizationId: z.string().uuid(),
17
+ name: z.string().min(2).max(150),
18
+ code: z.string().max(50).optional(),
19
+ country: z.string().max(100).optional(),
20
+ supportEmail: z.string().email().optional().or(z.literal('')),
21
+ supportPhone: z.string().max(30).optional(),
22
+ website: z.string().url().optional().or(z.literal('')),
23
+ });
24
+
25
+ export const updateManufacturerSchema = createManufacturerSchema.partial().omit({ organizationId: true });
26
+
27
+ export const createDeviceSchema = z.object({
28
+ organizationId: z.string().uuid(),
29
+ hospitalId: z.string().uuid(),
30
+ departmentId: z.string().uuid(),
31
+ locationId: z.string().uuid().optional(),
32
+ exactLocationDescription: z.string().max(200).optional(),
33
+ name: z.string().min(2).max(200),
34
+ assetNumber: z.string().min(1).max(50),
35
+ serialNumber: z.string().max(100).optional(),
36
+ deviceCategoryId: z.string().uuid().optional().or(z.literal('')),
37
+ manufacturerId: z.string().uuid().optional(),
38
+ deviceModelId: z.string().uuid().optional(),
39
+ modelNameFree: z.string().max(200).optional(),
40
+ criticalityLevel: z.enum(['critical', 'high', 'medium', 'low']).optional(),
41
+ riskClassification: z.enum(['class_i', 'class_iia', 'class_iib', 'class_iii']).optional(),
42
+ purchaseDate: z.string().optional(),
43
+ purchaseCost: z.string().optional(),
44
+ currency: z.string().max(5).optional(),
45
+ installationDate: z.string().optional(),
46
+ commissioningDate: z.string().optional(),
47
+ warrantyStartDate: z.string().optional(),
48
+ warrantyEndDate: z.string().optional(),
49
+ technicalSpecifications: z.any().optional(),
50
+ assignedEngineerUserId: z.string().uuid().optional(),
51
+ });
52
+
53
+ export const updateDeviceSchema = createDeviceSchema.partial().omit({ organizationId: true });
54
+
55
+ export const transferDeviceSchema = z.object({
56
+ destinationHospitalId: z.string().uuid(),
57
+ destinationDepartmentId: z.string().uuid(),
58
+ destinationLocationId: z.string().uuid().optional(),
59
+ transferReason: z.enum(['relocation', 'temporary_loan', 'department_change', 'correction', 'replacement', 'workshop', 'storage', 'other']),
60
+ notes: z.string().max(500).optional(),
61
+ });
62
+
63
+ export const changeDeviceStatusSchema = z.object({
64
+ newStatusCode: z.enum([
65
+ 'operational', 'operational_with_limitations', 'under_maintenance', 'under_repair',
66
+ 'waiting_for_parts', 'awaiting_release', 'out_of_service', 'standby', 'decommissioned'
67
+ ]),
68
+ reason: z.string().min(5).max(500),
69
+ notes: z.string().max(1000).optional(),
70
+ limitationsNote: z.string().max(500).optional(),
71
+ ticketId: z.string().uuid().optional(),
72
+ maintenanceRecordId: z.string().uuid().optional(),
73
+ });
74
+
75
+ export const decommissionDeviceSchema = z.object({
76
+ reason: z.string().min(10).max(500),
77
+ disposition: z.string().max(200).optional(),
78
+ technicalApprovalNotes: z.string().max(500).optional(),
79
+ });
80
+
81
+ export const deviceFilterSchema = z.object({
82
+ search: z.string().optional(),
83
+ organizationId: z.string().uuid().optional(),
84
+ hospitalId: z.string().uuid().optional(),
85
+ departmentId: z.string().uuid().optional(),
86
+ locationId: z.string().uuid().optional(),
87
+ categoryId: z.string().uuid().optional(),
88
+ manufacturerId: z.string().uuid().optional(),
89
+ status: z.string().optional(),
90
+ criticality: z.string().optional(),
91
+ page: z.number().int().positive().optional(),
92
+ pageSize: z.number().int().positive().max(100).optional(),
93
+ });
94
+
@@ -0,0 +1,70 @@
1
+ import { z } from 'zod';
2
+
3
+ // ── Round Status Values ─────────────────────────────────────────────────────────
4
+
5
+ export const roundStatusValues = ['scheduled', 'in_progress', 'completed', 'missed', 'cancelled'] as const;
6
+ export type RoundStatus = (typeof roundStatusValues)[number];
7
+
8
+ export const roundVisitStatusValues = ['pending', 'completed', 'skipped'] as const;
9
+ export type RoundVisitStatus = (typeof roundVisitStatusValues)[number];
10
+
11
+ export const shiftTypeValues = ['morning', 'evening', 'night'] as const;
12
+ export type ShiftType = (typeof shiftTypeValues)[number];
13
+
14
+ // ── Start Round ─────────────────────────────────────────────────────────────────
15
+
16
+ export const startRoundSchema = z.object({
17
+ hospitalId: z.string().uuid('Invalid hospital ID'),
18
+ shiftType: z.enum(shiftTypeValues).default('morning'),
19
+ password: z.string().min(1, 'Password is required for electronic signature'),
20
+ });
21
+
22
+ // ── Complete Department Visit ────────────────────────────────────────────────────
23
+
24
+ export const completeDepartmentVisitSchema = z.object({
25
+ visitId: z.string().uuid('Invalid visit ID'),
26
+ noteText: z.string().max(2000, 'Note must be under 2000 characters').optional(),
27
+ deviceMentions: z.array(z.object({
28
+ deviceId: z.string().uuid(),
29
+ deviceName: z.string(),
30
+ internalCode: z.string(),
31
+ })).optional().default([]),
32
+ });
33
+
34
+ // ── Add Visit Note ──────────────────────────────────────────────────────────────
35
+
36
+ export const addVisitNoteSchema = z.object({
37
+ visitId: z.string().uuid('Invalid visit ID'),
38
+ noteText: z.string().min(1, 'Note cannot be empty').max(2000, 'Note must be under 2000 characters'),
39
+ deviceMentions: z.array(z.object({
40
+ deviceId: z.string().uuid(),
41
+ deviceName: z.string(),
42
+ internalCode: z.string(),
43
+ })).optional().default([]),
44
+ });
45
+
46
+ // ── Complete Round ──────────────────────────────────────────────────────────────
47
+
48
+ export const completeRoundSchema = z.object({
49
+ roundId: z.string().uuid('Invalid round ID'),
50
+ });
51
+
52
+ // ── Round History Filters ───────────────────────────────────────────────────────
53
+
54
+ export const roundHistoryFilterSchema = z.object({
55
+ hospitalId: z.string().uuid().optional(),
56
+ engineerId: z.string().uuid().optional(),
57
+ status: z.enum(roundStatusValues).optional(),
58
+ dateFrom: z.string().optional(), // YYYY-MM-DD
59
+ dateTo: z.string().optional(), // YYYY-MM-DD
60
+ page: z.coerce.number().int().min(1).default(1),
61
+ pageSize: z.coerce.number().int().min(1).max(100).default(20),
62
+ });
63
+
64
+ // ── Calendar Data ───────────────────────────────────────────────────────────────
65
+
66
+ export const roundCalendarQuerySchema = z.object({
67
+ engineerId: z.string().uuid(),
68
+ month: z.coerce.number().int().min(1).max(12),
69
+ year: z.coerce.number().int().min(2020).max(2100),
70
+ });
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+
3
+ export const createHospitalSchema = z.object({
4
+ organizationId: z.string().uuid('Invalid organization ID'),
5
+ name: z.string().min(2, 'Name must be at least 2 characters').max(100),
6
+ code: z.string().min(2, 'Code must be at least 2 characters').max(20),
7
+ address: z.string().optional().or(z.literal('')),
8
+ city: z.string().optional().or(z.literal('')),
9
+ country: z.string().optional().or(z.literal('')),
10
+ phone: z.string().optional().or(z.literal('')),
11
+ email: z.string().email('Invalid email address').optional().or(z.literal('')),
12
+ timezone: z.string().optional().or(z.literal('')),
13
+ status: z.enum(['active', 'inactive', 'archived']).default('active'),
14
+ });
15
+
16
+ export const updateHospitalSchema = createHospitalSchema.partial().omit({ organizationId: true });
17
+
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+
3
+ export const createLocationSchema = z.object({
4
+ organizationId: z.string().uuid(),
5
+ hospitalId: z.string().uuid(),
6
+ departmentId: z.string().uuid(),
7
+ parentLocationId: z.string().uuid().optional(),
8
+ locationType: z.enum(['building', 'floor', 'room', 'area', 'workshop', 'store', 'other']),
9
+ name: z.string().min(1).max(100),
10
+ code: z.string().optional(),
11
+ status: z.enum(['active', 'inactive', 'archived']).default('active'),
12
+ });
13
+
14
+ export const updateLocationSchema = createLocationSchema.partial().omit({ organizationId: true, hospitalId: true, departmentId: true });
15
+
@@ -0,0 +1,211 @@
1
+ import { z } from 'zod';
2
+
3
+ // ── Maintenance Task Validators ──────────────────────────────────────────────
4
+
5
+ export const createMaintenanceTaskSchema = z.object({
6
+ organizationId: z.string().uuid(),
7
+ hospitalId: z.string().uuid(),
8
+ departmentId: z.string().uuid().optional(),
9
+ locationId: z.string().uuid().optional(),
10
+ deviceId: z.string().uuid(),
11
+ serviceTicketId: z.string().uuid().optional(),
12
+ maintenanceScheduleOccurrenceId: z.string().uuid().optional(),
13
+ maintenanceType: z.enum([
14
+ 'preventive_maintenance', 'corrective_maintenance', 'troubleshooting',
15
+ 'inspection', 'calibration', 'electrical_safety_testing',
16
+ 'performance_testing', 'installation_commissioning',
17
+ 'software_configuration', 'decommissioning'
18
+ ]),
19
+ technicalProblemCategory: z.enum([
20
+ 'electrical_power', 'display_user_interface', 'alarm_safety',
21
+ 'sensor_measurement', 'mechanical', 'software_configuration',
22
+ 'performance_output', 'accessory_consumable', 'physical_damage',
23
+ 'communication_network', 'no_fault_found', 'other'
24
+ ]).optional(),
25
+ title: z.string().min(5).max(200),
26
+ description: z.string().optional(),
27
+ assignedEngineerUserId: z.string().uuid().optional(),
28
+ priorityCode: z.enum(['p1_critical', 'p2_high', 'p3_normal', 'p4_low']).default('p3_normal'),
29
+ dueDate: z.string().datetime().optional(),
30
+ checklistTemplateVersionId: z.string().uuid().optional(),
31
+ });
32
+
33
+ export const assignMaintenanceTaskSchema = z.object({
34
+ assignedEngineerUserId: z.string().uuid(),
35
+ handoffNote: z.string().optional(),
36
+ });
37
+
38
+ export const updateMaintenanceTaskStageSchema = z.object({
39
+ waitingDependencyType: z.enum(['parts', 'requester', 'vendor', 'department_access']).optional(),
40
+ waitingReason: z.string().min(5).optional(),
41
+ resumeNote: z.string().optional(),
42
+ });
43
+
44
+ export const completeMaintenanceTaskSchema = z.object({
45
+ diagnosis: z.string().optional(),
46
+ rootCause: z.string().optional(),
47
+ workPerformed: z.string().min(10),
48
+ findings: z.string().optional(),
49
+ recommendations: z.string().optional(),
50
+ finalResultCode: z.enum([
51
+ 'passed', 'passed_with_limitations', 'failed', 'no_fault_found',
52
+ 'decommission_recommended', 'requires_external_service'
53
+ ]),
54
+ finalDeviceStatusCode: z.enum([
55
+ 'operational', 'operational_with_limitations', 'under_maintenance', 'under_repair',
56
+ 'waiting_for_parts', 'awaiting_release', 'out_of_service', 'standby', 'decommissioned'
57
+ ]),
58
+ });
59
+
60
+ export const cancelMaintenanceTaskSchema = z.object({
61
+ cancellationReason: z.string().min(10),
62
+ });
63
+
64
+ // ── PM Plan Validators ──────────────────────────────────────────────────────
65
+
66
+ export const createMaintenancePlanSchema = z.object({
67
+ organizationId: z.string().uuid(),
68
+ hospitalId: z.string().uuid(),
69
+ departmentId: z.string().uuid().optional(),
70
+ deviceId: z.string().uuid(),
71
+ planTypeCode: z.enum([
72
+ 'preventive_maintenance', 'corrective_maintenance', 'troubleshooting',
73
+ 'inspection', 'calibration', 'electrical_safety_testing',
74
+ 'performance_testing', 'installation_commissioning',
75
+ 'software_configuration', 'decommissioning'
76
+ ]).default('preventive_maintenance'),
77
+ title: z.string().optional(),
78
+ calculationMethod: z.enum(['fixed_calendar', 'completion_based']).default('fixed_calendar'),
79
+ frequencyInterval: z.number().int().min(1),
80
+ frequencyUnit: z.enum(['days', 'weeks', 'months', 'years']),
81
+ startBaselineDate: z.string().datetime(),
82
+ leadTimeDays: z.number().int().min(1).default(30),
83
+ assignedEngineerUserId: z.string().uuid().optional(),
84
+ checklistTemplateVersionId: z.string().uuid().optional(),
85
+ instructions: z.string().optional(),
86
+ });
87
+
88
+ export const updateMaintenancePlanSchema = z.object({
89
+ title: z.string().optional(),
90
+ frequencyInterval: z.number().int().min(1).optional(),
91
+ frequencyUnit: z.enum(['days', 'weeks', 'months', 'years']).optional(),
92
+ leadTimeDays: z.number().int().min(1).optional(),
93
+ assignedEngineerUserId: z.string().uuid().optional(),
94
+ checklistTemplateVersionId: z.string().uuid().optional(),
95
+ instructions: z.string().optional(),
96
+ revisionNote: z.string().min(5),
97
+ });
98
+
99
+ export const deferOccurrenceSchema = z.object({
100
+ newDueDate: z.string().datetime(),
101
+ deferralReason: z.string().min(10),
102
+ });
103
+
104
+ // ── Checklist Validators ────────────────────────────────────────────────────
105
+
106
+ export const checklistItemSchema = z.object({
107
+ id: z.string(),
108
+ label: z.string().min(3),
109
+ description: z.string().optional(),
110
+ isRequired: z.boolean().default(true),
111
+ itemOrder: z.number().int().min(0),
112
+ inputType: z.enum(['pass_fail', 'measurement', 'text', 'options']),
113
+ toleranceMin: z.number().optional(),
114
+ toleranceMax: z.number().optional(),
115
+ unit: z.string().optional(),
116
+ options: z.array(z.string()).optional(),
117
+ });
118
+
119
+ export const createChecklistTemplateSchema = z.object({
120
+ organizationId: z.string().uuid(),
121
+ deviceCategoryId: z.string().uuid().optional(),
122
+ name: z.string().min(3),
123
+ code: z.string().optional(),
124
+ maintenanceType: z.enum([
125
+ 'preventive_maintenance', 'corrective_maintenance', 'troubleshooting',
126
+ 'inspection', 'calibration', 'electrical_safety_testing',
127
+ 'performance_testing', 'installation_commissioning',
128
+ 'software_configuration', 'decommissioning'
129
+ ]).optional(),
130
+ items: z.array(checklistItemSchema).min(1),
131
+ });
132
+
133
+ export const createChecklistVersionSchema = z.object({
134
+ items: z.array(checklistItemSchema).min(1),
135
+ });
136
+
137
+ export const saveChecklistResultSchema = z.object({
138
+ templateItemId: z.string(),
139
+ resultCode: z.enum(['passed', 'failed', 'not_applicable', 'requires_follow_up']).optional(),
140
+ measuredValue: z.string().optional(),
141
+ unit: z.string().optional(),
142
+ notes: z.string().optional(),
143
+ });
144
+
145
+ export const saveChecklistProgressSchema = z.object({
146
+ items: z.array(saveChecklistResultSchema).min(1),
147
+ });
148
+
149
+ // ── Parts & Costs Validators ────────────────────────────────────────────────
150
+
151
+ export const maintenancePartSchema = z.object({
152
+ partNumber: z.string().min(1),
153
+ partName: z.string().min(1),
154
+ quantity: z.number().int().min(1).default(1),
155
+ unitCost: z.number().int().min(0).optional(),
156
+ currency: z.string().default('USD'),
157
+ supplierName: z.string().optional(),
158
+ notes: z.string().optional(),
159
+ });
160
+
161
+ export const maintenanceCostSchema = z.object({
162
+ costType: z.enum(['part', 'vendor_service', 'labor', 'transport', 'other']),
163
+ amount: z.number().int().min(0),
164
+ currency: z.string().default('USD'),
165
+ vendorName: z.string().optional(),
166
+ invoiceNumber: z.string().optional(),
167
+ description: z.string().optional(),
168
+ });
169
+
170
+ // ── Electronic Signature Validators ─────────────────────────────────────────
171
+
172
+ export const electronicSignatureSchema = z.object({
173
+ signaturePurpose: z.enum(['perform', 'review', 'release', 'approve', 'reject', 'amend']),
174
+ authMethod: z.enum(['password_reauth']).default('password_reauth'),
175
+ password: z.string().min(1),
176
+ comments: z.string().optional(),
177
+ });
178
+
179
+ export const reviewWorkSchema = z.object({
180
+ decision: z.enum(['approve', 'reject']),
181
+ reviewNotes: z.string().optional(),
182
+ rejectionReason: z.string().optional(),
183
+ password: z.string().min(1),
184
+ });
185
+
186
+ export const releaseDeviceSchema = z.object({
187
+ finalDeviceStatusCode: z.enum(['operational', 'operational_with_limitations']),
188
+ releaseNotes: z.string().optional(),
189
+ password: z.string().min(1),
190
+ });
191
+
192
+ export const amendRecordSchema = z.object({
193
+ amendmentReason: z.string().min(10),
194
+ corrections: z.record(z.string(), z.unknown()),
195
+ password: z.string().min(1),
196
+ });
197
+
198
+ // ── Task Filter Schema ──────────────────────────────────────────────────────
199
+
200
+ export const maintenanceTaskFilterSchema = z.object({
201
+ search: z.string().optional(),
202
+ statusCode: z.string().optional(),
203
+ maintenanceType: z.string().optional(),
204
+ assignedEngineerUserId: z.string().uuid().optional(),
205
+ hospitalId: z.string().uuid().optional(),
206
+ departmentId: z.string().uuid().optional(),
207
+ deviceId: z.string().uuid().optional(),
208
+ page: z.number().int().min(1).default(1),
209
+ pageSize: z.number().int().min(1).max(100).default(20),
210
+ });
211
+
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+
3
+ export const createOrganizationSchema = z.object({
4
+ name: z.string().min(2).max(100),
5
+ code: z.string().min(2).max(20).toUpperCase(),
6
+ defaultTimezone: z.string().default('UTC'),
7
+ status: z.enum(['active', 'archived']).default('active'),
8
+ });
9
+
10
+ export const updateOrganizationSchema = createOrganizationSchema.partial();
11
+
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+
3
+ export const reportDateRangeSchema = z.object({
4
+ startDate: z.string().datetime().optional(), // ISO date string
5
+ endDate: z.string().datetime().optional(),
6
+ timezone: z.string().default('UTC'),
7
+ });
8
+
9
+ export const reportFiltersSchema = z.object({
10
+ organizationId: z.string().uuid().optional(),
11
+ hospitalId: z.string().uuid().optional(),
12
+ departmentId: z.string().uuid().optional(),
13
+ deviceCategoryId: z.string().uuid().optional(),
14
+ status: z.string().optional(),
15
+ groupBy: z.enum(['hospital', 'department', 'category', 'status', 'manufacturer', 'priority', 'engineer', 'month', 'maintenance_type']).optional(),
16
+ startDate: z.string().datetime().optional(),
17
+ endDate: z.string().datetime().optional(),
18
+ timezone: z.string().default('UTC'),
19
+ page: z.number().int().positive().default(1),
20
+ pageSize: z.number().int().positive().max(100).default(25),
21
+ });
22
+
23
+ export const inventoryReportFiltersSchema = reportFiltersSchema.extend({});
24
+ export const ticketPerformanceFiltersSchema = reportFiltersSchema.extend({});
25
+ export const maintenanceComplianceFiltersSchema = reportFiltersSchema.extend({});
26
+ export const costReportFiltersSchema = reportFiltersSchema.extend({});
27
+ export const workloadReportFiltersSchema = reportFiltersSchema.extend({});
28
+ export const deviceHistoryFiltersSchema = reportFiltersSchema.extend({
29
+ deviceId: z.string().uuid().optional(),
30
+ });
31
+