@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,184 @@
1
+ 'use client';
2
+
3
+ import { Clock, UserCheck, Zap } from 'lucide-react';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ interface WaitingForEngineerBannerProps {
7
+ /** When the ticket was reported */
8
+ reportedAt: Date | string;
9
+ /** The ticket's reported impact level */
10
+ reportedImpact?: string;
11
+ /** Whether to show in compact mode (for card lists) */
12
+ compact?: boolean;
13
+ className?: string;
14
+ children?: React.ReactNode;
15
+ }
16
+
17
+ function getWaitingDuration(reportedAt: Date | string): string {
18
+ const now = Date.now();
19
+ const reported = new Date(reportedAt).getTime();
20
+ const diffMs = now - reported;
21
+ const minutes = Math.floor(diffMs / (1000 * 60));
22
+ const hours = Math.floor(minutes / 60);
23
+ const days = Math.floor(hours / 24);
24
+
25
+ if (days > 0) return `${days}d ${hours % 24}h`;
26
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
27
+ if (minutes > 0) return `${minutes}m`;
28
+ return 'just now';
29
+ }
30
+
31
+ /**
32
+ * Visual indicator that a ticket is waiting for an engineer to accept it.
33
+ * Used on ticket cards and ticket detail pages.
34
+ */
35
+ export function WaitingForEngineerBanner({
36
+ reportedAt,
37
+ reportedImpact,
38
+ compact = false,
39
+ className,
40
+ children,
41
+ }: WaitingForEngineerBannerProps) {
42
+ const waitDuration = getWaitingDuration(reportedAt);
43
+ const isCritical = reportedImpact === 'patient_care_affected';
44
+ const isUrgent = reportedImpact === 'device_not_usable' || isCritical;
45
+
46
+ if (compact) {
47
+ return (
48
+ <div
49
+ className={cn(
50
+ 'flex items-center gap-1.5 text-xs font-medium',
51
+ isCritical
52
+ ? 'text-red-600 dark:text-red-400'
53
+ : isUrgent
54
+ ? 'text-amber-600 dark:text-amber-400'
55
+ : 'text-blue-600 dark:text-blue-400',
56
+ className,
57
+ )}
58
+ >
59
+ <Clock className="w-3 h-3 animate-pulse" />
60
+ <span>Waiting for engineer · {waitDuration}</span>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ return (
66
+ <div
67
+ className={cn(
68
+ 'rounded-lg border p-4',
69
+ isCritical
70
+ ? 'bg-red-50 border-red-300 dark:bg-red-950/30 dark:border-red-800'
71
+ : isUrgent
72
+ ? 'bg-amber-50 border-amber-300 dark:bg-amber-950/30 dark:border-amber-800'
73
+ : 'bg-blue-50 border-blue-200 dark:bg-blue-950/30 dark:border-blue-800',
74
+ className,
75
+ )}
76
+ >
77
+ <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
78
+ <div className="flex items-start gap-3 flex-1 min-w-0">
79
+ <div
80
+ className={cn(
81
+ 'p-2 rounded-lg shrink-0 mt-0.5 sm:mt-0',
82
+ isCritical
83
+ ? 'bg-red-100 dark:bg-red-900/50'
84
+ : isUrgent
85
+ ? 'bg-amber-100 dark:bg-amber-900/50'
86
+ : 'bg-blue-100 dark:bg-blue-900/50',
87
+ )}
88
+ >
89
+ {isCritical ? (
90
+ <Zap className="w-5 h-5 text-red-600 dark:text-red-400" />
91
+ ) : (
92
+ <Clock className="w-5 h-5 text-amber-600 dark:text-amber-400 animate-pulse" />
93
+ )}
94
+ </div>
95
+ <div className="flex-1 min-w-0">
96
+ <h4
97
+ className={cn(
98
+ 'font-semibold text-sm',
99
+ isCritical
100
+ ? 'text-red-800 dark:text-red-200'
101
+ : isUrgent
102
+ ? 'text-amber-800 dark:text-amber-200'
103
+ : 'text-blue-800 dark:text-blue-200',
104
+ )}
105
+ >
106
+ {isCritical
107
+ ? '🚨 URGENT — Waiting for engineer to accept'
108
+ : '⏳ Waiting for engineer to accept'}
109
+ </h4>
110
+ <p
111
+ className={cn(
112
+ 'text-xs mt-1',
113
+ isCritical
114
+ ? 'text-red-700 dark:text-red-300'
115
+ : isUrgent
116
+ ? 'text-amber-700 dark:text-amber-300'
117
+ : 'text-blue-700 dark:text-blue-300',
118
+ )}
119
+ >
120
+ No engineer has accepted this ticket yet. Waiting for {waitDuration}.
121
+ {isCritical && ' This ticket affects patient care and requires immediate attention.'}
122
+ </p>
123
+ </div>
124
+ </div>
125
+ {children && (
126
+ <div className="shrink-0 self-end sm:self-center pl-10 sm:pl-0">
127
+ {children}
128
+ </div>
129
+ )}
130
+ </div>
131
+ </div>
132
+ );
133
+ }
134
+
135
+ // ── Accepted by Engineer Banner ──────────────────────────────────────────────────
136
+
137
+ interface AcceptedByEngineerBannerProps {
138
+ engineerName: string;
139
+ acceptedAt: Date | string;
140
+ compact?: boolean;
141
+ className?: string;
142
+ }
143
+
144
+ /**
145
+ * Shows that a ticket has been accepted by an engineer.
146
+ * Used on the staff ticket view to reassure the reporter.
147
+ */
148
+ export function AcceptedByEngineerBanner({
149
+ engineerName,
150
+ acceptedAt,
151
+ compact = false,
152
+ className,
153
+ }: AcceptedByEngineerBannerProps) {
154
+ const acceptedDate = new Date(acceptedAt);
155
+ const timeStr = acceptedDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
156
+ const dateStr = acceptedDate.toLocaleDateString([], { month: 'short', day: 'numeric' });
157
+
158
+ if (compact) {
159
+ return (
160
+ <div className={cn('flex items-center gap-1.5 text-xs font-medium text-green-600 dark:text-green-400', className)}>
161
+ <UserCheck className="w-3 h-3" />
162
+ <span>Accepted by {engineerName}</span>
163
+ </div>
164
+ );
165
+ }
166
+
167
+ return (
168
+ <div className={cn('rounded-lg border bg-green-50 border-green-200 dark:bg-green-950/30 dark:border-green-800 p-4', className)}>
169
+ <div className="flex items-start gap-3">
170
+ <div className="p-2 rounded-lg bg-green-100 dark:bg-green-900/50 shrink-0">
171
+ <UserCheck className="w-5 h-5 text-green-600 dark:text-green-400" />
172
+ </div>
173
+ <div className="flex-1 min-w-0">
174
+ <h4 className="font-semibold text-sm text-green-800 dark:text-green-200">
175
+ ✅ Engineer {engineerName} accepted this ticket
176
+ </h4>
177
+ <p className="text-xs text-green-700 dark:text-green-300 mt-1">
178
+ Accepted on {dateStr} at {timeStr}. Your reported issue is being investigated.
179
+ </p>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ );
184
+ }
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+
6
+ /**
7
+ * Auto-refreshes the tickets list view when new ticket notifications arrive,
8
+ * or periodically when the tab is visible.
9
+ */
10
+ export function TicketListAutoRefresh({ pollIntervalMs = 10000 }: { pollIntervalMs?: number }) {
11
+ const router = useRouter();
12
+ const lastRefreshRef = useRef(Date.now());
13
+
14
+ const triggerRefresh = () => {
15
+ const now = Date.now();
16
+ if (now - lastRefreshRef.current < 3000) return;
17
+ lastRefreshRef.current = now;
18
+ router.refresh();
19
+ };
20
+
21
+ useEffect(() => {
22
+ // 1. Listen for global notification event from NotificationBell
23
+ const handleNotification = (event: Event) => {
24
+ const customEvent = event as CustomEvent<any[]>;
25
+ const notifs = customEvent.detail;
26
+ if (!Array.isArray(notifs)) return;
27
+
28
+ const hasTicketEvent = notifs.some(
29
+ (n) =>
30
+ n.relatedEntityType === 'service_ticket' ||
31
+ (n.notificationType && n.notificationType.startsWith('ticket_'))
32
+ );
33
+
34
+ if (hasTicketEvent) {
35
+ triggerRefresh();
36
+ }
37
+ };
38
+
39
+ window.addEventListener('bemms:notification', handleNotification);
40
+
41
+ // 2. Periodic refresh when tab is visible
42
+ const interval = setInterval(() => {
43
+ if (!document.hidden) {
44
+ triggerRefresh();
45
+ }
46
+ }, pollIntervalMs);
47
+
48
+ return () => {
49
+ window.removeEventListener('bemms:notification', handleNotification);
50
+ clearInterval(interval);
51
+ };
52
+ }, [router, pollIntervalMs]);
53
+
54
+ return null;
55
+ }
@@ -0,0 +1,235 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Avatar, AvatarFallback } from '@/components/ui/avatar';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Badge } from '@/components/ui/badge';
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ DialogTrigger,
13
+ } from '@/components/ui/dialog';
14
+ import {
15
+ User,
16
+ Phone,
17
+ Mail,
18
+ Building2,
19
+ Stethoscope,
20
+ IdCard,
21
+ MapPin,
22
+ ExternalLink,
23
+ ShieldCheck,
24
+ } from 'lucide-react';
25
+
26
+ interface ReporterInfo {
27
+ id?: string;
28
+ fullName?: string | null;
29
+ email?: string | null;
30
+ phone?: string | null;
31
+ jobTitle?: string | null;
32
+ employeeIdentifier?: string | null;
33
+ departmentName?: string | null;
34
+ hospitalName?: string | null;
35
+ reportedLocationCorrection?: string | null;
36
+ requesterContactName?: string | null;
37
+ requesterContactPhone?: string | null;
38
+ }
39
+
40
+ interface TicketReporterProfileProps {
41
+ reporter: ReporterInfo;
42
+ isBiomedOrAdmin?: boolean;
43
+ }
44
+
45
+ export function TicketReporterProfile({
46
+ reporter,
47
+ isBiomedOrAdmin = false,
48
+ }: TicketReporterProfileProps) {
49
+ const [open, setOpen] = useState(false);
50
+
51
+ const name = reporter.fullName || reporter.requesterContactName || 'Unknown Reporter';
52
+ const jobTitle = reporter.jobTitle || 'Department Clinical Staff';
53
+ const phone = reporter.requesterContactPhone || reporter.phone;
54
+ const extension = reporter.employeeIdentifier;
55
+ const department = reporter.departmentName || 'Hospital Department';
56
+ const hospital = reporter.hospitalName || 'Hospital';
57
+
58
+ const initials = name
59
+ .split(' ')
60
+ .map((n) => n[0])
61
+ .join('')
62
+ .substring(0, 2)
63
+ .toUpperCase();
64
+
65
+ return (
66
+ <div className="space-y-3">
67
+ {/* ── Reporter Summary Card in Sidebar ──────────────────────────── */}
68
+ <div className="p-3.5 rounded-xl border bg-muted/20 space-y-3">
69
+ <div className="flex items-start gap-3">
70
+ <Avatar className="h-10 w-10 shrink-0 border">
71
+ <AvatarFallback className="bg-primary/10 text-primary font-bold text-xs">
72
+ {initials}
73
+ </AvatarFallback>
74
+ </Avatar>
75
+ <div className="min-w-0 flex-1">
76
+ <p className="font-bold text-sm text-foreground truncate">{name}</p>
77
+ <p className="text-xs text-muted-foreground flex items-center gap-1 mt-0.5 truncate">
78
+ <Stethoscope className="w-3 h-3 text-primary shrink-0" />
79
+ <span>{jobTitle}</span>
80
+ </p>
81
+ <p className="text-[11px] text-muted-foreground truncate mt-0.5">
82
+ {department}
83
+ </p>
84
+ </div>
85
+ </div>
86
+
87
+ {/* Quick Call & Email actions */}
88
+ <div className="space-y-1.5 pt-2 border-t text-xs">
89
+ {phone && (
90
+ <a
91
+ href={`tel:${phone}`}
92
+ className="flex items-center gap-2 p-1 rounded-md hover:bg-muted text-primary font-medium transition"
93
+ >
94
+ <Phone className="w-3.5 h-3.5 text-green-600 shrink-0" />
95
+ <span className="truncate">{phone}</span>
96
+ </a>
97
+ )}
98
+
99
+ {extension && (
100
+ <div className="flex items-center gap-2 p-1 text-muted-foreground">
101
+ <IdCard className="w-3.5 h-3.5 shrink-0" />
102
+ <span className="truncate">{extension}</span>
103
+ </div>
104
+ )}
105
+
106
+ {reporter.email && (
107
+ <a
108
+ href={`mailto:${reporter.email}`}
109
+ className="flex items-center gap-2 p-1 rounded-md hover:bg-muted text-primary font-medium transition"
110
+ >
111
+ <Mail className="w-3.5 h-3.5 text-blue-600 shrink-0" />
112
+ <span className="truncate">{reporter.email}</span>
113
+ </a>
114
+ )}
115
+ </div>
116
+
117
+ {/* Dialog Trigger to view full reporter details */}
118
+ <Dialog open={open} onOpenChange={setOpen}>
119
+ <DialogTrigger asChild>
120
+ <Button
121
+ variant="outline"
122
+ size="sm"
123
+ className="w-full text-xs h-8 gap-1.5 bg-card mt-1"
124
+ >
125
+ <User className="w-3.5 h-3.5" />
126
+ View Full Reporter Info
127
+ </Button>
128
+ </DialogTrigger>
129
+
130
+ <DialogContent className="max-w-md">
131
+ <DialogHeader>
132
+ <DialogTitle className="flex items-center gap-2 text-base">
133
+ <User className="w-4 h-4 text-primary" />
134
+ Department Reporter Information
135
+ </DialogTitle>
136
+ </DialogHeader>
137
+
138
+ <div className="space-y-4 pt-2">
139
+ {/* Header profile */}
140
+ <div className="flex items-center gap-3.5 p-3 rounded-xl bg-muted/30 border">
141
+ <Avatar className="h-12 w-12 border shadow-sm">
142
+ <AvatarFallback className="bg-primary text-primary-foreground font-bold text-base">
143
+ {initials}
144
+ </AvatarFallback>
145
+ </Avatar>
146
+ <div className="min-w-0 flex-1">
147
+ <h4 className="font-bold text-base text-foreground">{name}</h4>
148
+ <p className="text-xs text-muted-foreground flex items-center gap-1 font-medium mt-0.5">
149
+ <Stethoscope className="w-3.5 h-3.5 text-primary" />
150
+ {jobTitle}
151
+ </p>
152
+ </div>
153
+ </div>
154
+
155
+ {/* Detail fields */}
156
+ <div className="grid grid-cols-2 gap-3 text-xs">
157
+ <div className="p-3 rounded-xl border bg-card space-y-1">
158
+ <span className="text-[10px] text-muted-foreground font-semibold uppercase">
159
+ Department
160
+ </span>
161
+ <p className="font-semibold text-foreground">{department}</p>
162
+ </div>
163
+
164
+ <div className="p-3 rounded-xl border bg-card space-y-1">
165
+ <span className="text-[10px] text-muted-foreground font-semibold uppercase">
166
+ Hospital
167
+ </span>
168
+ <p className="font-semibold text-foreground">{hospital}</p>
169
+ </div>
170
+
171
+ <div className="p-3 rounded-xl border bg-card space-y-1">
172
+ <span className="text-[10px] text-muted-foreground font-semibold uppercase">
173
+ Contact Phone
174
+ </span>
175
+ <p className="font-medium text-foreground">
176
+ {phone ? (
177
+ <a href={`tel:${phone}`} className="text-primary hover:underline">
178
+ {phone}
179
+ </a>
180
+ ) : (
181
+ 'Not provided'
182
+ )}
183
+ </p>
184
+ </div>
185
+
186
+ <div className="p-3 rounded-xl border bg-card space-y-1">
187
+ <span className="text-[10px] text-muted-foreground font-semibold uppercase">
188
+ Hospital Extension / Ward
189
+ </span>
190
+ <p className="font-medium text-foreground">{extension || 'Not provided'}</p>
191
+ </div>
192
+ </div>
193
+
194
+ {reporter.email && (
195
+ <div className="p-3 rounded-xl border bg-card text-xs space-y-1">
196
+ <span className="text-[10px] text-muted-foreground font-semibold uppercase">
197
+ Official Email
198
+ </span>
199
+ <p className="font-medium text-foreground">
200
+ <a href={`mailto:${reporter.email}`} className="text-primary hover:underline">
201
+ {reporter.email}
202
+ </a>
203
+ </p>
204
+ </div>
205
+ )}
206
+
207
+ {reporter.reportedLocationCorrection && (
208
+ <div className="p-3 rounded-xl border bg-amber-50/50 dark:bg-amber-950/20 border-amber-200 dark:border-amber-800 text-xs space-y-1">
209
+ <span className="text-[10px] text-amber-800 dark:text-amber-300 font-semibold uppercase flex items-center gap-1">
210
+ <MapPin className="w-3 h-3" /> Reported Room / Bed Location
211
+ </span>
212
+ <p className="font-medium text-amber-950 dark:text-amber-100">
213
+ {reporter.reportedLocationCorrection}
214
+ </p>
215
+ </div>
216
+ )}
217
+
218
+ {/* Direct call action */}
219
+ {phone && (
220
+ <div className="pt-2">
221
+ <Button asChild className="w-full gap-2">
222
+ <a href={`tel:${phone}`}>
223
+ <Phone className="w-4 h-4" />
224
+ Call Reporter Now ({phone})
225
+ </a>
226
+ </Button>
227
+ </div>
228
+ )}
229
+ </div>
230
+ </DialogContent>
231
+ </Dialog>
232
+ </div>
233
+ </div>
234
+ );
235
+ }
@@ -0,0 +1,32 @@
1
+ "use client"
2
+
3
+ import { SignatureTimeline } from "@/components/maintenance/signature-card"
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
5
+ import { ShieldCheck } from "lucide-react"
6
+
7
+ interface TicketSignaturesPanelProps {
8
+ signatures: any[]
9
+ }
10
+
11
+ export function TicketSignaturesPanel({ signatures = [] }: TicketSignaturesPanelProps) {
12
+ if (!signatures || signatures.length === 0) {
13
+ return null
14
+ }
15
+
16
+ return (
17
+ <Card className="border-indigo-100 dark:border-indigo-950/60 bg-gradient-to-b from-indigo-50/20 to-transparent">
18
+ <CardHeader className="pb-3 border-b border-border/50">
19
+ <div className="flex items-center gap-2">
20
+ <ShieldCheck className="w-5 h-5 text-indigo-600 dark:text-indigo-400" />
21
+ <CardTitle className="text-base font-semibold">
22
+ Electronic Signatures ({signatures.length})
23
+ </CardTitle>
24
+ </div>
25
+ </CardHeader>
26
+ <CardContent className="pt-4">
27
+ <SignatureTimeline signatures={signatures} />
28
+ </CardContent>
29
+ </Card>
30
+ )
31
+ }
32
+
@@ -0,0 +1,131 @@
1
+ import { TicketStatusBadge, TicketPriorityBadge } from "./ticket-badges"
2
+ import { DeviceStatusBadge } from "@/components/devices/device-badges"
3
+ import { User, Calendar } from "lucide-react"
4
+
5
+ const eventTypeLabels: Record<string, string> = {
6
+ created: "Ticket Created",
7
+ acknowledged: "Acknowledged",
8
+ triaged: "Triaged",
9
+ assigned: "Assigned to Engineer",
10
+ reassigned: "Reassigned",
11
+ accepted: "Accepted by Engineer",
12
+ status_changed: "Status Changed",
13
+ priority_changed: "Priority Changed",
14
+ waiting_for_info: "Waiting for Information",
15
+ info_received: "Information Received",
16
+ maintenance_task_linked: "Maintenance Task Linked",
17
+ resolved: "Resolved",
18
+ closed: "Closed",
19
+ cancelled: "Cancelled",
20
+ reopened: "Reopened",
21
+ }
22
+
23
+ function getEventDotClass(eventType: string): string {
24
+ switch (eventType) {
25
+ case 'status_changed':
26
+ // Amber/Orange for status updates
27
+ return 'bg-amber-500 ring-4 ring-amber-100 dark:ring-amber-950/60'
28
+ case 'waiting_for_info':
29
+ return 'bg-yellow-500 ring-4 ring-yellow-100 dark:ring-yellow-950/60'
30
+ case 'reopened':
31
+ return 'bg-orange-500 ring-4 ring-orange-100 dark:ring-orange-950/60'
32
+ case 'resolved':
33
+ return 'bg-emerald-500 ring-4 ring-emerald-100 dark:ring-emerald-950/60'
34
+ case 'closed':
35
+ return 'bg-slate-600 ring-4 ring-slate-100 dark:ring-slate-800'
36
+ case 'cancelled':
37
+ return 'bg-rose-400 ring-4 ring-rose-100 dark:ring-rose-950/60'
38
+ case 'created':
39
+ return 'bg-blue-500 ring-4 ring-blue-100 dark:ring-blue-950/60'
40
+ case 'acknowledged':
41
+ case 'triaged':
42
+ return 'bg-sky-500 ring-4 ring-sky-100 dark:ring-sky-950/60'
43
+ case 'assigned':
44
+ case 'reassigned':
45
+ return 'bg-violet-500 ring-4 ring-violet-100 dark:ring-violet-950/60'
46
+ case 'accepted':
47
+ return 'bg-indigo-500 ring-4 ring-indigo-100 dark:ring-indigo-950/60'
48
+ default:
49
+ return 'bg-amber-500 ring-4 ring-amber-100 dark:ring-amber-950/60'
50
+ }
51
+ }
52
+
53
+ interface TimelineEntry {
54
+ id: string
55
+ eventType: string
56
+ previousValueJsonb?: any
57
+ newValueJsonb?: any
58
+ reason?: string | null
59
+ visibility: string
60
+ actor?: { fullName: string } | null
61
+ createdAt: Date | string
62
+ }
63
+
64
+ export function TicketTimeline({ entries }: { entries: TimelineEntry[] }) {
65
+ if (!entries || entries.length === 0) {
66
+ return <p className="text-sm text-muted-foreground py-4">No timeline events recorded.</p>
67
+ }
68
+
69
+ return (
70
+ <div className="relative">
71
+ <div className="absolute left-4 top-0 bottom-0 w-px bg-border" />
72
+ <div className="space-y-6">
73
+ {entries.map((entry) => (
74
+ <div key={entry.id} className="relative pl-10">
75
+ <div
76
+ className={`absolute left-2.5 top-1.5 w-3 h-3 rounded-full border-2 border-background ${getEventDotClass(
77
+ entry.eventType
78
+ )}`}
79
+ />
80
+ <div className="space-y-1">
81
+ <div className="flex items-center gap-2 flex-wrap">
82
+ <span className="font-medium text-sm">
83
+ {eventTypeLabels[entry.eventType] || entry.eventType}
84
+ </span>
85
+ {entry.visibility === 'internal' && (
86
+ <span className="text-xs bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded font-medium">
87
+ Internal
88
+ </span>
89
+ )}
90
+ </div>
91
+ {entry.reason && (
92
+ <p className="text-sm text-foreground">{entry.reason}</p>
93
+ )}
94
+ {entry.newValueJsonb && typeof entry.newValueJsonb === 'object' && (
95
+ <div className="flex items-center gap-1.5 flex-wrap pt-0.5">
96
+ {entry.newValueJsonb.statusCode && (
97
+ <TicketStatusBadge status={entry.newValueJsonb.statusCode} />
98
+ )}
99
+ {entry.newValueJsonb.deviceStatusCode && (
100
+ <DeviceStatusBadge status={entry.newValueJsonb.deviceStatusCode} />
101
+ )}
102
+ {entry.newValueJsonb.priorityCode && (
103
+ <TicketPriorityBadge priority={entry.newValueJsonb.priorityCode} />
104
+ )}
105
+ {entry.newValueJsonb.partName && (
106
+ <span className="text-xs bg-amber-100 dark:bg-amber-950/60 text-amber-800 dark:text-amber-200 border border-amber-300 dark:border-amber-800 px-2 py-0.5 rounded font-medium inline-flex items-center gap-1">
107
+ 📦 {entry.newValueJsonb.partName}
108
+ </span>
109
+ )}
110
+ </div>
111
+ )}
112
+ <div className="flex items-center gap-4 text-xs text-muted-foreground">
113
+ <span className="flex items-center gap-1">
114
+ <Calendar className="w-3 h-3" />
115
+ {new Date(entry.createdAt).toLocaleString()}
116
+ </span>
117
+ {entry.actor && (
118
+ <span className="flex items-center gap-1">
119
+ <User className="w-3 h-3" />
120
+ {entry.actor.fullName}
121
+ </span>
122
+ )}
123
+ </div>
124
+ </div>
125
+ </div>
126
+ ))}
127
+ </div>
128
+ </div>
129
+ )
130
+ }
131
+