@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,166 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ DialogTrigger,
11
+ DialogFooter,
12
+ DialogDescription,
13
+ } from "@/components/ui/dialog";
14
+ import { Button } from "@/components/ui/button";
15
+ import { Label } from "@/components/ui/label";
16
+ import { Textarea } from "@/components/ui/textarea";
17
+ import { UserX, UserCheck, Loader2, AlertTriangle } from "lucide-react";
18
+ import { deactivateUser, reactivateUser } from "@/lib/actions/users";
19
+
20
+ interface UserStatusDialogProps {
21
+ userId: string;
22
+ userName: string;
23
+ currentStatus: "active" | "invited" | "deactivated" | "archived" | string;
24
+ }
25
+
26
+ export function UserStatusDialog({
27
+ userId,
28
+ userName,
29
+ currentStatus,
30
+ }: UserStatusDialogProps) {
31
+ const router = useRouter();
32
+ const [open, setOpen] = useState(false);
33
+ const [loading, setLoading] = useState(false);
34
+ const [reason, setReason] = useState("");
35
+ const [error, setError] = useState("");
36
+
37
+ const isDeactivating = currentStatus === "active";
38
+
39
+ const handleStatusChange = async (e: React.FormEvent) => {
40
+ e.preventDefault();
41
+ if (isDeactivating && !reason.trim()) {
42
+ setError("Please provide a reason for deactivating this user (required for 21 CFR Part 11 audit compliance).");
43
+ return;
44
+ }
45
+
46
+ setLoading(true);
47
+ setError("");
48
+
49
+ try {
50
+ const result = isDeactivating
51
+ ? await deactivateUser(userId, reason.trim())
52
+ : await reactivateUser(userId, reason.trim() || undefined);
53
+
54
+ if (result.success) {
55
+ setOpen(false);
56
+ setReason("");
57
+ router.refresh();
58
+ } else {
59
+ setError((result as any)?.error || "Failed to update user status");
60
+ }
61
+ } catch (err: any) {
62
+ setError(err?.message || "An unexpected error occurred");
63
+ } finally {
64
+ setLoading(false);
65
+ }
66
+ };
67
+
68
+ return (
69
+ <Dialog open={open} onOpenChange={setOpen}>
70
+ <DialogTrigger asChild>
71
+ {isDeactivating ? (
72
+ <Button variant="destructive" size="sm" className="min-h-[38px]">
73
+ <UserX className="w-4 h-4 mr-2" />
74
+ Deactivate User
75
+ </Button>
76
+ ) : (
77
+ <Button
78
+ variant="default"
79
+ size="sm"
80
+ className="bg-green-600 hover:bg-green-700 min-h-[38px]"
81
+ >
82
+ <UserCheck className="w-4 h-4 mr-2" />
83
+ Reactivate User
84
+ </Button>
85
+ )}
86
+ </DialogTrigger>
87
+
88
+ <DialogContent className="sm:max-w-[450px]">
89
+ <form onSubmit={handleStatusChange} className="space-y-4">
90
+ <DialogHeader>
91
+ <div className="flex items-center gap-2">
92
+ {isDeactivating ? (
93
+ <div className="p-2 rounded-full bg-red-100 text-red-600">
94
+ <AlertTriangle className="w-5 h-5" />
95
+ </div>
96
+ ) : (
97
+ <div className="p-2 rounded-full bg-green-100 text-green-600">
98
+ <UserCheck className="w-5 h-5" />
99
+ </div>
100
+ )}
101
+ <DialogTitle>
102
+ {isDeactivating ? `Deactivate ${userName}` : `Reactivate ${userName}`}
103
+ </DialogTitle>
104
+ </div>
105
+ <DialogDescription className="text-xs pt-1">
106
+ {isDeactivating
107
+ ? "Deactivating will revoke immediate login access while preserving all historical audit logs, digital signatures, and task records."
108
+ : "Reactivating will restore login access and allow role assignments for this user."}
109
+ </DialogDescription>
110
+ </DialogHeader>
111
+
112
+ {error && (
113
+ <div className="bg-destructive/10 text-destructive text-xs p-2.5 rounded-md border border-destructive/20">
114
+ {error}
115
+ </div>
116
+ )}
117
+
118
+ <div className="space-y-2">
119
+ <Label htmlFor="status-reason" className="text-xs font-medium">
120
+ {isDeactivating ? "Reason for Deactivation *" : "Notes / Change Reason"}
121
+ </Label>
122
+ <Textarea
123
+ id="status-reason"
124
+ value={reason}
125
+ onChange={(e) => setReason(e.target.value)}
126
+ placeholder={
127
+ isDeactivating
128
+ ? "e.g. Employee departure, temporary suspension, or role transfer..."
129
+ : "e.g. Returned from leave, reactivation approved..."
130
+ }
131
+ className="text-xs resize-none"
132
+ rows={3}
133
+ required={isDeactivating}
134
+ />
135
+ <p className="text-[11px] text-muted-foreground">
136
+ This note will be permanently recorded in the 21 CFR Part 11 system audit trail.
137
+ </p>
138
+ </div>
139
+
140
+ <DialogFooter className="gap-2 sm:gap-0 pt-2">
141
+ <Button
142
+ type="button"
143
+ variant="outline"
144
+ size="sm"
145
+ onClick={() => setOpen(false)}
146
+ disabled={loading}
147
+ >
148
+ Cancel
149
+ </Button>
150
+ <Button
151
+ type="submit"
152
+ variant={isDeactivating ? "destructive" : "default"}
153
+ size="sm"
154
+ disabled={loading}
155
+ className={!isDeactivating ? "bg-green-600 hover:bg-green-700" : ""}
156
+ >
157
+ {loading && <Loader2 className="w-3.5 h-3.5 mr-1.5 animate-spin" />}
158
+ {isDeactivating ? "Confirm Deactivation" : "Confirm Reactivation"}
159
+ </Button>
160
+ </DialogFooter>
161
+ </form>
162
+ </DialogContent>
163
+ </Dialog>
164
+ );
165
+ }
166
+
@@ -0,0 +1,514 @@
1
+ 'use client';
2
+
3
+ import { useState, useMemo, useEffect } from 'react';
4
+ import { useRouter, useSearchParams } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { PageHeader } from '@/components/shared/page-header';
7
+ import { StatusBadge } from '@/components/shared/status-badge';
8
+ import { Button } from '@/components/ui/button';
9
+ import { Badge } from '@/components/ui/badge';
10
+ import { Input } from '@/components/ui/input';
11
+ import { Card, CardContent } from '@/components/ui/card';
12
+ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
13
+ import {
14
+ Select,
15
+ SelectContent,
16
+ SelectItem,
17
+ SelectTrigger,
18
+ SelectValue,
19
+ } from '@/components/ui/select';
20
+ import {
21
+ Plus,
22
+ Eye,
23
+ Edit,
24
+ Search,
25
+ Users,
26
+ Wrench,
27
+ Stethoscope,
28
+ ShieldCheck,
29
+ Building2,
30
+ CheckCircle2,
31
+ FilterX,
32
+ } from 'lucide-react';
33
+ import { AddUserTypeDialog, UserTypeSelection } from '@/components/admin/add-user-type-dialog';
34
+ import { AddEngineerDialog } from '@/components/admin/add-engineer-dialog';
35
+ import { AddDepartmentUserDialog } from '@/components/admin/add-department-user-dialog';
36
+
37
+ export interface UserItem {
38
+ id: string;
39
+ fullName: string;
40
+ email: string;
41
+ employeeIdentifier?: string | null;
42
+ jobTitle?: string | null;
43
+ phone?: string | null;
44
+ accountStatus?: string | null;
45
+ status?: string | null;
46
+ lastLoginAt?: Date | string | null;
47
+ roles?: { id: string; code: string; name: string }[];
48
+ scopes?: any[];
49
+ departmentName?: string | null;
50
+ hospitalName?: string | null;
51
+ userType?: 'engineer' | 'department' | 'admin' | 'other';
52
+ createdAt?: Date | string | null;
53
+ }
54
+
55
+ interface UsersPageClientProps {
56
+ users: UserItem[];
57
+ }
58
+
59
+ export function UsersPageClient({ users }: UsersPageClientProps) {
60
+ const router = useRouter();
61
+ const searchParams = useSearchParams();
62
+
63
+ // Dialog states
64
+ const [isTypeDialogOpen, setIsTypeDialogOpen] = useState(false);
65
+ const [isEngineerDialogOpen, setIsEngineerDialogOpen] = useState(false);
66
+ const [isDepartmentDialogOpen, setIsDepartmentDialogOpen] = useState(false);
67
+
68
+ // Check URL query param for auto-opening add dialog (e.g., /admin/users?action=new)
69
+ useEffect(() => {
70
+ if (searchParams.get('action') === 'new') {
71
+ setIsTypeDialogOpen(true);
72
+ }
73
+ }, [searchParams]);
74
+
75
+ // Search & Filter state
76
+ const [searchQuery, setSearchQuery] = useState('');
77
+ const [categoryFilter, setCategoryFilter] = useState<'all' | 'engineer' | 'department' | 'admin'>('all');
78
+ const [statusFilter, setStatusFilter] = useState<string>('all');
79
+
80
+ const handleSelectType = (type: UserTypeSelection) => {
81
+ setIsTypeDialogOpen(false);
82
+ if (type === 'engineer') {
83
+ setIsEngineerDialogOpen(true);
84
+ } else {
85
+ setIsDepartmentDialogOpen(true);
86
+ }
87
+ };
88
+
89
+ const handleSuccess = () => {
90
+ router.refresh();
91
+ };
92
+
93
+ // Metrics counts
94
+ const stats = useMemo(() => {
95
+ const total = users.length;
96
+ const engineers = users.filter((u) => u.userType === 'engineer').length;
97
+ const departmentStaff = users.filter((u) => u.userType === 'department').length;
98
+ const activeAccounts = users.filter((u) => (u.accountStatus || u.status) === 'active').length;
99
+ const admins = users.filter((u) => u.userType === 'admin').length;
100
+
101
+ return { total, engineers, departmentStaff, activeAccounts, admins };
102
+ }, [users]);
103
+
104
+ // Filtered users
105
+ const filteredUsers = useMemo(() => {
106
+ return users.filter((u) => {
107
+ // 1. Category tab filter
108
+ if (categoryFilter !== 'all' && u.userType !== categoryFilter) {
109
+ return false;
110
+ }
111
+
112
+ // 2. Status filter
113
+ const userStatus = u.accountStatus || u.status || '';
114
+ if (statusFilter !== 'all' && userStatus !== statusFilter) {
115
+ return false;
116
+ }
117
+
118
+ // 3. Search query (name, email, identifier, title, department, hospital)
119
+ if (searchQuery.trim()) {
120
+ const query = searchQuery.toLowerCase().trim();
121
+ const matchesName = u.fullName?.toLowerCase().includes(query);
122
+ const matchesEmail = u.email?.toLowerCase().includes(query);
123
+ const matchesIdentifier = u.employeeIdentifier?.toLowerCase().includes(query);
124
+ const matchesTitle = u.jobTitle?.toLowerCase().includes(query);
125
+ const matchesDept = u.departmentName?.toLowerCase().includes(query);
126
+ const matchesHosp = u.hospitalName?.toLowerCase().includes(query);
127
+ const matchesRole = u.roles?.some((r) =>
128
+ r.name?.toLowerCase().includes(query) || r.code?.toLowerCase().includes(query)
129
+ );
130
+
131
+ if (!matchesName && !matchesEmail && !matchesIdentifier && !matchesTitle && !matchesDept && !matchesHosp && !matchesRole) {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ return true;
137
+ });
138
+ }, [users, categoryFilter, statusFilter, searchQuery]);
139
+
140
+ return (
141
+ <div className="space-y-6">
142
+ {/* ── Page Header ── */}
143
+ <PageHeader
144
+ title="User Governance"
145
+ description="Provision, configure, and monitor biomedical engineers, department clinicians, and system administrators"
146
+ >
147
+ <Button onClick={() => setIsTypeDialogOpen(true)} className="gap-2 shadow-xs">
148
+ <Plus className="w-4 h-4" />
149
+ Add User
150
+ </Button>
151
+ </PageHeader>
152
+
153
+ {/* ── Key Metrics Cards ── */}
154
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
155
+ <Card className="border shadow-2xs">
156
+ <CardContent className="p-4 flex items-center justify-between">
157
+ <div className="space-y-0.5">
158
+ <span className="text-xs font-medium text-muted-foreground">Total Users</span>
159
+ <p className="text-2xl font-bold tracking-tight text-foreground">{stats.total}</p>
160
+ </div>
161
+ <div className="w-10 h-10 rounded-xl bg-muted/60 flex items-center justify-center text-muted-foreground">
162
+ <Users className="w-5 h-5" />
163
+ </div>
164
+ </CardContent>
165
+ </Card>
166
+
167
+ <Card className="border shadow-2xs">
168
+ <CardContent className="p-4 flex items-center justify-between">
169
+ <div className="space-y-0.5">
170
+ <span className="text-xs font-medium text-muted-foreground">Biomedical Engineers</span>
171
+ <p className="text-2xl font-bold tracking-tight text-blue-600 dark:text-blue-400">{stats.engineers}</p>
172
+ </div>
173
+ <div className="w-10 h-10 rounded-xl bg-blue-100/80 text-blue-600 dark:bg-blue-950/60 dark:text-blue-400 flex items-center justify-center">
174
+ <Wrench className="w-5 h-5" />
175
+ </div>
176
+ </CardContent>
177
+ </Card>
178
+
179
+ <Card className="border shadow-2xs">
180
+ <CardContent className="p-4 flex items-center justify-between">
181
+ <div className="space-y-0.5">
182
+ <span className="text-xs font-medium text-muted-foreground">Clinical & Dept Staff</span>
183
+ <p className="text-2xl font-bold tracking-tight text-purple-600 dark:text-purple-400">{stats.departmentStaff}</p>
184
+ </div>
185
+ <div className="w-10 h-10 rounded-xl bg-purple-100/80 text-purple-600 dark:bg-purple-950/60 dark:text-purple-400 flex items-center justify-center">
186
+ <Stethoscope className="w-5 h-5" />
187
+ </div>
188
+ </CardContent>
189
+ </Card>
190
+
191
+ <Card className="border shadow-2xs">
192
+ <CardContent className="p-4 flex items-center justify-between">
193
+ <div className="space-y-0.5">
194
+ <span className="text-xs font-medium text-muted-foreground">Active Accounts</span>
195
+ <p className="text-2xl font-bold tracking-tight text-emerald-600 dark:text-emerald-400">{stats.activeAccounts}</p>
196
+ </div>
197
+ <div className="w-10 h-10 rounded-xl bg-emerald-100/80 text-emerald-600 dark:bg-emerald-950/60 dark:text-emerald-400 flex items-center justify-center">
198
+ <CheckCircle2 className="w-5 h-5" />
199
+ </div>
200
+ </CardContent>
201
+ </Card>
202
+ </div>
203
+
204
+ {/* ── Search & Filter Controls ── */}
205
+ <div className="space-y-3">
206
+ <div className="flex flex-col md:flex-row gap-3 items-stretch md:items-center justify-between">
207
+ {/* Category Tabs */}
208
+ <Tabs
209
+ value={categoryFilter}
210
+ onValueChange={(val) => setCategoryFilter(val as any)}
211
+ className="w-full md:w-auto"
212
+ >
213
+ <TabsList className="grid grid-cols-4 w-full md:w-auto">
214
+ <TabsTrigger value="all" className="text-xs font-medium gap-1 px-3">
215
+ All <span className="text-[10px] text-muted-foreground">({stats.total})</span>
216
+ </TabsTrigger>
217
+ <TabsTrigger value="engineer" className="text-xs font-medium gap-1 px-3">
218
+ <Wrench className="w-3.5 h-3.5 text-blue-500" />
219
+ Engineers <span className="text-[10px] text-muted-foreground">({stats.engineers})</span>
220
+ </TabsTrigger>
221
+ <TabsTrigger value="department" className="text-xs font-medium gap-1 px-3">
222
+ <Stethoscope className="w-3.5 h-3.5 text-purple-500" />
223
+ Staff <span className="text-[10px] text-muted-foreground">({stats.departmentStaff})</span>
224
+ </TabsTrigger>
225
+ <TabsTrigger value="admin" className="text-xs font-medium gap-1 px-3">
226
+ <ShieldCheck className="w-3.5 h-3.5 text-amber-500" />
227
+ Admins <span className="text-[10px] text-muted-foreground">({stats.admins})</span>
228
+ </TabsTrigger>
229
+ </TabsList>
230
+ </Tabs>
231
+
232
+ {/* Search and Status Dropdown */}
233
+ <div className="flex items-center gap-2 w-full md:w-auto">
234
+ <div className="relative flex-1 md:w-64">
235
+ <Search className="w-4 h-4 absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
236
+ <Input
237
+ placeholder="Search users, email, dept..."
238
+ value={searchQuery}
239
+ onChange={(e) => setSearchQuery(e.target.value)}
240
+ className="pl-8 text-xs h-9 bg-background"
241
+ />
242
+ </div>
243
+
244
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
245
+ <SelectTrigger className="w-[130px] h-9 text-xs">
246
+ <SelectValue placeholder="Status" />
247
+ </SelectTrigger>
248
+ <SelectContent>
249
+ <SelectItem value="all">All Statuses</SelectItem>
250
+ <SelectItem value="active">Active</SelectItem>
251
+ <SelectItem value="invited">Invited</SelectItem>
252
+ <SelectItem value="deactivated">Deactivated</SelectItem>
253
+ </SelectContent>
254
+ </Select>
255
+
256
+ {(searchQuery || statusFilter !== 'all' || categoryFilter !== 'all') && (
257
+ <Button
258
+ variant="ghost"
259
+ size="icon"
260
+ onClick={() => {
261
+ setSearchQuery('');
262
+ setStatusFilter('all');
263
+ setCategoryFilter('all');
264
+ }}
265
+ className="h-9 w-9 text-muted-foreground hover:text-foreground"
266
+ title="Clear filters"
267
+ >
268
+ <FilterX className="w-4 h-4" />
269
+ </Button>
270
+ )}
271
+ </div>
272
+ </div>
273
+
274
+ {/* ── Users Table ── */}
275
+ <div className="rounded-xl border bg-card shadow-2xs overflow-hidden">
276
+ <div className="overflow-x-auto">
277
+ <table className="w-full text-left text-sm border-collapse">
278
+ <thead>
279
+ <tr className="border-b bg-muted/40 text-xs font-semibold text-muted-foreground">
280
+ <th className="py-3 px-4">User</th>
281
+ <th className="py-3 px-3">Type</th>
282
+ <th className="py-3 px-3">Roles</th>
283
+ <th className="py-3 px-3">Facility / Department</th>
284
+ <th className="py-3 px-3">Status</th>
285
+ <th className="py-3 px-3">Last Login</th>
286
+ <th className="py-3 px-4 text-right">Actions</th>
287
+ </tr>
288
+ </thead>
289
+ <tbody className="divide-y divide-border/60">
290
+ {filteredUsers.length > 0 ? (
291
+ filteredUsers.map((item) => {
292
+ const initials = item.fullName
293
+ ? item.fullName
294
+ .split(' ')
295
+ .map((n) => n[0])
296
+ .slice(0, 2)
297
+ .join('')
298
+ .toUpperCase()
299
+ : 'U';
300
+
301
+ const isEngineer = item.userType === 'engineer';
302
+ const isStaff = item.userType === 'department';
303
+ const isAdmin = item.userType === 'admin';
304
+
305
+ return (
306
+ <tr
307
+ key={item.id}
308
+ className="hover:bg-muted/30 transition-colors group"
309
+ >
310
+ {/* User Identity */}
311
+ <td className="py-3 px-4">
312
+ <div className="flex items-center gap-3">
313
+ <div
314
+ className={`w-9 h-9 rounded-full flex items-center justify-center font-bold text-xs shrink-0 ${
315
+ isEngineer
316
+ ? 'bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300'
317
+ : isStaff
318
+ ? 'bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-300'
319
+ : 'bg-muted text-foreground'
320
+ }`}
321
+ >
322
+ {initials}
323
+ </div>
324
+ <div className="flex flex-col min-w-0">
325
+ <span className="font-semibold text-foreground text-xs leading-snug truncate">
326
+ {item.fullName}
327
+ </span>
328
+ <span className="text-[11px] text-muted-foreground truncate">
329
+ {item.email}
330
+ </span>
331
+ {item.employeeIdentifier && (
332
+ <span className="font-mono text-[10px] text-muted-foreground">
333
+ ID: {item.employeeIdentifier}
334
+ </span>
335
+ )}
336
+ </div>
337
+ </div>
338
+ </td>
339
+
340
+ {/* Account Type Badge */}
341
+ <td className="py-3 px-3 whitespace-nowrap">
342
+ {isEngineer ? (
343
+ <Badge
344
+ variant="outline"
345
+ className="text-[11px] gap-1 border-blue-200 bg-blue-50/70 text-blue-700 dark:border-blue-900/60 dark:bg-blue-950/40 dark:text-blue-300 font-medium"
346
+ >
347
+ <Wrench className="w-3 h-3 text-blue-500" />
348
+ Biomed Engineer
349
+ </Badge>
350
+ ) : isStaff ? (
351
+ <Badge
352
+ variant="outline"
353
+ className="text-[11px] gap-1 border-purple-200 bg-purple-50/70 text-purple-700 dark:border-purple-900/60 dark:bg-purple-950/40 dark:text-purple-300 font-medium"
354
+ >
355
+ <Stethoscope className="w-3 h-3 text-purple-500" />
356
+ Clinical Staff
357
+ </Badge>
358
+ ) : isAdmin ? (
359
+ <Badge
360
+ variant="outline"
361
+ className="text-[11px] gap-1 border-amber-200 bg-amber-50/70 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/40 dark:text-amber-300 font-medium"
362
+ >
363
+ <ShieldCheck className="w-3 h-3 text-amber-500" />
364
+ Administrator
365
+ </Badge>
366
+ ) : (
367
+ <Badge variant="outline" className="text-[11px] text-muted-foreground font-medium">
368
+ User
369
+ </Badge>
370
+ )}
371
+ </td>
372
+
373
+ {/* Roles */}
374
+ <td className="py-3 px-3">
375
+ <div className="flex gap-1 flex-wrap max-w-xs">
376
+ {item.roles && item.roles.length > 0 ? (
377
+ item.roles.map((role) => (
378
+ <Badge
379
+ key={role.id || role.code}
380
+ variant="secondary"
381
+ className="text-[10px] px-1.5 py-0 font-normal"
382
+ >
383
+ {role.name || role.code}
384
+ </Badge>
385
+ ))
386
+ ) : (
387
+ <span className="text-muted-foreground text-xs italic">No roles</span>
388
+ )}
389
+ </div>
390
+ </td>
391
+
392
+ {/* Facility / Department */}
393
+ <td className="py-3 px-3">
394
+ <div className="flex flex-col text-xs">
395
+ {item.departmentName ? (
396
+ <span className="font-medium text-foreground flex items-center gap-1">
397
+ <Building2 className="w-3 h-3 text-muted-foreground" />
398
+ {item.departmentName}
399
+ </span>
400
+ ) : (
401
+ <span className="text-muted-foreground text-xs italic">
402
+ Facility-wide
403
+ </span>
404
+ )}
405
+ {item.hospitalName && (
406
+ <span className="text-[11px] text-muted-foreground">
407
+ {item.hospitalName}
408
+ </span>
409
+ )}
410
+ </div>
411
+ </td>
412
+
413
+ {/* Status */}
414
+ <td className="py-3 px-3 whitespace-nowrap">
415
+ <StatusBadge status={item.accountStatus || item.status} />
416
+ </td>
417
+
418
+ {/* Last Login */}
419
+ <td className="py-3 px-3 whitespace-nowrap text-xs text-muted-foreground">
420
+ {item.lastLoginAt ? (
421
+ new Date(item.lastLoginAt).toLocaleDateString(undefined, {
422
+ month: 'short',
423
+ day: 'numeric',
424
+ year: 'numeric',
425
+ })
426
+ ) : (
427
+ <span className="italic text-muted-foreground/70">Never</span>
428
+ )}
429
+ </td>
430
+
431
+ {/* Actions */}
432
+ <td className="py-3 px-4 text-right whitespace-nowrap">
433
+ <div className="flex items-center justify-end gap-1">
434
+ <Button
435
+ variant="ghost"
436
+ size="icon"
437
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
438
+ asChild
439
+ title="View user details"
440
+ >
441
+ <Link href={`/admin/users/${item.id}`}>
442
+ <Eye className="w-4 h-4" />
443
+ </Link>
444
+ </Button>
445
+ <Button
446
+ variant="ghost"
447
+ size="icon"
448
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
449
+ asChild
450
+ title="Edit user profile"
451
+ >
452
+ <Link href={`/admin/users/${item.id}/edit`}>
453
+ <Edit className="w-4 h-4" />
454
+ </Link>
455
+ </Button>
456
+ </div>
457
+ </td>
458
+ </tr>
459
+ );
460
+ })
461
+ ) : (
462
+ <tr>
463
+ <td colSpan={7} className="text-center py-12 text-muted-foreground">
464
+ <div className="flex flex-col items-center justify-center space-y-2">
465
+ <Users className="w-8 h-8 text-muted-foreground/50" />
466
+ <p className="text-sm font-medium">No users found</p>
467
+ <p className="text-xs text-muted-foreground/80 max-w-xs">
468
+ {searchQuery || statusFilter !== 'all' || categoryFilter !== 'all'
469
+ ? 'Try adjusting your search terms or active filters.'
470
+ : 'Get started by adding your first biomedical engineer or department staff member.'}
471
+ </p>
472
+ {!searchQuery && statusFilter === 'all' && categoryFilter === 'all' && (
473
+ <Button
474
+ size="sm"
475
+ onClick={() => setIsTypeDialogOpen(true)}
476
+ className="mt-2 gap-1.5"
477
+ >
478
+ <Plus className="w-3.5 h-3.5" />
479
+ Add User
480
+ </Button>
481
+ )}
482
+ </div>
483
+ </td>
484
+ </tr>
485
+ )}
486
+ </tbody>
487
+ </table>
488
+ </div>
489
+ </div>
490
+ </div>
491
+
492
+ {/* ── Dialog 1: Add User Type Selector ── */}
493
+ <AddUserTypeDialog
494
+ open={isTypeDialogOpen}
495
+ onOpenChange={setIsTypeDialogOpen}
496
+ onSelect={handleSelectType}
497
+ />
498
+
499
+ {/* ── Dialog 2a: Add Biomedical Engineer ── */}
500
+ <AddEngineerDialog
501
+ open={isEngineerDialogOpen}
502
+ onOpenChange={setIsEngineerDialogOpen}
503
+ onSuccess={handleSuccess}
504
+ />
505
+
506
+ {/* ── Dialog 2b: Add Department Staff ── */}
507
+ <AddDepartmentUserDialog
508
+ open={isDepartmentDialogOpen}
509
+ onOpenChange={setIsDepartmentDialogOpen}
510
+ onSuccess={handleSuccess}
511
+ />
512
+ </div>
513
+ );
514
+ }