@omersx/bemms 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +203 -2
  3. package/drizzle.config.ts +11 -0
  4. package/next.config.ts +51 -0
  5. package/package.json +99 -3
  6. package/postcss.config.mjs +7 -0
  7. package/public/hospital_demo_devices.csv +32 -0
  8. package/public/icons/icon-192.png +0 -0
  9. package/public/icons/icon-512.png +0 -0
  10. package/public/icons/icon-maskable-512.png +0 -0
  11. package/public/manifest.webmanifest +31 -0
  12. package/public/sw.js +177 -0
  13. package/src/app/(auth)/layout.tsx +20 -0
  14. package/src/app/(auth)/login/page.tsx +135 -0
  15. package/src/app/(dashboard)/admin/audit-logs/page.tsx +178 -0
  16. package/src/app/(dashboard)/admin/data/page.tsx +37 -0
  17. package/src/app/(dashboard)/admin/departments/[deptId]/edit/page.tsx +206 -0
  18. package/src/app/(dashboard)/admin/departments/[deptId]/page.tsx +71 -0
  19. package/src/app/(dashboard)/admin/departments/new/page.tsx +6 -0
  20. package/src/app/(dashboard)/admin/departments/page.tsx +6 -0
  21. package/src/app/(dashboard)/admin/general/page.tsx +374 -0
  22. package/src/app/(dashboard)/admin/hospitals/[hospitalId]/edit/page.tsx +304 -0
  23. package/src/app/(dashboard)/admin/hospitals/[hospitalId]/page.tsx +94 -0
  24. package/src/app/(dashboard)/admin/hospitals/new/page.tsx +302 -0
  25. package/src/app/(dashboard)/admin/hospitals/page.tsx +6 -0
  26. package/src/app/(dashboard)/admin/layout.tsx +49 -0
  27. package/src/app/(dashboard)/admin/organizations/[orgId]/edit/page.tsx +172 -0
  28. package/src/app/(dashboard)/admin/organizations/[orgId]/page.tsx +82 -0
  29. package/src/app/(dashboard)/admin/organizations/new/page.tsx +150 -0
  30. package/src/app/(dashboard)/admin/organizations/page.tsx +6 -0
  31. package/src/app/(dashboard)/admin/page.tsx +258 -0
  32. package/src/app/(dashboard)/admin/roles/page.tsx +153 -0
  33. package/src/app/(dashboard)/admin/settings/page.tsx +69 -0
  34. package/src/app/(dashboard)/admin/users/[userId]/edit/page.tsx +153 -0
  35. package/src/app/(dashboard)/admin/users/[userId]/page.tsx +317 -0
  36. package/src/app/(dashboard)/admin/users/invite/page.tsx +3 -0
  37. package/src/app/(dashboard)/admin/users/new/page.tsx +5 -0
  38. package/src/app/(dashboard)/admin/users/page.tsx +23 -0
  39. package/src/app/(dashboard)/dashboard/page.tsx +462 -0
  40. package/src/app/(dashboard)/departments/[deptId]/page.tsx +39 -0
  41. package/src/app/(dashboard)/departments/new/page.tsx +330 -0
  42. package/src/app/(dashboard)/departments/page.tsx +52 -0
  43. package/src/app/(dashboard)/devices/[id]/edit/page.tsx +131 -0
  44. package/src/app/(dashboard)/devices/[id]/page.tsx +408 -0
  45. package/src/app/(dashboard)/devices/[id]/qr/page.tsx +54 -0
  46. package/src/app/(dashboard)/devices/new/page.tsx +382 -0
  47. package/src/app/(dashboard)/devices/page.tsx +80 -0
  48. package/src/app/(dashboard)/engineering-team/page.tsx +41 -0
  49. package/src/app/(dashboard)/layout.tsx +43 -0
  50. package/src/app/(dashboard)/maintenance/checklists/page.tsx +57 -0
  51. package/src/app/(dashboard)/maintenance/page.tsx +231 -0
  52. package/src/app/(dashboard)/maintenance/plans/page.tsx +142 -0
  53. package/src/app/(dashboard)/maintenance/tasks/[id]/page.tsx +997 -0
  54. package/src/app/(dashboard)/maintenance/tasks/page.tsx +119 -0
  55. package/src/app/(dashboard)/more/page.tsx +310 -0
  56. package/src/app/(dashboard)/my-department/page.tsx +151 -0
  57. package/src/app/(dashboard)/notifications/notification-list.tsx +103 -0
  58. package/src/app/(dashboard)/notifications/page.tsx +59 -0
  59. package/src/app/(dashboard)/profile/page.tsx +27 -0
  60. package/src/app/(dashboard)/reports/costs/page.tsx +127 -0
  61. package/src/app/(dashboard)/reports/history/page.tsx +105 -0
  62. package/src/app/(dashboard)/reports/inventory/page.tsx +116 -0
  63. package/src/app/(dashboard)/reports/maintenance/page.tsx +136 -0
  64. package/src/app/(dashboard)/reports/page.tsx +120 -0
  65. package/src/app/(dashboard)/reports/tickets/page.tsx +139 -0
  66. package/src/app/(dashboard)/reports/workload/page.tsx +135 -0
  67. package/src/app/(dashboard)/rounds/[id]/page.tsx +197 -0
  68. package/src/app/(dashboard)/rounds/history/page.tsx +82 -0
  69. package/src/app/(dashboard)/rounds/page.tsx +107 -0
  70. package/src/app/(dashboard)/scan/[code]/page.tsx +154 -0
  71. package/src/app/(dashboard)/scan/page.tsx +73 -0
  72. package/src/app/(dashboard)/team/page.tsx +42 -0
  73. package/src/app/(dashboard)/tickets/[id]/page.tsx +465 -0
  74. package/src/app/(dashboard)/tickets/create/page.tsx +722 -0
  75. package/src/app/(dashboard)/tickets/page.tsx +55 -0
  76. package/src/app/(dashboard)/tickets/triage/page.tsx +161 -0
  77. package/src/app/api/admin/data/export/route.ts +316 -0
  78. package/src/app/api/auth/[...nextauth]/route.ts +3 -0
  79. package/src/app/api/health/route.ts +46 -0
  80. package/src/app/api/notifications/sse/route.ts +91 -0
  81. package/src/app/api/notifications/subscribe/route.ts +54 -0
  82. package/src/app/api/notifications/vapid-key/route.ts +7 -0
  83. package/src/app/api/reports/export/route.ts +102 -0
  84. package/src/app/api/tickets/[id]/attachments/route.ts +129 -0
  85. package/src/app/api/tickets/[id]/live-sync/route.ts +52 -0
  86. package/src/app/error.tsx +54 -0
  87. package/src/app/globals.css +89 -0
  88. package/src/app/layout.tsx +59 -0
  89. package/src/app/not-found.tsx +32 -0
  90. package/src/app/page.tsx +5 -0
  91. package/src/components/admin/add-department-user-dialog.tsx +759 -0
  92. package/src/components/admin/add-engineer-dialog.tsx +773 -0
  93. package/src/components/admin/add-user-type-dialog.tsx +89 -0
  94. package/src/components/admin/audit-log-export.tsx +113 -0
  95. package/src/components/admin/audit-log-filters.tsx +255 -0
  96. package/src/components/admin/audit-log-table.tsx +383 -0
  97. package/src/components/admin/data-management/danger-zone.tsx +301 -0
  98. package/src/components/admin/data-management/data-management-tabs.tsx +61 -0
  99. package/src/components/admin/data-management/export-center.tsx +301 -0
  100. package/src/components/admin/data-management/import-wizard.tsx +558 -0
  101. package/src/components/admin/settings-forms/features-form.tsx +115 -0
  102. package/src/components/admin/settings-forms/health-panel.tsx +182 -0
  103. package/src/components/admin/settings-forms/localization-form.tsx +203 -0
  104. package/src/components/admin/settings-forms/retention-form.tsx +164 -0
  105. package/src/components/admin/settings-forms/security-form.tsx +186 -0
  106. package/src/components/admin/system-settings-tabs.tsx +88 -0
  107. package/src/components/admin/user-role-dialog.tsx +293 -0
  108. package/src/components/admin/user-status-dialog.tsx +166 -0
  109. package/src/components/admin/users-page-client.tsx +514 -0
  110. package/src/components/dashboard/action-buttons.tsx +93 -0
  111. package/src/components/dashboard/admin-alerts.tsx +86 -0
  112. package/src/components/dashboard/availability-alerts.tsx +65 -0
  113. package/src/components/dashboard/maintenance-due-list.tsx +87 -0
  114. package/src/components/dashboard/round-widget.tsx +110 -0
  115. package/src/components/dashboard/stat-card.tsx +38 -0
  116. package/src/components/dashboard/ticket-list-compact.tsx +86 -0
  117. package/src/components/departments/department-cards-view.tsx +244 -0
  118. package/src/components/departments/department-detail-view.tsx +467 -0
  119. package/src/components/departments/department-visuals.tsx +190 -0
  120. package/src/components/departments/my-department-view.tsx +333 -0
  121. package/src/components/devices/change-status-dialog.tsx +146 -0
  122. package/src/components/devices/device-action-buttons.tsx +125 -0
  123. package/src/components/devices/device-availability-banner.tsx +59 -0
  124. package/src/components/devices/device-badges.tsx +55 -0
  125. package/src/components/devices/device-documents-tab.tsx +93 -0
  126. package/src/components/devices/device-maintenance-tab.tsx +113 -0
  127. package/src/components/devices/device-mini-qr.tsx +109 -0
  128. package/src/components/devices/device-qr-card.tsx +187 -0
  129. package/src/components/devices/device-signatures-tab.tsx +607 -0
  130. package/src/components/devices/device-tickets-tab.tsx +139 -0
  131. package/src/components/devices/device-timelines.tsx +124 -0
  132. package/src/components/devices/qr-camera-scanner.tsx +167 -0
  133. package/src/components/devices/qr-code-display.tsx +139 -0
  134. package/src/components/devices/quick-add-category-dialog.tsx +135 -0
  135. package/src/components/devices/quick-add-manufacturer-dialog.tsx +212 -0
  136. package/src/components/devices/transfer-device-dialog.tsx +190 -0
  137. package/src/components/layout/header.tsx +80 -0
  138. package/src/components/layout/mobile-nav.tsx +91 -0
  139. package/src/components/layout/more-item-badge.tsx +15 -0
  140. package/src/components/layout/nav-badge.tsx +72 -0
  141. package/src/components/layout/sidebar.tsx +297 -0
  142. package/src/components/maintenance/maintenance-badges.tsx +579 -0
  143. package/src/components/maintenance/maintenance-nav.tsx +107 -0
  144. package/src/components/maintenance/parts-costs-manager.tsx +909 -0
  145. package/src/components/maintenance/signature-card.tsx +413 -0
  146. package/src/components/maintenance/task-action-bar.tsx +955 -0
  147. package/src/components/notifications/notification-bell.tsx +367 -0
  148. package/src/components/notifications/notification-item.tsx +93 -0
  149. package/src/components/notifications/notification-preferences.tsx +182 -0
  150. package/src/components/profile/user-profile-view.tsx +626 -0
  151. package/src/components/providers/nav-counts-provider.tsx +235 -0
  152. package/src/components/providers/session-provider.tsx +7 -0
  153. package/src/components/providers/theme-provider.tsx +124 -0
  154. package/src/components/pwa/install-prompt.tsx +82 -0
  155. package/src/components/pwa/sw-register.tsx +43 -0
  156. package/src/components/reports/export-button.tsx +62 -0
  157. package/src/components/reports/filter-chips.tsx +47 -0
  158. package/src/components/reports/summary-cards.tsx +40 -0
  159. package/src/components/reports/transparency-header.tsx +54 -0
  160. package/src/components/rounds/department-visit-sheet.tsx +199 -0
  161. package/src/components/rounds/mention-textarea.tsx +187 -0
  162. package/src/components/rounds/round-calendar.tsx +233 -0
  163. package/src/components/rounds/round-department-list.tsx +281 -0
  164. package/src/components/rounds/round-history-client.tsx +218 -0
  165. package/src/components/rounds/start-round-card.tsx +184 -0
  166. package/src/components/shared/confirm-dialog.tsx +67 -0
  167. package/src/components/shared/data-table.tsx +104 -0
  168. package/src/components/shared/page-header.tsx +20 -0
  169. package/src/components/shared/sign-out-button.tsx +52 -0
  170. package/src/components/shared/status-badge.tsx +42 -0
  171. package/src/components/shared/table-search.tsx +55 -0
  172. package/src/components/signatures/sign-action-dialog.tsx +253 -0
  173. package/src/components/signatures/signature-history.tsx +121 -0
  174. package/src/components/signatures/signature-status-panel.tsx +164 -0
  175. package/src/components/team/department-coverage.tsx +141 -0
  176. package/src/components/team/edit-engineer-dialog.tsx +242 -0
  177. package/src/components/team/engineer-card.tsx +180 -0
  178. package/src/components/team/engineer-details-dialog.tsx +432 -0
  179. package/src/components/team/schedule-calendar.tsx +204 -0
  180. package/src/components/team/staff-engineer-team-view.tsx +368 -0
  181. package/src/components/team/team-header-stats.tsx +88 -0
  182. package/src/components/team/team-page-client.tsx +216 -0
  183. package/src/components/theme/theme-selector-card.tsx +63 -0
  184. package/src/components/theme/theme-toggle.tsx +65 -0
  185. package/src/components/tickets/staff-status-label.tsx +58 -0
  186. package/src/components/tickets/staff-ticket-card.tsx +109 -0
  187. package/src/components/tickets/staff-ticket-progress-tracker.tsx +405 -0
  188. package/src/components/tickets/staff-tickets-view.tsx +182 -0
  189. package/src/components/tickets/ticket-action-buttons.tsx +873 -0
  190. package/src/components/tickets/ticket-auto-refresh.tsx +197 -0
  191. package/src/components/tickets/ticket-badges.tsx +43 -0
  192. package/src/components/tickets/ticket-card.tsx +197 -0
  193. package/src/components/tickets/ticket-cards-view.tsx +395 -0
  194. package/src/components/tickets/ticket-comment-section.tsx +684 -0
  195. package/src/components/tickets/ticket-dispatch-status.tsx +184 -0
  196. package/src/components/tickets/ticket-list-auto-refresh.tsx +55 -0
  197. package/src/components/tickets/ticket-reporter-profile.tsx +235 -0
  198. package/src/components/tickets/ticket-signatures-panel.tsx +32 -0
  199. package/src/components/tickets/ticket-timeline.tsx +131 -0
  200. package/src/components/tickets/ticket-triage-panel.tsx +124 -0
  201. package/src/components/tickets/tickets-nav.tsx +107 -0
  202. package/src/components/ui/avatar.tsx +43 -0
  203. package/src/components/ui/badge.tsx +34 -0
  204. package/src/components/ui/button.tsx +58 -0
  205. package/src/components/ui/card.tsx +77 -0
  206. package/src/components/ui/checkbox.tsx +31 -0
  207. package/src/components/ui/dialog.tsx +123 -0
  208. package/src/components/ui/dropdown-menu.tsx +126 -0
  209. package/src/components/ui/form.tsx +179 -0
  210. package/src/components/ui/input.tsx +24 -0
  211. package/src/components/ui/label.tsx +27 -0
  212. package/src/components/ui/popover.tsx +34 -0
  213. package/src/components/ui/progress.tsx +36 -0
  214. package/src/components/ui/scroll-area.tsx +47 -0
  215. package/src/components/ui/select.tsx +160 -0
  216. package/src/components/ui/separator.tsx +25 -0
  217. package/src/components/ui/sheet.tsx +131 -0
  218. package/src/components/ui/switch.tsx +30 -0
  219. package/src/components/ui/table.tsx +121 -0
  220. package/src/components/ui/tabs.tsx +62 -0
  221. package/src/components/ui/textarea.tsx +23 -0
  222. package/src/components/ui/tooltip.tsx +27 -0
  223. package/src/lib/actions/__tests__/devices.test.ts +176 -0
  224. package/src/lib/actions/__tests__/signatures.test.ts +303 -0
  225. package/src/lib/actions/__tests__/tickets.test.ts +243 -0
  226. package/src/lib/actions/audit-logs.ts +298 -0
  227. package/src/lib/actions/checklists.ts +429 -0
  228. package/src/lib/actions/dashboard.ts +358 -0
  229. package/src/lib/actions/data-management.ts +723 -0
  230. package/src/lib/actions/departments.ts +369 -0
  231. package/src/lib/actions/device-categories.ts +117 -0
  232. package/src/lib/actions/devices.ts +748 -0
  233. package/src/lib/actions/engineer-rounds.ts +973 -0
  234. package/src/lib/actions/engineers.ts +285 -0
  235. package/src/lib/actions/hospitals.ts +148 -0
  236. package/src/lib/actions/locations.ts +115 -0
  237. package/src/lib/actions/maintenance-plans.ts +316 -0
  238. package/src/lib/actions/maintenance-tasks.ts +415 -0
  239. package/src/lib/actions/manufacturers.ts +132 -0
  240. package/src/lib/actions/nav-counts.ts +144 -0
  241. package/src/lib/actions/notifications.ts +136 -0
  242. package/src/lib/actions/organizations.ts +99 -0
  243. package/src/lib/actions/qr.ts +147 -0
  244. package/src/lib/actions/report-history.ts +77 -0
  245. package/src/lib/actions/reports.ts +192 -0
  246. package/src/lib/actions/roles.ts +32 -0
  247. package/src/lib/actions/signature-auth.ts +377 -0
  248. package/src/lib/actions/signatures.ts +532 -0
  249. package/src/lib/actions/system-settings.ts +274 -0
  250. package/src/lib/actions/tickets.ts +1445 -0
  251. package/src/lib/actions/users.ts +740 -0
  252. package/src/lib/audit/index.ts +103 -0
  253. package/src/lib/auth/__tests__/rbac.test.ts +33 -0
  254. package/src/lib/auth/index.ts +338 -0
  255. package/src/lib/auth/password.ts +10 -0
  256. package/src/lib/auth/rbac.ts +103 -0
  257. package/src/lib/auth/types.ts +57 -0
  258. package/src/lib/constants/departments.ts +16 -0
  259. package/src/lib/constants/manufacturers.ts +32 -0
  260. package/src/lib/db/index.ts +23 -0
  261. package/src/lib/db/schema/audit-logs.ts +42 -0
  262. package/src/lib/db/schema/departments.ts +39 -0
  263. package/src/lib/db/schema/device-categories.ts +38 -0
  264. package/src/lib/db/schema/devices.ts +195 -0
  265. package/src/lib/db/schema/engineer-profiles.ts +63 -0
  266. package/src/lib/db/schema/engineer-rounds.ts +115 -0
  267. package/src/lib/db/schema/enums.ts +148 -0
  268. package/src/lib/db/schema/hospitals.ts +34 -0
  269. package/src/lib/db/schema/index.ts +24 -0
  270. package/src/lib/db/schema/locations.ts +46 -0
  271. package/src/lib/db/schema/maintenance-plans.ts +99 -0
  272. package/src/lib/db/schema/maintenance-records.ts +161 -0
  273. package/src/lib/db/schema/maintenance-tasks.ts +196 -0
  274. package/src/lib/db/schema/manufacturers.ts +60 -0
  275. package/src/lib/db/schema/notifications.ts +48 -0
  276. package/src/lib/db/schema/organizations.ts +24 -0
  277. package/src/lib/db/schema/report-history.ts +28 -0
  278. package/src/lib/db/schema/roles.ts +59 -0
  279. package/src/lib/db/schema/sessions.ts +46 -0
  280. package/src/lib/db/schema/signature-events.ts +48 -0
  281. package/src/lib/db/schema/signature-policies.ts +60 -0
  282. package/src/lib/db/schema/signatures.ts +87 -0
  283. package/src/lib/db/schema/tickets.ts +169 -0
  284. package/src/lib/db/schema/user-assignments.ts +85 -0
  285. package/src/lib/db/schema/users.ts +39 -0
  286. package/src/lib/db/seed.ts +131 -0
  287. package/src/lib/logger/__tests__/logger.test.ts +154 -0
  288. package/src/lib/logger/formatters.ts +159 -0
  289. package/src/lib/logger/index.ts +184 -0
  290. package/src/lib/logger/types.ts +48 -0
  291. package/src/lib/notifications/create.ts +71 -0
  292. package/src/lib/notifications/dispatcher.ts +490 -0
  293. package/src/lib/notifications/index.ts +7 -0
  294. package/src/lib/notifications/push-sender.ts +128 -0
  295. package/src/lib/notifications/push.ts +64 -0
  296. package/src/lib/notifications/triggers.ts +166 -0
  297. package/src/lib/utils/__tests__/crypto.test.ts +30 -0
  298. package/src/lib/utils/__tests__/export.test.ts +55 -0
  299. package/src/lib/utils/__tests__/nav-badge.test.ts +147 -0
  300. package/src/lib/utils/__tests__/staff-status.test.ts +50 -0
  301. package/src/lib/utils/attestations.ts +19 -0
  302. package/src/lib/utils/crypto.ts +18 -0
  303. package/src/lib/utils/csv-parser.ts +181 -0
  304. package/src/lib/utils/export.ts +45 -0
  305. package/src/lib/utils/nav-badge.ts +225 -0
  306. package/src/lib/utils/sound.ts +152 -0
  307. package/src/lib/utils/staff-status.ts +115 -0
  308. package/src/lib/utils.ts +6 -0
  309. package/src/lib/validators/__tests__/auth.test.ts +127 -0
  310. package/src/lib/validators/__tests__/data-management.test.ts +182 -0
  311. package/src/lib/validators/__tests__/devices-profile.test.ts +129 -0
  312. package/src/lib/validators/__tests__/devices.test.ts +98 -0
  313. package/src/lib/validators/__tests__/engineer-rounds.test.ts +170 -0
  314. package/src/lib/validators/__tests__/maintenance.test.ts +178 -0
  315. package/src/lib/validators/__tests__/signatures.test.ts +154 -0
  316. package/src/lib/validators/__tests__/system-settings.test.ts +119 -0
  317. package/src/lib/validators/__tests__/tickets.test.ts +176 -0
  318. package/src/lib/validators/__tests__/users.test.ts +92 -0
  319. package/src/lib/validators/auth.ts +32 -0
  320. package/src/lib/validators/data-management.ts +67 -0
  321. package/src/lib/validators/departments.ts +14 -0
  322. package/src/lib/validators/devices.ts +94 -0
  323. package/src/lib/validators/engineer-rounds.ts +70 -0
  324. package/src/lib/validators/hospitals.ts +17 -0
  325. package/src/lib/validators/locations.ts +15 -0
  326. package/src/lib/validators/maintenance.ts +211 -0
  327. package/src/lib/validators/organizations.ts +11 -0
  328. package/src/lib/validators/reports.ts +31 -0
  329. package/src/lib/validators/signatures.ts +49 -0
  330. package/src/lib/validators/system-settings.ts +55 -0
  331. package/src/lib/validators/tickets.ts +125 -0
  332. package/src/lib/validators/users.ts +43 -0
  333. package/src/middleware.ts +45 -0
  334. package/tailwind.config.ts +76 -0
  335. package/tsconfig.json +28 -0
