@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,383 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
5
|
+
import { format } from 'date-fns';
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from '@/components/ui/dialog';
|
|
12
|
+
import {
|
|
13
|
+
Table,
|
|
14
|
+
TableBody,
|
|
15
|
+
TableCell,
|
|
16
|
+
TableHead,
|
|
17
|
+
TableHeader,
|
|
18
|
+
TableRow,
|
|
19
|
+
} from '@/components/ui/table';
|
|
20
|
+
import { Button } from '@/components/ui/button';
|
|
21
|
+
import { Badge } from '@/components/ui/badge';
|
|
22
|
+
import {
|
|
23
|
+
Eye,
|
|
24
|
+
Clock,
|
|
25
|
+
User,
|
|
26
|
+
Database,
|
|
27
|
+
ChevronLeft,
|
|
28
|
+
ChevronRight,
|
|
29
|
+
ShieldAlert,
|
|
30
|
+
Info,
|
|
31
|
+
Calendar,
|
|
32
|
+
Globe,
|
|
33
|
+
Hash,
|
|
34
|
+
} from 'lucide-react';
|
|
35
|
+
|
|
36
|
+
export interface AuditLogItem {
|
|
37
|
+
id: string;
|
|
38
|
+
timestamp: string | Date;
|
|
39
|
+
actorUserId: string | null;
|
|
40
|
+
actorUser?: {
|
|
41
|
+
id: string;
|
|
42
|
+
fullName: string;
|
|
43
|
+
email: string;
|
|
44
|
+
} | null;
|
|
45
|
+
entityType: string;
|
|
46
|
+
entityId: string;
|
|
47
|
+
actionType: string;
|
|
48
|
+
changeReason?: string | null;
|
|
49
|
+
previousState?: any;
|
|
50
|
+
newState?: any;
|
|
51
|
+
ipAddress?: string | null;
|
|
52
|
+
sessionId?: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AuditLogTableProps {
|
|
56
|
+
logs: AuditLogItem[];
|
|
57
|
+
pagination: {
|
|
58
|
+
page: number;
|
|
59
|
+
pageSize: number;
|
|
60
|
+
total: number;
|
|
61
|
+
totalPages: number;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getActionBadgeStyle(action: string) {
|
|
66
|
+
switch (action) {
|
|
67
|
+
case 'CREATE':
|
|
68
|
+
return 'bg-emerald-100 text-emerald-800 border-emerald-300 dark:bg-emerald-950 dark:text-emerald-300 dark:border-emerald-800';
|
|
69
|
+
case 'UPDATE':
|
|
70
|
+
return 'bg-blue-100 text-blue-800 border-blue-300 dark:bg-blue-950 dark:text-blue-300 dark:border-blue-800';
|
|
71
|
+
case 'DEACTIVATE':
|
|
72
|
+
case 'PURGE':
|
|
73
|
+
return 'bg-red-100 text-red-800 border-red-300 dark:bg-red-950 dark:text-red-300 dark:border-red-800';
|
|
74
|
+
case 'LOGIN':
|
|
75
|
+
return 'bg-teal-100 text-teal-800 border-teal-300 dark:bg-teal-950 dark:text-teal-300 dark:border-teal-800';
|
|
76
|
+
case 'LOGOUT':
|
|
77
|
+
return 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-800';
|
|
78
|
+
case 'FAILED_LOGIN':
|
|
79
|
+
return 'bg-rose-100 text-rose-800 border-rose-300 dark:bg-rose-950 dark:text-rose-300 dark:border-rose-800';
|
|
80
|
+
case 'ARCHIVE':
|
|
81
|
+
return 'bg-purple-100 text-purple-800 border-purple-300 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-800';
|
|
82
|
+
case 'TRANSFER':
|
|
83
|
+
return 'bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950 dark:text-indigo-300 dark:border-indigo-800';
|
|
84
|
+
case 'SIGN':
|
|
85
|
+
case 'POLICY_CHANGE':
|
|
86
|
+
return 'bg-cyan-100 text-cyan-800 border-cyan-300 dark:bg-cyan-950 dark:text-cyan-300 dark:border-cyan-800';
|
|
87
|
+
default:
|
|
88
|
+
return 'bg-slate-100 text-slate-800 border-slate-300 dark:bg-slate-800 dark:text-slate-200';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseJsonState(val: any): string | null {
|
|
93
|
+
if (val === null || val === undefined) return null;
|
|
94
|
+
if (typeof val === 'string') {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(val);
|
|
97
|
+
return JSON.stringify(parsed, null, 2);
|
|
98
|
+
} catch {
|
|
99
|
+
return val;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return JSON.stringify(val, null, 2);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Audit log data table with color-coded badges, row inspector dialog,
|
|
107
|
+
* and responsive pagination controls.
|
|
108
|
+
*/
|
|
109
|
+
export function AuditLogTable({ logs, pagination }: AuditLogTableProps) {
|
|
110
|
+
const router = useRouter();
|
|
111
|
+
const searchParams = useSearchParams();
|
|
112
|
+
const [selectedLog, setSelectedLog] = useState<AuditLogItem | null>(null);
|
|
113
|
+
|
|
114
|
+
const { page, pageSize, total, totalPages } = pagination;
|
|
115
|
+
|
|
116
|
+
const handlePageChange = (newPage: number) => {
|
|
117
|
+
if (newPage < 1 || newPage > totalPages) return;
|
|
118
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
119
|
+
params.set('page', newPage.toString());
|
|
120
|
+
router.push(`?${params.toString()}`);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const startEntry = total === 0 ? 0 : (page - 1) * pageSize + 1;
|
|
124
|
+
const endEntry = Math.min(page * pageSize, total);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div className="space-y-4">
|
|
128
|
+
<div className="rounded-md border bg-card overflow-hidden">
|
|
129
|
+
<Table>
|
|
130
|
+
<TableHeader>
|
|
131
|
+
<TableRow className="bg-muted/50 hover:bg-muted/50">
|
|
132
|
+
<TableHead className="w-[180px]">Timestamp</TableHead>
|
|
133
|
+
<TableHead className="w-[180px]">Actor</TableHead>
|
|
134
|
+
<TableHead className="w-[130px]">Action</TableHead>
|
|
135
|
+
<TableHead className="w-[140px]">Entity Type</TableHead>
|
|
136
|
+
<TableHead className="w-[160px]">Entity ID</TableHead>
|
|
137
|
+
<TableHead>Reason</TableHead>
|
|
138
|
+
<TableHead className="w-[80px] text-right">Details</TableHead>
|
|
139
|
+
</TableRow>
|
|
140
|
+
</TableHeader>
|
|
141
|
+
<TableBody>
|
|
142
|
+
{logs.length === 0 ? (
|
|
143
|
+
<TableRow>
|
|
144
|
+
<TableCell colSpan={7} className="h-40 text-center">
|
|
145
|
+
<div className="flex flex-col items-center justify-center text-muted-foreground space-y-2">
|
|
146
|
+
<Database className="h-8 w-8 text-muted-foreground/50" />
|
|
147
|
+
<p className="text-sm font-medium">No audit logs found.</p>
|
|
148
|
+
<p className="text-xs">Try adjusting your filters or search term.</p>
|
|
149
|
+
</div>
|
|
150
|
+
</TableCell>
|
|
151
|
+
</TableRow>
|
|
152
|
+
) : (
|
|
153
|
+
logs.map((log) => {
|
|
154
|
+
let formattedTime = 'Invalid Date';
|
|
155
|
+
try {
|
|
156
|
+
formattedTime = format(new Date(log.timestamp), 'MMM dd, yyyy HH:mm:ss');
|
|
157
|
+
} catch {
|
|
158
|
+
formattedTime = String(log.timestamp);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const actorName = log.actorUser?.fullName || 'System';
|
|
162
|
+
const actorEmail = log.actorUser?.email || '';
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<TableRow
|
|
166
|
+
key={log.id}
|
|
167
|
+
onClick={() => setSelectedLog(log)}
|
|
168
|
+
className="cursor-pointer hover:bg-muted/60 transition-colors"
|
|
169
|
+
>
|
|
170
|
+
<TableCell className="font-mono text-xs whitespace-nowrap text-muted-foreground">
|
|
171
|
+
<div className="flex items-center gap-1.5">
|
|
172
|
+
<Clock className="h-3.5 w-3.5 text-muted-foreground/70" />
|
|
173
|
+
<span>{formattedTime}</span>
|
|
174
|
+
</div>
|
|
175
|
+
</TableCell>
|
|
176
|
+
|
|
177
|
+
<TableCell>
|
|
178
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
179
|
+
<User className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
180
|
+
<div className="truncate text-xs">
|
|
181
|
+
<p className="font-medium text-foreground truncate">{actorName}</p>
|
|
182
|
+
{actorEmail && (
|
|
183
|
+
<p className="text-[11px] text-muted-foreground truncate">{actorEmail}</p>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</TableCell>
|
|
188
|
+
|
|
189
|
+
<TableCell>
|
|
190
|
+
<Badge
|
|
191
|
+
variant="outline"
|
|
192
|
+
className={`text-xs font-semibold px-2 py-0.5 border ${getActionBadgeStyle(
|
|
193
|
+
log.actionType
|
|
194
|
+
)}`}
|
|
195
|
+
>
|
|
196
|
+
{log.actionType}
|
|
197
|
+
</Badge>
|
|
198
|
+
</TableCell>
|
|
199
|
+
|
|
200
|
+
<TableCell className="text-xs font-medium text-foreground">
|
|
201
|
+
<Badge variant="secondary" className="font-mono text-[11px]">
|
|
202
|
+
{log.entityType}
|
|
203
|
+
</Badge>
|
|
204
|
+
</TableCell>
|
|
205
|
+
|
|
206
|
+
<TableCell className="text-xs font-mono text-muted-foreground truncate max-w-[150px]" title={log.entityId}>
|
|
207
|
+
{log.entityId}
|
|
208
|
+
</TableCell>
|
|
209
|
+
|
|
210
|
+
<TableCell className="text-xs text-muted-foreground max-w-[200px] truncate" title={log.changeReason || ''}>
|
|
211
|
+
{log.changeReason || <span className="text-muted-foreground/50 italic">None</span>}
|
|
212
|
+
</TableCell>
|
|
213
|
+
|
|
214
|
+
<TableCell className="text-right">
|
|
215
|
+
<Button
|
|
216
|
+
variant="ghost"
|
|
217
|
+
size="icon"
|
|
218
|
+
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
|
219
|
+
onClick={(e) => {
|
|
220
|
+
e.stopPropagation();
|
|
221
|
+
setSelectedLog(log);
|
|
222
|
+
}}
|
|
223
|
+
title="View details"
|
|
224
|
+
>
|
|
225
|
+
<Eye className="h-4 w-4" />
|
|
226
|
+
</Button>
|
|
227
|
+
</TableCell>
|
|
228
|
+
</TableRow>
|
|
229
|
+
);
|
|
230
|
+
})
|
|
231
|
+
)}
|
|
232
|
+
</TableBody>
|
|
233
|
+
</Table>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* Pagination Controls */}
|
|
237
|
+
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 px-1 text-sm text-muted-foreground">
|
|
238
|
+
<div>
|
|
239
|
+
Showing <span className="font-medium text-foreground">{startEntry}</span> to{' '}
|
|
240
|
+
<span className="font-medium text-foreground">{endEntry}</span> of{' '}
|
|
241
|
+
<span className="font-medium text-foreground">{total}</span> entries
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<div className="flex items-center gap-2">
|
|
245
|
+
<Button
|
|
246
|
+
variant="outline"
|
|
247
|
+
size="sm"
|
|
248
|
+
onClick={() => handlePageChange(page - 1)}
|
|
249
|
+
disabled={page <= 1}
|
|
250
|
+
className="h-8 px-2.5"
|
|
251
|
+
>
|
|
252
|
+
<ChevronLeft className="h-4 w-4 mr-1" />
|
|
253
|
+
Previous
|
|
254
|
+
</Button>
|
|
255
|
+
|
|
256
|
+
<span className="text-xs font-medium px-2">
|
|
257
|
+
Page {page} of {totalPages}
|
|
258
|
+
</span>
|
|
259
|
+
|
|
260
|
+
<Button
|
|
261
|
+
variant="outline"
|
|
262
|
+
size="sm"
|
|
263
|
+
onClick={() => handlePageChange(page + 1)}
|
|
264
|
+
disabled={page >= totalPages}
|
|
265
|
+
className="h-8 px-2.5"
|
|
266
|
+
>
|
|
267
|
+
Next
|
|
268
|
+
<ChevronRight className="h-4 w-4 ml-1" />
|
|
269
|
+
</Button>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{/* Audit Log Detail Dialog */}
|
|
274
|
+
<Dialog open={Boolean(selectedLog)} onOpenChange={(open) => !open && setSelectedLog(null)}>
|
|
275
|
+
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
|
|
276
|
+
{selectedLog && (
|
|
277
|
+
<>
|
|
278
|
+
<DialogHeader>
|
|
279
|
+
<div className="flex items-center justify-between gap-2 mr-6">
|
|
280
|
+
<DialogTitle className="flex items-center gap-2 text-lg">
|
|
281
|
+
<Info className="h-5 w-5 text-primary" />
|
|
282
|
+
Audit Log Entry #{selectedLog.id}
|
|
283
|
+
</DialogTitle>
|
|
284
|
+
<Badge
|
|
285
|
+
variant="outline"
|
|
286
|
+
className={`text-xs font-semibold px-2 py-0.5 border ${getActionBadgeStyle(
|
|
287
|
+
selectedLog.actionType
|
|
288
|
+
)}`}
|
|
289
|
+
>
|
|
290
|
+
{selectedLog.actionType}
|
|
291
|
+
</Badge>
|
|
292
|
+
</div>
|
|
293
|
+
</DialogHeader>
|
|
294
|
+
|
|
295
|
+
{/* Metadata grid */}
|
|
296
|
+
<div className="grid grid-cols-2 gap-3 p-3 bg-muted/40 rounded-lg text-xs mt-2">
|
|
297
|
+
<div className="flex items-center gap-2">
|
|
298
|
+
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
299
|
+
<div>
|
|
300
|
+
<span className="text-muted-foreground">Timestamp: </span>
|
|
301
|
+
<span className="font-mono font-medium">
|
|
302
|
+
{new Date(selectedLog.timestamp).toLocaleString()}
|
|
303
|
+
</span>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div className="flex items-center gap-2">
|
|
308
|
+
<User className="h-4 w-4 text-muted-foreground" />
|
|
309
|
+
<div>
|
|
310
|
+
<span className="text-muted-foreground">Actor: </span>
|
|
311
|
+
<span className="font-medium">
|
|
312
|
+
{selectedLog.actorUser?.fullName || selectedLog.actorUserId || 'System'}
|
|
313
|
+
</span>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div className="flex items-center gap-2">
|
|
318
|
+
<Hash className="h-4 w-4 text-muted-foreground" />
|
|
319
|
+
<div>
|
|
320
|
+
<span className="text-muted-foreground">Entity: </span>
|
|
321
|
+
<span className="font-mono font-medium">
|
|
322
|
+
{selectedLog.entityType} ({selectedLog.entityId})
|
|
323
|
+
</span>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<div className="flex items-center gap-2">
|
|
328
|
+
<Globe className="h-4 w-4 text-muted-foreground" />
|
|
329
|
+
<div>
|
|
330
|
+
<span className="text-muted-foreground">IP Address: </span>
|
|
331
|
+
<span className="font-mono">{selectedLog.ipAddress || 'N/A'}</span>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
{selectedLog.changeReason && (
|
|
337
|
+
<div className="space-y-1">
|
|
338
|
+
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
339
|
+
Reason / Context
|
|
340
|
+
</h4>
|
|
341
|
+
<p className="text-sm bg-muted/30 p-2.5 rounded border">
|
|
342
|
+
{selectedLog.changeReason}
|
|
343
|
+
</p>
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
|
|
347
|
+
{/* State Diffs / JSON viewer */}
|
|
348
|
+
<div className="space-y-4">
|
|
349
|
+
{selectedLog.previousState ? (
|
|
350
|
+
<div>
|
|
351
|
+
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1.5">
|
|
352
|
+
Previous State
|
|
353
|
+
</h4>
|
|
354
|
+
<pre className="bg-slate-950 text-slate-100 p-3.5 rounded-md text-xs font-mono overflow-x-auto max-h-60 border border-slate-800">
|
|
355
|
+
{parseJsonState(selectedLog.previousState)}
|
|
356
|
+
</pre>
|
|
357
|
+
</div>
|
|
358
|
+
) : null}
|
|
359
|
+
|
|
360
|
+
{selectedLog.newState ? (
|
|
361
|
+
<div>
|
|
362
|
+
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1.5">
|
|
363
|
+
New State / Details
|
|
364
|
+
</h4>
|
|
365
|
+
<pre className="bg-slate-950 text-slate-100 p-3.5 rounded-md text-xs font-mono overflow-x-auto max-h-60 border border-slate-800">
|
|
366
|
+
{parseJsonState(selectedLog.newState)}
|
|
367
|
+
</pre>
|
|
368
|
+
</div>
|
|
369
|
+
) : null}
|
|
370
|
+
|
|
371
|
+
{!selectedLog.previousState && !selectedLog.newState && (
|
|
372
|
+
<p className="text-xs text-muted-foreground italic py-2 text-center">
|
|
373
|
+
No state payload recorded for this event.
|
|
374
|
+
</p>
|
|
375
|
+
)}
|
|
376
|
+
</div>
|
|
377
|
+
</>
|
|
378
|
+
)}
|
|
379
|
+
</DialogContent>
|
|
380
|
+
</Dialog>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Input } from '@/components/ui/input';
|
|
8
|
+
import { Label } from '@/components/ui/label';
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
} from '@/components/ui/dialog';
|
|
17
|
+
import { AlertTriangle, Trash2, RotateCcw, ShieldAlert, Lock, Loader2, CheckCircle2 } from 'lucide-react';
|
|
18
|
+
import { toast } from 'sonner';
|
|
19
|
+
import { purgeData } from '@/lib/actions/data-management';
|
|
20
|
+
|
|
21
|
+
export function DangerZone() {
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
const [selectedScope, setSelectedScope] = useState<'test_transactions' | 'factory_reset'>('test_transactions');
|
|
24
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
25
|
+
const [confirmText, setConfirmText] = useState('');
|
|
26
|
+
const [password, setPassword] = useState('');
|
|
27
|
+
const [reason, setReason] = useState('');
|
|
28
|
+
const [purging, setPurging] = useState(false);
|
|
29
|
+
|
|
30
|
+
const expectedPhrase = selectedScope === 'test_transactions' ? 'CLEAR TEST DATA' : 'RESET ALL DATA';
|
|
31
|
+
|
|
32
|
+
const normalizePhrase = (s: string) => s.trim().toUpperCase().replace(/[_-\s]+/g, ' ');
|
|
33
|
+
const isPhraseMatch = selectedScope === 'test_transactions'
|
|
34
|
+
? normalizePhrase(confirmText) === 'CLEAR TEST DATA' || normalizePhrase(confirmText) === 'PURGE TEST DATA'
|
|
35
|
+
: normalizePhrase(confirmText) === 'RESET ALL DATA';
|
|
36
|
+
|
|
37
|
+
const handleOpenDialog = (scope: 'test_transactions' | 'factory_reset') => {
|
|
38
|
+
setSelectedScope(scope);
|
|
39
|
+
setConfirmText('');
|
|
40
|
+
setPassword('');
|
|
41
|
+
setReason('');
|
|
42
|
+
setDialogOpen(true);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleConfirmPurge = async (e: React.FormEvent) => {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
|
|
48
|
+
if (!isPhraseMatch) {
|
|
49
|
+
toast.error(`Confirmation phrase must match "${expectedPhrase}"`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!password) {
|
|
54
|
+
toast.error('Administrator password is required to re-authenticate');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setPurging(true);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const res = await purgeData({
|
|
62
|
+
scope: selectedScope,
|
|
63
|
+
confirmText: expectedPhrase,
|
|
64
|
+
password,
|
|
65
|
+
reason: reason.trim() || undefined,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (res.success) {
|
|
69
|
+
toast.success(res.message || 'Operation executed successfully');
|
|
70
|
+
setDialogOpen(false);
|
|
71
|
+
router.refresh();
|
|
72
|
+
} else {
|
|
73
|
+
toast.error(res.error || 'Failed to execute data reset');
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
toast.error('An unexpected error occurred');
|
|
77
|
+
} finally {
|
|
78
|
+
setPurging(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="space-y-6">
|
|
84
|
+
<Card className="border-destructive/40 bg-destructive/5">
|
|
85
|
+
<CardHeader>
|
|
86
|
+
<div className="flex items-center gap-3">
|
|
87
|
+
<div className="h-10 w-10 rounded-full bg-destructive/10 text-destructive flex items-center justify-center shrink-0">
|
|
88
|
+
<AlertTriangle className="h-6 w-6" />
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
<CardTitle className="text-xl text-destructive flex items-center gap-2">
|
|
92
|
+
Danger Zone & Data Reset
|
|
93
|
+
</CardTitle>
|
|
94
|
+
<CardDescription>
|
|
95
|
+
High-friction administrative tools for clearing test records or resetting the environment prior to clinical launch.
|
|
96
|
+
</CardDescription>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</CardHeader>
|
|
100
|
+
|
|
101
|
+
<CardContent className="space-y-6">
|
|
102
|
+
{/* Action 1: Clear Test Data */}
|
|
103
|
+
<div className="p-5 border rounded-lg bg-background flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
104
|
+
<div className="space-y-1">
|
|
105
|
+
<h5 className="font-semibold text-base flex items-center gap-2">
|
|
106
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
107
|
+
Clear Test & Transactional Records
|
|
108
|
+
</h5>
|
|
109
|
+
<p className="text-xs text-muted-foreground max-w-xl">
|
|
110
|
+
Deletes all service tickets, sample work orders, and test maintenance logs generated during user training.
|
|
111
|
+
<strong className="text-foreground block mt-1">
|
|
112
|
+
Preserves: Medical equipment inventory, hospital departments, users, and system roles.
|
|
113
|
+
</strong>
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
<Button
|
|
117
|
+
variant="destructive"
|
|
118
|
+
onClick={() => handleOpenDialog('test_transactions')}
|
|
119
|
+
className="shrink-0 gap-2"
|
|
120
|
+
>
|
|
121
|
+
<Trash2 className="h-4 w-4" />
|
|
122
|
+
Clear Test Data
|
|
123
|
+
</Button>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Action 2: Full Factory Reset */}
|
|
127
|
+
<div className="p-5 border rounded-lg bg-background flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
128
|
+
<div className="space-y-1">
|
|
129
|
+
<h5 className="font-semibold text-base flex items-center gap-2 text-destructive">
|
|
130
|
+
<RotateCcw className="h-4 w-4" />
|
|
131
|
+
Full System Factory Reset
|
|
132
|
+
</h5>
|
|
133
|
+
<p className="text-xs text-muted-foreground max-w-xl">
|
|
134
|
+
Restores the BEMMS application back to its fresh post-installation state. Clears devices, tickets,
|
|
135
|
+
and custom data while re-establishing default system administrative access.
|
|
136
|
+
</p>
|
|
137
|
+
</div>
|
|
138
|
+
<Button
|
|
139
|
+
variant="outline"
|
|
140
|
+
onClick={() => handleOpenDialog('factory_reset')}
|
|
141
|
+
className="border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground shrink-0 gap-2"
|
|
142
|
+
>
|
|
143
|
+
<RotateCcw className="h-4 w-4" />
|
|
144
|
+
Factory Reset
|
|
145
|
+
</Button>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{/* Section 16 Regulatory Notice */}
|
|
149
|
+
<div className="p-4 border rounded-lg bg-muted/60 flex items-start gap-3">
|
|
150
|
+
<ShieldAlert className="h-5 w-5 text-muted-foreground shrink-0 mt-0.5" />
|
|
151
|
+
<div className="text-xs text-muted-foreground space-y-1">
|
|
152
|
+
<p className="font-semibold text-foreground">
|
|
153
|
+
FDA 21 CFR Part 11 & BEMMS Section 16 Regulatory Safeguard:
|
|
154
|
+
</p>
|
|
155
|
+
<p>
|
|
156
|
+
In clinical hospital deployments, signed medical equipment maintenance histories are permanent legal records.
|
|
157
|
+
All data purge actions require administrator re-authentication and are permanently recorded in the immutable audit log.
|
|
158
|
+
</p>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</CardContent>
|
|
162
|
+
</Card>
|
|
163
|
+
|
|
164
|
+
{/* Confirmation & Re-auth Modal */}
|
|
165
|
+
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
166
|
+
<DialogContent className="sm:max-w-md">
|
|
167
|
+
<form onSubmit={handleConfirmPurge}>
|
|
168
|
+
<DialogHeader>
|
|
169
|
+
<DialogTitle className="flex items-center gap-2 text-destructive">
|
|
170
|
+
<AlertTriangle className="h-5 w-5" />
|
|
171
|
+
{selectedScope === 'test_transactions'
|
|
172
|
+
? 'Confirm Clear Test Data'
|
|
173
|
+
: 'Confirm Full Factory Reset'}
|
|
174
|
+
</DialogTitle>
|
|
175
|
+
<DialogDescription className="text-xs">
|
|
176
|
+
This operation is irreversible. Please follow the two-factor safety confirmation steps below.
|
|
177
|
+
</DialogDescription>
|
|
178
|
+
</DialogHeader>
|
|
179
|
+
|
|
180
|
+
<div className="space-y-4 py-4">
|
|
181
|
+
{/* Step 1: Confirmation Phrase */}
|
|
182
|
+
<div className="space-y-2">
|
|
183
|
+
<div className="flex items-center justify-between">
|
|
184
|
+
<Label htmlFor="confirmPhrase" className="text-xs font-semibold">
|
|
185
|
+
1. Type confirmation phrase:
|
|
186
|
+
</Label>
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
onClick={() => setConfirmText(expectedPhrase)}
|
|
190
|
+
className="text-[11px] text-primary hover:underline cursor-pointer font-medium"
|
|
191
|
+
>
|
|
192
|
+
Auto-fill
|
|
193
|
+
</button>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="flex items-center gap-2">
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
onClick={() => setConfirmText(expectedPhrase)}
|
|
199
|
+
className="cursor-pointer inline-flex items-center gap-1 font-mono text-xs font-bold px-2 py-0.5 rounded bg-destructive/15 text-destructive border border-destructive/30 hover:bg-destructive/25 transition-colors"
|
|
200
|
+
title="Click to auto-fill"
|
|
201
|
+
>
|
|
202
|
+
{expectedPhrase}
|
|
203
|
+
</button>
|
|
204
|
+
<span className="text-[11px] text-muted-foreground">(click to fill or type)</span>
|
|
205
|
+
</div>
|
|
206
|
+
<Input
|
|
207
|
+
id="confirmPhrase"
|
|
208
|
+
value={confirmText}
|
|
209
|
+
onChange={(e) => setConfirmText(e.target.value)}
|
|
210
|
+
placeholder={expectedPhrase}
|
|
211
|
+
className={`font-mono text-xs transition-colors ${
|
|
212
|
+
isPhraseMatch
|
|
213
|
+
? 'border-emerald-500 ring-1 ring-emerald-500 bg-emerald-500/5'
|
|
214
|
+
: confirmText.length > 0
|
|
215
|
+
? 'border-amber-500 ring-1 ring-amber-500/30'
|
|
216
|
+
: ''
|
|
217
|
+
}`}
|
|
218
|
+
autoComplete="off"
|
|
219
|
+
/>
|
|
220
|
+
{isPhraseMatch ? (
|
|
221
|
+
<p className="text-[11px] text-emerald-600 dark:text-emerald-400 font-medium flex items-center gap-1">
|
|
222
|
+
<CheckCircle2 className="h-3.5 w-3.5" />
|
|
223
|
+
Confirmation phrase matched
|
|
224
|
+
</p>
|
|
225
|
+
) : confirmText.length > 0 ? (
|
|
226
|
+
<p className="text-[11px] text-amber-600 dark:text-amber-400">
|
|
227
|
+
Type "{expectedPhrase}" (spaces or underscores, case-insensitive)
|
|
228
|
+
</p>
|
|
229
|
+
) : null}
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Step 2: Password Re-Authentication */}
|
|
233
|
+
<div className="space-y-2">
|
|
234
|
+
<Label htmlFor="adminPassword" className="text-xs font-semibold flex items-center gap-1.5">
|
|
235
|
+
<Lock className="h-3.5 w-3.5 text-muted-foreground" />
|
|
236
|
+
2. Enter Administrator Password:
|
|
237
|
+
</Label>
|
|
238
|
+
<Input
|
|
239
|
+
id="adminPassword"
|
|
240
|
+
type="password"
|
|
241
|
+
value={password}
|
|
242
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
243
|
+
placeholder="Your current account password"
|
|
244
|
+
autoComplete="current-password"
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{/* Step 3: Justification Note */}
|
|
249
|
+
<div className="space-y-2">
|
|
250
|
+
<Label htmlFor="purgeReason" className="text-xs font-semibold">
|
|
251
|
+
3. Audit Justification Reason:
|
|
252
|
+
</Label>
|
|
253
|
+
<Input
|
|
254
|
+
id="purgeReason"
|
|
255
|
+
value={reason}
|
|
256
|
+
onChange={(e) => setReason(e.target.value)}
|
|
257
|
+
placeholder="e.g., Pre-go-live test data cleanup"
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<DialogFooter className="flex gap-2 sm:justify-end">
|
|
263
|
+
<Button
|
|
264
|
+
type="button"
|
|
265
|
+
variant="outline"
|
|
266
|
+
onClick={() => setDialogOpen(false)}
|
|
267
|
+
disabled={purging}
|
|
268
|
+
>
|
|
269
|
+
Cancel
|
|
270
|
+
</Button>
|
|
271
|
+
<Button
|
|
272
|
+
type="submit"
|
|
273
|
+
variant="destructive"
|
|
274
|
+
disabled={purging || !isPhraseMatch || !password}
|
|
275
|
+
className="gap-2"
|
|
276
|
+
>
|
|
277
|
+
{purging ? (
|
|
278
|
+
<>
|
|
279
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
280
|
+
{selectedScope === 'test_transactions' ? 'Clearing Test Data...' : 'Resetting System...'}
|
|
281
|
+
</>
|
|
282
|
+
) : selectedScope === 'test_transactions' ? (
|
|
283
|
+
<>
|
|
284
|
+
<Trash2 className="h-4 w-4" />
|
|
285
|
+
Clear Test Data
|
|
286
|
+
</>
|
|
287
|
+
) : (
|
|
288
|
+
<>
|
|
289
|
+
<RotateCcw className="h-4 w-4" />
|
|
290
|
+
Execute Factory Reset
|
|
291
|
+
</>
|
|
292
|
+
)}
|
|
293
|
+
</Button>
|
|
294
|
+
</DialogFooter>
|
|
295
|
+
</form>
|
|
296
|
+
</DialogContent>
|
|
297
|
+
</Dialog>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|