@omersx/bemms 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +203 -2
- package/drizzle.config.ts +11 -0
- package/next.config.ts +51 -0
- package/package.json +99 -3
- package/postcss.config.mjs +7 -0
- package/public/hospital_demo_devices.csv +32 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon-maskable-512.png +0 -0
- package/public/manifest.webmanifest +31 -0
- package/public/sw.js +177 -0
- package/src/app/(auth)/layout.tsx +20 -0
- package/src/app/(auth)/login/page.tsx +135 -0
- package/src/app/(dashboard)/admin/audit-logs/page.tsx +178 -0
- package/src/app/(dashboard)/admin/data/page.tsx +37 -0
- package/src/app/(dashboard)/admin/departments/[deptId]/edit/page.tsx +206 -0
- package/src/app/(dashboard)/admin/departments/[deptId]/page.tsx +71 -0
- package/src/app/(dashboard)/admin/departments/new/page.tsx +6 -0
- package/src/app/(dashboard)/admin/departments/page.tsx +6 -0
- package/src/app/(dashboard)/admin/general/page.tsx +374 -0
- package/src/app/(dashboard)/admin/hospitals/[hospitalId]/edit/page.tsx +304 -0
- package/src/app/(dashboard)/admin/hospitals/[hospitalId]/page.tsx +94 -0
- package/src/app/(dashboard)/admin/hospitals/new/page.tsx +302 -0
- package/src/app/(dashboard)/admin/hospitals/page.tsx +6 -0
- package/src/app/(dashboard)/admin/layout.tsx +49 -0
- package/src/app/(dashboard)/admin/organizations/[orgId]/edit/page.tsx +172 -0
- package/src/app/(dashboard)/admin/organizations/[orgId]/page.tsx +82 -0
- package/src/app/(dashboard)/admin/organizations/new/page.tsx +150 -0
- package/src/app/(dashboard)/admin/organizations/page.tsx +6 -0
- package/src/app/(dashboard)/admin/page.tsx +258 -0
- package/src/app/(dashboard)/admin/roles/page.tsx +153 -0
- package/src/app/(dashboard)/admin/settings/page.tsx +69 -0
- package/src/app/(dashboard)/admin/users/[userId]/edit/page.tsx +153 -0
- package/src/app/(dashboard)/admin/users/[userId]/page.tsx +317 -0
- package/src/app/(dashboard)/admin/users/invite/page.tsx +3 -0
- package/src/app/(dashboard)/admin/users/new/page.tsx +5 -0
- package/src/app/(dashboard)/admin/users/page.tsx +23 -0
- package/src/app/(dashboard)/dashboard/page.tsx +462 -0
- package/src/app/(dashboard)/departments/[deptId]/page.tsx +39 -0
- package/src/app/(dashboard)/departments/new/page.tsx +330 -0
- package/src/app/(dashboard)/departments/page.tsx +52 -0
- package/src/app/(dashboard)/devices/[id]/edit/page.tsx +131 -0
- package/src/app/(dashboard)/devices/[id]/page.tsx +408 -0
- package/src/app/(dashboard)/devices/[id]/qr/page.tsx +54 -0
- package/src/app/(dashboard)/devices/new/page.tsx +382 -0
- package/src/app/(dashboard)/devices/page.tsx +80 -0
- package/src/app/(dashboard)/engineering-team/page.tsx +41 -0
- package/src/app/(dashboard)/layout.tsx +43 -0
- package/src/app/(dashboard)/maintenance/checklists/page.tsx +57 -0
- package/src/app/(dashboard)/maintenance/page.tsx +231 -0
- package/src/app/(dashboard)/maintenance/plans/page.tsx +142 -0
- package/src/app/(dashboard)/maintenance/tasks/[id]/page.tsx +997 -0
- package/src/app/(dashboard)/maintenance/tasks/page.tsx +119 -0
- package/src/app/(dashboard)/more/page.tsx +310 -0
- package/src/app/(dashboard)/my-department/page.tsx +151 -0
- package/src/app/(dashboard)/notifications/notification-list.tsx +103 -0
- package/src/app/(dashboard)/notifications/page.tsx +59 -0
- package/src/app/(dashboard)/profile/page.tsx +27 -0
- package/src/app/(dashboard)/reports/costs/page.tsx +127 -0
- package/src/app/(dashboard)/reports/history/page.tsx +105 -0
- package/src/app/(dashboard)/reports/inventory/page.tsx +116 -0
- package/src/app/(dashboard)/reports/maintenance/page.tsx +136 -0
- package/src/app/(dashboard)/reports/page.tsx +120 -0
- package/src/app/(dashboard)/reports/tickets/page.tsx +139 -0
- package/src/app/(dashboard)/reports/workload/page.tsx +135 -0
- package/src/app/(dashboard)/rounds/[id]/page.tsx +197 -0
- package/src/app/(dashboard)/rounds/history/page.tsx +82 -0
- package/src/app/(dashboard)/rounds/page.tsx +107 -0
- package/src/app/(dashboard)/scan/[code]/page.tsx +154 -0
- package/src/app/(dashboard)/scan/page.tsx +73 -0
- package/src/app/(dashboard)/team/page.tsx +42 -0
- package/src/app/(dashboard)/tickets/[id]/page.tsx +465 -0
- package/src/app/(dashboard)/tickets/create/page.tsx +722 -0
- package/src/app/(dashboard)/tickets/page.tsx +55 -0
- package/src/app/(dashboard)/tickets/triage/page.tsx +161 -0
- package/src/app/api/admin/data/export/route.ts +316 -0
- package/src/app/api/auth/[...nextauth]/route.ts +3 -0
- package/src/app/api/health/route.ts +46 -0
- package/src/app/api/notifications/sse/route.ts +91 -0
- package/src/app/api/notifications/subscribe/route.ts +54 -0
- package/src/app/api/notifications/vapid-key/route.ts +7 -0
- package/src/app/api/reports/export/route.ts +102 -0
- package/src/app/api/tickets/[id]/attachments/route.ts +129 -0
- package/src/app/api/tickets/[id]/live-sync/route.ts +52 -0
- package/src/app/error.tsx +54 -0
- package/src/app/globals.css +89 -0
- package/src/app/layout.tsx +59 -0
- package/src/app/not-found.tsx +32 -0
- package/src/app/page.tsx +5 -0
- package/src/components/admin/add-department-user-dialog.tsx +759 -0
- package/src/components/admin/add-engineer-dialog.tsx +773 -0
- package/src/components/admin/add-user-type-dialog.tsx +89 -0
- package/src/components/admin/audit-log-export.tsx +113 -0
- package/src/components/admin/audit-log-filters.tsx +255 -0
- package/src/components/admin/audit-log-table.tsx +383 -0
- package/src/components/admin/data-management/danger-zone.tsx +301 -0
- package/src/components/admin/data-management/data-management-tabs.tsx +61 -0
- package/src/components/admin/data-management/export-center.tsx +301 -0
- package/src/components/admin/data-management/import-wizard.tsx +558 -0
- package/src/components/admin/settings-forms/features-form.tsx +115 -0
- package/src/components/admin/settings-forms/health-panel.tsx +182 -0
- package/src/components/admin/settings-forms/localization-form.tsx +203 -0
- package/src/components/admin/settings-forms/retention-form.tsx +164 -0
- package/src/components/admin/settings-forms/security-form.tsx +186 -0
- package/src/components/admin/system-settings-tabs.tsx +88 -0
- package/src/components/admin/user-role-dialog.tsx +293 -0
- package/src/components/admin/user-status-dialog.tsx +166 -0
- package/src/components/admin/users-page-client.tsx +514 -0
- package/src/components/dashboard/action-buttons.tsx +93 -0
- package/src/components/dashboard/admin-alerts.tsx +86 -0
- package/src/components/dashboard/availability-alerts.tsx +65 -0
- package/src/components/dashboard/maintenance-due-list.tsx +87 -0
- package/src/components/dashboard/round-widget.tsx +110 -0
- package/src/components/dashboard/stat-card.tsx +38 -0
- package/src/components/dashboard/ticket-list-compact.tsx +86 -0
- package/src/components/departments/department-cards-view.tsx +244 -0
- package/src/components/departments/department-detail-view.tsx +467 -0
- package/src/components/departments/department-visuals.tsx +190 -0
- package/src/components/departments/my-department-view.tsx +333 -0
- package/src/components/devices/change-status-dialog.tsx +146 -0
- package/src/components/devices/device-action-buttons.tsx +125 -0
- package/src/components/devices/device-availability-banner.tsx +59 -0
- package/src/components/devices/device-badges.tsx +55 -0
- package/src/components/devices/device-documents-tab.tsx +93 -0
- package/src/components/devices/device-maintenance-tab.tsx +113 -0
- package/src/components/devices/device-mini-qr.tsx +109 -0
- package/src/components/devices/device-qr-card.tsx +187 -0
- package/src/components/devices/device-signatures-tab.tsx +607 -0
- package/src/components/devices/device-tickets-tab.tsx +139 -0
- package/src/components/devices/device-timelines.tsx +124 -0
- package/src/components/devices/qr-camera-scanner.tsx +167 -0
- package/src/components/devices/qr-code-display.tsx +139 -0
- package/src/components/devices/quick-add-category-dialog.tsx +135 -0
- package/src/components/devices/quick-add-manufacturer-dialog.tsx +212 -0
- package/src/components/devices/transfer-device-dialog.tsx +190 -0
- package/src/components/layout/header.tsx +80 -0
- package/src/components/layout/mobile-nav.tsx +91 -0
- package/src/components/layout/more-item-badge.tsx +15 -0
- package/src/components/layout/nav-badge.tsx +72 -0
- package/src/components/layout/sidebar.tsx +297 -0
- package/src/components/maintenance/maintenance-badges.tsx +579 -0
- package/src/components/maintenance/maintenance-nav.tsx +107 -0
- package/src/components/maintenance/parts-costs-manager.tsx +909 -0
- package/src/components/maintenance/signature-card.tsx +413 -0
- package/src/components/maintenance/task-action-bar.tsx +955 -0
- package/src/components/notifications/notification-bell.tsx +367 -0
- package/src/components/notifications/notification-item.tsx +93 -0
- package/src/components/notifications/notification-preferences.tsx +182 -0
- package/src/components/profile/user-profile-view.tsx +626 -0
- package/src/components/providers/nav-counts-provider.tsx +235 -0
- package/src/components/providers/session-provider.tsx +7 -0
- package/src/components/providers/theme-provider.tsx +124 -0
- package/src/components/pwa/install-prompt.tsx +82 -0
- package/src/components/pwa/sw-register.tsx +43 -0
- package/src/components/reports/export-button.tsx +62 -0
- package/src/components/reports/filter-chips.tsx +47 -0
- package/src/components/reports/summary-cards.tsx +40 -0
- package/src/components/reports/transparency-header.tsx +54 -0
- package/src/components/rounds/department-visit-sheet.tsx +199 -0
- package/src/components/rounds/mention-textarea.tsx +187 -0
- package/src/components/rounds/round-calendar.tsx +233 -0
- package/src/components/rounds/round-department-list.tsx +281 -0
- package/src/components/rounds/round-history-client.tsx +218 -0
- package/src/components/rounds/start-round-card.tsx +184 -0
- package/src/components/shared/confirm-dialog.tsx +67 -0
- package/src/components/shared/data-table.tsx +104 -0
- package/src/components/shared/page-header.tsx +20 -0
- package/src/components/shared/sign-out-button.tsx +52 -0
- package/src/components/shared/status-badge.tsx +42 -0
- package/src/components/shared/table-search.tsx +55 -0
- package/src/components/signatures/sign-action-dialog.tsx +253 -0
- package/src/components/signatures/signature-history.tsx +121 -0
- package/src/components/signatures/signature-status-panel.tsx +164 -0
- package/src/components/team/department-coverage.tsx +141 -0
- package/src/components/team/edit-engineer-dialog.tsx +242 -0
- package/src/components/team/engineer-card.tsx +180 -0
- package/src/components/team/engineer-details-dialog.tsx +432 -0
- package/src/components/team/schedule-calendar.tsx +204 -0
- package/src/components/team/staff-engineer-team-view.tsx +368 -0
- package/src/components/team/team-header-stats.tsx +88 -0
- package/src/components/team/team-page-client.tsx +216 -0
- package/src/components/theme/theme-selector-card.tsx +63 -0
- package/src/components/theme/theme-toggle.tsx +65 -0
- package/src/components/tickets/staff-status-label.tsx +58 -0
- package/src/components/tickets/staff-ticket-card.tsx +109 -0
- package/src/components/tickets/staff-ticket-progress-tracker.tsx +405 -0
- package/src/components/tickets/staff-tickets-view.tsx +182 -0
- package/src/components/tickets/ticket-action-buttons.tsx +873 -0
- package/src/components/tickets/ticket-auto-refresh.tsx +197 -0
- package/src/components/tickets/ticket-badges.tsx +43 -0
- package/src/components/tickets/ticket-card.tsx +197 -0
- package/src/components/tickets/ticket-cards-view.tsx +395 -0
- package/src/components/tickets/ticket-comment-section.tsx +684 -0
- package/src/components/tickets/ticket-dispatch-status.tsx +184 -0
- package/src/components/tickets/ticket-list-auto-refresh.tsx +55 -0
- package/src/components/tickets/ticket-reporter-profile.tsx +235 -0
- package/src/components/tickets/ticket-signatures-panel.tsx +32 -0
- package/src/components/tickets/ticket-timeline.tsx +131 -0
- package/src/components/tickets/ticket-triage-panel.tsx +124 -0
- package/src/components/tickets/tickets-nav.tsx +107 -0
- package/src/components/ui/avatar.tsx +43 -0
- package/src/components/ui/badge.tsx +34 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/card.tsx +77 -0
- package/src/components/ui/checkbox.tsx +31 -0
- package/src/components/ui/dialog.tsx +123 -0
- package/src/components/ui/dropdown-menu.tsx +126 -0
- package/src/components/ui/form.tsx +179 -0
- package/src/components/ui/input.tsx +24 -0
- package/src/components/ui/label.tsx +27 -0
- package/src/components/ui/popover.tsx +34 -0
- package/src/components/ui/progress.tsx +36 -0
- package/src/components/ui/scroll-area.tsx +47 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/sheet.tsx +131 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +121 -0
- package/src/components/ui/tabs.tsx +62 -0
- package/src/components/ui/textarea.tsx +23 -0
- package/src/components/ui/tooltip.tsx +27 -0
- package/src/lib/actions/__tests__/devices.test.ts +176 -0
- package/src/lib/actions/__tests__/signatures.test.ts +303 -0
- package/src/lib/actions/__tests__/tickets.test.ts +243 -0
- package/src/lib/actions/audit-logs.ts +298 -0
- package/src/lib/actions/checklists.ts +429 -0
- package/src/lib/actions/dashboard.ts +358 -0
- package/src/lib/actions/data-management.ts +723 -0
- package/src/lib/actions/departments.ts +369 -0
- package/src/lib/actions/device-categories.ts +117 -0
- package/src/lib/actions/devices.ts +748 -0
- package/src/lib/actions/engineer-rounds.ts +973 -0
- package/src/lib/actions/engineers.ts +285 -0
- package/src/lib/actions/hospitals.ts +148 -0
- package/src/lib/actions/locations.ts +115 -0
- package/src/lib/actions/maintenance-plans.ts +316 -0
- package/src/lib/actions/maintenance-tasks.ts +415 -0
- package/src/lib/actions/manufacturers.ts +132 -0
- package/src/lib/actions/nav-counts.ts +144 -0
- package/src/lib/actions/notifications.ts +136 -0
- package/src/lib/actions/organizations.ts +99 -0
- package/src/lib/actions/qr.ts +147 -0
- package/src/lib/actions/report-history.ts +77 -0
- package/src/lib/actions/reports.ts +192 -0
- package/src/lib/actions/roles.ts +32 -0
- package/src/lib/actions/signature-auth.ts +377 -0
- package/src/lib/actions/signatures.ts +532 -0
- package/src/lib/actions/system-settings.ts +274 -0
- package/src/lib/actions/tickets.ts +1445 -0
- package/src/lib/actions/users.ts +740 -0
- package/src/lib/audit/index.ts +103 -0
- package/src/lib/auth/__tests__/rbac.test.ts +33 -0
- package/src/lib/auth/index.ts +338 -0
- package/src/lib/auth/password.ts +10 -0
- package/src/lib/auth/rbac.ts +103 -0
- package/src/lib/auth/types.ts +57 -0
- package/src/lib/constants/departments.ts +16 -0
- package/src/lib/constants/manufacturers.ts +32 -0
- package/src/lib/db/index.ts +23 -0
- package/src/lib/db/schema/audit-logs.ts +42 -0
- package/src/lib/db/schema/departments.ts +39 -0
- package/src/lib/db/schema/device-categories.ts +38 -0
- package/src/lib/db/schema/devices.ts +195 -0
- package/src/lib/db/schema/engineer-profiles.ts +63 -0
- package/src/lib/db/schema/engineer-rounds.ts +115 -0
- package/src/lib/db/schema/enums.ts +148 -0
- package/src/lib/db/schema/hospitals.ts +34 -0
- package/src/lib/db/schema/index.ts +24 -0
- package/src/lib/db/schema/locations.ts +46 -0
- package/src/lib/db/schema/maintenance-plans.ts +99 -0
- package/src/lib/db/schema/maintenance-records.ts +161 -0
- package/src/lib/db/schema/maintenance-tasks.ts +196 -0
- package/src/lib/db/schema/manufacturers.ts +60 -0
- package/src/lib/db/schema/notifications.ts +48 -0
- package/src/lib/db/schema/organizations.ts +24 -0
- package/src/lib/db/schema/report-history.ts +28 -0
- package/src/lib/db/schema/roles.ts +59 -0
- package/src/lib/db/schema/sessions.ts +46 -0
- package/src/lib/db/schema/signature-events.ts +48 -0
- package/src/lib/db/schema/signature-policies.ts +60 -0
- package/src/lib/db/schema/signatures.ts +87 -0
- package/src/lib/db/schema/tickets.ts +169 -0
- package/src/lib/db/schema/user-assignments.ts +85 -0
- package/src/lib/db/schema/users.ts +39 -0
- package/src/lib/db/seed.ts +131 -0
- package/src/lib/logger/__tests__/logger.test.ts +154 -0
- package/src/lib/logger/formatters.ts +159 -0
- package/src/lib/logger/index.ts +184 -0
- package/src/lib/logger/types.ts +48 -0
- package/src/lib/notifications/create.ts +71 -0
- package/src/lib/notifications/dispatcher.ts +490 -0
- package/src/lib/notifications/index.ts +7 -0
- package/src/lib/notifications/push-sender.ts +128 -0
- package/src/lib/notifications/push.ts +64 -0
- package/src/lib/notifications/triggers.ts +166 -0
- package/src/lib/utils/__tests__/crypto.test.ts +30 -0
- package/src/lib/utils/__tests__/export.test.ts +55 -0
- package/src/lib/utils/__tests__/nav-badge.test.ts +147 -0
- package/src/lib/utils/__tests__/staff-status.test.ts +50 -0
- package/src/lib/utils/attestations.ts +19 -0
- package/src/lib/utils/crypto.ts +18 -0
- package/src/lib/utils/csv-parser.ts +181 -0
- package/src/lib/utils/export.ts +45 -0
- package/src/lib/utils/nav-badge.ts +225 -0
- package/src/lib/utils/sound.ts +152 -0
- package/src/lib/utils/staff-status.ts +115 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validators/__tests__/auth.test.ts +127 -0
- package/src/lib/validators/__tests__/data-management.test.ts +182 -0
- package/src/lib/validators/__tests__/devices-profile.test.ts +129 -0
- package/src/lib/validators/__tests__/devices.test.ts +98 -0
- package/src/lib/validators/__tests__/engineer-rounds.test.ts +170 -0
- package/src/lib/validators/__tests__/maintenance.test.ts +178 -0
- package/src/lib/validators/__tests__/signatures.test.ts +154 -0
- package/src/lib/validators/__tests__/system-settings.test.ts +119 -0
- package/src/lib/validators/__tests__/tickets.test.ts +176 -0
- package/src/lib/validators/__tests__/users.test.ts +92 -0
- package/src/lib/validators/auth.ts +32 -0
- package/src/lib/validators/data-management.ts +67 -0
- package/src/lib/validators/departments.ts +14 -0
- package/src/lib/validators/devices.ts +94 -0
- package/src/lib/validators/engineer-rounds.ts +70 -0
- package/src/lib/validators/hospitals.ts +17 -0
- package/src/lib/validators/locations.ts +15 -0
- package/src/lib/validators/maintenance.ts +211 -0
- package/src/lib/validators/organizations.ts +11 -0
- package/src/lib/validators/reports.ts +31 -0
- package/src/lib/validators/signatures.ts +49 -0
- package/src/lib/validators/system-settings.ts +55 -0
- package/src/lib/validators/tickets.ts +125 -0
- package/src/lib/validators/users.ts +43 -0
- package/src/middleware.ts +45 -0
- package/tailwind.config.ts +76 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { redirect } from 'next/navigation';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { requireRole } from '@/lib/auth/rbac';
|
|
4
|
+
import { getOrganizationSettings, getSystemHealth } from '@/lib/actions/system-settings';
|
|
5
|
+
import { SystemSettingsTabs } from '@/components/admin/system-settings-tabs';
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
7
|
+
import { AlertCircle } from 'lucide-react';
|
|
8
|
+
|
|
9
|
+
export default async function SystemSettingsPage() {
|
|
10
|
+
const session = await auth();
|
|
11
|
+
if (!session?.user) {
|
|
12
|
+
redirect('/login');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Authorize only SYS_ADMIN and ORG_ADMIN
|
|
16
|
+
try {
|
|
17
|
+
await requireRole('SYS_ADMIN', 'ORG_ADMIN');
|
|
18
|
+
} catch {
|
|
19
|
+
redirect('/dashboard');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const [settingsRes, healthRes] = await Promise.all([
|
|
23
|
+
getOrganizationSettings(),
|
|
24
|
+
getSystemHealth(),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
if (!settingsRes.success) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="p-6">
|
|
30
|
+
<Card className="border-destructive/30">
|
|
31
|
+
<CardHeader>
|
|
32
|
+
<CardTitle className="flex items-center gap-2 text-destructive">
|
|
33
|
+
<AlertCircle className="h-5 w-5" />
|
|
34
|
+
Unable to Load System Settings
|
|
35
|
+
</CardTitle>
|
|
36
|
+
</CardHeader>
|
|
37
|
+
<CardContent>
|
|
38
|
+
<p className="text-sm text-muted-foreground">
|
|
39
|
+
{settingsRes.error}
|
|
40
|
+
</p>
|
|
41
|
+
</CardContent>
|
|
42
|
+
</Card>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fallbackHealth = {
|
|
48
|
+
status: 'healthy' as const,
|
|
49
|
+
databaseStatus: 'connected' as const,
|
|
50
|
+
databasePingMs: 0,
|
|
51
|
+
environment: 'development',
|
|
52
|
+
uptimeSeconds: 0,
|
|
53
|
+
serverTime: new Date().toISOString(),
|
|
54
|
+
memoryUsage: { rssMb: 0, heapUsedMb: 0, heapTotalMb: 0 },
|
|
55
|
+
runtime: { nodeVersion: 'v20', platform: 'win32', arch: 'x64' },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="space-y-6">
|
|
60
|
+
<SystemSettingsTabs
|
|
61
|
+
settings={settingsRes.data}
|
|
62
|
+
organizationId={settingsRes.organizationId}
|
|
63
|
+
organizationName={settingsRes.organizationName}
|
|
64
|
+
healthData={healthRes.data || fallbackHealth}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, use } from "react"
|
|
4
|
+
import { useRouter } from "next/navigation"
|
|
5
|
+
import { useForm } from "react-hook-form"
|
|
6
|
+
import { getUserById, updateUserProfile } from "@/lib/actions/users"
|
|
7
|
+
import { PageHeader } from "@/components/shared/page-header"
|
|
8
|
+
import { Button } from "@/components/ui/button"
|
|
9
|
+
import { Input } from "@/components/ui/input"
|
|
10
|
+
import { Label } from "@/components/ui/label"
|
|
11
|
+
import { Card, CardContent } from "@/components/ui/card"
|
|
12
|
+
import { ArrowLeft, Loader2, CheckCircle2 } from "lucide-react"
|
|
13
|
+
import Link from "next/link"
|
|
14
|
+
import { toast } from "sonner"
|
|
15
|
+
|
|
16
|
+
export default function EditUserPage({ params }: { params: Promise<{ userId: string }> }) {
|
|
17
|
+
const resolvedParams = use(params)
|
|
18
|
+
const router = useRouter()
|
|
19
|
+
const [loading, setLoading] = useState(true)
|
|
20
|
+
const [submitting, setSubmitting] = useState(false)
|
|
21
|
+
|
|
22
|
+
const { register, handleSubmit, setValue } = useForm()
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
async function loadUser() {
|
|
26
|
+
const res = await getUserById(resolvedParams.userId)
|
|
27
|
+
if (res.success && res.data) {
|
|
28
|
+
const u = res.data
|
|
29
|
+
setValue("fullName", u.fullName)
|
|
30
|
+
setValue("phone", u.phone || "")
|
|
31
|
+
setValue("jobTitle", u.jobTitle || "")
|
|
32
|
+
setValue("employeeIdentifier", u.employeeIdentifier || "")
|
|
33
|
+
setLoading(false)
|
|
34
|
+
} else {
|
|
35
|
+
toast.error("Failed to load user profile")
|
|
36
|
+
setLoading(false)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
loadUser()
|
|
40
|
+
}, [resolvedParams.userId, setValue])
|
|
41
|
+
|
|
42
|
+
const onSubmit = async (data: any) => {
|
|
43
|
+
setSubmitting(true)
|
|
44
|
+
try {
|
|
45
|
+
const res = await updateUserProfile(resolvedParams.userId, data)
|
|
46
|
+
if (res.success) {
|
|
47
|
+
toast.success("Profile updated successfully")
|
|
48
|
+
router.push(`/admin/users/${resolvedParams.userId}`)
|
|
49
|
+
} else {
|
|
50
|
+
toast.error((res as any)?.error || "Error updating user")
|
|
51
|
+
}
|
|
52
|
+
} catch (err: any) {
|
|
53
|
+
toast.error(err?.message || "An unexpected error occurred")
|
|
54
|
+
} finally {
|
|
55
|
+
setSubmitting(false)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (loading) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex h-64 items-center justify-center">
|
|
62
|
+
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="space-y-6 max-w-2xl mx-auto pb-12">
|
|
69
|
+
<div className="flex items-center gap-2">
|
|
70
|
+
<Button asChild variant="ghost" size="sm" className="h-8 px-2 text-muted-foreground">
|
|
71
|
+
<Link href={`/admin/users/${resolvedParams.userId}`}>
|
|
72
|
+
<ArrowLeft className="w-4 h-4 mr-1" />
|
|
73
|
+
Back to User Details
|
|
74
|
+
</Link>
|
|
75
|
+
</Button>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<PageHeader
|
|
79
|
+
title="Edit Profile"
|
|
80
|
+
description="Update user identity and contact information"
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<Card className="border shadow-xs">
|
|
84
|
+
<CardContent className="pt-6">
|
|
85
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
86
|
+
<div className="space-y-1.5">
|
|
87
|
+
<Label htmlFor="fullName" className="text-xs font-semibold">
|
|
88
|
+
Full Name *
|
|
89
|
+
</Label>
|
|
90
|
+
<Input
|
|
91
|
+
id="fullName"
|
|
92
|
+
placeholder="e.g. Dr. Ahmed Salem"
|
|
93
|
+
{...register("fullName", { required: true })}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div className="space-y-1.5">
|
|
98
|
+
<Label htmlFor="jobTitle" className="text-xs font-semibold">
|
|
99
|
+
Job / Clinical Title
|
|
100
|
+
</Label>
|
|
101
|
+
<Input
|
|
102
|
+
id="jobTitle"
|
|
103
|
+
placeholder="e.g. Biomedical Engineer, Head Nurse"
|
|
104
|
+
{...register("jobTitle")}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div className="space-y-1.5">
|
|
109
|
+
<Label htmlFor="employeeIdentifier" className="text-xs font-semibold">
|
|
110
|
+
Employee ID / Username
|
|
111
|
+
</Label>
|
|
112
|
+
<Input
|
|
113
|
+
id="employeeIdentifier"
|
|
114
|
+
placeholder="e.g. EMP-1042"
|
|
115
|
+
{...register("employeeIdentifier")}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div className="space-y-1.5">
|
|
120
|
+
<Label htmlFor="phone" className="text-xs font-semibold">
|
|
121
|
+
Direct Phone / Mobile
|
|
122
|
+
</Label>
|
|
123
|
+
<Input
|
|
124
|
+
id="phone"
|
|
125
|
+
placeholder="+966 50 123 4567"
|
|
126
|
+
{...register("phone")}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div className="flex justify-end gap-3 pt-4 border-t">
|
|
131
|
+
<Button asChild variant="outline" disabled={submitting}>
|
|
132
|
+
<Link href={`/admin/users/${resolvedParams.userId}`}>Cancel</Link>
|
|
133
|
+
</Button>
|
|
134
|
+
<Button type="submit" disabled={submitting} className="gap-2">
|
|
135
|
+
{submitting ? (
|
|
136
|
+
<>
|
|
137
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
138
|
+
Updating...
|
|
139
|
+
</>
|
|
140
|
+
) : (
|
|
141
|
+
<>
|
|
142
|
+
<CheckCircle2 className="w-4 h-4" />
|
|
143
|
+
Update Profile
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
</Button>
|
|
147
|
+
</div>
|
|
148
|
+
</form>
|
|
149
|
+
</CardContent>
|
|
150
|
+
</Card>
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { getUserById } from "@/lib/actions/users"
|
|
2
|
+
import { getAuditLogs } from "@/lib/actions/audit-logs"
|
|
3
|
+
import { getRoles } from "@/lib/actions/roles"
|
|
4
|
+
import { PageHeader } from "@/components/shared/page-header"
|
|
5
|
+
import { StatusBadge } from "@/components/shared/status-badge"
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
8
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
9
|
+
import { Badge } from "@/components/ui/badge"
|
|
10
|
+
import { UserStatusDialog } from "@/components/admin/user-status-dialog"
|
|
11
|
+
import { UserRoleDialog } from "@/components/admin/user-role-dialog"
|
|
12
|
+
import Link from "next/link"
|
|
13
|
+
import {
|
|
14
|
+
Edit,
|
|
15
|
+
ArrowLeft,
|
|
16
|
+
Building2,
|
|
17
|
+
Wrench,
|
|
18
|
+
Stethoscope,
|
|
19
|
+
MapPin,
|
|
20
|
+
Calendar,
|
|
21
|
+
Phone,
|
|
22
|
+
Radio,
|
|
23
|
+
} from "lucide-react"
|
|
24
|
+
|
|
25
|
+
export const dynamic = "force-dynamic"
|
|
26
|
+
|
|
27
|
+
export default async function UserDetailPage({ params }: { params: Promise<{ userId: string }> }) {
|
|
28
|
+
const { userId } = await params
|
|
29
|
+
const [result, logsResult, rolesResult] = await Promise.all([
|
|
30
|
+
getUserById(userId),
|
|
31
|
+
getAuditLogs({ actorId: userId, pageSize: 10 }),
|
|
32
|
+
getRoles(),
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
const user = result?.success ? result.data : null
|
|
36
|
+
|
|
37
|
+
if (!user) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="p-8 text-center space-y-4">
|
|
40
|
+
<h2 className="text-xl font-semibold">User not found</h2>
|
|
41
|
+
<Button asChild variant="outline">
|
|
42
|
+
<Link href="/admin/users">
|
|
43
|
+
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
44
|
+
Back to Users
|
|
45
|
+
</Link>
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const activityLogs = logsResult?.success && logsResult.data ? logsResult.data : []
|
|
52
|
+
const availableRoles = rolesResult?.success && Array.isArray(rolesResult.data) ? rolesResult.data : []
|
|
53
|
+
const accessScopes = (user as any).accessScopes || []
|
|
54
|
+
const primaryScope = accessScopes.find((s: any) => s.departmentId) || accessScopes[0] || null
|
|
55
|
+
const engineerProfile = (user as any).engineerProfile
|
|
56
|
+
|
|
57
|
+
const roleCodes = ((user as any).roles || []).map((r: any) => r.code)
|
|
58
|
+
const isEngineer = roleCodes.some((code: string) =>
|
|
59
|
+
['BIOMED_ENG', 'BIOMED_TECH', 'BIOMED_MGR'].includes(code)
|
|
60
|
+
)
|
|
61
|
+
const isStaff = roleCodes.some((code: string) =>
|
|
62
|
+
['STAFF', 'DEPT_MGR'].includes(code)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="space-y-6">
|
|
67
|
+
<div className="flex items-center gap-2">
|
|
68
|
+
<Button asChild variant="ghost" size="sm" className="h-8 px-2 text-muted-foreground">
|
|
69
|
+
<Link href="/admin/users">
|
|
70
|
+
<ArrowLeft className="w-4 h-4 mr-1" />
|
|
71
|
+
Back to Users
|
|
72
|
+
</Link>
|
|
73
|
+
</Button>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<PageHeader
|
|
77
|
+
title={user.fullName}
|
|
78
|
+
description={user.email}
|
|
79
|
+
>
|
|
80
|
+
<div className="flex gap-2">
|
|
81
|
+
<Button asChild variant="outline">
|
|
82
|
+
<Link href={`/admin/users/${user.id}/edit`}>
|
|
83
|
+
<Edit className="w-4 h-4 mr-2" />
|
|
84
|
+
Edit Profile
|
|
85
|
+
</Link>
|
|
86
|
+
</Button>
|
|
87
|
+
<UserStatusDialog
|
|
88
|
+
userId={user.id}
|
|
89
|
+
userName={user.fullName}
|
|
90
|
+
currentStatus={user.accountStatus || 'active'}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</PageHeader>
|
|
94
|
+
|
|
95
|
+
<Tabs defaultValue="profile">
|
|
96
|
+
<TabsList>
|
|
97
|
+
<TabsTrigger value="profile">Profile & Facility</TabsTrigger>
|
|
98
|
+
<TabsTrigger value="roles">Roles & Access</TabsTrigger>
|
|
99
|
+
<TabsTrigger value="activity">Activity Trail</TabsTrigger>
|
|
100
|
+
</TabsList>
|
|
101
|
+
|
|
102
|
+
<TabsContent value="profile" className="mt-6 space-y-6">
|
|
103
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
104
|
+
{/* General User Information */}
|
|
105
|
+
<Card>
|
|
106
|
+
<CardHeader>
|
|
107
|
+
<CardTitle className="text-base font-semibold">User Information</CardTitle>
|
|
108
|
+
</CardHeader>
|
|
109
|
+
<CardContent className="grid gap-4 sm:grid-cols-2">
|
|
110
|
+
<div>
|
|
111
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Status</span>
|
|
112
|
+
<StatusBadge status={user.accountStatus} />
|
|
113
|
+
</div>
|
|
114
|
+
<div>
|
|
115
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Employee ID / Username</span>
|
|
116
|
+
<span className="text-sm font-semibold">{user.employeeIdentifier || "N/A"}</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div>
|
|
119
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Official Email</span>
|
|
120
|
+
<span className="text-sm">{user.email}</span>
|
|
121
|
+
</div>
|
|
122
|
+
<div>
|
|
123
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Job / Clinical Title</span>
|
|
124
|
+
<span className="text-sm">{user.jobTitle || "N/A"}</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div>
|
|
127
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Direct Phone</span>
|
|
128
|
+
<span className="text-sm">{user.phone || "N/A"}</span>
|
|
129
|
+
</div>
|
|
130
|
+
<div>
|
|
131
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Organization</span>
|
|
132
|
+
<span className="text-sm font-medium">{(user as any).organization?.name || "N/A"}</span>
|
|
133
|
+
</div>
|
|
134
|
+
<div>
|
|
135
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Last Login</span>
|
|
136
|
+
<span className="text-xs text-muted-foreground">
|
|
137
|
+
{user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : "Never logged in"}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
<div>
|
|
141
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Account Created</span>
|
|
142
|
+
<span className="text-xs text-muted-foreground">
|
|
143
|
+
{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : "N/A"}
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
</CardContent>
|
|
147
|
+
</Card>
|
|
148
|
+
|
|
149
|
+
{/* Facility & Department Allocation */}
|
|
150
|
+
<Card>
|
|
151
|
+
<CardHeader>
|
|
152
|
+
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
|
153
|
+
<Building2 className="w-4 h-4 text-primary" />
|
|
154
|
+
Facility & Department Allocation
|
|
155
|
+
</CardTitle>
|
|
156
|
+
</CardHeader>
|
|
157
|
+
<CardContent className="space-y-4">
|
|
158
|
+
{primaryScope ? (
|
|
159
|
+
<div className="space-y-3">
|
|
160
|
+
<div className="flex items-start gap-3 p-3 rounded-lg border bg-muted/20">
|
|
161
|
+
<div className="p-2 rounded-md bg-primary/10 text-primary shrink-0 mt-0.5">
|
|
162
|
+
{isStaff ? (
|
|
163
|
+
<Stethoscope className="w-4 h-4" />
|
|
164
|
+
) : isEngineer ? (
|
|
165
|
+
<Wrench className="w-4 h-4" />
|
|
166
|
+
) : (
|
|
167
|
+
<Building2 className="w-4 h-4" />
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
<div className="space-y-1 min-w-0">
|
|
171
|
+
<div className="flex items-center gap-2">
|
|
172
|
+
<span className="font-semibold text-sm">
|
|
173
|
+
{primaryScope.department?.name || "Facility-Wide"}
|
|
174
|
+
</span>
|
|
175
|
+
{primaryScope.department?.code && (
|
|
176
|
+
<Badge variant="outline" className="font-mono text-[10px]">
|
|
177
|
+
{primaryScope.department.code}
|
|
178
|
+
</Badge>
|
|
179
|
+
)}
|
|
180
|
+
<Badge variant="secondary" className="text-[10px] uppercase">
|
|
181
|
+
{primaryScope.scopeType}
|
|
182
|
+
</Badge>
|
|
183
|
+
</div>
|
|
184
|
+
{primaryScope.hospital?.name && (
|
|
185
|
+
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
|
186
|
+
<MapPin className="w-3 h-3" />
|
|
187
|
+
{primaryScope.hospital.name}
|
|
188
|
+
</p>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
) : (
|
|
194
|
+
<div className="text-center py-6 text-sm text-muted-foreground border border-dashed rounded-lg">
|
|
195
|
+
No department or facility scope assigned. Account has organization-wide default access.
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{/* Additional Access Scopes */}
|
|
200
|
+
{accessScopes.length > 1 && (
|
|
201
|
+
<div className="space-y-2 pt-2 border-t">
|
|
202
|
+
<span className="text-xs font-semibold text-muted-foreground block">
|
|
203
|
+
Additional Facility Scopes ({accessScopes.length})
|
|
204
|
+
</span>
|
|
205
|
+
<div className="space-y-1.5 max-h-36 overflow-y-auto">
|
|
206
|
+
{accessScopes.map((scope: any) => (
|
|
207
|
+
<div key={scope.id} className="text-xs flex items-center justify-between p-2 rounded border bg-card">
|
|
208
|
+
<span>{scope.department?.name || scope.hospital?.name || scope.scopeType}</span>
|
|
209
|
+
<Badge variant="outline" className="text-[10px]">{scope.scopeType}</Badge>
|
|
210
|
+
</div>
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</CardContent>
|
|
216
|
+
</Card>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{/* Biomedical Engineer Profile Configuration Card */}
|
|
220
|
+
{engineerProfile && (
|
|
221
|
+
<Card className="border-blue-200 dark:border-blue-900/50 bg-blue-50/20 dark:bg-blue-950/10">
|
|
222
|
+
<CardHeader>
|
|
223
|
+
<CardTitle className="text-base font-semibold flex items-center gap-2 text-blue-900 dark:text-blue-300">
|
|
224
|
+
<Wrench className="w-4 h-4 text-blue-600" />
|
|
225
|
+
Biomedical Engineering Roster Profile
|
|
226
|
+
</CardTitle>
|
|
227
|
+
</CardHeader>
|
|
228
|
+
<CardContent className="grid gap-4 sm:grid-cols-3">
|
|
229
|
+
<div>
|
|
230
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Technical Specialization</span>
|
|
231
|
+
<Badge variant="outline" className="bg-blue-100/60 text-blue-800 dark:bg-blue-950 dark:text-blue-300 border-blue-300">
|
|
232
|
+
{engineerProfile.specialization || "General Biomedical"}
|
|
233
|
+
</Badge>
|
|
234
|
+
</div>
|
|
235
|
+
<div>
|
|
236
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Current Duty Status</span>
|
|
237
|
+
<div className="flex items-center gap-1.5 text-xs font-medium">
|
|
238
|
+
<Radio className="w-3.5 h-3.5 text-blue-600 animate-pulse" />
|
|
239
|
+
<span className="capitalize">{engineerProfile.dutyStatus?.replace('_', ' ')}</span>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
<div>
|
|
243
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Extension / Bleep</span>
|
|
244
|
+
<span className="text-sm font-mono">{engineerProfile.extension || "N/A"}</span>
|
|
245
|
+
</div>
|
|
246
|
+
<div>
|
|
247
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Shift Hours</span>
|
|
248
|
+
<span className="text-xs">{engineerProfile.shiftHours || "Standard Hospital Shift"}</span>
|
|
249
|
+
</div>
|
|
250
|
+
<div>
|
|
251
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Emergency Mobile</span>
|
|
252
|
+
<span className="text-xs flex items-center gap-1">
|
|
253
|
+
<Phone className="w-3 h-3 text-muted-foreground" />
|
|
254
|
+
{engineerProfile.mobileNumber || user.phone || "N/A"}
|
|
255
|
+
</span>
|
|
256
|
+
</div>
|
|
257
|
+
<div>
|
|
258
|
+
<span className="font-medium text-xs text-muted-foreground block mb-1">Team Roster Link</span>
|
|
259
|
+
<Button asChild variant="link" size="sm" className="h-auto p-0 text-xs text-blue-600">
|
|
260
|
+
<Link href="/team">View in Engineering Team →</Link>
|
|
261
|
+
</Button>
|
|
262
|
+
</div>
|
|
263
|
+
</CardContent>
|
|
264
|
+
</Card>
|
|
265
|
+
)}
|
|
266
|
+
</TabsContent>
|
|
267
|
+
|
|
268
|
+
<TabsContent value="roles" className="mt-6 space-y-6">
|
|
269
|
+
<Card>
|
|
270
|
+
<CardHeader>
|
|
271
|
+
<CardTitle className="text-base font-semibold">Assigned Roles & Access Privileges</CardTitle>
|
|
272
|
+
</CardHeader>
|
|
273
|
+
<CardContent>
|
|
274
|
+
<UserRoleDialog
|
|
275
|
+
userId={user.id}
|
|
276
|
+
assignedRoles={(user as any).roles || []}
|
|
277
|
+
availableRoles={availableRoles}
|
|
278
|
+
/>
|
|
279
|
+
</CardContent>
|
|
280
|
+
</Card>
|
|
281
|
+
</TabsContent>
|
|
282
|
+
|
|
283
|
+
<TabsContent value="activity" className="mt-6">
|
|
284
|
+
<Card>
|
|
285
|
+
<CardHeader>
|
|
286
|
+
<CardTitle className="text-base font-semibold">Recent Activity Audit Trail (21 CFR Part 11)</CardTitle>
|
|
287
|
+
</CardHeader>
|
|
288
|
+
<CardContent>
|
|
289
|
+
{activityLogs && activityLogs.length > 0 ? (
|
|
290
|
+
<div className="space-y-4">
|
|
291
|
+
{activityLogs.map((log: any) => (
|
|
292
|
+
<div key={log.id} className="border-b pb-4 last:border-0 last:pb-0">
|
|
293
|
+
<div className="flex justify-between items-start mb-1">
|
|
294
|
+
<span className="font-medium text-xs">{log.action}</span>
|
|
295
|
+
<span className="text-[11px] text-muted-foreground">
|
|
296
|
+
{log.timestamp ? new Date(log.timestamp).toLocaleString() : ""}
|
|
297
|
+
</span>
|
|
298
|
+
</div>
|
|
299
|
+
<div className="text-xs text-muted-foreground">
|
|
300
|
+
{log.entityType} ({log.entityId})
|
|
301
|
+
{log.changeReason && (
|
|
302
|
+
<span className="italic block mt-0.5">Reason: "{log.changeReason}"</span>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
))}
|
|
307
|
+
</div>
|
|
308
|
+
) : (
|
|
309
|
+
<div className="text-muted-foreground py-6 text-center text-xs">No activity logged for this user.</div>
|
|
310
|
+
)}
|
|
311
|
+
</CardContent>
|
|
312
|
+
</Card>
|
|
313
|
+
</TabsContent>
|
|
314
|
+
</Tabs>
|
|
315
|
+
</div>
|
|
316
|
+
)
|
|
317
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Suspense } from "react"
|
|
2
|
+
import { getUsers } from "@/lib/actions/users"
|
|
3
|
+
import { UsersPageClient } from "@/components/admin/users-page-client"
|
|
4
|
+
import { Loader2 } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic"
|
|
7
|
+
|
|
8
|
+
export default async function UsersPage() {
|
|
9
|
+
const result = await getUsers()
|
|
10
|
+
const users = result?.success && result.data ? (result.data as any[]) : []
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Suspense
|
|
14
|
+
fallback={
|
|
15
|
+
<div className="flex h-96 items-center justify-center">
|
|
16
|
+
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
17
|
+
</div>
|
|
18
|
+
}
|
|
19
|
+
>
|
|
20
|
+
<UsersPageClient users={users} />
|
|
21
|
+
</Suspense>
|
|
22
|
+
)
|
|
23
|
+
}
|