@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,166 @@
|
|
|
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 { Textarea } from "@/components/ui/textarea";
|
|
17
|
+
import { UserX, UserCheck, Loader2, AlertTriangle } from "lucide-react";
|
|
18
|
+
import { deactivateUser, reactivateUser } from "@/lib/actions/users";
|
|
19
|
+
|
|
20
|
+
interface UserStatusDialogProps {
|
|
21
|
+
userId: string;
|
|
22
|
+
userName: string;
|
|
23
|
+
currentStatus: "active" | "invited" | "deactivated" | "archived" | string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function UserStatusDialog({
|
|
27
|
+
userId,
|
|
28
|
+
userName,
|
|
29
|
+
currentStatus,
|
|
30
|
+
}: UserStatusDialogProps) {
|
|
31
|
+
const router = useRouter();
|
|
32
|
+
const [open, setOpen] = useState(false);
|
|
33
|
+
const [loading, setLoading] = useState(false);
|
|
34
|
+
const [reason, setReason] = useState("");
|
|
35
|
+
const [error, setError] = useState("");
|
|
36
|
+
|
|
37
|
+
const isDeactivating = currentStatus === "active";
|
|
38
|
+
|
|
39
|
+
const handleStatusChange = async (e: React.FormEvent) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
if (isDeactivating && !reason.trim()) {
|
|
42
|
+
setError("Please provide a reason for deactivating this user (required for 21 CFR Part 11 audit compliance).");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setLoading(true);
|
|
47
|
+
setError("");
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const result = isDeactivating
|
|
51
|
+
? await deactivateUser(userId, reason.trim())
|
|
52
|
+
: await reactivateUser(userId, reason.trim() || undefined);
|
|
53
|
+
|
|
54
|
+
if (result.success) {
|
|
55
|
+
setOpen(false);
|
|
56
|
+
setReason("");
|
|
57
|
+
router.refresh();
|
|
58
|
+
} else {
|
|
59
|
+
setError((result as any)?.error || "Failed to update user status");
|
|
60
|
+
}
|
|
61
|
+
} catch (err: any) {
|
|
62
|
+
setError(err?.message || "An unexpected error occurred");
|
|
63
|
+
} finally {
|
|
64
|
+
setLoading(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
70
|
+
<DialogTrigger asChild>
|
|
71
|
+
{isDeactivating ? (
|
|
72
|
+
<Button variant="destructive" size="sm" className="min-h-[38px]">
|
|
73
|
+
<UserX className="w-4 h-4 mr-2" />
|
|
74
|
+
Deactivate User
|
|
75
|
+
</Button>
|
|
76
|
+
) : (
|
|
77
|
+
<Button
|
|
78
|
+
variant="default"
|
|
79
|
+
size="sm"
|
|
80
|
+
className="bg-green-600 hover:bg-green-700 min-h-[38px]"
|
|
81
|
+
>
|
|
82
|
+
<UserCheck className="w-4 h-4 mr-2" />
|
|
83
|
+
Reactivate User
|
|
84
|
+
</Button>
|
|
85
|
+
)}
|
|
86
|
+
</DialogTrigger>
|
|
87
|
+
|
|
88
|
+
<DialogContent className="sm:max-w-[450px]">
|
|
89
|
+
<form onSubmit={handleStatusChange} className="space-y-4">
|
|
90
|
+
<DialogHeader>
|
|
91
|
+
<div className="flex items-center gap-2">
|
|
92
|
+
{isDeactivating ? (
|
|
93
|
+
<div className="p-2 rounded-full bg-red-100 text-red-600">
|
|
94
|
+
<AlertTriangle className="w-5 h-5" />
|
|
95
|
+
</div>
|
|
96
|
+
) : (
|
|
97
|
+
<div className="p-2 rounded-full bg-green-100 text-green-600">
|
|
98
|
+
<UserCheck className="w-5 h-5" />
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
<DialogTitle>
|
|
102
|
+
{isDeactivating ? `Deactivate ${userName}` : `Reactivate ${userName}`}
|
|
103
|
+
</DialogTitle>
|
|
104
|
+
</div>
|
|
105
|
+
<DialogDescription className="text-xs pt-1">
|
|
106
|
+
{isDeactivating
|
|
107
|
+
? "Deactivating will revoke immediate login access while preserving all historical audit logs, digital signatures, and task records."
|
|
108
|
+
: "Reactivating will restore login access and allow role assignments for this user."}
|
|
109
|
+
</DialogDescription>
|
|
110
|
+
</DialogHeader>
|
|
111
|
+
|
|
112
|
+
{error && (
|
|
113
|
+
<div className="bg-destructive/10 text-destructive text-xs p-2.5 rounded-md border border-destructive/20">
|
|
114
|
+
{error}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
<div className="space-y-2">
|
|
119
|
+
<Label htmlFor="status-reason" className="text-xs font-medium">
|
|
120
|
+
{isDeactivating ? "Reason for Deactivation *" : "Notes / Change Reason"}
|
|
121
|
+
</Label>
|
|
122
|
+
<Textarea
|
|
123
|
+
id="status-reason"
|
|
124
|
+
value={reason}
|
|
125
|
+
onChange={(e) => setReason(e.target.value)}
|
|
126
|
+
placeholder={
|
|
127
|
+
isDeactivating
|
|
128
|
+
? "e.g. Employee departure, temporary suspension, or role transfer..."
|
|
129
|
+
: "e.g. Returned from leave, reactivation approved..."
|
|
130
|
+
}
|
|
131
|
+
className="text-xs resize-none"
|
|
132
|
+
rows={3}
|
|
133
|
+
required={isDeactivating}
|
|
134
|
+
/>
|
|
135
|
+
<p className="text-[11px] text-muted-foreground">
|
|
136
|
+
This note will be permanently recorded in the 21 CFR Part 11 system audit trail.
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<DialogFooter className="gap-2 sm:gap-0 pt-2">
|
|
141
|
+
<Button
|
|
142
|
+
type="button"
|
|
143
|
+
variant="outline"
|
|
144
|
+
size="sm"
|
|
145
|
+
onClick={() => setOpen(false)}
|
|
146
|
+
disabled={loading}
|
|
147
|
+
>
|
|
148
|
+
Cancel
|
|
149
|
+
</Button>
|
|
150
|
+
<Button
|
|
151
|
+
type="submit"
|
|
152
|
+
variant={isDeactivating ? "destructive" : "default"}
|
|
153
|
+
size="sm"
|
|
154
|
+
disabled={loading}
|
|
155
|
+
className={!isDeactivating ? "bg-green-600 hover:bg-green-700" : ""}
|
|
156
|
+
>
|
|
157
|
+
{loading && <Loader2 className="w-3.5 h-3.5 mr-1.5 animate-spin" />}
|
|
158
|
+
{isDeactivating ? "Confirm Deactivation" : "Confirm Reactivation"}
|
|
159
|
+
</Button>
|
|
160
|
+
</DialogFooter>
|
|
161
|
+
</form>
|
|
162
|
+
</DialogContent>
|
|
163
|
+
</Dialog>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
4
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { PageHeader } from '@/components/shared/page-header';
|
|
7
|
+
import { StatusBadge } from '@/components/shared/status-badge';
|
|
8
|
+
import { Button } from '@/components/ui/button';
|
|
9
|
+
import { Badge } from '@/components/ui/badge';
|
|
10
|
+
import { Input } from '@/components/ui/input';
|
|
11
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
12
|
+
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
13
|
+
import {
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue,
|
|
19
|
+
} from '@/components/ui/select';
|
|
20
|
+
import {
|
|
21
|
+
Plus,
|
|
22
|
+
Eye,
|
|
23
|
+
Edit,
|
|
24
|
+
Search,
|
|
25
|
+
Users,
|
|
26
|
+
Wrench,
|
|
27
|
+
Stethoscope,
|
|
28
|
+
ShieldCheck,
|
|
29
|
+
Building2,
|
|
30
|
+
CheckCircle2,
|
|
31
|
+
FilterX,
|
|
32
|
+
} from 'lucide-react';
|
|
33
|
+
import { AddUserTypeDialog, UserTypeSelection } from '@/components/admin/add-user-type-dialog';
|
|
34
|
+
import { AddEngineerDialog } from '@/components/admin/add-engineer-dialog';
|
|
35
|
+
import { AddDepartmentUserDialog } from '@/components/admin/add-department-user-dialog';
|
|
36
|
+
|
|
37
|
+
export interface UserItem {
|
|
38
|
+
id: string;
|
|
39
|
+
fullName: string;
|
|
40
|
+
email: string;
|
|
41
|
+
employeeIdentifier?: string | null;
|
|
42
|
+
jobTitle?: string | null;
|
|
43
|
+
phone?: string | null;
|
|
44
|
+
accountStatus?: string | null;
|
|
45
|
+
status?: string | null;
|
|
46
|
+
lastLoginAt?: Date | string | null;
|
|
47
|
+
roles?: { id: string; code: string; name: string }[];
|
|
48
|
+
scopes?: any[];
|
|
49
|
+
departmentName?: string | null;
|
|
50
|
+
hospitalName?: string | null;
|
|
51
|
+
userType?: 'engineer' | 'department' | 'admin' | 'other';
|
|
52
|
+
createdAt?: Date | string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface UsersPageClientProps {
|
|
56
|
+
users: UserItem[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function UsersPageClient({ users }: UsersPageClientProps) {
|
|
60
|
+
const router = useRouter();
|
|
61
|
+
const searchParams = useSearchParams();
|
|
62
|
+
|
|
63
|
+
// Dialog states
|
|
64
|
+
const [isTypeDialogOpen, setIsTypeDialogOpen] = useState(false);
|
|
65
|
+
const [isEngineerDialogOpen, setIsEngineerDialogOpen] = useState(false);
|
|
66
|
+
const [isDepartmentDialogOpen, setIsDepartmentDialogOpen] = useState(false);
|
|
67
|
+
|
|
68
|
+
// Check URL query param for auto-opening add dialog (e.g., /admin/users?action=new)
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (searchParams.get('action') === 'new') {
|
|
71
|
+
setIsTypeDialogOpen(true);
|
|
72
|
+
}
|
|
73
|
+
}, [searchParams]);
|
|
74
|
+
|
|
75
|
+
// Search & Filter state
|
|
76
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
77
|
+
const [categoryFilter, setCategoryFilter] = useState<'all' | 'engineer' | 'department' | 'admin'>('all');
|
|
78
|
+
const [statusFilter, setStatusFilter] = useState<string>('all');
|
|
79
|
+
|
|
80
|
+
const handleSelectType = (type: UserTypeSelection) => {
|
|
81
|
+
setIsTypeDialogOpen(false);
|
|
82
|
+
if (type === 'engineer') {
|
|
83
|
+
setIsEngineerDialogOpen(true);
|
|
84
|
+
} else {
|
|
85
|
+
setIsDepartmentDialogOpen(true);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handleSuccess = () => {
|
|
90
|
+
router.refresh();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Metrics counts
|
|
94
|
+
const stats = useMemo(() => {
|
|
95
|
+
const total = users.length;
|
|
96
|
+
const engineers = users.filter((u) => u.userType === 'engineer').length;
|
|
97
|
+
const departmentStaff = users.filter((u) => u.userType === 'department').length;
|
|
98
|
+
const activeAccounts = users.filter((u) => (u.accountStatus || u.status) === 'active').length;
|
|
99
|
+
const admins = users.filter((u) => u.userType === 'admin').length;
|
|
100
|
+
|
|
101
|
+
return { total, engineers, departmentStaff, activeAccounts, admins };
|
|
102
|
+
}, [users]);
|
|
103
|
+
|
|
104
|
+
// Filtered users
|
|
105
|
+
const filteredUsers = useMemo(() => {
|
|
106
|
+
return users.filter((u) => {
|
|
107
|
+
// 1. Category tab filter
|
|
108
|
+
if (categoryFilter !== 'all' && u.userType !== categoryFilter) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 2. Status filter
|
|
113
|
+
const userStatus = u.accountStatus || u.status || '';
|
|
114
|
+
if (statusFilter !== 'all' && userStatus !== statusFilter) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 3. Search query (name, email, identifier, title, department, hospital)
|
|
119
|
+
if (searchQuery.trim()) {
|
|
120
|
+
const query = searchQuery.toLowerCase().trim();
|
|
121
|
+
const matchesName = u.fullName?.toLowerCase().includes(query);
|
|
122
|
+
const matchesEmail = u.email?.toLowerCase().includes(query);
|
|
123
|
+
const matchesIdentifier = u.employeeIdentifier?.toLowerCase().includes(query);
|
|
124
|
+
const matchesTitle = u.jobTitle?.toLowerCase().includes(query);
|
|
125
|
+
const matchesDept = u.departmentName?.toLowerCase().includes(query);
|
|
126
|
+
const matchesHosp = u.hospitalName?.toLowerCase().includes(query);
|
|
127
|
+
const matchesRole = u.roles?.some((r) =>
|
|
128
|
+
r.name?.toLowerCase().includes(query) || r.code?.toLowerCase().includes(query)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (!matchesName && !matchesEmail && !matchesIdentifier && !matchesTitle && !matchesDept && !matchesHosp && !matchesRole) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return true;
|
|
137
|
+
});
|
|
138
|
+
}, [users, categoryFilter, statusFilter, searchQuery]);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className="space-y-6">
|
|
142
|
+
{/* ── Page Header ── */}
|
|
143
|
+
<PageHeader
|
|
144
|
+
title="User Governance"
|
|
145
|
+
description="Provision, configure, and monitor biomedical engineers, department clinicians, and system administrators"
|
|
146
|
+
>
|
|
147
|
+
<Button onClick={() => setIsTypeDialogOpen(true)} className="gap-2 shadow-xs">
|
|
148
|
+
<Plus className="w-4 h-4" />
|
|
149
|
+
Add User
|
|
150
|
+
</Button>
|
|
151
|
+
</PageHeader>
|
|
152
|
+
|
|
153
|
+
{/* ── Key Metrics Cards ── */}
|
|
154
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
155
|
+
<Card className="border shadow-2xs">
|
|
156
|
+
<CardContent className="p-4 flex items-center justify-between">
|
|
157
|
+
<div className="space-y-0.5">
|
|
158
|
+
<span className="text-xs font-medium text-muted-foreground">Total Users</span>
|
|
159
|
+
<p className="text-2xl font-bold tracking-tight text-foreground">{stats.total}</p>
|
|
160
|
+
</div>
|
|
161
|
+
<div className="w-10 h-10 rounded-xl bg-muted/60 flex items-center justify-center text-muted-foreground">
|
|
162
|
+
<Users className="w-5 h-5" />
|
|
163
|
+
</div>
|
|
164
|
+
</CardContent>
|
|
165
|
+
</Card>
|
|
166
|
+
|
|
167
|
+
<Card className="border shadow-2xs">
|
|
168
|
+
<CardContent className="p-4 flex items-center justify-between">
|
|
169
|
+
<div className="space-y-0.5">
|
|
170
|
+
<span className="text-xs font-medium text-muted-foreground">Biomedical Engineers</span>
|
|
171
|
+
<p className="text-2xl font-bold tracking-tight text-blue-600 dark:text-blue-400">{stats.engineers}</p>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="w-10 h-10 rounded-xl bg-blue-100/80 text-blue-600 dark:bg-blue-950/60 dark:text-blue-400 flex items-center justify-center">
|
|
174
|
+
<Wrench className="w-5 h-5" />
|
|
175
|
+
</div>
|
|
176
|
+
</CardContent>
|
|
177
|
+
</Card>
|
|
178
|
+
|
|
179
|
+
<Card className="border shadow-2xs">
|
|
180
|
+
<CardContent className="p-4 flex items-center justify-between">
|
|
181
|
+
<div className="space-y-0.5">
|
|
182
|
+
<span className="text-xs font-medium text-muted-foreground">Clinical & Dept Staff</span>
|
|
183
|
+
<p className="text-2xl font-bold tracking-tight text-purple-600 dark:text-purple-400">{stats.departmentStaff}</p>
|
|
184
|
+
</div>
|
|
185
|
+
<div className="w-10 h-10 rounded-xl bg-purple-100/80 text-purple-600 dark:bg-purple-950/60 dark:text-purple-400 flex items-center justify-center">
|
|
186
|
+
<Stethoscope className="w-5 h-5" />
|
|
187
|
+
</div>
|
|
188
|
+
</CardContent>
|
|
189
|
+
</Card>
|
|
190
|
+
|
|
191
|
+
<Card className="border shadow-2xs">
|
|
192
|
+
<CardContent className="p-4 flex items-center justify-between">
|
|
193
|
+
<div className="space-y-0.5">
|
|
194
|
+
<span className="text-xs font-medium text-muted-foreground">Active Accounts</span>
|
|
195
|
+
<p className="text-2xl font-bold tracking-tight text-emerald-600 dark:text-emerald-400">{stats.activeAccounts}</p>
|
|
196
|
+
</div>
|
|
197
|
+
<div className="w-10 h-10 rounded-xl bg-emerald-100/80 text-emerald-600 dark:bg-emerald-950/60 dark:text-emerald-400 flex items-center justify-center">
|
|
198
|
+
<CheckCircle2 className="w-5 h-5" />
|
|
199
|
+
</div>
|
|
200
|
+
</CardContent>
|
|
201
|
+
</Card>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{/* ── Search & Filter Controls ── */}
|
|
205
|
+
<div className="space-y-3">
|
|
206
|
+
<div className="flex flex-col md:flex-row gap-3 items-stretch md:items-center justify-between">
|
|
207
|
+
{/* Category Tabs */}
|
|
208
|
+
<Tabs
|
|
209
|
+
value={categoryFilter}
|
|
210
|
+
onValueChange={(val) => setCategoryFilter(val as any)}
|
|
211
|
+
className="w-full md:w-auto"
|
|
212
|
+
>
|
|
213
|
+
<TabsList className="grid grid-cols-4 w-full md:w-auto">
|
|
214
|
+
<TabsTrigger value="all" className="text-xs font-medium gap-1 px-3">
|
|
215
|
+
All <span className="text-[10px] text-muted-foreground">({stats.total})</span>
|
|
216
|
+
</TabsTrigger>
|
|
217
|
+
<TabsTrigger value="engineer" className="text-xs font-medium gap-1 px-3">
|
|
218
|
+
<Wrench className="w-3.5 h-3.5 text-blue-500" />
|
|
219
|
+
Engineers <span className="text-[10px] text-muted-foreground">({stats.engineers})</span>
|
|
220
|
+
</TabsTrigger>
|
|
221
|
+
<TabsTrigger value="department" className="text-xs font-medium gap-1 px-3">
|
|
222
|
+
<Stethoscope className="w-3.5 h-3.5 text-purple-500" />
|
|
223
|
+
Staff <span className="text-[10px] text-muted-foreground">({stats.departmentStaff})</span>
|
|
224
|
+
</TabsTrigger>
|
|
225
|
+
<TabsTrigger value="admin" className="text-xs font-medium gap-1 px-3">
|
|
226
|
+
<ShieldCheck className="w-3.5 h-3.5 text-amber-500" />
|
|
227
|
+
Admins <span className="text-[10px] text-muted-foreground">({stats.admins})</span>
|
|
228
|
+
</TabsTrigger>
|
|
229
|
+
</TabsList>
|
|
230
|
+
</Tabs>
|
|
231
|
+
|
|
232
|
+
{/* Search and Status Dropdown */}
|
|
233
|
+
<div className="flex items-center gap-2 w-full md:w-auto">
|
|
234
|
+
<div className="relative flex-1 md:w-64">
|
|
235
|
+
<Search className="w-4 h-4 absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
236
|
+
<Input
|
|
237
|
+
placeholder="Search users, email, dept..."
|
|
238
|
+
value={searchQuery}
|
|
239
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
240
|
+
className="pl-8 text-xs h-9 bg-background"
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
245
|
+
<SelectTrigger className="w-[130px] h-9 text-xs">
|
|
246
|
+
<SelectValue placeholder="Status" />
|
|
247
|
+
</SelectTrigger>
|
|
248
|
+
<SelectContent>
|
|
249
|
+
<SelectItem value="all">All Statuses</SelectItem>
|
|
250
|
+
<SelectItem value="active">Active</SelectItem>
|
|
251
|
+
<SelectItem value="invited">Invited</SelectItem>
|
|
252
|
+
<SelectItem value="deactivated">Deactivated</SelectItem>
|
|
253
|
+
</SelectContent>
|
|
254
|
+
</Select>
|
|
255
|
+
|
|
256
|
+
{(searchQuery || statusFilter !== 'all' || categoryFilter !== 'all') && (
|
|
257
|
+
<Button
|
|
258
|
+
variant="ghost"
|
|
259
|
+
size="icon"
|
|
260
|
+
onClick={() => {
|
|
261
|
+
setSearchQuery('');
|
|
262
|
+
setStatusFilter('all');
|
|
263
|
+
setCategoryFilter('all');
|
|
264
|
+
}}
|
|
265
|
+
className="h-9 w-9 text-muted-foreground hover:text-foreground"
|
|
266
|
+
title="Clear filters"
|
|
267
|
+
>
|
|
268
|
+
<FilterX className="w-4 h-4" />
|
|
269
|
+
</Button>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
{/* ── Users Table ── */}
|
|
275
|
+
<div className="rounded-xl border bg-card shadow-2xs overflow-hidden">
|
|
276
|
+
<div className="overflow-x-auto">
|
|
277
|
+
<table className="w-full text-left text-sm border-collapse">
|
|
278
|
+
<thead>
|
|
279
|
+
<tr className="border-b bg-muted/40 text-xs font-semibold text-muted-foreground">
|
|
280
|
+
<th className="py-3 px-4">User</th>
|
|
281
|
+
<th className="py-3 px-3">Type</th>
|
|
282
|
+
<th className="py-3 px-3">Roles</th>
|
|
283
|
+
<th className="py-3 px-3">Facility / Department</th>
|
|
284
|
+
<th className="py-3 px-3">Status</th>
|
|
285
|
+
<th className="py-3 px-3">Last Login</th>
|
|
286
|
+
<th className="py-3 px-4 text-right">Actions</th>
|
|
287
|
+
</tr>
|
|
288
|
+
</thead>
|
|
289
|
+
<tbody className="divide-y divide-border/60">
|
|
290
|
+
{filteredUsers.length > 0 ? (
|
|
291
|
+
filteredUsers.map((item) => {
|
|
292
|
+
const initials = item.fullName
|
|
293
|
+
? item.fullName
|
|
294
|
+
.split(' ')
|
|
295
|
+
.map((n) => n[0])
|
|
296
|
+
.slice(0, 2)
|
|
297
|
+
.join('')
|
|
298
|
+
.toUpperCase()
|
|
299
|
+
: 'U';
|
|
300
|
+
|
|
301
|
+
const isEngineer = item.userType === 'engineer';
|
|
302
|
+
const isStaff = item.userType === 'department';
|
|
303
|
+
const isAdmin = item.userType === 'admin';
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<tr
|
|
307
|
+
key={item.id}
|
|
308
|
+
className="hover:bg-muted/30 transition-colors group"
|
|
309
|
+
>
|
|
310
|
+
{/* User Identity */}
|
|
311
|
+
<td className="py-3 px-4">
|
|
312
|
+
<div className="flex items-center gap-3">
|
|
313
|
+
<div
|
|
314
|
+
className={`w-9 h-9 rounded-full flex items-center justify-center font-bold text-xs shrink-0 ${
|
|
315
|
+
isEngineer
|
|
316
|
+
? 'bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300'
|
|
317
|
+
: isStaff
|
|
318
|
+
? 'bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-300'
|
|
319
|
+
: 'bg-muted text-foreground'
|
|
320
|
+
}`}
|
|
321
|
+
>
|
|
322
|
+
{initials}
|
|
323
|
+
</div>
|
|
324
|
+
<div className="flex flex-col min-w-0">
|
|
325
|
+
<span className="font-semibold text-foreground text-xs leading-snug truncate">
|
|
326
|
+
{item.fullName}
|
|
327
|
+
</span>
|
|
328
|
+
<span className="text-[11px] text-muted-foreground truncate">
|
|
329
|
+
{item.email}
|
|
330
|
+
</span>
|
|
331
|
+
{item.employeeIdentifier && (
|
|
332
|
+
<span className="font-mono text-[10px] text-muted-foreground">
|
|
333
|
+
ID: {item.employeeIdentifier}
|
|
334
|
+
</span>
|
|
335
|
+
)}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</td>
|
|
339
|
+
|
|
340
|
+
{/* Account Type Badge */}
|
|
341
|
+
<td className="py-3 px-3 whitespace-nowrap">
|
|
342
|
+
{isEngineer ? (
|
|
343
|
+
<Badge
|
|
344
|
+
variant="outline"
|
|
345
|
+
className="text-[11px] gap-1 border-blue-200 bg-blue-50/70 text-blue-700 dark:border-blue-900/60 dark:bg-blue-950/40 dark:text-blue-300 font-medium"
|
|
346
|
+
>
|
|
347
|
+
<Wrench className="w-3 h-3 text-blue-500" />
|
|
348
|
+
Biomed Engineer
|
|
349
|
+
</Badge>
|
|
350
|
+
) : isStaff ? (
|
|
351
|
+
<Badge
|
|
352
|
+
variant="outline"
|
|
353
|
+
className="text-[11px] gap-1 border-purple-200 bg-purple-50/70 text-purple-700 dark:border-purple-900/60 dark:bg-purple-950/40 dark:text-purple-300 font-medium"
|
|
354
|
+
>
|
|
355
|
+
<Stethoscope className="w-3 h-3 text-purple-500" />
|
|
356
|
+
Clinical Staff
|
|
357
|
+
</Badge>
|
|
358
|
+
) : isAdmin ? (
|
|
359
|
+
<Badge
|
|
360
|
+
variant="outline"
|
|
361
|
+
className="text-[11px] gap-1 border-amber-200 bg-amber-50/70 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/40 dark:text-amber-300 font-medium"
|
|
362
|
+
>
|
|
363
|
+
<ShieldCheck className="w-3 h-3 text-amber-500" />
|
|
364
|
+
Administrator
|
|
365
|
+
</Badge>
|
|
366
|
+
) : (
|
|
367
|
+
<Badge variant="outline" className="text-[11px] text-muted-foreground font-medium">
|
|
368
|
+
User
|
|
369
|
+
</Badge>
|
|
370
|
+
)}
|
|
371
|
+
</td>
|
|
372
|
+
|
|
373
|
+
{/* Roles */}
|
|
374
|
+
<td className="py-3 px-3">
|
|
375
|
+
<div className="flex gap-1 flex-wrap max-w-xs">
|
|
376
|
+
{item.roles && item.roles.length > 0 ? (
|
|
377
|
+
item.roles.map((role) => (
|
|
378
|
+
<Badge
|
|
379
|
+
key={role.id || role.code}
|
|
380
|
+
variant="secondary"
|
|
381
|
+
className="text-[10px] px-1.5 py-0 font-normal"
|
|
382
|
+
>
|
|
383
|
+
{role.name || role.code}
|
|
384
|
+
</Badge>
|
|
385
|
+
))
|
|
386
|
+
) : (
|
|
387
|
+
<span className="text-muted-foreground text-xs italic">No roles</span>
|
|
388
|
+
)}
|
|
389
|
+
</div>
|
|
390
|
+
</td>
|
|
391
|
+
|
|
392
|
+
{/* Facility / Department */}
|
|
393
|
+
<td className="py-3 px-3">
|
|
394
|
+
<div className="flex flex-col text-xs">
|
|
395
|
+
{item.departmentName ? (
|
|
396
|
+
<span className="font-medium text-foreground flex items-center gap-1">
|
|
397
|
+
<Building2 className="w-3 h-3 text-muted-foreground" />
|
|
398
|
+
{item.departmentName}
|
|
399
|
+
</span>
|
|
400
|
+
) : (
|
|
401
|
+
<span className="text-muted-foreground text-xs italic">
|
|
402
|
+
Facility-wide
|
|
403
|
+
</span>
|
|
404
|
+
)}
|
|
405
|
+
{item.hospitalName && (
|
|
406
|
+
<span className="text-[11px] text-muted-foreground">
|
|
407
|
+
{item.hospitalName}
|
|
408
|
+
</span>
|
|
409
|
+
)}
|
|
410
|
+
</div>
|
|
411
|
+
</td>
|
|
412
|
+
|
|
413
|
+
{/* Status */}
|
|
414
|
+
<td className="py-3 px-3 whitespace-nowrap">
|
|
415
|
+
<StatusBadge status={item.accountStatus || item.status} />
|
|
416
|
+
</td>
|
|
417
|
+
|
|
418
|
+
{/* Last Login */}
|
|
419
|
+
<td className="py-3 px-3 whitespace-nowrap text-xs text-muted-foreground">
|
|
420
|
+
{item.lastLoginAt ? (
|
|
421
|
+
new Date(item.lastLoginAt).toLocaleDateString(undefined, {
|
|
422
|
+
month: 'short',
|
|
423
|
+
day: 'numeric',
|
|
424
|
+
year: 'numeric',
|
|
425
|
+
})
|
|
426
|
+
) : (
|
|
427
|
+
<span className="italic text-muted-foreground/70">Never</span>
|
|
428
|
+
)}
|
|
429
|
+
</td>
|
|
430
|
+
|
|
431
|
+
{/* Actions */}
|
|
432
|
+
<td className="py-3 px-4 text-right whitespace-nowrap">
|
|
433
|
+
<div className="flex items-center justify-end gap-1">
|
|
434
|
+
<Button
|
|
435
|
+
variant="ghost"
|
|
436
|
+
size="icon"
|
|
437
|
+
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
438
|
+
asChild
|
|
439
|
+
title="View user details"
|
|
440
|
+
>
|
|
441
|
+
<Link href={`/admin/users/${item.id}`}>
|
|
442
|
+
<Eye className="w-4 h-4" />
|
|
443
|
+
</Link>
|
|
444
|
+
</Button>
|
|
445
|
+
<Button
|
|
446
|
+
variant="ghost"
|
|
447
|
+
size="icon"
|
|
448
|
+
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
449
|
+
asChild
|
|
450
|
+
title="Edit user profile"
|
|
451
|
+
>
|
|
452
|
+
<Link href={`/admin/users/${item.id}/edit`}>
|
|
453
|
+
<Edit className="w-4 h-4" />
|
|
454
|
+
</Link>
|
|
455
|
+
</Button>
|
|
456
|
+
</div>
|
|
457
|
+
</td>
|
|
458
|
+
</tr>
|
|
459
|
+
);
|
|
460
|
+
})
|
|
461
|
+
) : (
|
|
462
|
+
<tr>
|
|
463
|
+
<td colSpan={7} className="text-center py-12 text-muted-foreground">
|
|
464
|
+
<div className="flex flex-col items-center justify-center space-y-2">
|
|
465
|
+
<Users className="w-8 h-8 text-muted-foreground/50" />
|
|
466
|
+
<p className="text-sm font-medium">No users found</p>
|
|
467
|
+
<p className="text-xs text-muted-foreground/80 max-w-xs">
|
|
468
|
+
{searchQuery || statusFilter !== 'all' || categoryFilter !== 'all'
|
|
469
|
+
? 'Try adjusting your search terms or active filters.'
|
|
470
|
+
: 'Get started by adding your first biomedical engineer or department staff member.'}
|
|
471
|
+
</p>
|
|
472
|
+
{!searchQuery && statusFilter === 'all' && categoryFilter === 'all' && (
|
|
473
|
+
<Button
|
|
474
|
+
size="sm"
|
|
475
|
+
onClick={() => setIsTypeDialogOpen(true)}
|
|
476
|
+
className="mt-2 gap-1.5"
|
|
477
|
+
>
|
|
478
|
+
<Plus className="w-3.5 h-3.5" />
|
|
479
|
+
Add User
|
|
480
|
+
</Button>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
</td>
|
|
484
|
+
</tr>
|
|
485
|
+
)}
|
|
486
|
+
</tbody>
|
|
487
|
+
</table>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
{/* ── Dialog 1: Add User Type Selector ── */}
|
|
493
|
+
<AddUserTypeDialog
|
|
494
|
+
open={isTypeDialogOpen}
|
|
495
|
+
onOpenChange={setIsTypeDialogOpen}
|
|
496
|
+
onSelect={handleSelectType}
|
|
497
|
+
/>
|
|
498
|
+
|
|
499
|
+
{/* ── Dialog 2a: Add Biomedical Engineer ── */}
|
|
500
|
+
<AddEngineerDialog
|
|
501
|
+
open={isEngineerDialogOpen}
|
|
502
|
+
onOpenChange={setIsEngineerDialogOpen}
|
|
503
|
+
onSuccess={handleSuccess}
|
|
504
|
+
/>
|
|
505
|
+
|
|
506
|
+
{/* ── Dialog 2b: Add Department Staff ── */}
|
|
507
|
+
<AddDepartmentUserDialog
|
|
508
|
+
open={isDepartmentDialogOpen}
|
|
509
|
+
onOpenChange={setIsDepartmentDialogOpen}
|
|
510
|
+
onSuccess={handleSuccess}
|
|
511
|
+
/>
|
|
512
|
+
</div>
|
|
513
|
+
);
|
|
514
|
+
}
|