@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,532 @@
1
+ 'use server';
2
+
3
+ import { db } from '@/lib/db';
4
+ import {
5
+ maintenanceTasks,
6
+ maintenanceRecords,
7
+ maintenanceParts,
8
+ maintenanceCosts,
9
+ maintenanceChecklistResults,
10
+ recordVersions,
11
+ electronicSignatures,
12
+ devices,
13
+ maintenancePlans,
14
+ maintenanceScheduleOccurrences,
15
+ } from '@/lib/db/schema';
16
+ import { requireAuth, requireScope } from '@/lib/auth/rbac';
17
+ import { createAuditLog } from '@/lib/audit';
18
+ import { eq, and } from 'drizzle-orm';
19
+ import { buildCanonicalSnapshot, computeSHA256 } from '@/lib/utils/crypto';
20
+ import { electronicSignatureSchema, reviewWorkSchema, releaseDeviceSchema } from '@/lib/validators/maintenance';
21
+ import { verifySignatureAuth, createSignatureEvent } from './signature-auth';
22
+ import { getAttestationText } from '@/lib/utils/attestations';
23
+ import { z } from 'zod';
24
+ import crypto from 'crypto';
25
+
26
+ function generateRecordNumber() {
27
+ const date = new Date();
28
+ const yearMonth = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}`;
29
+ const randomStr = crypto.randomBytes(3).toString('hex').toUpperCase();
30
+ return `MTR-${yearMonth}-${randomStr}`;
31
+ }
32
+
33
+ // Build the canonical data snapshot for a maintenance task
34
+ async function buildMaintenanceSnapshot(taskId: string, txDb?: any) {
35
+ const dbClient = txDb || db;
36
+ const task = await dbClient.query.maintenanceTasks.findFirst({
37
+ where: eq(maintenanceTasks.id, taskId),
38
+ with: {
39
+ device: true,
40
+ checklistResults: true,
41
+ },
42
+ });
43
+
44
+ if (!task) throw new Error('Task not found');
45
+
46
+ const parts = await dbClient.query.maintenanceParts.findMany({
47
+ where: eq(maintenanceParts.maintenanceTaskId, taskId),
48
+ });
49
+
50
+ const costs = await dbClient.query.maintenanceCosts.findMany({
51
+ where: eq(maintenanceCosts.maintenanceTaskId, taskId),
52
+ });
53
+
54
+ return {
55
+ taskId: task.id,
56
+ taskNumber: task.taskNumber,
57
+ deviceId: task.deviceId,
58
+ deviceSnapshot: task.deviceSnapshot,
59
+ maintenanceType: task.maintenanceType,
60
+ technicalProblemCategory: task.technicalProblemCategory,
61
+ title: task.title,
62
+ description: task.description,
63
+ checklistResults: (task.checklistResults || []).map((r: any) => ({
64
+ templateItemId: r.templateItemId,
65
+ itemLabel: r.itemLabelSnapshot,
66
+ resultCode: r.resultCode,
67
+ measuredValue: r.measuredValue,
68
+ unit: r.unit,
69
+ notes: r.notes,
70
+ })),
71
+ parts: parts.map((p: any) => ({
72
+ partNumber: p.partNumber,
73
+ partName: p.partName,
74
+ quantity: p.quantity,
75
+ unitCost: p.unitCost,
76
+ })),
77
+ costs: costs.map((c: any) => ({
78
+ costType: c.costType,
79
+ amount: c.amount,
80
+ description: c.description,
81
+ })),
82
+ startedAt: task.startedAt?.toISOString(),
83
+ completedAt: task.completedAt?.toISOString(),
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Performer signs the completed maintenance work.
89
+ * Creates a maintenance record, record version with SHA-256 hash, and electronic signature.
90
+ * Transitions task to awaiting_review.
91
+ *
92
+ * SECURITY: Verifies password re-authentication before signing.
93
+ * INTEGRITY: Wraps all operations in a database transaction.
94
+ * TRACEABILITY: Creates signature_events with hash chain.
95
+ */
96
+ export async function signMaintenanceRecord(
97
+ taskId: string,
98
+ completionData: {
99
+ diagnosis?: string;
100
+ rootCause?: string;
101
+ workPerformed: string;
102
+ findings?: string;
103
+ recommendations?: string;
104
+ finalResultCode: string;
105
+ finalDeviceStatusCode: string;
106
+ },
107
+ signatureInput: z.infer<typeof electronicSignatureSchema>
108
+ ) {
109
+ const user = await requireAuth();
110
+
111
+ try {
112
+ const sigData = electronicSignatureSchema.parse(signatureInput);
113
+
114
+ // ── 0. Re-authenticate the signer ──
115
+ const authResult = await verifySignatureAuth(user.id, sigData.password);
116
+ if (!authResult.success) {
117
+ return { success: false, error: authResult.error };
118
+ }
119
+
120
+ const task = await db.query.maintenanceTasks.findFirst({
121
+ where: eq(maintenanceTasks.id, taskId),
122
+ });
123
+ if (!task) return { success: false, error: 'Task not found' };
124
+ await requireScope({ organizationId: task.organizationId, hospitalId: task.hospitalId });
125
+
126
+ if (task.statusCode !== 'work_complete') {
127
+ return { success: false, error: `Task must be in work_complete state, currently: ${task.statusCode}` };
128
+ }
129
+
130
+ // ── Execute all DB writes in a single transaction ──
131
+ const result = await db.transaction(async (tx) => {
132
+ // 1. Create the maintenance record
133
+ const [record] = await tx.insert(maintenanceRecords).values({
134
+ recordNumber: generateRecordNumber(),
135
+ organizationId: task.organizationId,
136
+ deviceId: task.deviceId,
137
+ maintenanceTaskId: taskId,
138
+ serviceTicketId: task.serviceTicketId,
139
+ maintenanceType: task.maintenanceType,
140
+ technicalCategory: task.technicalProblemCategory,
141
+ diagnosis: completionData.diagnosis,
142
+ rootCause: completionData.rootCause,
143
+ workPerformed: completionData.workPerformed,
144
+ findings: completionData.findings,
145
+ recommendations: completionData.recommendations,
146
+ finalResultCode: completionData.finalResultCode as any,
147
+ finalDeviceStatusCode: completionData.finalDeviceStatusCode as any,
148
+ startedAt: task.startedAt || new Date(),
149
+ completedAt: task.completedAt || new Date(),
150
+ createdByUserId: user.id,
151
+ }).returning();
152
+
153
+ // 2. Build canonical snapshot and hash
154
+ const snapshotData = await buildMaintenanceSnapshot(taskId);
155
+ const fullSnapshot = {
156
+ ...snapshotData,
157
+ record: {
158
+ diagnosis: completionData.diagnosis,
159
+ rootCause: completionData.rootCause,
160
+ workPerformed: completionData.workPerformed,
161
+ findings: completionData.findings,
162
+ recommendations: completionData.recommendations,
163
+ finalResultCode: completionData.finalResultCode,
164
+ finalDeviceStatusCode: completionData.finalDeviceStatusCode,
165
+ },
166
+ };
167
+ const canonicalJson = buildCanonicalSnapshot(fullSnapshot as any);
168
+ const contentHash = computeSHA256(canonicalJson);
169
+
170
+ // 3. Create record version
171
+ const [version] = await tx.insert(recordVersions).values({
172
+ organizationId: task.organizationId,
173
+ entityType: 'maintenance_record',
174
+ entityId: record.id,
175
+ versionNumber: 1,
176
+ canonicalSnapshotJsonb: fullSnapshot,
177
+ contentHashSha256: contentHash,
178
+ createdByUserId: user.id,
179
+ }).returning();
180
+
181
+ // 4. Create electronic signature
182
+ const [signature] = await tx.insert(electronicSignatures).values({
183
+ organizationId: task.organizationId,
184
+ recordVersionId: version.id,
185
+ entityType: 'maintenance_record',
186
+ entityId: record.id,
187
+ signaturePurpose: 'perform',
188
+ signerUserId: user.id,
189
+ signerNameSnapshot: user.fullName || user.email || 'Unknown',
190
+ signerRoleSnapshot: user.roles?.join(', ') || 'Engineer',
191
+ attestationTextVersion: getAttestationText('perform'),
192
+ authMethod: 'password_reauth',
193
+ signedAt: new Date(),
194
+ signedContentHashSha256: contentHash,
195
+ comments: sigData.comments,
196
+ }).returning();
197
+
198
+ // 5. Create signature event (hash chain)
199
+ await createSignatureEvent(tx, {
200
+ signatureId: signature.id,
201
+ eventType: 'created',
202
+ actorUserId: user.id,
203
+ organizationId: task.organizationId,
204
+ newStatus: 'active',
205
+ });
206
+
207
+ // 6. Update task: link record, transition to awaiting_review
208
+ await tx.update(maintenanceTasks).set({
209
+ statusCode: 'awaiting_review',
210
+ resultRecordId: record.id,
211
+ updatedAt: new Date(),
212
+ updatedByUserId: user.id,
213
+ }).where(eq(maintenanceTasks.id, taskId));
214
+
215
+ // 7. Audit log
216
+ await createAuditLog(tx, {
217
+ actionType: 'SIGN',
218
+ entityType: 'MAINTENANCE_RECORD',
219
+ entityId: record.id,
220
+ organizationId: task.organizationId,
221
+ actorUserId: user.id,
222
+ newState: { purpose: 'perform', contentHash, signatureId: signature.id },
223
+ });
224
+
225
+ return { record, version, signature };
226
+ });
227
+
228
+ return { success: true, data: { record: result.record, version: result.version } };
229
+ } catch (error: any) {
230
+ return { success: false, error: error.message };
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Reviewer approves or rejects the maintenance work.
236
+ * Enforces Performer ≠ Reviewer.
237
+ *
238
+ * SECURITY: Verifies password re-authentication before signing.
239
+ * INTEGRITY: Wraps all operations in a database transaction.
240
+ * TRACEABILITY: Creates signature_events with hash chain.
241
+ */
242
+ export async function reviewMaintenanceRecord(
243
+ taskId: string,
244
+ input: z.infer<typeof reviewWorkSchema>
245
+ ) {
246
+ const user = await requireAuth();
247
+
248
+ try {
249
+ const data = reviewWorkSchema.parse(input);
250
+
251
+ // ── 0. Re-authenticate the reviewer ──
252
+ const authResult = await verifySignatureAuth(user.id, data.password);
253
+ if (!authResult.success) {
254
+ return { success: false, error: authResult.error };
255
+ }
256
+
257
+ const task = await db.query.maintenanceTasks.findFirst({
258
+ where: eq(maintenanceTasks.id, taskId),
259
+ });
260
+ if (!task) return { success: false, error: 'Task not found' };
261
+ await requireScope({ organizationId: task.organizationId, hospitalId: task.hospitalId });
262
+
263
+ if (task.statusCode !== 'awaiting_review') {
264
+ return { success: false, error: `Task must be in awaiting_review state` };
265
+ }
266
+
267
+ // Performer ≠ Reviewer enforcement
268
+ const performerSig = await db.query.electronicSignatures.findFirst({
269
+ where: and(
270
+ eq(electronicSignatures.entityId, task.resultRecordId!),
271
+ eq(electronicSignatures.signaturePurpose, 'perform'),
272
+ eq(electronicSignatures.signatureStatus, 'active'),
273
+ ),
274
+ });
275
+
276
+ if (performerSig && performerSig.signerUserId === user.id) {
277
+ return { success: false, error: 'Reviewer cannot be the same person as the performer' };
278
+ }
279
+
280
+ // Get the latest record version to sign against
281
+ const latestVersion = await db.query.recordVersions.findFirst({
282
+ where: and(
283
+ eq(recordVersions.entityId, task.resultRecordId!),
284
+ eq(recordVersions.entityType, 'maintenance_record')
285
+ ),
286
+ });
287
+ if (!latestVersion) return { success: false, error: 'Record version not found' };
288
+
289
+ // ── Execute in transaction ──
290
+ const result = await db.transaction(async (tx) => {
291
+ if (data.decision === 'approve') {
292
+ // Create reviewer signature
293
+ const [signature] = await tx.insert(electronicSignatures).values({
294
+ organizationId: task.organizationId,
295
+ recordVersionId: latestVersion.id,
296
+ entityType: 'maintenance_record',
297
+ entityId: task.resultRecordId!,
298
+ signaturePurpose: 'review',
299
+ signerUserId: user.id,
300
+ signerNameSnapshot: user.fullName || user.email || 'Unknown',
301
+ signerRoleSnapshot: user.roles?.join(', ') || 'Reviewer',
302
+ attestationTextVersion: getAttestationText('review'),
303
+ authMethod: 'password_reauth',
304
+ signedAt: new Date(),
305
+ signedContentHashSha256: latestVersion.contentHashSha256,
306
+ comments: data.reviewNotes,
307
+ }).returning();
308
+
309
+ // Create signature event
310
+ await createSignatureEvent(tx, {
311
+ signatureId: signature.id,
312
+ eventType: 'approved',
313
+ actorUserId: user.id,
314
+ organizationId: task.organizationId,
315
+ newStatus: 'active',
316
+ });
317
+
318
+ // Transition to awaiting_release
319
+ await tx.update(maintenanceTasks).set({
320
+ statusCode: 'awaiting_release',
321
+ updatedAt: new Date(),
322
+ updatedByUserId: user.id,
323
+ }).where(eq(maintenanceTasks.id, taskId));
324
+
325
+ await createAuditLog(tx, {
326
+ actionType: 'SIGN',
327
+ entityType: 'MAINTENANCE_RECORD',
328
+ entityId: task.resultRecordId!,
329
+ organizationId: task.organizationId,
330
+ actorUserId: user.id,
331
+ newState: { purpose: 'review', decision: 'approve', signatureId: signature.id },
332
+ });
333
+
334
+ return { decision: 'approve' as const, signatureId: signature.id };
335
+ } else {
336
+ // Reject -> returned_for_rework
337
+ await tx.update(maintenanceTasks).set({
338
+ statusCode: 'returned_for_rework',
339
+ updatedAt: new Date(),
340
+ updatedByUserId: user.id,
341
+ }).where(eq(maintenanceTasks.id, taskId));
342
+
343
+ await createAuditLog(tx, {
344
+ actionType: 'UPDATE',
345
+ entityType: 'MAINTENANCE_RECORD',
346
+ entityId: task.resultRecordId!,
347
+ organizationId: task.organizationId,
348
+ actorUserId: user.id,
349
+ newState: { purpose: 'review', decision: 'reject', reason: data.rejectionReason },
350
+ });
351
+
352
+ return { decision: 'reject' as const };
353
+ }
354
+ });
355
+
356
+ return { success: true, data: result };
357
+ } catch (error: any) {
358
+ return { success: false, error: error.message };
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Final release: authorizes device return to clinical use.
364
+ * Updates device status, next PM due date, and finalizes record.
365
+ *
366
+ * SECURITY: Verifies password re-authentication before signing.
367
+ * INTEGRITY: Wraps all operations in a database transaction.
368
+ * TRACEABILITY: Creates signature_events with hash chain.
369
+ */
370
+ export async function releaseDeviceMaintenance(
371
+ taskId: string,
372
+ input: z.infer<typeof releaseDeviceSchema>
373
+ ) {
374
+ const user = await requireAuth();
375
+
376
+ try {
377
+ const data = releaseDeviceSchema.parse(input);
378
+
379
+ // ── 0. Re-authenticate the release authority ──
380
+ const authResult = await verifySignatureAuth(user.id, data.password);
381
+ if (!authResult.success) {
382
+ return { success: false, error: authResult.error };
383
+ }
384
+
385
+ const task = await db.query.maintenanceTasks.findFirst({
386
+ where: eq(maintenanceTasks.id, taskId),
387
+ });
388
+ if (!task) return { success: false, error: 'Task not found' };
389
+ await requireScope({ organizationId: task.organizationId, hospitalId: task.hospitalId });
390
+
391
+ if (task.statusCode !== 'awaiting_release') {
392
+ return { success: false, error: `Task must be in awaiting_release state` };
393
+ }
394
+
395
+ // Get the latest record version
396
+ const latestVersion = await db.query.recordVersions.findFirst({
397
+ where: and(
398
+ eq(recordVersions.entityId, task.resultRecordId!),
399
+ eq(recordVersions.entityType, 'maintenance_record')
400
+ ),
401
+ });
402
+ if (!latestVersion) return { success: false, error: 'Record version not found' };
403
+
404
+ // ── Execute in transaction ──
405
+ await db.transaction(async (tx) => {
406
+ // Create release signature
407
+ const [signature] = await tx.insert(electronicSignatures).values({
408
+ organizationId: task.organizationId,
409
+ recordVersionId: latestVersion.id,
410
+ entityType: 'maintenance_record',
411
+ entityId: task.resultRecordId!,
412
+ signaturePurpose: 'release',
413
+ signerUserId: user.id,
414
+ signerNameSnapshot: user.fullName || user.email || 'Unknown',
415
+ signerRoleSnapshot: user.roles?.join(', ') || 'Release Authority',
416
+ attestationTextVersion: getAttestationText('release'),
417
+ authMethod: 'password_reauth',
418
+ signedAt: new Date(),
419
+ signedContentHashSha256: latestVersion.contentHashSha256,
420
+ comments: data.releaseNotes,
421
+ }).returning();
422
+
423
+ // Create signature event
424
+ await createSignatureEvent(tx, {
425
+ signatureId: signature.id,
426
+ eventType: 'approved',
427
+ actorUserId: user.id,
428
+ organizationId: task.organizationId,
429
+ newStatus: 'active',
430
+ });
431
+
432
+ // Finalize the maintenance record
433
+ await tx.update(maintenanceRecords).set({
434
+ isFinalized: true,
435
+ finalDeviceStatusCode: data.finalDeviceStatusCode as any,
436
+ updatedAt: new Date(),
437
+ }).where(eq(maintenanceRecords.id, task.resultRecordId!));
438
+
439
+ // Close the task
440
+ await tx.update(maintenanceTasks).set({
441
+ statusCode: 'closed',
442
+ updatedAt: new Date(),
443
+ updatedByUserId: user.id,
444
+ }).where(eq(maintenanceTasks.id, taskId));
445
+
446
+ // Update the device status
447
+ await tx.update(devices).set({
448
+ currentStatusCode: data.finalDeviceStatusCode as any,
449
+ lastMaintenanceCompletedAt: new Date(),
450
+ updatedAt: new Date(),
451
+ }).where(eq(devices.id, task.deviceId));
452
+
453
+ // If this task is linked to a PM occurrence, update the plan's next due date
454
+ if (task.maintenanceScheduleOccurrenceId) {
455
+ const occurrence = await db.query.maintenanceScheduleOccurrences.findFirst({
456
+ where: eq(maintenanceScheduleOccurrences.id, task.maintenanceScheduleOccurrenceId),
457
+ with: { maintenancePlan: true },
458
+ });
459
+
460
+ if (occurrence) {
461
+ await tx.update(maintenanceScheduleOccurrences).set({
462
+ dueState: 'completed',
463
+ qualifyingRecordId: task.resultRecordId,
464
+ }).where(eq(maintenanceScheduleOccurrences.id, occurrence.id));
465
+
466
+ if (occurrence.maintenancePlan && occurrence.maintenancePlan.calculationMethod === 'completion_based') {
467
+ const plan = occurrence.maintenancePlan;
468
+ const nextDate = new Date();
469
+ if (plan.frequencyUnit === 'days') nextDate.setDate(nextDate.getDate() + plan.frequencyInterval);
470
+ else if (plan.frequencyUnit === 'weeks') nextDate.setDate(nextDate.getDate() + (plan.frequencyInterval * 7));
471
+ else if (plan.frequencyUnit === 'months') nextDate.setMonth(nextDate.getMonth() + plan.frequencyInterval);
472
+ else if (plan.frequencyUnit === 'years') nextDate.setFullYear(nextDate.getFullYear() + plan.frequencyInterval);
473
+
474
+ await tx.update(maintenancePlans).set({
475
+ nextDueDate: nextDate,
476
+ lastQualifyingRecordId: task.resultRecordId,
477
+ updatedAt: new Date(),
478
+ }).where(eq(maintenancePlans.id, plan.id));
479
+ }
480
+ }
481
+ }
482
+
483
+ // Audit log
484
+ await createAuditLog(tx, {
485
+ actionType: 'SIGN',
486
+ entityType: 'MAINTENANCE_RECORD',
487
+ entityId: task.resultRecordId!,
488
+ organizationId: task.organizationId,
489
+ actorUserId: user.id,
490
+ newState: { purpose: 'release', finalDeviceStatus: data.finalDeviceStatusCode, signatureId: signature.id },
491
+ });
492
+ });
493
+
494
+ return { success: true };
495
+ } catch (error: any) {
496
+ return { success: false, error: error.message };
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Get all signatures for a maintenance record.
502
+ */
503
+ export async function getRecordSignatures(recordId: string) {
504
+ const user = await requireAuth();
505
+
506
+ try {
507
+ const record = await db.query.maintenanceRecords.findFirst({
508
+ where: eq(maintenanceRecords.id, recordId),
509
+ });
510
+ if (!record) return { success: false, error: 'Record not found' };
511
+ await requireScope({ organizationId: record.organizationId });
512
+
513
+ const versions = await db.query.recordVersions.findMany({
514
+ where: and(
515
+ eq(recordVersions.entityId, recordId),
516
+ eq(recordVersions.entityType, 'maintenance_record')
517
+ ),
518
+ });
519
+
520
+ const signatures = await db.query.electronicSignatures.findMany({
521
+ where: and(
522
+ eq(electronicSignatures.entityId, recordId),
523
+ eq(electronicSignatures.entityType, 'maintenance_record')
524
+ ),
525
+ });
526
+
527
+ return { success: true, data: { versions, signatures } };
528
+ } catch (error: any) {
529
+ return { success: false, error: error.message };
530
+ }
531
+ }
532
+