@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,374 @@
1
+ import { auth } from "@/lib/auth";
2
+ import { redirect } from "next/navigation";
3
+ import { getOrganizations } from "@/lib/actions/organizations";
4
+ import { getHospitals } from "@/lib/actions/hospitals";
5
+ import { getDeviceCategories } from "@/lib/actions/device-categories";
6
+ import { getManufacturers } from "@/lib/actions/manufacturers";
7
+ import { QuickAddCategoryDialog } from "@/components/devices/quick-add-category-dialog";
8
+ import { QuickAddManufacturerDialog } from "@/components/devices/quick-add-manufacturer-dialog";
9
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
10
+ import { PageHeader } from "@/components/shared/page-header";
11
+ import { DataTable } from "@/components/shared/data-table";
12
+ import { StatusBadge } from "@/components/shared/status-badge";
13
+ import { Button } from "@/components/ui/button";
14
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
15
+ import { Badge } from "@/components/ui/badge";
16
+ import Link from "next/link";
17
+ import { Building2, Hospital, Plus, Eye, Edit, Globe, Calendar, ShieldCheck, Cpu, Factory } from "lucide-react";
18
+ import { hasRole } from "@/lib/auth/rbac";
19
+
20
+ export const dynamic = "force-dynamic";
21
+
22
+ export default async function AdminGeneralPage() {
23
+ const session = await auth();
24
+ if (!session?.user) redirect("/login");
25
+
26
+ const user = session.user as { roles?: string[] };
27
+ const isSysAdmin = hasRole(user.roles || [], "SYS_ADMIN");
28
+
29
+ const [orgsResult, hospitalsResult, categoriesResult, manufacturersResult] = await Promise.all([
30
+ getOrganizations(),
31
+ getHospitals(),
32
+ getDeviceCategories(),
33
+ getManufacturers(),
34
+ ]);
35
+
36
+ const organizations = orgsResult.success && orgsResult.data ? orgsResult.data : [];
37
+ const hospitals = hospitalsResult.success && hospitalsResult.data ? hospitalsResult.data : [];
38
+ const categories = categoriesResult.success && categoriesResult.data ? categoriesResult.data : [];
39
+ const manufacturers = manufacturersResult.success && manufacturersResult.data ? manufacturersResult.data : [];
40
+
41
+ const currentOrg = organizations[0]; // Primary organization
42
+
43
+ const hospitalColumns = [
44
+ {
45
+ key: "code",
46
+ header: "Code",
47
+ render: (item: any) => (
48
+ <span className="font-mono text-xs font-semibold text-primary">
49
+ {item.code}
50
+ </span>
51
+ ),
52
+ },
53
+ {
54
+ key: "name",
55
+ header: "Hospital Name",
56
+ render: (item: any) => (
57
+ <Link
58
+ href={`/admin/hospitals/${item.id}`}
59
+ className="font-medium hover:text-primary transition-colors"
60
+ >
61
+ {item.name}
62
+ </Link>
63
+ ),
64
+ },
65
+ {
66
+ key: "city",
67
+ header: "City / Campus",
68
+ render: (item: any) => item.city || item.address || "—",
69
+ },
70
+ {
71
+ key: "status",
72
+ header: "Status",
73
+ render: (item: any) => <StatusBadge status={item.status} />,
74
+ },
75
+ {
76
+ key: "actions",
77
+ header: "Actions",
78
+ render: (item: any) => (
79
+ <div className="flex items-center gap-1">
80
+ <Button variant="ghost" size="sm" asChild>
81
+ <Link href={`/admin/hospitals/${item.id}`}>
82
+ <Eye className="w-4 h-4 mr-1" />
83
+ View
84
+ </Link>
85
+ </Button>
86
+ <Button variant="ghost" size="sm" asChild>
87
+ <Link href={`/admin/hospitals/${item.id}/edit`}>
88
+ <Edit className="w-4 h-4" />
89
+ </Link>
90
+ </Button>
91
+ </div>
92
+ ),
93
+ },
94
+ ];
95
+
96
+ const categoryColumns = [
97
+ {
98
+ key: "code",
99
+ header: "Code",
100
+ render: (item: any) => (
101
+ <span className="font-mono text-xs font-semibold text-primary">
102
+ {item.code}
103
+ </span>
104
+ ),
105
+ },
106
+ {
107
+ key: "name",
108
+ header: "Category Name",
109
+ render: (item: any) => (
110
+ <span className="font-medium text-foreground">{item.name}</span>
111
+ ),
112
+ },
113
+ {
114
+ key: "riskClassification",
115
+ header: "Risk Class",
116
+ render: (item: any) => item.riskClassification ? (
117
+ <Badge variant="outline" className="text-xs uppercase font-mono">
118
+ {item.riskClassification.replace('class_', 'Class ')}
119
+ </Badge>
120
+ ) : "—",
121
+ },
122
+ {
123
+ key: "criticalityLevel",
124
+ header: "Criticality",
125
+ render: (item: any) => item.criticalityLevel ? (
126
+ <Badge
127
+ variant={item.criticalityLevel === 'critical' ? 'destructive' : 'secondary'}
128
+ className="text-xs capitalize"
129
+ >
130
+ {item.criticalityLevel}
131
+ </Badge>
132
+ ) : "—",
133
+ },
134
+ {
135
+ key: "defaultPmIntervalDays",
136
+ header: "Default PM",
137
+ render: (item: any) => item.defaultPmIntervalDays ? `${item.defaultPmIntervalDays} days` : "—",
138
+ },
139
+ {
140
+ key: "description",
141
+ header: "Description",
142
+ render: (item: any) => (
143
+ <span className="text-xs text-muted-foreground truncate max-w-[200px] block">
144
+ {item.description || "—"}
145
+ </span>
146
+ ),
147
+ },
148
+ ];
149
+
150
+ const manufacturerColumns = [
151
+ {
152
+ key: "code",
153
+ header: "Code",
154
+ render: (item: any) => (
155
+ <span className="font-mono text-xs font-semibold text-primary">
156
+ {item.code || "—"}
157
+ </span>
158
+ ),
159
+ },
160
+ {
161
+ key: "name",
162
+ header: "Manufacturer Name",
163
+ render: (item: any) => (
164
+ <span className="font-medium text-foreground">{item.name}</span>
165
+ ),
166
+ },
167
+ {
168
+ key: "country",
169
+ header: "Country",
170
+ render: (item: any) => item.country || "—",
171
+ },
172
+ {
173
+ key: "contacts",
174
+ header: "Support Contact",
175
+ render: (item: any) => (
176
+ <div className="text-xs space-y-0.5">
177
+ {item.supportPhone && <div>{item.supportPhone}</div>}
178
+ {item.supportEmail && <div className="text-muted-foreground">{item.supportEmail}</div>}
179
+ {!item.supportPhone && !item.supportEmail && "—"}
180
+ </div>
181
+ ),
182
+ },
183
+ {
184
+ key: "website",
185
+ header: "Website",
186
+ render: (item: any) => item.website ? (
187
+ <a
188
+ href={item.website}
189
+ target="_blank"
190
+ rel="noreferrer"
191
+ className="text-xs text-primary hover:underline truncate max-w-[160px] block"
192
+ >
193
+ {item.website.replace(/^https?:\/\//, '')}
194
+ </a>
195
+ ) : "—",
196
+ },
197
+ ];
198
+
199
+ return (
200
+ <div className="space-y-8">
201
+ <PageHeader
202
+ title="General Settings"
203
+ description="Manage your health system organization and hospital facilities"
204
+ />
205
+
206
+ {/* ── Section 1: Health System Organization ── */}
207
+ <div className="space-y-3">
208
+ <div className="flex items-center justify-between">
209
+ <div className="flex items-center gap-2">
210
+ <Building2 className="w-5 h-5 text-primary" />
211
+ <h2 className="text-lg font-semibold tracking-tight">Organization Profile</h2>
212
+ </div>
213
+ {isSysAdmin && (
214
+ <div className="flex gap-2">
215
+ {organizations.length > 1 && (
216
+ <Button variant="outline" size="sm" asChild>
217
+ <Link href="/admin/organizations">Switch Organization</Link>
218
+ </Button>
219
+ )}
220
+ <Button variant="outline" size="sm" asChild>
221
+ <Link href="/admin/organizations/new">
222
+ <Plus className="w-4 h-4 mr-1" />
223
+ New Organization
224
+ </Link>
225
+ </Button>
226
+ </div>
227
+ )}
228
+ </div>
229
+
230
+ {currentOrg ? (
231
+ <Card className="border shadow-sm">
232
+ <CardContent className="p-5 sm:p-6 space-y-4">
233
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
234
+ <div className="flex items-start gap-3.5">
235
+ <div className="p-3 rounded-2xl bg-primary/10 text-primary shrink-0">
236
+ <ShieldCheck className="w-6 h-6" />
237
+ </div>
238
+ <div className="space-y-1">
239
+ <div className="flex items-center gap-2 flex-wrap">
240
+ <h3 className="text-xl font-bold">{currentOrg.name}</h3>
241
+ <Badge variant="outline" className="font-mono text-xs">
242
+ {currentOrg.code}
243
+ </Badge>
244
+ <StatusBadge status={currentOrg.status} />
245
+ </div>
246
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground pt-0.5">
247
+ <span className="flex items-center gap-1">
248
+ <Globe className="w-3.5 h-3.5" />
249
+ Timezone: {currentOrg.defaultTimezone || "UTC"}
250
+ </span>
251
+ {currentOrg.createdAt && (
252
+ <span className="flex items-center gap-1">
253
+ <Calendar className="w-3.5 h-3.5" />
254
+ Created: {new Date(currentOrg.createdAt).toLocaleDateString()}
255
+ </span>
256
+ )}
257
+ </div>
258
+ </div>
259
+ </div>
260
+
261
+ <div className="flex items-center gap-2">
262
+ <Button asChild variant="outline" size="sm">
263
+ <Link href={`/admin/organizations/${currentOrg.id}/edit`}>
264
+ <Edit className="w-4 h-4 mr-1.5" />
265
+ Edit Details
266
+ </Link>
267
+ </Button>
268
+ </div>
269
+ </div>
270
+ </CardContent>
271
+ </Card>
272
+ ) : (
273
+ <Card className="border-dashed">
274
+ <CardContent className="py-8 text-center text-muted-foreground text-sm">
275
+ No organization profile registered.
276
+ </CardContent>
277
+ </Card>
278
+ )}
279
+ </div>
280
+
281
+ {/* ── Section 2: Hospital Facilities ── */}
282
+ <div className="space-y-3">
283
+ <div className="flex items-center justify-between">
284
+ <div className="flex items-center gap-2">
285
+ <Hospital className="w-5 h-5 text-primary" />
286
+ <h2 className="text-lg font-semibold tracking-tight">Hospital Facilities & Campuses</h2>
287
+ <Badge variant="secondary" className="text-xs">
288
+ {hospitals.length}
289
+ </Badge>
290
+ </div>
291
+ <Button asChild size="sm">
292
+ <Link href="/admin/hospitals/new">
293
+ <Plus className="w-4 h-4 mr-1" />
294
+ Add Hospital
295
+ </Link>
296
+ </Button>
297
+ </div>
298
+
299
+ <DataTable
300
+ columns={hospitalColumns}
301
+ data={hospitals}
302
+ searchable
303
+ searchPlaceholder="Search hospitals by name, code, city..."
304
+ emptyMessage="No hospital facilities configured yet."
305
+ />
306
+ </div>
307
+
308
+ {/* ── Section 3: Medical Device Reference Catalogs ── */}
309
+ <div className="space-y-4 pt-2">
310
+ <div className="space-y-1">
311
+ <div className="flex items-center gap-2">
312
+ <Cpu className="w-5 h-5 text-primary" />
313
+ <h2 className="text-lg font-semibold tracking-tight">Device Master Catalogs & Reference Data</h2>
314
+ </div>
315
+ <p className="text-xs text-muted-foreground">
316
+ Standardize device classifications, risk categories, and approved manufacturers across your health system
317
+ </p>
318
+ </div>
319
+
320
+ <Tabs defaultValue="categories" className="space-y-4">
321
+ <TabsList>
322
+ <TabsTrigger value="categories" className="gap-2">
323
+ <Cpu className="w-3.5 h-3.5" />
324
+ Categories ({categories.length})
325
+ </TabsTrigger>
326
+ <TabsTrigger value="manufacturers" className="gap-2">
327
+ <Factory className="w-3.5 h-3.5" />
328
+ Manufacturers ({manufacturers.length})
329
+ </TabsTrigger>
330
+ </TabsList>
331
+
332
+ <TabsContent value="categories" className="space-y-3">
333
+ <div className="flex items-center justify-between">
334
+ <div className="text-sm font-medium text-muted-foreground">
335
+ Registered Device Categories
336
+ </div>
337
+ <QuickAddCategoryDialog
338
+ triggerLabel="Add Category"
339
+ triggerVariant="default"
340
+ />
341
+ </div>
342
+ <DataTable
343
+ columns={categoryColumns}
344
+ data={categories}
345
+ searchable
346
+ searchPlaceholder="Search categories by name, code, risk..."
347
+ emptyMessage="No device categories configured yet."
348
+ />
349
+ </TabsContent>
350
+
351
+ <TabsContent value="manufacturers" className="space-y-3">
352
+ <div className="flex items-center justify-between">
353
+ <div className="text-sm font-medium text-muted-foreground">
354
+ Approved Manufacturers & Vendors
355
+ </div>
356
+ <QuickAddManufacturerDialog
357
+ triggerLabel="Add Manufacturer"
358
+ triggerVariant="default"
359
+ />
360
+ </div>
361
+ <DataTable
362
+ columns={manufacturerColumns}
363
+ data={manufacturers}
364
+ searchable
365
+ searchPlaceholder="Search manufacturers by name, code, country..."
366
+ emptyMessage="No manufacturers configured yet."
367
+ />
368
+ </TabsContent>
369
+ </Tabs>
370
+ </div>
371
+ </div>
372
+ );
373
+ }
374
+
@@ -0,0 +1,304 @@
1
+ "use client";
2
+
3
+ import { PageHeader } from "@/components/shared/page-header";
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ Form,
7
+ FormControl,
8
+ FormField,
9
+ FormItem,
10
+ FormLabel,
11
+ FormMessage,
12
+ } from "@/components/ui/form";
13
+ import { Input } from "@/components/ui/input";
14
+ import {
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ } from "@/components/ui/select";
21
+ import { zodResolver } from "@hookform/resolvers/zod";
22
+ import { useForm } from "react-hook-form";
23
+ import * as z from "zod";
24
+ import { useRouter } from "next/navigation";
25
+ import { useEffect, useState, use } from "react";
26
+ import { getHospitalById, updateHospital } from "@/lib/actions/hospitals";
27
+ import { getOrganizations } from "@/lib/actions/organizations";
28
+
29
+ const formSchema = z.object({
30
+ organizationId: z.string().min(1, "Please select an organization"),
31
+ name: z.string().min(2, "Name must be at least 2 characters"),
32
+ code: z.string().min(2, "Code must be at least 2 characters").toUpperCase(),
33
+ address: z.string().min(5, "Address is required"),
34
+ city: z.string().min(2, "City is required"),
35
+ country: z.string().min(2, "Country is required"),
36
+ phone: z.string().optional(),
37
+ email: z.string().email("Invalid email").optional().or(z.literal("")),
38
+ timezone: z.string().min(1, "Please select a timezone"),
39
+ status: z.enum(["active", "inactive"]),
40
+ });
41
+
42
+ export default function EditHospitalPage({ params }: { params: Promise<{ hospitalId: string }> }) {
43
+ const resolvedParams = use(params);
44
+ const router = useRouter();
45
+ const [organizations, setOrganizations] = useState<any[]>([]);
46
+ const [isLoading, setIsLoading] = useState(true);
47
+
48
+ const form = useForm<z.infer<typeof formSchema>>({
49
+ resolver: zodResolver(formSchema),
50
+ defaultValues: {
51
+ organizationId: "",
52
+ name: "",
53
+ code: "",
54
+ address: "",
55
+ city: "",
56
+ country: "",
57
+ phone: "",
58
+ email: "",
59
+ timezone: "UTC",
60
+ status: "active",
61
+ },
62
+ });
63
+
64
+ useEffect(() => {
65
+ async function loadData() {
66
+ const [orgsResult, hospitalResult] = await Promise.all([
67
+ getOrganizations(),
68
+ getHospitalById(resolvedParams.hospitalId),
69
+ ]);
70
+
71
+ if (orgsResult.success && orgsResult.data) {
72
+ setOrganizations(orgsResult.data);
73
+ }
74
+
75
+ if (hospitalResult.success && hospitalResult.data) {
76
+ const h = hospitalResult.data;
77
+ form.reset({
78
+ organizationId: h.organizationId,
79
+ name: h.name,
80
+ code: h.code,
81
+ address: h.address || "",
82
+ city: h.city || "",
83
+ country: h.country || "",
84
+ phone: h.phone || "",
85
+ email: h.email || "",
86
+ timezone: h.timezone || "UTC",
87
+ status: (h.status === "inactive" ? "inactive" : "active") as "active" | "inactive",
88
+ });
89
+ }
90
+ setIsLoading(false);
91
+ }
92
+ loadData();
93
+ }, [resolvedParams.hospitalId, form]);
94
+
95
+ async function onSubmit(values: z.infer<typeof formSchema>) {
96
+ const result = await updateHospital(resolvedParams.hospitalId, values);
97
+ if (result.success) {
98
+ router.push(`/admin/hospitals/${resolvedParams.hospitalId}`);
99
+ } else {
100
+ console.error((result as any).error);
101
+ }
102
+ }
103
+
104
+ if (isLoading) {
105
+ return <div className="p-8 text-center">Loading...</div>;
106
+ }
107
+
108
+ return (
109
+ <div className="space-y-6 max-w-2xl">
110
+ <PageHeader title="Edit Hospital" description="Update hospital details" />
111
+
112
+ <Form {...form}>
113
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
114
+ <FormField
115
+ control={form.control}
116
+ name="organizationId"
117
+ render={({ field }) => (
118
+ <FormItem>
119
+ <FormLabel>Organization</FormLabel>
120
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
121
+ <FormControl>
122
+ <SelectTrigger>
123
+ <SelectValue placeholder="Select an organization" />
124
+ </SelectTrigger>
125
+ </FormControl>
126
+ <SelectContent>
127
+ {organizations.map((org) => (
128
+ <SelectItem key={org.id} value={org.id}>
129
+ {org.name}
130
+ </SelectItem>
131
+ ))}
132
+ </SelectContent>
133
+ </Select>
134
+ <FormMessage />
135
+ </FormItem>
136
+ )}
137
+ />
138
+
139
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
140
+ <FormField
141
+ control={form.control}
142
+ name="name"
143
+ render={({ field }) => (
144
+ <FormItem>
145
+ <FormLabel>Name</FormLabel>
146
+ <FormControl>
147
+ <Input placeholder="General Hospital" {...field} />
148
+ </FormControl>
149
+ <FormMessage />
150
+ </FormItem>
151
+ )}
152
+ />
153
+
154
+ <FormField
155
+ control={form.control}
156
+ name="code"
157
+ render={({ field }) => (
158
+ <FormItem>
159
+ <FormLabel>Code</FormLabel>
160
+ <FormControl>
161
+ <Input placeholder="GEN" {...field} />
162
+ </FormControl>
163
+ <FormMessage />
164
+ </FormItem>
165
+ )}
166
+ />
167
+ </div>
168
+
169
+ <FormField
170
+ control={form.control}
171
+ name="address"
172
+ render={({ field }) => (
173
+ <FormItem>
174
+ <FormLabel>Address</FormLabel>
175
+ <FormControl>
176
+ <Input placeholder="123 Health Ave" {...field} />
177
+ </FormControl>
178
+ <FormMessage />
179
+ </FormItem>
180
+ )}
181
+ />
182
+
183
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
184
+ <FormField
185
+ control={form.control}
186
+ name="city"
187
+ render={({ field }) => (
188
+ <FormItem>
189
+ <FormLabel>City</FormLabel>
190
+ <FormControl>
191
+ <Input placeholder="Metropolis" {...field} />
192
+ </FormControl>
193
+ <FormMessage />
194
+ </FormItem>
195
+ )}
196
+ />
197
+
198
+ <FormField
199
+ control={form.control}
200
+ name="country"
201
+ render={({ field }) => (
202
+ <FormItem>
203
+ <FormLabel>Country</FormLabel>
204
+ <FormControl>
205
+ <Input placeholder="USA" {...field} />
206
+ </FormControl>
207
+ <FormMessage />
208
+ </FormItem>
209
+ )}
210
+ />
211
+ </div>
212
+
213
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
214
+ <FormField
215
+ control={form.control}
216
+ name="phone"
217
+ render={({ field }) => (
218
+ <FormItem>
219
+ <FormLabel>Phone (Optional)</FormLabel>
220
+ <FormControl>
221
+ <Input placeholder="+1 555-0123" {...field} />
222
+ </FormControl>
223
+ <FormMessage />
224
+ </FormItem>
225
+ )}
226
+ />
227
+
228
+ <FormField
229
+ control={form.control}
230
+ name="email"
231
+ render={({ field }) => (
232
+ <FormItem>
233
+ <FormLabel>Email (Optional)</FormLabel>
234
+ <FormControl>
235
+ <Input placeholder="info@hospital.com" type="email" {...field} />
236
+ </FormControl>
237
+ <FormMessage />
238
+ </FormItem>
239
+ )}
240
+ />
241
+ </div>
242
+
243
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
244
+ <FormField
245
+ control={form.control}
246
+ name="timezone"
247
+ render={({ field }) => (
248
+ <FormItem>
249
+ <FormLabel>Timezone</FormLabel>
250
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
251
+ <FormControl>
252
+ <SelectTrigger>
253
+ <SelectValue placeholder="Select a timezone" />
254
+ </SelectTrigger>
255
+ </FormControl>
256
+ <SelectContent>
257
+ <SelectItem value="UTC">UTC</SelectItem>
258
+ <SelectItem value="America/New_York">Eastern Time (US & Canada)</SelectItem>
259
+ <SelectItem value="America/Chicago">Central Time (US & Canada)</SelectItem>
260
+ <SelectItem value="America/Denver">Mountain Time (US & Canada)</SelectItem>
261
+ <SelectItem value="America/Los_Angeles">Pacific Time (US & Canada)</SelectItem>
262
+ <SelectItem value="Europe/London">London</SelectItem>
263
+ <SelectItem value="Europe/Paris">Paris</SelectItem>
264
+ </SelectContent>
265
+ </Select>
266
+ <FormMessage />
267
+ </FormItem>
268
+ )}
269
+ />
270
+
271
+ <FormField
272
+ control={form.control}
273
+ name="status"
274
+ render={({ field }) => (
275
+ <FormItem>
276
+ <FormLabel>Status</FormLabel>
277
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
278
+ <FormControl>
279
+ <SelectTrigger>
280
+ <SelectValue placeholder="Select status" />
281
+ </SelectTrigger>
282
+ </FormControl>
283
+ <SelectContent>
284
+ <SelectItem value="active">Active</SelectItem>
285
+ <SelectItem value="inactive">Inactive</SelectItem>
286
+ </SelectContent>
287
+ </Select>
288
+ <FormMessage />
289
+ </FormItem>
290
+ )}
291
+ />
292
+ </div>
293
+
294
+ <div className="flex gap-4">
295
+ <Button type="submit">Save Changes</Button>
296
+ <Button type="button" variant="outline" onClick={() => router.back()}>
297
+ Cancel
298
+ </Button>
299
+ </div>
300
+ </form>
301
+ </Form>
302
+ </div>
303
+ );
304
+ }