@omersx/bemms 0.0.0-stage → 1.0.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.
- package/LICENSE +22 -0
- package/README.md +203 -2
- package/drizzle.config.ts +11 -0
- package/next.config.ts +51 -0
- package/package.json +99 -3
- package/postcss.config.mjs +7 -0
- package/public/hospital_demo_devices.csv +32 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon-maskable-512.png +0 -0
- package/public/manifest.webmanifest +31 -0
- package/public/sw.js +177 -0
- package/src/app/(auth)/layout.tsx +20 -0
- package/src/app/(auth)/login/page.tsx +135 -0
- package/src/app/(dashboard)/admin/audit-logs/page.tsx +178 -0
- package/src/app/(dashboard)/admin/data/page.tsx +37 -0
- package/src/app/(dashboard)/admin/departments/[deptId]/edit/page.tsx +206 -0
- package/src/app/(dashboard)/admin/departments/[deptId]/page.tsx +71 -0
- package/src/app/(dashboard)/admin/departments/new/page.tsx +6 -0
- package/src/app/(dashboard)/admin/departments/page.tsx +6 -0
- package/src/app/(dashboard)/admin/general/page.tsx +374 -0
- package/src/app/(dashboard)/admin/hospitals/[hospitalId]/edit/page.tsx +304 -0
- package/src/app/(dashboard)/admin/hospitals/[hospitalId]/page.tsx +94 -0
- package/src/app/(dashboard)/admin/hospitals/new/page.tsx +302 -0
- package/src/app/(dashboard)/admin/hospitals/page.tsx +6 -0
- package/src/app/(dashboard)/admin/layout.tsx +49 -0
- package/src/app/(dashboard)/admin/organizations/[orgId]/edit/page.tsx +172 -0
- package/src/app/(dashboard)/admin/organizations/[orgId]/page.tsx +82 -0
- package/src/app/(dashboard)/admin/organizations/new/page.tsx +150 -0
- package/src/app/(dashboard)/admin/organizations/page.tsx +6 -0
- package/src/app/(dashboard)/admin/page.tsx +258 -0
- package/src/app/(dashboard)/admin/roles/page.tsx +153 -0
- package/src/app/(dashboard)/admin/settings/page.tsx +69 -0
- package/src/app/(dashboard)/admin/users/[userId]/edit/page.tsx +153 -0
- package/src/app/(dashboard)/admin/users/[userId]/page.tsx +317 -0
- package/src/app/(dashboard)/admin/users/invite/page.tsx +3 -0
- package/src/app/(dashboard)/admin/users/new/page.tsx +5 -0
- package/src/app/(dashboard)/admin/users/page.tsx +23 -0
- package/src/app/(dashboard)/dashboard/page.tsx +462 -0
- package/src/app/(dashboard)/departments/[deptId]/page.tsx +39 -0
- package/src/app/(dashboard)/departments/new/page.tsx +330 -0
- package/src/app/(dashboard)/departments/page.tsx +52 -0
- package/src/app/(dashboard)/devices/[id]/edit/page.tsx +131 -0
- package/src/app/(dashboard)/devices/[id]/page.tsx +408 -0
- package/src/app/(dashboard)/devices/[id]/qr/page.tsx +54 -0
- package/src/app/(dashboard)/devices/new/page.tsx +382 -0
- package/src/app/(dashboard)/devices/page.tsx +80 -0
- package/src/app/(dashboard)/engineering-team/page.tsx +41 -0
- package/src/app/(dashboard)/layout.tsx +43 -0
- package/src/app/(dashboard)/maintenance/checklists/page.tsx +57 -0
- package/src/app/(dashboard)/maintenance/page.tsx +231 -0
- package/src/app/(dashboard)/maintenance/plans/page.tsx +142 -0
- package/src/app/(dashboard)/maintenance/tasks/[id]/page.tsx +997 -0
- package/src/app/(dashboard)/maintenance/tasks/page.tsx +119 -0
- package/src/app/(dashboard)/more/page.tsx +310 -0
- package/src/app/(dashboard)/my-department/page.tsx +151 -0
- package/src/app/(dashboard)/notifications/notification-list.tsx +103 -0
- package/src/app/(dashboard)/notifications/page.tsx +59 -0
- package/src/app/(dashboard)/profile/page.tsx +27 -0
- package/src/app/(dashboard)/reports/costs/page.tsx +127 -0
- package/src/app/(dashboard)/reports/history/page.tsx +105 -0
- package/src/app/(dashboard)/reports/inventory/page.tsx +116 -0
- package/src/app/(dashboard)/reports/maintenance/page.tsx +136 -0
- package/src/app/(dashboard)/reports/page.tsx +120 -0
- package/src/app/(dashboard)/reports/tickets/page.tsx +139 -0
- package/src/app/(dashboard)/reports/workload/page.tsx +135 -0
- package/src/app/(dashboard)/rounds/[id]/page.tsx +197 -0
- package/src/app/(dashboard)/rounds/history/page.tsx +82 -0
- package/src/app/(dashboard)/rounds/page.tsx +107 -0
- package/src/app/(dashboard)/scan/[code]/page.tsx +154 -0
- package/src/app/(dashboard)/scan/page.tsx +73 -0
- package/src/app/(dashboard)/team/page.tsx +42 -0
- package/src/app/(dashboard)/tickets/[id]/page.tsx +465 -0
- package/src/app/(dashboard)/tickets/create/page.tsx +722 -0
- package/src/app/(dashboard)/tickets/page.tsx +55 -0
- package/src/app/(dashboard)/tickets/triage/page.tsx +161 -0
- package/src/app/api/admin/data/export/route.ts +316 -0
- package/src/app/api/auth/[...nextauth]/route.ts +3 -0
- package/src/app/api/health/route.ts +46 -0
- package/src/app/api/notifications/sse/route.ts +91 -0
- package/src/app/api/notifications/subscribe/route.ts +54 -0
- package/src/app/api/notifications/vapid-key/route.ts +7 -0
- package/src/app/api/reports/export/route.ts +102 -0
- package/src/app/api/tickets/[id]/attachments/route.ts +129 -0
- package/src/app/api/tickets/[id]/live-sync/route.ts +52 -0
- package/src/app/error.tsx +54 -0
- package/src/app/globals.css +89 -0
- package/src/app/layout.tsx +59 -0
- package/src/app/not-found.tsx +32 -0
- package/src/app/page.tsx +5 -0
- package/src/components/admin/add-department-user-dialog.tsx +759 -0
- package/src/components/admin/add-engineer-dialog.tsx +773 -0
- package/src/components/admin/add-user-type-dialog.tsx +89 -0
- package/src/components/admin/audit-log-export.tsx +113 -0
- package/src/components/admin/audit-log-filters.tsx +255 -0
- package/src/components/admin/audit-log-table.tsx +383 -0
- package/src/components/admin/data-management/danger-zone.tsx +301 -0
- package/src/components/admin/data-management/data-management-tabs.tsx +61 -0
- package/src/components/admin/data-management/export-center.tsx +301 -0
- package/src/components/admin/data-management/import-wizard.tsx +558 -0
- package/src/components/admin/settings-forms/features-form.tsx +115 -0
- package/src/components/admin/settings-forms/health-panel.tsx +182 -0
- package/src/components/admin/settings-forms/localization-form.tsx +203 -0
- package/src/components/admin/settings-forms/retention-form.tsx +164 -0
- package/src/components/admin/settings-forms/security-form.tsx +186 -0
- package/src/components/admin/system-settings-tabs.tsx +88 -0
- package/src/components/admin/user-role-dialog.tsx +293 -0
- package/src/components/admin/user-status-dialog.tsx +166 -0
- package/src/components/admin/users-page-client.tsx +514 -0
- package/src/components/dashboard/action-buttons.tsx +93 -0
- package/src/components/dashboard/admin-alerts.tsx +86 -0
- package/src/components/dashboard/availability-alerts.tsx +65 -0
- package/src/components/dashboard/maintenance-due-list.tsx +87 -0
- package/src/components/dashboard/round-widget.tsx +110 -0
- package/src/components/dashboard/stat-card.tsx +38 -0
- package/src/components/dashboard/ticket-list-compact.tsx +86 -0
- package/src/components/departments/department-cards-view.tsx +244 -0
- package/src/components/departments/department-detail-view.tsx +467 -0
- package/src/components/departments/department-visuals.tsx +190 -0
- package/src/components/departments/my-department-view.tsx +333 -0
- package/src/components/devices/change-status-dialog.tsx +146 -0
- package/src/components/devices/device-action-buttons.tsx +125 -0
- package/src/components/devices/device-availability-banner.tsx +59 -0
- package/src/components/devices/device-badges.tsx +55 -0
- package/src/components/devices/device-documents-tab.tsx +93 -0
- package/src/components/devices/device-maintenance-tab.tsx +113 -0
- package/src/components/devices/device-mini-qr.tsx +109 -0
- package/src/components/devices/device-qr-card.tsx +187 -0
- package/src/components/devices/device-signatures-tab.tsx +607 -0
- package/src/components/devices/device-tickets-tab.tsx +139 -0
- package/src/components/devices/device-timelines.tsx +124 -0
- package/src/components/devices/qr-camera-scanner.tsx +167 -0
- package/src/components/devices/qr-code-display.tsx +139 -0
- package/src/components/devices/quick-add-category-dialog.tsx +135 -0
- package/src/components/devices/quick-add-manufacturer-dialog.tsx +212 -0
- package/src/components/devices/transfer-device-dialog.tsx +190 -0
- package/src/components/layout/header.tsx +80 -0
- package/src/components/layout/mobile-nav.tsx +91 -0
- package/src/components/layout/more-item-badge.tsx +15 -0
- package/src/components/layout/nav-badge.tsx +72 -0
- package/src/components/layout/sidebar.tsx +297 -0
- package/src/components/maintenance/maintenance-badges.tsx +579 -0
- package/src/components/maintenance/maintenance-nav.tsx +107 -0
- package/src/components/maintenance/parts-costs-manager.tsx +909 -0
- package/src/components/maintenance/signature-card.tsx +413 -0
- package/src/components/maintenance/task-action-bar.tsx +955 -0
- package/src/components/notifications/notification-bell.tsx +367 -0
- package/src/components/notifications/notification-item.tsx +93 -0
- package/src/components/notifications/notification-preferences.tsx +182 -0
- package/src/components/profile/user-profile-view.tsx +626 -0
- package/src/components/providers/nav-counts-provider.tsx +235 -0
- package/src/components/providers/session-provider.tsx +7 -0
- package/src/components/providers/theme-provider.tsx +124 -0
- package/src/components/pwa/install-prompt.tsx +82 -0
- package/src/components/pwa/sw-register.tsx +43 -0
- package/src/components/reports/export-button.tsx +62 -0
- package/src/components/reports/filter-chips.tsx +47 -0
- package/src/components/reports/summary-cards.tsx +40 -0
- package/src/components/reports/transparency-header.tsx +54 -0
- package/src/components/rounds/department-visit-sheet.tsx +199 -0
- package/src/components/rounds/mention-textarea.tsx +187 -0
- package/src/components/rounds/round-calendar.tsx +233 -0
- package/src/components/rounds/round-department-list.tsx +281 -0
- package/src/components/rounds/round-history-client.tsx +218 -0
- package/src/components/rounds/start-round-card.tsx +184 -0
- package/src/components/shared/confirm-dialog.tsx +67 -0
- package/src/components/shared/data-table.tsx +104 -0
- package/src/components/shared/page-header.tsx +20 -0
- package/src/components/shared/sign-out-button.tsx +52 -0
- package/src/components/shared/status-badge.tsx +42 -0
- package/src/components/shared/table-search.tsx +55 -0
- package/src/components/signatures/sign-action-dialog.tsx +253 -0
- package/src/components/signatures/signature-history.tsx +121 -0
- package/src/components/signatures/signature-status-panel.tsx +164 -0
- package/src/components/team/department-coverage.tsx +141 -0
- package/src/components/team/edit-engineer-dialog.tsx +242 -0
- package/src/components/team/engineer-card.tsx +180 -0
- package/src/components/team/engineer-details-dialog.tsx +432 -0
- package/src/components/team/schedule-calendar.tsx +204 -0
- package/src/components/team/staff-engineer-team-view.tsx +368 -0
- package/src/components/team/team-header-stats.tsx +88 -0
- package/src/components/team/team-page-client.tsx +216 -0
- package/src/components/theme/theme-selector-card.tsx +63 -0
- package/src/components/theme/theme-toggle.tsx +65 -0
- package/src/components/tickets/staff-status-label.tsx +58 -0
- package/src/components/tickets/staff-ticket-card.tsx +109 -0
- package/src/components/tickets/staff-ticket-progress-tracker.tsx +405 -0
- package/src/components/tickets/staff-tickets-view.tsx +182 -0
- package/src/components/tickets/ticket-action-buttons.tsx +873 -0
- package/src/components/tickets/ticket-auto-refresh.tsx +197 -0
- package/src/components/tickets/ticket-badges.tsx +43 -0
- package/src/components/tickets/ticket-card.tsx +197 -0
- package/src/components/tickets/ticket-cards-view.tsx +395 -0
- package/src/components/tickets/ticket-comment-section.tsx +684 -0
- package/src/components/tickets/ticket-dispatch-status.tsx +184 -0
- package/src/components/tickets/ticket-list-auto-refresh.tsx +55 -0
- package/src/components/tickets/ticket-reporter-profile.tsx +235 -0
- package/src/components/tickets/ticket-signatures-panel.tsx +32 -0
- package/src/components/tickets/ticket-timeline.tsx +131 -0
- package/src/components/tickets/ticket-triage-panel.tsx +124 -0
- package/src/components/tickets/tickets-nav.tsx +107 -0
- package/src/components/ui/avatar.tsx +43 -0
- package/src/components/ui/badge.tsx +34 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/card.tsx +77 -0
- package/src/components/ui/checkbox.tsx +31 -0
- package/src/components/ui/dialog.tsx +123 -0
- package/src/components/ui/dropdown-menu.tsx +126 -0
- package/src/components/ui/form.tsx +179 -0
- package/src/components/ui/input.tsx +24 -0
- package/src/components/ui/label.tsx +27 -0
- package/src/components/ui/popover.tsx +34 -0
- package/src/components/ui/progress.tsx +36 -0
- package/src/components/ui/scroll-area.tsx +47 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/sheet.tsx +131 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +121 -0
- package/src/components/ui/tabs.tsx +62 -0
- package/src/components/ui/textarea.tsx +23 -0
- package/src/components/ui/tooltip.tsx +27 -0
- package/src/lib/actions/__tests__/devices.test.ts +176 -0
- package/src/lib/actions/__tests__/signatures.test.ts +303 -0
- package/src/lib/actions/__tests__/tickets.test.ts +243 -0
- package/src/lib/actions/audit-logs.ts +298 -0
- package/src/lib/actions/checklists.ts +429 -0
- package/src/lib/actions/dashboard.ts +358 -0
- package/src/lib/actions/data-management.ts +723 -0
- package/src/lib/actions/departments.ts +369 -0
- package/src/lib/actions/device-categories.ts +117 -0
- package/src/lib/actions/devices.ts +748 -0
- package/src/lib/actions/engineer-rounds.ts +973 -0
- package/src/lib/actions/engineers.ts +285 -0
- package/src/lib/actions/hospitals.ts +148 -0
- package/src/lib/actions/locations.ts +115 -0
- package/src/lib/actions/maintenance-plans.ts +316 -0
- package/src/lib/actions/maintenance-tasks.ts +415 -0
- package/src/lib/actions/manufacturers.ts +132 -0
- package/src/lib/actions/nav-counts.ts +144 -0
- package/src/lib/actions/notifications.ts +136 -0
- package/src/lib/actions/organizations.ts +99 -0
- package/src/lib/actions/qr.ts +147 -0
- package/src/lib/actions/report-history.ts +77 -0
- package/src/lib/actions/reports.ts +192 -0
- package/src/lib/actions/roles.ts +32 -0
- package/src/lib/actions/signature-auth.ts +377 -0
- package/src/lib/actions/signatures.ts +532 -0
- package/src/lib/actions/system-settings.ts +274 -0
- package/src/lib/actions/tickets.ts +1445 -0
- package/src/lib/actions/users.ts +740 -0
- package/src/lib/audit/index.ts +103 -0
- package/src/lib/auth/__tests__/rbac.test.ts +33 -0
- package/src/lib/auth/index.ts +338 -0
- package/src/lib/auth/password.ts +10 -0
- package/src/lib/auth/rbac.ts +103 -0
- package/src/lib/auth/types.ts +57 -0
- package/src/lib/constants/departments.ts +16 -0
- package/src/lib/constants/manufacturers.ts +32 -0
- package/src/lib/db/index.ts +23 -0
- package/src/lib/db/schema/audit-logs.ts +42 -0
- package/src/lib/db/schema/departments.ts +39 -0
- package/src/lib/db/schema/device-categories.ts +38 -0
- package/src/lib/db/schema/devices.ts +195 -0
- package/src/lib/db/schema/engineer-profiles.ts +63 -0
- package/src/lib/db/schema/engineer-rounds.ts +115 -0
- package/src/lib/db/schema/enums.ts +148 -0
- package/src/lib/db/schema/hospitals.ts +34 -0
- package/src/lib/db/schema/index.ts +24 -0
- package/src/lib/db/schema/locations.ts +46 -0
- package/src/lib/db/schema/maintenance-plans.ts +99 -0
- package/src/lib/db/schema/maintenance-records.ts +161 -0
- package/src/lib/db/schema/maintenance-tasks.ts +196 -0
- package/src/lib/db/schema/manufacturers.ts +60 -0
- package/src/lib/db/schema/notifications.ts +48 -0
- package/src/lib/db/schema/organizations.ts +24 -0
- package/src/lib/db/schema/report-history.ts +28 -0
- package/src/lib/db/schema/roles.ts +59 -0
- package/src/lib/db/schema/sessions.ts +46 -0
- package/src/lib/db/schema/signature-events.ts +48 -0
- package/src/lib/db/schema/signature-policies.ts +60 -0
- package/src/lib/db/schema/signatures.ts +87 -0
- package/src/lib/db/schema/tickets.ts +169 -0
- package/src/lib/db/schema/user-assignments.ts +85 -0
- package/src/lib/db/schema/users.ts +39 -0
- package/src/lib/db/seed.ts +131 -0
- package/src/lib/logger/__tests__/logger.test.ts +154 -0
- package/src/lib/logger/formatters.ts +159 -0
- package/src/lib/logger/index.ts +184 -0
- package/src/lib/logger/types.ts +48 -0
- package/src/lib/notifications/create.ts +71 -0
- package/src/lib/notifications/dispatcher.ts +490 -0
- package/src/lib/notifications/index.ts +7 -0
- package/src/lib/notifications/push-sender.ts +128 -0
- package/src/lib/notifications/push.ts +64 -0
- package/src/lib/notifications/triggers.ts +166 -0
- package/src/lib/utils/__tests__/crypto.test.ts +30 -0
- package/src/lib/utils/__tests__/export.test.ts +55 -0
- package/src/lib/utils/__tests__/nav-badge.test.ts +147 -0
- package/src/lib/utils/__tests__/staff-status.test.ts +50 -0
- package/src/lib/utils/attestations.ts +19 -0
- package/src/lib/utils/crypto.ts +18 -0
- package/src/lib/utils/csv-parser.ts +181 -0
- package/src/lib/utils/export.ts +45 -0
- package/src/lib/utils/nav-badge.ts +225 -0
- package/src/lib/utils/sound.ts +152 -0
- package/src/lib/utils/staff-status.ts +115 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validators/__tests__/auth.test.ts +127 -0
- package/src/lib/validators/__tests__/data-management.test.ts +182 -0
- package/src/lib/validators/__tests__/devices-profile.test.ts +129 -0
- package/src/lib/validators/__tests__/devices.test.ts +98 -0
- package/src/lib/validators/__tests__/engineer-rounds.test.ts +170 -0
- package/src/lib/validators/__tests__/maintenance.test.ts +178 -0
- package/src/lib/validators/__tests__/signatures.test.ts +154 -0
- package/src/lib/validators/__tests__/system-settings.test.ts +119 -0
- package/src/lib/validators/__tests__/tickets.test.ts +176 -0
- package/src/lib/validators/__tests__/users.test.ts +92 -0
- package/src/lib/validators/auth.ts +32 -0
- package/src/lib/validators/data-management.ts +67 -0
- package/src/lib/validators/departments.ts +14 -0
- package/src/lib/validators/devices.ts +94 -0
- package/src/lib/validators/engineer-rounds.ts +70 -0
- package/src/lib/validators/hospitals.ts +17 -0
- package/src/lib/validators/locations.ts +15 -0
- package/src/lib/validators/maintenance.ts +211 -0
- package/src/lib/validators/organizations.ts +11 -0
- package/src/lib/validators/reports.ts +31 -0
- package/src/lib/validators/signatures.ts +49 -0
- package/src/lib/validators/system-settings.ts +55 -0
- package/src/lib/validators/tickets.ts +125 -0
- package/src/lib/validators/users.ts +43 -0
- package/src/middleware.ts +45 -0
- package/tailwind.config.ts +76 -0
- 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;
|