@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,607 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { Card, CardContent } from "@/components/ui/card"
5
+ import { Separator } from "@/components/ui/separator"
6
+ import { Badge } from "@/components/ui/badge"
7
+ import { Button } from "@/components/ui/button"
8
+ import {
9
+ ShieldCheck,
10
+ TicketCheck,
11
+ Wrench,
12
+ ClipboardCheck,
13
+ Gauge,
14
+ RotateCcw,
15
+ RefreshCw,
16
+ ExternalLink,
17
+ ChevronDown,
18
+ ChevronUp,
19
+ CheckCircle2,
20
+ ArrowRight,
21
+ Clock,
22
+ User,
23
+ Hash,
24
+ Copy,
25
+ Check,
26
+ MessageSquare,
27
+ Lock,
28
+ } from "lucide-react"
29
+ import Link from "next/link"
30
+ import { cn } from "@/lib/utils"
31
+ import { SignatureHistory } from "@/components/signatures/signature-history"
32
+ import type { ElectronicSignatureItem } from "@/components/maintenance/signature-card"
33
+
34
+ // ── Types & Activity Configuration ──────────────────────────────────────────
35
+
36
+ interface SourceGroup {
37
+ key: string
38
+ sourceType: string
39
+ sourceLabel: string
40
+ sourceNumber: string
41
+ sourceHref: string
42
+ signatures: ElectronicSignatureItem[]
43
+ }
44
+
45
+ const ACTIVITY_CONFIG: Record<
46
+ string,
47
+ {
48
+ icon: React.ComponentType<{ className?: string }>
49
+ label: string
50
+ badgeClass: string
51
+ borderClass: string
52
+ bgLight: string
53
+ }
54
+ > = {
55
+ ticket_resolution: {
56
+ icon: TicketCheck,
57
+ label: "Service Ticket",
58
+ badgeClass: "bg-blue-500/10 text-blue-400 border-blue-500/20",
59
+ borderClass: "border-l-blue-500",
60
+ bgLight: "hover:bg-blue-500/5",
61
+ },
62
+ preventive_maintenance: {
63
+ icon: ClipboardCheck,
64
+ label: "Preventive Maintenance (PM)",
65
+ badgeClass: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20",
66
+ borderClass: "border-l-emerald-500",
67
+ bgLight: "hover:bg-emerald-500/5",
68
+ },
69
+ calibration: {
70
+ icon: Gauge,
71
+ label: "Calibration",
72
+ badgeClass: "bg-purple-500/10 text-purple-400 border-purple-500/20",
73
+ borderClass: "border-l-purple-500",
74
+ bgLight: "hover:bg-purple-500/5",
75
+ },
76
+ corrective_maintenance: {
77
+ icon: Wrench,
78
+ label: "Corrective Maintenance",
79
+ badgeClass: "bg-indigo-500/10 text-indigo-400 border-indigo-500/20",
80
+ borderClass: "border-l-indigo-500",
81
+ bgLight: "hover:bg-indigo-500/5",
82
+ },
83
+ engineer_round: {
84
+ icon: RotateCcw,
85
+ label: "Daily Round / Inspection",
86
+ badgeClass: "bg-cyan-500/10 text-cyan-400 border-cyan-500/20",
87
+ borderClass: "border-l-cyan-500",
88
+ bgLight: "hover:bg-cyan-500/5",
89
+ },
90
+ device_status_change: {
91
+ icon: RefreshCw,
92
+ label: "Device Status Change",
93
+ badgeClass: "bg-amber-500/10 text-amber-400 border-amber-500/20",
94
+ borderClass: "border-l-amber-500",
95
+ bgLight: "hover:bg-amber-500/5",
96
+ },
97
+ }
98
+
99
+ const PURPOSE_LABELS: Record<string, string> = {
100
+ accept: "Work Accepted",
101
+ resolve: "Resolution Sign-Off",
102
+ close: "Ticket Closed",
103
+ perform: "Work Executed",
104
+ review: "Technical Review",
105
+ release: "Clinical Release",
106
+ approve: "Approved",
107
+ reject: "Rejected",
108
+ amend: "Amended",
109
+ void: "Voided",
110
+ }
111
+
112
+ function formatShortDate(dateValue: Date | string): string {
113
+ try {
114
+ const d = new Date(dateValue)
115
+ return d.toLocaleDateString("en-US", {
116
+ month: "short",
117
+ day: "numeric",
118
+ hour: "2-digit",
119
+ minute: "2-digit",
120
+ })
121
+ } catch {
122
+ return String(dateValue)
123
+ }
124
+ }
125
+
126
+ function formatFullTimestamp(
127
+ dateValue: Date | string,
128
+ timezone?: string | null
129
+ ): string {
130
+ try {
131
+ const d = new Date(dateValue)
132
+ const formatted = d.toLocaleString("en-US", {
133
+ year: "numeric",
134
+ month: "short",
135
+ day: "2-digit",
136
+ hour: "2-digit",
137
+ minute: "2-digit",
138
+ second: "2-digit",
139
+ hour12: true,
140
+ timeZoneName: "short",
141
+ })
142
+ return timezone && timezone !== "UTC" ? `${formatted} (${timezone})` : formatted
143
+ } catch {
144
+ return String(dateValue)
145
+ }
146
+ }
147
+
148
+ function groupSignaturesBySource(signatures: ElectronicSignatureItem[]): SourceGroup[] {
149
+ const groupMap = new Map<string, SourceGroup>()
150
+
151
+ for (const sig of signatures) {
152
+ const sourceType = (sig as any).sourceType || sig.entityType || "unknown"
153
+ const entityId = sig.entityId || "unknown"
154
+ const key = `${sourceType}:${entityId}`
155
+
156
+ if (!groupMap.has(key)) {
157
+ groupMap.set(key, {
158
+ key,
159
+ sourceType,
160
+ sourceLabel: (sig as any).sourceLabel || "",
161
+ sourceNumber: (sig as any).sourceNumber || "",
162
+ sourceHref: (sig as any).sourceHref || "",
163
+ signatures: [],
164
+ })
165
+ }
166
+ groupMap.get(key)!.signatures.push(sig)
167
+ }
168
+
169
+ // Sort groups: newest signature first
170
+ const groups = Array.from(groupMap.values())
171
+ groups.sort((a, b) => {
172
+ const newestA = Math.max(...a.signatures.map((s) => new Date(s.signedAt).getTime()))
173
+ const newestB = Math.max(...b.signatures.map((s) => new Date(s.signedAt).getTime()))
174
+ return newestB - newestA
175
+ })
176
+
177
+ // Sort signatures within each group chronologically (oldest to newest)
178
+ for (const group of groups) {
179
+ group.signatures.sort((a, b) => new Date(a.signedAt).getTime() - new Date(b.signedAt).getTime())
180
+ }
181
+
182
+ return groups
183
+ }
184
+
185
+ // ── Single Activity Collapsible Card ────────────────────────────────────────
186
+
187
+ function ActivitySignatureCard({ group }: { group: SourceGroup }) {
188
+ const [expanded, setExpanded] = useState(false)
189
+ const [copiedHash, setCopiedHash] = useState<string | null>(null)
190
+
191
+ const config = ACTIVITY_CONFIG[group.sourceType] || {
192
+ icon: ShieldCheck,
193
+ label: "Activity",
194
+ badgeClass: "bg-muted text-muted-foreground border-border",
195
+ borderClass: "border-l-slate-400",
196
+ bgLight: "hover:bg-muted/10",
197
+ }
198
+ const Icon = config.icon
199
+
200
+ const handleCopy = async (hash: string, e: React.MouseEvent) => {
201
+ e.stopPropagation()
202
+ try {
203
+ await navigator.clipboard.writeText(hash)
204
+ setCopiedHash(hash)
205
+ setTimeout(() => setCopiedHash(null), 2000)
206
+ } catch {
207
+ // Fallback
208
+ }
209
+ }
210
+
211
+ const latestSignature = group.signatures[group.signatures.length - 1]
212
+
213
+ return (
214
+ <Card
215
+ className={cn(
216
+ "overflow-hidden border border-l-4 transition-all duration-200",
217
+ config.borderClass,
218
+ expanded ? "shadow-md bg-card" : "bg-card/70 hover:bg-card hover:border-slate-700"
219
+ )}
220
+ >
221
+ {/* ── 1. Compact Clickable Header ────────────────────────────────────── */}
222
+ <div
223
+ onClick={() => setExpanded(!expanded)}
224
+ className="p-4 cursor-pointer select-none space-y-3"
225
+ >
226
+ <div className="flex flex-wrap items-center justify-between gap-3">
227
+ {/* Left: Badge + Identifier + Title */}
228
+ <div className="flex items-center gap-2.5 min-w-0">
229
+ <Badge
230
+ variant="outline"
231
+ className={cn("inline-flex items-center gap-1.5 text-xs font-semibold px-2.5 py-1 shrink-0", config.badgeClass)}
232
+ >
233
+ <Icon className="h-3.5 w-3.5" />
234
+ <span>{config.label}</span>
235
+ </Badge>
236
+
237
+ <div className="min-w-0 flex items-center gap-2">
238
+ <span className="font-semibold text-sm text-foreground truncate">
239
+ {group.sourceNumber || group.sourceLabel || "Record"}
240
+ </span>
241
+ {group.sourceNumber && group.sourceLabel && (
242
+ <span className="text-xs text-muted-foreground truncate hidden sm:inline">
243
+ — {group.sourceLabel}
244
+ </span>
245
+ )}
246
+ </div>
247
+
248
+ {/* Direct Link to Ticket / Task */}
249
+ {group.sourceHref && (
250
+ <Button
251
+ asChild
252
+ variant="ghost"
253
+ size="sm"
254
+ className="h-6 w-6 p-0 text-muted-foreground hover:text-primary hover:bg-primary/10 shrink-0"
255
+ onClick={(e) => e.stopPropagation()}
256
+ title="View full record"
257
+ >
258
+ <Link href={group.sourceHref}>
259
+ <ExternalLink className="h-3.5 w-3.5" />
260
+ </Link>
261
+ </Button>
262
+ )}
263
+ </div>
264
+
265
+ {/* Right: Signature Count + Verified Badge + Toggle Button */}
266
+ <div className="flex items-center gap-2 shrink-0">
267
+ <Badge
268
+ variant="secondary"
269
+ className="text-xs font-normal px-2 py-0.5"
270
+ >
271
+ {group.signatures.length} {group.signatures.length === 1 ? "Signature" : "Signatures"}
272
+ </Badge>
273
+
274
+ <Badge
275
+ variant="outline"
276
+ className="bg-emerald-500/10 text-emerald-500 border-emerald-500/20 text-xs hidden sm:inline-flex items-center gap-1"
277
+ >
278
+ <CheckCircle2 className="h-3 w-3" />
279
+ <span>Verified</span>
280
+ </Badge>
281
+
282
+ <Button
283
+ type="button"
284
+ variant="ghost"
285
+ size="sm"
286
+ className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground gap-1"
287
+ >
288
+ <span>{expanded ? "Collapse" : "View Details"}</span>
289
+ {expanded ? (
290
+ <ChevronUp className="h-3.5 w-3.5 text-primary" />
291
+ ) : (
292
+ <ChevronDown className="h-3.5 w-3.5" />
293
+ )}
294
+ </Button>
295
+ </div>
296
+ </div>
297
+
298
+ {/* ── 2. Compact Lifecycle Progress Stepper ─────────────────────────── */}
299
+ <div className="pt-0.5">
300
+ <div className="flex flex-wrap items-center gap-2 py-0.5">
301
+ {group.signatures.map((sig, idx) => {
302
+ const purposeKey = sig.signaturePurpose?.toLowerCase() || ""
303
+ const purposeText = PURPOSE_LABELS[purposeKey] || sig.signaturePurpose
304
+ const signer = sig.signerNameSnapshot || sig.signerUser?.fullName || "Signer"
305
+
306
+ return (
307
+ <div key={sig.id || idx} className="flex items-center gap-2 shrink-0">
308
+ <div className="flex items-center gap-1.5 px-2.5 py-1 rounded-md bg-muted/60 border border-border/70 text-xs">
309
+ <CheckCircle2 className="h-3.5 w-3.5 text-emerald-500 shrink-0" />
310
+ <span className="font-medium text-foreground">{purposeText}</span>
311
+ <span className="text-muted-foreground text-[11px]">• {signer}</span>
312
+ <span className="text-muted-foreground/70 text-[10px] hidden md:inline">
313
+ ({formatShortDate(sig.signedAt)})
314
+ </span>
315
+ </div>
316
+
317
+ {idx < group.signatures.length - 1 && (
318
+ <ArrowRight className="h-3 w-3 text-muted-foreground/50 shrink-0" />
319
+ )}
320
+ </div>
321
+ )
322
+ })}
323
+ </div>
324
+ </div>
325
+ </div>
326
+
327
+ {/* ── 3. Expanded Details Section ────────────────────────────────────── */}
328
+ {expanded && (
329
+ <div className="border-t bg-muted/20 px-4 py-4 space-y-4 animate-in fade-in-50 duration-150">
330
+ <div className="flex items-center justify-between text-xs text-muted-foreground pb-1">
331
+ <span className="font-semibold uppercase tracking-wider text-[11px]">
332
+ Signatures Breakdown
333
+ </span>
334
+ <span>
335
+ {group.signatures.length} cryptographically verified {group.signatures.length === 1 ? "record" : "records"}
336
+ </span>
337
+ </div>
338
+
339
+ <div className="space-y-3">
340
+ {group.signatures.map((sig, idx) => {
341
+ const purposeKey = sig.signaturePurpose?.toLowerCase() || ""
342
+ const purposeText = PURPOSE_LABELS[purposeKey] || sig.signaturePurpose
343
+ const signer = sig.signerNameSnapshot || sig.signerUser?.fullName || "Authorized Signer"
344
+ const role = sig.signerRoleSnapshot || "Biomedical Specialist"
345
+ const isCopied = copiedHash === sig.signedContentHashSha256
346
+
347
+ return (
348
+ <div
349
+ key={sig.id || idx}
350
+ className="rounded-lg border bg-card p-3.5 space-y-2.5 transition-colors"
351
+ >
352
+ {/* Signer & Stage Info */}
353
+ <div className="flex flex-wrap items-start justify-between gap-2">
354
+ <div className="flex items-start gap-2.5 min-w-0">
355
+ <div className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
356
+ <User className="h-3.5 w-3.5" />
357
+ </div>
358
+ <div className="min-w-0">
359
+ <div className="flex items-center gap-1.5 flex-wrap">
360
+ <span className="font-semibold text-xs text-foreground">
361
+ {signer}
362
+ </span>
363
+ <span className="text-[11px] text-muted-foreground">
364
+ • {role}
365
+ </span>
366
+ </div>
367
+ <div className="flex items-center gap-1 text-[11px] text-muted-foreground mt-0.5">
368
+ <Clock className="h-3 w-3 shrink-0" />
369
+ <span>{formatFullTimestamp(sig.signedAt, sig.displayTimezone)}</span>
370
+ </div>
371
+ </div>
372
+ </div>
373
+
374
+ <div className="flex items-center gap-1.5 shrink-0">
375
+ <Badge
376
+ variant="outline"
377
+ className="text-xs font-medium bg-primary/5 text-primary border-primary/20"
378
+ >
379
+ {purposeText}
380
+ </Badge>
381
+ <Badge
382
+ variant="outline"
383
+ className="text-[10px] bg-emerald-500/10 text-emerald-500 border-emerald-500/20 capitalize font-medium"
384
+ >
385
+ {sig.signatureStatus || "active"}
386
+ </Badge>
387
+ </div>
388
+ </div>
389
+
390
+ {/* Attestation Statement */}
391
+ {sig.attestationTextVersion && (
392
+ <blockquote className="rounded-md border-l-2 border-primary/40 bg-muted/40 px-3 py-1.5 text-xs italic text-foreground/90 leading-relaxed">
393
+ &quot;{sig.attestationTextVersion}&quot;
394
+ </blockquote>
395
+ )}
396
+
397
+ {/* Signer Comments */}
398
+ {sig.comments && (
399
+ <div className="flex items-start gap-2 rounded-md bg-muted/30 p-2 text-xs text-foreground/90 border border-border/50">
400
+ <MessageSquare className="h-3.5 w-3.5 mt-0.5 shrink-0 text-muted-foreground" />
401
+ <div className="min-w-0 space-y-0.5">
402
+ <span className="font-semibold text-[10px] uppercase tracking-wider text-muted-foreground block">
403
+ Signer Note:
404
+ </span>
405
+ <p className="whitespace-pre-wrap">{sig.comments}</p>
406
+ </div>
407
+ </div>
408
+ )}
409
+
410
+ {/* SHA-256 Hash */}
411
+ {sig.signedContentHashSha256 && (
412
+ <div className="flex items-center justify-between gap-2 rounded-md border bg-muted/40 px-2.5 py-1.5">
413
+ <div className="flex items-center gap-1.5 min-w-0 font-mono text-[11px] text-muted-foreground">
414
+ <Hash className="h-3.5 w-3.5 shrink-0 text-primary/70" />
415
+ <span className="truncate selection:bg-primary/20" title={sig.signedContentHashSha256}>
416
+ SHA-256: {sig.signedContentHashSha256}
417
+ </span>
418
+ </div>
419
+
420
+ <Button
421
+ type="button"
422
+ variant="ghost"
423
+ size="sm"
424
+ onClick={(e) => handleCopy(sig.signedContentHashSha256, e)}
425
+ className="h-5 gap-1 px-1.5 text-[10px] text-muted-foreground hover:text-foreground shrink-0"
426
+ title="Copy SHA-256 hash"
427
+ >
428
+ {isCopied ? (
429
+ <>
430
+ <Check className="h-3 w-3 text-emerald-500" />
431
+ <span className="text-emerald-500 font-medium">Copied</span>
432
+ </>
433
+ ) : (
434
+ <>
435
+ <Copy className="h-3 w-3" />
436
+ <span>Copy</span>
437
+ </>
438
+ )}
439
+ </Button>
440
+ </div>
441
+ )}
442
+ </div>
443
+ )
444
+ })}
445
+ </div>
446
+ </div>
447
+ )}
448
+ </Card>
449
+ )
450
+ }
451
+
452
+ // ── Main DeviceSignaturesTab ────────────────────────────────────────────────
453
+
454
+ interface DeviceSignaturesTabProps {
455
+ signatures: any[]
456
+ signatureEvents: any[]
457
+ deviceId?: string
458
+ deviceName?: string
459
+ }
460
+
461
+ export function DeviceSignaturesTab({
462
+ signatures = [],
463
+ signatureEvents = [],
464
+ deviceId,
465
+ deviceName,
466
+ }: DeviceSignaturesTabProps) {
467
+ const [showHashChain, setShowHashChain] = useState(false)
468
+ const hasSignatures = signatures && signatures.length > 0
469
+
470
+ // Group signatures by source (tickets, maintenance, pm, rounds, status changes)
471
+ const sourceGroups = hasSignatures
472
+ ? groupSignaturesBySource(signatures as ElectronicSignatureItem[])
473
+ : []
474
+
475
+ return (
476
+ <div className="space-y-4">
477
+ {/* Sleek Top Banner */}
478
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 p-3.5 bg-muted/20 border rounded-lg">
479
+ <div className="flex items-center gap-2.5">
480
+ <div className="h-8 w-8 rounded-full bg-primary/10 text-primary flex items-center justify-center shrink-0">
481
+ <ShieldCheck className="w-4 h-4" />
482
+ </div>
483
+ <div>
484
+ <h3 className="text-sm font-semibold text-foreground">
485
+ Electronic Signatures & Sign-Offs
486
+ </h3>
487
+ <p className="text-xs text-muted-foreground">
488
+ Cryptographically verified sign-offs for tickets, maintenance, and device operations.
489
+ </p>
490
+ </div>
491
+ </div>
492
+
493
+ {hasSignatures && (
494
+ <div className="flex items-center gap-2 text-xs text-muted-foreground self-end sm:self-center">
495
+ <span className="font-semibold text-foreground">
496
+ {sourceGroups.length} {sourceGroups.length === 1 ? "Signed Record" : "Signed Records"}
497
+ </span>
498
+ <span>•</span>
499
+ <span>{signatures.length} {signatures.length === 1 ? "signature" : "signatures"}</span>
500
+ </div>
501
+ )}
502
+ </div>
503
+
504
+ {/* Main Signatures Content */}
505
+ {hasSignatures ? (
506
+ <div className="space-y-3">
507
+ {/* Each Activity gets exactly ONE clean, collapsible card */}
508
+ {sourceGroups.map((group) => (
509
+ <ActivitySignatureCard key={group.key} group={group} />
510
+ ))}
511
+
512
+ {/* Cryptographic Hash Chain Audit Trail (Collapsible at bottom) */}
513
+ {signatureEvents && signatureEvents.length > 0 && (
514
+ <div className="pt-2">
515
+ <Card className="border-dashed bg-card/40">
516
+ <div
517
+ onClick={() => setShowHashChain(!showHashChain)}
518
+ className="p-3 cursor-pointer select-none flex items-center justify-between text-xs text-muted-foreground hover:text-foreground transition-colors"
519
+ >
520
+ <div className="flex items-center gap-2">
521
+ <Lock className="w-3.5 h-3.5 text-primary" />
522
+ <span className="font-medium">
523
+ Cryptographic Audit Trail & Hash Chain ({signatureEvents.length} Events)
524
+ </span>
525
+ </div>
526
+ <Button variant="ghost" size="sm" className="h-6 px-2 text-xs gap-1">
527
+ <span>{showHashChain ? "Hide Chain" : "Inspect Chain"}</span>
528
+ {showHashChain ? (
529
+ <ChevronUp className="h-3.5 w-3.5" />
530
+ ) : (
531
+ <ChevronDown className="h-3.5 w-3.5" />
532
+ )}
533
+ </Button>
534
+ </div>
535
+
536
+ {showHashChain && (
537
+ <div className="px-3 pb-3 border-t pt-3">
538
+ <SignatureHistory events={signatureEvents} />
539
+ </div>
540
+ )}
541
+ </Card>
542
+ </div>
543
+ )}
544
+ </div>
545
+ ) : (
546
+ /* Explanatory Empty State when 0 signatures exist */
547
+ <Card className="border-dashed">
548
+ <CardContent className="p-6 sm:p-8 space-y-6">
549
+ <div className="text-center max-w-lg mx-auto space-y-2">
550
+ <div className="w-12 h-12 rounded-full bg-primary/10 text-primary flex items-center justify-center mx-auto">
551
+ <ShieldCheck className="w-6 h-6" />
552
+ </div>
553
+ <h4 className="text-base font-semibold">No Electronic Signatures Recorded Yet</h4>
554
+ <p className="text-xs text-muted-foreground leading-relaxed">
555
+ Signatures are automatically captured with cryptographic verification when biomedical engineers and managers complete formal work orders on this equipment.
556
+ </p>
557
+ </div>
558
+
559
+ {/* How Signatures Are Created */}
560
+ <div className="grid sm:grid-cols-3 gap-4 pt-2">
561
+ <div className="p-3.5 rounded-lg border bg-card space-y-1.5 text-left">
562
+ <div className="flex items-center gap-2 text-xs font-semibold text-foreground">
563
+ <span className="w-5 h-5 rounded-full bg-amber-500/10 text-amber-500 flex items-center justify-center text-[10px] font-bold">1</span>
564
+ <span>Work Order Sign-Off</span>
565
+ </div>
566
+ <p className="text-[11px] text-muted-foreground leading-normal">
567
+ Engineer executes repair or calibration checklist and signs with credentials to certify work performed.
568
+ </p>
569
+ </div>
570
+
571
+ <div className="p-3.5 rounded-lg border bg-card space-y-1.5 text-left">
572
+ <div className="flex items-center gap-2 text-xs font-semibold text-foreground">
573
+ <span className="w-5 h-5 rounded-full bg-blue-500/10 text-blue-500 flex items-center justify-center text-[10px] font-bold">2</span>
574
+ <span>Supervisor Review</span>
575
+ </div>
576
+ <p className="text-[11px] text-muted-foreground leading-normal">
577
+ Biomedical manager or peer reviews electrical safety measurements and co-signs the task.
578
+ </p>
579
+ </div>
580
+
581
+ <div className="p-3.5 rounded-lg border bg-card space-y-1.5 text-left">
582
+ <div className="flex items-center gap-2 text-xs font-semibold text-foreground">
583
+ <span className="w-5 h-5 rounded-full bg-emerald-500/10 text-emerald-500 flex items-center justify-center text-[10px] font-bold">3</span>
584
+ <span>Clinical Release</span>
585
+ </div>
586
+ <p className="text-[11px] text-muted-foreground leading-normal">
587
+ Final release certificate signed to officially declare the device safe for patient use and return to the ICU.
588
+ </p>
589
+ </div>
590
+ </div>
591
+
592
+ {/* Action to Work Orders */}
593
+ <div className="flex justify-center pt-2">
594
+ <Button asChild variant="outline" className="gap-2">
595
+ <Link href="/maintenance/tasks">
596
+ <Wrench className="w-4 h-4 text-primary" />
597
+ <span>Go to Work Orders</span>
598
+ <ArrowRight className="w-3.5 h-3.5 text-muted-foreground" />
599
+ </Link>
600
+ </Button>
601
+ </div>
602
+ </CardContent>
603
+ </Card>
604
+ )}
605
+ </div>
606
+ )
607
+ }