@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,166 @@
1
+ import { db } from '@/lib/db';
2
+ import { serviceTickets, maintenanceTasks, devices, users } from '@/lib/db/schema';
3
+ import { eq } from 'drizzle-orm';
4
+ import { createNotification } from './create';
5
+
6
+ /**
7
+ * Trigger notification when a ticket is assigned to an engineer.
8
+ */
9
+ export async function notifyTicketAssigned(ticketId: string, engineerUserId: string) {
10
+ const ticket = await db.query.serviceTickets.findFirst({
11
+ where: eq(serviceTickets.id, ticketId),
12
+ with: { device: { columns: { name: true } } },
13
+ });
14
+ if (!ticket) return;
15
+
16
+ await createNotification({
17
+ organizationId: ticket.organizationId,
18
+ recipientUserId: engineerUserId,
19
+ notificationType: 'ticket_assigned',
20
+ title: `Ticket ${ticket.ticketNumber} assigned to you`,
21
+ body: `${ticket.title}${ticket.device ? ` — ${ticket.device.name}` : ''}`,
22
+ relatedEntityType: 'ticket',
23
+ relatedEntityId: ticketId,
24
+ actionUrl: `/tickets/${ticketId}`,
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Trigger notification when a ticket status changes (notify reporter).
30
+ */
31
+ export async function notifyTicketStatusChanged(ticketId: string, newStatus: string) {
32
+ const ticket = await db.query.serviceTickets.findFirst({
33
+ where: eq(serviceTickets.id, ticketId),
34
+ });
35
+ if (!ticket || !ticket.reportedByUserId) return;
36
+
37
+ const statusLabel = newStatus.replace(/_/g, ' ');
38
+
39
+ await createNotification({
40
+ organizationId: ticket.organizationId,
41
+ recipientUserId: ticket.reportedByUserId,
42
+ notificationType: 'ticket_updated',
43
+ title: `Ticket ${ticket.ticketNumber} is now ${statusLabel}`,
44
+ body: ticket.title,
45
+ relatedEntityType: 'ticket',
46
+ relatedEntityId: ticketId,
47
+ actionUrl: `/tickets/${ticketId}`,
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Trigger notification when a comment is added to a ticket.
53
+ */
54
+ export async function notifyTicketComment(ticketId: string, commenterId: string) {
55
+ const ticket = await db.query.serviceTickets.findFirst({
56
+ where: eq(serviceTickets.id, ticketId),
57
+ });
58
+ if (!ticket) return;
59
+
60
+ const commenter = await db.query.users.findFirst({
61
+ where: eq(users.id, commenterId),
62
+ columns: { fullName: true },
63
+ });
64
+
65
+ // Notify reporter if commenter is not the reporter
66
+ if (ticket.reportedByUserId && ticket.reportedByUserId !== commenterId) {
67
+ await createNotification({
68
+ organizationId: ticket.organizationId,
69
+ recipientUserId: ticket.reportedByUserId,
70
+ notificationType: 'ticket_comment',
71
+ title: `New comment on ${ticket.ticketNumber}`,
72
+ body: `${commenter?.fullName || 'Someone'} commented on your ticket`,
73
+ relatedEntityType: 'ticket',
74
+ relatedEntityId: ticketId,
75
+ actionUrl: `/tickets/${ticketId}`,
76
+ });
77
+ }
78
+
79
+ // Notify assigned engineer if commenter is not the engineer
80
+ if (ticket.assignedEngineerUserId && ticket.assignedEngineerUserId !== commenterId) {
81
+ await createNotification({
82
+ organizationId: ticket.organizationId,
83
+ recipientUserId: ticket.assignedEngineerUserId,
84
+ notificationType: 'ticket_comment',
85
+ title: `New comment on ${ticket.ticketNumber}`,
86
+ body: `${commenter?.fullName || 'Someone'} commented on the ticket`,
87
+ relatedEntityType: 'ticket',
88
+ relatedEntityId: ticketId,
89
+ actionUrl: `/tickets/${ticketId}`,
90
+ });
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Trigger notification when a maintenance task needs review.
96
+ */
97
+ export async function notifyTaskReviewRequired(taskId: string, reviewerUserId: string) {
98
+ const task = await db.query.maintenanceTasks.findFirst({
99
+ where: eq(maintenanceTasks.id, taskId),
100
+ with: { device: { columns: { name: true } } },
101
+ });
102
+ if (!task) return;
103
+
104
+ await createNotification({
105
+ organizationId: task.organizationId,
106
+ recipientUserId: reviewerUserId,
107
+ notificationType: 'task_review_required',
108
+ title: `Task ${task.taskNumber} needs review`,
109
+ body: `${task.title}${task.device ? ` — ${task.device.name}` : ''}`,
110
+ relatedEntityType: 'task',
111
+ relatedEntityId: taskId,
112
+ actionUrl: `/maintenance/tasks/${taskId}`,
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Trigger notification when a PM task is due soon.
118
+ */
119
+ export async function notifyPMDueSoon(taskId: string, engineerUserId: string) {
120
+ const task = await db.query.maintenanceTasks.findFirst({
121
+ where: eq(maintenanceTasks.id, taskId),
122
+ with: { device: { columns: { name: true } } },
123
+ });
124
+ if (!task) return;
125
+
126
+ await createNotification({
127
+ organizationId: task.organizationId,
128
+ recipientUserId: engineerUserId,
129
+ notificationType: 'pm_due_soon',
130
+ title: `PM task ${task.taskNumber} due soon`,
131
+ body: `${task.title}${task.device ? ` — ${task.device.name}` : ''}`,
132
+ relatedEntityType: 'task',
133
+ relatedEntityId: taskId,
134
+ actionUrl: `/maintenance/tasks/${taskId}`,
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Trigger notification when a device status changes.
140
+ */
141
+ export async function notifyDeviceStatusChanged(
142
+ deviceId: string,
143
+ newStatus: string,
144
+ recipientUserIds: string[]
145
+ ) {
146
+ const device = await db.query.devices.findFirst({
147
+ where: eq(devices.id, deviceId),
148
+ });
149
+ if (!device) return;
150
+
151
+ const statusLabel = newStatus.replace(/_/g, ' ');
152
+
153
+ for (const userId of recipientUserIds) {
154
+ await createNotification({
155
+ organizationId: device.organizationId,
156
+ recipientUserId: userId,
157
+ notificationType: 'device_status_changed',
158
+ title: `${device.name} is now ${statusLabel}`,
159
+ body: `Asset ${device.assetNumber || device.name} status changed`,
160
+ relatedEntityType: 'device',
161
+ relatedEntityId: deviceId,
162
+ actionUrl: `/devices/${deviceId}`,
163
+ });
164
+ }
165
+ }
166
+
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildCanonicalSnapshot, computeSHA256 } from '../crypto';
3
+
4
+ describe('buildCanonicalSnapshot', () => {
5
+ it('sorts keys deterministically regardless of input key order', () => {
6
+ const objA = { z: 1, a: 2, m: 3 };
7
+ const objB = { a: 2, m: 3, z: 1 };
8
+
9
+ const snapshotA = buildCanonicalSnapshot(objA);
10
+ const snapshotB = buildCanonicalSnapshot(objB);
11
+
12
+ expect(snapshotA).toBe(snapshotB);
13
+ expect(snapshotA).toBe('{"a":2,"m":3,"z":1}');
14
+ });
15
+ });
16
+
17
+ describe('computeSHA256', () => {
18
+ it('computes a 64-character hex hash', () => {
19
+ const hash = computeSHA256('hello world');
20
+ expect(hash).toHaveLength(64);
21
+ expect(hash).toBe('b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9');
22
+ });
23
+
24
+ it('produces identical hashes for identical canonical snapshots', () => {
25
+ const hash1 = computeSHA256(buildCanonicalSnapshot({ id: '1', title: 'Test' }));
26
+ const hash2 = computeSHA256(buildCanonicalSnapshot({ title: 'Test', id: '1' }));
27
+ expect(hash1).toBe(hash2);
28
+ });
29
+ });
30
+
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateCSV, formatReportMetadata } from '../export';
3
+
4
+ describe('generateCSV', () => {
5
+ const columns = [
6
+ { key: 'name', header: 'Device Name' },
7
+ { key: 'assetNumber', header: 'Asset #' },
8
+ { key: 'status', header: 'Current Status' },
9
+ ];
10
+
11
+ it('outputs BOM and header row when data is empty', () => {
12
+ const csv = generateCSV([], columns);
13
+ expect(csv.startsWith('\uFEFF')).toBe(true);
14
+ expect(csv).toContain('"Device Name","Asset #","Current Status"');
15
+ });
16
+
17
+ it('formats rows with quotes and escapes embedded quotes', () => {
18
+ const data = [
19
+ { name: 'Monitor "Pro"', assetNumber: 'AST-100', status: 'operational' },
20
+ { name: 'Pump, Syringe', assetNumber: 'AST-101', status: 'under_repair' },
21
+ ];
22
+ const csv = generateCSV(data, columns);
23
+ expect(csv).toContain('"Monitor ""Pro"""');
24
+ expect(csv).toContain('"Pump, Syringe"');
25
+ });
26
+
27
+ it('handles null and undefined values cleanly', () => {
28
+ const data = [
29
+ { name: 'Sensor', assetNumber: null, status: undefined },
30
+ ];
31
+ const csv = generateCSV(data, columns);
32
+ expect(csv).toContain('"Sensor","",""');
33
+ });
34
+ });
35
+
36
+ describe('formatReportMetadata', () => {
37
+ it('formats report transparency header lines correctly', () => {
38
+ const date = new Date('2026-09-01T12:00:00Z');
39
+ const header = formatReportMetadata({
40
+ reportType: 'inventory',
41
+ filters: { departmentId: 'dept-123' },
42
+ dateRange: { start: '2026-01-01', end: '2026-09-01' },
43
+ timezone: 'UTC',
44
+ generatedAt: date,
45
+ generatedBy: 'Admin User',
46
+ });
47
+
48
+ expect(header).toContain('# Report Type: inventory');
49
+ expect(header).toContain('# Generated By: Admin User');
50
+ expect(header).toContain('# Date Range Start: 2026-01-01');
51
+ expect(header).toContain('# Date Range End: 2026-09-01');
52
+ expect(header).toContain('dept-123');
53
+ });
54
+ });
55
+
@@ -0,0 +1,147 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ navBadgeVariants,
4
+ formatNavBadge,
5
+ } from '@/lib/utils/nav-badge';
6
+
7
+ describe('NavBadge system', () => {
8
+ describe('navBadgeVariants formatting & class generation', () => {
9
+ it('generates standard count badge classes with default options', () => {
10
+ const classes = navBadgeVariants({
11
+ variant: 'default',
12
+ type: 'count',
13
+ position: 'trailing',
14
+ });
15
+ expect(classes).toContain('bg-primary');
16
+ expect(classes).toContain('ml-auto');
17
+ expect(classes).toContain('rounded-full');
18
+ });
19
+
20
+ it('generates icon-corner position classes for collapsed sidebars and mobile tabs', () => {
21
+ const classes = navBadgeVariants({
22
+ variant: 'danger',
23
+ type: 'count',
24
+ position: 'icon-corner',
25
+ });
26
+ expect(classes).toContain('absolute');
27
+ expect(classes).toContain('-top-1');
28
+ expect(classes).toContain('-right-1');
29
+ expect(classes).toContain('bg-destructive');
30
+ });
31
+
32
+ it('generates dot badge classes for maintenance indicators', () => {
33
+ const classes = navBadgeVariants({
34
+ variant: 'warning',
35
+ type: 'dot',
36
+ position: 'icon-corner',
37
+ });
38
+ expect(classes).toContain('rounded-full');
39
+ expect(classes).toContain('w-2.5');
40
+ expect(classes).toContain('h-2.5');
41
+ expect(classes).toContain('bg-amber-500');
42
+ });
43
+
44
+ it('generates status badge classes for custom status text', () => {
45
+ const classes = navBadgeVariants({
46
+ variant: 'success',
47
+ type: 'status',
48
+ position: 'trailing',
49
+ });
50
+ expect(classes).toContain('bg-emerald-600');
51
+ expect(classes).toContain('uppercase');
52
+ expect(classes).toContain('truncate');
53
+ });
54
+ });
55
+
56
+ describe('formatNavBadge logic', () => {
57
+ it('hides count badge when value is 0 and hideWhenZero is true', () => {
58
+ const result = formatNavBadge({
59
+ type: 'count',
60
+ value: 0,
61
+ hideWhenZero: true,
62
+ });
63
+ expect(result.visible).toBe(false);
64
+ });
65
+
66
+ it('displays count badge when value is 0 and hideWhenZero is false', () => {
67
+ const result = formatNavBadge({
68
+ type: 'count',
69
+ value: 0,
70
+ hideWhenZero: false,
71
+ });
72
+ expect(result.visible).toBe(true);
73
+ expect(result.display).toBe('0');
74
+ });
75
+
76
+ it('displays normal count under maxCount', () => {
77
+ const result = formatNavBadge({
78
+ type: 'count',
79
+ value: 12,
80
+ maxCount: 99,
81
+ });
82
+ expect(result.visible).toBe(true);
83
+ expect(result.display).toBe('12');
84
+ expect(result.accessibleDescription).toBe('12 items');
85
+ });
86
+
87
+ it('formats counts exceeding maxCount with a plus suffix', () => {
88
+ const result = formatNavBadge({
89
+ type: 'count',
90
+ value: 150,
91
+ maxCount: 99,
92
+ });
93
+ expect(result.visible).toBe(true);
94
+ expect(result.display).toBe('99+');
95
+ expect(result.accessibleDescription).toBe('More than 99 items');
96
+ });
97
+
98
+ it('formats custom maxCount correctly (e.g. maxCount: 9)', () => {
99
+ const result = formatNavBadge({
100
+ type: 'count',
101
+ value: 15,
102
+ maxCount: 9,
103
+ });
104
+ expect(result.visible).toBe(true);
105
+ expect(result.display).toBe('9+');
106
+ });
107
+
108
+ it('uses custom aria-label when provided for accessibility', () => {
109
+ const result = formatNavBadge({
110
+ type: 'count',
111
+ value: 5,
112
+ ariaLabel: '5 unread notifications',
113
+ });
114
+ expect(result.accessibleDescription).toBe('5 unread notifications');
115
+ });
116
+
117
+ it('supports dot indicator without numerical value', () => {
118
+ const result = formatNavBadge({
119
+ type: 'dot',
120
+ ariaLabel: 'Maintenance due',
121
+ });
122
+ expect(result.visible).toBe(true);
123
+ expect(result.isDot).toBe(true);
124
+ expect(result.accessibleDescription).toBe('Maintenance due');
125
+ });
126
+
127
+ it('supports status labels with proper text formatting', () => {
128
+ const result = formatNavBadge({
129
+ type: 'status',
130
+ value: 'New',
131
+ ariaLabel: 'New feature',
132
+ });
133
+ expect(result.visible).toBe(true);
134
+ expect(result.display).toBe('New');
135
+ expect(result.accessibleDescription).toBe('New feature');
136
+ });
137
+
138
+ it('hides badge when enabled is explicitly false', () => {
139
+ const result = formatNavBadge({
140
+ enabled: false,
141
+ type: 'count',
142
+ value: 10,
143
+ });
144
+ expect(result.visible).toBe(false);
145
+ });
146
+ });
147
+ });
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getStaffStatus, getStaffStatusStyle, isStaffOnly } from '@/lib/utils/staff-status';
3
+
4
+ describe('Staff Status Translations', () => {
5
+ it('translates internal status codes to patient/clinical-friendly labels', () => {
6
+ expect(getStaffStatus('new').label).toBe('Submitted');
7
+ expect(getStaffStatus('acknowledged').label).toBe('Reviewed');
8
+ expect(getStaffStatus('in_triage').label).toBe('Being reviewed');
9
+ expect(getStaffStatus('assigned').label).toBe('Assigned to Engineering');
10
+ expect(getStaffStatus('in_progress').label).toBe('Being repaired');
11
+ expect(getStaffStatus('waiting_parts_vendor').label).toBe('Waiting for parts');
12
+ expect(getStaffStatus('waiting_requester').label).toBe('Response requested');
13
+ expect(getStaffStatus('resolved').label).toBe('Resolved');
14
+ expect(getStaffStatus('closed').label).toBe('Closed');
15
+ expect(getStaffStatus('reopened').label).toBe('Reopened');
16
+ });
17
+
18
+ it('provides sensible fallback for unknown status codes', () => {
19
+ const unknown = getStaffStatus('custom_status_code');
20
+ expect(unknown.label).toBe('custom status code');
21
+ expect(unknown.color).toBe('gray');
22
+ });
23
+
24
+ it('provides theme styles for status colors', () => {
25
+ const inProgressStyle = getStaffStatusStyle('in_progress');
26
+ expect(inProgressStyle.bg).toContain('amber');
27
+
28
+ const resolvedStyle = getStaffStatusStyle('resolved');
29
+ expect(resolvedStyle.bg).toContain('green');
30
+
31
+ const waitingRequesterStyle = getStaffStatusStyle('waiting_requester');
32
+ expect(waitingRequesterStyle.bg).toContain('red');
33
+ });
34
+
35
+ it('correctly identifies staff-only roles without biomedical or admin privileges', () => {
36
+ expect(isStaffOnly(['STAFF'])).toBe(true);
37
+ expect(isStaffOnly(['DEPT_MGR'])).toBe(true);
38
+ expect(isStaffOnly(['STAFF', 'DEPT_MGR'])).toBe(true);
39
+
40
+ // Elevated roles must NOT be staff-only
41
+ expect(isStaffOnly(['STAFF', 'BIOMED_ENG'])).toBe(false);
42
+ expect(isStaffOnly(['STAFF', 'BIOMED_TECH'])).toBe(false);
43
+ expect(isStaffOnly(['STAFF', 'BIOMED_MGR'])).toBe(false);
44
+ expect(isStaffOnly(['STAFF', 'SYS_ADMIN'])).toBe(false);
45
+ expect(isStaffOnly(['ORG_ADMIN'])).toBe(false);
46
+ expect(isStaffOnly(['HOSP_ADMIN'])).toBe(false);
47
+ expect(isStaffOnly(['BIOMED_ENG'])).toBe(false);
48
+ expect(isStaffOnly([])).toBe(false);
49
+ });
50
+ });
@@ -0,0 +1,19 @@
1
+ // ── Attestation Templates ───────────────────────────────────────────────────
2
+
3
+ export const ATTESTATION_TEMPLATES: Record<string, string> = {
4
+ perform: 'I confirm that I performed the recorded maintenance/inspection and that the information entered is accurate to the best of my knowledge.',
5
+ review: 'I have reviewed the performed work, findings, evidence, and test results, and confirm technical completeness and accuracy.',
6
+ approve: 'I approve the recorded work, findings, and recommendations in accordance with the applicable policy.',
7
+ release: 'I authorize the release of this medical device for clinical use with the specified operational status.',
8
+ accept: 'I accept responsibility for the assigned work and confirm I will perform it in accordance with applicable procedures.',
9
+ resolve: 'I confirm that the issue has been resolved and the resolution summary accurately describes the work performed.',
10
+ close: 'I confirm that the ticket resolution is sufficiently documented and complete under the applicable policy.',
11
+ reject: 'I am returning this work for correction. The stated reason identifies the issues that must be addressed.',
12
+ amend: 'I am submitting a correction to the previously signed record. The amendment reason describes the changes made.',
13
+ void: 'I am voiding this signature/record. The stated reason documents why this action is necessary.',
14
+ };
15
+
16
+ export function getAttestationText(purpose: string): string {
17
+ return ATTESTATION_TEMPLATES[purpose] || `I confirm this ${purpose} action and that the information is accurate.`;
18
+ }
19
+
@@ -0,0 +1,18 @@
1
+ import { createHash } from 'crypto';
2
+
3
+ /**
4
+ * Build a canonical JSON snapshot for cryptographic hashing.
5
+ * Keys are sorted deterministically to ensure the same content
6
+ * always produces the same hash regardless of insertion order.
7
+ */
8
+ export function buildCanonicalSnapshot(data: Record<string, unknown>): string {
9
+ return JSON.stringify(data, Object.keys(data).sort(), 0);
10
+ }
11
+
12
+ /**
13
+ * Compute SHA-256 hash of a canonical JSON snapshot string.
14
+ */
15
+ export function computeSHA256(content: string): string {
16
+ return createHash('sha256').update(content, 'utf8').digest('hex');
17
+ }
18
+
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Robust CSV parser that handles quotes, escaped quotes, and newlines.
3
+ */
4
+ export function parseCSV(text: string): Record<string, string>[] {
5
+ const cleanText = text.replace(/^\uFEFF/, '').trim(); // Remove BOM if present
6
+ if (!cleanText) return [];
7
+
8
+ const rows: string[][] = [];
9
+ let currentRow: string[] = [];
10
+ let currentField = '';
11
+ let inQuotes = false;
12
+
13
+ for (let i = 0; i < cleanText.length; i++) {
14
+ const char = cleanText[i];
15
+ const nextChar = cleanText[i + 1];
16
+
17
+ if (inQuotes) {
18
+ if (char === '"' && nextChar === '"') {
19
+ currentField += '"';
20
+ i++; // Skip escaped quote
21
+ } else if (char === '"') {
22
+ inQuotes = false;
23
+ } else {
24
+ currentField += char;
25
+ }
26
+ } else {
27
+ if (char === '"') {
28
+ inQuotes = true;
29
+ } else if (char === ',') {
30
+ currentRow.push(currentField.trim());
31
+ currentField = '';
32
+ } else if (char === '\r') {
33
+ // Skip CR in CRLF
34
+ continue;
35
+ } else if (char === '\n') {
36
+ currentRow.push(currentField.trim());
37
+ if (currentRow.some((field) => field.length > 0)) {
38
+ rows.push(currentRow);
39
+ }
40
+ currentRow = [];
41
+ currentField = '';
42
+ } else {
43
+ currentField += char;
44
+ }
45
+ }
46
+ }
47
+
48
+ // Flush remaining field/row
49
+ if (currentField || currentRow.length > 0) {
50
+ currentRow.push(currentField.trim());
51
+ if (currentRow.some((field) => field.length > 0)) {
52
+ rows.push(currentRow);
53
+ }
54
+ }
55
+
56
+ if (rows.length < 2) return [];
57
+
58
+ // Normalize header keys: lowercase, remove non-alphanumeric except underscore
59
+ const rawHeaders = rows[0];
60
+ const headers = rawHeaders.map((h) =>
61
+ h
62
+ .toLowerCase()
63
+ .replace(/[\s\-_]+/g, '_')
64
+ .replace(/[^a-z0-9_]/g, '')
65
+ );
66
+
67
+ const records: Record<string, string>[] = [];
68
+
69
+ for (let i = 1; i < rows.length; i++) {
70
+ const row = rows[i];
71
+ const record: Record<string, string> = {};
72
+ headers.forEach((header, idx) => {
73
+ record[header] = row[idx] !== undefined ? row[idx].trim() : '';
74
+ });
75
+ records.push(record);
76
+ }
77
+
78
+ return records;
79
+ }
80
+
81
+ /**
82
+ * Returns a ready-to-use CSV template string with BOM and sample rows.
83
+ */
84
+ export function getDeviceImportTemplateCSV(): string {
85
+ const BOM = '\uFEFF';
86
+ const headers = [
87
+ 'asset_number',
88
+ 'name',
89
+ 'model',
90
+ 'serial_number',
91
+ 'category',
92
+ 'manufacturer',
93
+ 'department',
94
+ 'location',
95
+ 'risk_classification',
96
+ 'criticality',
97
+ 'purchase_date',
98
+ 'purchase_cost',
99
+ 'installation_date',
100
+ 'warranty_start_date',
101
+ 'warranty_end_date',
102
+ ].join(',');
103
+
104
+ const samples = [
105
+ [
106
+ 'BEMMS-DEV-001',
107
+ 'Infusion Pump IV-300',
108
+ 'IV-3000',
109
+ 'SN-8492014',
110
+ 'Infusion Pump',
111
+ 'Baxter',
112
+ 'Emergency Department',
113
+ 'Room 102 - Trauma Bay',
114
+ 'class_iib',
115
+ 'high',
116
+ '2023-01-15',
117
+ '3500.00',
118
+ '2023-02-01',
119
+ '2023-02-01',
120
+ '2025-02-01',
121
+ ].join(','),
122
+ [
123
+ 'BEMMS-DEV-002',
124
+ 'Patient Monitor IntelliVue',
125
+ 'MX450',
126
+ 'SN-9921443',
127
+ 'Patient Monitor',
128
+ 'Philips Healthcare',
129
+ 'Intensive Care Unit (ICU)',
130
+ 'Bed 04',
131
+ 'class_iib',
132
+ 'high',
133
+ '2023-03-10',
134
+ '12500.00',
135
+ '2023-03-25',
136
+ '2023-03-25',
137
+ '2026-03-25',
138
+ ].join(','),
139
+ [
140
+ 'BEMMS-DEV-003',
141
+ 'Centrifuge 5424 R',
142
+ '5424 R',
143
+ 'SN-1029481',
144
+ 'Laboratory Centrifuge',
145
+ 'Eppendorf',
146
+ 'Central Laboratory',
147
+ 'Bench 3',
148
+ 'class_i',
149
+ 'low',
150
+ '2023-05-20',
151
+ '4800.00',
152
+ '2023-06-01',
153
+ '2023-06-01',
154
+ '2025-06-01',
155
+ ].join(','),
156
+ ].join('\n');
157
+
158
+ return `${BOM}${headers}\n${samples}\n`;
159
+ }
160
+
161
+ /**
162
+ * Returns a ready-to-use CSV template string with BOM and sample rows for departments.
163
+ */
164
+ export function getDepartmentImportTemplateCSV(): string {
165
+ const BOM = '\uFEFF';
166
+ const headers = [
167
+ 'name',
168
+ 'code',
169
+ 'department_type',
170
+ 'hospital_name',
171
+ ].join(',');
172
+
173
+ const samples = [
174
+ ['Emergency Department', 'ED', 'emergency', 'Central Teaching Hospital'].join(','),
175
+ ['Intensive Care Unit', 'ICU', 'critical_care', 'Central Teaching Hospital'].join(','),
176
+ ['Radiology Department', 'RAD', 'diagnostic', 'Central Teaching Hospital'].join(','),
177
+ ].join('\n');
178
+
179
+ return `${BOM}${headers}\n${samples}\n`;
180
+ }
181
+