@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,139 @@
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+ import { Ticket, Plus, AlertCircle, Clock, CheckCircle2, User, ChevronRight } from "lucide-react"
5
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
6
+ import { Button } from "@/components/ui/button"
7
+ import { TicketStatusBadge, TicketPriorityBadge } from "@/components/tickets/ticket-badges"
8
+
9
+ interface DeviceTicketsTabProps {
10
+ tickets: any[]
11
+ deviceId: string
12
+ deviceName: string
13
+ }
14
+
15
+ export function DeviceTicketsTab({ tickets = [], deviceId, deviceName }: DeviceTicketsTabProps) {
16
+ const total = tickets.length
17
+ const openCount = tickets.filter(t => !["resolved", "closed", "cancelled"].includes(t.statusCode)).length
18
+ const inProgressCount = tickets.filter(t => t.statusCode === "in_progress").length
19
+ const resolvedCount = tickets.filter(t => ["resolved", "closed"].includes(t.statusCode)).length
20
+
21
+ return (
22
+ <div className="space-y-6">
23
+ {/* 1. Summary Metrics */}
24
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
25
+ <Card className="p-4 flex flex-col items-center justify-center text-center">
26
+ <span className="text-2xl font-bold">{total}</span>
27
+ <span className="text-xs text-muted-foreground mt-0.5">Total Tickets</span>
28
+ </Card>
29
+ <Card className="p-4 flex flex-col items-center justify-center text-center">
30
+ <span className="text-2xl font-bold text-amber-600">{openCount}</span>
31
+ <span className="text-xs text-muted-foreground mt-0.5">Open / Active</span>
32
+ </Card>
33
+ <Card className="p-4 flex flex-col items-center justify-center text-center">
34
+ <span className="text-2xl font-bold text-blue-600">{inProgressCount}</span>
35
+ <span className="text-xs text-muted-foreground mt-0.5">In Progress</span>
36
+ </Card>
37
+ <Card className="p-4 flex flex-col items-center justify-center text-center">
38
+ <span className="text-2xl font-bold text-emerald-600">{resolvedCount}</span>
39
+ <span className="text-xs text-muted-foreground mt-0.5">Resolved / Closed</span>
40
+ </Card>
41
+ </div>
42
+
43
+ {/* 2. Ticket History Section Header & Action */}
44
+ <div className="flex items-center justify-between">
45
+ <div>
46
+ <h3 className="text-base font-semibold">Service &amp; Problem History</h3>
47
+ <p className="text-xs text-muted-foreground">
48
+ All incident reports and service tickets submitted for this device
49
+ </p>
50
+ </div>
51
+ <Button asChild size="sm" className="gap-1.5">
52
+ <Link href={`/tickets/create?deviceId=${deviceId}&source=device_profile`}>
53
+ <Plus className="w-4 h-4" />
54
+ Report a Problem
55
+ </Link>
56
+ </Button>
57
+ </div>
58
+
59
+ {/* 3. Ticket List */}
60
+ {tickets.length === 0 ? (
61
+ <div className="text-center py-12 border rounded-lg bg-muted/20 space-y-3">
62
+ <Ticket className="w-12 h-12 text-muted-foreground mx-auto opacity-40" />
63
+ <div>
64
+ <h4 className="text-sm font-medium text-foreground">No Tickets Reported</h4>
65
+ <p className="text-xs text-muted-foreground max-w-sm mx-auto mt-1">
66
+ This equipment currently has no reported problems or maintenance tickets in the registry.
67
+ </p>
68
+ </div>
69
+ <Button asChild variant="outline" size="sm" className="mt-2">
70
+ <Link href={`/tickets/create?deviceId=${deviceId}&source=device_profile`}>
71
+ <Plus className="w-4 h-4 mr-1.5" />
72
+ Report First Ticket
73
+ </Link>
74
+ </Button>
75
+ </div>
76
+ ) : (
77
+ <div className="space-y-3">
78
+ {tickets.map((ticket) => {
79
+ const reportedDate = ticket.reportedAt || ticket.createdAt
80
+ const formattedDate = reportedDate
81
+ ? new Date(reportedDate).toISOString().replace("T", " ").substring(0, 16)
82
+ : "Unknown date"
83
+
84
+ return (
85
+ <Link
86
+ key={ticket.id}
87
+ href={`/tickets/${ticket.id}`}
88
+ className="block group"
89
+ >
90
+ <Card className="transition-all hover:border-primary/50 hover:shadow-xs">
91
+ <CardContent className="p-4 sm:p-5">
92
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
93
+ {/* Left: Info */}
94
+ <div className="space-y-1.5 flex-1 min-w-0">
95
+ <div className="flex items-center gap-2 flex-wrap">
96
+ <span className="font-mono text-xs font-semibold text-primary">
97
+ {ticket.ticketNumber}
98
+ </span>
99
+ <TicketPriorityBadge priority={ticket.priorityCode} />
100
+ <TicketStatusBadge status={ticket.statusCode} />
101
+ </div>
102
+ <h4 className="font-medium text-sm text-foreground group-hover:text-primary transition-colors line-clamp-1">
103
+ {ticket.title}
104
+ </h4>
105
+ {ticket.description && (
106
+ <p className="text-xs text-muted-foreground line-clamp-2">
107
+ {ticket.description}
108
+ </p>
109
+ )}
110
+ </div>
111
+
112
+ {/* Right: Meta & Arrow */}
113
+ <div className="flex sm:flex-col items-center sm:items-end justify-between sm:justify-center text-xs text-muted-foreground shrink-0 border-t sm:border-t-0 pt-2 sm:pt-0 gap-1.5">
114
+ <div className="flex items-center gap-1">
115
+ <Clock className="w-3.5 h-3.5" />
116
+ <span suppressHydrationWarning>{formattedDate} UTC</span>
117
+ </div>
118
+ {ticket.assignedEngineer?.fullName && (
119
+ <div className="flex items-center gap-1 text-foreground/80">
120
+ <User className="w-3.5 h-3.5 text-muted-foreground" />
121
+ <span>{ticket.assignedEngineer.fullName}</span>
122
+ </div>
123
+ )}
124
+ <span className="text-primary group-hover:translate-x-0.5 transition-transform hidden sm:inline-flex items-center gap-0.5 text-xs font-medium">
125
+ View details <ChevronRight className="w-3.5 h-3.5" />
126
+ </span>
127
+ </div>
128
+ </div>
129
+ </CardContent>
130
+ </Card>
131
+ </Link>
132
+ )
133
+ })}
134
+ </div>
135
+ )}
136
+ </div>
137
+ )
138
+ }
139
+
@@ -0,0 +1,124 @@
1
+ import { DeviceStatusBadge } from "./device-badges"
2
+ import { User, Calendar } from "lucide-react"
3
+
4
+ interface StatusEntry {
5
+ id: string
6
+ previousStatusCode?: string | null
7
+ newStatusCode: string
8
+ reason?: string | null
9
+ notes?: string | null
10
+ effectiveAt: Date | string
11
+ changedByUser?: { fullName: string } | null
12
+ }
13
+
14
+ interface LocationEntry {
15
+ id: string
16
+ newHospital?: { name: string } | null
17
+ newDepartment?: { name: string } | null
18
+ transferReason: string
19
+ notes?: string | null
20
+ effectiveAt: Date | string
21
+ changedByUser?: { fullName: string } | null
22
+ }
23
+
24
+ export function DeviceStatusTimeline({ entries }: { entries: StatusEntry[] }) {
25
+ if (!entries || entries.length === 0) {
26
+ return <p className="text-sm text-muted-foreground py-4">No status history recorded.</p>
27
+ }
28
+
29
+ return (
30
+ <div className="relative">
31
+ <div className="absolute left-4 top-0 bottom-0 w-px bg-border" />
32
+ <div className="space-y-6">
33
+ {entries.map((entry) => (
34
+ <div key={entry.id} className="relative pl-10">
35
+ <div className="absolute left-2.5 top-1.5 w-3 h-3 rounded-full bg-primary border-2 border-background" />
36
+ <div className="space-y-1">
37
+ <div className="flex items-center gap-2 flex-wrap">
38
+ {entry.previousStatusCode && (
39
+ <>
40
+ <DeviceStatusBadge status={entry.previousStatusCode} />
41
+ <span className="text-muted-foreground">→</span>
42
+ </>
43
+ )}
44
+ <DeviceStatusBadge status={entry.newStatusCode} />
45
+ </div>
46
+ {entry.reason && (
47
+ <p className="text-sm text-foreground">{entry.reason}</p>
48
+ )}
49
+ {entry.notes && (
50
+ <p className="text-xs text-muted-foreground italic">{entry.notes}</p>
51
+ )}
52
+ <div className="flex items-center gap-4 text-xs text-muted-foreground">
53
+ <span className="flex items-center gap-1">
54
+ <Calendar className="w-3 h-3" />
55
+ {new Date(entry.effectiveAt).toLocaleString()}
56
+ </span>
57
+ {entry.changedByUser && (
58
+ <span className="flex items-center gap-1">
59
+ <User className="w-3 h-3" />
60
+ {entry.changedByUser.fullName}
61
+ </span>
62
+ )}
63
+ </div>
64
+ </div>
65
+ </div>
66
+ ))}
67
+ </div>
68
+ </div>
69
+ )
70
+ }
71
+
72
+ const transferReasonLabels: Record<string, string> = {
73
+ relocation: "Relocation",
74
+ temporary_loan: "Temporary Loan",
75
+ department_change: "Department Change",
76
+ correction: "Correction",
77
+ replacement: "Replacement",
78
+ workshop: "To Workshop",
79
+ storage: "To Storage",
80
+ other: "Other",
81
+ }
82
+
83
+ export function DeviceLocationTimeline({ entries }: { entries: LocationEntry[] }) {
84
+ if (!entries || entries.length === 0) {
85
+ return <p className="text-sm text-muted-foreground py-4">No location history recorded.</p>
86
+ }
87
+
88
+ return (
89
+ <div className="relative">
90
+ <div className="absolute left-4 top-0 bottom-0 w-px bg-border" />
91
+ <div className="space-y-6">
92
+ {entries.map((entry) => (
93
+ <div key={entry.id} className="relative pl-10">
94
+ <div className="absolute left-2.5 top-1.5 w-3 h-3 rounded-full bg-blue-500 border-2 border-background" />
95
+ <div className="space-y-1">
96
+ <p className="text-sm font-medium">
97
+ {entry.newHospital?.name || "Unknown Hospital"} — {entry.newDepartment?.name || "Unknown Department"}
98
+ </p>
99
+ <p className="text-xs text-muted-foreground">
100
+ Reason: {transferReasonLabels[entry.transferReason] || entry.transferReason}
101
+ </p>
102
+ {entry.notes && (
103
+ <p className="text-xs text-muted-foreground italic">{entry.notes}</p>
104
+ )}
105
+ <div className="flex items-center gap-4 text-xs text-muted-foreground">
106
+ <span className="flex items-center gap-1">
107
+ <Calendar className="w-3 h-3" />
108
+ {new Date(entry.effectiveAt).toLocaleString()}
109
+ </span>
110
+ {entry.changedByUser && (
111
+ <span className="flex items-center gap-1">
112
+ <User className="w-3 h-3" />
113
+ {entry.changedByUser.fullName}
114
+ </span>
115
+ )}
116
+ </div>
117
+ </div>
118
+ </div>
119
+ ))}
120
+ </div>
121
+ </div>
122
+ )
123
+ }
124
+
@@ -0,0 +1,167 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Button } from "@/components/ui/button"
5
+ import { Camera, CameraOff, QrCode, Loader2 } from "lucide-react"
6
+ import { useRouter } from "next/navigation"
7
+ import { toast } from "sonner"
8
+ import { playQrBeep, unlockAudio } from "@/lib/utils/sound"
9
+
10
+ export function QrCameraScanner() {
11
+ const router = useRouter()
12
+ const [isScanning, setIsScanning] = React.useState(false)
13
+ const [hasCamera, setHasCamera] = React.useState<boolean | null>(null)
14
+ const [errorMsg, setErrorMsg] = React.useState<string | null>(null)
15
+ const scannerRef = React.useRef<any>(null)
16
+
17
+ // Handle scanned text
18
+ const handleScanSuccess = React.useCallback(
19
+ (decodedText: string) => {
20
+ // Stop scanner immediately
21
+ if (scannerRef.current) {
22
+ scannerRef.current.clear().catch(() => {})
23
+ }
24
+ setIsScanning(false)
25
+
26
+ // Play beep sound to indicate successful capture (works on native devices)
27
+ playQrBeep()
28
+
29
+ toast.success("QR Code detected!")
30
+
31
+ // Process decoded text
32
+ const cleanText = decodedText.trim()
33
+
34
+ // 1. If it's a full URL containing /scan/
35
+ if (cleanText.includes("/scan/")) {
36
+ const parts = cleanText.split("/scan/")
37
+ const code = parts[parts.length - 1].split("?")[0].replace(/\/$/, "")
38
+ router.push(`/scan/${code}`)
39
+ return
40
+ }
41
+
42
+ // 2. If it's directly an opaque reference
43
+ if (cleanText.startsWith("dv_")) {
44
+ router.push(`/scan/${cleanText}`)
45
+ return
46
+ }
47
+
48
+ // 3. Fallback: Search by asset number
49
+ router.push(`/devices?search=${encodeURIComponent(cleanText)}`)
50
+ },
51
+ [router]
52
+ )
53
+
54
+ const startScanner = async () => {
55
+ setErrorMsg(null)
56
+ setIsScanning(true)
57
+
58
+ // Unlock Web Audio on user gesture so beep can play on mobile devices
59
+ unlockAudio()
60
+
61
+ try {
62
+ const { Html5QrcodeScanner } = await import("html5-qrcode")
63
+
64
+ // Small delay to ensure the container div exists
65
+ setTimeout(() => {
66
+ try {
67
+ const scanner = new Html5QrcodeScanner(
68
+ "bemms-qr-reader",
69
+ {
70
+ fps: 10,
71
+ qrbox: { width: 250, height: 250 },
72
+ aspectRatio: 1.0,
73
+ showTorchButtonIfSupported: true,
74
+ },
75
+ false
76
+ )
77
+
78
+ scannerRef.current = scanner
79
+
80
+ scanner.render(
81
+ (decodedText) => handleScanSuccess(decodedText),
82
+ (error) => {
83
+ // Non-fatal scanning frames error, can be safely ignored
84
+ }
85
+ )
86
+ } catch (err: any) {
87
+ setErrorMsg(err.message || "Failed to initialize camera")
88
+ setIsScanning(false)
89
+ }
90
+ }, 100)
91
+ } catch (err: any) {
92
+ setErrorMsg("Camera scanner library could not be loaded")
93
+ setIsScanning(false)
94
+ }
95
+ }
96
+
97
+ const stopScanner = () => {
98
+ if (scannerRef.current) {
99
+ scannerRef.current.clear().catch(() => {})
100
+ scannerRef.current = null
101
+ }
102
+ setIsScanning(false)
103
+ }
104
+
105
+ React.useEffect(() => {
106
+ return () => {
107
+ if (scannerRef.current) {
108
+ scannerRef.current.clear().catch(() => {})
109
+ }
110
+ }
111
+ }, [])
112
+
113
+ return (
114
+ <div className="space-y-4">
115
+ {/* Scanner Viewport Container */}
116
+ <div className="relative aspect-square max-w-sm mx-auto bg-muted rounded-xl flex flex-col items-center justify-center border-2 border-dashed overflow-hidden">
117
+ {isScanning ? (
118
+ <div id="bemms-qr-reader" className="w-full h-full" />
119
+ ) : (
120
+ <div className="flex flex-col items-center justify-center p-6 text-center gap-3">
121
+ <div className="h-16 w-16 rounded-full bg-primary/10 text-primary flex items-center justify-center">
122
+ <QrCode className="w-8 h-8" />
123
+ </div>
124
+ <div className="space-y-1">
125
+ <p className="font-semibold text-sm">Camera QR Scanner</p>
126
+ <p className="text-xs text-muted-foreground max-w-xs">
127
+ Position the BEMMS device QR code within camera view to open the profile.
128
+ </p>
129
+ </div>
130
+
131
+ {errorMsg && (
132
+ <p className="text-xs text-destructive font-medium px-2 bg-destructive/10 py-1 rounded">
133
+ {errorMsg}
134
+ </p>
135
+ )}
136
+
137
+ <Button
138
+ type="button"
139
+ variant="default"
140
+ onClick={startScanner}
141
+ className="mt-2 gap-2"
142
+ >
143
+ <Camera className="w-4 h-4" />
144
+ Enable Camera
145
+ </Button>
146
+ </div>
147
+ )}
148
+ </div>
149
+
150
+ {isScanning && (
151
+ <div className="flex justify-center">
152
+ <Button
153
+ type="button"
154
+ variant="outline"
155
+ size="sm"
156
+ onClick={stopScanner}
157
+ className="gap-2 text-destructive hover:bg-destructive/10"
158
+ >
159
+ <CameraOff className="w-4 h-4" />
160
+ Stop Camera
161
+ </Button>
162
+ </div>
163
+ )}
164
+ </div>
165
+ )
166
+ }
167
+
@@ -0,0 +1,139 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { QRCodeCanvas, QRCodeSVG } from "qrcode.react"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Download, Copy, Check, QrCode } from "lucide-react"
7
+ import { toast } from "sonner"
8
+ import { cn } from "@/lib/utils"
9
+
10
+ export interface QrCodeDisplayProps {
11
+ value: string;
12
+ size?: number;
13
+ assetNumber?: string;
14
+ deviceName?: string;
15
+ className?: string;
16
+ showDownloadOptions?: boolean;
17
+ }
18
+
19
+ export function QrCodeDisplay({
20
+ value,
21
+ size = 200,
22
+ assetNumber = "device",
23
+ deviceName = "Medical Device",
24
+ className,
25
+ showDownloadOptions = true,
26
+ }: QrCodeDisplayProps) {
27
+ const [copied, setCopied] = React.useState(false)
28
+ const canvasRef = React.useRef<HTMLDivElement>(null)
29
+
30
+ const handleDownloadPng = () => {
31
+ if (!canvasRef.current) return
32
+ const canvas = canvasRef.current.querySelector("canvas")
33
+ if (!canvas) {
34
+ toast.error("Could not find QR canvas element")
35
+ return
36
+ }
37
+
38
+ try {
39
+ // Create full printable resolution image with border & label
40
+ const padding = 24
41
+ const textHeight = 48
42
+ const exportCanvas = document.createElement("canvas")
43
+ exportCanvas.width = canvas.width + padding * 2
44
+ exportCanvas.height = canvas.height + padding * 2 + textHeight
45
+ const ctx = exportCanvas.getContext("2d")
46
+
47
+ if (ctx) {
48
+ // Fill white background
49
+ ctx.fillStyle = "#ffffff"
50
+ ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height)
51
+
52
+ // Draw QR
53
+ ctx.drawImage(canvas, padding, padding)
54
+
55
+ // Draw label text
56
+ ctx.fillStyle = "#0f172a"
57
+ ctx.font = "bold 14px sans-serif"
58
+ ctx.textAlign = "center"
59
+ ctx.fillText(assetNumber, exportCanvas.width / 2, canvas.height + padding + 18)
60
+
61
+ ctx.fillStyle = "#64748b"
62
+ ctx.font = "11px sans-serif"
63
+ ctx.fillText(deviceName.slice(0, 30), exportCanvas.width / 2, canvas.height + padding + 36)
64
+
65
+ const link = document.createElement("a")
66
+ const safeName = assetNumber.replace(/[^a-zA-Z0-9_-]/g, "_")
67
+ link.download = `BEMMS_QR_${safeName}.png`
68
+ link.href = exportCanvas.toDataURL("image/png")
69
+ link.click()
70
+ toast.success("QR code image downloaded successfully")
71
+ }
72
+ } catch (err: any) {
73
+ toast.error("Failed to download image: " + err.message)
74
+ }
75
+ }
76
+
77
+ const handleCopyUrl = async () => {
78
+ try {
79
+ await navigator.clipboard.writeText(value)
80
+ setCopied(true)
81
+ toast.success("QR URL copied to clipboard")
82
+ setTimeout(() => setCopied(false), 2000)
83
+ } catch {
84
+ toast.error("Failed to copy to clipboard")
85
+ }
86
+ }
87
+
88
+ return (
89
+ <div className={cn("flex flex-col items-center gap-4", className)}>
90
+ <div
91
+ ref={canvasRef}
92
+ className="bg-white p-4 rounded-xl shadow-xs border flex items-center justify-center"
93
+ >
94
+ <QRCodeCanvas
95
+ value={value}
96
+ size={size}
97
+ level="H"
98
+ includeMargin={false}
99
+ />
100
+ </div>
101
+
102
+ {showDownloadOptions && (
103
+ <div className="flex flex-wrap items-center justify-center gap-2">
104
+ <Button
105
+ type="button"
106
+ variant="outline"
107
+ size="sm"
108
+ onClick={handleDownloadPng}
109
+ className="gap-1.5"
110
+ >
111
+ <Download className="w-4 h-4 text-primary" />
112
+ <span>Download PNG</span>
113
+ </Button>
114
+
115
+ <Button
116
+ type="button"
117
+ variant="ghost"
118
+ size="sm"
119
+ onClick={handleCopyUrl}
120
+ className="gap-1.5"
121
+ >
122
+ {copied ? (
123
+ <>
124
+ <Check className="w-4 h-4 text-emerald-600" />
125
+ <span className="text-emerald-600">Copied</span>
126
+ </>
127
+ ) : (
128
+ <>
129
+ <Copy className="w-4 h-4" />
130
+ <span>Copy Link</span>
131
+ </>
132
+ )}
133
+ </Button>
134
+ </div>
135
+ )}
136
+ </div>
137
+ )
138
+ }
139
+