@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,558 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useCallback } from 'react';
|
|
4
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Badge } from '@/components/ui/badge';
|
|
7
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
8
|
+
import {
|
|
9
|
+
Download,
|
|
10
|
+
UploadCloud,
|
|
11
|
+
FileText,
|
|
12
|
+
FileSpreadsheet,
|
|
13
|
+
CheckCircle2,
|
|
14
|
+
AlertTriangle,
|
|
15
|
+
XCircle,
|
|
16
|
+
Loader2,
|
|
17
|
+
ArrowRight,
|
|
18
|
+
ArrowLeft,
|
|
19
|
+
RotateCcw,
|
|
20
|
+
Layers,
|
|
21
|
+
Building2,
|
|
22
|
+
Info
|
|
23
|
+
} from 'lucide-react';
|
|
24
|
+
import { toast } from 'sonner';
|
|
25
|
+
|
|
26
|
+
// Next lines are ignored if they don't exist yet, per instructions they will be added later
|
|
27
|
+
import {
|
|
28
|
+
validateDeviceImport,
|
|
29
|
+
executeDeviceImport,
|
|
30
|
+
validateDepartmentImport,
|
|
31
|
+
executeDepartmentImport
|
|
32
|
+
} from '@/lib/actions/data-management';
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
getDeviceImportTemplateCSV,
|
|
36
|
+
getDepartmentImportTemplateCSV
|
|
37
|
+
} from '@/lib/utils/csv-parser';
|
|
38
|
+
|
|
39
|
+
import type { DeviceImportValidationResult } from '@/lib/validators/data-management';
|
|
40
|
+
|
|
41
|
+
type EntityType = 'devices' | 'departments';
|
|
42
|
+
|
|
43
|
+
interface ValidationData {
|
|
44
|
+
totalRows: number;
|
|
45
|
+
validCount: number;
|
|
46
|
+
warningCount: number;
|
|
47
|
+
errorCount: number;
|
|
48
|
+
rows: any[]; // any to accommodate both devices and departments
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ImportWizard() {
|
|
52
|
+
const [step, setStep] = useState<1 | 2 | 3>(1);
|
|
53
|
+
const [entity, setEntity] = useState<EntityType>('devices');
|
|
54
|
+
const [file, setFile] = useState<File | null>(null);
|
|
55
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
56
|
+
const [validating, setValidating] = useState(false);
|
|
57
|
+
const [importing, setImporting] = useState(false);
|
|
58
|
+
const [validationData, setValidationData] = useState<ValidationData | null>(null);
|
|
59
|
+
const [importedCount, setImportedCount] = useState<number>(0);
|
|
60
|
+
|
|
61
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
62
|
+
|
|
63
|
+
const handleDownloadTemplate = async (format: 'csv' | 'excel') => {
|
|
64
|
+
try {
|
|
65
|
+
const csvContent = entity === 'devices'
|
|
66
|
+
? getDeviceImportTemplateCSV()
|
|
67
|
+
: getDepartmentImportTemplateCSV();
|
|
68
|
+
|
|
69
|
+
if (format === 'excel') {
|
|
70
|
+
const XLSX = await import('xlsx');
|
|
71
|
+
const wb = XLSX.read(csvContent, { type: 'string' });
|
|
72
|
+
const excelBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
|
|
73
|
+
const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
74
|
+
const url = URL.createObjectURL(blob);
|
|
75
|
+
const link = document.createElement('a');
|
|
76
|
+
link.href = url;
|
|
77
|
+
link.setAttribute('download', `bemms_${entity}_template.xlsx`);
|
|
78
|
+
document.body.appendChild(link);
|
|
79
|
+
link.click();
|
|
80
|
+
document.body.removeChild(link);
|
|
81
|
+
URL.revokeObjectURL(url);
|
|
82
|
+
toast.success(`Downloaded ${entity} Excel template (.xlsx)`);
|
|
83
|
+
} else {
|
|
84
|
+
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
85
|
+
const url = URL.createObjectURL(blob);
|
|
86
|
+
const link = document.createElement('a');
|
|
87
|
+
link.href = url;
|
|
88
|
+
link.setAttribute('download', `bemms_${entity}_template.csv`);
|
|
89
|
+
document.body.appendChild(link);
|
|
90
|
+
link.click();
|
|
91
|
+
document.body.removeChild(link);
|
|
92
|
+
URL.revokeObjectURL(url);
|
|
93
|
+
toast.success(`Downloaded ${entity} CSV template (.csv)`);
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
toast.error('Failed to download template');
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const parseFileToCsv = async (fileToParse: File): Promise<string> => {
|
|
101
|
+
if (fileToParse.name.endsWith('.csv')) {
|
|
102
|
+
return await fileToParse.text();
|
|
103
|
+
} else if (fileToParse.name.endsWith('.xlsx') || fileToParse.name.endsWith('.xls')) {
|
|
104
|
+
const XLSX = await import('xlsx');
|
|
105
|
+
const data = await fileToParse.arrayBuffer();
|
|
106
|
+
const workbook = XLSX.read(data, { type: 'array' });
|
|
107
|
+
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
|
108
|
+
return XLSX.utils.sheet_to_csv(sheet);
|
|
109
|
+
}
|
|
110
|
+
throw new Error('Unsupported file format');
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleFileSet = (selectedFile: File) => {
|
|
114
|
+
if (selectedFile.name.endsWith('.csv') || selectedFile.name.endsWith('.xlsx') || selectedFile.name.endsWith('.xls')) {
|
|
115
|
+
setFile(selectedFile);
|
|
116
|
+
} else {
|
|
117
|
+
toast.error('Please upload a .csv, .xlsx, or .xls file');
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
setIsDragging(true);
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
setIsDragging(true);
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
const handleDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
setIsDragging(false);
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
136
|
+
const handleDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
setIsDragging(false);
|
|
139
|
+
|
|
140
|
+
const droppedFile = e.dataTransfer.files[0];
|
|
141
|
+
if (droppedFile) {
|
|
142
|
+
handleFileSet(droppedFile);
|
|
143
|
+
}
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
147
|
+
const selectedFile = e.target.files?.[0];
|
|
148
|
+
if (selectedFile) {
|
|
149
|
+
handleFileSet(selectedFile);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const handleValidate = async () => {
|
|
154
|
+
if (!file) return;
|
|
155
|
+
|
|
156
|
+
setValidating(true);
|
|
157
|
+
setValidationData(null);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const csvText = await parseFileToCsv(file);
|
|
161
|
+
|
|
162
|
+
let res;
|
|
163
|
+
if (entity === 'devices') {
|
|
164
|
+
res = await validateDeviceImport(csvText);
|
|
165
|
+
} else {
|
|
166
|
+
res = await validateDepartmentImport(csvText);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (res.success) {
|
|
170
|
+
setValidationData({
|
|
171
|
+
totalRows: res.totalRows,
|
|
172
|
+
validCount: res.validCount,
|
|
173
|
+
warningCount: res.warningCount,
|
|
174
|
+
errorCount: res.errorCount,
|
|
175
|
+
rows: res.rows,
|
|
176
|
+
});
|
|
177
|
+
toast.info(`Validated ${res.totalRows} rows`);
|
|
178
|
+
setStep(2);
|
|
179
|
+
} else {
|
|
180
|
+
toast.error(res.error || 'Failed to validate file');
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
toast.error('Could not read or parse the selected file');
|
|
184
|
+
} finally {
|
|
185
|
+
setValidating(false);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const handleExecuteImport = async () => {
|
|
190
|
+
if (!validationData) return;
|
|
191
|
+
setImporting(true);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
let res;
|
|
195
|
+
if (entity === 'devices') {
|
|
196
|
+
res = await executeDeviceImport(validationData.rows);
|
|
197
|
+
} else {
|
|
198
|
+
res = await executeDepartmentImport(validationData.rows);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (res.success) {
|
|
202
|
+
toast.success(`Successfully imported ${res.importedCount} records!`);
|
|
203
|
+
setImportedCount(res.importedCount);
|
|
204
|
+
setStep(3);
|
|
205
|
+
} else {
|
|
206
|
+
toast.error(res.error || 'Failed to complete import');
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
toast.error('An unexpected error occurred during import execution');
|
|
210
|
+
} finally {
|
|
211
|
+
setImporting(false);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const handleReset = () => {
|
|
216
|
+
setFile(null);
|
|
217
|
+
setValidationData(null);
|
|
218
|
+
setStep(1);
|
|
219
|
+
if (fileInputRef.current) fileInputRef.current.value = '';
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const importableCount = validationData ? validationData.validCount + validationData.warningCount : 0;
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<div className="space-y-6">
|
|
226
|
+
{/* 1. Entity Type Selector */}
|
|
227
|
+
{step === 1 && (
|
|
228
|
+
<>
|
|
229
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
230
|
+
<Card
|
|
231
|
+
className={`cursor-pointer transition-all hover:border-primary/50 ${entity === 'devices' ? 'border-primary ring-1 ring-primary' : ''}`}
|
|
232
|
+
onClick={() => setEntity('devices')}
|
|
233
|
+
>
|
|
234
|
+
<CardContent className="flex items-center gap-4 p-4">
|
|
235
|
+
<div className={`p-2 rounded-full ${entity === 'devices' ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}>
|
|
236
|
+
<Layers className="h-6 w-6" />
|
|
237
|
+
</div>
|
|
238
|
+
<div>
|
|
239
|
+
<h3 className="font-medium text-base">Medical Devices</h3>
|
|
240
|
+
<p className="text-sm text-muted-foreground">Import equipment and assets</p>
|
|
241
|
+
</div>
|
|
242
|
+
</CardContent>
|
|
243
|
+
</Card>
|
|
244
|
+
|
|
245
|
+
<Card
|
|
246
|
+
className={`cursor-pointer transition-all hover:border-primary/50 ${entity === 'departments' ? 'border-primary ring-1 ring-primary' : ''}`}
|
|
247
|
+
onClick={() => setEntity('departments')}
|
|
248
|
+
>
|
|
249
|
+
<CardContent className="flex items-center gap-4 p-4">
|
|
250
|
+
<div className={`p-2 rounded-full ${entity === 'departments' ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}>
|
|
251
|
+
<Building2 className="h-6 w-6" />
|
|
252
|
+
</div>
|
|
253
|
+
<div>
|
|
254
|
+
<h3 className="font-medium text-base">Departments</h3>
|
|
255
|
+
<p className="text-sm text-muted-foreground">Import hospital departments</p>
|
|
256
|
+
</div>
|
|
257
|
+
</CardContent>
|
|
258
|
+
</Card>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
{/* Relationship Workflow Guidance */}
|
|
262
|
+
{entity === 'devices' ? (
|
|
263
|
+
<div className="flex items-start gap-3 rounded-lg border border-blue-500/20 bg-blue-500/10 p-3 text-sm text-blue-900 dark:text-blue-200">
|
|
264
|
+
<Info className="h-5 w-5 shrink-0 text-blue-600 dark:text-blue-400 mt-0.5" />
|
|
265
|
+
<div className="space-y-1">
|
|
266
|
+
<span className="font-semibold text-xs uppercase tracking-wider text-blue-700 dark:text-blue-300">Recommended Sequence</span>
|
|
267
|
+
<p className="text-xs text-blue-800/90 dark:text-blue-200">
|
|
268
|
+
Medical devices are associated with hospital departments. If your facility has new or unlisted wards,{' '}
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
onClick={() => setEntity('departments')}
|
|
272
|
+
className="font-semibold underline underline-offset-2 hover:text-blue-600 dark:hover:text-blue-100"
|
|
273
|
+
>
|
|
274
|
+
import departments first
|
|
275
|
+
</button>
|
|
276
|
+
, then import devices referencing those department names.
|
|
277
|
+
</p>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
) : (
|
|
281
|
+
<div className="flex items-start gap-3 rounded-lg border border-emerald-500/20 bg-emerald-500/10 p-3 text-sm text-emerald-900 dark:text-emerald-200">
|
|
282
|
+
<Info className="h-5 w-5 shrink-0 text-emerald-600 dark:text-emerald-400 mt-0.5" />
|
|
283
|
+
<div className="space-y-1">
|
|
284
|
+
<span className="font-semibold text-xs uppercase tracking-wider text-emerald-700 dark:text-emerald-300">Organizational Hierarchy</span>
|
|
285
|
+
<p className="text-xs text-emerald-800/90 dark:text-emerald-200">
|
|
286
|
+
Importing departments registers hospital clinical areas (e.g. ICU, Radiology, Emergency). Once imported, you can immediately switch to{' '}
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
onClick={() => setEntity('devices')}
|
|
290
|
+
className="font-semibold underline underline-offset-2 hover:text-emerald-600 dark:hover:text-emerald-100"
|
|
291
|
+
>
|
|
292
|
+
importing medical devices
|
|
293
|
+
</button>{' '}
|
|
294
|
+
to assign assets to them.
|
|
295
|
+
</p>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
</>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
<Card>
|
|
303
|
+
<CardHeader>
|
|
304
|
+
<div className="flex flex-col space-y-4">
|
|
305
|
+
<div>
|
|
306
|
+
<CardTitle className="text-xl flex items-center gap-2">
|
|
307
|
+
<UploadCloud className="h-5 w-5 text-primary" />
|
|
308
|
+
Data Import Wizard
|
|
309
|
+
</CardTitle>
|
|
310
|
+
<CardDescription>
|
|
311
|
+
Follow the steps to import data into the system
|
|
312
|
+
</CardDescription>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
{/* 2. Three-Step Visual Stepper */}
|
|
316
|
+
<div className="relative py-4">
|
|
317
|
+
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-muted -translate-y-1/2"></div>
|
|
318
|
+
<div className="relative flex justify-between">
|
|
319
|
+
<div className="flex flex-col items-center gap-2 bg-card px-2">
|
|
320
|
+
<div className={`flex h-8 w-8 items-center justify-center rounded-full border-2 ${step >= 1 ? 'border-primary bg-primary text-primary-foreground' : 'border-muted bg-muted text-muted-foreground'}`}>
|
|
321
|
+
{step > 1 ? <CheckCircle2 className="h-5 w-5" /> : '1'}
|
|
322
|
+
</div>
|
|
323
|
+
<span className="text-xs font-medium">Upload</span>
|
|
324
|
+
</div>
|
|
325
|
+
<div className="flex flex-col items-center gap-2 bg-card px-2">
|
|
326
|
+
<div className={`flex h-8 w-8 items-center justify-center rounded-full border-2 ${step >= 2 ? 'border-primary bg-primary text-primary-foreground' : 'border-muted bg-muted text-muted-foreground'}`}>
|
|
327
|
+
{step > 2 ? <CheckCircle2 className="h-5 w-5" /> : '2'}
|
|
328
|
+
</div>
|
|
329
|
+
<span className="text-xs font-medium">Preview</span>
|
|
330
|
+
</div>
|
|
331
|
+
<div className="flex flex-col items-center gap-2 bg-card px-2">
|
|
332
|
+
<div className={`flex h-8 w-8 items-center justify-center rounded-full border-2 ${step >= 3 ? 'border-primary bg-primary text-primary-foreground' : 'border-muted bg-muted text-muted-foreground'}`}>
|
|
333
|
+
{step > 3 ? <CheckCircle2 className="h-5 w-5" /> : '3'}
|
|
334
|
+
</div>
|
|
335
|
+
<span className="text-xs font-medium">Results</span>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</CardHeader>
|
|
341
|
+
|
|
342
|
+
<CardContent className="space-y-6 pt-4">
|
|
343
|
+
{/* Step 1: Upload Zone */}
|
|
344
|
+
{step === 1 && (
|
|
345
|
+
<div className="space-y-6">
|
|
346
|
+
<div className="flex flex-col sm:flex-row gap-3">
|
|
347
|
+
<Button variant="outline" onClick={() => handleDownloadTemplate('csv')} className="flex-1 gap-2">
|
|
348
|
+
<FileText className="h-4 w-4" />
|
|
349
|
+
Download CSV Template
|
|
350
|
+
</Button>
|
|
351
|
+
<Button variant="outline" onClick={() => handleDownloadTemplate('excel')} className="flex-1 gap-2">
|
|
352
|
+
<FileSpreadsheet className="h-4 w-4" />
|
|
353
|
+
Download Excel Template
|
|
354
|
+
</Button>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<div
|
|
358
|
+
onDragOver={handleDragOver}
|
|
359
|
+
onDragEnter={handleDragEnter}
|
|
360
|
+
onDragLeave={handleDragLeave}
|
|
361
|
+
onDrop={handleDrop}
|
|
362
|
+
className={`border-2 border-dashed rounded-xl min-h-[200px] p-8 text-center transition-all flex flex-col items-center justify-center space-y-4 ${
|
|
363
|
+
isDragging
|
|
364
|
+
? 'border-primary bg-primary/5'
|
|
365
|
+
: 'border-muted-foreground/25 hover:border-primary/50'
|
|
366
|
+
}`}
|
|
367
|
+
>
|
|
368
|
+
{!file ? (
|
|
369
|
+
<>
|
|
370
|
+
<UploadCloud className="h-12 w-12 text-muted-foreground" />
|
|
371
|
+
<div className="space-y-1">
|
|
372
|
+
<p className="font-bold text-lg">Drop your file here</p>
|
|
373
|
+
<p className="text-sm text-muted-foreground">
|
|
374
|
+
Supports CSV and Excel (.xlsx, .xls) files
|
|
375
|
+
</p>
|
|
376
|
+
</div>
|
|
377
|
+
<input
|
|
378
|
+
ref={fileInputRef}
|
|
379
|
+
type="file"
|
|
380
|
+
accept=".csv, .xlsx, .xls"
|
|
381
|
+
className="hidden"
|
|
382
|
+
onChange={handleFileChange}
|
|
383
|
+
/>
|
|
384
|
+
<Button onClick={() => fileInputRef.current?.click()}>
|
|
385
|
+
Browse Files
|
|
386
|
+
</Button>
|
|
387
|
+
</>
|
|
388
|
+
) : (
|
|
389
|
+
<div className="flex flex-col items-center space-y-4">
|
|
390
|
+
<div className="flex items-center gap-3 bg-muted p-4 rounded-lg">
|
|
391
|
+
{file.name.endsWith('.csv') ? (
|
|
392
|
+
<FileText className="h-8 w-8 text-primary" />
|
|
393
|
+
) : (
|
|
394
|
+
<FileSpreadsheet className="h-8 w-8 text-emerald-600" />
|
|
395
|
+
)}
|
|
396
|
+
<div className="text-left text-sm">
|
|
397
|
+
<p className="font-medium truncate max-w-[200px]">{file.name}</p>
|
|
398
|
+
<p className="text-muted-foreground">
|
|
399
|
+
{(file.size / 1024).toFixed(1)} KB
|
|
400
|
+
</p>
|
|
401
|
+
</div>
|
|
402
|
+
<Button variant="ghost" size="icon" onClick={() => setFile(null)} className="ml-2">
|
|
403
|
+
<XCircle className="h-5 w-5 text-destructive" />
|
|
404
|
+
</Button>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
)}
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
{file && (
|
|
411
|
+
<div className="flex justify-end">
|
|
412
|
+
<Button onClick={handleValidate} disabled={validating} className="w-full sm:w-auto">
|
|
413
|
+
{validating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
414
|
+
Validate & Preview
|
|
415
|
+
</Button>
|
|
416
|
+
</div>
|
|
417
|
+
)}
|
|
418
|
+
</div>
|
|
419
|
+
)}
|
|
420
|
+
|
|
421
|
+
{/* Step 2: Preview (Validation Results) */}
|
|
422
|
+
{step === 2 && validationData && (
|
|
423
|
+
<div className="space-y-4">
|
|
424
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 p-4 bg-muted/40 rounded-lg border">
|
|
425
|
+
<div>
|
|
426
|
+
<span className="text-xs text-muted-foreground">Total Rows</span>
|
|
427
|
+
<div className="text-xl font-bold">{validationData.totalRows}</div>
|
|
428
|
+
</div>
|
|
429
|
+
<div>
|
|
430
|
+
<span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium">Ready to Import</span>
|
|
431
|
+
<div className="text-xl font-bold text-emerald-600 dark:text-emerald-400">
|
|
432
|
+
{importableCount}
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
<div>
|
|
436
|
+
<span className="text-xs text-amber-600 dark:text-amber-400 font-medium">Auto-Create Notices</span>
|
|
437
|
+
<div className="text-xl font-bold text-amber-600 dark:text-amber-400">
|
|
438
|
+
{validationData.warningCount}
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
<div>
|
|
442
|
+
<span className="text-xs text-destructive font-medium">Blocking Errors</span>
|
|
443
|
+
<div className="text-xl font-bold text-destructive">{validationData.errorCount}</div>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<div className="border rounded-lg overflow-x-auto max-h-[380px]">
|
|
448
|
+
<Table>
|
|
449
|
+
<TableHeader>
|
|
450
|
+
<TableRow>
|
|
451
|
+
<TableHead className="w-16">Row</TableHead>
|
|
452
|
+
<TableHead>Status</TableHead>
|
|
453
|
+
{entity === 'devices' ? (
|
|
454
|
+
<>
|
|
455
|
+
<TableHead>Asset Tag</TableHead>
|
|
456
|
+
<TableHead>Device Name</TableHead>
|
|
457
|
+
<TableHead>Category</TableHead>
|
|
458
|
+
<TableHead>Department</TableHead>
|
|
459
|
+
</>
|
|
460
|
+
) : (
|
|
461
|
+
<>
|
|
462
|
+
<TableHead>Department Name</TableHead>
|
|
463
|
+
<TableHead>Code</TableHead>
|
|
464
|
+
<TableHead>Type</TableHead>
|
|
465
|
+
<TableHead>Hospital</TableHead>
|
|
466
|
+
</>
|
|
467
|
+
)}
|
|
468
|
+
<TableHead>Notes</TableHead>
|
|
469
|
+
</TableRow>
|
|
470
|
+
</TableHeader>
|
|
471
|
+
<TableBody>
|
|
472
|
+
{validationData.rows.map((row: any) => (
|
|
473
|
+
<TableRow key={row.rowNumber} className={row.status === 'error' ? 'bg-destructive/5' : ''}>
|
|
474
|
+
<TableCell className="font-mono text-xs text-muted-foreground">{row.rowNumber}</TableCell>
|
|
475
|
+
<TableCell>
|
|
476
|
+
{row.status === 'valid' && (
|
|
477
|
+
<Badge variant="outline" className="text-emerald-600 border-emerald-500/30 bg-emerald-500/10 gap-1">
|
|
478
|
+
<CheckCircle2 className="h-3 w-3" />
|
|
479
|
+
Valid
|
|
480
|
+
</Badge>
|
|
481
|
+
)}
|
|
482
|
+
{row.status === 'warning' && (
|
|
483
|
+
<Badge variant="outline" className="text-amber-600 border-amber-500/30 bg-amber-500/10 gap-1">
|
|
484
|
+
<AlertTriangle className="h-3 w-3" />
|
|
485
|
+
Notice
|
|
486
|
+
</Badge>
|
|
487
|
+
)}
|
|
488
|
+
{row.status === 'error' && (
|
|
489
|
+
<Badge variant="destructive" className="gap-1">
|
|
490
|
+
<XCircle className="h-3 w-3" />
|
|
491
|
+
Invalid
|
|
492
|
+
</Badge>
|
|
493
|
+
)}
|
|
494
|
+
</TableCell>
|
|
495
|
+
{entity === 'devices' ? (
|
|
496
|
+
<>
|
|
497
|
+
<TableCell className="font-mono text-xs font-semibold">{row.data.assetNumber || '—'}</TableCell>
|
|
498
|
+
<TableCell className="font-medium text-xs">{row.data.name || '—'}</TableCell>
|
|
499
|
+
<TableCell className="text-xs">{row.data.categoryName || '—'}</TableCell>
|
|
500
|
+
<TableCell className="text-xs">{row.data.departmentName || '—'}</TableCell>
|
|
501
|
+
</>
|
|
502
|
+
) : (
|
|
503
|
+
<>
|
|
504
|
+
<TableCell className="font-medium text-xs">{row.data.name || '—'}</TableCell>
|
|
505
|
+
<TableCell className="font-mono text-xs">{row.data.code || '—'}</TableCell>
|
|
506
|
+
<TableCell className="text-xs">{row.data.departmentType || '—'}</TableCell>
|
|
507
|
+
<TableCell className="text-xs">{row.data.hospitalName || '—'}</TableCell>
|
|
508
|
+
</>
|
|
509
|
+
)}
|
|
510
|
+
<TableCell className="text-xs text-muted-foreground max-w-xs">
|
|
511
|
+
{row.messages?.length > 0 ? row.messages.join(' • ') : 'Ready to import'}
|
|
512
|
+
</TableCell>
|
|
513
|
+
</TableRow>
|
|
514
|
+
))}
|
|
515
|
+
</TableBody>
|
|
516
|
+
</Table>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
)}
|
|
520
|
+
|
|
521
|
+
{/* Step 3: Results */}
|
|
522
|
+
{step === 3 && (
|
|
523
|
+
<div className="flex flex-col items-center justify-center py-8 space-y-4 text-center">
|
|
524
|
+
<CheckCircle2 className="h-16 w-16 text-emerald-500" />
|
|
525
|
+
<h2 className="text-2xl font-bold">Import Completed Successfully</h2>
|
|
526
|
+
<p className="text-muted-foreground">
|
|
527
|
+
{importedCount} records imported into the system
|
|
528
|
+
</p>
|
|
529
|
+
<Button onClick={handleReset} variant="outline" className="mt-4 gap-2">
|
|
530
|
+
<RotateCcw className="h-4 w-4" />
|
|
531
|
+
Import Another File
|
|
532
|
+
</Button>
|
|
533
|
+
</div>
|
|
534
|
+
)}
|
|
535
|
+
</CardContent>
|
|
536
|
+
|
|
537
|
+
{step === 2 && (
|
|
538
|
+
<CardFooter className="flex flex-col sm:flex-row items-center justify-between border-t pt-4 gap-3">
|
|
539
|
+
<Button variant="ghost" size="sm" onClick={() => setStep(1)} className="gap-2">
|
|
540
|
+
<ArrowLeft className="h-4 w-4" />
|
|
541
|
+
Back to Upload
|
|
542
|
+
</Button>
|
|
543
|
+
<Button
|
|
544
|
+
onClick={handleExecuteImport}
|
|
545
|
+
disabled={importing || importableCount === 0}
|
|
546
|
+
className="gap-2 w-full sm:w-auto"
|
|
547
|
+
>
|
|
548
|
+
{importing && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
549
|
+
{!importing && <ArrowRight className="h-4 w-4" />}
|
|
550
|
+
Confirm Import ({importableCount} Records)
|
|
551
|
+
</Button>
|
|
552
|
+
</CardFooter>
|
|
553
|
+
)}
|
|
554
|
+
</Card>
|
|
555
|
+
</div>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { Label } from '@/components/ui/label';
|
|
6
|
+
import { Input } from '@/components/ui/input';
|
|
7
|
+
import { Button } from '@/components/ui/button';
|
|
8
|
+
import { Switch } from '@/components/ui/switch';
|
|
9
|
+
import { Sliders, Route, Loader2, Save } from 'lucide-react';
|
|
10
|
+
import { toast } from 'sonner';
|
|
11
|
+
import { updateOrganizationSettings } from '@/lib/actions/system-settings';
|
|
12
|
+
import type { FeaturesSettings } from '@/lib/validators/system-settings';
|
|
13
|
+
|
|
14
|
+
interface FeaturesFormProps {
|
|
15
|
+
initialData?: FeaturesSettings;
|
|
16
|
+
organizationId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function FeaturesForm({ initialData, organizationId }: FeaturesFormProps) {
|
|
20
|
+
const [formData, setFormData] = useState<FeaturesSettings>(
|
|
21
|
+
initialData || { engineerRoundEnabled: false }
|
|
22
|
+
);
|
|
23
|
+
const [changeReason, setChangeReason] = useState('');
|
|
24
|
+
const [saving, setSaving] = useState(false);
|
|
25
|
+
|
|
26
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
setSaving(true);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const res = await updateOrganizationSettings({
|
|
32
|
+
organizationId,
|
|
33
|
+
section: 'features',
|
|
34
|
+
settings: formData,
|
|
35
|
+
changeReason: changeReason.trim() || 'Updated optional feature toggles',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (res.success) {
|
|
39
|
+
toast.success('Feature settings saved successfully');
|
|
40
|
+
setChangeReason('');
|
|
41
|
+
} else {
|
|
42
|
+
toast.error(res.error || 'Failed to update feature settings');
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
toast.error('An unexpected error occurred');
|
|
46
|
+
} finally {
|
|
47
|
+
setSaving(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<form onSubmit={handleSubmit}>
|
|
53
|
+
<Card>
|
|
54
|
+
<CardHeader>
|
|
55
|
+
<CardTitle className="flex items-center gap-2 text-xl">
|
|
56
|
+
<Sliders className="h-5 w-5 text-primary" />
|
|
57
|
+
Optional Module & Feature Toggles
|
|
58
|
+
</CardTitle>
|
|
59
|
+
<CardDescription>
|
|
60
|
+
Enable or deactivate optional modules and workflows to tailor BEMMS to your hospital's operations.
|
|
61
|
+
</CardDescription>
|
|
62
|
+
</CardHeader>
|
|
63
|
+
|
|
64
|
+
<CardContent className="space-y-6">
|
|
65
|
+
<div className="rounded-lg border p-4 transition-colors hover:bg-muted/30">
|
|
66
|
+
<div className="flex items-start justify-between gap-4">
|
|
67
|
+
<div className="flex gap-3">
|
|
68
|
+
<div className="mt-1 rounded-md bg-primary/10 p-2 text-primary">
|
|
69
|
+
<Route className="h-5 w-5" />
|
|
70
|
+
</div>
|
|
71
|
+
<div className="space-y-1">
|
|
72
|
+
<Label htmlFor="engineerRoundEnabled" className="text-base font-semibold cursor-pointer">
|
|
73
|
+
Engineer's Daily Round
|
|
74
|
+
</Label>
|
|
75
|
+
<p className="text-sm text-muted-foreground">
|
|
76
|
+
Digitize the daily physical walk-through performed by biomedical engineers across hospital
|
|
77
|
+
departments with electronic signatures, to-do checklists, notes with @device mentions, and round history tracking.
|
|
78
|
+
</p>
|
|
79
|
+
<p className="text-xs text-muted-foreground">
|
|
80
|
+
When disabled, the Daily Round menu items and scheduling will be hidden.
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<Switch
|
|
86
|
+
id="engineerRoundEnabled"
|
|
87
|
+
checked={formData.engineerRoundEnabled}
|
|
88
|
+
onCheckedChange={(checked) =>
|
|
89
|
+
setFormData((prev) => ({ ...prev, engineerRoundEnabled: checked }))
|
|
90
|
+
}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div className="space-y-2 pt-2 border-t">
|
|
96
|
+
<Label htmlFor="changeReasonFeatures">Change Reason (for compliance audit trail)</Label>
|
|
97
|
+
<Input
|
|
98
|
+
id="changeReasonFeatures"
|
|
99
|
+
placeholder="e.g., Enabled Daily Rounds for biomedical engineering staff"
|
|
100
|
+
value={changeReason}
|
|
101
|
+
onChange={(e) => setChangeReason(e.target.value)}
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
</CardContent>
|
|
105
|
+
|
|
106
|
+
<CardFooter className="flex justify-end border-t pt-4">
|
|
107
|
+
<Button type="submit" disabled={saving} className="gap-2">
|
|
108
|
+
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
|
109
|
+
Save Changes
|
|
110
|
+
</Button>
|
|
111
|
+
</CardFooter>
|
|
112
|
+
</Card>
|
|
113
|
+
</form>
|
|
114
|
+
);
|
|
115
|
+
}
|