@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,182 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Activity, Database, Server, Cpu, Clock, RefreshCw, ShieldAlert, CheckCircle2, AlertCircle } from 'lucide-react';
|
|
8
|
+
import { getSystemHealth, type SystemHealthData } from '@/lib/actions/system-settings';
|
|
9
|
+
import { toast } from 'sonner';
|
|
10
|
+
|
|
11
|
+
interface HealthPanelProps {
|
|
12
|
+
initialData: SystemHealthData;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatUptime(seconds: number): string {
|
|
16
|
+
const days = Math.floor(seconds / (3600 * 24));
|
|
17
|
+
const hours = Math.floor((seconds % (3600 * 24)) / 3600);
|
|
18
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
19
|
+
const secs = Math.floor(seconds % 60);
|
|
20
|
+
|
|
21
|
+
const parts = [];
|
|
22
|
+
if (days > 0) parts.push(`${days}d`);
|
|
23
|
+
if (hours > 0 || days > 0) parts.push(`${hours}h`);
|
|
24
|
+
if (minutes > 0 || hours > 0 || days > 0) parts.push(`${minutes}m`);
|
|
25
|
+
parts.push(`${secs}s`);
|
|
26
|
+
|
|
27
|
+
return parts.join(' ');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function HealthPanel({ initialData }: HealthPanelProps) {
|
|
31
|
+
const [health, setHealth] = useState<SystemHealthData>(initialData);
|
|
32
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
33
|
+
|
|
34
|
+
const handleRefresh = async () => {
|
|
35
|
+
setRefreshing(true);
|
|
36
|
+
try {
|
|
37
|
+
const res = await getSystemHealth();
|
|
38
|
+
if (res.success && res.data) {
|
|
39
|
+
setHealth(res.data);
|
|
40
|
+
toast.success('Telemetry updated');
|
|
41
|
+
} else {
|
|
42
|
+
toast.error(res.error || 'Failed to refresh health status');
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
toast.error('An error occurred while fetching health data');
|
|
46
|
+
} finally {
|
|
47
|
+
setRefreshing(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const isHealthy = health.status === 'healthy';
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-6">
|
|
55
|
+
<Card>
|
|
56
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
|
57
|
+
<div>
|
|
58
|
+
<CardTitle className="flex items-center gap-2 text-xl">
|
|
59
|
+
<Activity className="h-5 w-5 text-primary" />
|
|
60
|
+
Operational Telemetry & System Health
|
|
61
|
+
</CardTitle>
|
|
62
|
+
<CardDescription>
|
|
63
|
+
Safe, read-only status of database connections, runtime environment, and background health.
|
|
64
|
+
</CardDescription>
|
|
65
|
+
</div>
|
|
66
|
+
<Button
|
|
67
|
+
variant="outline"
|
|
68
|
+
size="sm"
|
|
69
|
+
onClick={handleRefresh}
|
|
70
|
+
disabled={refreshing}
|
|
71
|
+
className="gap-2"
|
|
72
|
+
>
|
|
73
|
+
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
|
74
|
+
Refresh
|
|
75
|
+
</Button>
|
|
76
|
+
</CardHeader>
|
|
77
|
+
|
|
78
|
+
<CardContent className="space-y-6">
|
|
79
|
+
{/* Status Header Banner */}
|
|
80
|
+
<div
|
|
81
|
+
className={`p-4 rounded-lg flex items-center justify-between border ${
|
|
82
|
+
isHealthy
|
|
83
|
+
? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-700 dark:text-emerald-400'
|
|
84
|
+
: 'bg-amber-500/10 border-amber-500/20 text-amber-700 dark:text-amber-400'
|
|
85
|
+
}`}
|
|
86
|
+
>
|
|
87
|
+
<div className="flex items-center gap-3">
|
|
88
|
+
{isHealthy ? (
|
|
89
|
+
<CheckCircle2 className="h-6 w-6 text-emerald-600 dark:text-emerald-400 shrink-0" />
|
|
90
|
+
) : (
|
|
91
|
+
<AlertCircle className="h-6 w-6 text-amber-600 dark:text-amber-400 shrink-0" />
|
|
92
|
+
)}
|
|
93
|
+
<div>
|
|
94
|
+
<h4 className="font-semibold text-base">
|
|
95
|
+
{isHealthy ? 'All Systems Operational' : 'System Degraded'}
|
|
96
|
+
</h4>
|
|
97
|
+
<p className="text-xs opacity-90">
|
|
98
|
+
{isHealthy
|
|
99
|
+
? 'PostgreSQL database connected and query latency within acceptable limits.'
|
|
100
|
+
: 'System is experiencing elevated latency or service degradation.'}
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<Badge variant={isHealthy ? 'default' : 'destructive'} className="uppercase">
|
|
105
|
+
{health.status}
|
|
106
|
+
</Badge>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Metrics Grid */}
|
|
110
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
111
|
+
{/* Database Metric */}
|
|
112
|
+
<div className="p-4 border rounded-lg bg-card space-y-1">
|
|
113
|
+
<div className="flex items-center justify-between text-muted-foreground">
|
|
114
|
+
<span className="text-xs font-medium">Database Latency</span>
|
|
115
|
+
<Database className="h-4 w-4" />
|
|
116
|
+
</div>
|
|
117
|
+
<div className="text-2xl font-bold">
|
|
118
|
+
{health.databasePingMs >= 0 ? `${health.databasePingMs}ms` : 'Error'}
|
|
119
|
+
</div>
|
|
120
|
+
<span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium flex items-center gap-1">
|
|
121
|
+
<span className="h-2 w-2 rounded-full bg-emerald-500 inline-block" />
|
|
122
|
+
PostgreSQL 16 Connected
|
|
123
|
+
</span>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Uptime Metric */}
|
|
127
|
+
<div className="p-4 border rounded-lg bg-card space-y-1">
|
|
128
|
+
<div className="flex items-center justify-between text-muted-foreground">
|
|
129
|
+
<span className="text-xs font-medium">Application Uptime</span>
|
|
130
|
+
<Clock className="h-4 w-4" />
|
|
131
|
+
</div>
|
|
132
|
+
<div className="text-2xl font-bold">{formatUptime(health.uptimeSeconds)}</div>
|
|
133
|
+
<span className="text-xs text-muted-foreground">
|
|
134
|
+
Environment: <span className="font-medium text-foreground">{health.environment}</span>
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Memory Metric */}
|
|
139
|
+
<div className="p-4 border rounded-lg bg-card space-y-1">
|
|
140
|
+
<div className="flex items-center justify-between text-muted-foreground">
|
|
141
|
+
<span className="text-xs font-medium">Memory (RSS / Heap)</span>
|
|
142
|
+
<Cpu className="h-4 w-4" />
|
|
143
|
+
</div>
|
|
144
|
+
<div className="text-2xl font-bold">{health.memoryUsage.rssMb} MB</div>
|
|
145
|
+
<span className="text-xs text-muted-foreground">
|
|
146
|
+
Heap Used: {health.memoryUsage.heapUsedMb} MB / {health.memoryUsage.heapTotalMb} MB
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{/* Runtime Metric */}
|
|
151
|
+
<div className="p-4 border rounded-lg bg-card space-y-1">
|
|
152
|
+
<div className="flex items-center justify-between text-muted-foreground">
|
|
153
|
+
<span className="text-xs font-medium">Platform & Node</span>
|
|
154
|
+
<Server className="h-4 w-4" />
|
|
155
|
+
</div>
|
|
156
|
+
<div className="text-2xl font-bold">{health.runtime.nodeVersion}</div>
|
|
157
|
+
<span className="text-xs text-muted-foreground">
|
|
158
|
+
OS: {health.runtime.platform} ({health.runtime.arch})
|
|
159
|
+
</span>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Section 14.2 Compliance Notice */}
|
|
164
|
+
<div className="p-4 border rounded-lg bg-muted/40 flex items-start gap-3">
|
|
165
|
+
<ShieldAlert className="h-5 w-5 text-muted-foreground shrink-0 mt-0.5" />
|
|
166
|
+
<div className="text-xs text-muted-foreground space-y-1">
|
|
167
|
+
<p className="font-medium text-foreground">
|
|
168
|
+
BEMMS Specification Section 14.2 Compliance Safeguard:
|
|
169
|
+
</p>
|
|
170
|
+
<p>
|
|
171
|
+
PostgreSQL credentials, application encryption keys, signing PIN secrets, and TLS certificates
|
|
172
|
+
are strictly isolated in protected server configuration (.env / Docker secrets). They are never
|
|
173
|
+
accessible or returned to application web sessions to prevent credential leakage.
|
|
174
|
+
</p>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</CardContent>
|
|
178
|
+
</Card>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
@@ -0,0 +1,203 @@
|
|
|
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
9
|
+
import { Globe, Clock, Calendar, Languages, Loader2, Save } from 'lucide-react';
|
|
10
|
+
import { toast } from 'sonner';
|
|
11
|
+
import { updateOrganizationSettings } from '@/lib/actions/system-settings';
|
|
12
|
+
import type { LocalizationSettings } from '@/lib/validators/system-settings';
|
|
13
|
+
|
|
14
|
+
interface LocalizationFormProps {
|
|
15
|
+
initialData: LocalizationSettings;
|
|
16
|
+
organizationId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const COMMON_TIMEZONES = [
|
|
20
|
+
{ value: 'UTC', label: 'UTC (Coordinated Universal Time)' },
|
|
21
|
+
{ value: 'Africa/Cairo', label: 'Africa/Cairo (UTC+2 / UTC+3)' },
|
|
22
|
+
{ value: 'Asia/Riyadh', label: 'Asia/Riyadh (UTC+3)' },
|
|
23
|
+
{ value: 'Asia/Dubai', label: 'Asia/Dubai (UTC+4)' },
|
|
24
|
+
{ value: 'Europe/London', label: 'Europe/London (UTC+0 / UTC+1)' },
|
|
25
|
+
{ value: 'America/New_York', label: 'America/New_York (UTC-5 / UTC-4)' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function LocalizationForm({ initialData, organizationId }: LocalizationFormProps) {
|
|
29
|
+
const [formData, setFormData] = useState<LocalizationSettings>(initialData);
|
|
30
|
+
const [changeReason, setChangeReason] = useState('');
|
|
31
|
+
const [saving, setSaving] = useState(false);
|
|
32
|
+
|
|
33
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
setSaving(true);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const res = await updateOrganizationSettings({
|
|
39
|
+
organizationId,
|
|
40
|
+
section: 'localization',
|
|
41
|
+
settings: formData,
|
|
42
|
+
changeReason: changeReason.trim() || 'Updated localization and regional settings',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (res.success) {
|
|
46
|
+
toast.success('Localization settings saved successfully');
|
|
47
|
+
setChangeReason('');
|
|
48
|
+
} else {
|
|
49
|
+
toast.error(res.error || 'Failed to update localization settings');
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
toast.error('An unexpected error occurred');
|
|
53
|
+
} finally {
|
|
54
|
+
setSaving(false);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<form onSubmit={handleSubmit}>
|
|
60
|
+
<Card>
|
|
61
|
+
<CardHeader>
|
|
62
|
+
<CardTitle className="flex items-center gap-2 text-xl">
|
|
63
|
+
<Globe className="h-5 w-5 text-primary" />
|
|
64
|
+
Localization & Regional Preferences
|
|
65
|
+
</CardTitle>
|
|
66
|
+
<CardDescription>
|
|
67
|
+
Configure time display, calendar formats, and operational language for this facility.
|
|
68
|
+
</CardDescription>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
|
|
71
|
+
<CardContent className="space-y-6">
|
|
72
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
73
|
+
{/* Timezone */}
|
|
74
|
+
<div className="space-y-2">
|
|
75
|
+
<Label htmlFor="timezone" className="flex items-center gap-2">
|
|
76
|
+
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
77
|
+
Default Facility Timezone
|
|
78
|
+
</Label>
|
|
79
|
+
<Select
|
|
80
|
+
value={formData.timezone}
|
|
81
|
+
onValueChange={(val) => setFormData((prev) => ({ ...prev, timezone: val }))}
|
|
82
|
+
>
|
|
83
|
+
<SelectTrigger id="timezone">
|
|
84
|
+
<SelectValue placeholder="Select timezone" />
|
|
85
|
+
</SelectTrigger>
|
|
86
|
+
<SelectContent>
|
|
87
|
+
{COMMON_TIMEZONES.map((tz) => (
|
|
88
|
+
<SelectItem key={tz.value} value={tz.value}>
|
|
89
|
+
{tz.label}
|
|
90
|
+
</SelectItem>
|
|
91
|
+
))}
|
|
92
|
+
</SelectContent>
|
|
93
|
+
</Select>
|
|
94
|
+
<p className="text-xs text-muted-foreground">
|
|
95
|
+
All maintenance schedules, PM occurrences, and audit events will display in this timezone.
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Language */}
|
|
100
|
+
<div className="space-y-2">
|
|
101
|
+
<Label htmlFor="language" className="flex items-center gap-2">
|
|
102
|
+
<Languages className="h-4 w-4 text-muted-foreground" />
|
|
103
|
+
Default Language
|
|
104
|
+
</Label>
|
|
105
|
+
<Select
|
|
106
|
+
value={formData.language}
|
|
107
|
+
onValueChange={(val: 'en' | 'ar') => setFormData((prev) => ({ ...prev, language: val }))}
|
|
108
|
+
>
|
|
109
|
+
<SelectTrigger id="language">
|
|
110
|
+
<SelectValue placeholder="Select language" />
|
|
111
|
+
</SelectTrigger>
|
|
112
|
+
<SelectContent>
|
|
113
|
+
<SelectItem value="en">English (US / UK)</SelectItem>
|
|
114
|
+
<SelectItem value="ar">العربية (Arabic - Future Support)</SelectItem>
|
|
115
|
+
</SelectContent>
|
|
116
|
+
</Select>
|
|
117
|
+
<p className="text-xs text-muted-foreground">
|
|
118
|
+
Application interface language preference (Section 14.1).
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{/* Date Format */}
|
|
123
|
+
<div className="space-y-2">
|
|
124
|
+
<Label htmlFor="dateFormat" className="flex items-center gap-2">
|
|
125
|
+
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
126
|
+
Date Format
|
|
127
|
+
</Label>
|
|
128
|
+
<Select
|
|
129
|
+
value={formData.dateFormat}
|
|
130
|
+
onValueChange={(val: 'YYYY-MM-DD' | 'DD/MM/YYYY' | 'MM/DD/YYYY') =>
|
|
131
|
+
setFormData((prev) => ({ ...prev, dateFormat: val }))
|
|
132
|
+
}
|
|
133
|
+
>
|
|
134
|
+
<SelectTrigger id="dateFormat">
|
|
135
|
+
<SelectValue placeholder="Select date format" />
|
|
136
|
+
</SelectTrigger>
|
|
137
|
+
<SelectContent>
|
|
138
|
+
<SelectItem value="YYYY-MM-DD">YYYY-MM-DD (ISO 8601 - Recommended)</SelectItem>
|
|
139
|
+
<SelectItem value="DD/MM/YYYY">DD/MM/YYYY (UK / EU)</SelectItem>
|
|
140
|
+
<SelectItem value="MM/DD/YYYY">MM/DD/YYYY (US)</SelectItem>
|
|
141
|
+
</SelectContent>
|
|
142
|
+
</Select>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Time Format */}
|
|
146
|
+
<div className="space-y-2">
|
|
147
|
+
<Label htmlFor="timeFormat" className="flex items-center gap-2">
|
|
148
|
+
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
149
|
+
Time Display
|
|
150
|
+
</Label>
|
|
151
|
+
<Select
|
|
152
|
+
value={formData.timeFormat}
|
|
153
|
+
onValueChange={(val: '24h' | '12h') => setFormData((prev) => ({ ...prev, timeFormat: val }))}
|
|
154
|
+
>
|
|
155
|
+
<SelectTrigger id="timeFormat">
|
|
156
|
+
<SelectValue placeholder="Select time format" />
|
|
157
|
+
</SelectTrigger>
|
|
158
|
+
<SelectContent>
|
|
159
|
+
<SelectItem value="24h">24-hour (14:30) - Clinical Standard</SelectItem>
|
|
160
|
+
<SelectItem value="12h">12-hour (2:30 PM)</SelectItem>
|
|
161
|
+
</SelectContent>
|
|
162
|
+
</Select>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* Audit Trace Reason */}
|
|
167
|
+
<div className="space-y-2 pt-2 border-t">
|
|
168
|
+
<Label htmlFor="changeReason" className="text-sm font-medium">
|
|
169
|
+
Change Reason (21 CFR Part 11 Audit Trace)
|
|
170
|
+
</Label>
|
|
171
|
+
<Input
|
|
172
|
+
id="changeReason"
|
|
173
|
+
placeholder="e.g., Hospital shifted default timezone to local regional time"
|
|
174
|
+
value={changeReason}
|
|
175
|
+
onChange={(e) => setChangeReason(e.target.value)}
|
|
176
|
+
className="max-w-xl"
|
|
177
|
+
/>
|
|
178
|
+
<p className="text-xs text-muted-foreground">
|
|
179
|
+
Optional documentation justification recorded in the immutable audit log.
|
|
180
|
+
</p>
|
|
181
|
+
</div>
|
|
182
|
+
</CardContent>
|
|
183
|
+
|
|
184
|
+
<CardFooter className="flex justify-end border-t pt-4">
|
|
185
|
+
<Button type="submit" disabled={saving} className="gap-2">
|
|
186
|
+
{saving ? (
|
|
187
|
+
<>
|
|
188
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
189
|
+
Saving...
|
|
190
|
+
</>
|
|
191
|
+
) : (
|
|
192
|
+
<>
|
|
193
|
+
<Save className="h-4 w-4" />
|
|
194
|
+
Save Localization
|
|
195
|
+
</>
|
|
196
|
+
)}
|
|
197
|
+
</Button>
|
|
198
|
+
</CardFooter>
|
|
199
|
+
</Card>
|
|
200
|
+
</form>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
@@ -0,0 +1,164 @@
|
|
|
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 { Database, HardDrive, CalendarClock, Loader2, Save } from 'lucide-react';
|
|
9
|
+
import { toast } from 'sonner';
|
|
10
|
+
import { updateOrganizationSettings } from '@/lib/actions/system-settings';
|
|
11
|
+
import type { RetentionSettings } from '@/lib/validators/system-settings';
|
|
12
|
+
|
|
13
|
+
interface RetentionFormProps {
|
|
14
|
+
initialData: RetentionSettings;
|
|
15
|
+
organizationId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function RetentionForm({ initialData, organizationId }: RetentionFormProps) {
|
|
19
|
+
const [formData, setFormData] = useState<RetentionSettings>(initialData);
|
|
20
|
+
const [changeReason, setChangeReason] = useState('');
|
|
21
|
+
const [saving, setSaving] = useState(false);
|
|
22
|
+
|
|
23
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
setSaving(true);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const res = await updateOrganizationSettings({
|
|
29
|
+
organizationId,
|
|
30
|
+
section: 'retention',
|
|
31
|
+
settings: formData,
|
|
32
|
+
changeReason: changeReason.trim() || 'Updated data retention and operational thresholds',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (res.success) {
|
|
36
|
+
toast.success('Data retention settings saved successfully');
|
|
37
|
+
setChangeReason('');
|
|
38
|
+
} else {
|
|
39
|
+
toast.error(res.error || 'Failed to update retention settings');
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
toast.error('An unexpected error occurred');
|
|
43
|
+
} finally {
|
|
44
|
+
setSaving(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<form onSubmit={handleSubmit}>
|
|
50
|
+
<Card>
|
|
51
|
+
<CardHeader>
|
|
52
|
+
<CardTitle className="flex items-center gap-2 text-xl">
|
|
53
|
+
<Database className="h-5 w-5 text-primary" />
|
|
54
|
+
Data Retention & Operational Windows
|
|
55
|
+
</CardTitle>
|
|
56
|
+
<CardDescription>
|
|
57
|
+
Configure legal audit trail archiving periods, PM notification lead times, and attachment quotas.
|
|
58
|
+
</CardDescription>
|
|
59
|
+
</CardHeader>
|
|
60
|
+
|
|
61
|
+
<CardContent className="space-y-6">
|
|
62
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
63
|
+
{/* Audit Log Retention */}
|
|
64
|
+
<div className="space-y-2">
|
|
65
|
+
<Label htmlFor="auditRetention" className="flex items-center gap-2">
|
|
66
|
+
<Database className="h-4 w-4 text-muted-foreground" />
|
|
67
|
+
Audit Trail Retention (Days)
|
|
68
|
+
</Label>
|
|
69
|
+
<Input
|
|
70
|
+
id="auditRetention"
|
|
71
|
+
type="number"
|
|
72
|
+
min={30}
|
|
73
|
+
max={3650}
|
|
74
|
+
value={formData.auditLogRetentionDays}
|
|
75
|
+
onChange={(e) =>
|
|
76
|
+
setFormData((prev) => ({ ...prev, auditLogRetentionDays: parseInt(e.target.value) || 365 }))
|
|
77
|
+
}
|
|
78
|
+
/>
|
|
79
|
+
<p className="text-xs text-muted-foreground">
|
|
80
|
+
Minimum duration immutable audit records are preserved for regulatory inspection (365 days = 1 year).
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* PM Alert Window */}
|
|
85
|
+
<div className="space-y-2">
|
|
86
|
+
<Label htmlFor="pmAlertWindow" className="flex items-center gap-2">
|
|
87
|
+
<CalendarClock className="h-4 w-4 text-muted-foreground" />
|
|
88
|
+
Preventive Maintenance Alert Window (Days)
|
|
89
|
+
</Label>
|
|
90
|
+
<Input
|
|
91
|
+
id="pmAlertWindow"
|
|
92
|
+
type="number"
|
|
93
|
+
min={1}
|
|
94
|
+
max={90}
|
|
95
|
+
value={formData.pmAlertWindowDays}
|
|
96
|
+
onChange={(e) =>
|
|
97
|
+
setFormData((prev) => ({ ...prev, pmAlertWindowDays: parseInt(e.target.value) || 14 }))
|
|
98
|
+
}
|
|
99
|
+
/>
|
|
100
|
+
<p className="text-xs text-muted-foreground">
|
|
101
|
+
Advance notification notice before scheduled PM occurrences become due (standard: 14 days).
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Attachment Quota */}
|
|
106
|
+
<div className="space-y-2">
|
|
107
|
+
<Label htmlFor="attachmentQuota" className="flex items-center gap-2">
|
|
108
|
+
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
|
109
|
+
Facility Attachment Storage Quota (MB)
|
|
110
|
+
</Label>
|
|
111
|
+
<Input
|
|
112
|
+
id="attachmentQuota"
|
|
113
|
+
type="number"
|
|
114
|
+
min={100}
|
|
115
|
+
max={100000}
|
|
116
|
+
value={formData.attachmentQuotaMb}
|
|
117
|
+
onChange={(e) =>
|
|
118
|
+
setFormData((prev) => ({ ...prev, attachmentQuotaMb: parseInt(e.target.value) || 5000 }))
|
|
119
|
+
}
|
|
120
|
+
/>
|
|
121
|
+
<p className="text-xs text-muted-foreground">
|
|
122
|
+
Total storage allocation for manuals, service sheets, and photo evidence (5000 MB = 5 GB).
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Audit Trace Reason */}
|
|
128
|
+
<div className="space-y-2 pt-2 border-t">
|
|
129
|
+
<Label htmlFor="changeReason" className="text-sm font-medium">
|
|
130
|
+
Change Reason (21 CFR Part 11 Audit Trace)
|
|
131
|
+
</Label>
|
|
132
|
+
<Input
|
|
133
|
+
id="changeReason"
|
|
134
|
+
placeholder="e.g., Clinical engineering policy update on PM warning lead times"
|
|
135
|
+
value={changeReason}
|
|
136
|
+
onChange={(e) => setChangeReason(e.target.value)}
|
|
137
|
+
className="max-w-xl"
|
|
138
|
+
/>
|
|
139
|
+
<p className="text-xs text-muted-foreground">
|
|
140
|
+
Required rationale recorded in the immutable audit log upon modification.
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
</CardContent>
|
|
144
|
+
|
|
145
|
+
<CardFooter className="flex justify-end border-t pt-4">
|
|
146
|
+
<Button type="submit" disabled={saving} className="gap-2">
|
|
147
|
+
{saving ? (
|
|
148
|
+
<>
|
|
149
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
150
|
+
Saving...
|
|
151
|
+
</>
|
|
152
|
+
) : (
|
|
153
|
+
<>
|
|
154
|
+
<Save className="h-4 w-4" />
|
|
155
|
+
Save Retention Settings
|
|
156
|
+
</>
|
|
157
|
+
)}
|
|
158
|
+
</Button>
|
|
159
|
+
</CardFooter>
|
|
160
|
+
</Card>
|
|
161
|
+
</form>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|