@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,413 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { useState } from "react"
|
|
5
|
+
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
|
6
|
+
import { Badge } from "@/components/ui/badge"
|
|
7
|
+
import { Button } from "@/components/ui/button"
|
|
8
|
+
import { cn } from "@/lib/utils"
|
|
9
|
+
import {
|
|
10
|
+
ShieldCheck,
|
|
11
|
+
User,
|
|
12
|
+
Clock,
|
|
13
|
+
Hash,
|
|
14
|
+
Copy,
|
|
15
|
+
Check,
|
|
16
|
+
MessageSquare,
|
|
17
|
+
Sparkles,
|
|
18
|
+
Eye,
|
|
19
|
+
Wrench,
|
|
20
|
+
AlertCircle,
|
|
21
|
+
XCircle,
|
|
22
|
+
RotateCcw,
|
|
23
|
+
CheckCircle2,
|
|
24
|
+
FileCheck,
|
|
25
|
+
} from "lucide-react"
|
|
26
|
+
|
|
27
|
+
// ── Types & Constants ────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export interface ElectronicSignatureItem {
|
|
30
|
+
id: string
|
|
31
|
+
organizationId?: string
|
|
32
|
+
recordVersionId?: string
|
|
33
|
+
entityType?: string
|
|
34
|
+
entityId?: string
|
|
35
|
+
signaturePurpose:
|
|
36
|
+
| "perform"
|
|
37
|
+
| "review"
|
|
38
|
+
| "release"
|
|
39
|
+
| "approve"
|
|
40
|
+
| "reject"
|
|
41
|
+
| "amend"
|
|
42
|
+
| "void"
|
|
43
|
+
| "accept"
|
|
44
|
+
| "resolve"
|
|
45
|
+
| "close"
|
|
46
|
+
| string
|
|
47
|
+
signerUserId?: string
|
|
48
|
+
signerNameSnapshot: string
|
|
49
|
+
signerRoleSnapshot: string
|
|
50
|
+
signerScopeSnapshot?: string | null
|
|
51
|
+
attestationTextVersion: string
|
|
52
|
+
authMethod?: string
|
|
53
|
+
signedAt: Date | string
|
|
54
|
+
displayTimezone?: string | null
|
|
55
|
+
signedContentHashSha256: string
|
|
56
|
+
signatureStatus: "active" | "superseded" | "rejected" | "voided" | string
|
|
57
|
+
comments?: string | null
|
|
58
|
+
signerUser?: {
|
|
59
|
+
id?: string
|
|
60
|
+
fullName?: string
|
|
61
|
+
email?: string
|
|
62
|
+
} | null
|
|
63
|
+
// Source context enrichment (from getDeviceSignatures)
|
|
64
|
+
sourceType?: string
|
|
65
|
+
sourceLabel?: string
|
|
66
|
+
sourceNumber?: string
|
|
67
|
+
sourceHref?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const PURPOSE_CONFIG: Record<
|
|
71
|
+
string,
|
|
72
|
+
{ label: string; badgeClass: string; icon: React.ComponentType<{ className?: string }> }
|
|
73
|
+
> = {
|
|
74
|
+
perform: {
|
|
75
|
+
label: "Performer / Work Executed",
|
|
76
|
+
badgeClass:
|
|
77
|
+
"bg-blue-100 text-blue-800 border-blue-300 dark:bg-blue-950 dark:text-blue-300 dark:border-blue-800",
|
|
78
|
+
icon: Wrench,
|
|
79
|
+
},
|
|
80
|
+
review: {
|
|
81
|
+
label: "Technical Reviewer",
|
|
82
|
+
badgeClass:
|
|
83
|
+
"bg-purple-100 text-purple-800 border-purple-300 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-800",
|
|
84
|
+
icon: Eye,
|
|
85
|
+
},
|
|
86
|
+
release: {
|
|
87
|
+
label: "Release Authority",
|
|
88
|
+
badgeClass:
|
|
89
|
+
"bg-emerald-100 text-emerald-800 border-emerald-300 dark:bg-emerald-950 dark:text-emerald-300 dark:border-emerald-800",
|
|
90
|
+
icon: Sparkles,
|
|
91
|
+
},
|
|
92
|
+
approve: {
|
|
93
|
+
label: "Approval",
|
|
94
|
+
badgeClass:
|
|
95
|
+
"bg-teal-100 text-teal-800 border-teal-300 dark:bg-teal-950 dark:text-teal-300 dark:border-teal-800",
|
|
96
|
+
icon: CheckCircle2,
|
|
97
|
+
},
|
|
98
|
+
reject: {
|
|
99
|
+
label: "Rejection",
|
|
100
|
+
badgeClass:
|
|
101
|
+
"bg-rose-100 text-rose-800 border-rose-300 dark:bg-rose-950 dark:text-rose-300 dark:border-rose-800",
|
|
102
|
+
icon: XCircle,
|
|
103
|
+
},
|
|
104
|
+
amend: {
|
|
105
|
+
label: "Record Amendment",
|
|
106
|
+
badgeClass:
|
|
107
|
+
"bg-orange-100 text-orange-800 border-orange-300 dark:bg-orange-950 dark:text-orange-300 dark:border-orange-800",
|
|
108
|
+
icon: RotateCcw,
|
|
109
|
+
},
|
|
110
|
+
void: {
|
|
111
|
+
label: "Voided",
|
|
112
|
+
badgeClass:
|
|
113
|
+
"bg-rose-100 text-rose-800 border-rose-300 dark:bg-rose-950 dark:text-rose-300 dark:border-rose-800",
|
|
114
|
+
icon: AlertCircle,
|
|
115
|
+
},
|
|
116
|
+
accept: {
|
|
117
|
+
label: "Engineer Acceptance / Work Started",
|
|
118
|
+
badgeClass:
|
|
119
|
+
"bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950 dark:text-indigo-300 dark:border-indigo-800",
|
|
120
|
+
icon: ShieldCheck,
|
|
121
|
+
},
|
|
122
|
+
resolve: {
|
|
123
|
+
label: "Resolution Sign-Off",
|
|
124
|
+
badgeClass:
|
|
125
|
+
"bg-emerald-100 text-emerald-800 border-emerald-300 dark:bg-emerald-950 dark:text-emerald-300 dark:border-emerald-800",
|
|
126
|
+
icon: CheckCircle2,
|
|
127
|
+
},
|
|
128
|
+
close: {
|
|
129
|
+
label: "Ticket Closure Sign-Off",
|
|
130
|
+
badgeClass:
|
|
131
|
+
"bg-slate-100 text-slate-800 border-slate-300 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-800",
|
|
132
|
+
icon: FileCheck,
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const STATUS_CONFIG: Record<
|
|
137
|
+
string,
|
|
138
|
+
{
|
|
139
|
+
label: string
|
|
140
|
+
borderClass: string
|
|
141
|
+
badgeClass: string
|
|
142
|
+
dotClass: string
|
|
143
|
+
}
|
|
144
|
+
> = {
|
|
145
|
+
active: {
|
|
146
|
+
label: "Active",
|
|
147
|
+
borderClass: "border-l-4 border-l-emerald-500 bg-card",
|
|
148
|
+
badgeClass:
|
|
149
|
+
"bg-emerald-100 text-emerald-800 border-emerald-300 dark:bg-emerald-950 dark:text-emerald-300 dark:border-emerald-800",
|
|
150
|
+
dotClass: "bg-emerald-500 text-white ring-emerald-200 dark:ring-emerald-900",
|
|
151
|
+
},
|
|
152
|
+
superseded: {
|
|
153
|
+
label: "Superseded",
|
|
154
|
+
borderClass:
|
|
155
|
+
"border-l-4 border-l-slate-400 bg-slate-50/50 dark:bg-slate-900/30 opacity-90",
|
|
156
|
+
badgeClass:
|
|
157
|
+
"bg-slate-100 text-slate-700 border-slate-300 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-700",
|
|
158
|
+
dotClass: "bg-slate-400 text-white ring-slate-200 dark:ring-slate-800",
|
|
159
|
+
},
|
|
160
|
+
voided: {
|
|
161
|
+
label: "Voided",
|
|
162
|
+
borderClass:
|
|
163
|
+
"border-l-4 border-l-rose-500 bg-rose-50/30 dark:bg-rose-950/20",
|
|
164
|
+
badgeClass:
|
|
165
|
+
"bg-rose-100 text-rose-800 border-rose-300 dark:bg-rose-950 dark:text-rose-300 dark:border-rose-800",
|
|
166
|
+
dotClass: "bg-rose-500 text-white ring-rose-200 dark:ring-rose-900",
|
|
167
|
+
},
|
|
168
|
+
rejected: {
|
|
169
|
+
label: "Rejected",
|
|
170
|
+
borderClass:
|
|
171
|
+
"border-l-4 border-l-rose-500 bg-rose-50/30 dark:bg-rose-950/20",
|
|
172
|
+
badgeClass:
|
|
173
|
+
"bg-rose-100 text-rose-800 border-rose-300 dark:bg-rose-950 dark:text-rose-300 dark:border-rose-800",
|
|
174
|
+
dotClass: "bg-rose-500 text-white ring-rose-200 dark:ring-rose-900",
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function formatSignatureTimestamp(
|
|
179
|
+
dateValue: Date | string,
|
|
180
|
+
timezone?: string | null
|
|
181
|
+
): string {
|
|
182
|
+
try {
|
|
183
|
+
const d = new Date(dateValue)
|
|
184
|
+
const formatted = d.toLocaleString("en-US", {
|
|
185
|
+
year: "numeric",
|
|
186
|
+
month: "short",
|
|
187
|
+
day: "2-digit",
|
|
188
|
+
hour: "2-digit",
|
|
189
|
+
minute: "2-digit",
|
|
190
|
+
second: "2-digit",
|
|
191
|
+
hour12: true,
|
|
192
|
+
timeZoneName: "short",
|
|
193
|
+
})
|
|
194
|
+
return timezone && timezone !== "UTC" ? `${formatted} (${timezone})` : formatted
|
|
195
|
+
} catch {
|
|
196
|
+
return String(dateValue)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── 1. SignatureCard Component ────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
export interface SignatureCardProps {
|
|
203
|
+
signature: ElectronicSignatureItem | any
|
|
204
|
+
className?: string
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function SignatureCard({ signature, className }: SignatureCardProps) {
|
|
208
|
+
const [copied, setCopied] = useState(false)
|
|
209
|
+
|
|
210
|
+
const purpose = signature?.signaturePurpose?.toLowerCase() || "perform"
|
|
211
|
+
const purposeMeta = PURPOSE_CONFIG[purpose] || {
|
|
212
|
+
label: purpose.replace(/_/g, " ").replace(/\b\w/g, (c: string) => c.toUpperCase()),
|
|
213
|
+
badgeClass: "bg-muted text-muted-foreground border-border",
|
|
214
|
+
icon: ShieldCheck,
|
|
215
|
+
}
|
|
216
|
+
const PurposeIcon = purposeMeta.icon
|
|
217
|
+
|
|
218
|
+
const status = signature?.signatureStatus?.toLowerCase() || "active"
|
|
219
|
+
const statusMeta = STATUS_CONFIG[status] || STATUS_CONFIG.active
|
|
220
|
+
|
|
221
|
+
const signerName =
|
|
222
|
+
signature?.signerNameSnapshot ||
|
|
223
|
+
signature?.signerUser?.fullName ||
|
|
224
|
+
"Authorized Signer"
|
|
225
|
+
const signerRole = signature?.signerRoleSnapshot || "Biomedical Specialist"
|
|
226
|
+
|
|
227
|
+
const handleCopyHash = async () => {
|
|
228
|
+
if (!signature?.signedContentHashSha256) return
|
|
229
|
+
try {
|
|
230
|
+
await navigator.clipboard.writeText(signature.signedContentHashSha256)
|
|
231
|
+
setCopied(true)
|
|
232
|
+
setTimeout(() => setCopied(false), 2000)
|
|
233
|
+
} catch {
|
|
234
|
+
// Fallback
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<Card
|
|
240
|
+
className={cn(
|
|
241
|
+
"overflow-hidden transition-all shadow-sm border",
|
|
242
|
+
statusMeta.borderClass,
|
|
243
|
+
className
|
|
244
|
+
)}
|
|
245
|
+
>
|
|
246
|
+
<CardHeader className="p-4 pb-3">
|
|
247
|
+
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
248
|
+
{/* Signer Info */}
|
|
249
|
+
<div className="flex items-start gap-2.5 min-w-0">
|
|
250
|
+
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
|
|
251
|
+
<User className="h-4 w-4" />
|
|
252
|
+
</div>
|
|
253
|
+
<div className="min-w-0">
|
|
254
|
+
<div className="flex items-center gap-2">
|
|
255
|
+
<span className="font-semibold text-sm text-foreground truncate">
|
|
256
|
+
{signerName}
|
|
257
|
+
</span>
|
|
258
|
+
<span className="text-xs text-muted-foreground truncate">
|
|
259
|
+
• {signerRole}
|
|
260
|
+
</span>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-0.5">
|
|
263
|
+
<Clock className="h-3 w-3 shrink-0" />
|
|
264
|
+
<span>{formatSignatureTimestamp(signature.signedAt, signature.displayTimezone)}</span>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Badges: Purpose & Status */}
|
|
270
|
+
<div className="flex flex-wrap items-center gap-1.5 ml-auto">
|
|
271
|
+
<Badge
|
|
272
|
+
variant="outline"
|
|
273
|
+
className={cn("inline-flex items-center gap-1 text-xs font-medium", purposeMeta.badgeClass)}
|
|
274
|
+
>
|
|
275
|
+
<PurposeIcon className="h-3 w-3" />
|
|
276
|
+
<span>{purposeMeta.label}</span>
|
|
277
|
+
</Badge>
|
|
278
|
+
|
|
279
|
+
<Badge
|
|
280
|
+
variant="outline"
|
|
281
|
+
className={cn("capitalize text-xs font-semibold", statusMeta.badgeClass)}
|
|
282
|
+
>
|
|
283
|
+
{statusMeta.label}
|
|
284
|
+
</Badge>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</CardHeader>
|
|
288
|
+
|
|
289
|
+
<CardContent className="p-4 pt-0 space-y-3">
|
|
290
|
+
{/* Attestation Text in Quote Block */}
|
|
291
|
+
{signature?.attestationTextVersion && (
|
|
292
|
+
<blockquote className="relative rounded-r-md border-l-2 border-primary/50 bg-muted/40 px-3 py-2 text-xs italic text-foreground/90 leading-relaxed">
|
|
293
|
+
"{signature.attestationTextVersion}"
|
|
294
|
+
</blockquote>
|
|
295
|
+
)}
|
|
296
|
+
|
|
297
|
+
{/* Comments if any */}
|
|
298
|
+
{signature?.comments && (
|
|
299
|
+
<div className="flex items-start gap-2 rounded-md bg-muted/30 p-2.5 text-xs text-foreground/90 border border-border/60">
|
|
300
|
+
<MessageSquare className="h-3.5 w-3.5 mt-0.5 shrink-0 text-muted-foreground" />
|
|
301
|
+
<div className="min-w-0 space-y-0.5">
|
|
302
|
+
<span className="font-semibold text-[11px] uppercase tracking-wider text-muted-foreground block">
|
|
303
|
+
Signer Comments:
|
|
304
|
+
</span>
|
|
305
|
+
<p className="whitespace-pre-wrap leading-normal">{signature.comments}</p>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
|
|
310
|
+
{/* SHA-256 Hash Display with Copy Button */}
|
|
311
|
+
{signature?.signedContentHashSha256 && (
|
|
312
|
+
<div className="flex items-center justify-between gap-2 rounded-md border bg-muted/50 px-2.5 py-1.5">
|
|
313
|
+
<div className="flex items-center gap-1.5 min-w-0 font-mono text-[11px] text-muted-foreground">
|
|
314
|
+
<Hash className="h-3.5 w-3.5 shrink-0 text-primary/70" />
|
|
315
|
+
<span className="truncate selection:bg-primary/20" title={signature.signedContentHashSha256}>
|
|
316
|
+
SHA-256: {signature.signedContentHashSha256}
|
|
317
|
+
</span>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<Button
|
|
321
|
+
type="button"
|
|
322
|
+
variant="ghost"
|
|
323
|
+
size="sm"
|
|
324
|
+
onClick={handleCopyHash}
|
|
325
|
+
className="h-6 gap-1 px-2 text-[11px] text-muted-foreground hover:text-foreground hover:bg-background shrink-0"
|
|
326
|
+
title="Copy cryptographic signature hash"
|
|
327
|
+
>
|
|
328
|
+
{copied ? (
|
|
329
|
+
<>
|
|
330
|
+
<Check className="h-3 w-3 text-emerald-600 dark:text-emerald-400" />
|
|
331
|
+
<span className="text-emerald-600 dark:text-emerald-400 font-medium">Copied</span>
|
|
332
|
+
</>
|
|
333
|
+
) : (
|
|
334
|
+
<>
|
|
335
|
+
<Copy className="h-3 w-3" />
|
|
336
|
+
<span>Copy</span>
|
|
337
|
+
</>
|
|
338
|
+
)}
|
|
339
|
+
</Button>
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
</CardContent>
|
|
343
|
+
</Card>
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── 2. SignatureTimeline Component ────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
export interface SignatureTimelineProps {
|
|
350
|
+
signatures?: ElectronicSignatureItem[] | any[]
|
|
351
|
+
className?: string
|
|
352
|
+
emptyMessage?: string
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function SignatureTimeline({
|
|
356
|
+
signatures = [],
|
|
357
|
+
className,
|
|
358
|
+
emptyMessage = "No electronic signatures recorded for this record yet.",
|
|
359
|
+
}: SignatureTimelineProps) {
|
|
360
|
+
// Sort chronologically (oldest first)
|
|
361
|
+
const sortedSignatures = [...signatures].sort((a, b) => {
|
|
362
|
+
const timeA = a.signedAt ? new Date(a.signedAt).getTime() : 0
|
|
363
|
+
const timeB = b.signedAt ? new Date(b.signedAt).getTime() : 0
|
|
364
|
+
return timeA - timeB
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
if (sortedSignatures.length === 0) {
|
|
368
|
+
return (
|
|
369
|
+
<div className={cn("rounded-lg border border-dashed p-8 text-center", className)}>
|
|
370
|
+
<FileCheck className="mx-auto h-8 w-8 text-muted-foreground/40 mb-2" />
|
|
371
|
+
<p className="text-sm font-medium text-muted-foreground">{emptyMessage}</p>
|
|
372
|
+
<p className="text-xs text-muted-foreground/70 mt-1">
|
|
373
|
+
Signatures will appear here after review and release execution.
|
|
374
|
+
</p>
|
|
375
|
+
</div>
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<div className={cn("relative space-y-6 pl-6 sm:pl-8", className)}>
|
|
381
|
+
{/* Vertical Connecting Dotted Line */}
|
|
382
|
+
<div
|
|
383
|
+
className="absolute left-2.5 sm:left-3.5 top-3 bottom-3 w-0.5 border-l-2 border-dotted border-slate-300 dark:border-slate-700"
|
|
384
|
+
aria-hidden="true"
|
|
385
|
+
/>
|
|
386
|
+
|
|
387
|
+
{sortedSignatures.map((signature, index) => {
|
|
388
|
+
const status = signature?.signatureStatus?.toLowerCase() || "active"
|
|
389
|
+
const statusMeta = STATUS_CONFIG[status] || STATUS_CONFIG.active
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<div key={signature.id || `sig-${index}`} className="relative">
|
|
393
|
+
{/* Timeline Node Bullet */}
|
|
394
|
+
<div
|
|
395
|
+
className={cn(
|
|
396
|
+
"absolute -left-6 sm:-left-8 top-4 flex h-5 w-5 sm:h-6 sm:w-6 items-center justify-center rounded-full ring-4 shadow-xs",
|
|
397
|
+
statusMeta.dotClass
|
|
398
|
+
)}
|
|
399
|
+
>
|
|
400
|
+
<ShieldCheck className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
{/* Signature Card */}
|
|
404
|
+
<SignatureCard signature={signature} />
|
|
405
|
+
</div>
|
|
406
|
+
)
|
|
407
|
+
})}
|
|
408
|
+
</div>
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export default SignatureTimeline
|
|
413
|
+
|