@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,82 @@
|
|
|
1
|
+
import { PageHeader } from "@/components/shared/page-header";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
4
|
+
import { StatusBadge } from "@/components/shared/status-badge";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { getOrganizationById } from "@/lib/actions/organizations";
|
|
7
|
+
import { notFound } from "next/navigation";
|
|
8
|
+
|
|
9
|
+
export default async function OrganizationDetailPage({ params }: { params: Promise<{ orgId: string }> }) {
|
|
10
|
+
const { orgId } = await params;
|
|
11
|
+
const result = await getOrganizationById(orgId);
|
|
12
|
+
|
|
13
|
+
if (!result.success || !result.data) {
|
|
14
|
+
notFound();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const org = result.data;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="space-y-6">
|
|
21
|
+
<PageHeader title={org.name} description="Organization details">
|
|
22
|
+
<Button variant="outline" asChild>
|
|
23
|
+
<Link href={`/admin/organizations/${org.id}/edit`}>Edit</Link>
|
|
24
|
+
</Button>
|
|
25
|
+
<Button variant="destructive">Archive</Button>
|
|
26
|
+
</PageHeader>
|
|
27
|
+
|
|
28
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
29
|
+
<Card>
|
|
30
|
+
<CardHeader>
|
|
31
|
+
<CardTitle>Basic Information</CardTitle>
|
|
32
|
+
</CardHeader>
|
|
33
|
+
<CardContent className="space-y-4">
|
|
34
|
+
<div>
|
|
35
|
+
<div className="text-sm font-medium text-muted-foreground">Code</div>
|
|
36
|
+
<div>{org.code}</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div>
|
|
39
|
+
<div className="text-sm font-medium text-muted-foreground">Status</div>
|
|
40
|
+
<div className="mt-1">
|
|
41
|
+
<StatusBadge status={org.status} />
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div>
|
|
45
|
+
<div className="text-sm font-medium text-muted-foreground">Timezone</div>
|
|
46
|
+
<div>{org.defaultTimezone}</div>
|
|
47
|
+
</div>
|
|
48
|
+
</CardContent>
|
|
49
|
+
</Card>
|
|
50
|
+
|
|
51
|
+
<Card>
|
|
52
|
+
<CardHeader>
|
|
53
|
+
<CardTitle>Linked Hospitals</CardTitle>
|
|
54
|
+
</CardHeader>
|
|
55
|
+
<CardContent>
|
|
56
|
+
<div className="text-3xl font-bold">0</div>
|
|
57
|
+
<p className="text-sm text-muted-foreground mt-1">Total hospitals</p>
|
|
58
|
+
<Button variant="link" className="px-0 mt-4" asChild>
|
|
59
|
+
<Link href={`/admin/hospitals?orgId=${org.id}`}>View Hospitals →</Link>
|
|
60
|
+
</Button>
|
|
61
|
+
</CardContent>
|
|
62
|
+
</Card>
|
|
63
|
+
|
|
64
|
+
<Card>
|
|
65
|
+
<CardHeader>
|
|
66
|
+
<CardTitle>Settings</CardTitle>
|
|
67
|
+
</CardHeader>
|
|
68
|
+
<CardContent className="space-y-4">
|
|
69
|
+
<div>
|
|
70
|
+
<div className="text-sm font-medium text-muted-foreground">Created At</div>
|
|
71
|
+
<div>{new Date(org.createdAt || Date.now()).toLocaleDateString()}</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
<div className="text-sm font-medium text-muted-foreground">Updated At</div>
|
|
75
|
+
<div>{new Date(org.updatedAt || Date.now()).toLocaleDateString()}</div>
|
|
76
|
+
</div>
|
|
77
|
+
</CardContent>
|
|
78
|
+
</Card>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PageHeader } from "@/components/shared/page-header";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import {
|
|
6
|
+
Form,
|
|
7
|
+
FormControl,
|
|
8
|
+
FormField,
|
|
9
|
+
FormItem,
|
|
10
|
+
FormLabel,
|
|
11
|
+
FormMessage,
|
|
12
|
+
} from "@/components/ui/form";
|
|
13
|
+
import { Input } from "@/components/ui/input";
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from "@/components/ui/select";
|
|
21
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
22
|
+
import { useForm } from "react-hook-form";
|
|
23
|
+
import * as z from "zod";
|
|
24
|
+
import { useRouter } from "next/navigation";
|
|
25
|
+
import { createOrganization } from "@/lib/actions/organizations";
|
|
26
|
+
|
|
27
|
+
const formSchema = z.object({
|
|
28
|
+
name: z.string().min(2, "Name must be at least 2 characters"),
|
|
29
|
+
code: z.string().min(2, "Code must be at least 2 characters").toUpperCase(),
|
|
30
|
+
defaultTimezone: z.string().min(1, "Please select a timezone"),
|
|
31
|
+
status: z.enum(["active", "archived"]),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export default function NewOrganizationPage() {
|
|
35
|
+
const router = useRouter();
|
|
36
|
+
|
|
37
|
+
const form = useForm<z.infer<typeof formSchema>>({
|
|
38
|
+
resolver: zodResolver(formSchema),
|
|
39
|
+
defaultValues: {
|
|
40
|
+
name: "",
|
|
41
|
+
code: "",
|
|
42
|
+
defaultTimezone: "UTC",
|
|
43
|
+
status: "active",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
async function onSubmit(values: z.infer<typeof formSchema>) {
|
|
48
|
+
const result = await createOrganization(values);
|
|
49
|
+
if (result.success) {
|
|
50
|
+
router.push("/admin/organizations");
|
|
51
|
+
} else {
|
|
52
|
+
console.error((result as any).error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="space-y-6 max-w-2xl">
|
|
58
|
+
<PageHeader title="Add Organization" description="Create a new organization" />
|
|
59
|
+
|
|
60
|
+
<Form {...form}>
|
|
61
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
62
|
+
<FormField
|
|
63
|
+
control={form.control}
|
|
64
|
+
name="name"
|
|
65
|
+
render={({ field }) => (
|
|
66
|
+
<FormItem>
|
|
67
|
+
<FormLabel>Name</FormLabel>
|
|
68
|
+
<FormControl>
|
|
69
|
+
<Input placeholder="Acme Healthcare" {...field} />
|
|
70
|
+
</FormControl>
|
|
71
|
+
<FormMessage />
|
|
72
|
+
</FormItem>
|
|
73
|
+
)}
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<FormField
|
|
77
|
+
control={form.control}
|
|
78
|
+
name="code"
|
|
79
|
+
render={({ field }) => (
|
|
80
|
+
<FormItem>
|
|
81
|
+
<FormLabel>Code</FormLabel>
|
|
82
|
+
<FormControl>
|
|
83
|
+
<Input placeholder="ACME" {...field} />
|
|
84
|
+
</FormControl>
|
|
85
|
+
<FormMessage />
|
|
86
|
+
</FormItem>
|
|
87
|
+
)}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<FormField
|
|
91
|
+
control={form.control}
|
|
92
|
+
name="defaultTimezone"
|
|
93
|
+
render={({ field }) => (
|
|
94
|
+
<FormItem>
|
|
95
|
+
<FormLabel>Default Timezone</FormLabel>
|
|
96
|
+
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
97
|
+
<FormControl>
|
|
98
|
+
<SelectTrigger>
|
|
99
|
+
<SelectValue placeholder="Select a timezone" />
|
|
100
|
+
</SelectTrigger>
|
|
101
|
+
</FormControl>
|
|
102
|
+
<SelectContent>
|
|
103
|
+
<SelectItem value="UTC">UTC</SelectItem>
|
|
104
|
+
<SelectItem value="America/New_York">Eastern Time (US & Canada)</SelectItem>
|
|
105
|
+
<SelectItem value="America/Chicago">Central Time (US & Canada)</SelectItem>
|
|
106
|
+
<SelectItem value="America/Denver">Mountain Time (US & Canada)</SelectItem>
|
|
107
|
+
<SelectItem value="America/Los_Angeles">Pacific Time (US & Canada)</SelectItem>
|
|
108
|
+
<SelectItem value="Europe/London">London</SelectItem>
|
|
109
|
+
<SelectItem value="Europe/Paris">Paris</SelectItem>
|
|
110
|
+
</SelectContent>
|
|
111
|
+
</Select>
|
|
112
|
+
<FormMessage />
|
|
113
|
+
</FormItem>
|
|
114
|
+
)}
|
|
115
|
+
/>
|
|
116
|
+
|
|
117
|
+
<FormField
|
|
118
|
+
control={form.control}
|
|
119
|
+
name="status"
|
|
120
|
+
render={({ field }) => (
|
|
121
|
+
<FormItem>
|
|
122
|
+
<FormLabel>Status</FormLabel>
|
|
123
|
+
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
124
|
+
<FormControl>
|
|
125
|
+
<SelectTrigger>
|
|
126
|
+
<SelectValue placeholder="Select status" />
|
|
127
|
+
</SelectTrigger>
|
|
128
|
+
</FormControl>
|
|
129
|
+
<SelectContent>
|
|
130
|
+
<SelectItem value="active">Active</SelectItem>
|
|
131
|
+
<SelectItem value="archived">Archived</SelectItem>
|
|
132
|
+
</SelectContent>
|
|
133
|
+
</Select>
|
|
134
|
+
<FormMessage />
|
|
135
|
+
</FormItem>
|
|
136
|
+
)}
|
|
137
|
+
/>
|
|
138
|
+
|
|
139
|
+
<div className="flex gap-4">
|
|
140
|
+
<Button type="submit">Create Organization</Button>
|
|
141
|
+
<Button type="button" variant="outline" onClick={() => router.back()}>
|
|
142
|
+
Cancel
|
|
143
|
+
</Button>
|
|
144
|
+
</div>
|
|
145
|
+
</form>
|
|
146
|
+
</Form>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import { PageHeader } from "@/components/shared/page-header";
|
|
4
|
+
import { AdminAlerts } from "@/components/dashboard/admin-alerts";
|
|
5
|
+
import { getAdminAlerts, getAdminDashboardStats } from "@/lib/actions/dashboard";
|
|
6
|
+
import { getAuditLogs } from "@/lib/actions/audit-logs";
|
|
7
|
+
import Link from "next/link";
|
|
8
|
+
import {
|
|
9
|
+
Building2,
|
|
10
|
+
Hospital,
|
|
11
|
+
Users,
|
|
12
|
+
Shield,
|
|
13
|
+
Layers,
|
|
14
|
+
Cpu,
|
|
15
|
+
UserPlus,
|
|
16
|
+
ShieldCheck,
|
|
17
|
+
FileSpreadsheet,
|
|
18
|
+
History,
|
|
19
|
+
ArrowRight,
|
|
20
|
+
Activity,
|
|
21
|
+
UploadCloud,
|
|
22
|
+
} from "lucide-react";
|
|
23
|
+
import { Badge } from "@/components/ui/badge";
|
|
24
|
+
|
|
25
|
+
export const dynamic = "force-dynamic";
|
|
26
|
+
|
|
27
|
+
export default async function AdminDashboardPage() {
|
|
28
|
+
const [stats, alerts, auditRes] = await Promise.all([
|
|
29
|
+
getAdminDashboardStats(),
|
|
30
|
+
getAdminAlerts(),
|
|
31
|
+
getAuditLogs({ pageSize: 6 }),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const recentLogs = auditRes.success && auditRes.data ? auditRes.data : [];
|
|
35
|
+
|
|
36
|
+
const statCards = [
|
|
37
|
+
{
|
|
38
|
+
title: "Health System",
|
|
39
|
+
value: stats.totalOrganizations.toString(),
|
|
40
|
+
subtitle: "Registered Organization",
|
|
41
|
+
icon: Building2,
|
|
42
|
+
href: "/admin/general",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: "Hospital Campuses",
|
|
46
|
+
value: stats.totalHospitals.toString(),
|
|
47
|
+
subtitle: "Active Facilities",
|
|
48
|
+
icon: Hospital,
|
|
49
|
+
href: "/admin/general",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
title: "Active Users",
|
|
53
|
+
value: `${stats.activeUsers} / ${stats.totalUsers}`,
|
|
54
|
+
subtitle: "Active / Total Accounts",
|
|
55
|
+
icon: Users,
|
|
56
|
+
href: "/admin/users",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
title: "System Roles",
|
|
60
|
+
value: stats.totalRoles.toString(),
|
|
61
|
+
subtitle: "Configured Roles",
|
|
62
|
+
icon: Shield,
|
|
63
|
+
href: "/admin/roles",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: "Departments",
|
|
67
|
+
value: stats.totalDepartments.toString(),
|
|
68
|
+
subtitle: "Operational Units",
|
|
69
|
+
icon: Layers,
|
|
70
|
+
href: "/departments",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
title: "Medical Devices",
|
|
74
|
+
value: stats.totalDevices.toString(),
|
|
75
|
+
subtitle: "Total Registered",
|
|
76
|
+
icon: Cpu,
|
|
77
|
+
href: "/devices",
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="space-y-6">
|
|
83
|
+
<PageHeader
|
|
84
|
+
title="Admin Dashboard"
|
|
85
|
+
description="System configuration, security governance, and operational health"
|
|
86
|
+
>
|
|
87
|
+
<Button variant="outline" asChild>
|
|
88
|
+
<Link href="/admin/users?action=new">
|
|
89
|
+
<UserPlus className="w-4 h-4 mr-2" />
|
|
90
|
+
Add User
|
|
91
|
+
</Link>
|
|
92
|
+
</Button>
|
|
93
|
+
<Button asChild>
|
|
94
|
+
<Link href="/admin/general">
|
|
95
|
+
<Building2 className="w-4 h-4 mr-2" />
|
|
96
|
+
General Settings
|
|
97
|
+
</Link>
|
|
98
|
+
</Button>
|
|
99
|
+
</PageHeader>
|
|
100
|
+
|
|
101
|
+
{/* ── Actionable Alerts ── */}
|
|
102
|
+
<AdminAlerts alerts={alerts} />
|
|
103
|
+
|
|
104
|
+
{/* ── Key System Metrics ── */}
|
|
105
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
|
106
|
+
{statCards.map((stat, index) => (
|
|
107
|
+
<Link key={index} href={stat.href} className="group">
|
|
108
|
+
<Card className="h-full transition-all hover:border-primary/50 hover:shadow-sm">
|
|
109
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
110
|
+
<CardTitle className="text-xs font-medium text-muted-foreground group-hover:text-primary transition-colors">
|
|
111
|
+
{stat.title}
|
|
112
|
+
</CardTitle>
|
|
113
|
+
<stat.icon className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
|
114
|
+
</CardHeader>
|
|
115
|
+
<CardContent>
|
|
116
|
+
<div className="text-2xl font-bold tracking-tight">{stat.value}</div>
|
|
117
|
+
<p className="text-[11px] text-muted-foreground mt-0.5">{stat.subtitle}</p>
|
|
118
|
+
</CardContent>
|
|
119
|
+
</Card>
|
|
120
|
+
</Link>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
125
|
+
{/* ── Recent Activity / Audit Feed ── */}
|
|
126
|
+
<Card className="lg:col-span-2">
|
|
127
|
+
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
|
128
|
+
<div className="space-y-1">
|
|
129
|
+
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
|
130
|
+
<Activity className="w-4 h-4 text-primary" />
|
|
131
|
+
Recent System Audit Activity
|
|
132
|
+
</CardTitle>
|
|
133
|
+
<p className="text-xs text-muted-foreground">
|
|
134
|
+
Immutable record of administrative operations (21 CFR Part 11)
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
<Button variant="ghost" size="sm" asChild>
|
|
138
|
+
<Link href="/admin/audit-logs" className="text-xs flex items-center gap-1">
|
|
139
|
+
View All <ArrowRight className="w-3.5 h-3.5 ml-1" />
|
|
140
|
+
</Link>
|
|
141
|
+
</Button>
|
|
142
|
+
</CardHeader>
|
|
143
|
+
<CardContent>
|
|
144
|
+
{recentLogs.length > 0 ? (
|
|
145
|
+
<div className="divide-y divide-border/60">
|
|
146
|
+
{recentLogs.map((log: any) => {
|
|
147
|
+
const actorName = log.actorUser?.fullName || log.actorUser?.email || "System";
|
|
148
|
+
const dateStr = log.timestamp
|
|
149
|
+
? new Date(log.timestamp).toLocaleString(undefined, {
|
|
150
|
+
month: "short",
|
|
151
|
+
day: "numeric",
|
|
152
|
+
hour: "2-digit",
|
|
153
|
+
minute: "2-digit",
|
|
154
|
+
})
|
|
155
|
+
: "—";
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div key={log.id} className="py-3 flex items-start justify-between gap-4 first:pt-0 last:pb-0">
|
|
159
|
+
<div className="space-y-1 min-w-0">
|
|
160
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
161
|
+
<Badge variant="outline" className="font-mono text-[10px] uppercase">
|
|
162
|
+
{log.actionType}
|
|
163
|
+
</Badge>
|
|
164
|
+
<span className="text-sm font-medium truncate">{actorName}</span>
|
|
165
|
+
<span className="text-xs text-muted-foreground">
|
|
166
|
+
on <span className="font-medium text-foreground">{log.entityType}</span>
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
169
|
+
{log.changeReason && (
|
|
170
|
+
<p className="text-xs text-muted-foreground italic truncate">
|
|
171
|
+
Reason: "{log.changeReason}"
|
|
172
|
+
</p>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
<span className="text-xs text-muted-foreground whitespace-nowrap shrink-0">{dateStr}</span>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
})}
|
|
179
|
+
</div>
|
|
180
|
+
) : (
|
|
181
|
+
<div className="py-8 text-center text-sm text-muted-foreground">
|
|
182
|
+
No administrative activity recorded yet.
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</CardContent>
|
|
186
|
+
</Card>
|
|
187
|
+
|
|
188
|
+
{/* ── Quick Administrative Management ── */}
|
|
189
|
+
<Card>
|
|
190
|
+
<CardHeader className="pb-3">
|
|
191
|
+
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
|
192
|
+
<ShieldCheck className="w-4 h-4 text-primary" />
|
|
193
|
+
Administrative Portals
|
|
194
|
+
</CardTitle>
|
|
195
|
+
<p className="text-xs text-muted-foreground">Quick access to core admin functions</p>
|
|
196
|
+
</CardHeader>
|
|
197
|
+
<CardContent className="space-y-2">
|
|
198
|
+
<Link
|
|
199
|
+
href="/admin/users"
|
|
200
|
+
className="flex items-start gap-3 p-3 rounded-lg border border-border/60 hover:border-primary/50 hover:bg-muted/30 transition-colors"
|
|
201
|
+
>
|
|
202
|
+
<Users className="w-4 h-4 text-primary mt-0.5 shrink-0" />
|
|
203
|
+
<div className="space-y-0.5">
|
|
204
|
+
<div className="text-sm font-medium">User Governance</div>
|
|
205
|
+
<div className="text-xs text-muted-foreground">Manage user accounts, roles & scopes</div>
|
|
206
|
+
</div>
|
|
207
|
+
</Link>
|
|
208
|
+
|
|
209
|
+
<Link
|
|
210
|
+
href="/admin/roles"
|
|
211
|
+
className="flex items-start gap-3 p-3 rounded-lg border border-border/60 hover:border-primary/50 hover:bg-muted/30 transition-colors"
|
|
212
|
+
>
|
|
213
|
+
<Shield className="w-4 h-4 text-primary mt-0.5 shrink-0" />
|
|
214
|
+
<div className="space-y-0.5">
|
|
215
|
+
<div className="text-sm font-medium">Roles & Permissions</div>
|
|
216
|
+
<div className="text-xs text-muted-foreground">Verify RBAC and separation of duties</div>
|
|
217
|
+
</div>
|
|
218
|
+
</Link>
|
|
219
|
+
|
|
220
|
+
<Link
|
|
221
|
+
href="/admin/general"
|
|
222
|
+
className="flex items-start gap-3 p-3 rounded-lg border border-border/60 hover:border-primary/50 hover:bg-muted/30 transition-colors"
|
|
223
|
+
>
|
|
224
|
+
<FileSpreadsheet className="w-4 h-4 text-primary mt-0.5 shrink-0" />
|
|
225
|
+
<div className="space-y-0.5">
|
|
226
|
+
<div className="text-sm font-medium">General & Master Data</div>
|
|
227
|
+
<div className="text-xs text-muted-foreground">Health system, hospitals & device catalogs</div>
|
|
228
|
+
</div>
|
|
229
|
+
</Link>
|
|
230
|
+
|
|
231
|
+
<Link
|
|
232
|
+
href="/admin/data"
|
|
233
|
+
className="flex items-start gap-3 p-3 rounded-lg border border-border/60 hover:border-primary/50 hover:bg-muted/30 transition-colors"
|
|
234
|
+
>
|
|
235
|
+
<UploadCloud className="w-4 h-4 text-primary mt-0.5 shrink-0" />
|
|
236
|
+
<div className="space-y-0.5">
|
|
237
|
+
<div className="text-sm font-medium">Export & Import Center</div>
|
|
238
|
+
<div className="text-xs text-muted-foreground">Bulk CSV/Excel import and dataset backup</div>
|
|
239
|
+
</div>
|
|
240
|
+
</Link>
|
|
241
|
+
|
|
242
|
+
<Link
|
|
243
|
+
href="/admin/audit-logs"
|
|
244
|
+
className="flex items-start gap-3 p-3 rounded-lg border border-border/60 hover:border-primary/50 hover:bg-muted/30 transition-colors"
|
|
245
|
+
>
|
|
246
|
+
<History className="w-4 h-4 text-primary mt-0.5 shrink-0" />
|
|
247
|
+
<div className="space-y-0.5">
|
|
248
|
+
<div className="text-sm font-medium">Audit Trail</div>
|
|
249
|
+
<div className="text-xs text-muted-foreground">Comprehensive compliance logs</div>
|
|
250
|
+
</div>
|
|
251
|
+
</Link>
|
|
252
|
+
</CardContent>
|
|
253
|
+
</Card>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { getPermissionsMatrix } from "@/lib/actions/roles"
|
|
2
|
+
import { PageHeader } from "@/components/shared/page-header"
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
4
|
+
import { Check, X, ShieldAlert, ShieldCheck, Info } from "lucide-react"
|
|
5
|
+
import { Badge } from "@/components/ui/badge"
|
|
6
|
+
|
|
7
|
+
const DEFAULT_ROLE_PERMISSIONS: Record<string, string[]> = {
|
|
8
|
+
SYS_ADMIN: ['DEVICES', 'REPORTS', 'ADMIN', 'AUDIT'],
|
|
9
|
+
ORG_ADMIN: ['DEVICES', 'REPORTS', 'ADMIN', 'AUDIT'],
|
|
10
|
+
HOSP_ADMIN: ['DEVICES', 'REPORTS', 'ADMIN', 'AUDIT'],
|
|
11
|
+
BIOMED_MGR: ['DEVICES', 'TICKETS', 'MAINTENANCE', 'PM_CALIBRATION', 'SIGNATURES', 'REPORTS', 'AUDIT'],
|
|
12
|
+
BIOMED_ENG: ['DEVICES', 'TICKETS', 'MAINTENANCE', 'PM_CALIBRATION', 'SIGNATURES', 'REPORTS'],
|
|
13
|
+
BIOMED_TECH: ['DEVICES', 'TICKETS', 'MAINTENANCE', 'PM_CALIBRATION', 'REPORTS'],
|
|
14
|
+
DEPT_MGR: ['DEVICES', 'TICKETS', 'REPORTS'],
|
|
15
|
+
STAFF: ['DEVICES', 'TICKETS'],
|
|
16
|
+
AUDITOR: ['DEVICES', 'TICKETS', 'MAINTENANCE', 'PM_CALIBRATION', 'SIGNATURES', 'REPORTS', 'AUDIT'],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ROLE_DESCRIPTIONS: Record<string, { label: string; boundary: string }> = {
|
|
20
|
+
SYS_ADMIN: { label: "System Administrator", boundary: "Platform settings, users, and audit logs. No clinical sign-off authority." },
|
|
21
|
+
ORG_ADMIN: { label: "Organization Admin", boundary: "Health system administration and hospital configuration." },
|
|
22
|
+
HOSP_ADMIN: { label: "Hospital Admin", boundary: "Facility-level administrative operations and departmental oversight." },
|
|
23
|
+
BIOMED_MGR: { label: "Biomedical Manager", boundary: "Clinical engineering supervisor; reviews evidence and releases critical devices." },
|
|
24
|
+
BIOMED_ENG: { label: "Biomedical Engineer", boundary: "Triage, diagnosis, work execution, test recording, and performer signatures." },
|
|
25
|
+
BIOMED_TECH: { label: "Biomedical Technician", boundary: "Routine maintenance and ticket handling. Cannot sign release for critical equipment." },
|
|
26
|
+
DEPT_MGR: { label: "Department Manager", boundary: "Departmental request tracking and operational asset awareness." },
|
|
27
|
+
STAFF: { label: "Clinical Staff / Doctor", boundary: "QR device lookup, issue reporting, and requester ticket tracking." },
|
|
28
|
+
AUDITOR: { label: "Quality / Compliance Auditor", boundary: "Immutable read-only access to audit logs, histories, and signatures." },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default async function RolesPage() {
|
|
32
|
+
const result = await getPermissionsMatrix()
|
|
33
|
+
const rolePerms = (result?.success && Array.isArray(result.data)) ? result.data : []
|
|
34
|
+
|
|
35
|
+
const roles = ['SYS_ADMIN', 'ORG_ADMIN', 'HOSP_ADMIN', 'BIOMED_MGR', 'BIOMED_ENG', 'BIOMED_TECH', 'DEPT_MGR', 'STAFF', 'AUDITOR']
|
|
36
|
+
const modules = ['DEVICES', 'TICKETS', 'MAINTENANCE', 'PM_CALIBRATION', 'SIGNATURES', 'REPORTS', 'ADMIN', 'AUDIT']
|
|
37
|
+
|
|
38
|
+
// Helper to check if a role has a module permission
|
|
39
|
+
const hasPermission = (roleCode: string, moduleName: string) => {
|
|
40
|
+
// Separation of Duties (Section 3.3 / Rule 8):
|
|
41
|
+
// System Administrators manage infrastructure and users, but CANNOT sign maintenance records or release medical devices
|
|
42
|
+
if ((roleCode === 'SYS_ADMIN' || roleCode === 'ORG_ADMIN' || roleCode === 'HOSP_ADMIN') &&
|
|
43
|
+
(moduleName === 'SIGNATURES' || moduleName === 'MAINTENANCE')) {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const hasDbGrant = rolePerms.some((rp: any) =>
|
|
48
|
+
rp.role?.code === roleCode &&
|
|
49
|
+
rp.permission?.module?.toUpperCase() === moduleName.toUpperCase()
|
|
50
|
+
)
|
|
51
|
+
if (hasDbGrant) return true
|
|
52
|
+
|
|
53
|
+
// Fall back to documented clinical RBAC matrix
|
|
54
|
+
return DEFAULT_ROLE_PERMISSIONS[roleCode]?.includes(moduleName) ?? false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="space-y-6">
|
|
59
|
+
<PageHeader
|
|
60
|
+
title="Roles & Permissions Matrix"
|
|
61
|
+
description="Role-based access control (RBAC) governance and clinical separation of duties"
|
|
62
|
+
/>
|
|
63
|
+
|
|
64
|
+
{/* ── Governance & Separation of Duties Compliance Notice ── */}
|
|
65
|
+
<div className="rounded-xl border border-blue-200 bg-blue-50/50 p-4 space-y-2">
|
|
66
|
+
<div className="flex items-center gap-2 text-blue-900 font-semibold text-sm">
|
|
67
|
+
<ShieldCheck className="w-5 h-5 text-blue-600 shrink-0" />
|
|
68
|
+
<span>Separation of Duties Policy (21 CFR Part 11 / BEMMS Section 3.3)</span>
|
|
69
|
+
</div>
|
|
70
|
+
<p className="text-xs text-blue-800 leading-relaxed">
|
|
71
|
+
In compliance with medical device regulatory standards, administrative privileges are strictly decoupled from clinical engineering authorities. <strong>System Administrators</strong> manage accounts, security, and catalog settings, but cannot execute maintenance records or sign device release approvals without a verified biomedical engineering credential.
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* ── Role Permission Matrix Table ── */}
|
|
76
|
+
<Card>
|
|
77
|
+
<CardHeader className="pb-3 border-b">
|
|
78
|
+
<CardTitle className="text-base font-semibold">System Module Access Matrix</CardTitle>
|
|
79
|
+
</CardHeader>
|
|
80
|
+
<CardContent className="pt-4 overflow-x-auto">
|
|
81
|
+
<table className="w-full text-sm text-left">
|
|
82
|
+
<thead className="bg-muted/50 text-muted-foreground uppercase text-xs">
|
|
83
|
+
<tr>
|
|
84
|
+
<th className="px-4 py-3 font-medium">Role</th>
|
|
85
|
+
{modules.map((mod: string) => (
|
|
86
|
+
<th key={mod} className="px-3 py-3 font-medium text-center">{mod}</th>
|
|
87
|
+
))}
|
|
88
|
+
</tr>
|
|
89
|
+
</thead>
|
|
90
|
+
<tbody className="divide-y">
|
|
91
|
+
{roles.map((role: string) => (
|
|
92
|
+
<tr key={role} className="hover:bg-muted/30">
|
|
93
|
+
<td className="px-4 py-3.5">
|
|
94
|
+
<div className="font-semibold text-sm">{role}</div>
|
|
95
|
+
<div className="text-xs text-muted-foreground">
|
|
96
|
+
{ROLE_DESCRIPTIONS[role]?.label || role}
|
|
97
|
+
</div>
|
|
98
|
+
</td>
|
|
99
|
+
{modules.map((mod: string) => {
|
|
100
|
+
const granted = hasPermission(role, mod)
|
|
101
|
+
const isProtectedClinical = (role.includes('ADMIN') && (mod === 'SIGNATURES' || mod === 'MAINTENANCE'))
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<td key={`${role}-${mod}`} className="px-3 py-3.5 text-center">
|
|
105
|
+
{granted ? (
|
|
106
|
+
<div className="inline-flex items-center justify-center p-1 rounded bg-green-50 text-green-600">
|
|
107
|
+
<Check className="w-4 h-4" />
|
|
108
|
+
</div>
|
|
109
|
+
) : isProtectedClinical ? (
|
|
110
|
+
<div className="inline-flex items-center justify-center p-1 rounded bg-red-50 text-red-400" title="Restricted by Separation of Duties policy">
|
|
111
|
+
<X className="w-4 h-4" />
|
|
112
|
+
</div>
|
|
113
|
+
) : (
|
|
114
|
+
<div className="inline-flex items-center justify-center p-1 text-muted-foreground/30">
|
|
115
|
+
<X className="w-4 h-4" />
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</td>
|
|
119
|
+
)
|
|
120
|
+
})}
|
|
121
|
+
</tr>
|
|
122
|
+
))}
|
|
123
|
+
</tbody>
|
|
124
|
+
</table>
|
|
125
|
+
</CardContent>
|
|
126
|
+
</Card>
|
|
127
|
+
|
|
128
|
+
{/* ── Role Boundaries Legend ── */}
|
|
129
|
+
<Card>
|
|
130
|
+
<CardHeader className="pb-3 border-b">
|
|
131
|
+
<CardTitle className="text-sm font-semibold flex items-center gap-2">
|
|
132
|
+
<Info className="w-4 h-4 text-primary" />
|
|
133
|
+
Role Safety Boundaries & Delegations
|
|
134
|
+
</CardTitle>
|
|
135
|
+
</CardHeader>
|
|
136
|
+
<CardContent className="pt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
137
|
+
{roles.map((role) => (
|
|
138
|
+
<div key={role} className="p-3 rounded-lg border bg-muted/20 space-y-1">
|
|
139
|
+
<div className="flex items-center gap-2">
|
|
140
|
+
<span className="font-mono text-xs font-bold text-primary">{role}</span>
|
|
141
|
+
<span className="text-xs text-muted-foreground font-medium">({ROLE_DESCRIPTIONS[role]?.label})</span>
|
|
142
|
+
</div>
|
|
143
|
+
<p className="text-xs text-muted-foreground leading-normal">
|
|
144
|
+
{ROLE_DESCRIPTIONS[role]?.boundary}
|
|
145
|
+
</p>
|
|
146
|
+
</div>
|
|
147
|
+
))}
|
|
148
|
+
</CardContent>
|
|
149
|
+
</Card>
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|