@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,186 @@
|
|
|
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 { ShieldCheck, Lock, KeyRound, AlertTriangle, Loader2, Save } from 'lucide-react';
|
|
10
|
+
import { toast } from 'sonner';
|
|
11
|
+
import { updateOrganizationSettings } from '@/lib/actions/system-settings';
|
|
12
|
+
import type { SecuritySettings } from '@/lib/validators/system-settings';
|
|
13
|
+
|
|
14
|
+
interface SecurityFormProps {
|
|
15
|
+
initialData: SecuritySettings;
|
|
16
|
+
organizationId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function SecurityForm({ initialData, organizationId }: SecurityFormProps) {
|
|
20
|
+
const [formData, setFormData] = useState<SecuritySettings>(initialData);
|
|
21
|
+
const [changeReason, setChangeReason] = useState('');
|
|
22
|
+
const [saving, setSaving] = useState(false);
|
|
23
|
+
|
|
24
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
setSaving(true);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const res = await updateOrganizationSettings({
|
|
30
|
+
organizationId,
|
|
31
|
+
section: 'security',
|
|
32
|
+
settings: formData,
|
|
33
|
+
changeReason: changeReason.trim() || 'Updated security and compliance policies',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (res.success) {
|
|
37
|
+
toast.success('Security settings saved successfully');
|
|
38
|
+
setChangeReason('');
|
|
39
|
+
} else {
|
|
40
|
+
toast.error(res.error || 'Failed to update security settings');
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
toast.error('An unexpected error occurred');
|
|
44
|
+
} finally {
|
|
45
|
+
setSaving(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<form onSubmit={handleSubmit}>
|
|
51
|
+
<Card>
|
|
52
|
+
<CardHeader>
|
|
53
|
+
<CardTitle className="flex items-center gap-2 text-xl">
|
|
54
|
+
<ShieldCheck className="h-5 w-5 text-primary" />
|
|
55
|
+
Security & 21 CFR Part 11 Compliance
|
|
56
|
+
</CardTitle>
|
|
57
|
+
<CardDescription>
|
|
58
|
+
Configure authentication thresholds, inactivity timeouts, and electronic signature safeguards.
|
|
59
|
+
</CardDescription>
|
|
60
|
+
</CardHeader>
|
|
61
|
+
|
|
62
|
+
<CardContent className="space-y-6">
|
|
63
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
64
|
+
{/* Session Timeout */}
|
|
65
|
+
<div className="space-y-2">
|
|
66
|
+
<Label htmlFor="sessionTimeout" className="flex items-center gap-2">
|
|
67
|
+
<Lock className="h-4 w-4 text-muted-foreground" />
|
|
68
|
+
Session Inactivity Timeout (Minutes)
|
|
69
|
+
</Label>
|
|
70
|
+
<Input
|
|
71
|
+
id="sessionTimeout"
|
|
72
|
+
type="number"
|
|
73
|
+
min={5}
|
|
74
|
+
max={1440}
|
|
75
|
+
value={formData.sessionTimeoutMinutes}
|
|
76
|
+
onChange={(e) =>
|
|
77
|
+
setFormData((prev) => ({ ...prev, sessionTimeoutMinutes: parseInt(e.target.value) || 30 }))
|
|
78
|
+
}
|
|
79
|
+
/>
|
|
80
|
+
<p className="text-xs text-muted-foreground">
|
|
81
|
+
Inactivity duration before active user sessions are terminated automatically (standard: 15-30m).
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Password Expiry */}
|
|
86
|
+
<div className="space-y-2">
|
|
87
|
+
<Label htmlFor="passwordExpiry" className="flex items-center gap-2">
|
|
88
|
+
<KeyRound className="h-4 w-4 text-muted-foreground" />
|
|
89
|
+
Password Expiration Policy (Days)
|
|
90
|
+
</Label>
|
|
91
|
+
<Input
|
|
92
|
+
id="passwordExpiry"
|
|
93
|
+
type="number"
|
|
94
|
+
min={0}
|
|
95
|
+
max={365}
|
|
96
|
+
value={formData.passwordExpiryDays}
|
|
97
|
+
onChange={(e) =>
|
|
98
|
+
setFormData((prev) => ({ ...prev, passwordExpiryDays: parseInt(e.target.value) || 0 }))
|
|
99
|
+
}
|
|
100
|
+
/>
|
|
101
|
+
<p className="text-xs text-muted-foreground">
|
|
102
|
+
Set to 0 to disable automated expiry, or specify hospital policy interval (e.g., 90 days).
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Max Login Attempts */}
|
|
107
|
+
<div className="space-y-2">
|
|
108
|
+
<Label htmlFor="maxAttempts" className="flex items-center gap-2">
|
|
109
|
+
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
|
|
110
|
+
Max Failed Login Attempts
|
|
111
|
+
</Label>
|
|
112
|
+
<Input
|
|
113
|
+
id="maxAttempts"
|
|
114
|
+
type="number"
|
|
115
|
+
min={3}
|
|
116
|
+
max={10}
|
|
117
|
+
value={formData.maxLoginAttempts}
|
|
118
|
+
onChange={(e) =>
|
|
119
|
+
setFormData((prev) => ({ ...prev, maxLoginAttempts: parseInt(e.target.value) || 5 }))
|
|
120
|
+
}
|
|
121
|
+
/>
|
|
122
|
+
<p className="text-xs text-muted-foreground">
|
|
123
|
+
Account lockout threshold to prevent brute-force attacks.
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Re-auth for Signatures */}
|
|
128
|
+
<div className="flex flex-col justify-between space-y-2 p-4 border rounded-lg bg-muted/20">
|
|
129
|
+
<div className="flex items-center justify-between">
|
|
130
|
+
<div className="space-y-0.5 pr-4">
|
|
131
|
+
<Label htmlFor="requireReAuth" className="font-medium text-sm">
|
|
132
|
+
Re-Authenticate for Electronic Signatures
|
|
133
|
+
</Label>
|
|
134
|
+
<p className="text-xs text-muted-foreground">
|
|
135
|
+
Enforces password/PIN re-entry when signing maintenance release and calibration records (FDA 21 CFR Part 11).
|
|
136
|
+
</p>
|
|
137
|
+
</div>
|
|
138
|
+
<Switch
|
|
139
|
+
id="requireReAuth"
|
|
140
|
+
checked={formData.requireReAuthForSignatures}
|
|
141
|
+
onCheckedChange={(val) =>
|
|
142
|
+
setFormData((prev) => ({ ...prev, requireReAuthForSignatures: val }))
|
|
143
|
+
}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Audit Trace Reason */}
|
|
150
|
+
<div className="space-y-2 pt-2 border-t">
|
|
151
|
+
<Label htmlFor="changeReason" className="text-sm font-medium">
|
|
152
|
+
Change Reason (21 CFR Part 11 Audit Trace)
|
|
153
|
+
</Label>
|
|
154
|
+
<Input
|
|
155
|
+
id="changeReason"
|
|
156
|
+
placeholder="e.g., Hospital IT security policy annual review"
|
|
157
|
+
value={changeReason}
|
|
158
|
+
onChange={(e) => setChangeReason(e.target.value)}
|
|
159
|
+
className="max-w-xl"
|
|
160
|
+
/>
|
|
161
|
+
<p className="text-xs text-muted-foreground">
|
|
162
|
+
Required reason for modifying clinical and infrastructure security parameters.
|
|
163
|
+
</p>
|
|
164
|
+
</div>
|
|
165
|
+
</CardContent>
|
|
166
|
+
|
|
167
|
+
<CardFooter className="flex justify-end border-t pt-4">
|
|
168
|
+
<Button type="submit" disabled={saving} className="gap-2">
|
|
169
|
+
{saving ? (
|
|
170
|
+
<>
|
|
171
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
172
|
+
Saving...
|
|
173
|
+
</>
|
|
174
|
+
) : (
|
|
175
|
+
<>
|
|
176
|
+
<Save className="h-4 w-4" />
|
|
177
|
+
Save Security Settings
|
|
178
|
+
</>
|
|
179
|
+
)}
|
|
180
|
+
</Button>
|
|
181
|
+
</CardFooter>
|
|
182
|
+
</Card>
|
|
183
|
+
</form>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
4
|
+
import { Globe, ShieldCheck, Database, Activity, Sliders } from 'lucide-react';
|
|
5
|
+
import { LocalizationForm } from './settings-forms/localization-form';
|
|
6
|
+
import { SecurityForm } from './settings-forms/security-form';
|
|
7
|
+
import { RetentionForm } from './settings-forms/retention-form';
|
|
8
|
+
import { HealthPanel } from './settings-forms/health-panel';
|
|
9
|
+
import { FeaturesForm } from './settings-forms/features-form';
|
|
10
|
+
import type { SystemSettings } from '@/lib/validators/system-settings';
|
|
11
|
+
import type { SystemHealthData } from '@/lib/actions/system-settings';
|
|
12
|
+
|
|
13
|
+
interface SystemSettingsTabsProps {
|
|
14
|
+
settings: SystemSettings;
|
|
15
|
+
organizationId: string;
|
|
16
|
+
organizationName?: string;
|
|
17
|
+
healthData: SystemHealthData;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function SystemSettingsTabs({
|
|
21
|
+
settings,
|
|
22
|
+
organizationId,
|
|
23
|
+
organizationName,
|
|
24
|
+
healthData,
|
|
25
|
+
}: SystemSettingsTabsProps) {
|
|
26
|
+
return (
|
|
27
|
+
<div className="space-y-6">
|
|
28
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
|
29
|
+
<div>
|
|
30
|
+
<h1 className="text-2xl font-bold tracking-tight">System Settings</h1>
|
|
31
|
+
<p className="text-sm text-muted-foreground">
|
|
32
|
+
Configure system policies, localization, security rules, and data retention
|
|
33
|
+
{organizationName ? ` for ${organizationName}` : ''}.
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<Tabs defaultValue="localization" className="space-y-6">
|
|
39
|
+
<TabsList className="grid grid-cols-2 md:grid-cols-5 w-full h-auto p-1 gap-1">
|
|
40
|
+
<TabsTrigger value="localization" className="flex items-center gap-2 py-2">
|
|
41
|
+
<Globe className="h-4 w-4" />
|
|
42
|
+
<span className="hidden sm:inline">Localization</span>
|
|
43
|
+
<span className="sm:hidden">Locale</span>
|
|
44
|
+
</TabsTrigger>
|
|
45
|
+
<TabsTrigger value="security" className="flex items-center gap-2 py-2">
|
|
46
|
+
<ShieldCheck className="h-4 w-4" />
|
|
47
|
+
<span>Security</span>
|
|
48
|
+
</TabsTrigger>
|
|
49
|
+
<TabsTrigger value="retention" className="flex items-center gap-2 py-2">
|
|
50
|
+
<Database className="h-4 w-4" />
|
|
51
|
+
<span className="hidden sm:inline">Data Retention</span>
|
|
52
|
+
<span className="sm:hidden">Retention</span>
|
|
53
|
+
</TabsTrigger>
|
|
54
|
+
<TabsTrigger value="features" className="flex items-center gap-2 py-2">
|
|
55
|
+
<Sliders className="h-4 w-4" />
|
|
56
|
+
<span>Features</span>
|
|
57
|
+
</TabsTrigger>
|
|
58
|
+
<TabsTrigger value="health" className="flex items-center gap-2 py-2">
|
|
59
|
+
<Activity className="h-4 w-4" />
|
|
60
|
+
<span className="hidden sm:inline">System Health</span>
|
|
61
|
+
<span className="sm:hidden">Health</span>
|
|
62
|
+
</TabsTrigger>
|
|
63
|
+
</TabsList>
|
|
64
|
+
|
|
65
|
+
<TabsContent value="localization" className="space-y-4">
|
|
66
|
+
<LocalizationForm initialData={settings.localization} organizationId={organizationId} />
|
|
67
|
+
</TabsContent>
|
|
68
|
+
|
|
69
|
+
<TabsContent value="security" className="space-y-4">
|
|
70
|
+
<SecurityForm initialData={settings.security} organizationId={organizationId} />
|
|
71
|
+
</TabsContent>
|
|
72
|
+
|
|
73
|
+
<TabsContent value="retention" className="space-y-4">
|
|
74
|
+
<RetentionForm initialData={settings.retention} organizationId={organizationId} />
|
|
75
|
+
</TabsContent>
|
|
76
|
+
|
|
77
|
+
<TabsContent value="features" className="space-y-4">
|
|
78
|
+
<FeaturesForm initialData={settings.features} organizationId={organizationId} />
|
|
79
|
+
</TabsContent>
|
|
80
|
+
|
|
81
|
+
<TabsContent value="health" className="space-y-4">
|
|
82
|
+
<HealthPanel initialData={healthData} />
|
|
83
|
+
</TabsContent>
|
|
84
|
+
</Tabs>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
DialogTrigger,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
} from "@/components/ui/dialog";
|
|
14
|
+
import { Button } from "@/components/ui/button";
|
|
15
|
+
import { Label } from "@/components/ui/label";
|
|
16
|
+
import { Plus, Trash2, Loader2, ShieldCheck, AlertCircle } from "lucide-react";
|
|
17
|
+
import { assignUserRole, revokeUserRole } from "@/lib/actions/users";
|
|
18
|
+
|
|
19
|
+
interface RoleOption {
|
|
20
|
+
id: string;
|
|
21
|
+
code: string;
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface AssignedRole {
|
|
27
|
+
id: string;
|
|
28
|
+
assignmentId: string;
|
|
29
|
+
code: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description?: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface UserRoleDialogProps {
|
|
35
|
+
userId: string;
|
|
36
|
+
assignedRoles: AssignedRole[];
|
|
37
|
+
availableRoles: RoleOption[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function UserRoleDialog({
|
|
41
|
+
userId,
|
|
42
|
+
assignedRoles,
|
|
43
|
+
availableRoles,
|
|
44
|
+
}: UserRoleDialogProps) {
|
|
45
|
+
const router = useRouter();
|
|
46
|
+
|
|
47
|
+
// Assign Role State
|
|
48
|
+
const [assignOpen, setAssignOpen] = useState(false);
|
|
49
|
+
const [selectedRoleId, setSelectedRoleId] = useState("");
|
|
50
|
+
const [assignLoading, setAssignLoading] = useState(false);
|
|
51
|
+
const [assignError, setAssignError] = useState("");
|
|
52
|
+
|
|
53
|
+
// Revoke Role State
|
|
54
|
+
const [revokeOpen, setRevokeOpen] = useState(false);
|
|
55
|
+
const [roleToRevoke, setRoleToRevoke] = useState<AssignedRole | null>(null);
|
|
56
|
+
const [revokeLoading, setRevokeLoading] = useState(false);
|
|
57
|
+
const [revokeError, setRevokeError] = useState("");
|
|
58
|
+
|
|
59
|
+
// Filter out roles already assigned
|
|
60
|
+
const unassignedRoles = availableRoles.filter(
|
|
61
|
+
(role) => !assignedRoles.some((ar) => ar.id === role.id || ar.code === role.code)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const handleAssignRole = async (e: React.FormEvent) => {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
if (!selectedRoleId) {
|
|
67
|
+
setAssignError("Please select a role to assign.");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setAssignLoading(true);
|
|
72
|
+
setAssignError("");
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const result = await assignUserRole({
|
|
76
|
+
userId,
|
|
77
|
+
roleId: selectedRoleId,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (result.success) {
|
|
81
|
+
setAssignOpen(false);
|
|
82
|
+
setSelectedRoleId("");
|
|
83
|
+
router.refresh();
|
|
84
|
+
} else {
|
|
85
|
+
setAssignError((result as any)?.error || "Failed to assign role");
|
|
86
|
+
}
|
|
87
|
+
} catch (err: any) {
|
|
88
|
+
setAssignError(err?.message || "An unexpected error occurred");
|
|
89
|
+
} finally {
|
|
90
|
+
setAssignLoading(false);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleRevokeRole = async () => {
|
|
95
|
+
if (!roleToRevoke) return;
|
|
96
|
+
|
|
97
|
+
setRevokeLoading(true);
|
|
98
|
+
setRevokeError("");
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const result = await revokeUserRole(roleToRevoke.assignmentId);
|
|
102
|
+
|
|
103
|
+
if (result.success) {
|
|
104
|
+
setRevokeOpen(false);
|
|
105
|
+
setRoleToRevoke(null);
|
|
106
|
+
router.refresh();
|
|
107
|
+
} else {
|
|
108
|
+
setRevokeError((result as any)?.error || "Failed to revoke role");
|
|
109
|
+
}
|
|
110
|
+
} catch (err: any) {
|
|
111
|
+
setRevokeError(err?.message || "An unexpected error occurred");
|
|
112
|
+
} finally {
|
|
113
|
+
setRevokeLoading(false);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="space-y-4">
|
|
119
|
+
{/* ── Assign Role Trigger ── */}
|
|
120
|
+
<div className="flex justify-end">
|
|
121
|
+
<Dialog open={assignOpen} onOpenChange={setAssignOpen}>
|
|
122
|
+
<DialogTrigger asChild>
|
|
123
|
+
<Button size="sm" className="gap-1.5 min-h-[36px]">
|
|
124
|
+
<Plus className="w-4 h-4" />
|
|
125
|
+
Assign Role
|
|
126
|
+
</Button>
|
|
127
|
+
</DialogTrigger>
|
|
128
|
+
<DialogContent className="sm:max-w-[450px]">
|
|
129
|
+
<form onSubmit={handleAssignRole} className="space-y-4">
|
|
130
|
+
<DialogHeader>
|
|
131
|
+
<DialogTitle className="flex items-center gap-2">
|
|
132
|
+
<ShieldCheck className="w-5 h-5 text-primary" />
|
|
133
|
+
Assign System Role
|
|
134
|
+
</DialogTitle>
|
|
135
|
+
<DialogDescription className="text-xs">
|
|
136
|
+
Grant operational or administrative access permissions to this user.
|
|
137
|
+
</DialogDescription>
|
|
138
|
+
</DialogHeader>
|
|
139
|
+
|
|
140
|
+
{assignError && (
|
|
141
|
+
<div className="bg-destructive/10 text-destructive text-xs p-2.5 rounded-md border border-destructive/20">
|
|
142
|
+
{assignError}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
<div className="space-y-2">
|
|
147
|
+
<Label htmlFor="role-select" className="text-xs font-medium">
|
|
148
|
+
Select Role *
|
|
149
|
+
</Label>
|
|
150
|
+
{unassignedRoles.length > 0 ? (
|
|
151
|
+
<select
|
|
152
|
+
id="role-select"
|
|
153
|
+
value={selectedRoleId}
|
|
154
|
+
onChange={(e) => setSelectedRoleId(e.target.value)}
|
|
155
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
156
|
+
required
|
|
157
|
+
>
|
|
158
|
+
<option value="">Choose a role...</option>
|
|
159
|
+
{unassignedRoles.map((role) => (
|
|
160
|
+
<option key={role.id} value={role.id}>
|
|
161
|
+
{role.name} ({role.code})
|
|
162
|
+
</option>
|
|
163
|
+
))}
|
|
164
|
+
</select>
|
|
165
|
+
) : (
|
|
166
|
+
<p className="text-xs text-muted-foreground italic p-2 border rounded bg-muted/20">
|
|
167
|
+
All available system roles have already been assigned to this user.
|
|
168
|
+
</p>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{selectedRoleId && (
|
|
172
|
+
<p className="text-xs text-muted-foreground pt-1">
|
|
173
|
+
{unassignedRoles.find((r) => r.id === selectedRoleId)?.description}
|
|
174
|
+
</p>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<DialogFooter className="gap-2 sm:gap-0 pt-2">
|
|
179
|
+
<Button
|
|
180
|
+
type="button"
|
|
181
|
+
variant="outline"
|
|
182
|
+
size="sm"
|
|
183
|
+
onClick={() => setAssignOpen(false)}
|
|
184
|
+
disabled={assignLoading}
|
|
185
|
+
>
|
|
186
|
+
Cancel
|
|
187
|
+
</Button>
|
|
188
|
+
<Button
|
|
189
|
+
type="submit"
|
|
190
|
+
size="sm"
|
|
191
|
+
disabled={assignLoading || unassignedRoles.length === 0}
|
|
192
|
+
>
|
|
193
|
+
{assignLoading && <Loader2 className="w-3.5 h-3.5 mr-1.5 animate-spin" />}
|
|
194
|
+
Confirm Assignment
|
|
195
|
+
</Button>
|
|
196
|
+
</DialogFooter>
|
|
197
|
+
</form>
|
|
198
|
+
</DialogContent>
|
|
199
|
+
</Dialog>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{/* ── Assigned Roles List ── */}
|
|
203
|
+
{assignedRoles.length > 0 ? (
|
|
204
|
+
<div className="space-y-3">
|
|
205
|
+
{assignedRoles.map((role) => (
|
|
206
|
+
<div
|
|
207
|
+
key={role.assignmentId || role.id}
|
|
208
|
+
className="flex items-center justify-between p-3.5 rounded-lg border bg-card hover:bg-muted/20 transition-colors"
|
|
209
|
+
>
|
|
210
|
+
<div className="space-y-0.5 min-w-0 pr-4">
|
|
211
|
+
<div className="flex items-center gap-2">
|
|
212
|
+
<span className="font-semibold text-sm">{role.name}</span>
|
|
213
|
+
<span className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded bg-muted text-muted-foreground border">
|
|
214
|
+
{role.code}
|
|
215
|
+
</span>
|
|
216
|
+
</div>
|
|
217
|
+
{role.description && (
|
|
218
|
+
<p className="text-xs text-muted-foreground truncate">{role.description}</p>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<Button
|
|
223
|
+
type="button"
|
|
224
|
+
variant="ghost"
|
|
225
|
+
size="sm"
|
|
226
|
+
className="text-red-500 hover:text-red-700 hover:bg-red-50 shrink-0 h-8 gap-1 text-xs"
|
|
227
|
+
onClick={() => {
|
|
228
|
+
setRoleToRevoke(role);
|
|
229
|
+
setRevokeOpen(true);
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
<Trash2 className="w-3.5 h-3.5" />
|
|
233
|
+
Revoke
|
|
234
|
+
</Button>
|
|
235
|
+
</div>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
) : (
|
|
239
|
+
<div className="text-center py-8 text-sm text-muted-foreground border border-dashed rounded-lg">
|
|
240
|
+
No roles assigned yet. Click "Assign Role" above to grant permissions.
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
|
|
244
|
+
{/* ── Revoke Confirmation Dialog ── */}
|
|
245
|
+
<Dialog open={revokeOpen} onOpenChange={setRevokeOpen}>
|
|
246
|
+
<DialogContent className="sm:max-w-[420px]">
|
|
247
|
+
<DialogHeader>
|
|
248
|
+
<div className="flex items-center gap-2">
|
|
249
|
+
<div className="p-2 rounded-full bg-red-100 text-red-600">
|
|
250
|
+
<AlertCircle className="w-5 h-5" />
|
|
251
|
+
</div>
|
|
252
|
+
<DialogTitle>Revoke Role</DialogTitle>
|
|
253
|
+
</div>
|
|
254
|
+
<DialogDescription className="text-xs pt-1">
|
|
255
|
+
Are you sure you want to revoke the{" "}
|
|
256
|
+
<strong className="text-foreground">{roleToRevoke?.name}</strong> role? The user will
|
|
257
|
+
immediately lose the privileges associated with this role.
|
|
258
|
+
</DialogDescription>
|
|
259
|
+
</DialogHeader>
|
|
260
|
+
|
|
261
|
+
{revokeError && (
|
|
262
|
+
<div className="bg-destructive/10 text-destructive text-xs p-2.5 rounded-md border border-destructive/20">
|
|
263
|
+
{revokeError}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
<DialogFooter className="gap-2 sm:gap-0 pt-2">
|
|
268
|
+
<Button
|
|
269
|
+
type="button"
|
|
270
|
+
variant="outline"
|
|
271
|
+
size="sm"
|
|
272
|
+
onClick={() => setRevokeOpen(false)}
|
|
273
|
+
disabled={revokeLoading}
|
|
274
|
+
>
|
|
275
|
+
Cancel
|
|
276
|
+
</Button>
|
|
277
|
+
<Button
|
|
278
|
+
type="button"
|
|
279
|
+
variant="destructive"
|
|
280
|
+
size="sm"
|
|
281
|
+
onClick={handleRevokeRole}
|
|
282
|
+
disabled={revokeLoading}
|
|
283
|
+
>
|
|
284
|
+
{revokeLoading && <Loader2 className="w-3.5 h-3.5 mr-1.5 animate-spin" />}
|
|
285
|
+
Confirm Revocation
|
|
286
|
+
</Button>
|
|
287
|
+
</DialogFooter>
|
|
288
|
+
</DialogContent>
|
|
289
|
+
</Dialog>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|