@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,333 @@
1
+ 'use client';
2
+
3
+ import { useState, useMemo } from 'react';
4
+ import Link from 'next/link';
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
6
+ import { Button } from '@/components/ui/button';
7
+ import { Input } from '@/components/ui/input';
8
+ import { Badge } from '@/components/ui/badge';
9
+ import { DeviceStatusBadge } from '@/components/devices/device-badges';
10
+ import { PageHeader } from '@/components/shared/page-header';
11
+ import {
12
+ Search,
13
+ Monitor,
14
+ AlertTriangle,
15
+ Wrench,
16
+ CheckCircle,
17
+ PlusCircle,
18
+ TicketCheck,
19
+ } from 'lucide-react';
20
+ import { cn } from '@/lib/utils';
21
+
22
+ interface DeviceItem {
23
+ id: string;
24
+ name: string;
25
+ assetNumber: string;
26
+ currentStatusCode: string;
27
+ locationName: string;
28
+ categoryName: string;
29
+ manufacturerName: string;
30
+ activeTickets: {
31
+ id: string;
32
+ ticketNumber: string;
33
+ title: string;
34
+ statusCode: string;
35
+ }[];
36
+ }
37
+
38
+ interface MyDepartmentViewProps {
39
+ department: {
40
+ name: string;
41
+ hospitalName: string;
42
+ };
43
+ devices: DeviceItem[];
44
+ stats: {
45
+ total: number;
46
+ operational: number;
47
+ underMaintenance: number;
48
+ outOfService: number;
49
+ };
50
+ }
51
+
52
+ type StatusFilter = 'all' | 'operational' | 'maintenance' | 'out_of_service';
53
+
54
+ export function MyDepartmentView({
55
+ department,
56
+ devices,
57
+ stats,
58
+ }: MyDepartmentViewProps) {
59
+ const [search, setSearch] = useState('');
60
+ const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
61
+
62
+ const filteredDevices = useMemo(() => {
63
+ let filtered = devices;
64
+
65
+ // Search filter
66
+ if (search.trim()) {
67
+ const q = search.toLowerCase();
68
+ filtered = filtered.filter(
69
+ (d) =>
70
+ d.name.toLowerCase().includes(q) ||
71
+ d.assetNumber.toLowerCase().includes(q) ||
72
+ d.locationName.toLowerCase().includes(q) ||
73
+ d.categoryName.toLowerCase().includes(q) ||
74
+ d.manufacturerName.toLowerCase().includes(q)
75
+ );
76
+ }
77
+
78
+ // Status filter
79
+ if (statusFilter === 'operational') {
80
+ filtered = filtered.filter(
81
+ (d) => d.currentStatusCode === 'operational'
82
+ );
83
+ } else if (statusFilter === 'maintenance') {
84
+ filtered = filtered.filter((d) =>
85
+ [
86
+ 'under_repair',
87
+ 'under_maintenance',
88
+ 'waiting_for_parts',
89
+ 'under_investigation',
90
+ ].includes(d.currentStatusCode)
91
+ );
92
+ } else if (statusFilter === 'out_of_service') {
93
+ filtered = filtered.filter((d) =>
94
+ ['out_of_service', 'decommissioned'].includes(d.currentStatusCode)
95
+ );
96
+ }
97
+
98
+ // Sort: unavailable devices first
99
+ return filtered.sort((a, b) => {
100
+ const priority: Record<string, number> = {
101
+ out_of_service: 0,
102
+ decommissioned: 1,
103
+ under_repair: 2,
104
+ under_maintenance: 3,
105
+ waiting_for_parts: 4,
106
+ under_investigation: 5,
107
+ operational: 6,
108
+ };
109
+ return (
110
+ (priority[a.currentStatusCode] ?? 10) -
111
+ (priority[b.currentStatusCode] ?? 10)
112
+ );
113
+ });
114
+ }, [devices, search, statusFilter]);
115
+
116
+ const filters: { key: StatusFilter; label: string; count: number }[] = [
117
+ { key: 'all', label: 'All', count: stats.total },
118
+ { key: 'operational', label: 'Available', count: stats.operational },
119
+ {
120
+ key: 'maintenance',
121
+ label: 'Under Maint.',
122
+ count: stats.underMaintenance,
123
+ },
124
+ {
125
+ key: 'out_of_service',
126
+ label: 'Out of Service',
127
+ count: stats.outOfService,
128
+ },
129
+ ];
130
+
131
+ return (
132
+ <div className="space-y-6 max-w-3xl mx-auto">
133
+ <PageHeader
134
+ title="My Department"
135
+ description={`${department.name} \u00B7 ${department.hospitalName}`}
136
+ >
137
+ <Link href="/tickets/create">
138
+ <Button size="lg" className="min-h-[44px]">
139
+ <PlusCircle className="mr-2 h-5 w-5" />
140
+ Report Problem
141
+ </Button>
142
+ </Link>
143
+ </PageHeader>
144
+
145
+ {/* Summary Stats */}
146
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
147
+ <Card className="shadow-sm">
148
+ <CardContent className="p-3 text-center">
149
+ <p className="text-2xl font-bold">{stats.total}</p>
150
+ <p className="text-xs text-muted-foreground">Total Devices</p>
151
+ </CardContent>
152
+ </Card>
153
+ <Card className="shadow-sm border-green-200 dark:border-green-900 bg-green-50/50 dark:bg-green-950/20">
154
+ <CardContent className="p-3 text-center">
155
+ <p className="text-2xl font-bold text-green-700 dark:text-green-400">
156
+ {stats.operational}
157
+ </p>
158
+ <p className="text-xs text-green-600 dark:text-green-500">
159
+ Available
160
+ </p>
161
+ </CardContent>
162
+ </Card>
163
+ <Card className="shadow-sm border-amber-200 dark:border-amber-900 bg-amber-50/50 dark:bg-amber-950/20">
164
+ <CardContent className="p-3 text-center">
165
+ <p className="text-2xl font-bold text-amber-700 dark:text-amber-400">
166
+ {stats.underMaintenance}
167
+ </p>
168
+ <p className="text-xs text-amber-600 dark:text-amber-500">
169
+ Maintenance
170
+ </p>
171
+ </CardContent>
172
+ </Card>
173
+ <Card className="shadow-sm border-red-200 dark:border-red-900 bg-red-50/50 dark:bg-red-950/20">
174
+ <CardContent className="p-3 text-center">
175
+ <p className="text-2xl font-bold text-red-700 dark:text-red-400">
176
+ {stats.outOfService}
177
+ </p>
178
+ <p className="text-xs text-red-600 dark:text-red-500">
179
+ Out of Service
180
+ </p>
181
+ </CardContent>
182
+ </Card>
183
+ </div>
184
+
185
+ {/* Search */}
186
+ <div className="relative">
187
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
188
+ <Input
189
+ placeholder="Search devices by name, asset number, location..."
190
+ value={search}
191
+ onChange={(e) => setSearch(e.target.value)}
192
+ className="pl-10"
193
+ />
194
+ </div>
195
+
196
+ {/* Filter Chips */}
197
+ <div className="flex gap-2 flex-wrap">
198
+ {filters.map((f) => (
199
+ <button
200
+ key={f.key}
201
+ onClick={() => setStatusFilter(f.key)}
202
+ className={cn(
203
+ 'text-xs px-3 py-1.5 rounded-full border font-medium transition-colors whitespace-nowrap',
204
+ statusFilter === f.key
205
+ ? 'bg-primary text-primary-foreground border-primary'
206
+ : 'bg-card text-muted-foreground border-border hover:bg-muted'
207
+ )}
208
+ >
209
+ {f.label} ({f.count})
210
+ </button>
211
+ ))}
212
+ </div>
213
+
214
+ {/* Device List */}
215
+ {filteredDevices.length === 0 ? (
216
+ <div className="text-center py-12 border rounded-lg bg-muted/20">
217
+ <Monitor className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
218
+ <p className="text-sm text-muted-foreground">
219
+ {search
220
+ ? 'No devices match your search.'
221
+ : 'No devices in this category.'}
222
+ </p>
223
+ </div>
224
+ ) : (
225
+ <div className="space-y-3">
226
+ {filteredDevices.map((device) => (
227
+ <StaffDeviceCard key={device.id} device={device} />
228
+ ))}
229
+ </div>
230
+ )}
231
+ </div>
232
+ );
233
+ }
234
+
235
+ function StaffDeviceCard({ device }: { device: DeviceItem }) {
236
+ const isOOS = ['out_of_service', 'decommissioned'].includes(
237
+ device.currentStatusCode
238
+ );
239
+ const isMaint = [
240
+ 'under_repair',
241
+ 'under_maintenance',
242
+ 'waiting_for_parts',
243
+ 'under_investigation',
244
+ ].includes(device.currentStatusCode);
245
+ const hasActiveTicket = device.activeTickets.length > 0;
246
+
247
+ return (
248
+ <Card
249
+ className={cn(
250
+ 'shadow-sm overflow-hidden',
251
+ isOOS && 'border-red-200 dark:border-red-900',
252
+ isMaint && 'border-amber-200 dark:border-amber-900'
253
+ )}
254
+ >
255
+ {/* Status Banner for non-operational devices */}
256
+ {isOOS && (
257
+ <div className="bg-red-50 dark:bg-red-950/30 px-4 py-2 text-xs font-semibold text-red-700 dark:text-red-300 flex items-center gap-1.5 border-b border-red-200 dark:border-red-900">
258
+ <AlertTriangle className="w-3.5 h-3.5" />
259
+ OUT OF SERVICE — Do not use
260
+ </div>
261
+ )}
262
+ {isMaint && (
263
+ <div className="bg-amber-50 dark:bg-amber-950/30 px-4 py-2 text-xs font-semibold text-amber-700 dark:text-amber-300 flex items-center gap-1.5 border-b border-amber-200 dark:border-amber-900">
264
+ <Wrench className="w-3.5 h-3.5" />
265
+ UNDER MAINTENANCE
266
+ </div>
267
+ )}
268
+
269
+ <CardContent className="p-4">
270
+ {/* Device Info */}
271
+ <div className="flex items-start justify-between gap-3">
272
+ <div className="min-w-0">
273
+ <h4 className="font-semibold text-sm truncate">{device.name}</h4>
274
+ <p className="text-xs text-muted-foreground truncate">
275
+ {device.assetNumber}
276
+ {device.locationName && ` · ${device.locationName}`}
277
+ </p>
278
+ {device.manufacturerName && (
279
+ <p className="text-xs text-muted-foreground truncate mt-0.5">
280
+ {device.manufacturerName}
281
+ {device.categoryName && ` · ${device.categoryName}`}
282
+ </p>
283
+ )}
284
+ </div>
285
+ <DeviceStatusBadge status={device.currentStatusCode} />
286
+ </div>
287
+
288
+ {/* Active Ticket Alert */}
289
+ {hasActiveTicket && (
290
+ <div className="mt-3 space-y-1.5">
291
+ {device.activeTickets.map((ticket) => (
292
+ <Link
293
+ key={ticket.id}
294
+ href={`/tickets/${ticket.id}`}
295
+ className="flex items-center gap-2 text-xs bg-amber-50 dark:bg-amber-950/30 rounded-lg px-2.5 py-1.5 border border-amber-200 dark:border-amber-800 text-amber-800 dark:text-amber-200 hover:bg-amber-100 dark:hover:bg-amber-950/50 transition-colors"
296
+ >
297
+ <TicketCheck className="w-3.5 h-3.5 shrink-0" />
298
+ <span className="truncate">
299
+ <strong>{ticket.ticketNumber}</strong> — {ticket.title}
300
+ </span>
301
+ </Link>
302
+ ))}
303
+ </div>
304
+ )}
305
+
306
+ {/* Actions */}
307
+ <div className="mt-3 flex gap-2">
308
+ {hasActiveTicket ? (
309
+ <Button asChild variant="outline" size="sm" className="min-h-[36px]">
310
+ <Link href={`/tickets/${device.activeTickets[0].id}`}>
311
+ <TicketCheck className="mr-1.5 h-3.5 w-3.5" />
312
+ View Ticket
313
+ </Link>
314
+ </Button>
315
+ ) : (
316
+ <Button asChild size="sm" className="min-h-[36px]">
317
+ <Link href={`/tickets/create?deviceId=${device.id}`}>
318
+ <PlusCircle className="mr-1.5 h-3.5 w-3.5" />
319
+ Report Problem
320
+ </Link>
321
+ </Button>
322
+ )}
323
+ <Button asChild variant="outline" size="sm" className="min-h-[36px]">
324
+ <Link href={`/devices/${device.id}`}>
325
+ <Monitor className="mr-1.5 h-3.5 w-3.5" />
326
+ View Device
327
+ </Link>
328
+ </Button>
329
+ </div>
330
+ </CardContent>
331
+ </Card>
332
+ );
333
+ }
@@ -0,0 +1,146 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { useRouter } from "next/navigation"
5
+ import { toast } from "sonner"
6
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
7
+ import { Button } from "@/components/ui/button"
8
+ import { Input } from "@/components/ui/input"
9
+ import { Label } from "@/components/ui/label"
10
+ import { Textarea } from "@/components/ui/textarea"
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
12
+ import { changeDeviceStatus } from "@/lib/actions/devices"
13
+ import { DeviceStatusBadge } from "./device-badges"
14
+
15
+ interface ChangeStatusDialogProps {
16
+ open: boolean;
17
+ onOpenChange: (open: boolean) => void;
18
+ deviceId: string;
19
+ deviceName: string;
20
+ currentStatus: string;
21
+ onSuccess: () => void;
22
+ }
23
+
24
+ export function ChangeStatusDialog({
25
+ open,
26
+ onOpenChange,
27
+ deviceId,
28
+ deviceName,
29
+ currentStatus,
30
+ onSuccess
31
+ }: ChangeStatusDialogProps) {
32
+ const router = useRouter()
33
+ const [loading, setLoading] = useState(false)
34
+
35
+ const [status, setStatus] = useState("")
36
+ const [reason, setReason] = useState("")
37
+ const [limitationsNote, setLimitationsNote] = useState("")
38
+ const [notes, setNotes] = useState("")
39
+
40
+ const handleSubmit = async (e: React.FormEvent) => {
41
+ e.preventDefault()
42
+ if (!status || !reason) {
43
+ toast.error("Please fill in all required fields")
44
+ return
45
+ }
46
+
47
+ setLoading(true)
48
+ try {
49
+ await changeDeviceStatus(deviceId, {
50
+ newStatusCode: status as any,
51
+ reason,
52
+ notes,
53
+ limitationsNote: status === 'operational_with_limitations' ? limitationsNote : undefined,
54
+ })
55
+ toast.success("Device status updated successfully")
56
+ onSuccess()
57
+ onOpenChange(false)
58
+ router.refresh()
59
+ } catch (error) {
60
+ console.error(error)
61
+ toast.error("Failed to update status")
62
+ } finally {
63
+ setLoading(false)
64
+ }
65
+ }
66
+
67
+ return (
68
+ <Dialog open={open} onOpenChange={onOpenChange}>
69
+ <DialogContent className="sm:max-w-[500px]">
70
+ <form onSubmit={handleSubmit}>
71
+ <DialogHeader>
72
+ <DialogTitle>Change Device Status</DialogTitle>
73
+ </DialogHeader>
74
+ <div className="grid gap-4 py-4">
75
+ <div className="flex items-center gap-2 mb-2">
76
+ <span className="text-sm font-medium">Current Status:</span>
77
+ <DeviceStatusBadge status={currentStatus} />
78
+ </div>
79
+
80
+ <div className="grid gap-2">
81
+ <Label htmlFor="status">New Status *</Label>
82
+ <Select value={status} onValueChange={setStatus} required>
83
+ <SelectTrigger>
84
+ <SelectValue placeholder="Select new status" />
85
+ </SelectTrigger>
86
+ <SelectContent>
87
+ <SelectItem value="operational">Operational</SelectItem>
88
+ <SelectItem value="operational_with_limitations">Operational with Limitations</SelectItem>
89
+ <SelectItem value="under_maintenance">Under Maintenance</SelectItem>
90
+ <SelectItem value="under_repair">Under Repair</SelectItem>
91
+ <SelectItem value="waiting_for_parts">Waiting for Parts</SelectItem>
92
+ <SelectItem value="awaiting_release">Awaiting Release</SelectItem>
93
+ <SelectItem value="out_of_service">Out of Service</SelectItem>
94
+ <SelectItem value="standby">Standby</SelectItem>
95
+ </SelectContent>
96
+ </Select>
97
+ </div>
98
+
99
+ <div className="grid gap-2">
100
+ <Label htmlFor="reason">Reason for Change *</Label>
101
+ <Input
102
+ id="reason"
103
+ placeholder="Brief reason for status change"
104
+ value={reason}
105
+ onChange={(e) => setReason(e.target.value)}
106
+ required
107
+ />
108
+ </div>
109
+
110
+ {status === 'operational_with_limitations' && (
111
+ <div className="grid gap-2">
112
+ <Label htmlFor="limitations">Limitations Note *</Label>
113
+ <Textarea
114
+ id="limitations"
115
+ placeholder="Describe the operational limitations"
116
+ value={limitationsNote}
117
+ onChange={(e) => setLimitationsNote(e.target.value)}
118
+ required
119
+ />
120
+ </div>
121
+ )}
122
+
123
+ <div className="grid gap-2">
124
+ <Label htmlFor="notes">Additional Notes (Optional)</Label>
125
+ <Textarea
126
+ id="notes"
127
+ placeholder="Any other relevant details"
128
+ value={notes}
129
+ onChange={(e) => setNotes(e.target.value)}
130
+ />
131
+ </div>
132
+ </div>
133
+ <DialogFooter>
134
+ <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
135
+ Cancel
136
+ </Button>
137
+ <Button type="submit" disabled={loading}>
138
+ {loading ? "Updating..." : "Update Status"}
139
+ </Button>
140
+ </DialogFooter>
141
+ </form>
142
+ </DialogContent>
143
+ </Dialog>
144
+ )
145
+ }
146
+
@@ -0,0 +1,125 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Button } from "@/components/ui/button"
5
+ import { AlertTriangle, QrCode, Edit, ArrowLeftRight, Activity } from "lucide-react"
6
+ import Link from "next/link"
7
+ import { TransferDeviceDialog } from "./transfer-device-dialog"
8
+ import { ChangeStatusDialog } from "./change-status-dialog"
9
+ import { useRouter } from "next/navigation"
10
+
11
+ export interface DeviceActionButtonsProps {
12
+ deviceId: string;
13
+ deviceName: string;
14
+ currentHospital: string;
15
+ currentDepartment: string;
16
+ currentLocation?: string;
17
+ currentStatus: string;
18
+ allowedActions?: string[];
19
+ }
20
+
21
+ export function DeviceActionButtons({
22
+ deviceId,
23
+ deviceName,
24
+ currentHospital,
25
+ currentDepartment,
26
+ currentLocation,
27
+ currentStatus,
28
+ allowedActions = ["report_problem", "view_qr", "edit", "transfer", "change_status"],
29
+ }: DeviceActionButtonsProps) {
30
+ const router = useRouter()
31
+ const [transferOpen, setTransferOpen] = React.useState(false)
32
+ const [statusOpen, setStatusOpen] = React.useState(false)
33
+
34
+ const canReport = allowedActions.includes("report_problem")
35
+ const canViewQr = allowedActions.includes("view_qr")
36
+ const canEdit = allowedActions.includes("edit")
37
+ const canTransfer = allowedActions.includes("transfer")
38
+ const canChangeStatus = allowedActions.includes("change_status")
39
+
40
+ const handleRefresh = () => {
41
+ router.refresh()
42
+ }
43
+
44
+ return (
45
+ <>
46
+ <div className="flex flex-wrap items-center gap-2">
47
+ {canReport && (
48
+ <Link href={`/tickets/create?deviceId=${deviceId}&source=device_profile`}>
49
+ <Button
50
+ variant="default"
51
+ size="sm"
52
+ className="bg-amber-600 hover:bg-amber-700 text-white font-medium"
53
+ >
54
+ <AlertTriangle className="w-4 h-4 mr-1.5" />
55
+ Report Problem
56
+ </Button>
57
+ </Link>
58
+ )}
59
+
60
+ {canViewQr && (
61
+ <Link href={`/devices/${deviceId}/qr`}>
62
+ <Button variant="outline" size="sm">
63
+ <QrCode className="w-4 h-4 mr-1.5" />
64
+ QR Code
65
+ </Button>
66
+ </Link>
67
+ )}
68
+
69
+ {canEdit && (
70
+ <Link href={`/devices/${deviceId}/edit`}>
71
+ <Button variant="outline" size="sm">
72
+ <Edit className="w-4 h-4 mr-1.5" />
73
+ Edit
74
+ </Button>
75
+ </Link>
76
+ )}
77
+
78
+ {canTransfer && (
79
+ <Button
80
+ variant="outline"
81
+ size="sm"
82
+ onClick={() => setTransferOpen(true)}
83
+ >
84
+ <ArrowLeftRight className="w-4 h-4 mr-1.5" />
85
+ Transfer
86
+ </Button>
87
+ )}
88
+
89
+ {canChangeStatus && (
90
+ <Button
91
+ variant="outline"
92
+ size="sm"
93
+ onClick={() => setStatusOpen(true)}
94
+ >
95
+ <Activity className="w-4 h-4 mr-1.5" />
96
+ Change Status
97
+ </Button>
98
+ )}
99
+ </div>
100
+
101
+ {/* Transfer Dialog */}
102
+ <TransferDeviceDialog
103
+ open={transferOpen}
104
+ onOpenChange={setTransferOpen}
105
+ deviceId={deviceId}
106
+ deviceName={deviceName}
107
+ currentHospital={currentHospital}
108
+ currentDepartment={currentDepartment}
109
+ currentLocation={currentLocation}
110
+ onSuccess={handleRefresh}
111
+ />
112
+
113
+ {/* Status Change Dialog */}
114
+ <ChangeStatusDialog
115
+ open={statusOpen}
116
+ onOpenChange={setStatusOpen}
117
+ deviceId={deviceId}
118
+ deviceName={deviceName}
119
+ currentStatus={currentStatus}
120
+ onSuccess={handleRefresh}
121
+ />
122
+ </>
123
+ )
124
+ }
125
+
@@ -0,0 +1,59 @@
1
+ import { AlertTriangle, CheckCircle, AlertCircle, Wrench, Clock } from "lucide-react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ const bannerConfig: Record<string, { icon: any; bg: string; border: string; text: string; message: string }> = {
5
+ operational: {
6
+ icon: CheckCircle, bg: "bg-green-50", border: "border-green-300", text: "text-green-800",
7
+ message: "This device is operational and available for use.",
8
+ },
9
+ operational_with_limitations: {
10
+ icon: AlertTriangle, bg: "bg-amber-50", border: "border-amber-300", text: "text-amber-800",
11
+ message: "This device is operational with limitations. Check notes below before use.",
12
+ },
13
+ under_maintenance: {
14
+ icon: Wrench, bg: "bg-blue-50", border: "border-blue-300", text: "text-blue-800",
15
+ message: "This device is currently under maintenance. Do not use until released by Biomedical Engineering.",
16
+ },
17
+ under_repair: {
18
+ icon: Wrench, bg: "bg-orange-50", border: "border-orange-300", text: "text-orange-800",
19
+ message: "This device is under repair. Do not use until released by Biomedical Engineering.",
20
+ },
21
+ waiting_for_parts: {
22
+ icon: Clock, bg: "bg-yellow-50", border: "border-yellow-300", text: "text-yellow-800",
23
+ message: "This device is waiting for parts. Do not use.",
24
+ },
25
+ awaiting_release: {
26
+ icon: Clock, bg: "bg-purple-50", border: "border-purple-300", text: "text-purple-800",
27
+ message: "This device is awaiting release after maintenance. Do not use until formally released.",
28
+ },
29
+ out_of_service: {
30
+ icon: AlertCircle, bg: "bg-red-50", border: "border-red-300", text: "text-red-800",
31
+ message: "OUT OF SERVICE — Do not use until released by Biomedical Engineering.",
32
+ },
33
+ standby: {
34
+ icon: Clock, bg: "bg-gray-50", border: "border-gray-300", text: "text-gray-800",
35
+ message: "This device is on standby.",
36
+ },
37
+ decommissioned: {
38
+ icon: AlertCircle, bg: "bg-slate-100", border: "border-slate-300", text: "text-slate-600",
39
+ message: "This device has been decommissioned and is no longer in service.",
40
+ },
41
+ }
42
+
43
+ export function DeviceAvailabilityBanner({ status, limitationsNote }: { status: string; limitationsNote?: string | null }) {
44
+ const config = bannerConfig[status] || bannerConfig.operational
45
+ const Icon = config.icon
46
+
47
+ return (
48
+ <div className={cn("rounded-lg border-2 p-4 flex items-start gap-3", config.bg, config.border)}>
49
+ <Icon className={cn("w-6 h-6 mt-0.5 flex-shrink-0", config.text)} />
50
+ <div>
51
+ <p className={cn("font-semibold text-base", config.text)}>{config.message}</p>
52
+ {limitationsNote && status === 'operational_with_limitations' && (
53
+ <p className={cn("mt-1 text-sm", config.text)}>Note: {limitationsNote}</p>
54
+ )}
55
+ </div>
56
+ </div>
57
+ )
58
+ }
59
+