@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,773 @@
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 {
29
+ Wrench,
30
+ Lock,
31
+ Building2,
32
+ Cpu,
33
+ Sparkles,
34
+ Eye,
35
+ EyeOff,
36
+ Loader2,
37
+ CheckCircle2,
38
+ KeyRound,
39
+ ShieldCheck,
40
+ UserCheck,
41
+ } from 'lucide-react';
42
+
43
+ export interface AddEngineerDialogProps {
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;
63
+ }
64
+
65
+ interface RoleOption {
66
+ id: string;
67
+ code: string;
68
+ name: string;
69
+ }
70
+
71
+ type DutyStatus = 'on_duty' | 'on_call' | 'in_maintenance' | 'off_duty';
72
+
73
+ const COMMON_SPECIALIZATIONS = [
74
+ 'General Biomedical',
75
+ 'ICU & Ventilators',
76
+ 'Radiology & Imaging',
77
+ 'Surgical & OR',
78
+ 'Dialysis & Renal',
79
+ 'Laboratory & Diagnostic',
80
+ ];
81
+
82
+ export function AddEngineerDialog({
83
+ open,
84
+ onOpenChange,
85
+ onSuccess,
86
+ }: AddEngineerDialogProps) {
87
+ // Form fields
88
+ const [fullName, setFullName] = useState('');
89
+ const [email, setEmail] = useState('');
90
+ const [jobTitle, setJobTitle] = useState('Biomedical Engineer');
91
+ const [username, setUsername] = useState('');
92
+ const [password, setPassword] = useState('');
93
+ const [showPassword, setShowPassword] = useState(false);
94
+ const [organizationId, setOrganizationId] = useState('');
95
+ const [hospitalId, setHospitalId] = useState('');
96
+ const [departmentId, setDepartmentId] = useState('');
97
+ const [specialization, setSpecialization] = useState('');
98
+ const [dutyStatus, setDutyStatus] = useState<DutyStatus>('on_duty');
99
+ const [phone, setPhone] = useState('');
100
+ const [extension, setExtension] = useState('');
101
+
102
+ // Dropdown data options
103
+ const [organizations, setOrganizations] = useState<OrganizationOption[]>([]);
104
+ const [hospitals, setHospitals] = useState<HospitalOption[]>([]);
105
+ const [departments, setDepartments] = useState<DepartmentOption[]>([]);
106
+ const [roles, setRoles] = useState<RoleOption[]>([]);
107
+
108
+ // Loading states
109
+ const [loadingInitialData, setLoadingInitialData] = useState(false);
110
+ const [loadingHospitals, setLoadingHospitals] = useState(false);
111
+ const [loadingDepartments, setLoadingDepartments] = useState(false);
112
+ const [isSubmitting, setIsSubmitting] = useState(false);
113
+
114
+ const resetForm = useCallback(() => {
115
+ setFullName('');
116
+ setEmail('');
117
+ setJobTitle('Biomedical Engineer');
118
+ setUsername('');
119
+ setPassword('');
120
+ setShowPassword(false);
121
+ setHospitalId('');
122
+ setDepartmentId('');
123
+ setSpecialization('');
124
+ setDutyStatus('on_duty');
125
+ setPhone('');
126
+ setExtension('');
127
+ if (organizations.length !== 1) {
128
+ setOrganizationId('');
129
+ }
130
+ }, [organizations.length]);
131
+
132
+ // Load organizations and roles on dialog open
133
+ useEffect(() => {
134
+ if (!open) return;
135
+
136
+ let isMounted = true;
137
+ async function fetchInitialData() {
138
+ setLoadingInitialData(true);
139
+ try {
140
+ const [orgRes, rolesRes] = await Promise.all([
141
+ getOrganizations(),
142
+ getRoles(),
143
+ ]);
144
+
145
+ if (!isMounted) return;
146
+
147
+ if (orgRes.success && orgRes.data) {
148
+ setOrganizations(orgRes.data);
149
+ // Auto-select if only one organization exists
150
+ if (orgRes.data.length === 1) {
151
+ setOrganizationId(orgRes.data[0].id);
152
+ }
153
+ } else if (orgRes.error) {
154
+ toast.error(`Failed to load organizations: ${orgRes.error}`);
155
+ }
156
+
157
+ if (rolesRes.success && rolesRes.data) {
158
+ setRoles(rolesRes.data);
159
+ } else if (rolesRes.error) {
160
+ toast.error(`Failed to load roles: ${rolesRes.error}`);
161
+ }
162
+ } catch (err: any) {
163
+ if (isMounted) {
164
+ toast.error(err.message || 'Error loading dialog options');
165
+ }
166
+ } finally {
167
+ if (isMounted) {
168
+ setLoadingInitialData(false);
169
+ }
170
+ }
171
+ }
172
+
173
+ fetchInitialData();
174
+
175
+ return () => {
176
+ isMounted = false;
177
+ };
178
+ }, [open]);
179
+
180
+ // Load hospitals when organization selection changes
181
+ useEffect(() => {
182
+ if (!open || !organizationId) {
183
+ setHospitals([]);
184
+ setHospitalId('');
185
+ setDepartments([]);
186
+ setDepartmentId('');
187
+ return;
188
+ }
189
+
190
+ let isMounted = true;
191
+ async function fetchHospitals() {
192
+ setLoadingHospitals(true);
193
+ try {
194
+ const hospRes = await getHospitals({ organizationId });
195
+ if (!isMounted) return;
196
+
197
+ if (hospRes.success && hospRes.data) {
198
+ setHospitals(hospRes.data);
199
+ } else if (hospRes.error) {
200
+ toast.error(`Failed to load hospitals: ${hospRes.error}`);
201
+ }
202
+ } catch (err: any) {
203
+ if (isMounted) {
204
+ toast.error(err.message || 'Error loading hospitals');
205
+ }
206
+ } finally {
207
+ if (isMounted) {
208
+ setLoadingHospitals(false);
209
+ }
210
+ }
211
+ }
212
+
213
+ fetchHospitals();
214
+
215
+ return () => {
216
+ isMounted = false;
217
+ };
218
+ }, [open, organizationId]);
219
+
220
+ // Load departments when hospital selection changes
221
+ useEffect(() => {
222
+ if (!open || !hospitalId || hospitalId === 'none') {
223
+ setDepartments([]);
224
+ setDepartmentId('');
225
+ return;
226
+ }
227
+
228
+ let isMounted = true;
229
+ async function fetchDepartments() {
230
+ setLoadingDepartments(true);
231
+ try {
232
+ const deptRes = await getDepartments({ hospitalId });
233
+ if (!isMounted) return;
234
+
235
+ if (deptRes.success && deptRes.data) {
236
+ setDepartments(deptRes.data);
237
+ } else if (deptRes.error) {
238
+ toast.error(`Failed to load departments: ${deptRes.error}`);
239
+ }
240
+ } catch (err: any) {
241
+ if (isMounted) {
242
+ toast.error(err.message || 'Error loading departments');
243
+ }
244
+ } finally {
245
+ if (isMounted) {
246
+ setLoadingDepartments(false);
247
+ }
248
+ }
249
+ }
250
+
251
+ fetchDepartments();
252
+
253
+ return () => {
254
+ isMounted = false;
255
+ };
256
+ }, [open, hospitalId]);
257
+
258
+ // Password generator matching existing system pattern
259
+ const generateRandomPassword = () => {
260
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$';
261
+ let pwd = 'Bemms@';
262
+ for (let i = 0; i < 4; i++) {
263
+ pwd += chars.charAt(Math.floor(Math.random() * chars.length));
264
+ }
265
+ setPassword(pwd);
266
+ setShowPassword(true);
267
+ toast.info(`Generated temporary password: ${pwd}`);
268
+ };
269
+
270
+ const handleSubmit = async (e: React.FormEvent) => {
271
+ e.preventDefault();
272
+
273
+ // Validations
274
+ if (!fullName.trim() || fullName.trim().length < 2) {
275
+ toast.error('Full name is required (minimum 2 characters)');
276
+ return;
277
+ }
278
+
279
+ if (!email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
280
+ toast.error('A valid official email is required');
281
+ return;
282
+ }
283
+
284
+ if (!organizationId) {
285
+ toast.error('Please select an organization');
286
+ return;
287
+ }
288
+
289
+ if (password.trim() && password.trim().length < 6) {
290
+ toast.error('Password must be at least 6 characters long');
291
+ return;
292
+ }
293
+
294
+ setIsSubmitting(true);
295
+
296
+ try {
297
+ // Find the BIOMED_ENG role
298
+ let currentRoles = roles;
299
+ if (currentRoles.length === 0) {
300
+ const rolesRes = await getRoles();
301
+ if (rolesRes.success && rolesRes.data) {
302
+ currentRoles = rolesRes.data;
303
+ setRoles(rolesRes.data);
304
+ }
305
+ }
306
+
307
+ const biomedRole = currentRoles.find((r) => r.code === 'BIOMED_ENG');
308
+ if (!biomedRole) {
309
+ toast.error('Biomedical Engineer role (BIOMED_ENG) was not found in the system');
310
+ setIsSubmitting(false);
311
+ return;
312
+ }
313
+
314
+ const trimmedUsername = username.trim() || undefined;
315
+ const trimmedExtension = extension.trim() || undefined;
316
+ const finalIdentifier = trimmedExtension || trimmedUsername || undefined;
317
+
318
+ const res = await addUser({
319
+ fullName: fullName.trim(),
320
+ email: email.trim().toLowerCase(),
321
+ jobTitle: jobTitle.trim() || 'Biomedical Engineer',
322
+ username: trimmedUsername,
323
+ employeeIdentifier: finalIdentifier,
324
+ phone: phone.trim() || undefined,
325
+ password: password.trim() ? password.trim() : null,
326
+ organizationId,
327
+ roleIds: [biomedRole.id],
328
+ hospitalId: hospitalId && hospitalId !== 'none' ? hospitalId : undefined,
329
+ departmentId: departmentId && departmentId !== 'all' ? departmentId : undefined,
330
+ specialization: specialization.trim() || undefined,
331
+ dutyStatus,
332
+ });
333
+
334
+ if (res.success) {
335
+ toast.success(`Biomedical Engineer "${fullName.trim()}" created successfully!`);
336
+ resetForm();
337
+ onOpenChange(false);
338
+ onSuccess();
339
+ } else {
340
+ toast.error((res as any)?.error || 'Failed to create engineer');
341
+ }
342
+ } catch (err: any) {
343
+ toast.error(err?.message || 'An unexpected error occurred while creating engineer');
344
+ } finally {
345
+ setIsSubmitting(false);
346
+ }
347
+ };
348
+
349
+ return (
350
+ <Dialog open={open} onOpenChange={onOpenChange}>
351
+ <DialogContent className="max-w-xl max-h-[85vh] overflow-y-auto sm:max-w-xl">
352
+ <DialogHeader>
353
+ <div className="flex items-center gap-2">
354
+ <div className="p-2 rounded-lg bg-primary/10 text-primary">
355
+ <UserCheck className="w-5 h-5" />
356
+ </div>
357
+ <div>
358
+ <DialogTitle className="text-lg font-semibold">
359
+ Add Biomedical Engineer
360
+ </DialogTitle>
361
+ <DialogDescription className="text-xs text-muted-foreground">
362
+ Set up an engineer profile with facility allocation, shift readiness, and system credentials.
363
+ </DialogDescription>
364
+ </div>
365
+ </div>
366
+ </DialogHeader>
367
+
368
+ <form onSubmit={handleSubmit} className="space-y-6 pt-2">
369
+ {/* SECTION 1: Engineer Identity */}
370
+ <div className="space-y-3">
371
+ <h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground flex items-center gap-2 border-b pb-1.5">
372
+ <Wrench className="w-3.5 h-3.5 text-primary" />
373
+ 1. Engineer Identity
374
+ </h3>
375
+
376
+ <div className="grid gap-3 sm:grid-cols-2">
377
+ <div className="space-y-1.5">
378
+ <Label htmlFor="eng-fullName" className="text-xs font-medium">
379
+ Full Name <span className="text-destructive">*</span>
380
+ </Label>
381
+ <Input
382
+ id="eng-fullName"
383
+ placeholder="e.g. Eng. Tariq Mansour"
384
+ value={fullName}
385
+ onChange={(e) => setFullName(e.target.value)}
386
+ disabled={isSubmitting}
387
+ required
388
+ />
389
+ </div>
390
+
391
+ <div className="space-y-1.5">
392
+ <Label htmlFor="eng-email" className="text-xs font-medium">
393
+ Official Email <span className="text-destructive">*</span>
394
+ </Label>
395
+ <Input
396
+ id="eng-email"
397
+ type="email"
398
+ placeholder="engineer@hospital.org"
399
+ value={email}
400
+ onChange={(e) => setEmail(e.target.value)}
401
+ disabled={isSubmitting}
402
+ required
403
+ />
404
+ </div>
405
+ </div>
406
+
407
+ <div className="space-y-1.5">
408
+ <Label htmlFor="eng-jobTitle" className="text-xs font-medium">
409
+ Job Title
410
+ </Label>
411
+ <Input
412
+ id="eng-jobTitle"
413
+ placeholder="Biomedical Engineer"
414
+ value={jobTitle}
415
+ onChange={(e) => setJobTitle(e.target.value)}
416
+ disabled={isSubmitting}
417
+ />
418
+ </div>
419
+ </div>
420
+
421
+ {/* SECTION 2: Login Credentials */}
422
+ <div className="space-y-3 p-3.5 rounded-lg border border-primary/20 bg-primary/5">
423
+ <div className="flex items-center justify-between border-b border-primary/20 pb-2">
424
+ <h3 className="text-xs font-semibold uppercase tracking-wider text-foreground flex items-center gap-2">
425
+ <Lock className="w-3.5 h-3.5 text-primary" />
426
+ 2. Login Credentials
427
+ </h3>
428
+ <span className="text-[11px] text-muted-foreground font-medium">
429
+ Instant System Access
430
+ </span>
431
+ </div>
432
+
433
+ <div className="grid gap-3 sm:grid-cols-2">
434
+ {/* Username */}
435
+ <div className="space-y-1.5">
436
+ <Label htmlFor="eng-username" className="text-xs font-medium">
437
+ Login Username <span className="text-muted-foreground font-normal">(Optional)</span>
438
+ </Label>
439
+ <Input
440
+ id="eng-username"
441
+ placeholder="e.g. eng.tariq, EMP-4011"
442
+ className="bg-background"
443
+ value={username}
444
+ onChange={(e) => setUsername(e.target.value)}
445
+ disabled={isSubmitting}
446
+ />
447
+ <p className="text-[11px] text-muted-foreground">
448
+ Can be used alongside email to log in.
449
+ </p>
450
+ </div>
451
+
452
+ {/* Password */}
453
+ <div className="space-y-1.5">
454
+ <div className="flex items-center justify-between">
455
+ <Label htmlFor="eng-password" className="text-xs font-medium flex items-center gap-1">
456
+ <KeyRound className="w-3 h-3 text-primary" />
457
+ Initial Password
458
+ </Label>
459
+ <button
460
+ type="button"
461
+ onClick={generateRandomPassword}
462
+ disabled={isSubmitting}
463
+ className="text-[11px] text-primary hover:underline flex items-center gap-1 cursor-pointer font-medium disabled:opacity-50"
464
+ >
465
+ <Sparkles className="w-3 h-3" />
466
+ Generate
467
+ </button>
468
+ </div>
469
+ <div className="relative">
470
+ <Input
471
+ id="eng-password"
472
+ type={showPassword ? 'text' : 'password'}
473
+ placeholder="Min 6 characters"
474
+ className="bg-background pr-10"
475
+ value={password}
476
+ onChange={(e) => setPassword(e.target.value)}
477
+ disabled={isSubmitting}
478
+ />
479
+ <Button
480
+ type="button"
481
+ variant="ghost"
482
+ size="icon"
483
+ className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
484
+ onClick={() => setShowPassword(!showPassword)}
485
+ tabIndex={-1}
486
+ >
487
+ {showPassword ? (
488
+ <EyeOff className="h-4 w-4 text-muted-foreground" />
489
+ ) : (
490
+ <Eye className="h-4 w-4 text-muted-foreground" />
491
+ )}
492
+ <span className="sr-only">Toggle password visibility</span>
493
+ </Button>
494
+ </div>
495
+ <p className="text-[11px] text-muted-foreground">
496
+ Leave empty to send an invite or activate immediately.
497
+ </p>
498
+ </div>
499
+ </div>
500
+ </div>
501
+
502
+ {/* SECTION 3: Hospital Assignment */}
503
+ <div className="space-y-3">
504
+ <h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground flex items-center gap-2 border-b pb-1.5">
505
+ <Building2 className="w-3.5 h-3.5 text-primary" />
506
+ 3. Hospital Assignment & Scope
507
+ </h3>
508
+
509
+ <div className="space-y-3">
510
+ {/* Organization */}
511
+ <div className="space-y-1.5">
512
+ <Label className="text-xs font-medium">
513
+ Organization <span className="text-destructive">*</span>
514
+ </Label>
515
+ <Select
516
+ value={organizationId}
517
+ onValueChange={(val) => {
518
+ setOrganizationId(val);
519
+ setHospitalId('');
520
+ setDepartmentId('');
521
+ }}
522
+ disabled={loadingInitialData || isSubmitting || organizations.length === 0}
523
+ >
524
+ <SelectTrigger id="eng-organization">
525
+ <SelectValue
526
+ placeholder={
527
+ loadingInitialData
528
+ ? 'Loading organizations...'
529
+ : 'Select Organization *'
530
+ }
531
+ />
532
+ </SelectTrigger>
533
+ <SelectContent>
534
+ {organizations.map((org) => (
535
+ <SelectItem key={org.id} value={org.id}>
536
+ {org.name}
537
+ </SelectItem>
538
+ ))}
539
+ </SelectContent>
540
+ </Select>
541
+ </div>
542
+
543
+ <div className="grid gap-3 sm:grid-cols-2">
544
+ {/* Hospital Facility */}
545
+ <div className="space-y-1.5">
546
+ <Label className="text-xs font-medium">Hospital Facility</Label>
547
+ <Select
548
+ value={hospitalId || 'none'}
549
+ onValueChange={(val) => {
550
+ const nextHosp = val === 'none' ? '' : val;
551
+ setHospitalId(nextHosp);
552
+ setDepartmentId('');
553
+ }}
554
+ disabled={
555
+ !organizationId ||
556
+ loadingHospitals ||
557
+ isSubmitting ||
558
+ hospitals.length === 0
559
+ }
560
+ >
561
+ <SelectTrigger id="eng-hospital">
562
+ <SelectValue
563
+ placeholder={
564
+ loadingHospitals
565
+ ? 'Loading hospitals...'
566
+ : !organizationId
567
+ ? 'Select organization first'
568
+ : hospitals.length === 0
569
+ ? 'No hospitals available'
570
+ : 'Select Hospital Facility'
571
+ }
572
+ />
573
+ </SelectTrigger>
574
+ <SelectContent>
575
+ <SelectItem value="none">No Specific Facility (Unassigned)</SelectItem>
576
+ {hospitals.map((hosp) => (
577
+ <SelectItem key={hosp.id} value={hosp.id}>
578
+ {hosp.name}
579
+ </SelectItem>
580
+ ))}
581
+ </SelectContent>
582
+ </Select>
583
+ </div>
584
+
585
+ {/* Covered Departments */}
586
+ <div className="space-y-1.5">
587
+ <Label className="text-xs font-medium">Covered Department Scope</Label>
588
+ <Select
589
+ value={departmentId || 'all'}
590
+ onValueChange={(val) => {
591
+ setDepartmentId(val === 'all' ? '' : val);
592
+ }}
593
+ disabled={
594
+ !hospitalId ||
595
+ hospitalId === 'none' ||
596
+ loadingDepartments ||
597
+ isSubmitting ||
598
+ departments.length === 0
599
+ }
600
+ >
601
+ <SelectTrigger id="eng-department">
602
+ <SelectValue
603
+ placeholder={
604
+ loadingDepartments
605
+ ? 'Loading departments...'
606
+ : !hospitalId || hospitalId === 'none'
607
+ ? 'Select hospital first'
608
+ : departments.length === 0
609
+ ? 'No departments found'
610
+ : 'All Departments (Facility-wide)'
611
+ }
612
+ />
613
+ </SelectTrigger>
614
+ <SelectContent>
615
+ <SelectItem value="all">
616
+ All Departments (Facility-wide Coverage)
617
+ </SelectItem>
618
+ {departments.map((dept) => (
619
+ <SelectItem key={dept.id} value={dept.id}>
620
+ {dept.name} {dept.code ? `(${dept.code})` : ''}
621
+ </SelectItem>
622
+ ))}
623
+ </SelectContent>
624
+ </Select>
625
+ </div>
626
+ </div>
627
+ </div>
628
+ </div>
629
+
630
+ {/* SECTION 4: Engineering Configuration */}
631
+ <div className="space-y-3">
632
+ <h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground flex items-center gap-2 border-b pb-1.5">
633
+ <Cpu className="w-3.5 h-3.5 text-primary" />
634
+ 4. Engineering Configuration
635
+ </h3>
636
+
637
+ <div className="space-y-3">
638
+ {/* Specialization */}
639
+ <div className="space-y-1.5">
640
+ <Label htmlFor="eng-specialization" className="text-xs font-medium">
641
+ Technical Specialization
642
+ </Label>
643
+ <Input
644
+ id="eng-specialization"
645
+ placeholder="e.g. ICU & Ventilators, Radiology & CT, Dialysis, General Biomedical"
646
+ value={specialization}
647
+ onChange={(e) => setSpecialization(e.target.value)}
648
+ disabled={isSubmitting}
649
+ />
650
+ <div className="flex flex-wrap gap-1.5 pt-1">
651
+ {COMMON_SPECIALIZATIONS.map((spec) => (
652
+ <button
653
+ key={spec}
654
+ type="button"
655
+ onClick={() => setSpecialization(spec)}
656
+ disabled={isSubmitting}
657
+ className={`text-[10px] px-2 py-0.5 rounded-full border transition-colors ${
658
+ specialization === spec
659
+ ? 'bg-primary text-primary-foreground border-primary'
660
+ : 'bg-muted/50 hover:bg-muted text-muted-foreground border-border'
661
+ }`}
662
+ >
663
+ {spec}
664
+ </button>
665
+ ))}
666
+ </div>
667
+ </div>
668
+
669
+ {/* Duty Status */}
670
+ <div className="space-y-1.5">
671
+ <Label className="text-xs font-medium">Initial Duty Status</Label>
672
+ <Select
673
+ value={dutyStatus}
674
+ onValueChange={(val: DutyStatus) => setDutyStatus(val)}
675
+ disabled={isSubmitting}
676
+ >
677
+ <SelectTrigger id="eng-dutyStatus">
678
+ <SelectValue placeholder="Select Duty Status" />
679
+ </SelectTrigger>
680
+ <SelectContent>
681
+ <SelectItem value="on_duty">
682
+ <div className="flex items-center gap-2">
683
+ <span className="w-2 h-2 rounded-full bg-emerald-500" />
684
+ <span>🟢 On Duty (Available for Dispatch)</span>
685
+ </div>
686
+ </SelectItem>
687
+ <SelectItem value="on_call">
688
+ <div className="flex items-center gap-2">
689
+ <span className="w-2 h-2 rounded-full bg-amber-500" />
690
+ <span>🟡 On Call (Emergency Standby)</span>
691
+ </div>
692
+ </SelectItem>
693
+ <SelectItem value="in_maintenance">
694
+ <div className="flex items-center gap-2">
695
+ <span className="w-2 h-2 rounded-full bg-blue-500" />
696
+ <span>🔵 In Maintenance (Busy on Task)</span>
697
+ </div>
698
+ </SelectItem>
699
+ <SelectItem value="off_duty">
700
+ <div className="flex items-center gap-2">
701
+ <span className="w-2 h-2 rounded-full bg-slate-400" />
702
+ <span>⚪ Off Duty</span>
703
+ </div>
704
+ </SelectItem>
705
+ </SelectContent>
706
+ </Select>
707
+ </div>
708
+
709
+ {/* Phone & Extension */}
710
+ <div className="grid gap-3 sm:grid-cols-2">
711
+ <div className="space-y-1.5">
712
+ <Label htmlFor="eng-phone" className="text-xs font-medium">
713
+ Phone / Mobile
714
+ </Label>
715
+ <Input
716
+ id="eng-phone"
717
+ placeholder="+966 50 123 4567"
718
+ value={phone}
719
+ onChange={(e) => setPhone(e.target.value)}
720
+ disabled={isSubmitting}
721
+ />
722
+ </div>
723
+
724
+ <div className="space-y-1.5">
725
+ <Label htmlFor="eng-extension" className="text-xs font-medium">
726
+ Extension / Bleep
727
+ </Label>
728
+ <Input
729
+ id="eng-extension"
730
+ placeholder="Ext. 204 or Bleep 18"
731
+ value={extension}
732
+ onChange={(e) => setExtension(e.target.value)}
733
+ disabled={isSubmitting}
734
+ />
735
+ </div>
736
+ </div>
737
+ </div>
738
+ </div>
739
+
740
+ <DialogFooter className="gap-2 sm:gap-0 pt-2 border-t">
741
+ <Button
742
+ type="button"
743
+ variant="outline"
744
+ disabled={isSubmitting}
745
+ onClick={() => onOpenChange(false)}
746
+ >
747
+ Cancel
748
+ </Button>
749
+ <Button
750
+ type="submit"
751
+ disabled={isSubmitting}
752
+ className="gap-2 min-w-[150px]"
753
+ >
754
+ {isSubmitting ? (
755
+ <>
756
+ <Loader2 className="w-4 h-4 animate-spin" />
757
+ Creating Engineer...
758
+ </>
759
+ ) : (
760
+ <>
761
+ <CheckCircle2 className="w-4 h-4" />
762
+ Create Engineer
763
+ </>
764
+ )}
765
+ </Button>
766
+ </DialogFooter>
767
+ </form>
768
+ </DialogContent>
769
+ </Dialog>
770
+ );
771
+ }
772
+
773
+ export default AddEngineerDialog;