@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,377 @@
1
+ 'use server';
2
+
3
+ import { db } from '@/lib/db';
4
+ import {
5
+ electronicSignatures,
6
+ signatureEvents,
7
+ signaturePolicies,
8
+ recordVersions,
9
+ users,
10
+ } from '@/lib/db/schema';
11
+ import { verifyPassword } from '@/lib/auth/password';
12
+ import { requireAuth } from '@/lib/auth/rbac';
13
+ import { computeSHA256 } from '@/lib/utils/crypto';
14
+ import { eq, and, isNull, desc } from 'drizzle-orm';
15
+ import type { SignaturePolicyRequirement } from '@/lib/db/schema/signature-policies';
16
+
17
+ // ── Types ────────────────────────────────────────────────────────────────────
18
+
19
+ export interface SignatureRequirement {
20
+ purpose: string;
21
+ allowedRoles: string[];
22
+ required: boolean;
23
+ }
24
+
25
+ export interface CompletedSignature {
26
+ id: string;
27
+ purpose: string;
28
+ signerName: string;
29
+ signerRole: string;
30
+ signedAt: Date;
31
+ status: string;
32
+ contentHash: string;
33
+ }
34
+
35
+ export interface SignatureStatusResult {
36
+ required: SignatureRequirement[];
37
+ completed: CompletedSignature[];
38
+ pending: SignatureRequirement[];
39
+ canSign: { purpose: string; allowed: boolean; reason?: string }[];
40
+ policyId: string | null;
41
+ requireIndependentReview: boolean;
42
+ performerCanRelease: boolean;
43
+ }
44
+
45
+ // ── Password Re-Authentication ──────────────────────────────────────────────
46
+
47
+ /**
48
+ * Verify the user's password before allowing a signature.
49
+ * Never stores the password — only records that re-authentication succeeded.
50
+ */
51
+ export async function verifySignatureAuth(
52
+ userId: string,
53
+ password: string
54
+ ): Promise<{ success: true } | { success: false; error: string }> {
55
+ if (!password || password.length === 0) {
56
+ return { success: false, error: 'Password is required for signature re-authentication' };
57
+ }
58
+
59
+ const user = await db.query.users.findFirst({
60
+ where: eq(users.id, userId),
61
+ columns: { id: true, passwordHash: true, accountStatus: true },
62
+ });
63
+
64
+ if (!user) {
65
+ return { success: false, error: 'User not found' };
66
+ }
67
+
68
+ if (user.accountStatus !== 'active') {
69
+ return { success: false, error: 'Account is not active' };
70
+ }
71
+
72
+ if (!user.passwordHash) {
73
+ return { success: false, error: 'No password set. Please set a password before signing.' };
74
+ }
75
+
76
+ const isValid = await verifyPassword(password, user.passwordHash);
77
+ if (!isValid) {
78
+ return { success: false, error: 'Incorrect password. Signature authentication failed.' };
79
+ }
80
+
81
+ return { success: true };
82
+ }
83
+
84
+ // ── Signature Event Hash Chain ──────────────────────────────────────────────
85
+
86
+ /**
87
+ * Create an append-only signature event with hash chaining.
88
+ * Each event's hash includes the previous event's hash, forming a tamper-detectable chain.
89
+ */
90
+ export async function createSignatureEvent(
91
+ txOrDb: any,
92
+ params: {
93
+ signatureId: string;
94
+ eventType: 'created' | 'reviewed' | 'approved' | 'rejected' | 'amended' | 'superseded' | 'voided';
95
+ actorUserId: string;
96
+ organizationId: string;
97
+ previousStatus?: string;
98
+ newStatus?: string;
99
+ reason?: string;
100
+ }
101
+ ): Promise<void> {
102
+ const dbClient = txOrDb || db;
103
+
104
+ // Get the last event for this signature to chain hashes
105
+ const lastEvent = await dbClient
106
+ .select({ currentEventHash: signatureEvents.currentEventHash })
107
+ .from(signatureEvents)
108
+ .where(eq(signatureEvents.signatureId, params.signatureId))
109
+ .orderBy(desc(signatureEvents.timestamp))
110
+ .limit(1);
111
+
112
+ const previousHash = lastEvent.length > 0 ? lastEvent[0].currentEventHash : null;
113
+
114
+ // Build the hash payload: eventType + actorUserId + timestamp + previousHash
115
+ const now = new Date();
116
+ const hashPayload = JSON.stringify({
117
+ signatureId: params.signatureId,
118
+ eventType: params.eventType,
119
+ actorUserId: params.actorUserId,
120
+ timestamp: now.toISOString(),
121
+ previousEventHash: previousHash,
122
+ });
123
+ const currentHash = computeSHA256(hashPayload);
124
+
125
+ await dbClient.insert(signatureEvents).values({
126
+ organizationId: params.organizationId,
127
+ signatureId: params.signatureId,
128
+ eventType: params.eventType,
129
+ actorUserId: params.actorUserId,
130
+ timestamp: now,
131
+ previousStatus: params.previousStatus as any,
132
+ newStatus: params.newStatus as any,
133
+ reason: params.reason,
134
+ previousEventHash: previousHash,
135
+ currentEventHash: currentHash,
136
+ });
137
+ }
138
+
139
+ // ── Policy Resolution ───────────────────────────────────────────────────────
140
+
141
+ /**
142
+ * Get the most specific applicable signature policy for a given context.
143
+ * Resolution order (most specific wins):
144
+ * 1. org + action + criticality + category
145
+ * 2. org + action + criticality (no category)
146
+ * 3. org + action (no criticality, no category) — default fallback
147
+ */
148
+ export async function getApplicablePolicy(params: {
149
+ organizationId: string;
150
+ actionType: string;
151
+ criticalityLevel?: string | null;
152
+ deviceCategoryId?: string | null;
153
+ }): Promise<{
154
+ policy: typeof signaturePolicies.$inferSelect | null;
155
+ requirements: SignaturePolicyRequirement[];
156
+ }> {
157
+ // Try most specific first: org + action + criticality + category
158
+ if (params.criticalityLevel && params.deviceCategoryId) {
159
+ const specific = await db.query.signaturePolicies.findFirst({
160
+ where: and(
161
+ eq(signaturePolicies.organizationId, params.organizationId),
162
+ eq(signaturePolicies.actionType, params.actionType as any),
163
+ eq(signaturePolicies.criticalityLevel, params.criticalityLevel as any),
164
+ eq(signaturePolicies.deviceCategoryId, params.deviceCategoryId),
165
+ eq(signaturePolicies.isActive, true),
166
+ ),
167
+ });
168
+ if (specific) {
169
+ return { policy: specific, requirements: specific.requiredSignatures as SignaturePolicyRequirement[] };
170
+ }
171
+ }
172
+
173
+ // Try: org + action + criticality (no category)
174
+ if (params.criticalityLevel) {
175
+ const byCriticality = await db.query.signaturePolicies.findFirst({
176
+ where: and(
177
+ eq(signaturePolicies.organizationId, params.organizationId),
178
+ eq(signaturePolicies.actionType, params.actionType as any),
179
+ eq(signaturePolicies.criticalityLevel, params.criticalityLevel as any),
180
+ isNull(signaturePolicies.deviceCategoryId),
181
+ eq(signaturePolicies.isActive, true),
182
+ ),
183
+ });
184
+ if (byCriticality) {
185
+ return { policy: byCriticality, requirements: byCriticality.requiredSignatures as SignaturePolicyRequirement[] };
186
+ }
187
+ }
188
+
189
+ // Fallback: org + action (no criticality, no category)
190
+ const defaultPolicy = await db.query.signaturePolicies.findFirst({
191
+ where: and(
192
+ eq(signaturePolicies.organizationId, params.organizationId),
193
+ eq(signaturePolicies.actionType, params.actionType as any),
194
+ isNull(signaturePolicies.criticalityLevel),
195
+ isNull(signaturePolicies.deviceCategoryId),
196
+ eq(signaturePolicies.isActive, true),
197
+ ),
198
+ });
199
+
200
+ if (defaultPolicy) {
201
+ return { policy: defaultPolicy, requirements: defaultPolicy.requiredSignatures as SignaturePolicyRequirement[] };
202
+ }
203
+
204
+ // No policy found — return a safe default (performer signature required)
205
+ return {
206
+ policy: null,
207
+ requirements: [
208
+ { purpose: 'perform', allowedRoles: ['BME_TECHNICIAN', 'BME_ENGINEER', 'BME_MANAGER'], required: true },
209
+ ],
210
+ };
211
+ }
212
+
213
+ // ── Signature Status for a Record ───────────────────────────────────────────
214
+
215
+ /**
216
+ * Get required & completed signatures status for an entity (maintenance record, etc.).
217
+ * Returns what's required, what's done, what's pending, and what the current user can sign.
218
+ */
219
+ export async function getSignatureStatus(
220
+ entityType: string,
221
+ entityId: string,
222
+ actionType?: string,
223
+ ): Promise<{ success: true; data: SignatureStatusResult } | { success: false; error: string }> {
224
+ const user = await requireAuth();
225
+
226
+ try {
227
+ // Get all active signatures for this entity
228
+ const sigs = await db.query.electronicSignatures.findMany({
229
+ where: and(
230
+ eq(electronicSignatures.entityId, entityId),
231
+ eq(electronicSignatures.entityType, entityType as any),
232
+ ),
233
+ });
234
+
235
+ const activeSigs = sigs.filter(s => s.signatureStatus === 'active');
236
+ const completed: CompletedSignature[] = activeSigs.map(s => ({
237
+ id: s.id,
238
+ purpose: s.signaturePurpose,
239
+ signerName: s.signerNameSnapshot,
240
+ signerRole: s.signerRoleSnapshot,
241
+ signedAt: s.signedAt,
242
+ status: s.signatureStatus,
243
+ contentHash: s.signedContentHashSha256,
244
+ }));
245
+
246
+ // Determine required signatures from policy
247
+ let requirements: SignaturePolicyRequirement[] = [
248
+ { purpose: 'perform', allowedRoles: ['BME_TECHNICIAN', 'BME_ENGINEER'], required: true },
249
+ ];
250
+ let policyId: string | null = null;
251
+ let requireIndependentReview = false;
252
+ let performerCanRelease = true;
253
+
254
+ if (actionType) {
255
+ // Look up the entity to find device criticality for policy resolution
256
+ const { policy, requirements: policyReqs } = await getApplicablePolicy({
257
+ organizationId: user.organizationId!,
258
+ actionType,
259
+ });
260
+ if (policy) {
261
+ requirements = policyReqs;
262
+ policyId = policy.id;
263
+ requireIndependentReview = policy.requireIndependentReview;
264
+ performerCanRelease = policy.performerCanRelease;
265
+ }
266
+ }
267
+
268
+ // Calculate pending: required but not yet completed
269
+ const completedPurposes = new Set(completed.map(c => c.purpose));
270
+ const pending = requirements.filter(r => r.required && !completedPurposes.has(r.purpose));
271
+
272
+ // Calculate what the current user can sign
273
+ const userRoles = user.roles || [];
274
+ const performerSig = activeSigs.find(s => s.signaturePurpose === 'perform');
275
+
276
+ const canSign = requirements.map(req => {
277
+ // Already signed?
278
+ if (completedPurposes.has(req.purpose)) {
279
+ return { purpose: req.purpose, allowed: false, reason: 'Already signed' };
280
+ }
281
+
282
+ // Check role
283
+ const hasRole = req.allowedRoles.some(r => userRoles.includes(r));
284
+ if (!hasRole) {
285
+ return { purpose: req.purpose, allowed: false, reason: 'Insufficient role' };
286
+ }
287
+
288
+ // Independent review check
289
+ if (requireIndependentReview && req.purpose === 'review' && performerSig) {
290
+ if (performerSig.signerUserId === user.id) {
291
+ return { purpose: req.purpose, allowed: false, reason: 'Reviewer cannot be the same person as the performer' };
292
+ }
293
+ }
294
+
295
+ // Release check
296
+ if (!performerCanRelease && req.purpose === 'release' && performerSig) {
297
+ if (performerSig.signerUserId === user.id) {
298
+ return { purpose: req.purpose, allowed: false, reason: 'Performer cannot release this device' };
299
+ }
300
+ }
301
+
302
+ // Check prior signatures are complete (e.g., can't review if not yet performed)
303
+ const purposeOrder = ['perform', 'review', 'approve', 'release'];
304
+ const thisIdx = purposeOrder.indexOf(req.purpose);
305
+ if (thisIdx > 0) {
306
+ const priorPurposes = purposeOrder.slice(0, thisIdx);
307
+ for (const prior of priorPurposes) {
308
+ const priorReq = requirements.find(r => r.purpose === prior && r.required);
309
+ if (priorReq && !completedPurposes.has(prior)) {
310
+ return { purpose: req.purpose, allowed: false, reason: `Waiting for ${prior} signature` };
311
+ }
312
+ }
313
+ }
314
+
315
+ return { purpose: req.purpose, allowed: true };
316
+ });
317
+
318
+ return {
319
+ success: true,
320
+ data: {
321
+ required: requirements,
322
+ completed,
323
+ pending,
324
+ canSign,
325
+ policyId,
326
+ requireIndependentReview,
327
+ performerCanRelease,
328
+ },
329
+ };
330
+ } catch (error: any) {
331
+ return { success: false, error: error.message };
332
+ }
333
+ }
334
+
335
+ // ── Get Signature Events for a Record ───────────────────────────────────────
336
+
337
+ /**
338
+ * Get all signature events (hash-chained audit trail) for signatures on an entity.
339
+ */
340
+ export async function getSignatureEvents(
341
+ entityType: string,
342
+ entityId: string,
343
+ ): Promise<{ success: true; data: (typeof signatureEvents.$inferSelect)[] } | { success: false; error: string }> {
344
+ await requireAuth();
345
+
346
+ try {
347
+ // First get all signature IDs for this entity
348
+ const sigs = await db.query.electronicSignatures.findMany({
349
+ where: and(
350
+ eq(electronicSignatures.entityId, entityId),
351
+ eq(electronicSignatures.entityType, entityType as any),
352
+ ),
353
+ columns: { id: true },
354
+ });
355
+
356
+ if (sigs.length === 0) {
357
+ return { success: true, data: [] };
358
+ }
359
+
360
+ // Get all events for these signatures
361
+ const allEvents: (typeof signatureEvents.$inferSelect)[] = [];
362
+ for (const sig of sigs) {
363
+ const events = await db.query.signatureEvents.findMany({
364
+ where: eq(signatureEvents.signatureId, sig.id),
365
+ });
366
+ allEvents.push(...events);
367
+ }
368
+
369
+ // Sort chronologically
370
+ allEvents.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
371
+
372
+ return { success: true, data: allEvents };
373
+ } catch (error: any) {
374
+ return { success: false, error: error.message };
375
+ }
376
+ }
377
+