@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,759 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ } from '@/components/ui/dialog';
12
+ import { Button } from '@/components/ui/button';
13
+ import { Input } from '@/components/ui/input';
14
+ import { Label } from '@/components/ui/label';
15
+ import {
16
+ Select,
17
+ SelectContent,
18
+ SelectItem,
19
+ SelectTrigger,
20
+ SelectValue,
21
+ } from '@/components/ui/select';
22
+ import { addUser } from '@/lib/actions/users';
23
+ import { getOrganizations } from '@/lib/actions/organizations';
24
+ import { getHospitals } from '@/lib/actions/hospitals';
25
+ import { getDepartments } from '@/lib/actions/departments';
26
+ import { getRoles } from '@/lib/actions/roles';
27
+ import { toast } from 'sonner';
28
+ import { cn } from '@/lib/utils';
29
+ import {
30
+ Loader2,
31
+ Sparkles,
32
+ Eye,
33
+ EyeOff,
34
+ User,
35
+ KeyRound,
36
+ Building2,
37
+ ShieldCheck,
38
+ Phone,
39
+ Check,
40
+ Stethoscope,
41
+ } from 'lucide-react';
42
+
43
+ export interface AddDepartmentUserDialogProps {
44
+ open: boolean;
45
+ onOpenChange: (open: boolean) => void;
46
+ onSuccess: () => void;
47
+ }
48
+
49
+ interface OrganizationOption {
50
+ id: string;
51
+ name: string;
52
+ }
53
+
54
+ interface HospitalOption {
55
+ id: string;
56
+ name: string;
57
+ }
58
+
59
+ interface DepartmentOption {
60
+ id: string;
61
+ name: string;
62
+ code?: string | null;
63
+ }
64
+
65
+ interface RoleOption {
66
+ id: string;
67
+ code: string;
68
+ name: string;
69
+ }
70
+
71
+ export function AddDepartmentUserDialog({
72
+ open,
73
+ onOpenChange,
74
+ onSuccess,
75
+ }: AddDepartmentUserDialogProps) {
76
+ // Form State
77
+ const [fullName, setFullName] = useState('');
78
+ const [email, setEmail] = useState('');
79
+ const [jobTitle, setJobTitle] = useState('');
80
+ const [username, setUsername] = useState('');
81
+ const [password, setPassword] = useState('');
82
+ const [showPassword, setShowPassword] = useState(false);
83
+ const [phone, setPhone] = useState('');
84
+
85
+ // Department Assignment State
86
+ const [organizationId, setOrganizationId] = useState('');
87
+ const [hospitalId, setHospitalId] = useState('');
88
+ const [departmentId, setDepartmentId] = useState('');
89
+
90
+ // Role Access Level State ('STAFF' or 'DEPT_MGR')
91
+ const [selectedRoleCode, setSelectedRoleCode] = useState<'STAFF' | 'DEPT_MGR'>('STAFF');
92
+
93
+ // Loaded Options
94
+ const [organizations, setOrganizations] = useState<OrganizationOption[]>([]);
95
+ const [hospitalsList, setHospitalsList] = useState<HospitalOption[]>([]);
96
+ const [departmentsList, setDepartmentsList] = useState<DepartmentOption[]>([]);
97
+ const [roles, setRoles] = useState<RoleOption[]>([]);
98
+
99
+ // Loading States
100
+ const [isLoadingInitial, setIsLoadingInitial] = useState(false);
101
+ const [isLoadingHospitals, setIsLoadingHospitals] = useState(false);
102
+ const [isLoadingDepartments, setIsLoadingDepartments] = useState(false);
103
+ const [isSubmitting, setIsSubmitting] = useState(false);
104
+
105
+ // Reset form inputs
106
+ const resetForm = useCallback(() => {
107
+ setFullName('');
108
+ setEmail('');
109
+ setJobTitle('');
110
+ setUsername('');
111
+ setPassword('');
112
+ setShowPassword(false);
113
+ setPhone('');
114
+ setSelectedRoleCode('STAFF');
115
+ setHospitalId('');
116
+ setDepartmentId('');
117
+ }, []);
118
+
119
+ // Fetch initial organizations and roles when dialog opens
120
+ useEffect(() => {
121
+ if (!open) {
122
+ resetForm();
123
+ return;
124
+ }
125
+
126
+ let isMounted = true;
127
+ const loadInitialData = async () => {
128
+ setIsLoadingInitial(true);
129
+ try {
130
+ const [orgRes, rolesRes] = await Promise.all([
131
+ getOrganizations(),
132
+ getRoles(),
133
+ ]);
134
+
135
+ if (!isMounted) return;
136
+
137
+ if (orgRes.success && Array.isArray(orgRes.data)) {
138
+ setOrganizations(orgRes.data);
139
+ if (orgRes.data.length === 1) {
140
+ setOrganizationId(orgRes.data[0].id);
141
+ }
142
+ }
143
+
144
+ if (rolesRes.success && Array.isArray(rolesRes.data)) {
145
+ setRoles(rolesRes.data);
146
+ }
147
+ } catch (err: any) {
148
+ if (isMounted) {
149
+ toast.error(err?.message || 'Failed to load initial data');
150
+ }
151
+ } finally {
152
+ if (isMounted) {
153
+ setIsLoadingInitial(false);
154
+ }
155
+ }
156
+ };
157
+
158
+ loadInitialData();
159
+
160
+ return () => {
161
+ isMounted = false;
162
+ };
163
+ }, [open, resetForm]);
164
+
165
+ // Load hospitals when organization is selected
166
+ useEffect(() => {
167
+ if (!organizationId) {
168
+ setHospitalsList([]);
169
+ setHospitalId('');
170
+ setDepartmentsList([]);
171
+ setDepartmentId('');
172
+ return;
173
+ }
174
+
175
+ let isMounted = true;
176
+ const loadHospitalsData = async () => {
177
+ setIsLoadingHospitals(true);
178
+ try {
179
+ const hospRes = await getHospitals({ organizationId });
180
+ if (!isMounted) return;
181
+
182
+ if (hospRes.success && Array.isArray(hospRes.data)) {
183
+ setHospitalsList(hospRes.data);
184
+ if (hospRes.data.length === 1) {
185
+ setHospitalId(hospRes.data[0].id);
186
+ }
187
+ } else {
188
+ setHospitalsList([]);
189
+ }
190
+ } catch (err: any) {
191
+ if (isMounted) {
192
+ toast.error(err?.message || 'Failed to load hospitals');
193
+ }
194
+ } finally {
195
+ if (isMounted) {
196
+ setIsLoadingHospitals(false);
197
+ }
198
+ }
199
+ };
200
+
201
+ loadHospitalsData();
202
+
203
+ return () => {
204
+ isMounted = false;
205
+ };
206
+ }, [organizationId]);
207
+
208
+ // Load departments when hospital is selected
209
+ useEffect(() => {
210
+ if (!hospitalId) {
211
+ setDepartmentsList([]);
212
+ setDepartmentId('');
213
+ return;
214
+ }
215
+
216
+ let isMounted = true;
217
+ const loadDepartmentsData = async () => {
218
+ setIsLoadingDepartments(true);
219
+ try {
220
+ const deptRes = await getDepartments({ hospitalId });
221
+ if (!isMounted) return;
222
+
223
+ if (deptRes.success && Array.isArray(deptRes.data)) {
224
+ setDepartmentsList(deptRes.data);
225
+ if (deptRes.data.length === 1) {
226
+ setDepartmentId(deptRes.data[0].id);
227
+ }
228
+ } else {
229
+ setDepartmentsList([]);
230
+ }
231
+ } catch (err: any) {
232
+ if (isMounted) {
233
+ toast.error(err?.message || 'Failed to load departments');
234
+ }
235
+ } finally {
236
+ if (isMounted) {
237
+ setIsLoadingDepartments(false);
238
+ }
239
+ }
240
+ };
241
+
242
+ loadDepartmentsData();
243
+
244
+ return () => {
245
+ isMounted = false;
246
+ };
247
+ }, [hospitalId]);
248
+
249
+ // Random Password Generator
250
+ const generateRandomPassword = () => {
251
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%';
252
+ let pwd = 'Bemms@';
253
+ for (let i = 0; i < 4; i++) {
254
+ pwd += chars.charAt(Math.floor(Math.random() * chars.length));
255
+ }
256
+ setPassword(pwd);
257
+ setShowPassword(true);
258
+ toast.info(`Generated temporary password: ${pwd}`);
259
+ };
260
+
261
+ // Form Submit Handler
262
+ const handleSubmit = async (e: React.FormEvent) => {
263
+ e.preventDefault();
264
+
265
+ // Validations
266
+ const trimmedName = fullName.trim();
267
+ if (!trimmedName || trimmedName.length < 2) {
268
+ toast.error('Full name is required (at least 2 characters)');
269
+ return;
270
+ }
271
+
272
+ const trimmedEmail = email.trim().toLowerCase();
273
+ if (!trimmedEmail || !trimmedEmail.includes('@')) {
274
+ toast.error('A valid official email address is required');
275
+ return;
276
+ }
277
+
278
+ if (!organizationId) {
279
+ toast.error('Please select an organization');
280
+ return;
281
+ }
282
+
283
+ if (!hospitalId) {
284
+ toast.error('Please select a hospital');
285
+ return;
286
+ }
287
+
288
+ if (!departmentId) {
289
+ toast.error('Please select a hospital department');
290
+ return;
291
+ }
292
+
293
+ if (password && password.trim().length > 0 && password.trim().length < 6) {
294
+ toast.error('Initial password must be at least 6 characters long');
295
+ return;
296
+ }
297
+
298
+ const roleObj = roles.find((r) => r.code === selectedRoleCode);
299
+ if (!roleObj) {
300
+ toast.error(`The "${selectedRoleCode}" role was not found in the system. Please reload or contact an administrator.`);
301
+ return;
302
+ }
303
+
304
+ setIsSubmitting(true);
305
+ try {
306
+ const finalUsername = username.trim() || undefined;
307
+
308
+ const res = await addUser({
309
+ fullName: trimmedName,
310
+ email: trimmedEmail,
311
+ username: finalUsername,
312
+ employeeIdentifier: finalUsername,
313
+ phone: phone.trim() || undefined,
314
+ jobTitle: jobTitle.trim() || undefined,
315
+ password: password.trim() ? password.trim() : null,
316
+ organizationId,
317
+ roleIds: [roleObj.id],
318
+ hospitalId,
319
+ departmentId,
320
+ });
321
+
322
+ if (res.success) {
323
+ toast.success(`Department user "${trimmedName}" created successfully!`);
324
+ resetForm();
325
+ onSuccess();
326
+ onOpenChange(false);
327
+ } else {
328
+ toast.error((res as any)?.error || 'Failed to create department user');
329
+ }
330
+ } catch (err: any) {
331
+ toast.error(err?.message || 'An unexpected error occurred while adding user');
332
+ } finally {
333
+ setIsSubmitting(false);
334
+ }
335
+ };
336
+
337
+ return (
338
+ <Dialog open={open} onOpenChange={onOpenChange}>
339
+ <DialogContent className="sm:max-w-xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden">
340
+ {/* Dialog Header */}
341
+ <DialogHeader className="p-6 pb-4 border-b shrink-0 pr-12 text-left">
342
+ <DialogTitle className="text-lg font-semibold flex items-center gap-2">
343
+ <Stethoscope className="w-5 h-5 text-primary" />
344
+ Add Department User
345
+ </DialogTitle>
346
+ <DialogDescription className="text-sm text-muted-foreground mt-1">
347
+ Create a clinical or administrative staff account assigned to a hospital department.
348
+ </DialogDescription>
349
+ </DialogHeader>
350
+
351
+ {/* Scrollable Form Body */}
352
+ <form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0 overflow-hidden">
353
+ <div className="flex-1 overflow-y-auto p-6 space-y-6">
354
+ {/* 1. Staff Identity */}
355
+ <div className="space-y-4">
356
+ <div className="flex items-center gap-2 pb-1 border-b text-xs font-semibold uppercase tracking-wider text-muted-foreground">
357
+ <User className="w-3.5 h-3.5 text-primary" />
358
+ Staff Identity
359
+ </div>
360
+
361
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
362
+ <div className="space-y-1.5">
363
+ <Label htmlFor="dept-user-name" className="text-xs font-medium">
364
+ Full Name <span className="text-destructive">*</span>
365
+ </Label>
366
+ <Input
367
+ id="dept-user-name"
368
+ placeholder="e.g. Dr. Sara Ahmed or Nurse John"
369
+ value={fullName}
370
+ onChange={(e) => setFullName(e.target.value)}
371
+ required
372
+ disabled={isSubmitting}
373
+ />
374
+ </div>
375
+ <div className="space-y-1.5">
376
+ <Label htmlFor="dept-user-email" className="text-xs font-medium">
377
+ Official Email <span className="text-destructive">*</span>
378
+ </Label>
379
+ <Input
380
+ id="dept-user-email"
381
+ type="email"
382
+ placeholder="sara.ahmed@hospital.org"
383
+ value={email}
384
+ onChange={(e) => setEmail(e.target.value)}
385
+ required
386
+ disabled={isSubmitting}
387
+ />
388
+ </div>
389
+ </div>
390
+
391
+ <div className="space-y-1.5">
392
+ <Label htmlFor="dept-user-job-title" className="text-xs font-medium">
393
+ Job / Clinical Title
394
+ </Label>
395
+ <Input
396
+ id="dept-user-job-title"
397
+ list="clinical-title-options"
398
+ placeholder="e.g. Head Nurse, Ward Physician, Lab Technician"
399
+ value={jobTitle}
400
+ onChange={(e) => setJobTitle(e.target.value)}
401
+ disabled={isSubmitting}
402
+ />
403
+ <datalist id="clinical-title-options">
404
+ <option value="Head Nurse" />
405
+ <option value="Ward Physician" />
406
+ <option value="Lab Technician" />
407
+ <option value="Charge Nurse" />
408
+ <option value="Clinical Supervisor" />
409
+ <option value="Radiology Technician" />
410
+ <option value="ICU Specialist" />
411
+ <option value="Department Administrator" />
412
+ </datalist>
413
+ </div>
414
+ </div>
415
+
416
+ {/* 2. Login Credentials */}
417
+ <div className="space-y-4">
418
+ <div className="flex items-center gap-2 pb-1 border-b text-xs font-semibold uppercase tracking-wider text-muted-foreground">
419
+ <KeyRound className="w-3.5 h-3.5 text-primary" />
420
+ Login Credentials
421
+ </div>
422
+
423
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
424
+ <div className="space-y-1.5">
425
+ <Label htmlFor="dept-user-username" className="text-xs font-medium">
426
+ Username / Employee ID
427
+ </Label>
428
+ <Input
429
+ id="dept-user-username"
430
+ placeholder="e.g. sahmed or EMP-1042"
431
+ value={username}
432
+ onChange={(e) => setUsername(e.target.value)}
433
+ disabled={isSubmitting}
434
+ />
435
+ <p className="text-[11px] text-muted-foreground">
436
+ Optional. Used for direct identifier login.
437
+ </p>
438
+ </div>
439
+
440
+ <div className="space-y-1.5">
441
+ <div className="flex items-center justify-between">
442
+ <Label htmlFor="dept-user-password" className="text-xs font-medium">
443
+ Password
444
+ </Label>
445
+ <Button
446
+ type="button"
447
+ variant="ghost"
448
+ size="sm"
449
+ onClick={generateRandomPassword}
450
+ disabled={isSubmitting}
451
+ className="h-6 px-1.5 text-xs text-primary hover:text-primary/80 gap-1"
452
+ >
453
+ <Sparkles className="w-3 h-3" />
454
+ Generate
455
+ </Button>
456
+ </div>
457
+ <div className="relative">
458
+ <Input
459
+ id="dept-user-password"
460
+ type={showPassword ? 'text' : 'password'}
461
+ placeholder="Min 6 characters (or blank to invite)"
462
+ value={password}
463
+ onChange={(e) => setPassword(e.target.value)}
464
+ disabled={isSubmitting}
465
+ className="pr-10"
466
+ />
467
+ <Button
468
+ type="button"
469
+ variant="ghost"
470
+ size="sm"
471
+ onClick={() => setShowPassword(!showPassword)}
472
+ tabIndex={-1}
473
+ disabled={isSubmitting}
474
+ className="absolute right-0 top-0 h-full px-3 text-muted-foreground hover:text-foreground"
475
+ >
476
+ {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
477
+ </Button>
478
+ </div>
479
+ <p className="text-[11px] text-muted-foreground">
480
+ Leave blank to send an invite, or enter password to activate now.
481
+ </p>
482
+ </div>
483
+ </div>
484
+ </div>
485
+
486
+ {/* 3. Department Assignment */}
487
+ <div className="space-y-4">
488
+ <div className="flex items-center gap-2 pb-1 border-b text-xs font-semibold uppercase tracking-wider text-muted-foreground">
489
+ <Building2 className="w-3.5 h-3.5 text-primary" />
490
+ Department Assignment
491
+ </div>
492
+
493
+ <div className="space-y-3">
494
+ {/* Organization */}
495
+ <div className="space-y-1.5">
496
+ <Label htmlFor="dept-user-org" className="text-xs font-medium">
497
+ Organization <span className="text-destructive">*</span>
498
+ </Label>
499
+ <Select
500
+ value={organizationId}
501
+ onValueChange={(val) => {
502
+ setOrganizationId(val);
503
+ setHospitalId('');
504
+ setDepartmentId('');
505
+ }}
506
+ disabled={isSubmitting || isLoadingInitial || organizations.length === 0}
507
+ >
508
+ <SelectTrigger id="dept-user-org">
509
+ <SelectValue placeholder={isLoadingInitial ? 'Loading organizations...' : 'Select organization'} />
510
+ </SelectTrigger>
511
+ <SelectContent>
512
+ {organizations.map((org) => (
513
+ <SelectItem key={org.id} value={org.id}>
514
+ {org.name}
515
+ </SelectItem>
516
+ ))}
517
+ </SelectContent>
518
+ </Select>
519
+ </div>
520
+
521
+ {/* Hospital and Department side-by-side */}
522
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
523
+ {/* Hospital */}
524
+ <div className="space-y-1.5">
525
+ <Label htmlFor="dept-user-hospital" className="text-xs font-medium">
526
+ Hospital <span className="text-destructive">*</span>
527
+ </Label>
528
+ <Select
529
+ value={hospitalId}
530
+ onValueChange={(val) => {
531
+ setHospitalId(val);
532
+ setDepartmentId('');
533
+ }}
534
+ disabled={isSubmitting || !organizationId || isLoadingHospitals || hospitalsList.length === 0}
535
+ >
536
+ <SelectTrigger id="dept-user-hospital">
537
+ <SelectValue
538
+ placeholder={
539
+ !organizationId
540
+ ? 'Select organization first'
541
+ : isLoadingHospitals
542
+ ? 'Loading hospitals...'
543
+ : hospitalsList.length === 0
544
+ ? 'No hospitals available'
545
+ : 'Select hospital'
546
+ }
547
+ />
548
+ </SelectTrigger>
549
+ <SelectContent>
550
+ {hospitalsList.map((hosp) => (
551
+ <SelectItem key={hosp.id} value={hosp.id}>
552
+ {hosp.name}
553
+ </SelectItem>
554
+ ))}
555
+ </SelectContent>
556
+ </Select>
557
+ </div>
558
+
559
+ {/* Department */}
560
+ <div className="space-y-1.5">
561
+ <Label htmlFor="dept-user-department" className="text-xs font-medium">
562
+ Department <span className="text-destructive">*</span>
563
+ </Label>
564
+ <Select
565
+ value={departmentId}
566
+ onValueChange={setDepartmentId}
567
+ disabled={isSubmitting || !hospitalId || isLoadingDepartments || departmentsList.length === 0}
568
+ >
569
+ <SelectTrigger id="dept-user-department">
570
+ <SelectValue
571
+ placeholder={
572
+ !hospitalId
573
+ ? 'Select hospital first'
574
+ : isLoadingDepartments
575
+ ? 'Loading departments...'
576
+ : departmentsList.length === 0
577
+ ? 'No departments found'
578
+ : 'Select department'
579
+ }
580
+ />
581
+ </SelectTrigger>
582
+ <SelectContent>
583
+ {departmentsList.map((dept) => (
584
+ <SelectItem key={dept.id} value={dept.id}>
585
+ {dept.name} {dept.code ? `(${dept.code})` : ''}
586
+ </SelectItem>
587
+ ))}
588
+ </SelectContent>
589
+ </Select>
590
+ </div>
591
+ </div>
592
+ </div>
593
+ </div>
594
+
595
+ {/* 4. Access Level (Radio Button Style Selector) */}
596
+ <div className="space-y-3">
597
+ <div className="flex items-center gap-2 pb-1 border-b text-xs font-semibold uppercase tracking-wider text-muted-foreground">
598
+ <ShieldCheck className="w-3.5 h-3.5 text-primary" />
599
+ Access Level &amp; Role <span className="text-destructive">*</span>
600
+ </div>
601
+
602
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3" role="radiogroup" aria-label="Access Level">
603
+ {/* STAFF Role Option */}
604
+ <div
605
+ role="radio"
606
+ aria-checked={selectedRoleCode === 'STAFF'}
607
+ tabIndex={0}
608
+ onClick={() => setSelectedRoleCode('STAFF')}
609
+ onKeyDown={(e) => {
610
+ if (e.key === ' ' || e.key === 'Enter') {
611
+ e.preventDefault();
612
+ setSelectedRoleCode('STAFF');
613
+ }
614
+ }}
615
+ className={cn(
616
+ 'cursor-pointer rounded-lg border p-3.5 transition-all text-left flex flex-col justify-between relative focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
617
+ selectedRoleCode === 'STAFF'
618
+ ? 'border-primary bg-primary/5 ring-1 ring-primary/30 shadow-xs'
619
+ : 'border-border bg-card hover:border-muted-foreground/30 hover:bg-muted/20'
620
+ )}
621
+ >
622
+ <div>
623
+ <div className="flex items-center justify-between mb-2">
624
+ <div className="flex items-center gap-2.5">
625
+ <div
626
+ className={cn(
627
+ 'w-4 h-4 rounded-full border flex items-center justify-center transition-colors',
628
+ selectedRoleCode === 'STAFF'
629
+ ? 'border-primary bg-primary text-primary-foreground'
630
+ : 'border-muted-foreground/50'
631
+ )}
632
+ >
633
+ {selectedRoleCode === 'STAFF' && (
634
+ <div className="w-1.5 h-1.5 rounded-full bg-background" />
635
+ )}
636
+ </div>
637
+ <span className="text-sm font-semibold text-foreground">Clinical Staff</span>
638
+ </div>
639
+ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-medium">
640
+ STAFF
641
+ </span>
642
+ </div>
643
+ <p className="text-xs text-muted-foreground leading-relaxed pl-6.5">
644
+ Can report equipment problems, scan QR codes, and view their own tickets.
645
+ </p>
646
+ </div>
647
+ <div className="mt-2.5 pl-6.5 flex items-center gap-1.5 text-[11px] text-muted-foreground">
648
+ <Check className="w-3 h-3 text-emerald-600 dark:text-emerald-400 shrink-0" />
649
+ <span>Standard reporting &amp; equipment access</span>
650
+ </div>
651
+ </div>
652
+
653
+ {/* DEPT_MGR Role Option */}
654
+ <div
655
+ role="radio"
656
+ aria-checked={selectedRoleCode === 'DEPT_MGR'}
657
+ tabIndex={0}
658
+ onClick={() => setSelectedRoleCode('DEPT_MGR')}
659
+ onKeyDown={(e) => {
660
+ if (e.key === ' ' || e.key === 'Enter') {
661
+ e.preventDefault();
662
+ setSelectedRoleCode('DEPT_MGR');
663
+ }
664
+ }}
665
+ className={cn(
666
+ 'cursor-pointer rounded-lg border p-3.5 transition-all text-left flex flex-col justify-between relative focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
667
+ selectedRoleCode === 'DEPT_MGR'
668
+ ? 'border-primary bg-primary/5 ring-1 ring-primary/30 shadow-xs'
669
+ : 'border-border bg-card hover:border-muted-foreground/30 hover:bg-muted/20'
670
+ )}
671
+ >
672
+ <div>
673
+ <div className="flex items-center justify-between mb-2">
674
+ <div className="flex items-center gap-2.5">
675
+ <div
676
+ className={cn(
677
+ 'w-4 h-4 rounded-full border flex items-center justify-center transition-colors',
678
+ selectedRoleCode === 'DEPT_MGR'
679
+ ? 'border-primary bg-primary text-primary-foreground'
680
+ : 'border-muted-foreground/50'
681
+ )}
682
+ >
683
+ {selectedRoleCode === 'DEPT_MGR' && (
684
+ <div className="w-1.5 h-1.5 rounded-full bg-background" />
685
+ )}
686
+ </div>
687
+ <span className="text-sm font-semibold text-foreground">Department Manager</span>
688
+ </div>
689
+ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-medium">
690
+ DEPT_MGR
691
+ </span>
692
+ </div>
693
+ <p className="text-xs text-muted-foreground leading-relaxed pl-6.5">
694
+ All staff permissions plus department oversight, ticket approval, and asset review.
695
+ </p>
696
+ </div>
697
+ <div className="mt-2.5 pl-6.5 flex items-center gap-1.5 text-[11px] text-muted-foreground">
698
+ <Check className="w-3 h-3 text-emerald-600 dark:text-emerald-400 shrink-0" />
699
+ <span>Managerial approval &amp; department oversight</span>
700
+ </div>
701
+ </div>
702
+ </div>
703
+ </div>
704
+
705
+ {/* 5. Contact */}
706
+ <div className="space-y-4">
707
+ <div className="flex items-center gap-2 pb-1 border-b text-xs font-semibold uppercase tracking-wider text-muted-foreground">
708
+ <Phone className="w-3.5 h-3.5 text-primary" />
709
+ Contact Information
710
+ </div>
711
+
712
+ <div className="space-y-1.5">
713
+ <Label htmlFor="dept-user-phone" className="text-xs font-medium">
714
+ Direct Phone / Mobile
715
+ </Label>
716
+ <Input
717
+ id="dept-user-phone"
718
+ type="tel"
719
+ placeholder="e.g. +249 91 234 5678 or Ext. 402"
720
+ value={phone}
721
+ onChange={(e) => setPhone(e.target.value)}
722
+ disabled={isSubmitting}
723
+ />
724
+ <p className="text-[11px] text-muted-foreground">
725
+ Optional contact number for clinical coordination and work order updates.
726
+ </p>
727
+ </div>
728
+ </div>
729
+ </div>
730
+
731
+ {/* Dialog Footer */}
732
+ <DialogFooter className="p-4 px-6 border-t bg-muted/20 shrink-0 flex flex-row items-center justify-end gap-2">
733
+ <Button
734
+ type="button"
735
+ variant="outline"
736
+ onClick={() => onOpenChange(false)}
737
+ disabled={isSubmitting}
738
+ >
739
+ Cancel
740
+ </Button>
741
+ <Button
742
+ type="submit"
743
+ disabled={isSubmitting || isLoadingInitial}
744
+ >
745
+ {isSubmitting ? (
746
+ <>
747
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
748
+ Creating...
749
+ </>
750
+ ) : (
751
+ 'Create Department User'
752
+ )}
753
+ </Button>
754
+ </DialogFooter>
755
+ </form>
756
+ </DialogContent>
757
+ </Dialog>
758
+ );
759
+ }