@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,873 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { useRouter } from "next/navigation"
5
+ import { toast } from "sonner"
6
+ import {
7
+ acceptTicket,
8
+ resolveTicket,
9
+ closeTicket,
10
+ holdTicketForParts,
11
+ resumeTicketWork,
12
+ reopenTicket,
13
+ updateTicketDeviceStatus,
14
+ } from "@/lib/actions/tickets"
15
+ import { Button } from "@/components/ui/button"
16
+ import { Input } from "@/components/ui/input"
17
+ import { Label } from "@/components/ui/label"
18
+ import { Textarea } from "@/components/ui/textarea"
19
+ import { Badge } from "@/components/ui/badge"
20
+ import {
21
+ Dialog,
22
+ DialogContent,
23
+ DialogDescription,
24
+ DialogFooter,
25
+ DialogHeader,
26
+ DialogTitle,
27
+ DialogTrigger,
28
+ } from "@/components/ui/dialog"
29
+ import {
30
+ ShieldCheck,
31
+ CheckCircle,
32
+ XCircle,
33
+ PlayCircle,
34
+ Lock,
35
+ Package,
36
+ Wrench,
37
+ RotateCcw,
38
+ AlertTriangle,
39
+ } from "lucide-react"
40
+
41
+ interface TicketActionButtonsProps {
42
+ ticketId: string
43
+ statusCode: string
44
+ hasDevice: boolean
45
+ ticketNumber?: string
46
+ currentDeviceStatus?: string
47
+ }
48
+
49
+ export function TicketActionButtons({
50
+ ticketId,
51
+ statusCode,
52
+ hasDevice,
53
+ ticketNumber,
54
+ currentDeviceStatus,
55
+ }: TicketActionButtonsProps) {
56
+ const canAccept = ["new", "acknowledged", "in_triage", "waiting_requester"].includes(statusCode)
57
+
58
+ return (
59
+ <div className="flex items-center gap-2 flex-wrap">
60
+ {canAccept && (
61
+ <AcceptButton ticketId={ticketId} />
62
+ )}
63
+ {statusCode === "in_progress" && (
64
+ <>
65
+ {hasDevice && (
66
+ <UpdateDeviceStatusDialog
67
+ ticketId={ticketId}
68
+ ticketNumber={ticketNumber}
69
+ currentDeviceStatus={currentDeviceStatus}
70
+ />
71
+ )}
72
+ <ResolveDialog ticketId={ticketId} hasDevice={hasDevice} ticketNumber={ticketNumber} />
73
+ </>
74
+ )}
75
+ {statusCode === "waiting_parts_vendor" && (
76
+ <>
77
+ <ResumeWorkButton ticketId={ticketId} />
78
+ {hasDevice && (
79
+ <UpdateDeviceStatusDialog
80
+ ticketId={ticketId}
81
+ ticketNumber={ticketNumber}
82
+ currentDeviceStatus={currentDeviceStatus}
83
+ />
84
+ )}
85
+ <ResolveDialog ticketId={ticketId} hasDevice={hasDevice} ticketNumber={ticketNumber} />
86
+ </>
87
+ )}
88
+ {statusCode === "resolved" && (
89
+ <>
90
+ <CloseDialog ticketId={ticketId} ticketNumber={ticketNumber} />
91
+ <ReopenDialog ticketId={ticketId} ticketNumber={ticketNumber} />
92
+ </>
93
+ )}
94
+ {["closed", "cancelled"].includes(statusCode) && (
95
+ <ReopenDialog ticketId={ticketId} ticketNumber={ticketNumber} />
96
+ )}
97
+ </div>
98
+ )
99
+ }
100
+
101
+ // ── 1-Click Accept Button ───────────────────────────────────────────────────
102
+
103
+ function AcceptButton({ ticketId }: { ticketId: string }) {
104
+ const router = useRouter()
105
+ const [loading, setLoading] = useState(false)
106
+
107
+ const handleAccept = async () => {
108
+ setLoading(true)
109
+ const result = await acceptTicket(ticketId)
110
+
111
+ if (result?.success) {
112
+ toast.success("Ticket accepted and moved to In Progress")
113
+ router.refresh()
114
+ } else {
115
+ toast.error((result as any)?.error || "Failed to accept ticket")
116
+ }
117
+ setLoading(false)
118
+ }
119
+
120
+ return (
121
+ <Button
122
+ size="sm"
123
+ onClick={handleAccept}
124
+ disabled={loading}
125
+ className="gap-1.5 bg-indigo-600 hover:bg-indigo-700 text-white font-medium shadow-xs"
126
+ >
127
+ <CheckCircle className="w-4 h-4" />
128
+ <span>{loading ? "Accepting..." : "Accept Ticket"}</span>
129
+ </Button>
130
+ )
131
+ }
132
+
133
+ // ── Resolve Dialog (with Electronic Signature) ──────────────────────────────────
134
+
135
+ function ResolveDialog({ ticketId, hasDevice, ticketNumber }: { ticketId: string; hasDevice: boolean; ticketNumber?: string }) {
136
+ const router = useRouter()
137
+ const [open, setOpen] = useState(false)
138
+ const [loading, setLoading] = useState(false)
139
+ const [summary, setSummary] = useState("")
140
+ const [deviceStatus, setDeviceStatus] = useState("operational")
141
+ const [limitationsNote, setLimitationsNote] = useState("")
142
+ const [password, setPassword] = useState("")
143
+ const [comments, setComments] = useState("")
144
+
145
+ const handleResolve = async () => {
146
+ if (summary.trim().length < 10) {
147
+ toast.error("Resolution summary must be at least 10 characters")
148
+ return
149
+ }
150
+
151
+ if (!password) {
152
+ toast.error("Password is required for electronic signature authentication")
153
+ return
154
+ }
155
+
156
+ setLoading(true)
157
+ const result = await resolveTicket(ticketId, {
158
+ resolutionSummary: summary.trim(),
159
+ finalDeviceStatusCode: deviceStatus,
160
+ limitationsNote: deviceStatus === "operational_with_limitations" ? limitationsNote.trim() || undefined : undefined,
161
+ signaturePassword: password,
162
+ signatureComments: comments.trim() || undefined,
163
+ })
164
+
165
+ if (result?.success) {
166
+ toast.success("Ticket resolved & signed with cryptographic SHA-256 hash")
167
+ setOpen(false)
168
+ setSummary("")
169
+ setLimitationsNote("")
170
+ setPassword("")
171
+ setComments("")
172
+ router.refresh()
173
+ } else {
174
+ toast.error((result as any)?.error || "Failed to resolve ticket")
175
+ }
176
+ setLoading(false)
177
+ }
178
+
179
+ return (
180
+ <Dialog open={open} onOpenChange={setOpen}>
181
+ <DialogTrigger asChild>
182
+ <Button variant="outline" size="sm" className="gap-1.5 border-emerald-600/40 text-emerald-700 hover:bg-emerald-50 dark:hover:bg-emerald-950/30">
183
+ <CheckCircle className="w-4 h-4 text-emerald-600" />
184
+ Resolve &amp; Sign
185
+ </Button>
186
+ </DialogTrigger>
187
+ <DialogContent className="sm:max-w-md max-h-[90vh] overflow-y-auto">
188
+ <DialogHeader>
189
+ <div className="flex items-center gap-2">
190
+ <CheckCircle className="w-5 h-5 text-emerald-600" />
191
+ <DialogTitle>Resolve Ticket with Electronic Signature</DialogTitle>
192
+ </div>
193
+ <DialogDescription>
194
+ {ticketNumber ? `Ticket ${ticketNumber}` : "Service Ticket"} — Document the fix and cryptographically sign off.
195
+ </DialogDescription>
196
+ </DialogHeader>
197
+
198
+ <div className="space-y-4 py-2">
199
+ {/* Resolution Description */}
200
+ <div>
201
+ <Label htmlFor="resolutionSummary">Resolution Summary *</Label>
202
+ <Textarea
203
+ id="resolutionSummary"
204
+ value={summary}
205
+ onChange={(e) => setSummary(e.target.value)}
206
+ placeholder="Describe the fix: what was found, what was done, and the test result..."
207
+ rows={3}
208
+ className="mt-1.5"
209
+ />
210
+ <p className="text-xs text-muted-foreground mt-1">
211
+ Minimum 10 characters. This will be visible to the reporter and in compliance reports.
212
+ </p>
213
+ </div>
214
+
215
+ {/* Final Device Status */}
216
+ {hasDevice && (
217
+ <div className="space-y-3">
218
+ <div>
219
+ <Label htmlFor="finalDeviceStatus">Device Status After Fix</Label>
220
+ <select
221
+ id="finalDeviceStatus"
222
+ value={deviceStatus}
223
+ onChange={(e) => setDeviceStatus(e.target.value)}
224
+ className="w-full mt-1.5 rounded-md border border-input bg-background px-3 py-2 text-sm"
225
+ >
226
+ <option value="operational">✅ Operational (Safe for Clinical Use)</option>
227
+ <option value="operational_with_limitations">⚠️ Operational with Limitations</option>
228
+ <option value="awaiting_release">⏳ Awaiting Release (QA Verification)</option>
229
+ <option value="standby">💤 Standby (Reserve Equipment)</option>
230
+ <option value="out_of_service">🚫 Out of Service (Unrepairable / Condemned)</option>
231
+ <option value="decommissioned">🗑️ Decommissioned (Permanently Retired)</option>
232
+ </select>
233
+ </div>
234
+
235
+ {/* Limitations Note — shown only when operational_with_limitations */}
236
+ {deviceStatus === "operational_with_limitations" && (
237
+ <div className="rounded-md bg-amber-50/70 dark:bg-amber-950/40 p-3 border border-amber-200/80 dark:border-amber-800 space-y-2">
238
+ <div className="flex items-center gap-1.5 text-xs font-semibold text-amber-900 dark:text-amber-200">
239
+ <AlertTriangle className="w-3.5 h-3.5 text-amber-600" />
240
+ <span>Operating Limitations Notice</span>
241
+ </div>
242
+ <Textarea
243
+ value={limitationsNote}
244
+ onChange={(e) => setLimitationsNote(e.target.value)}
245
+ placeholder="Describe what functions or modes are limited and any clinical usage restrictions..."
246
+ rows={2}
247
+ className="text-sm bg-white dark:bg-zinc-900"
248
+ />
249
+ <p className="text-[11px] text-muted-foreground">
250
+ This note will be displayed on the device profile and department dashboard so clinical staff know the equipment&apos;s constraints.
251
+ </p>
252
+ </div>
253
+ )}
254
+
255
+ {/* Out of Service / Decommissioned Warning */}
256
+ {(deviceStatus === "out_of_service" || deviceStatus === "decommissioned") && (
257
+ <div className="rounded bg-rose-50 dark:bg-rose-950/40 p-2.5 border border-rose-300 dark:border-rose-800 text-xs text-rose-900 dark:text-rose-200">
258
+ <AlertTriangle className="w-3.5 h-3.5 inline mr-1 text-rose-600" />
259
+ <strong>Notice:</strong>{" "}
260
+ {deviceStatus === "decommissioned"
261
+ ? "Selecting Decommissioned permanently retires this device. It will be removed from active inventory."
262
+ : "Selecting Out of Service resolves this ticket by declaring the equipment unrepairable. The device will remain non-clinical."}
263
+ </div>
264
+ )}
265
+
266
+ <p className="text-xs text-muted-foreground">
267
+ Tip: If the equipment is still under repair, close this dialog and use <strong>Update Status</strong> instead.
268
+ </p>
269
+ </div>
270
+ )}
271
+
272
+ {/* Legal Attestation Statement */}
273
+ <div className="rounded-md bg-emerald-50/50 dark:bg-emerald-950/30 p-3 border border-emerald-200/60 text-xs text-emerald-950 dark:text-emerald-200">
274
+ <span className="font-semibold block mb-1">Attestation Statement:</span>
275
+ <p className="italic text-muted-foreground">
276
+ &quot;I confirm that the issue has been resolved and the resolution summary accurately describes the work performed.&quot;
277
+ </p>
278
+ </div>
279
+
280
+ {/* Signature Re-Authentication Password */}
281
+ <div>
282
+ <Label htmlFor="resolvePassword" className="flex items-center gap-1.5">
283
+ <Lock className="w-3.5 h-3.5 text-muted-foreground" />
284
+ Electronic Signature Password *
285
+ </Label>
286
+ <Input
287
+ id="resolvePassword"
288
+ type="password"
289
+ value={password}
290
+ onChange={(e) => setPassword(e.target.value)}
291
+ placeholder="Enter your account password to sign..."
292
+ className="mt-1.5"
293
+ />
294
+ <p className="text-[11px] text-muted-foreground mt-1">
295
+ Generates a tamper-evident SHA-256 hash locked in the hospital regulatory audit chain.
296
+ </p>
297
+ </div>
298
+
299
+ {/* Optional Comments */}
300
+ <div>
301
+ <Label htmlFor="resolveComments">Comments / Sign-Off Note (optional)</Label>
302
+ <Input
303
+ id="resolveComments"
304
+ value={comments}
305
+ onChange={(e) => setComments(e.target.value)}
306
+ placeholder="e.g. Device passed safety check, handed back to ward sister..."
307
+ className="mt-1.5 text-sm"
308
+ />
309
+ </div>
310
+ </div>
311
+
312
+ <DialogFooter>
313
+ <Button variant="ghost" onClick={() => setOpen(false)} disabled={loading}>
314
+ Cancel
315
+ </Button>
316
+ <Button
317
+ onClick={handleResolve}
318
+ disabled={loading || summary.trim().length < 10 || !password}
319
+ className="bg-emerald-600 hover:bg-emerald-700 text-white gap-1.5"
320
+ >
321
+ <ShieldCheck className="w-4 h-4" />
322
+ {loading ? "Signing..." : "Sign & Mark as Resolved"}
323
+ </Button>
324
+ </DialogFooter>
325
+ </DialogContent>
326
+ </Dialog>
327
+ )
328
+ }
329
+
330
+ // ── Close Dialog (with Electronic Signature) ────────────────────────────────────
331
+
332
+ function CloseDialog({ ticketId, ticketNumber }: { ticketId: string; ticketNumber?: string }) {
333
+ const router = useRouter()
334
+ const [open, setOpen] = useState(false)
335
+ const [loading, setLoading] = useState(false)
336
+ const [reason, setReason] = useState("")
337
+ const [password, setPassword] = useState("")
338
+ const [comments, setComments] = useState("")
339
+
340
+ const handleClose = async () => {
341
+ if (!password) {
342
+ toast.error("Password is required for electronic signature authentication")
343
+ return
344
+ }
345
+
346
+ setLoading(true)
347
+ const result = await closeTicket(ticketId, {
348
+ closureReason: reason.trim() || undefined,
349
+ signaturePassword: password,
350
+ signatureComments: comments.trim() || undefined,
351
+ })
352
+
353
+ if (result?.success) {
354
+ toast.success("Ticket closed & signed with cryptographic SHA-256 hash")
355
+ setOpen(false)
356
+ setReason("")
357
+ setPassword("")
358
+ setComments("")
359
+ router.refresh()
360
+ } else {
361
+ toast.error((result as any)?.error || "Failed to close ticket")
362
+ }
363
+ setLoading(false)
364
+ }
365
+
366
+ return (
367
+ <Dialog open={open} onOpenChange={setOpen}>
368
+ <DialogTrigger asChild>
369
+ <Button variant="outline" size="sm" className="gap-1.5">
370
+ <XCircle className="w-4 h-4" />
371
+ Close &amp; Sign
372
+ </Button>
373
+ </DialogTrigger>
374
+ <DialogContent className="sm:max-w-md">
375
+ <DialogHeader>
376
+ <div className="flex items-center gap-2">
377
+ <XCircle className="w-5 h-5 text-muted-foreground" />
378
+ <DialogTitle>Close Ticket with Electronic Signature</DialogTitle>
379
+ </div>
380
+ <DialogDescription>
381
+ {ticketNumber ? `Ticket ${ticketNumber}` : "Service Ticket"} — Permanent closure sign-off confirming resolution is verified.
382
+ </DialogDescription>
383
+ </DialogHeader>
384
+
385
+ <div className="space-y-4 py-2">
386
+ {/* Legal Attestation Statement */}
387
+ <div className="rounded-md bg-muted/50 p-3 border text-xs">
388
+ <span className="font-semibold block mb-1">Attestation Statement:</span>
389
+ <p className="italic text-muted-foreground">
390
+ &quot;I confirm that the ticket resolution is sufficiently documented and complete under the applicable policy.&quot;
391
+ </p>
392
+ </div>
393
+
394
+ <div>
395
+ <Label htmlFor="closureReason">Closure Reason / Final Verification Note</Label>
396
+ <Textarea
397
+ id="closureReason"
398
+ value={reason}
399
+ onChange={(e) => setReason(e.target.value)}
400
+ placeholder="e.g. Ward confirmed device operational and functioning in clinical use..."
401
+ rows={2}
402
+ className="mt-1.5"
403
+ />
404
+ </div>
405
+
406
+ <div>
407
+ <Label htmlFor="closePassword" className="flex items-center gap-1.5">
408
+ <Lock className="w-3.5 h-3.5 text-muted-foreground" />
409
+ Electronic Signature Password *
410
+ </Label>
411
+ <Input
412
+ id="closePassword"
413
+ type="password"
414
+ value={password}
415
+ onChange={(e) => setPassword(e.target.value)}
416
+ placeholder="Enter your account password to sign..."
417
+ className="mt-1.5"
418
+ />
419
+ </div>
420
+ </div>
421
+
422
+ <DialogFooter>
423
+ <Button variant="ghost" onClick={() => setOpen(false)} disabled={loading}>
424
+ Cancel
425
+ </Button>
426
+ <Button
427
+ onClick={handleClose}
428
+ disabled={loading || !password}
429
+ className="gap-1.5"
430
+ >
431
+ <ShieldCheck className="w-4 h-4" />
432
+ {loading ? "Signing..." : "Sign & Close Ticket"}
433
+ </Button>
434
+ </DialogFooter>
435
+ </DialogContent>
436
+ </Dialog>
437
+ )
438
+ }
439
+
440
+ // ── Wait for Parts Dialog ───────────────────────────────────────────────────────
441
+
442
+ function WaitForPartsDialog({ ticketId, ticketNumber }: { ticketId: string; ticketNumber?: string }) {
443
+ const router = useRouter()
444
+ const [open, setOpen] = useState(false)
445
+ const [loading, setLoading] = useState(false)
446
+ const [reason, setReason] = useState("")
447
+ const [partDescription, setPartDescription] = useState("")
448
+
449
+ const handleHold = async () => {
450
+ if (!reason.trim()) {
451
+ toast.error("Please specify what parts or vendor dependency is needed")
452
+ return
453
+ }
454
+
455
+ setLoading(true)
456
+ const result = await holdTicketForParts(ticketId, {
457
+ waitingReason: reason.trim(),
458
+ partDescription: partDescription.trim() || undefined,
459
+ })
460
+
461
+ if (result?.success) {
462
+ toast.success("Ticket put on hold: Waiting for spare parts")
463
+ setOpen(false)
464
+ setReason("")
465
+ setPartDescription("")
466
+ router.refresh()
467
+ } else {
468
+ toast.error((result as any)?.error || "Failed to update ticket")
469
+ }
470
+ setLoading(false)
471
+ }
472
+
473
+ return (
474
+ <Dialog open={open} onOpenChange={setOpen}>
475
+ <DialogTrigger asChild>
476
+ <Button variant="outline" size="sm" className="gap-1.5 border-amber-300 text-amber-800 hover:bg-amber-50 dark:border-amber-800 dark:text-amber-300 dark:hover:bg-amber-950/40">
477
+ <Package className="w-4 h-4 text-amber-600" />
478
+ Wait for Parts
479
+ </Button>
480
+ </DialogTrigger>
481
+ <DialogContent className="sm:max-w-md">
482
+ <DialogHeader>
483
+ <div className="flex items-center gap-2">
484
+ <Package className="w-5 h-5 text-amber-600" />
485
+ <DialogTitle>Hold Ticket for Spare Parts</DialogTitle>
486
+ </div>
487
+ <DialogDescription>
488
+ {ticketNumber ? `Ticket ${ticketNumber}` : "Service Ticket"} — Pause repair work until replacement parts or vendor service arrive.
489
+ </DialogDescription>
490
+ </DialogHeader>
491
+
492
+ <div className="space-y-4 py-2">
493
+ <div>
494
+ <Label htmlFor="partReason">Parts Needed / Dependency *</Label>
495
+ <Input
496
+ id="partReason"
497
+ value={reason}
498
+ onChange={(e) => setReason(e.target.value)}
499
+ placeholder="e.g. Display cable, power supply board, valve kit..."
500
+ className="mt-1.5"
501
+ />
502
+ </div>
503
+
504
+ <div>
505
+ <Label htmlFor="partDetails">Supplier / Order Details (optional)</Label>
506
+ <Textarea
507
+ id="partDetails"
508
+ value={partDescription}
509
+ onChange={(e) => setPartDescription(e.target.value)}
510
+ placeholder="e.g. Mindray PO# 49201, expected delivery in 3 days..."
511
+ rows={2}
512
+ className="mt-1.5"
513
+ />
514
+ </div>
515
+
516
+ <div className="rounded-md bg-amber-50/60 dark:bg-amber-950/30 p-3 border border-amber-200 text-xs text-amber-900 dark:text-amber-200">
517
+ <p className="font-medium">Device Status Change:</p>
518
+ <p className="text-muted-foreground mt-0.5">
519
+ The linked medical device will be automatically marked as <strong>📦 Waiting for Spare Parts</strong> in department dashboards.
520
+ </p>
521
+ </div>
522
+ </div>
523
+
524
+ <DialogFooter>
525
+ <Button variant="ghost" onClick={() => setOpen(false)} disabled={loading}>
526
+ Cancel
527
+ </Button>
528
+ <Button
529
+ onClick={handleHold}
530
+ disabled={loading || !reason.trim()}
531
+ className="bg-amber-600 hover:bg-amber-700 text-white gap-1.5"
532
+ >
533
+ <Package className="w-4 h-4" />
534
+ {loading ? "Updating..." : "Place on Hold"}
535
+ </Button>
536
+ </DialogFooter>
537
+ </DialogContent>
538
+ </Dialog>
539
+ )
540
+ }
541
+
542
+ // ── Resume Work Button ─────────────────────────────────────────────────────────
543
+
544
+ export function ResumeWorkButton({ ticketId }: { ticketId: string }) {
545
+ const router = useRouter()
546
+ const [loading, setLoading] = useState(false)
547
+
548
+ const handleResume = async () => {
549
+ setLoading(true)
550
+ const result = await resumeTicketWork(ticketId)
551
+ if (result?.success) {
552
+ toast.success("Spare parts received! Ticket resumed to In Progress")
553
+ router.refresh()
554
+ } else {
555
+ toast.error((result as any)?.error || "Failed to resume ticket")
556
+ }
557
+ setLoading(false)
558
+ }
559
+
560
+ return (
561
+ <Button
562
+ size="sm"
563
+ onClick={handleResume}
564
+ disabled={loading}
565
+ className="bg-indigo-600 hover:bg-indigo-700 text-white gap-1.5 shadow-sm"
566
+ >
567
+ <PlayCircle className="w-4 h-4" />
568
+ {loading ? "Resuming..." : "Parts Received (Resume)"}
569
+ </Button>
570
+ )
571
+ }
572
+
573
+ // ── Update Device Status Dialog (Interim Progress) ───────────────────────────
574
+
575
+ function UpdateDeviceStatusDialog({
576
+ ticketId,
577
+ ticketNumber,
578
+ currentDeviceStatus = "under_repair",
579
+ }: {
580
+ ticketId: string
581
+ ticketNumber?: string
582
+ currentDeviceStatus?: string
583
+ }) {
584
+ const router = useRouter()
585
+ const [open, setOpen] = useState(false)
586
+ const [loading, setLoading] = useState(false)
587
+ const [deviceStatus, setDeviceStatus] = useState(currentDeviceStatus || "under_repair")
588
+ const [notes, setNotes] = useState("")
589
+ const [partName, setPartName] = useState("")
590
+ const [supplierDetails, setSupplierDetails] = useState("")
591
+ const [limitationsNote, setLimitationsNote] = useState("")
592
+
593
+ const isWaiting = deviceStatus === "waiting_for_parts"
594
+
595
+ const handleUpdate = async () => {
596
+ if (isWaiting && partName.trim().length < 3) {
597
+ toast.error("Please enter the required spare part name (minimum 3 characters)")
598
+ return
599
+ }
600
+
601
+ if (!isWaiting && notes.trim().length < 3) {
602
+ toast.error("Please enter a progress note (minimum 3 characters)")
603
+ return
604
+ }
605
+
606
+ setLoading(true)
607
+ const isLimited = deviceStatus === "operational_with_limitations"
608
+ const result = await updateTicketDeviceStatus(ticketId, {
609
+ deviceStatusCode: deviceStatus,
610
+ notes: isWaiting ? (notes.trim() || `Waiting for ${partName.trim()}`) : notes.trim(),
611
+ partName: isWaiting ? partName.trim() : undefined,
612
+ supplierDetails: isWaiting && supplierDetails.trim() ? supplierDetails.trim() : undefined,
613
+ limitationsNote: isLimited ? limitationsNote.trim() || undefined : undefined,
614
+ })
615
+
616
+ if (result?.success) {
617
+ if (isWaiting) {
618
+ toast.success("Equipment placed on hold: waiting for spare parts")
619
+ } else {
620
+ toast.success("Device status updated and progress recorded to ticket")
621
+ }
622
+ setOpen(false)
623
+ setNotes("")
624
+ setPartName("")
625
+ setSupplierDetails("")
626
+ setLimitationsNote("")
627
+ router.refresh()
628
+ } else {
629
+ toast.error((result as any)?.error || "Failed to update device status")
630
+ }
631
+ setLoading(false)
632
+ }
633
+
634
+ return (
635
+ <Dialog open={open} onOpenChange={setOpen}>
636
+ <DialogTrigger asChild>
637
+ <Button variant="outline" size="sm" className="gap-1.5 border-slate-300 dark:border-slate-700 text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800">
638
+ <Wrench className="w-4 h-4 text-slate-500" />
639
+ Update Status
640
+ </Button>
641
+ </DialogTrigger>
642
+ <DialogContent className="sm:max-w-md max-h-[90vh] overflow-y-auto">
643
+ <DialogHeader>
644
+ <div className="flex items-center gap-2">
645
+ <Wrench className="w-5 h-5 text-slate-700 dark:text-slate-300" />
646
+ <DialogTitle>Update Equipment Status &amp; Progress</DialogTitle>
647
+ </div>
648
+ <DialogDescription>
649
+ {ticketNumber ? `Ticket ${ticketNumber}` : "Service Ticket"} — Update equipment status while keeping ticket in progress.
650
+ </DialogDescription>
651
+ </DialogHeader>
652
+
653
+ <div className="space-y-4 py-2">
654
+ <div>
655
+ <Label htmlFor="activeDeviceStatus">Current Equipment Status</Label>
656
+ <select
657
+ id="activeDeviceStatus"
658
+ value={deviceStatus}
659
+ onChange={(e) => setDeviceStatus(e.target.value)}
660
+ className="w-full mt-1.5 rounded-md border border-input bg-background px-3 py-2 text-sm"
661
+ >
662
+ <option value="under_repair">🛠️ Under Repair (Active Work)</option>
663
+ <option value="waiting_for_parts">📦 Waiting for Spare Parts / Vendor</option>
664
+ <option value="under_maintenance">🔧 Under Maintenance (Routine Service)</option>
665
+ <option value="operational_with_limitations">⚠️ Operational with Limitations (Partial Function)</option>
666
+ <option value="out_of_service">🚫 Out of Service (Unsafe / Quarantined)</option>
667
+ <option value="standby">💤 Standby (Temporarily Idle)</option>
668
+ </select>
669
+ <p className="text-xs text-muted-foreground mt-1">
670
+ Reflects real-time equipment availability to hospital clinical staff.
671
+ </p>
672
+ </div>
673
+
674
+ {isWaiting ? (
675
+ <div className="space-y-3 pt-1">
676
+ <div className="rounded-md bg-amber-50/70 dark:bg-amber-950/40 p-3 border border-amber-200 text-xs text-amber-950 dark:text-amber-200 space-y-1">
677
+ <div className="flex items-center gap-1.5 font-semibold text-amber-900 dark:text-amber-200">
678
+ <Package className="w-4 h-4 text-amber-600" />
679
+ <span>Spare Parts Hold Workflow</span>
680
+ </div>
681
+ <p className="text-muted-foreground">
682
+ The ticket will be placed on hold and equipment marked <strong>Waiting for Spare Parts</strong>. A dedicated tracking card will appear on the ticket.
683
+ </p>
684
+ </div>
685
+
686
+ <div>
687
+ <Label htmlFor="partName">Required Spare Part / Component *</Label>
688
+ <Input
689
+ id="partName"
690
+ value={partName}
691
+ onChange={(e) => setPartName(e.target.value)}
692
+ placeholder="e.g. Power supply board, ECG trunk cable, O2 sensor..."
693
+ className="mt-1.5 text-sm"
694
+ />
695
+ </div>
696
+
697
+ <div>
698
+ <Label htmlFor="supplierDetails">Supplier / Order Tracking Details (optional)</Label>
699
+ <Input
700
+ id="supplierDetails"
701
+ value={supplierDetails}
702
+ onChange={(e) => setSupplierDetails(e.target.value)}
703
+ placeholder="e.g. Mindray PO# 49201, expected delivery in 3 days..."
704
+ className="mt-1.5 text-sm"
705
+ />
706
+ </div>
707
+
708
+ <div>
709
+ <Label htmlFor="progressNote">Additional Work Notes (optional)</Label>
710
+ <Textarea
711
+ id="progressNote"
712
+ value={notes}
713
+ onChange={(e) => setNotes(e.target.value)}
714
+ placeholder="e.g. Unit quarantined on bench; removed faulty component."
715
+ rows={2}
716
+ className="mt-1.5 text-sm"
717
+ />
718
+ </div>
719
+ </div>
720
+ ) : (
721
+ <div className="space-y-3">
722
+ {/* Operational with Limitations — contextual card */}
723
+ {deviceStatus === "operational_with_limitations" && (
724
+ <div className="rounded-md bg-amber-50/70 dark:bg-amber-950/40 p-3 border border-amber-200/80 dark:border-amber-800 space-y-2">
725
+ <div className="flex items-center gap-1.5 text-xs font-semibold text-amber-900 dark:text-amber-200">
726
+ <AlertTriangle className="w-3.5 h-3.5 text-amber-600" />
727
+ <span>Operating Limitations Notice</span>
728
+ </div>
729
+ <Textarea
730
+ value={limitationsNote}
731
+ onChange={(e) => setLimitationsNote(e.target.value)}
732
+ placeholder="Describe what functions or modes are limited and any clinical usage restrictions..."
733
+ rows={2}
734
+ className="text-sm bg-white dark:bg-zinc-900"
735
+ />
736
+ <p className="text-[11px] text-muted-foreground">
737
+ This note will be shown on the device profile so clinical staff understand equipment constraints.
738
+ </p>
739
+ </div>
740
+ )}
741
+
742
+ {/* Out of Service — contextual warning */}
743
+ {deviceStatus === "out_of_service" && (
744
+ <div className="rounded bg-rose-50 dark:bg-rose-950/40 p-2.5 border border-rose-300 dark:border-rose-800 text-xs text-rose-900 dark:text-rose-200">
745
+ <AlertTriangle className="w-3.5 h-3.5 inline mr-1 text-rose-600" />
746
+ <strong>Warning:</strong> This marks the device as unsafe / quarantined. Clinical staff will be informed that this equipment is unavailable. Use <strong>Resolve &amp; Sign</strong> if repair is complete.
747
+ </div>
748
+ )}
749
+
750
+ <div>
751
+ <Label htmlFor="progressNote">Progress Note / Diagnostic Findings *</Label>
752
+ <Textarea
753
+ id="progressNote"
754
+ value={notes}
755
+ onChange={(e) => setNotes(e.target.value)}
756
+ placeholder="e.g. Unit disassembled on bench; power supply capacitor failed. Ordered replacement component..."
757
+ rows={3}
758
+ className="mt-1.5 text-sm"
759
+ />
760
+ <p className="text-xs text-muted-foreground mt-1">
761
+ Recorded in the ticket timeline and device audit history without closing or resolving this ticket.
762
+ </p>
763
+ </div>
764
+ </div>
765
+ )}
766
+ </div>
767
+
768
+ <DialogFooter>
769
+ <Button variant="ghost" onClick={() => setOpen(false)} disabled={loading}>
770
+ Cancel
771
+ </Button>
772
+ <Button
773
+ onClick={handleUpdate}
774
+ disabled={loading || (isWaiting ? partName.trim().length < 3 : notes.trim().length < 3)}
775
+ className={`gap-1.5 ${isWaiting ? "bg-amber-600 hover:bg-amber-700 text-white" : ""}`}
776
+ >
777
+ {isWaiting ? <Package className="w-4 h-4" /> : null}
778
+ {loading ? "Saving..." : isWaiting ? "Place on Hold (Waiting for Parts)" : "Save Status & Note"}
779
+ </Button>
780
+ </DialogFooter>
781
+ </DialogContent>
782
+ </Dialog>
783
+ )
784
+ }
785
+
786
+ // ── Reopen / Resume Work Dialog ────────────────────────────────────────────────
787
+
788
+ function ReopenDialog({ ticketId, ticketNumber }: { ticketId: string; ticketNumber?: string }) {
789
+ const router = useRouter()
790
+ const [open, setOpen] = useState(false)
791
+ const [loading, setLoading] = useState(false)
792
+ const [reason, setReason] = useState("")
793
+
794
+ const handleReopen = async () => {
795
+ if (reason.trim().length < 5) {
796
+ toast.error("Please provide a reason (minimum 5 characters)")
797
+ return
798
+ }
799
+
800
+ setLoading(true)
801
+ const result = await reopenTicket(ticketId, {
802
+ reopenReason: reason.trim(),
803
+ })
804
+
805
+ if (result?.success) {
806
+ toast.success("Ticket reopened — repair work resumed and device marked Under Repair")
807
+ setOpen(false)
808
+ setReason("")
809
+ router.refresh()
810
+ } else {
811
+ toast.error((result as any)?.error || "Failed to reopen ticket")
812
+ }
813
+ setLoading(false)
814
+ }
815
+
816
+ return (
817
+ <Dialog open={open} onOpenChange={setOpen}>
818
+ <DialogTrigger asChild>
819
+ <Button variant="outline" size="sm" className="gap-1.5 border-amber-500/40 text-amber-700 hover:bg-amber-50 dark:hover:bg-amber-950/30">
820
+ <RotateCcw className="w-4 h-4 text-amber-600" />
821
+ Reopen / Resume Work
822
+ </Button>
823
+ </DialogTrigger>
824
+ <DialogContent className="sm:max-w-md">
825
+ <DialogHeader>
826
+ <div className="flex items-center gap-2">
827
+ <RotateCcw className="w-5 h-5 text-amber-600" />
828
+ <DialogTitle>Reopen Ticket &amp; Resume Work</DialogTitle>
829
+ </div>
830
+ <DialogDescription>
831
+ {ticketNumber ? `Ticket ${ticketNumber}` : "Service Ticket"} — Return ticket to active work and mark device Under Repair.
832
+ </DialogDescription>
833
+ </DialogHeader>
834
+
835
+ <div className="space-y-4 py-2">
836
+ <div className="rounded-md bg-amber-50/70 dark:bg-amber-950/40 p-3 border border-amber-200/80 text-xs text-amber-950 dark:text-amber-200">
837
+ <p>
838
+ Reopening clears previous resolution details and moves the ticket back to <strong>In Progress</strong>. The device will be marked <strong>Under Repair</strong>.
839
+ </p>
840
+ </div>
841
+
842
+ <div>
843
+ <Label htmlFor="reopenReason">Reason for Reopening *</Label>
844
+ <Textarea
845
+ id="reopenReason"
846
+ value={reason}
847
+ onChange={(e) => setReason(e.target.value)}
848
+ placeholder="e.g. Issue recurred during clinical check, or repair incomplete..."
849
+ rows={3}
850
+ className="mt-1.5 text-sm"
851
+ />
852
+ </div>
853
+ </div>
854
+
855
+ <DialogFooter>
856
+ <Button variant="ghost" onClick={() => setOpen(false)} disabled={loading}>
857
+ Cancel
858
+ </Button>
859
+ <Button
860
+ onClick={handleReopen}
861
+ disabled={loading || reason.trim().length < 5}
862
+ className="bg-amber-600 hover:bg-amber-700 text-white gap-1.5"
863
+ >
864
+ <RotateCcw className="w-4 h-4" />
865
+ {loading ? "Reopening..." : "Resume Work"}
866
+ </Button>
867
+ </DialogFooter>
868
+ </DialogContent>
869
+ </Dialog>
870
+ )
871
+ }
872
+
873
+