@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,405 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import { cn } from '@/lib/utils';
5
+ import { getStaffStatus, getStaffStatusStyle } from '@/lib/utils/staff-status';
6
+ import {
7
+ CheckCircle2,
8
+ Clock,
9
+ Wrench,
10
+ Package,
11
+ MessageCircle,
12
+ CheckCircle,
13
+ AlertTriangle,
14
+ User,
15
+ Phone,
16
+ Mail,
17
+ ShieldCheck,
18
+ Send,
19
+ } from 'lucide-react';
20
+ import { Button } from '@/components/ui/button';
21
+ import { Textarea } from '@/components/ui/textarea';
22
+ import { useState } from 'react';
23
+ import { addTicketComment } from '@/lib/actions/tickets';
24
+
25
+ interface StaffTicketProgressTrackerProps {
26
+ ticket: {
27
+ id: string;
28
+ ticketNumber: string;
29
+ statusCode: string;
30
+ reportedAt?: Date | string | null;
31
+ createdAt?: Date | string | null;
32
+ resolvedAt?: Date | string | null;
33
+ waitingReason?: string | null;
34
+ waitingDependencyType?: string | null;
35
+ assignedEngineerUserId?: string | null;
36
+ assignedEngineer?: {
37
+ fullName?: string | null;
38
+ email?: string | null;
39
+ phone?: string | null;
40
+ } | null;
41
+ finalDeviceStatusCode?: string | null;
42
+ resolutionSummary?: string | null;
43
+ };
44
+ engineerContact?: {
45
+ mobileNumber?: string | null;
46
+ extension?: string | null;
47
+ dutyStatus?: string | null;
48
+ } | null;
49
+ }
50
+
51
+ interface Step {
52
+ id: string;
53
+ label: string;
54
+ sublabel: string;
55
+ status: 'complete' | 'current' | 'upcoming' | 'alert';
56
+ }
57
+
58
+ export function StaffTicketProgressTracker({
59
+ ticket,
60
+ engineerContact,
61
+ }: StaffTicketProgressTrackerProps) {
62
+ const [replyText, setReplyText] = useState('');
63
+ const [submittingReply, setSubmittingReply] = useState(false);
64
+ const [replySuccess, setReplySuccess] = useState(false);
65
+
66
+ const statusInfo = getStaffStatus(ticket.statusCode);
67
+ const statusStyle = getStaffStatusStyle(ticket.statusCode);
68
+
69
+ const isResolved = ['resolved', 'closed'].includes(ticket.statusCode);
70
+ const isWaitingParts = ticket.statusCode === 'waiting_parts_vendor';
71
+ const isWaitingResponse = ticket.statusCode === 'waiting_requester';
72
+
73
+ // Compute 5 linear steps for clinical staff
74
+ const steps: Step[] = useMemo(() => {
75
+ // Stage 1: Submitted
76
+ // Stage 2: Reviewed
77
+ // Stage 3: Assigned to Engineer
78
+ // Stage 4: In Repair / Hold
79
+ // Stage 5: Resolved
80
+
81
+ const code = ticket.statusCode;
82
+
83
+ let s1: Step['status'] = 'complete';
84
+ let s2: Step['status'] = 'upcoming';
85
+ let s3: Step['status'] = 'upcoming';
86
+ let s4: Step['status'] = 'upcoming';
87
+ let s5: Step['status'] = 'upcoming';
88
+
89
+ if (code === 'new') {
90
+ s1 = 'complete';
91
+ s2 = 'current';
92
+ } else if (code === 'acknowledged' || code === 'in_triage') {
93
+ s1 = 'complete';
94
+ s2 = 'complete';
95
+ s3 = 'current';
96
+ } else if (code === 'assigned') {
97
+ s1 = 'complete';
98
+ s2 = 'complete';
99
+ s3 = 'complete';
100
+ s4 = 'current';
101
+ } else if (code === 'in_progress') {
102
+ s1 = 'complete';
103
+ s2 = 'complete';
104
+ s3 = 'complete';
105
+ s4 = 'current';
106
+ } else if (code === 'waiting_parts_vendor') {
107
+ s1 = 'complete';
108
+ s2 = 'complete';
109
+ s3 = 'complete';
110
+ s4 = 'alert';
111
+ } else if (code === 'waiting_requester') {
112
+ s1 = 'complete';
113
+ s2 = 'complete';
114
+ s3 = 'complete';
115
+ s4 = 'alert';
116
+ } else if (isResolved) {
117
+ s1 = 'complete';
118
+ s2 = 'complete';
119
+ s3 = 'complete';
120
+ s4 = 'complete';
121
+ s5 = 'complete';
122
+ }
123
+
124
+ return [
125
+ {
126
+ id: '1',
127
+ label: '1. Submitted',
128
+ sublabel: ticket.reportedAt
129
+ ? new Date(ticket.reportedAt).toLocaleDateString([], { month: 'short', day: 'numeric' })
130
+ : 'Logged',
131
+ status: s1,
132
+ },
133
+ {
134
+ id: '2',
135
+ label: '2. Reviewed',
136
+ sublabel: 'Biomedical Triage',
137
+ status: s2,
138
+ },
139
+ {
140
+ id: '3',
141
+ label: '3. Assigned',
142
+ sublabel: ticket.assignedEngineer?.fullName
143
+ ? ticket.assignedEngineer.fullName.split(' ')[0]
144
+ : 'Pending engineer',
145
+ status: s3,
146
+ },
147
+ {
148
+ id: '4',
149
+ label: isWaitingParts
150
+ ? '4. Parts On Order'
151
+ : isWaitingResponse
152
+ ? '4. Needs Reply'
153
+ : '4. Being Repaired',
154
+ sublabel: isWaitingParts
155
+ ? 'Waiting parts'
156
+ : isWaitingResponse
157
+ ? 'Doctor reply needed'
158
+ : 'Active repair',
159
+ status: s4,
160
+ },
161
+ {
162
+ id: '5',
163
+ label: '5. Resolved',
164
+ sublabel: isResolved ? 'Ready for clinical use' : 'Pending testing',
165
+ status: s5,
166
+ },
167
+ ];
168
+ }, [ticket.statusCode, ticket.reportedAt, ticket.assignedEngineer, isWaitingParts, isWaitingResponse, isResolved]);
169
+
170
+ const handleSendReply = async () => {
171
+ if (!replyText.trim()) return;
172
+ setSubmittingReply(true);
173
+ const res = await addTicketComment(ticket.id, {
174
+ body: replyText.trim(),
175
+ visibility: 'public',
176
+ });
177
+ if (res?.success) {
178
+ setReplySuccess(true);
179
+ setReplyText('');
180
+ }
181
+ setSubmittingReply(false);
182
+ };
183
+
184
+ return (
185
+ <div className="space-y-4">
186
+ {/* ── Visual Stepper Bar ─────────────────────────────────────────── */}
187
+ <div className="bg-card rounded-2xl border p-4 sm:p-5 shadow-sm space-y-4">
188
+ <div className="flex items-center justify-between flex-wrap gap-2 pb-3 border-b border-border/60">
189
+ <div>
190
+ <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
191
+ Repair Progress Tracker
192
+ </span>
193
+ <h3 className="text-base font-bold text-foreground flex items-center gap-2 mt-0.5">
194
+ <span>{statusInfo.icon}</span>
195
+ <span>{statusInfo.label}</span>
196
+ </h3>
197
+ </div>
198
+ <span
199
+ className={cn(
200
+ 'text-xs px-2.5 py-1 rounded-full font-medium border',
201
+ statusStyle.bg,
202
+ statusStyle.text,
203
+ statusStyle.border
204
+ )}
205
+ >
206
+ {statusInfo.description || statusInfo.label}
207
+ </span>
208
+ </div>
209
+
210
+ {/* Stepper Dots & Line */}
211
+ <div className="grid grid-cols-5 gap-1 pt-1">
212
+ {steps.map((step, idx) => {
213
+ const isCompleted = step.status === 'complete';
214
+ const isCurrent = step.status === 'current';
215
+ const isAlert = step.status === 'alert';
216
+
217
+ return (
218
+ <div key={step.id} className="flex flex-col items-center text-center relative group">
219
+ {/* Connecting Line between steps */}
220
+ {idx < steps.length - 1 && (
221
+ <div
222
+ className={cn(
223
+ 'hidden sm:block absolute top-3.5 left-1/2 w-full h-0.5 -z-0',
224
+ isCompleted ? 'bg-primary' : 'bg-muted'
225
+ )}
226
+ />
227
+ )}
228
+
229
+ {/* Step Circle */}
230
+ <div
231
+ className={cn(
232
+ 'w-7 h-7 sm:w-8 sm:h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all relative z-10',
233
+ isCompleted && 'bg-primary text-primary-foreground shadow-sm',
234
+ isCurrent && 'bg-primary/20 text-primary border-2 border-primary ring-4 ring-primary/10 animate-pulse',
235
+ isAlert && 'bg-amber-500 text-white ring-4 ring-amber-500/20 shadow-sm animate-bounce',
236
+ step.status === 'upcoming' && 'bg-muted text-muted-foreground border'
237
+ )}
238
+ >
239
+ {isCompleted ? (
240
+ <CheckCircle2 className="w-4 h-4" />
241
+ ) : isAlert ? (
242
+ '!'
243
+ ) : (
244
+ <span>{step.id}</span>
245
+ )}
246
+ </div>
247
+
248
+ {/* Step Label */}
249
+ <div className="mt-2 min-w-0 px-0.5">
250
+ <p
251
+ className={cn(
252
+ 'text-[11px] sm:text-xs font-semibold truncate leading-tight',
253
+ (isCurrent || isAlert) && 'text-primary font-bold',
254
+ isCompleted && 'text-foreground',
255
+ step.status === 'upcoming' && 'text-muted-foreground'
256
+ )}
257
+ >
258
+ {step.label}
259
+ </p>
260
+ <p className="text-[10px] text-muted-foreground truncate hidden sm:block mt-0.5">
261
+ {step.sublabel}
262
+ </p>
263
+ </div>
264
+ </div>
265
+ );
266
+ })}
267
+ </div>
268
+ </div>
269
+
270
+ {/* ── Status-Specific Clinical Guidance Cards ────────────────────── */}
271
+
272
+ {/* 1. If Waiting for Response from Staff */}
273
+ {isWaitingResponse && (
274
+ <div className="bg-amber-50 dark:bg-amber-950/40 border-2 border-amber-300 dark:border-amber-700 rounded-2xl p-4 sm:p-5 shadow-sm space-y-3">
275
+ <div className="flex items-start gap-3">
276
+ <div className="p-2 bg-amber-500/10 text-amber-700 dark:text-amber-300 rounded-xl">
277
+ <MessageCircle className="w-5 h-5" />
278
+ </div>
279
+ <div>
280
+ <h4 className="font-bold text-sm text-amber-900 dark:text-amber-100">
281
+ Biomedical Engineering has asked a question
282
+ </h4>
283
+ <p className="text-xs text-amber-800 dark:text-amber-200 mt-0.5 leading-relaxed">
284
+ The technician needs more details from the ward staff to proceed with repairing this device. Please provide the requested answer below.
285
+ </p>
286
+ </div>
287
+ </div>
288
+
289
+ {replySuccess ? (
290
+ <div className="p-3 bg-green-100 dark:bg-green-950 text-green-800 dark:text-green-200 rounded-xl text-xs font-medium flex items-center gap-2">
291
+ <CheckCircle className="w-4 h-4 text-green-600" />
292
+ Thank you! Your reply was sent directly to the biomedical team.
293
+ </div>
294
+ ) : (
295
+ <div className="space-y-2 pt-1">
296
+ <Textarea
297
+ placeholder="Type your response to the engineer here (e.g. 'Yes, the alarm was sounding every 30 seconds from pump channel 2')..."
298
+ value={replyText}
299
+ onChange={(e) => setReplyText(e.target.value)}
300
+ rows={2}
301
+ className="bg-card text-xs sm:text-sm"
302
+ />
303
+ <div className="flex justify-end">
304
+ <Button
305
+ size="sm"
306
+ onClick={handleSendReply}
307
+ disabled={submittingReply || !replyText.trim()}
308
+ className="bg-amber-600 hover:bg-amber-700 text-white text-xs gap-1.5"
309
+ >
310
+ <Send className="w-3.5 h-3.5" />
311
+ {submittingReply ? 'Sending...' : 'Send Answer to Engineer'}
312
+ </Button>
313
+ </div>
314
+ </div>
315
+ )}
316
+ </div>
317
+ )}
318
+
319
+ {/* 2. If Waiting for Spare Parts */}
320
+ {isWaitingParts && (
321
+ <div className="bg-amber-50/60 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800 rounded-2xl p-4 shadow-sm flex items-start gap-3">
322
+ <div className="p-2 bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300 rounded-xl">
323
+ <Package className="w-5 h-5" />
324
+ </div>
325
+ <div className="space-y-1">
326
+ <h4 className="font-bold text-sm text-amber-950 dark:text-amber-100">
327
+ Spare Parts On Order
328
+ </h4>
329
+ <p className="text-xs text-muted-foreground leading-relaxed">
330
+ Required component: <strong className="text-foreground">{ticket.waitingReason || 'Replacement part ordered from vendor'}</strong>.
331
+ Repair will resume as soon as the package arrives at the hospital biomedical workshop.
332
+ </p>
333
+ </div>
334
+ </div>
335
+ )}
336
+
337
+ {/* 3. If Assigned to Engineer */}
338
+ {ticket.assignedEngineer?.fullName && (
339
+ <div className="bg-blue-50/50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900/60 rounded-2xl p-4 shadow-sm flex flex-col sm:flex-row sm:items-center justify-between gap-3">
340
+ <div className="flex items-center gap-3">
341
+ <div className="w-11 h-11 rounded-full bg-blue-100 dark:bg-blue-900/80 text-blue-700 dark:text-blue-300 font-bold text-sm flex items-center justify-center shrink-0">
342
+ {ticket.assignedEngineer.fullName.substring(0, 2).toUpperCase()}
343
+ </div>
344
+ <div>
345
+ <div className="flex items-center gap-2">
346
+ <h4 className="font-bold text-sm text-foreground">
347
+ {ticket.assignedEngineer.fullName}
348
+ </h4>
349
+ <span className="text-[10px] bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-1.5 py-0.2 rounded font-medium">
350
+ Assigned Engineer
351
+ </span>
352
+ </div>
353
+ <p className="text-xs text-muted-foreground">
354
+ Biomedical Engineering · Handling your device repair
355
+ </p>
356
+ </div>
357
+ </div>
358
+
359
+ {/* Quick Call & Email actions */}
360
+ <div className="flex items-center gap-2 pt-2 sm:pt-0 border-t sm:border-t-0 border-border/40">
361
+ {(engineerContact?.extension || engineerContact?.mobileNumber || ticket.assignedEngineer.phone) && (
362
+ <Button asChild variant="outline" size="sm" className="h-8 text-xs gap-1.5 bg-card">
363
+ <a
364
+ href={`tel:${
365
+ engineerContact?.mobileNumber ||
366
+ engineerContact?.extension ||
367
+ ticket.assignedEngineer.phone
368
+ }`}
369
+ >
370
+ <Phone className="w-3.5 h-3.5 text-green-600" />
371
+ <span>Call {engineerContact?.extension ? `Ext. ${engineerContact.extension}` : 'Engineer'}</span>
372
+ </a>
373
+ </Button>
374
+ )}
375
+ {ticket.assignedEngineer.email && (
376
+ <Button asChild variant="outline" size="sm" className="h-8 text-xs gap-1.5 bg-card">
377
+ <a href={`mailto:${ticket.assignedEngineer.email}`}>
378
+ <Mail className="w-3.5 h-3.5 text-blue-600" />
379
+ <span>Email</span>
380
+ </a>
381
+ </Button>
382
+ )}
383
+ </div>
384
+ </div>
385
+ )}
386
+
387
+ {/* 4. If Resolved */}
388
+ {isResolved && (
389
+ <div className="bg-green-50/70 dark:bg-green-950/30 border border-green-200 dark:border-green-800 rounded-2xl p-4 shadow-sm flex items-start gap-3">
390
+ <div className="p-2 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded-xl">
391
+ <ShieldCheck className="w-5 h-5" />
392
+ </div>
393
+ <div>
394
+ <h4 className="font-bold text-sm text-green-950 dark:text-green-100">
395
+ Equipment Tested & Cleared for Clinical Use
396
+ </h4>
397
+ <p className="text-xs text-green-800 dark:text-green-300 mt-0.5 leading-relaxed">
398
+ Biomedical repair work is complete. The equipment has passed functional safety testing and is safe for bedside deployment.
399
+ </p>
400
+ </div>
401
+ </div>
402
+ )}
403
+ </div>
404
+ );
405
+ }
@@ -0,0 +1,182 @@
1
+ 'use client';
2
+
3
+ import { useState, useMemo } from 'react';
4
+ import Link from 'next/link';
5
+ import { PageHeader } from '@/components/shared/page-header';
6
+ import { StaffTicketCard, StaffTicketItem } from '@/components/tickets/staff-ticket-card';
7
+ import { Button } from '@/components/ui/button';
8
+ import { Input } from '@/components/ui/input';
9
+ import { PlusCircle, Search, TicketCheck, MessageCircle, CheckCircle, Wrench } from 'lucide-react';
10
+ import { cn } from '@/lib/utils';
11
+
12
+ interface StaffTicketsViewProps {
13
+ tickets: StaffTicketItem[];
14
+ }
15
+
16
+ type TicketFilter = 'all' | 'active' | 'needs_response' | 'resolved';
17
+
18
+ export function StaffTicketsView({ tickets }: StaffTicketsViewProps) {
19
+ const [filter, setFilter] = useState<TicketFilter>('all');
20
+ const [search, setSearch] = useState('');
21
+
22
+ const counts = useMemo(() => {
23
+ const total = tickets.length;
24
+ const needsResponse = tickets.filter((t) => t.statusCode === 'waiting_requester').length;
25
+ const resolved = tickets.filter((t) => ['resolved', 'closed'].includes(t.statusCode)).length;
26
+ const active = total - resolved;
27
+ return { total, active, needsResponse, resolved };
28
+ }, [tickets]);
29
+
30
+ const filteredTickets = useMemo(() => {
31
+ let result = tickets;
32
+
33
+ // Filter by tab
34
+ if (filter === 'active') {
35
+ result = result.filter((t) => !['resolved', 'closed', 'cancelled'].includes(t.statusCode));
36
+ } else if (filter === 'needs_response') {
37
+ result = result.filter((t) => t.statusCode === 'waiting_requester');
38
+ } else if (filter === 'resolved') {
39
+ result = result.filter((t) => ['resolved', 'closed'].includes(t.statusCode));
40
+ }
41
+
42
+ // Filter by search query
43
+ if (search.trim()) {
44
+ const q = search.toLowerCase();
45
+ result = result.filter(
46
+ (t) =>
47
+ t.ticketNumber.toLowerCase().includes(q) ||
48
+ t.title.toLowerCase().includes(q) ||
49
+ (t.deviceName && t.deviceName.toLowerCase().includes(q)) ||
50
+ (t.assignedEngineerName && t.assignedEngineerName.toLowerCase().includes(q))
51
+ );
52
+ }
53
+
54
+ return result;
55
+ }, [tickets, filter, search]);
56
+
57
+ return (
58
+ <div className="space-y-6 max-w-3xl mx-auto pb-10">
59
+ <PageHeader
60
+ title="My Tickets"
61
+ description="Monitor biomedical repair progress and communicate with engineers"
62
+ >
63
+ <Link href="/tickets/create">
64
+ <Button size="lg" className="min-h-[44px]">
65
+ <PlusCircle className="mr-2 h-5 w-5" />
66
+ Report Problem
67
+ </Button>
68
+ </Link>
69
+ </PageHeader>
70
+
71
+ {/* Action banner if any tickets need response */}
72
+ {counts.needsResponse > 0 && filter !== 'needs_response' && (
73
+ <div className="bg-amber-50 dark:bg-amber-950/40 border border-amber-300 dark:border-amber-800 rounded-xl p-3.5 flex items-center justify-between gap-3 text-xs">
74
+ <div className="flex items-center gap-2 text-amber-900 dark:text-amber-200">
75
+ <MessageCircle className="w-4 h-4 text-amber-600 shrink-0" />
76
+ <span>
77
+ <strong>{counts.needsResponse} ticket(s)</strong> require your response so engineering can proceed.
78
+ </span>
79
+ </div>
80
+ <button
81
+ onClick={() => setFilter('needs_response')}
82
+ className="text-primary font-semibold hover:underline shrink-0"
83
+ >
84
+ Show Tickets →
85
+ </button>
86
+ </div>
87
+ )}
88
+
89
+ {/* Search Input */}
90
+ <div className="relative">
91
+ <Search className="w-4 h-4 absolute left-3 top-3 text-muted-foreground" />
92
+ <Input
93
+ placeholder="Search by ticket number, issue, or equipment..."
94
+ value={search}
95
+ onChange={(e) => setSearch(e.target.value)}
96
+ className="pl-9 pr-4 text-sm bg-card"
97
+ />
98
+ </div>
99
+
100
+ {/* Filter Tabs / Pills */}
101
+ <div className="flex gap-2 overflow-x-auto pb-1">
102
+ <button
103
+ onClick={() => setFilter('all')}
104
+ className={cn(
105
+ 'text-xs px-3.5 py-1.5 rounded-full border font-medium transition-all whitespace-nowrap',
106
+ filter === 'all'
107
+ ? 'bg-primary text-primary-foreground border-primary shadow-sm'
108
+ : 'bg-card text-muted-foreground border-border hover:bg-muted'
109
+ )}
110
+ >
111
+ All ({counts.total})
112
+ </button>
113
+
114
+ <button
115
+ onClick={() => setFilter('active')}
116
+ className={cn(
117
+ 'text-xs px-3.5 py-1.5 rounded-full border font-medium transition-all whitespace-nowrap flex items-center gap-1.5',
118
+ filter === 'active'
119
+ ? 'bg-primary text-primary-foreground border-primary shadow-sm'
120
+ : 'bg-card text-muted-foreground border-border hover:bg-muted'
121
+ )}
122
+ >
123
+ <Wrench className="w-3 h-3" />
124
+ Active Repair ({counts.active})
125
+ </button>
126
+
127
+ {counts.needsResponse > 0 && (
128
+ <button
129
+ onClick={() => setFilter('needs_response')}
130
+ className={cn(
131
+ 'text-xs px-3.5 py-1.5 rounded-full border font-medium transition-all whitespace-nowrap flex items-center gap-1.5',
132
+ filter === 'needs_response'
133
+ ? 'bg-amber-600 text-white border-amber-600 shadow-sm'
134
+ : 'bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-200 border-amber-300 hover:bg-amber-100'
135
+ )}
136
+ >
137
+ <MessageCircle className="w-3 h-3" />
138
+ Needs Reply ({counts.needsResponse})
139
+ </button>
140
+ )}
141
+
142
+ <button
143
+ onClick={() => setFilter('resolved')}
144
+ className={cn(
145
+ 'text-xs px-3.5 py-1.5 rounded-full border font-medium transition-all whitespace-nowrap flex items-center gap-1.5',
146
+ filter === 'resolved'
147
+ ? 'bg-primary text-primary-foreground border-primary shadow-sm'
148
+ : 'bg-card text-muted-foreground border-border hover:bg-muted'
149
+ )}
150
+ >
151
+ <CheckCircle className="w-3 h-3" />
152
+ Resolved ({counts.resolved})
153
+ </button>
154
+ </div>
155
+
156
+ {/* Ticket List */}
157
+ {filteredTickets.length === 0 ? (
158
+ <div className="text-center py-12 border rounded-2xl bg-card shadow-sm space-y-2">
159
+ <TicketCheck className="w-8 h-8 text-muted-foreground mx-auto" />
160
+ <p className="text-muted-foreground text-sm font-medium">
161
+ {search
162
+ ? 'No tickets match your search.'
163
+ : filter === 'needs_response'
164
+ ? 'No tickets currently waiting for your response.'
165
+ : filter === 'active'
166
+ ? 'No active tickets in progress.'
167
+ : "You haven't reported any equipment problems yet."}
168
+ </p>
169
+ <p className="text-muted-foreground text-xs">
170
+ Scan a device QR code or click &quot;Report Problem&quot; above to submit an issue.
171
+ </p>
172
+ </div>
173
+ ) : (
174
+ <div className="space-y-3">
175
+ {filteredTickets.map((ticket) => (
176
+ <StaffTicketCard key={ticket.id} ticket={ticket} />
177
+ ))}
178
+ </div>
179
+ )}
180
+ </div>
181
+ );
182
+ }