@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,45 @@
1
+ // Generate CSV with BOM for Excel compatibility
2
+ export function generateCSV(data: Record<string, unknown>[], columns: { key: string; header: string }[]): string {
3
+ const BOM = '\uFEFF';
4
+ if (!data || data.length === 0) {
5
+ return BOM + columns.map(c => `"${c.header.replace(/"/g, '""')}"`).join(',');
6
+ }
7
+
8
+ const headerRow = columns.map(c => `"${c.header.replace(/"/g, '""')}"`).join(',');
9
+ const rows = data.map(row =>
10
+ columns.map(c => {
11
+ let val = row[c.key];
12
+ if (val === null || val === undefined) val = '';
13
+ return `"${String(val).replace(/"/g, '""')}"`;
14
+ }).join(',')
15
+ );
16
+
17
+ return BOM + [headerRow, ...rows].join('\n');
18
+ }
19
+
20
+ // Format report metadata for transparency header
21
+ export function formatReportMetadata(params: {
22
+ reportType: string;
23
+ filters: Record<string, unknown>;
24
+ dateRange?: { start?: string; end?: string };
25
+ timezone: string;
26
+ generatedAt: Date;
27
+ generatedBy: string;
28
+ }): string {
29
+ const lines = [
30
+ `Report Type: ${params.reportType}`,
31
+ `Generated At: ${params.generatedAt.toISOString()} (${params.timezone})`,
32
+ `Generated By: ${params.generatedBy}`,
33
+ ];
34
+
35
+ if (params.dateRange?.start) lines.push(`Date Range Start: ${params.dateRange.start}`);
36
+ if (params.dateRange?.end) lines.push(`Date Range End: ${params.dateRange.end}`);
37
+
38
+ const filterEntries = Object.entries(params.filters).filter(([k, v]) => v !== undefined && k !== 'page' && k !== 'pageSize' && k !== 'startDate' && k !== 'endDate' && k !== 'timezone');
39
+ if (filterEntries.length > 0) {
40
+ lines.push(`Filters: ${JSON.stringify(Object.fromEntries(filterEntries))}`);
41
+ }
42
+
43
+ return lines.map(line => `# ${line}`).join('\n') + '\n\n';
44
+ }
45
+
@@ -0,0 +1,225 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+
3
+ export type NavBadgeType = 'count' | 'dot' | 'status';
4
+ export type NavBadgePosition = 'trailing' | 'icon-corner' | 'inline';
5
+
6
+ export const navBadgeVariants = cva(
7
+ 'transition-all duration-200 select-none font-medium',
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'bg-primary text-primary-foreground',
12
+ primary: 'bg-primary text-primary-foreground',
13
+ secondary: 'bg-secondary text-secondary-foreground',
14
+ success: 'bg-emerald-600 text-white dark:bg-emerald-500',
15
+ warning: 'bg-amber-500 text-white dark:bg-amber-600',
16
+ danger: 'bg-destructive text-destructive-foreground',
17
+ destructive: 'bg-destructive text-destructive-foreground',
18
+ outline: 'border border-border bg-background text-foreground',
19
+ },
20
+ position: {
21
+ trailing: 'ml-auto shrink-0',
22
+ inline: 'shrink-0',
23
+ 'icon-corner': 'absolute -top-1 -right-1 z-10 pointer-events-none ring-2 ring-card',
24
+ },
25
+ type: {
26
+ count: 'inline-flex items-center justify-center font-semibold text-center',
27
+ status: 'inline-flex items-center justify-center font-medium uppercase tracking-wider',
28
+ dot: 'rounded-full inline-block shrink-0',
29
+ },
30
+ },
31
+ compoundVariants: [
32
+ // Count badge sizing
33
+ {
34
+ type: 'count',
35
+ position: 'trailing',
36
+ className: 'min-w-[20px] h-5 px-1.5 rounded-full text-[11px] leading-none',
37
+ },
38
+ {
39
+ type: 'count',
40
+ position: 'inline',
41
+ className: 'min-w-[18px] h-4.5 px-1.5 rounded-full text-[10px] leading-none',
42
+ },
43
+ {
44
+ type: 'count',
45
+ position: 'icon-corner',
46
+ className: 'min-w-[18px] h-4.5 px-1 rounded-full text-[10px] leading-none shadow-xs font-bold',
47
+ },
48
+ // Status badge sizing
49
+ {
50
+ type: 'status',
51
+ position: 'trailing',
52
+ className: 'px-2 py-0.5 rounded-md text-[10px] max-w-[80px] truncate',
53
+ },
54
+ {
55
+ type: 'status',
56
+ position: 'inline',
57
+ className: 'px-1.5 py-0.5 rounded text-[9px] max-w-[70px] truncate',
58
+ },
59
+ {
60
+ type: 'status',
61
+ position: 'icon-corner',
62
+ className: 'px-1 py-0.5 rounded text-[8px] leading-none max-w-[50px] truncate',
63
+ },
64
+ // Dot badge sizing
65
+ {
66
+ type: 'dot',
67
+ position: 'icon-corner',
68
+ className: 'w-2.5 h-2.5',
69
+ },
70
+ {
71
+ type: 'dot',
72
+ position: 'trailing',
73
+ className: 'w-2 h-2 ml-auto',
74
+ },
75
+ {
76
+ type: 'dot',
77
+ position: 'inline',
78
+ className: 'w-2 h-2',
79
+ },
80
+ ],
81
+ defaultVariants: {
82
+ variant: 'default',
83
+ position: 'trailing',
84
+ type: 'count',
85
+ },
86
+ }
87
+ );
88
+
89
+ export type NavBadgeVariant = NonNullable<VariantProps<typeof navBadgeVariants>['variant']>;
90
+
91
+ export interface NavBadgeConfig {
92
+ /**
93
+ * Whether the badge is enabled and visible.
94
+ * Default: true
95
+ */
96
+ enabled?: boolean;
97
+ /**
98
+ * Badge type:
99
+ * - 'count': numerical count with maxCount formatting (e.g. 5, 99+)
100
+ * - 'dot': compact circular indicator
101
+ * - 'status': short text label (e.g. "NEW", "DUE")
102
+ * Default: 'count'
103
+ */
104
+ type?: NavBadgeType;
105
+ /**
106
+ * Numerical count or text value for status badges.
107
+ */
108
+ value?: number | string | null;
109
+ /**
110
+ * Visual variant/color theme.
111
+ * Default: 'default'
112
+ */
113
+ variant?: NavBadgeVariant;
114
+ /**
115
+ * Placement relative to the navigation item:
116
+ * - 'trailing': at the right end of the item (ideal for expanded sidebars)
117
+ * - 'icon-corner': anchored to the top-right of the icon (ideal for collapsed sidebars & mobile nav)
118
+ * - 'inline': alongside the label
119
+ * Default: 'trailing'
120
+ */
121
+ position?: NavBadgePosition;
122
+ /**
123
+ * Maximum count before formatting with "+" (e.g. 99 -> "99+").
124
+ * Default: 99
125
+ */
126
+ maxCount?: number;
127
+ /**
128
+ * Whether to hide count badge when value is 0.
129
+ * Default: true
130
+ */
131
+ hideWhenZero?: boolean;
132
+ /**
133
+ * Whether to animate with a pulse effect (great for urgent alerts or activity dots).
134
+ * Default: false
135
+ */
136
+ pulse?: boolean;
137
+ /**
138
+ * Accessible description for screen readers (e.g. "5 unread notifications").
139
+ */
140
+ ariaLabel?: string;
141
+ /**
142
+ * Additional custom CSS classes.
143
+ */
144
+ className?: string;
145
+ }
146
+
147
+ export interface FormattedNavBadge {
148
+ visible: boolean;
149
+ display: string;
150
+ isDot: boolean;
151
+ accessibleDescription: string;
152
+ }
153
+
154
+ /**
155
+ * Format and calculate visibility of badge data according to badge type and configuration.
156
+ */
157
+ export function formatNavBadge(config: NavBadgeConfig): FormattedNavBadge {
158
+ const {
159
+ enabled = true,
160
+ type = 'count',
161
+ value,
162
+ maxCount = 99,
163
+ hideWhenZero = true,
164
+ ariaLabel,
165
+ } = config;
166
+
167
+ if (!enabled) {
168
+ return { visible: false, display: '', isDot: false, accessibleDescription: '' };
169
+ }
170
+
171
+ if (type === 'dot') {
172
+ if (typeof value === 'number' && value === 0 && hideWhenZero) {
173
+ return { visible: false, display: '', isDot: true, accessibleDescription: '' };
174
+ }
175
+ return {
176
+ visible: true,
177
+ display: '',
178
+ isDot: true,
179
+ accessibleDescription: ariaLabel || 'New activity indicator',
180
+ };
181
+ }
182
+
183
+ if (type === 'status') {
184
+ if (!value && value !== 0) {
185
+ return { visible: false, display: '', isDot: false, accessibleDescription: '' };
186
+ }
187
+ const str = String(value);
188
+ return {
189
+ visible: true,
190
+ display: str,
191
+ isDot: false,
192
+ accessibleDescription: ariaLabel || `Status: ${str}`,
193
+ };
194
+ }
195
+
196
+ // Count type
197
+ const numValue = typeof value === 'number' ? value : Number(value);
198
+ if (isNaN(numValue)) {
199
+ if (value === null || value === undefined || value === '') {
200
+ return { visible: false, display: '', isDot: false, accessibleDescription: '' };
201
+ }
202
+ const str = String(value);
203
+ return {
204
+ visible: true,
205
+ display: str,
206
+ isDot: false,
207
+ accessibleDescription: ariaLabel || `${str} items`,
208
+ };
209
+ }
210
+
211
+ if (numValue === 0 && hideWhenZero) {
212
+ return { visible: false, display: '', isDot: false, accessibleDescription: '' };
213
+ }
214
+
215
+ const display = numValue > maxCount ? `${maxCount}+` : String(numValue);
216
+ const accessibleDescription =
217
+ ariaLabel || (numValue > maxCount ? `More than ${maxCount} items` : `${numValue} items`);
218
+
219
+ return {
220
+ visible: true,
221
+ display,
222
+ isDot: false,
223
+ accessibleDescription,
224
+ };
225
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * BEMMS Sound Utilities
3
+ *
4
+ * Uses the Web Audio API to synthesize sounds programmatically.
5
+ * No external audio files needed — works on all modern browsers and native devices.
6
+ *
7
+ * Web Audio API is supported on iOS Safari, Android Chrome, and all desktop browsers.
8
+ * On mobile, sounds require a prior user gesture (tap) to unlock the AudioContext.
9
+ */
10
+
11
+ let audioContext: AudioContext | null = null;
12
+
13
+ /**
14
+ * Get or create the shared AudioContext.
15
+ * Must be called after a user gesture on mobile to unlock audio.
16
+ */
17
+ function getAudioContext(): AudioContext | null {
18
+ if (typeof window === 'undefined') return null;
19
+
20
+ if (!audioContext) {
21
+ const AudioCtx =
22
+ window.AudioContext || (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
23
+ if (!AudioCtx) return null;
24
+ audioContext = new AudioCtx();
25
+ }
26
+
27
+ // Resume if suspended (required on mobile after tab switch / background)
28
+ if (audioContext.state === 'suspended') {
29
+ audioContext.resume().catch(() => {});
30
+ }
31
+
32
+ return audioContext;
33
+ }
34
+
35
+ // ── QR Scan Beep ─────────────────────────────────────────────────────────────────
36
+
37
+ /**
38
+ * Play a short, clear "beep" to indicate a successful QR scan.
39
+ * Two-tone chirp that feels satisfying and professional on mobile devices.
40
+ */
41
+ export function playQrBeep(): void {
42
+ const ctx = getAudioContext();
43
+ if (!ctx) return;
44
+
45
+ const now = ctx.currentTime;
46
+
47
+ // --- Tone 1: rising chirp ---
48
+ const osc1 = ctx.createOscillator();
49
+ const gain1 = ctx.createGain();
50
+ osc1.type = 'sine';
51
+ osc1.frequency.setValueAtTime(1200, now);
52
+ osc1.frequency.linearRampToValueAtTime(1800, now + 0.08);
53
+ gain1.gain.setValueAtTime(0.35, now);
54
+ gain1.gain.exponentialRampToValueAtTime(0.01, now + 0.12);
55
+ osc1.connect(gain1);
56
+ gain1.connect(ctx.destination);
57
+ osc1.start(now);
58
+ osc1.stop(now + 0.12);
59
+
60
+ // --- Tone 2: confirmation note ---
61
+ const osc2 = ctx.createOscillator();
62
+ const gain2 = ctx.createGain();
63
+ osc2.type = 'sine';
64
+ osc2.frequency.setValueAtTime(1800, now + 0.1);
65
+ gain2.gain.setValueAtTime(0, now + 0.1);
66
+ gain2.gain.linearRampToValueAtTime(0.35, now + 0.12);
67
+ gain2.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
68
+ osc2.connect(gain2);
69
+ gain2.connect(ctx.destination);
70
+ osc2.start(now + 0.1);
71
+ osc2.stop(now + 0.3);
72
+ }
73
+
74
+ // ── Notification Sound ───────────────────────────────────────────────────────────
75
+
76
+ /**
77
+ * Play a pleasant notification chime for incoming alerts (new ticket, status change, etc.).
78
+ * Three ascending tones — distinct from the QR beep, gentle but attention-grabbing.
79
+ */
80
+ export function playNotificationSound(): void {
81
+ const ctx = getAudioContext();
82
+ if (!ctx) return;
83
+
84
+ const now = ctx.currentTime;
85
+
86
+ const tones = [
87
+ { freq: 660, start: 0, duration: 0.15 }, // E5
88
+ { freq: 880, start: 0.15, duration: 0.15 }, // A5
89
+ { freq: 1100, start: 0.30, duration: 0.25 }, // ~C#6
90
+ ];
91
+
92
+ tones.forEach(({ freq, start, duration }) => {
93
+ const osc = ctx.createOscillator();
94
+ const gain = ctx.createGain();
95
+ osc.type = 'sine';
96
+ osc.frequency.setValueAtTime(freq, now + start);
97
+ gain.gain.setValueAtTime(0, now + start);
98
+ gain.gain.linearRampToValueAtTime(0.25, now + start + 0.02);
99
+ gain.gain.exponentialRampToValueAtTime(0.01, now + start + duration);
100
+ osc.connect(gain);
101
+ gain.connect(ctx.destination);
102
+ osc.start(now + start);
103
+ osc.stop(now + start + duration);
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Play an urgent/critical notification sound — louder and more insistent.
109
+ * Used for critical priority tickets (patient care affected).
110
+ */
111
+ export function playUrgentNotificationSound(): void {
112
+ const ctx = getAudioContext();
113
+ if (!ctx) return;
114
+
115
+ const now = ctx.currentTime;
116
+
117
+ // Rapid two-pulse alarm tone
118
+ for (let i = 0; i < 2; i++) {
119
+ const offset = i * 0.3;
120
+
121
+ const osc = ctx.createOscillator();
122
+ const gain = ctx.createGain();
123
+ osc.type = 'square';
124
+ osc.frequency.setValueAtTime(880, now + offset);
125
+ osc.frequency.linearRampToValueAtTime(1320, now + offset + 0.1);
126
+ gain.gain.setValueAtTime(0.2, now + offset);
127
+ gain.gain.exponentialRampToValueAtTime(0.01, now + offset + 0.2);
128
+ osc.connect(gain);
129
+ gain.connect(ctx.destination);
130
+ osc.start(now + offset);
131
+ osc.stop(now + offset + 0.2);
132
+ }
133
+ }
134
+
135
+ // ── Unlock Audio on First User Gesture ───────────────────────────────────────────
136
+
137
+ /**
138
+ * Call this once on a user interaction (tap/click) to unlock audio on mobile devices.
139
+ * After this, sounds can play without further user gestures.
140
+ * Plays a silent buffer to unlock the AudioContext.
141
+ */
142
+ export function unlockAudio(): void {
143
+ const ctx = getAudioContext();
144
+ if (!ctx) return;
145
+
146
+ // Create and play a silent buffer to unlock
147
+ const buffer = ctx.createBuffer(1, 1, ctx.sampleRate);
148
+ const source = ctx.createBufferSource();
149
+ source.buffer = buffer;
150
+ source.connect(ctx.destination);
151
+ source.start(0);
152
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Staff-facing plain-language status translations.
3
+ * Department staff (doctors/nurses) see these instead of internal codes.
4
+ */
5
+
6
+ export interface StaffStatusInfo {
7
+ label: string;
8
+ color: 'blue' | 'indigo' | 'amber' | 'red' | 'green' | 'gray';
9
+ icon: string; // emoji
10
+ description: string;
11
+ }
12
+
13
+ const STAFF_STATUS_MAP: Record<string, StaffStatusInfo> = {
14
+ new: {
15
+ label: 'Submitted',
16
+ color: 'blue',
17
+ icon: '📝',
18
+ description: 'Your report has been submitted to Biomedical Engineering.',
19
+ },
20
+ acknowledged: {
21
+ label: 'Reviewed',
22
+ color: 'blue',
23
+ icon: '✓',
24
+ description: 'Your report has been reviewed by the biomedical team.',
25
+ },
26
+ in_triage: {
27
+ label: 'Being reviewed',
28
+ color: 'blue',
29
+ icon: '🔍',
30
+ description: 'The biomedical team is reviewing your report.',
31
+ },
32
+ assigned: {
33
+ label: 'Assigned to Engineering',
34
+ color: 'indigo',
35
+ icon: '👤',
36
+ description: 'An engineer has been assigned to work on this.',
37
+ },
38
+ in_progress: {
39
+ label: 'Being repaired',
40
+ color: 'amber',
41
+ icon: '🔧',
42
+ description: 'The engineer is actively working on this device.',
43
+ },
44
+ waiting_parts_vendor: {
45
+ label: 'Waiting for parts',
46
+ color: 'amber',
47
+ icon: '📦',
48
+ description: 'Repair is paused until replacement parts arrive.',
49
+ },
50
+ waiting_requester: {
51
+ label: 'Response requested',
52
+ color: 'red',
53
+ icon: '💬',
54
+ description: 'The biomedical team needs more information from you.',
55
+ },
56
+ resolved: {
57
+ label: 'Resolved',
58
+ color: 'green',
59
+ icon: '✅',
60
+ description: 'The issue has been fixed.',
61
+ },
62
+ closed: {
63
+ label: 'Closed',
64
+ color: 'gray',
65
+ icon: '✓',
66
+ description: 'This ticket is closed.',
67
+ },
68
+ cancelled: {
69
+ label: 'Cancelled',
70
+ color: 'gray',
71
+ icon: '—',
72
+ description: 'This ticket was cancelled.',
73
+ },
74
+ reopened: {
75
+ label: 'Reopened',
76
+ color: 'amber',
77
+ icon: '🔄',
78
+ description: 'This ticket has been reopened — work resumed.',
79
+ },
80
+ };
81
+
82
+ export function getStaffStatus(statusCode: string): StaffStatusInfo {
83
+ return STAFF_STATUS_MAP[statusCode] || {
84
+ label: statusCode.replace(/_/g, ' '),
85
+ color: 'gray' as const,
86
+ icon: '•',
87
+ description: '',
88
+ };
89
+ }
90
+
91
+ const STAFF_STATUS_STYLES: Record<string, { bg: string; text: string; border: string }> = {
92
+ blue: { bg: 'bg-blue-100 dark:bg-blue-950', text: 'text-blue-800 dark:text-blue-200', border: 'border-blue-200 dark:border-blue-800' },
93
+ indigo: { bg: 'bg-indigo-100 dark:bg-indigo-950', text: 'text-indigo-800 dark:text-indigo-200', border: 'border-indigo-200 dark:border-indigo-800' },
94
+ amber: { bg: 'bg-amber-100 dark:bg-amber-950', text: 'text-amber-800 dark:text-amber-200', border: 'border-amber-200 dark:border-amber-800' },
95
+ red: { bg: 'bg-red-100 dark:bg-red-950', text: 'text-red-800 dark:text-red-200', border: 'border-red-200 dark:border-red-800' },
96
+ green: { bg: 'bg-green-100 dark:bg-green-950', text: 'text-green-800 dark:text-green-200', border: 'border-green-200 dark:border-green-800' },
97
+ gray: { bg: 'bg-gray-100 dark:bg-gray-900', text: 'text-gray-600 dark:text-gray-400', border: 'border-gray-200 dark:border-gray-800' },
98
+ };
99
+
100
+ export function getStaffStatusStyle(statusCode: string) {
101
+ const info = getStaffStatus(statusCode);
102
+ return STAFF_STATUS_STYLES[info.color] || STAFF_STATUS_STYLES.gray;
103
+ }
104
+
105
+ /**
106
+ * Check if the given roles array represents a department staff user
107
+ * (STAFF or DEPT_MGR without any BME or admin roles).
108
+ */
109
+ export function isStaffOnly(roles: string[]): boolean {
110
+ const staffRoles = ['STAFF', 'DEPT_MGR'];
111
+ const elevatedRoles = ['SYS_ADMIN', 'ORG_ADMIN', 'HOSP_ADMIN', 'BIOMED_MGR', 'BIOMED_ENG', 'BIOMED_TECH', 'AUDITOR'];
112
+ const hasStaff = roles.some(r => staffRoles.includes(r));
113
+ const hasElevated = roles.some(r => elevatedRoles.includes(r));
114
+ return hasStaff && !hasElevated;
115
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { loginSchema, registerSchema, changePasswordSchema, passwordResetSchema } from '../auth';
3
+
4
+ describe('loginSchema', () => {
5
+ it('accepts valid email and password', () => {
6
+ const result = loginSchema.safeParse({
7
+ email: 'user@example.com',
8
+ password: 'password123',
9
+ });
10
+ expect(result.success).toBe(true);
11
+ });
12
+
13
+ it('accepts simple username like admin', () => {
14
+ const result = loginSchema.safeParse({
15
+ email: 'admin',
16
+ password: 'admin123',
17
+ });
18
+ expect(result.success).toBe(true);
19
+ });
20
+
21
+ it('rejects empty username/email', () => {
22
+ const result = loginSchema.safeParse({
23
+ email: '',
24
+ password: 'admin123',
25
+ });
26
+ expect(result.success).toBe(false);
27
+ });
28
+
29
+ it('rejects password shorter than 6 characters', () => {
30
+ const result = loginSchema.safeParse({
31
+ email: 'admin',
32
+ password: '12345',
33
+ });
34
+ expect(result.success).toBe(false);
35
+ });
36
+
37
+ it('rejects missing fields', () => {
38
+ const result = loginSchema.safeParse({});
39
+ expect(result.success).toBe(false);
40
+ });
41
+ });
42
+
43
+ describe('registerSchema', () => {
44
+ it('accepts valid registration data', () => {
45
+ const result = registerSchema.safeParse({
46
+ email: 'user@example.com',
47
+ password: 'password123',
48
+ fullName: 'John Doe',
49
+ });
50
+ expect(result.success).toBe(true);
51
+ });
52
+
53
+ it('requires fullName with min 2 characters', () => {
54
+ const result = registerSchema.safeParse({
55
+ email: 'user@example.com',
56
+ password: 'password123',
57
+ fullName: 'J',
58
+ });
59
+ expect(result.success).toBe(false);
60
+ });
61
+
62
+ it('allows optional phone and jobTitle', () => {
63
+ const result = registerSchema.safeParse({
64
+ email: 'user@example.com',
65
+ password: 'password123',
66
+ fullName: 'John Doe',
67
+ phone: '+1234567890',
68
+ jobTitle: 'Engineer',
69
+ });
70
+ expect(result.success).toBe(true);
71
+ });
72
+ });
73
+
74
+ describe('changePasswordSchema', () => {
75
+ it('accepts valid password with all requirements', () => {
76
+ const result = changePasswordSchema.safeParse({
77
+ currentPassword: 'oldPassword',
78
+ newPassword: 'NewP@ss1',
79
+ });
80
+ expect(result.success).toBe(true);
81
+ });
82
+
83
+ it('rejects password without uppercase', () => {
84
+ const result = changePasswordSchema.safeParse({
85
+ currentPassword: 'oldPassword',
86
+ newPassword: 'newp@ss1',
87
+ });
88
+ expect(result.success).toBe(false);
89
+ });
90
+
91
+ it('rejects password without lowercase', () => {
92
+ const result = changePasswordSchema.safeParse({
93
+ currentPassword: 'oldPassword',
94
+ newPassword: 'NEWP@SS1',
95
+ });
96
+ expect(result.success).toBe(false);
97
+ });
98
+
99
+ it('rejects password without number', () => {
100
+ const result = changePasswordSchema.safeParse({
101
+ currentPassword: 'oldPassword',
102
+ newPassword: 'NewP@ssw',
103
+ });
104
+ expect(result.success).toBe(false);
105
+ });
106
+
107
+ it('rejects password without special character', () => {
108
+ const result = changePasswordSchema.safeParse({
109
+ currentPassword: 'oldPassword',
110
+ newPassword: 'NewPass1s',
111
+ });
112
+ expect(result.success).toBe(false);
113
+ });
114
+ });
115
+
116
+ describe('passwordResetSchema', () => {
117
+ it('accepts valid email', () => {
118
+ const result = passwordResetSchema.safeParse({ email: 'user@example.com' });
119
+ expect(result.success).toBe(true);
120
+ });
121
+
122
+ it('rejects invalid email', () => {
123
+ const result = passwordResetSchema.safeParse({ email: 'bad' });
124
+ expect(result.success).toBe(false);
125
+ });
126
+ });
127
+