@@ -0,0 +1,197 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+
6
+ interface TicketAutoRefreshProps {
7
+ ticketId: string;
8
+ initialUpdatedAt?: string | Date | null;
9
+ initialStatusCode?: string;
10
+ initialCommentCount?: number;
11
+ initialAssignedEngineerUserId?: string | null;
12
+ pollIntervalMs?: number;
13
+ }
14
+
15
+ /**
16
+ * Live synchronization component for the ticket detail view.
17
+ *
18
+ * Provides instantaneous updates without full page reload:
19
+ * 1. Listens for window 'bemms:notification' events dispatched by NotificationBell (SSE & Push).
20
+ * 2. Directly connects to SSE as a fallback stream for instant pushes.
21
+ * 3. Fast-polls /api/tickets/[id]/live-sync (default 3.5s) when the tab is visible.
22
+ * 4. Refreshes immediately when the user switches back to this browser tab.
23
+ */
24
+ export function TicketAutoRefresh({
25
+ ticketId,
26
+ initialUpdatedAt,
27
+ initialStatusCode,
28
+ initialCommentCount = 0,
29
+ initialAssignedEngineerUserId,
30
+ pollIntervalMs = 3500,
31
+ }: TicketAutoRefreshProps) {
32
+ const router = useRouter();
33
+
34
+ const lastKnownRef = useRef({
35
+ updatedAt: initialUpdatedAt ? new Date(initialUpdatedAt).toISOString() : '',
36
+ statusCode: initialStatusCode || '',
37
+ commentCount: initialCommentCount,
38
+ assignedEngineerUserId: initialAssignedEngineerUserId || null,
39
+ });
40
+
41
+ const lastRefreshTimestampRef = useRef<number>(Date.now());
42
+ const isRefreshingRef = useRef<boolean>(false);
43
+
44
+ // Sync ref with server props whenever page re-renders
45
+ useEffect(() => {
46
+ lastKnownRef.current = {
47
+ updatedAt: initialUpdatedAt ? new Date(initialUpdatedAt).toISOString() : '',
48
+ statusCode: initialStatusCode || '',
49
+ commentCount: initialCommentCount,
50
+ assignedEngineerUserId: initialAssignedEngineerUserId || null,
51
+ };
52
+ isRefreshingRef.current = false;
53
+ }, [initialUpdatedAt, initialStatusCode, initialCommentCount, initialAssignedEngineerUserId]);
54
+
55
+ // Safe throttled refresh
56
+ const triggerRefresh = () => {
57
+ const now = Date.now();
58
+ if (now - lastRefreshTimestampRef.current < 1500) {
59
+ return;
60
+ }
61
+ lastRefreshTimestampRef.current = now;
62
+ isRefreshingRef.current = true;
63
+ router.refresh();
64
+ };
65
+
66
+ // 1. Listen for local notification events dispatched from NotificationBell
67
+ useEffect(() => {
68
+ const handleNotification = (event: Event) => {
69
+ const customEvent = event as CustomEvent<any[]>;
70
+ const notifications = customEvent.detail;
71
+ if (!Array.isArray(notifications)) return;
72
+
73
+ const isForThisTicket = notifications.some(
74
+ (n) =>
75
+ (n.relatedEntityType === 'service_ticket' && n.relatedEntityId === ticketId) ||
76
+ (n.actionUrl && n.actionUrl.includes(`/tickets/${ticketId}`)) ||
77
+ (n.data && n.data.ticketId === ticketId)
78
+ );
79
+
80
+ if (isForThisTicket) {
81
+ triggerRefresh();
82
+ }
83
+ };
84
+
85
+ window.addEventListener('bemms:notification', handleNotification);
86
+ return () => {
87
+ window.removeEventListener('bemms:notification', handleNotification);
88
+ };
89
+ }, [ticketId]);
90
+
91
+ // 2. Resilient SSE direct listener
92
+ useEffect(() => {
93
+ let eventSource: EventSource | null = null;
94
+ let reconnectTimeout: NodeJS.Timeout;
95
+
96
+ const connectSSE = () => {
97
+ try {
98
+ eventSource = new EventSource('/api/notifications/sse');
99
+
100
+ eventSource.onmessage = (event) => {
101
+ try {
102
+ const data = JSON.parse(event.data);
103
+ if (data.type === 'new_notifications' && Array.isArray(data.notifications)) {
104
+ const matched = data.notifications.some(
105
+ (n: any) =>
106
+ (n.relatedEntityType === 'service_ticket' && n.relatedEntityId === ticketId) ||
107
+ (n.actionUrl && n.actionUrl.includes(`/tickets/${ticketId}`))
108
+ );
109
+ if (matched) {
110
+ triggerRefresh();
111
+ }
112
+ }
113
+ } catch {
114
+ // Ignore parse errors
115
+ }
116
+ };
117
+
118
+ eventSource.onerror = () => {
119
+ eventSource?.close();
120
+ reconnectTimeout = setTimeout(connectSSE, 20000);
121
+ };
122
+ } catch {
123
+ // Ignore SSE connection errors
124
+ }
125
+ };
126
+
127
+ connectSSE();
128
+
129
+ return () => {
130
+ eventSource?.close();
131
+ clearTimeout(reconnectTimeout);
132
+ };
133
+ }, [ticketId]);
134
+
135
+ // 3. Fast live-sync polling (checks updatedAt, statusCode, commentCount)
136
+ useEffect(() => {
137
+ let isCancelled = false;
138
+
139
+ const checkLiveSync = async () => {
140
+ // Don't poll if document is hidden or refresh is pending
141
+ if (document.hidden || isRefreshingRef.current) return;
142
+
143
+ try {
144
+ const res = await fetch(`/api/tickets/${ticketId}/live-sync`, {
145
+ cache: 'no-store',
146
+ headers: { 'Cache-Control': 'no-cache' },
147
+ });
148
+
149
+ if (!res.ok || isCancelled) return;
150
+
151
+ const data = await res.json();
152
+ const current = lastKnownRef.current;
153
+
154
+ const hasChanged =
155
+ (data.updatedAt && current.updatedAt && data.updatedAt !== current.updatedAt) ||
156
+ (data.statusCode && current.statusCode && data.statusCode !== current.statusCode) ||
157
+ (typeof data.commentCount === 'number' && data.commentCount !== current.commentCount) ||
158
+ (data.assignedEngineerUserId !== undefined &&
159
+ data.assignedEngineerUserId !== current.assignedEngineerUserId);
160
+
161
+ if (hasChanged) {
162
+ lastKnownRef.current = {
163
+ updatedAt: data.updatedAt || current.updatedAt,
164
+ statusCode: data.statusCode || current.statusCode,
165
+ commentCount: data.commentCount ?? current.commentCount,
166
+ assignedEngineerUserId:
167
+ data.assignedEngineerUserId !== undefined
168
+ ? data.assignedEngineerUserId
169
+ : current.assignedEngineerUserId,
170
+ };
171
+ triggerRefresh();
172
+ }
173
+ } catch {
174
+ // Silently handle polling network blips
175
+ }
176
+ };
177
+
178
+ const intervalId = setInterval(checkLiveSync, pollIntervalMs);
179
+
180
+ // Refresh immediately when tab gains focus / becomes visible
181
+ const handleVisibilityChange = () => {
182
+ if (!document.hidden) {
183
+ checkLiveSync();
184
+ }
185
+ };
186
+
187
+ document.addEventListener('visibilitychange', handleVisibilityChange);
188
+
189
+ return () => {
190
+ isCancelled = true;
191
+ clearInterval(intervalId);
192
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
193
+ };
194
+ }, [ticketId, pollIntervalMs]);
195
+
196
+ return null;
197
+ }
@@ -0,0 +1,43 @@
1
+ import { cn } from "@/lib/utils"
2
+ import { Badge } from "@/components/ui/badge"
3
+
4
+ const statusConfig: Record<string, { label: string; className: string }> = {
5
+ new: { label: "New", className: "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-950 dark:text-blue-300 dark:border-blue-800" },
6
+ acknowledged: { label: "Acknowledged", className: "bg-sky-100 text-sky-800 border-sky-200 dark:bg-sky-950 dark:text-sky-300 dark:border-sky-800" },
7
+ in_triage: { label: "Under Assessment", className: "bg-indigo-100 text-indigo-800 border-indigo-200 dark:bg-indigo-950 dark:text-indigo-300 dark:border-indigo-800" },
8
+ in_progress: { label: "In Progress", className: "bg-violet-100 text-violet-800 border-violet-200 dark:bg-violet-950 dark:text-violet-300 dark:border-violet-800" },
9
+ waiting_requester: { label: "Info Needed", className: "bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-800" },
10
+ waiting_parts_vendor: { label: "Waiting Parts", className: "bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-950 dark:text-yellow-300 dark:border-yellow-800" },
11
+ resolved: { label: "Resolved", className: "bg-green-100 text-green-800 border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-800" },
12
+ closed: { label: "Closed", className: "bg-slate-100 text-slate-600 border-slate-200 dark:bg-slate-950 dark:text-slate-300 dark:border-slate-800" },
13
+ cancelled: { label: "Cancelled", className: "bg-gray-100 text-gray-500 border-gray-200 line-through dark:bg-gray-950 dark:text-gray-400 dark:border-gray-800" },
14
+ }
15
+
16
+ const priorityConfig: Record<string, { label: string; className: string }> = {
17
+ p1_critical: { label: "P1 Critical", className: "bg-red-100 text-red-800 border-red-300 font-bold dark:bg-red-950 dark:text-red-300 dark:border-red-800" },
18
+ p2_high: { label: "P2 High", className: "bg-orange-100 text-orange-800 border-orange-200 dark:bg-orange-950 dark:text-orange-300 dark:border-orange-800" },
19
+ p3_normal: { label: "P3 Normal", className: "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950 dark:text-blue-300 dark:border-blue-800" },
20
+ p4_low: { label: "P4 Low", className: "bg-gray-50 text-gray-600 border-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-700" },
21
+ }
22
+
23
+ const impactConfig: Record<string, { label: string; className: string }> = {
24
+ device_usable: { label: "Device Usable", className: "bg-green-50 text-green-700 border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-800" },
25
+ device_not_usable: { label: "Device Not Usable", className: "bg-orange-50 text-orange-700 border-orange-200 dark:bg-orange-950 dark:text-orange-300 dark:border-orange-800" },
26
+ patient_care_affected: { label: "Patient Care Affected", className: "bg-red-50 text-red-800 border-red-300 font-bold dark:bg-red-950 dark:text-red-300 dark:border-red-800" },
27
+ }
28
+
29
+ export function TicketStatusBadge({ status }: { status?: string | null }) {
30
+ const config = statusConfig[status || ''] || { label: status || 'Unknown', className: 'bg-muted text-muted-foreground' }
31
+ return <Badge variant="outline" className={cn("font-medium", config.className)}>{config.label}</Badge>
32
+ }
33
+
34
+ export function TicketPriorityBadge({ priority }: { priority?: string | null }) {
35
+ const config = priorityConfig[priority || ''] || { label: priority || 'N/A', className: 'bg-muted text-muted-foreground' }
36
+ return <Badge variant="outline" className={cn("font-medium text-xs", config.className)}>{config.label}</Badge>
37
+ }
38
+
39
+ export function TicketImpactBadge({ impact }: { impact?: string | null }) {
40
+ const config = impactConfig[impact || ''] || { label: impact || 'N/A', className: 'bg-muted text-muted-foreground' }
41
+ return <Badge variant="outline" className={cn("font-medium text-xs", config.className)}>{config.label}</Badge>
42
+ }
43
+
@@ -0,0 +1,197 @@
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+ import { cn } from "@/lib/utils"
5
+ import { TicketPriorityBadge, TicketStatusBadge } from "./ticket-badges"
6
+ import { WaitingForEngineerBanner } from "./ticket-dispatch-status"
7
+ import { Monitor, MapPin, User, Calendar, Package } from "lucide-react"
8
+
9
+ export interface TicketItem {
10
+ id: string
11
+ ticketNumber: string
12
+ title: string
13
+ description?: string | null
14
+ statusCode: string
15
+ priorityCode: string
16
+ reportedImpact?: string | null
17
+ waitingReason?: string | null
18
+ waitingDependencyType?: string | null
19
+ reportedAt?: Date | string | null
20
+ createdAt?: Date | string | null
21
+ device?: {
22
+ id?: string
23
+ name: string
24
+ assetNumber?: string | null
25
+ currentStatusCode?: string | null
26
+ } | null
27
+ hospital?: {
28
+ id?: string
29
+ name: string
30
+ } | null
31
+ department?: {
32
+ id?: string
33
+ name: string
34
+ } | null
35
+ reportedByUser?: {
36
+ id?: string
37
+ fullName?: string | null
38
+ } | null
39
+ assignedEngineer?: {
40
+ id?: string
41
+ fullName?: string | null
42
+ } | null
43
+ }
44
+
45
+ export function getTicketStatusStyle(statusCode: string) {
46
+ switch (statusCode) {
47
+ case "new":
48
+ case "acknowledged":
49
+ case "in_triage":
50
+ return {
51
+ rail: "border-l-rose-500",
52
+ bg: "bg-rose-50/35 dark:bg-rose-950/20 hover:bg-rose-50/70 dark:hover:bg-rose-950/30",
53
+ border: "border-rose-200/80 dark:border-rose-900/40",
54
+ label: "Attention Needed",
55
+ dot: "bg-rose-500",
56
+ }
57
+ case "in_progress":
58
+ return {
59
+ rail: "border-l-amber-500",
60
+ bg: "bg-amber-50/35 dark:bg-amber-950/20 hover:bg-amber-50/70 dark:hover:bg-amber-950/30",
61
+ border: "border-amber-200/80 dark:border-amber-900/40",
62
+ label: "In Progress",
63
+ dot: "bg-amber-500",
64
+ }
65
+ case "waiting_parts_vendor":
66
+ case "waiting_requester":
67
+ return {
68
+ rail: "border-l-orange-500",
69
+ bg: "bg-orange-50/35 dark:bg-orange-950/20 hover:bg-orange-50/70 dark:hover:bg-orange-950/30",
70
+ border: "border-orange-200/80 dark:border-orange-900/40",
71
+ label: "Waiting for Parts",
72
+ dot: "bg-orange-500",
73
+ }
74
+ case "resolved":
75
+ return {
76
+ rail: "border-l-emerald-500",
77
+ bg: "bg-emerald-50/35 dark:bg-emerald-950/20 hover:bg-emerald-50/70 dark:hover:bg-emerald-950/30",
78
+ border: "border-emerald-200/80 dark:border-emerald-900/40",
79
+ label: "Resolved",
80
+ dot: "bg-emerald-500",
81
+ }
82
+ case "closed":
83
+ return {
84
+ rail: "border-l-slate-400 dark:border-l-slate-600",
85
+ bg: "bg-slate-50/50 dark:bg-slate-900/20 hover:bg-slate-100/50 dark:hover:bg-slate-900/40",
86
+ border: "border-slate-200/80 dark:border-slate-800/40",
87
+ label: "Closed",
88
+ dot: "bg-slate-400",
89
+ }
90
+ case "cancelled":
91
+ default:
92
+ return {
93
+ rail: "border-l-zinc-300 dark:border-l-zinc-700",
94
+ bg: "bg-zinc-50/30 dark:bg-zinc-900/10 hover:bg-zinc-100/40 dark:hover:bg-zinc-900/30",
95
+ border: "border-zinc-200/80 dark:border-zinc-800/40",
96
+ label: "Cancelled",
97
+ dot: "bg-zinc-400",
98
+ }
99
+ }
100
+ }
101
+
102
+ export function TicketCard({ ticket }: { ticket: TicketItem }) {
103
+ const style = getTicketStatusStyle(ticket.statusCode)
104
+
105
+ return (
106
+ <Link
107
+ href={`/tickets/${ticket.id}`}
108
+ className={cn(
109
+ "group relative flex flex-col justify-between rounded-lg border border-l-4 transition-all duration-200 p-4 shadow-sm hover:shadow-md",
110
+ style.rail,
111
+ style.bg,
112
+ style.border
113
+ )}
114
+ >
115
+ <div>
116
+ {/* Top Header: Ticket Number & Badges */}
117
+ <div className="flex items-center justify-between gap-2 mb-2">
118
+ <span className="font-mono text-xs font-bold text-primary group-hover:underline">
119
+ {ticket.ticketNumber}
120
+ </span>
121
+ <div className="flex items-center gap-1.5 flex-wrap">
122
+ <TicketPriorityBadge priority={ticket.priorityCode} />
123
+ <TicketStatusBadge status={ticket.statusCode} />
124
+ </div>
125
+ </div>
126
+
127
+ {/* Title */}
128
+ <h3 className="font-semibold text-base text-foreground group-hover:text-primary transition-colors line-clamp-1 mb-2.5">
129
+ {ticket.title}
130
+ </h3>
131
+
132
+ {/* Device & Location Meta */}
133
+ <div className="space-y-1.5 text-xs text-muted-foreground mb-3">
134
+ <div className="flex items-center gap-1.5 truncate">
135
+ <Monitor className="w-3.5 h-3.5 shrink-0 text-slate-500" />
136
+ <span className="font-medium text-foreground truncate">
137
+ {ticket.device?.name || "No linked device"}
138
+ </span>
139
+ {ticket.device?.assetNumber && (
140
+ <span className="font-mono text-[11px] text-muted-foreground shrink-0">
141
+ ({ticket.device.assetNumber})
142
+ </span>
143
+ )}
144
+ </div>
145
+
146
+ {(ticket.hospital?.name || ticket.department?.name) && (
147
+ <div className="flex items-center gap-1.5 truncate">
148
+ <MapPin className="w-3.5 h-3.5 shrink-0 text-slate-500" />
149
+ <span className="truncate">
150
+ {ticket.hospital?.name ? `${ticket.hospital.name} / ` : ""}
151
+ {ticket.department?.name || "Unassigned Dept"}
152
+ </span>
153
+ </div>
154
+ )}
155
+ </div>
156
+
157
+ {/* Waiting for Spare Parts Pill */}
158
+ {ticket.statusCode === "waiting_parts_vendor" && ticket.waitingReason && (
159
+ <div className="mb-3 rounded bg-orange-100/80 dark:bg-orange-950/60 px-2.5 py-1.5 text-xs text-orange-950 dark:text-orange-200 border border-orange-200 dark:border-orange-800/60 flex items-center gap-1.5">
160
+ <Package className="w-3.5 h-3.5 shrink-0 text-orange-600 dark:text-orange-400" />
161
+ <span className="truncate font-medium">On Order: {ticket.waitingReason}</span>
162
+ </div>
163
+ )}
164
+ </div>
165
+
166
+ {/* Footer: Assignee & Date */}
167
+ <div className="flex items-center justify-between text-xs text-muted-foreground pt-3 border-t border-border/50 mt-1">
168
+ <div className="flex items-center gap-1.5 truncate">
169
+ <User className="w-3.5 h-3.5 shrink-0 text-slate-400" />
170
+ <span className="truncate">
171
+ {ticket.assignedEngineer?.fullName ? (
172
+ <span className="text-foreground font-medium">
173
+ {ticket.assignedEngineer.fullName}
174
+ </span>
175
+ ) : ["new", "acknowledged", "in_triage"].includes(ticket.statusCode) ? (
176
+ <WaitingForEngineerBanner
177
+ compact
178
+ reportedAt={ticket.reportedAt || ticket.createdAt || new Date()}
179
+ reportedImpact={ticket.reportedImpact || undefined}
180
+ />
181
+ ) : (
182
+ <span className="text-rose-600 dark:text-rose-400 font-semibold">
183
+ Unassigned
184
+ </span>
185
+ )}
186
+ </span>
187
+ </div>
188
+ <div className="flex items-center gap-1 text-[11px] shrink-0">
189
+ <Calendar className="w-3 h-3 text-slate-400" />
190
+ <span>
191
+ {new Date(ticket.reportedAt || ticket.createdAt || Date.now()).toLocaleDateString()}
192
+ </span>
193
+ </div>
194
+ </div>
195
+ </Link>
196
+ )
197
+ }