@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,382 @@
1
+ "use client"
2
+
3
+ import { createDevice } from "@/lib/actions/devices"
4
+ import { getDeviceCategories } from "@/lib/actions/device-categories"
5
+ import { getManufacturers } from "@/lib/actions/manufacturers"
6
+ import { getHospitals } from "@/lib/actions/hospitals"
7
+ import { getDepartments } from "@/lib/actions/departments"
8
+ import { PageHeader } from "@/components/shared/page-header"
9
+ import { Button } from "@/components/ui/button"
10
+ import { Input } from "@/components/ui/input"
11
+ import { Label } from "@/components/ui/label"
12
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
13
+ import { useRouter } from "next/navigation"
14
+ import { useState, useEffect } from "react"
15
+ import { QuickAddCategoryDialog } from "@/components/devices/quick-add-category-dialog"
16
+ import { QuickAddManufacturerDialog } from "@/components/devices/quick-add-manufacturer-dialog"
17
+ import { toast } from "sonner"
18
+ import { Loader2, ChevronDown, ChevronUp, Sparkles } from "lucide-react"
19
+
20
+ export default function NewDevicePage() {
21
+ const router = useRouter()
22
+ const [loading, setLoading] = useState(false)
23
+ const [error, setError] = useState("")
24
+ const [deviceName, setDeviceName] = useState("")
25
+ const [categories, setCategories] = useState<any[]>([])
26
+ const [selectedCategoryId, setSelectedCategoryId] = useState("")
27
+ const [manufacturers, setManufacturers] = useState<any[]>([])
28
+ const [selectedManufacturerId, setSelectedManufacturerId] = useState("")
29
+ const [hospitals, setHospitals] = useState<any[]>([])
30
+ const [selectedHospitalId, setSelectedHospitalId] = useState("")
31
+ const [departments, setDepartments] = useState<any[]>([])
32
+ const [selectedDepartmentId, setSelectedDepartmentId] = useState("")
33
+ const [organizationId, setOrganizationId] = useState("")
34
+ const [loadingDepts, setLoadingDepts] = useState(false)
35
+ const [showAdvanced, setShowAdvanced] = useState(false)
36
+
37
+ // Track auto-detected category name for helpful feedback
38
+ const [detectedCategoryName, setDetectedCategoryName] = useState("")
39
+
40
+ useEffect(() => {
41
+ async function loadData() {
42
+ try {
43
+ const [catRes, mfrRes, hospRes] = await Promise.all([
44
+ getDeviceCategories(),
45
+ getManufacturers(),
46
+ getHospitals(),
47
+ ])
48
+ if (catRes?.success && catRes.data) setCategories(catRes.data as any[])
49
+ if (mfrRes?.success && mfrRes.data) setManufacturers(mfrRes.data as any[])
50
+ if (hospRes?.success && hospRes.data && Array.isArray(hospRes.data)) {
51
+ const hospList = hospRes.data as any[]
52
+ setHospitals(hospList)
53
+ if (hospList.length > 0) {
54
+ const firstHosp = hospList[0]
55
+ setSelectedHospitalId(firstHosp.id)
56
+ setOrganizationId(firstHosp.organizationId)
57
+ setLoadingDepts(true)
58
+ const deptRes = await getDepartments({ hospitalId: firstHosp.id })
59
+ if (deptRes?.success && deptRes.data) setDepartments(deptRes.data as any[])
60
+ setLoadingDepts(false)
61
+ }
62
+ }
63
+ } catch (err) {
64
+ console.error("Failed to load initial data:", err)
65
+ }
66
+ }
67
+ loadData()
68
+ }, [])
69
+
70
+ const handleHospitalChange = async (hospitalId: string) => {
71
+ setSelectedHospitalId(hospitalId)
72
+ setSelectedDepartmentId("")
73
+ if (!hospitalId) { setDepartments([]); setOrganizationId(""); return }
74
+ const hosp = hospitals.find((h: any) => h.id === hospitalId)
75
+ if (hosp) setOrganizationId(hosp.organizationId)
76
+ setLoadingDepts(true)
77
+ try {
78
+ const deptRes = await getDepartments({ hospitalId })
79
+ if (deptRes?.success && deptRes.data) setDepartments(deptRes.data as any[])
80
+ else setDepartments([])
81
+ } catch { setDepartments([]) }
82
+ finally { setLoadingDepts(false) }
83
+ }
84
+
85
+ // Smart auto-detection when user types device name
86
+ const handleNameChange = (val: string) => {
87
+ setDeviceName(val)
88
+ if (!val.trim()) {
89
+ setDetectedCategoryName("")
90
+ return
91
+ }
92
+
93
+ const lower = val.toLowerCase()
94
+ const matched = categories.find((c: any) => {
95
+ const catLower = c.name.toLowerCase()
96
+ const codeLower = (c.code || "").toLowerCase()
97
+ return lower.includes(catLower) || catLower.includes(lower) || lower.includes(codeLower)
98
+ })
99
+
100
+ if (matched) {
101
+ setSelectedCategoryId(matched.id)
102
+ setDetectedCategoryName(matched.name)
103
+ } else {
104
+ setDetectedCategoryName("")
105
+ }
106
+ }
107
+
108
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
109
+ e.preventDefault()
110
+ setLoading(true)
111
+ setError("")
112
+
113
+ if (!organizationId) { setError("Please select a hospital first."); setLoading(false); return }
114
+ if (!selectedDepartmentId) { setError("Please select a department."); setLoading(false); return }
115
+
116
+ const fd = new FormData(e.currentTarget)
117
+ const data: any = {}
118
+ fd.forEach((v, k) => { if (v) data[k] = v })
119
+
120
+ data.name = deviceName
121
+ data.organizationId = organizationId
122
+ data.hospitalId = selectedHospitalId
123
+ data.departmentId = selectedDepartmentId
124
+ if (selectedCategoryId) data.deviceCategoryId = selectedCategoryId
125
+ if (selectedManufacturerId) data.manufacturerId = selectedManufacturerId
126
+
127
+ try {
128
+ const result = await createDevice(data)
129
+ if (result?.success) {
130
+ toast.success("Device registered successfully!")
131
+ router.push(`/devices/${(result as any).data.id}`)
132
+ } else {
133
+ const errMsg = (result as any)?.error || "Failed to create device"
134
+ setError(errMsg)
135
+ toast.error(errMsg)
136
+ }
137
+ } catch (err: any) {
138
+ const errMsg = err.message || "An unexpected error occurred"
139
+ setError(errMsg)
140
+ toast.error(errMsg)
141
+ } finally {
142
+ setLoading(false)
143
+ }
144
+ }
145
+
146
+ return (
147
+ <div className="space-y-6 max-w-5xl">
148
+ <PageHeader
149
+ title="Add New Device"
150
+ description="Register a new medical device in the hospital inventory"
151
+ />
152
+
153
+ {error && <div className="bg-destructive/10 text-destructive p-3 rounded-md text-sm">{error}</div>}
154
+
155
+ <form onSubmit={handleSubmit}>
156
+ <input type="hidden" name="organizationId" value={organizationId} />
157
+
158
+ <div className="grid gap-6 md:grid-cols-2">
159
+ {/* ── Card 1: Device Info ─────────────────────────────────── */}
160
+ <Card>
161
+ <CardHeader><CardTitle>Device Information</CardTitle></CardHeader>
162
+ <CardContent className="space-y-4">
163
+ <div>
164
+ <Label htmlFor="name">Device Name *</Label>
165
+ <Input
166
+ id="name"
167
+ name="name"
168
+ value={deviceName}
169
+ onChange={(e) => handleNameChange(e.target.value)}
170
+ placeholder="e.g. Infusion Pump 1, Defibrillator A..."
171
+ required
172
+ />
173
+ {detectedCategoryName && (
174
+ <p className="flex items-center gap-1.5 text-xs text-emerald-600 dark:text-emerald-400 mt-1.5 font-medium">
175
+ <Sparkles className="w-3.5 h-3.5" />
176
+ Auto-detected type: {detectedCategoryName}
177
+ </p>
178
+ )}
179
+ </div>
180
+ <div>
181
+ <Label htmlFor="assetNumber">Asset Number *</Label>
182
+ <Input id="assetNumber" name="assetNumber" placeholder="e.g. AST-2026-001" required />
183
+ </div>
184
+ <div>
185
+ <Label htmlFor="serialNumber">Serial Number (Optional)</Label>
186
+ <Input id="serialNumber" name="serialNumber" placeholder="Manufacturer serial #" />
187
+ </div>
188
+ </CardContent>
189
+ </Card>
190
+
191
+ {/* ── Card 2: Location ───────────────────────────────────── */}
192
+ <Card>
193
+ <CardHeader><CardTitle>Location</CardTitle></CardHeader>
194
+ <CardContent className="space-y-4">
195
+ <div>
196
+ <Label htmlFor="hospitalId">Hospital *</Label>
197
+ <select
198
+ id="hospitalId" name="hospitalId" required
199
+ value={selectedHospitalId}
200
+ onChange={(e) => handleHospitalChange(e.target.value)}
201
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
202
+ >
203
+ <option value="">Select hospital...</option>
204
+ {hospitals.map((h: any) => (
205
+ <option key={h.id} value={h.id}>{h.name}</option>
206
+ ))}
207
+ </select>
208
+ </div>
209
+ <div>
210
+ <div className="flex items-center justify-between mb-1">
211
+ <Label htmlFor="departmentId">Department *</Label>
212
+ {loadingDepts && (
213
+ <span className="flex items-center text-xs text-muted-foreground gap-1">
214
+ <Loader2 className="w-3 h-3 animate-spin" /> Loading...
215
+ </span>
216
+ )}
217
+ </div>
218
+ <select
219
+ id="departmentId" name="departmentId" required
220
+ value={selectedDepartmentId}
221
+ onChange={(e) => setSelectedDepartmentId(e.target.value)}
222
+ disabled={!selectedHospitalId || loadingDepts}
223
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-50"
224
+ >
225
+ <option value="">
226
+ {loadingDepts ? "Loading..." : !selectedHospitalId
227
+ ? "Select a hospital first..." : departments.length === 0
228
+ ? "No departments found" : "Select department..."}
229
+ </option>
230
+ {departments.map((d: any) => (
231
+ <option key={d.id} value={d.id}>{d.name}</option>
232
+ ))}
233
+ </select>
234
+ </div>
235
+ <div>
236
+ <Label htmlFor="exactLocationDescription">Exact Location (Optional)</Label>
237
+ <Input id="exactLocationDescription" name="exactLocationDescription"
238
+ placeholder="e.g. Room 302, Bay 4" />
239
+ </div>
240
+ </CardContent>
241
+ </Card>
242
+ </div>
243
+
244
+ {/* ── Optional Advanced Details Toggle ─────────────────────── */}
245
+ <div className="mt-6">
246
+ <button
247
+ type="button"
248
+ onClick={() => setShowAdvanced(!showAdvanced)}
249
+ className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors py-1 font-medium"
250
+ >
251
+ {showAdvanced ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
252
+ {showAdvanced ? "Hide" : "Show"} Advanced Details
253
+ <span className="text-xs text-muted-foreground/75 font-normal">(device type override, manufacturer, warranty...)</span>
254
+ </button>
255
+
256
+ {showAdvanced && (
257
+ <div className="grid gap-6 md:grid-cols-2 mt-4">
258
+ <Card>
259
+ <CardHeader><CardTitle className="text-base">Type & Classification (Optional)</CardTitle></CardHeader>
260
+ <CardContent className="space-y-4">
261
+ <div>
262
+ <div className="flex items-center justify-between mb-1">
263
+ <Label htmlFor="deviceCategoryId">Device Type</Label>
264
+ <QuickAddCategoryDialog
265
+ onSuccess={(newCat) => {
266
+ setCategories((prev) => [newCat, ...prev])
267
+ setSelectedCategoryId(newCat.id)
268
+ setDetectedCategoryName(newCat.name)
269
+ }}
270
+ />
271
+ </div>
272
+ <select
273
+ id="deviceCategoryId"
274
+ name="deviceCategoryId"
275
+ value={selectedCategoryId}
276
+ onChange={(e) => {
277
+ setSelectedCategoryId(e.target.value)
278
+ const c = categories.find((cat: any) => cat.id === e.target.value)
279
+ setDetectedCategoryName(c ? c.name : "")
280
+ }}
281
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
282
+ >
283
+ <option value="">Auto-assign (Default: General Medical Equipment)</option>
284
+ {categories.map((c: any) => (
285
+ <option key={c.id} value={c.id}>{c.name}</option>
286
+ ))}
287
+ </select>
288
+ <p className="text-xs text-muted-foreground mt-1">
289
+ Leave empty to auto-detect from name or use default.
290
+ </p>
291
+ </div>
292
+
293
+ <div>
294
+ <div className="flex items-center justify-between mb-1">
295
+ <Label htmlFor="manufacturerId">Manufacturer</Label>
296
+ <QuickAddManufacturerDialog
297
+ onSuccess={(newMfr) => {
298
+ setManufacturers((prev) => [newMfr, ...prev])
299
+ setSelectedManufacturerId(newMfr.id)
300
+ }}
301
+ />
302
+ </div>
303
+ <select
304
+ id="manufacturerId" name="manufacturerId"
305
+ value={selectedManufacturerId}
306
+ onChange={(e) => setSelectedManufacturerId(e.target.value)}
307
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
308
+ >
309
+ <option value="">Select manufacturer...</option>
310
+ {manufacturers.map((m: any) => (
311
+ <option key={m.id} value={m.id}>
312
+ {m.name} {m.country ? `(${m.country})` : ""}
313
+ </option>
314
+ ))}
315
+ </select>
316
+ </div>
317
+
318
+ <div>
319
+ <Label htmlFor="modelNameFree">Model Name</Label>
320
+ <Input id="modelNameFree" name="modelNameFree" placeholder="Model name/number" />
321
+ </div>
322
+ </CardContent>
323
+ </Card>
324
+
325
+ <Card>
326
+ <CardHeader>
327
+ <CardTitle className="text-base">Purchase & Warranty (Optional)</CardTitle>
328
+ </CardHeader>
329
+ <CardContent className="space-y-4">
330
+ <div className="grid grid-cols-2 gap-3">
331
+ <div>
332
+ <Label htmlFor="purchaseDate">Purchase Date</Label>
333
+ <Input id="purchaseDate" name="purchaseDate" type="date" />
334
+ </div>
335
+ <div>
336
+ <Label htmlFor="installationDate">Installation Date</Label>
337
+ <Input id="installationDate" name="installationDate" type="date" />
338
+ </div>
339
+ </div>
340
+
341
+ <div className="grid grid-cols-2 gap-3">
342
+ <div>
343
+ <Label htmlFor="purchaseCost">Purchase Cost</Label>
344
+ <Input id="purchaseCost" name="purchaseCost" type="number" step="0.01" placeholder="0.00" />
345
+ </div>
346
+ <div>
347
+ <Label htmlFor="currency">Currency</Label>
348
+ <Input id="currency" name="currency" defaultValue="USD" placeholder="USD" />
349
+ </div>
350
+ </div>
351
+
352
+ <div className="grid grid-cols-2 gap-3">
353
+ <div>
354
+ <Label htmlFor="warrantyStartDate">Warranty Start</Label>
355
+ <Input id="warrantyStartDate" name="warrantyStartDate" type="date" />
356
+ </div>
357
+ <div>
358
+ <Label htmlFor="warrantyEndDate">Warranty End Date</Label>
359
+ <Input id="warrantyEndDate" name="warrantyEndDate" type="date" />
360
+ </div>
361
+ </div>
362
+ </CardContent>
363
+ </Card>
364
+ </div>
365
+ )}
366
+ </div>
367
+
368
+ <div className="flex justify-end gap-3 mt-6">
369
+ <Button type="button" variant="outline" onClick={() => router.back()}>Cancel</Button>
370
+ <Button type="submit" disabled={loading} className="min-h-[44px] px-6">
371
+ {loading ? (
372
+ <><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Registering...</>
373
+ ) : (
374
+ "Register Device"
375
+ )}
376
+ </Button>
377
+ </div>
378
+ </form>
379
+ </div>
380
+ )
381
+ }
382
+
@@ -0,0 +1,80 @@
1
+ import { getDevices } from "@/lib/actions/devices"
2
+ import { PageHeader } from "@/components/shared/page-header"
3
+ import { DataTable } from "@/components/shared/data-table"
4
+ import { DeviceStatusBadge, CriticalityBadge } from "@/components/devices/device-badges"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Plus, QrCode } from "lucide-react"
7
+ import Link from "next/link"
8
+
9
+ export const dynamic = 'force-dynamic'
10
+
11
+ export default async function DevicesPage() {
12
+ const result = await getDevices()
13
+ const devicesList = (result?.success && Array.isArray(result.data)) ? result.data : []
14
+
15
+ const columns = [
16
+ { key: "internalCode", header: "Code", render: (item: any) => (
17
+ <Link href={`/devices/${item.id}`} className="text-primary hover:underline font-mono text-sm">
18
+ {item.internalCode}
19
+ </Link>
20
+ )},
21
+ { key: "name", header: "Device Name", render: (item: any) => (
22
+ <Link href={`/devices/${item.id}`} className="font-medium hover:underline">
23
+ {item.name}
24
+ </Link>
25
+ )},
26
+ { key: "assetNumber", header: "Asset #" },
27
+ { key: "category", header: "Category", render: (item: any) => item.deviceCategory?.name || "—" },
28
+ { key: "manufacturer", header: "Manufacturer", render: (item: any) => item.manufacturer?.name || "—" },
29
+ { key: "department", header: "Department", render: (item: any) => (
30
+ <span className="text-sm">
31
+ {[item.hospital?.name, item.department?.name].filter(Boolean).join(" / ") || "—"}
32
+ </span>
33
+ )},
34
+ { key: "location", header: "Location", render: (item: any) => item.location?.name || item.exactLocationDescription || "—" },
35
+ { key: "status", header: "Status", render: (item: any) => <DeviceStatusBadge status={item.currentStatusCode} /> },
36
+ { key: "criticality", header: "Criticality", render: (item: any) => <CriticalityBadge level={item.criticalityLevel} /> },
37
+ { key: "actions", header: "", render: (item: any) => (
38
+ <div className="flex gap-1">
39
+ <Button asChild variant="ghost" size="sm" title="View & Print QR">
40
+ <Link href={`/devices/${item.id}/qr`}>
41
+ <QrCode className="w-4 h-4" />
42
+ </Link>
43
+ </Button>
44
+ </div>
45
+ )},
46
+ ]
47
+
48
+ return (
49
+ <div className="space-y-6">
50
+ <PageHeader
51
+ title="Device Inventory"
52
+ description="Manage medical equipment and device lifecycle"
53
+ >
54
+ <div className="flex gap-2">
55
+ <Button asChild variant="outline">
56
+ <Link href="/scan">
57
+ <QrCode className="w-4 h-4 mr-2" />
58
+ Scan QR
59
+ </Link>
60
+ </Button>
61
+ <Button asChild>
62
+ <Link href="/devices/new">
63
+ <Plus className="w-4 h-4 mr-2" />
64
+ Add Device
65
+ </Link>
66
+ </Button>
67
+ </div>
68
+ </PageHeader>
69
+
70
+ <DataTable
71
+ data={devicesList}
72
+ columns={columns}
73
+ searchKey="name"
74
+ searchPlaceholder="Search devices by name, asset #, serial #..."
75
+ />
76
+ </div>
77
+ )
78
+ }
79
+
80
+
@@ -0,0 +1,41 @@
1
+ import { auth } from '@/lib/auth';
2
+ import { redirect } from 'next/navigation';
3
+ import { getEngineersTeam } from '@/lib/actions/engineers';
4
+ import { PageHeader } from '@/components/shared/page-header';
5
+ import { StaffEngineerTeamView } from '@/components/team/staff-engineer-team-view';
6
+
7
+ import { Button } from '@/components/ui/button';
8
+ import Link from 'next/link';
9
+ import { UsersRound } from 'lucide-react';
10
+ import { hasRole } from '@/lib/auth/rbac';
11
+
12
+ export const dynamic = 'force-dynamic';
13
+
14
+ export default async function EngineeringTeamPage() {
15
+ const session = await auth();
16
+ if (!session?.user) redirect('/login');
17
+
18
+ const teamRes = await getEngineersTeam();
19
+ const members = teamRes.data || [];
20
+ const userRoles = (session.user as any).roles || [];
21
+ const isBiomedOrAdmin = hasRole(userRoles, 'BIOMED_ENG', 'BIOMED_TECH', 'BIOMED_MGR', 'SYS_ADMIN', 'ORG_ADMIN');
22
+
23
+ return (
24
+ <div className="space-y-6 max-w-3xl mx-auto">
25
+ <PageHeader
26
+ title="Our Engineering Team"
27
+ description="Biomedical engineers available to support your department"
28
+ >
29
+ {isBiomedOrAdmin && (
30
+ <Button asChild variant="outline" size="sm">
31
+ <Link href="/team">
32
+ <UsersRound className="w-4 h-4 mr-2" />
33
+ Duty Schedule &amp; Coverage
34
+ </Link>
35
+ </Button>
36
+ )}
37
+ </PageHeader>
38
+ <StaffEngineerTeamView engineers={members} />
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,43 @@
1
+ import { auth } from '@/lib/auth';
2
+ import { redirect } from 'next/navigation';
3
+ import { Sidebar } from '@/components/layout/sidebar';
4
+ import { MobileNav } from '@/components/layout/mobile-nav';
5
+ import { Header } from '@/components/layout/header';
6
+ import { NavCountsProvider } from '@/components/providers/nav-counts-provider';
7
+
8
+ export default async function DashboardLayout({
9
+ children,
10
+ }: {
11
+ children: React.ReactNode;
12
+ }) {
13
+ const session = await auth();
14
+ if (!session?.user) {
15
+ redirect('/login');
16
+ }
17
+
18
+ const sessionUser = session.user as
19
+ | { name?: string; fullName?: string; email?: string; roles?: string[] }
20
+ | undefined;
21
+
22
+ const roles = sessionUser?.roles || [];
23
+
24
+ const user = {
25
+ name: sessionUser?.fullName || sessionUser?.name || 'Administrator',
26
+ role: roles[0] ? roles[0].replace(/_/g, ' ') : 'Admin',
27
+ email: sessionUser?.email || 'admin@hospital.org',
28
+ roles,
29
+ };
30
+
31
+ return (
32
+ <NavCountsProvider>
33
+ <div className="flex min-h-screen bg-slate-50/50 dark:bg-slate-950/50">
34
+ <Sidebar user={user} />
35
+ <div className="flex-1 flex flex-col min-w-0 md:ml-64 transition-all duration-300">
36
+ <Header user={user} />
37
+ <main className="flex-1 p-4 md:p-6 pb-24 md:pb-6">{children}</main>
38
+ </div>
39
+ <MobileNav roles={roles} />
40
+ </div>
41
+ </NavCountsProvider>
42
+ );
43
+ }
@@ -0,0 +1,57 @@
1
+ import { listChecklistTemplates } from "@/lib/actions/checklists"
2
+ import { PageHeader } from "@/components/shared/page-header"
3
+ import { MaintenanceNav } from "@/components/maintenance/maintenance-nav"
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
5
+ import { Button } from "@/components/ui/button"
6
+ import { ClipboardCheck, Plus } from "lucide-react"
7
+ import Link from "next/link"
8
+
9
+ export default async function ChecklistTemplatesPage() {
10
+ return (
11
+ <div className="space-y-6">
12
+ <PageHeader
13
+ title="Checklist Templates"
14
+ description="Standardized inspection checklists, test procedures, and maintenance protocols"
15
+ >
16
+ <Link href="/maintenance/checklists/new">
17
+ <Button>
18
+ <Plus className="w-4 h-4 mr-2" />
19
+ New Template
20
+ </Button>
21
+ </Link>
22
+ </PageHeader>
23
+
24
+ <MaintenanceNav />
25
+
26
+ <Card className="border-dashed">
27
+ <CardHeader className="text-center pb-2">
28
+ <div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 text-primary">
29
+ <ClipboardCheck className="h-6 w-6" />
30
+ </div>
31
+ <CardTitle className="text-xl">
32
+ Checklist template management coming soon
33
+ </CardTitle>
34
+ <CardDescription className="max-w-md mx-auto">
35
+ Design version-controlled maintenance checklists, safety inspection routines, and calibration steps with required pass/fail criteria and numerical measurements.
36
+ </CardDescription>
37
+ </CardHeader>
38
+ <CardContent className="flex flex-col items-center justify-center pb-8 pt-4">
39
+ <div className="flex gap-3">
40
+ <Link href="/maintenance/checklists/new">
41
+ <Button variant="outline" className="gap-2">
42
+ <Plus className="w-4 h-4" />
43
+ Create Template
44
+ </Button>
45
+ </Link>
46
+ <Link href="/maintenance">
47
+ <Button variant="ghost">
48
+ Back to Maintenance
49
+ </Button>
50
+ </Link>
51
+ </div>
52
+ </CardContent>
53
+ </Card>
54
+ </div>
55
+ )
56
+ }
57
+