@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.
- 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,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 & 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 & 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 & 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
|
+
}
|