@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,607 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { Card, CardContent } from "@/components/ui/card"
|
|
5
|
+
import { Separator } from "@/components/ui/separator"
|
|
6
|
+
import { Badge } from "@/components/ui/badge"
|
|
7
|
+
import { Button } from "@/components/ui/button"
|
|
8
|
+
import {
|
|
9
|
+
ShieldCheck,
|
|
10
|
+
TicketCheck,
|
|
11
|
+
Wrench,
|
|
12
|
+
ClipboardCheck,
|
|
13
|
+
Gauge,
|
|
14
|
+
RotateCcw,
|
|
15
|
+
RefreshCw,
|
|
16
|
+
ExternalLink,
|
|
17
|
+
ChevronDown,
|
|
18
|
+
ChevronUp,
|
|
19
|
+
CheckCircle2,
|
|
20
|
+
ArrowRight,
|
|
21
|
+
Clock,
|
|
22
|
+
User,
|
|
23
|
+
Hash,
|
|
24
|
+
Copy,
|
|
25
|
+
Check,
|
|
26
|
+
MessageSquare,
|
|
27
|
+
Lock,
|
|
28
|
+
} from "lucide-react"
|
|
29
|
+
import Link from "next/link"
|
|
30
|
+
import { cn } from "@/lib/utils"
|
|
31
|
+
import { SignatureHistory } from "@/components/signatures/signature-history"
|
|
32
|
+
import type { ElectronicSignatureItem } from "@/components/maintenance/signature-card"
|
|
33
|
+
|
|
34
|
+
// ── Types & Activity Configuration ──────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
interface SourceGroup {
|
|
37
|
+
key: string
|
|
38
|
+
sourceType: string
|
|
39
|
+
sourceLabel: string
|
|
40
|
+
sourceNumber: string
|
|
41
|
+
sourceHref: string
|
|
42
|
+
signatures: ElectronicSignatureItem[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ACTIVITY_CONFIG: Record<
|
|
46
|
+
string,
|
|
47
|
+
{
|
|
48
|
+
icon: React.ComponentType<{ className?: string }>
|
|
49
|
+
label: string
|
|
50
|
+
badgeClass: string
|
|
51
|
+
borderClass: string
|
|
52
|
+
bgLight: string
|
|
53
|
+
}
|
|
54
|
+
> = {
|
|
55
|
+
ticket_resolution: {
|
|
56
|
+
icon: TicketCheck,
|
|
57
|
+
label: "Service Ticket",
|
|
58
|
+
badgeClass: "bg-blue-500/10 text-blue-400 border-blue-500/20",
|
|
59
|
+
borderClass: "border-l-blue-500",
|
|
60
|
+
bgLight: "hover:bg-blue-500/5",
|
|
61
|
+
},
|
|
62
|
+
preventive_maintenance: {
|
|
63
|
+
icon: ClipboardCheck,
|
|
64
|
+
label: "Preventive Maintenance (PM)",
|
|
65
|
+
badgeClass: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20",
|
|
66
|
+
borderClass: "border-l-emerald-500",
|
|
67
|
+
bgLight: "hover:bg-emerald-500/5",
|
|
68
|
+
},
|
|
69
|
+
calibration: {
|
|
70
|
+
icon: Gauge,
|
|
71
|
+
label: "Calibration",
|
|
72
|
+
badgeClass: "bg-purple-500/10 text-purple-400 border-purple-500/20",
|
|
73
|
+
borderClass: "border-l-purple-500",
|
|
74
|
+
bgLight: "hover:bg-purple-500/5",
|
|
75
|
+
},
|
|
76
|
+
corrective_maintenance: {
|
|
77
|
+
icon: Wrench,
|
|
78
|
+
label: "Corrective Maintenance",
|
|
79
|
+
badgeClass: "bg-indigo-500/10 text-indigo-400 border-indigo-500/20",
|
|
80
|
+
borderClass: "border-l-indigo-500",
|
|
81
|
+
bgLight: "hover:bg-indigo-500/5",
|
|
82
|
+
},
|
|
83
|
+
engineer_round: {
|
|
84
|
+
icon: RotateCcw,
|
|
85
|
+
label: "Daily Round / Inspection",
|
|
86
|
+
badgeClass: "bg-cyan-500/10 text-cyan-400 border-cyan-500/20",
|
|
87
|
+
borderClass: "border-l-cyan-500",
|
|
88
|
+
bgLight: "hover:bg-cyan-500/5",
|
|
89
|
+
},
|
|
90
|
+
device_status_change: {
|
|
91
|
+
icon: RefreshCw,
|
|
92
|
+
label: "Device Status Change",
|
|
93
|
+
badgeClass: "bg-amber-500/10 text-amber-400 border-amber-500/20",
|
|
94
|
+
borderClass: "border-l-amber-500",
|
|
95
|
+
bgLight: "hover:bg-amber-500/5",
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const PURPOSE_LABELS: Record<string, string> = {
|
|
100
|
+
accept: "Work Accepted",
|
|
101
|
+
resolve: "Resolution Sign-Off",
|
|
102
|
+
close: "Ticket Closed",
|
|
103
|
+
perform: "Work Executed",
|
|
104
|
+
review: "Technical Review",
|
|
105
|
+
release: "Clinical Release",
|
|
106
|
+
approve: "Approved",
|
|
107
|
+
reject: "Rejected",
|
|
108
|
+
amend: "Amended",
|
|
109
|
+
void: "Voided",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatShortDate(dateValue: Date | string): string {
|
|
113
|
+
try {
|
|
114
|
+
const d = new Date(dateValue)
|
|
115
|
+
return d.toLocaleDateString("en-US", {
|
|
116
|
+
month: "short",
|
|
117
|
+
day: "numeric",
|
|
118
|
+
hour: "2-digit",
|
|
119
|
+
minute: "2-digit",
|
|
120
|
+
})
|
|
121
|
+
} catch {
|
|
122
|
+
return String(dateValue)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatFullTimestamp(
|
|
127
|
+
dateValue: Date | string,
|
|
128
|
+
timezone?: string | null
|
|
129
|
+
): string {
|
|
130
|
+
try {
|
|
131
|
+
const d = new Date(dateValue)
|
|
132
|
+
const formatted = d.toLocaleString("en-US", {
|
|
133
|
+
year: "numeric",
|
|
134
|
+
month: "short",
|
|
135
|
+
day: "2-digit",
|
|
136
|
+
hour: "2-digit",
|
|
137
|
+
minute: "2-digit",
|
|
138
|
+
second: "2-digit",
|
|
139
|
+
hour12: true,
|
|
140
|
+
timeZoneName: "short",
|
|
141
|
+
})
|
|
142
|
+
return timezone && timezone !== "UTC" ? `${formatted} (${timezone})` : formatted
|
|
143
|
+
} catch {
|
|
144
|
+
return String(dateValue)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function groupSignaturesBySource(signatures: ElectronicSignatureItem[]): SourceGroup[] {
|
|
149
|
+
const groupMap = new Map<string, SourceGroup>()
|
|
150
|
+
|
|
151
|
+
for (const sig of signatures) {
|
|
152
|
+
const sourceType = (sig as any).sourceType || sig.entityType || "unknown"
|
|
153
|
+
const entityId = sig.entityId || "unknown"
|
|
154
|
+
const key = `${sourceType}:${entityId}`
|
|
155
|
+
|
|
156
|
+
if (!groupMap.has(key)) {
|
|
157
|
+
groupMap.set(key, {
|
|
158
|
+
key,
|
|
159
|
+
sourceType,
|
|
160
|
+
sourceLabel: (sig as any).sourceLabel || "",
|
|
161
|
+
sourceNumber: (sig as any).sourceNumber || "",
|
|
162
|
+
sourceHref: (sig as any).sourceHref || "",
|
|
163
|
+
signatures: [],
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
groupMap.get(key)!.signatures.push(sig)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Sort groups: newest signature first
|
|
170
|
+
const groups = Array.from(groupMap.values())
|
|
171
|
+
groups.sort((a, b) => {
|
|
172
|
+
const newestA = Math.max(...a.signatures.map((s) => new Date(s.signedAt).getTime()))
|
|
173
|
+
const newestB = Math.max(...b.signatures.map((s) => new Date(s.signedAt).getTime()))
|
|
174
|
+
return newestB - newestA
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Sort signatures within each group chronologically (oldest to newest)
|
|
178
|
+
for (const group of groups) {
|
|
179
|
+
group.signatures.sort((a, b) => new Date(a.signedAt).getTime() - new Date(b.signedAt).getTime())
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return groups
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Single Activity Collapsible Card ────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
function ActivitySignatureCard({ group }: { group: SourceGroup }) {
|
|
188
|
+
const [expanded, setExpanded] = useState(false)
|
|
189
|
+
const [copiedHash, setCopiedHash] = useState<string | null>(null)
|
|
190
|
+
|
|
191
|
+
const config = ACTIVITY_CONFIG[group.sourceType] || {
|
|
192
|
+
icon: ShieldCheck,
|
|
193
|
+
label: "Activity",
|
|
194
|
+
badgeClass: "bg-muted text-muted-foreground border-border",
|
|
195
|
+
borderClass: "border-l-slate-400",
|
|
196
|
+
bgLight: "hover:bg-muted/10",
|
|
197
|
+
}
|
|
198
|
+
const Icon = config.icon
|
|
199
|
+
|
|
200
|
+
const handleCopy = async (hash: string, e: React.MouseEvent) => {
|
|
201
|
+
e.stopPropagation()
|
|
202
|
+
try {
|
|
203
|
+
await navigator.clipboard.writeText(hash)
|
|
204
|
+
setCopiedHash(hash)
|
|
205
|
+
setTimeout(() => setCopiedHash(null), 2000)
|
|
206
|
+
} catch {
|
|
207
|
+
// Fallback
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const latestSignature = group.signatures[group.signatures.length - 1]
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<Card
|
|
215
|
+
className={cn(
|
|
216
|
+
"overflow-hidden border border-l-4 transition-all duration-200",
|
|
217
|
+
config.borderClass,
|
|
218
|
+
expanded ? "shadow-md bg-card" : "bg-card/70 hover:bg-card hover:border-slate-700"
|
|
219
|
+
)}
|
|
220
|
+
>
|
|
221
|
+
{/* ── 1. Compact Clickable Header ────────────────────────────────────── */}
|
|
222
|
+
<div
|
|
223
|
+
onClick={() => setExpanded(!expanded)}
|
|
224
|
+
className="p-4 cursor-pointer select-none space-y-3"
|
|
225
|
+
>
|
|
226
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
227
|
+
{/* Left: Badge + Identifier + Title */}
|
|
228
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
229
|
+
<Badge
|
|
230
|
+
variant="outline"
|
|
231
|
+
className={cn("inline-flex items-center gap-1.5 text-xs font-semibold px-2.5 py-1 shrink-0", config.badgeClass)}
|
|
232
|
+
>
|
|
233
|
+
<Icon className="h-3.5 w-3.5" />
|
|
234
|
+
<span>{config.label}</span>
|
|
235
|
+
</Badge>
|
|
236
|
+
|
|
237
|
+
<div className="min-w-0 flex items-center gap-2">
|
|
238
|
+
<span className="font-semibold text-sm text-foreground truncate">
|
|
239
|
+
{group.sourceNumber || group.sourceLabel || "Record"}
|
|
240
|
+
</span>
|
|
241
|
+
{group.sourceNumber && group.sourceLabel && (
|
|
242
|
+
<span className="text-xs text-muted-foreground truncate hidden sm:inline">
|
|
243
|
+
— {group.sourceLabel}
|
|
244
|
+
</span>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{/* Direct Link to Ticket / Task */}
|
|
249
|
+
{group.sourceHref && (
|
|
250
|
+
<Button
|
|
251
|
+
asChild
|
|
252
|
+
variant="ghost"
|
|
253
|
+
size="sm"
|
|
254
|
+
className="h-6 w-6 p-0 text-muted-foreground hover:text-primary hover:bg-primary/10 shrink-0"
|
|
255
|
+
onClick={(e) => e.stopPropagation()}
|
|
256
|
+
title="View full record"
|
|
257
|
+
>
|
|
258
|
+
<Link href={group.sourceHref}>
|
|
259
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
260
|
+
</Link>
|
|
261
|
+
</Button>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Right: Signature Count + Verified Badge + Toggle Button */}
|
|
266
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
267
|
+
<Badge
|
|
268
|
+
variant="secondary"
|
|
269
|
+
className="text-xs font-normal px-2 py-0.5"
|
|
270
|
+
>
|
|
271
|
+
{group.signatures.length} {group.signatures.length === 1 ? "Signature" : "Signatures"}
|
|
272
|
+
</Badge>
|
|
273
|
+
|
|
274
|
+
<Badge
|
|
275
|
+
variant="outline"
|
|
276
|
+
className="bg-emerald-500/10 text-emerald-500 border-emerald-500/20 text-xs hidden sm:inline-flex items-center gap-1"
|
|
277
|
+
>
|
|
278
|
+
<CheckCircle2 className="h-3 w-3" />
|
|
279
|
+
<span>Verified</span>
|
|
280
|
+
</Badge>
|
|
281
|
+
|
|
282
|
+
<Button
|
|
283
|
+
type="button"
|
|
284
|
+
variant="ghost"
|
|
285
|
+
size="sm"
|
|
286
|
+
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground gap-1"
|
|
287
|
+
>
|
|
288
|
+
<span>{expanded ? "Collapse" : "View Details"}</span>
|
|
289
|
+
{expanded ? (
|
|
290
|
+
<ChevronUp className="h-3.5 w-3.5 text-primary" />
|
|
291
|
+
) : (
|
|
292
|
+
<ChevronDown className="h-3.5 w-3.5" />
|
|
293
|
+
)}
|
|
294
|
+
</Button>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
{/* ── 2. Compact Lifecycle Progress Stepper ─────────────────────────── */}
|
|
299
|
+
<div className="pt-0.5">
|
|
300
|
+
<div className="flex flex-wrap items-center gap-2 py-0.5">
|
|
301
|
+
{group.signatures.map((sig, idx) => {
|
|
302
|
+
const purposeKey = sig.signaturePurpose?.toLowerCase() || ""
|
|
303
|
+
const purposeText = PURPOSE_LABELS[purposeKey] || sig.signaturePurpose
|
|
304
|
+
const signer = sig.signerNameSnapshot || sig.signerUser?.fullName || "Signer"
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div key={sig.id || idx} className="flex items-center gap-2 shrink-0">
|
|
308
|
+
<div className="flex items-center gap-1.5 px-2.5 py-1 rounded-md bg-muted/60 border border-border/70 text-xs">
|
|
309
|
+
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-500 shrink-0" />
|
|
310
|
+
<span className="font-medium text-foreground">{purposeText}</span>
|
|
311
|
+
<span className="text-muted-foreground text-[11px]">• {signer}</span>
|
|
312
|
+
<span className="text-muted-foreground/70 text-[10px] hidden md:inline">
|
|
313
|
+
({formatShortDate(sig.signedAt)})
|
|
314
|
+
</span>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{idx < group.signatures.length - 1 && (
|
|
318
|
+
<ArrowRight className="h-3 w-3 text-muted-foreground/50 shrink-0" />
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
)
|
|
322
|
+
})}
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
{/* ── 3. Expanded Details Section ────────────────────────────────────── */}
|
|
328
|
+
{expanded && (
|
|
329
|
+
<div className="border-t bg-muted/20 px-4 py-4 space-y-4 animate-in fade-in-50 duration-150">
|
|
330
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground pb-1">
|
|
331
|
+
<span className="font-semibold uppercase tracking-wider text-[11px]">
|
|
332
|
+
Signatures Breakdown
|
|
333
|
+
</span>
|
|
334
|
+
<span>
|
|
335
|
+
{group.signatures.length} cryptographically verified {group.signatures.length === 1 ? "record" : "records"}
|
|
336
|
+
</span>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<div className="space-y-3">
|
|
340
|
+
{group.signatures.map((sig, idx) => {
|
|
341
|
+
const purposeKey = sig.signaturePurpose?.toLowerCase() || ""
|
|
342
|
+
const purposeText = PURPOSE_LABELS[purposeKey] || sig.signaturePurpose
|
|
343
|
+
const signer = sig.signerNameSnapshot || sig.signerUser?.fullName || "Authorized Signer"
|
|
344
|
+
const role = sig.signerRoleSnapshot || "Biomedical Specialist"
|
|
345
|
+
const isCopied = copiedHash === sig.signedContentHashSha256
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<div
|
|
349
|
+
key={sig.id || idx}
|
|
350
|
+
className="rounded-lg border bg-card p-3.5 space-y-2.5 transition-colors"
|
|
351
|
+
>
|
|
352
|
+
{/* Signer & Stage Info */}
|
|
353
|
+
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
354
|
+
<div className="flex items-start gap-2.5 min-w-0">
|
|
355
|
+
<div className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
|
|
356
|
+
<User className="h-3.5 w-3.5" />
|
|
357
|
+
</div>
|
|
358
|
+
<div className="min-w-0">
|
|
359
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
360
|
+
<span className="font-semibold text-xs text-foreground">
|
|
361
|
+
{signer}
|
|
362
|
+
</span>
|
|
363
|
+
<span className="text-[11px] text-muted-foreground">
|
|
364
|
+
• {role}
|
|
365
|
+
</span>
|
|
366
|
+
</div>
|
|
367
|
+
<div className="flex items-center gap-1 text-[11px] text-muted-foreground mt-0.5">
|
|
368
|
+
<Clock className="h-3 w-3 shrink-0" />
|
|
369
|
+
<span>{formatFullTimestamp(sig.signedAt, sig.displayTimezone)}</span>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
375
|
+
<Badge
|
|
376
|
+
variant="outline"
|
|
377
|
+
className="text-xs font-medium bg-primary/5 text-primary border-primary/20"
|
|
378
|
+
>
|
|
379
|
+
{purposeText}
|
|
380
|
+
</Badge>
|
|
381
|
+
<Badge
|
|
382
|
+
variant="outline"
|
|
383
|
+
className="text-[10px] bg-emerald-500/10 text-emerald-500 border-emerald-500/20 capitalize font-medium"
|
|
384
|
+
>
|
|
385
|
+
{sig.signatureStatus || "active"}
|
|
386
|
+
</Badge>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
{/* Attestation Statement */}
|
|
391
|
+
{sig.attestationTextVersion && (
|
|
392
|
+
<blockquote className="rounded-md border-l-2 border-primary/40 bg-muted/40 px-3 py-1.5 text-xs italic text-foreground/90 leading-relaxed">
|
|
393
|
+
"{sig.attestationTextVersion}"
|
|
394
|
+
</blockquote>
|
|
395
|
+
)}
|
|
396
|
+
|
|
397
|
+
{/* Signer Comments */}
|
|
398
|
+
{sig.comments && (
|
|
399
|
+
<div className="flex items-start gap-2 rounded-md bg-muted/30 p-2 text-xs text-foreground/90 border border-border/50">
|
|
400
|
+
<MessageSquare className="h-3.5 w-3.5 mt-0.5 shrink-0 text-muted-foreground" />
|
|
401
|
+
<div className="min-w-0 space-y-0.5">
|
|
402
|
+
<span className="font-semibold text-[10px] uppercase tracking-wider text-muted-foreground block">
|
|
403
|
+
Signer Note:
|
|
404
|
+
</span>
|
|
405
|
+
<p className="whitespace-pre-wrap">{sig.comments}</p>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
|
|
410
|
+
{/* SHA-256 Hash */}
|
|
411
|
+
{sig.signedContentHashSha256 && (
|
|
412
|
+
<div className="flex items-center justify-between gap-2 rounded-md border bg-muted/40 px-2.5 py-1.5">
|
|
413
|
+
<div className="flex items-center gap-1.5 min-w-0 font-mono text-[11px] text-muted-foreground">
|
|
414
|
+
<Hash className="h-3.5 w-3.5 shrink-0 text-primary/70" />
|
|
415
|
+
<span className="truncate selection:bg-primary/20" title={sig.signedContentHashSha256}>
|
|
416
|
+
SHA-256: {sig.signedContentHashSha256}
|
|
417
|
+
</span>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<Button
|
|
421
|
+
type="button"
|
|
422
|
+
variant="ghost"
|
|
423
|
+
size="sm"
|
|
424
|
+
onClick={(e) => handleCopy(sig.signedContentHashSha256, e)}
|
|
425
|
+
className="h-5 gap-1 px-1.5 text-[10px] text-muted-foreground hover:text-foreground shrink-0"
|
|
426
|
+
title="Copy SHA-256 hash"
|
|
427
|
+
>
|
|
428
|
+
{isCopied ? (
|
|
429
|
+
<>
|
|
430
|
+
<Check className="h-3 w-3 text-emerald-500" />
|
|
431
|
+
<span className="text-emerald-500 font-medium">Copied</span>
|
|
432
|
+
</>
|
|
433
|
+
) : (
|
|
434
|
+
<>
|
|
435
|
+
<Copy className="h-3 w-3" />
|
|
436
|
+
<span>Copy</span>
|
|
437
|
+
</>
|
|
438
|
+
)}
|
|
439
|
+
</Button>
|
|
440
|
+
</div>
|
|
441
|
+
)}
|
|
442
|
+
</div>
|
|
443
|
+
)
|
|
444
|
+
})}
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
448
|
+
</Card>
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ── Main DeviceSignaturesTab ────────────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
interface DeviceSignaturesTabProps {
|
|
455
|
+
signatures: any[]
|
|
456
|
+
signatureEvents: any[]
|
|
457
|
+
deviceId?: string
|
|
458
|
+
deviceName?: string
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function DeviceSignaturesTab({
|
|
462
|
+
signatures = [],
|
|
463
|
+
signatureEvents = [],
|
|
464
|
+
deviceId,
|
|
465
|
+
deviceName,
|
|
466
|
+
}: DeviceSignaturesTabProps) {
|
|
467
|
+
const [showHashChain, setShowHashChain] = useState(false)
|
|
468
|
+
const hasSignatures = signatures && signatures.length > 0
|
|
469
|
+
|
|
470
|
+
// Group signatures by source (tickets, maintenance, pm, rounds, status changes)
|
|
471
|
+
const sourceGroups = hasSignatures
|
|
472
|
+
? groupSignaturesBySource(signatures as ElectronicSignatureItem[])
|
|
473
|
+
: []
|
|
474
|
+
|
|
475
|
+
return (
|
|
476
|
+
<div className="space-y-4">
|
|
477
|
+
{/* Sleek Top Banner */}
|
|
478
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 p-3.5 bg-muted/20 border rounded-lg">
|
|
479
|
+
<div className="flex items-center gap-2.5">
|
|
480
|
+
<div className="h-8 w-8 rounded-full bg-primary/10 text-primary flex items-center justify-center shrink-0">
|
|
481
|
+
<ShieldCheck className="w-4 h-4" />
|
|
482
|
+
</div>
|
|
483
|
+
<div>
|
|
484
|
+
<h3 className="text-sm font-semibold text-foreground">
|
|
485
|
+
Electronic Signatures & Sign-Offs
|
|
486
|
+
</h3>
|
|
487
|
+
<p className="text-xs text-muted-foreground">
|
|
488
|
+
Cryptographically verified sign-offs for tickets, maintenance, and device operations.
|
|
489
|
+
</p>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
|
|
493
|
+
{hasSignatures && (
|
|
494
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground self-end sm:self-center">
|
|
495
|
+
<span className="font-semibold text-foreground">
|
|
496
|
+
{sourceGroups.length} {sourceGroups.length === 1 ? "Signed Record" : "Signed Records"}
|
|
497
|
+
</span>
|
|
498
|
+
<span>•</span>
|
|
499
|
+
<span>{signatures.length} {signatures.length === 1 ? "signature" : "signatures"}</span>
|
|
500
|
+
</div>
|
|
501
|
+
)}
|
|
502
|
+
</div>
|
|
503
|
+
|
|
504
|
+
{/* Main Signatures Content */}
|
|
505
|
+
{hasSignatures ? (
|
|
506
|
+
<div className="space-y-3">
|
|
507
|
+
{/* Each Activity gets exactly ONE clean, collapsible card */}
|
|
508
|
+
{sourceGroups.map((group) => (
|
|
509
|
+
<ActivitySignatureCard key={group.key} group={group} />
|
|
510
|
+
))}
|
|
511
|
+
|
|
512
|
+
{/* Cryptographic Hash Chain Audit Trail (Collapsible at bottom) */}
|
|
513
|
+
{signatureEvents && signatureEvents.length > 0 && (
|
|
514
|
+
<div className="pt-2">
|
|
515
|
+
<Card className="border-dashed bg-card/40">
|
|
516
|
+
<div
|
|
517
|
+
onClick={() => setShowHashChain(!showHashChain)}
|
|
518
|
+
className="p-3 cursor-pointer select-none flex items-center justify-between text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
519
|
+
>
|
|
520
|
+
<div className="flex items-center gap-2">
|
|
521
|
+
<Lock className="w-3.5 h-3.5 text-primary" />
|
|
522
|
+
<span className="font-medium">
|
|
523
|
+
Cryptographic Audit Trail & Hash Chain ({signatureEvents.length} Events)
|
|
524
|
+
</span>
|
|
525
|
+
</div>
|
|
526
|
+
<Button variant="ghost" size="sm" className="h-6 px-2 text-xs gap-1">
|
|
527
|
+
<span>{showHashChain ? "Hide Chain" : "Inspect Chain"}</span>
|
|
528
|
+
{showHashChain ? (
|
|
529
|
+
<ChevronUp className="h-3.5 w-3.5" />
|
|
530
|
+
) : (
|
|
531
|
+
<ChevronDown className="h-3.5 w-3.5" />
|
|
532
|
+
)}
|
|
533
|
+
</Button>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
{showHashChain && (
|
|
537
|
+
<div className="px-3 pb-3 border-t pt-3">
|
|
538
|
+
<SignatureHistory events={signatureEvents} />
|
|
539
|
+
</div>
|
|
540
|
+
)}
|
|
541
|
+
</Card>
|
|
542
|
+
</div>
|
|
543
|
+
)}
|
|
544
|
+
</div>
|
|
545
|
+
) : (
|
|
546
|
+
/* Explanatory Empty State when 0 signatures exist */
|
|
547
|
+
<Card className="border-dashed">
|
|
548
|
+
<CardContent className="p-6 sm:p-8 space-y-6">
|
|
549
|
+
<div className="text-center max-w-lg mx-auto space-y-2">
|
|
550
|
+
<div className="w-12 h-12 rounded-full bg-primary/10 text-primary flex items-center justify-center mx-auto">
|
|
551
|
+
<ShieldCheck className="w-6 h-6" />
|
|
552
|
+
</div>
|
|
553
|
+
<h4 className="text-base font-semibold">No Electronic Signatures Recorded Yet</h4>
|
|
554
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
555
|
+
Signatures are automatically captured with cryptographic verification when biomedical engineers and managers complete formal work orders on this equipment.
|
|
556
|
+
</p>
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
{/* How Signatures Are Created */}
|
|
560
|
+
<div className="grid sm:grid-cols-3 gap-4 pt-2">
|
|
561
|
+
<div className="p-3.5 rounded-lg border bg-card space-y-1.5 text-left">
|
|
562
|
+
<div className="flex items-center gap-2 text-xs font-semibold text-foreground">
|
|
563
|
+
<span className="w-5 h-5 rounded-full bg-amber-500/10 text-amber-500 flex items-center justify-center text-[10px] font-bold">1</span>
|
|
564
|
+
<span>Work Order Sign-Off</span>
|
|
565
|
+
</div>
|
|
566
|
+
<p className="text-[11px] text-muted-foreground leading-normal">
|
|
567
|
+
Engineer executes repair or calibration checklist and signs with credentials to certify work performed.
|
|
568
|
+
</p>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<div className="p-3.5 rounded-lg border bg-card space-y-1.5 text-left">
|
|
572
|
+
<div className="flex items-center gap-2 text-xs font-semibold text-foreground">
|
|
573
|
+
<span className="w-5 h-5 rounded-full bg-blue-500/10 text-blue-500 flex items-center justify-center text-[10px] font-bold">2</span>
|
|
574
|
+
<span>Supervisor Review</span>
|
|
575
|
+
</div>
|
|
576
|
+
<p className="text-[11px] text-muted-foreground leading-normal">
|
|
577
|
+
Biomedical manager or peer reviews electrical safety measurements and co-signs the task.
|
|
578
|
+
</p>
|
|
579
|
+
</div>
|
|
580
|
+
|
|
581
|
+
<div className="p-3.5 rounded-lg border bg-card space-y-1.5 text-left">
|
|
582
|
+
<div className="flex items-center gap-2 text-xs font-semibold text-foreground">
|
|
583
|
+
<span className="w-5 h-5 rounded-full bg-emerald-500/10 text-emerald-500 flex items-center justify-center text-[10px] font-bold">3</span>
|
|
584
|
+
<span>Clinical Release</span>
|
|
585
|
+
</div>
|
|
586
|
+
<p className="text-[11px] text-muted-foreground leading-normal">
|
|
587
|
+
Final release certificate signed to officially declare the device safe for patient use and return to the ICU.
|
|
588
|
+
</p>
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
{/* Action to Work Orders */}
|
|
593
|
+
<div className="flex justify-center pt-2">
|
|
594
|
+
<Button asChild variant="outline" className="gap-2">
|
|
595
|
+
<Link href="/maintenance/tasks">
|
|
596
|
+
<Wrench className="w-4 h-4 text-primary" />
|
|
597
|
+
<span>Go to Work Orders</span>
|
|
598
|
+
<ArrowRight className="w-3.5 h-3.5 text-muted-foreground" />
|
|
599
|
+
</Link>
|
|
600
|
+
</Button>
|
|
601
|
+
</div>
|
|
602
|
+
</CardContent>
|
|
603
|
+
</Card>
|
|
604
|
+
)}
|
|
605
|
+
</div>
|
|
606
|
+
)
|
|
607
|
+
}
|