@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,55 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
import { Badge } from "@/components/ui/badge"
|
|
3
|
+
|
|
4
|
+
const statusConfig: Record<string, { label: string; variant: string; className: string }> = {
|
|
5
|
+
operational: { label: "Operational", variant: "default", className: "bg-green-100 text-green-800 border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-800" },
|
|
6
|
+
operational_with_limitations: { label: "Limited Use", variant: "default", className: "bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-800" },
|
|
7
|
+
under_maintenance: { label: "Under Maintenance", variant: "default", className: "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-950 dark:text-blue-300 dark:border-blue-800" },
|
|
8
|
+
under_repair: { label: "Under Repair", variant: "default", className: "bg-orange-100 text-orange-800 border-orange-200 dark:bg-orange-950 dark:text-orange-300 dark:border-orange-800" },
|
|
9
|
+
waiting_for_parts: { label: "Waiting for Parts", variant: "default", className: "bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-950 dark:text-yellow-300 dark:border-yellow-800" },
|
|
10
|
+
awaiting_release: { label: "Awaiting Release", variant: "default", className: "bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-800" },
|
|
11
|
+
out_of_service: { label: "Out of Service", variant: "default", className: "bg-red-100 text-red-800 border-red-200 dark:bg-red-950 dark:text-red-300 dark:border-red-800" },
|
|
12
|
+
standby: { label: "Standby", variant: "default", className: "bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700" },
|
|
13
|
+
decommissioned: { label: "Decommissioned", variant: "default", className: "bg-slate-200 text-slate-600 border-slate-300 line-through dark:bg-slate-800 dark:text-slate-400 dark:border-slate-700" },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const criticalityConfig: Record<string, { label: string; className: string }> = {
|
|
17
|
+
critical: { label: "Critical", className: "bg-red-100 text-red-800 border-red-200 dark:bg-red-950 dark:text-red-300 dark:border-red-800" },
|
|
18
|
+
high: { label: "High", className: "bg-orange-100 text-orange-800 border-orange-200 dark:bg-orange-950 dark:text-orange-300 dark:border-orange-800" },
|
|
19
|
+
medium: { label: "Medium", className: "bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-950 dark:text-yellow-300 dark:border-yellow-800" },
|
|
20
|
+
low: { label: "Low", className: "bg-green-100 text-green-800 border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-800" },
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const riskConfig: Record<string, { label: string; className: string }> = {
|
|
24
|
+
class_i: { label: "Class I", className: "bg-green-50 text-green-700 border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-800" },
|
|
25
|
+
class_iia: { label: "Class IIa", className: "bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-950 dark:text-yellow-300 dark:border-yellow-800" },
|
|
26
|
+
class_iib: { label: "Class IIb", className: "bg-orange-50 text-orange-700 border-orange-200 dark:bg-orange-950 dark:text-orange-300 dark:border-orange-800" },
|
|
27
|
+
class_iii: { label: "Class III", className: "bg-red-50 text-red-700 border-red-200 dark:bg-red-950 dark:text-red-300 dark:border-red-800" },
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function DeviceStatusBadge({ status }: { status?: string | null }) {
|
|
31
|
+
const config = statusConfig[status || ''] || { label: status || 'Unknown', variant: 'secondary', className: 'bg-muted text-muted-foreground' }
|
|
32
|
+
return (
|
|
33
|
+
<Badge variant="outline" className={cn("font-medium", config.className)}>
|
|
34
|
+
{config.label}
|
|
35
|
+
</Badge>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function CriticalityBadge({ level }: { level?: string | null }) {
|
|
40
|
+
const config = criticalityConfig[level || ''] || { label: level || 'N/A', className: 'bg-muted text-muted-foreground' }
|
|
41
|
+
return (
|
|
42
|
+
<Badge variant="outline" className={cn("font-medium text-xs", config.className)}>
|
|
43
|
+
{config.label}
|
|
44
|
+
</Badge>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function RiskClassBadge({ classification }: { classification?: string | null }) {
|
|
49
|
+
const config = riskConfig[classification || ''] || { label: classification || 'N/A', className: 'bg-muted text-muted-foreground' }
|
|
50
|
+
return (
|
|
51
|
+
<Badge variant="outline" className={cn("font-medium text-xs", config.className)}>
|
|
52
|
+
{config.label}
|
|
53
|
+
</Badge>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { FileText, Download } from "lucide-react"
|
|
4
|
+
import { Card } from "@/components/ui/card"
|
|
5
|
+
import { Badge } from "@/components/ui/badge"
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import { Button } from "@/components/ui/button"
|
|
8
|
+
|
|
9
|
+
interface DeviceDocumentsTabProps {
|
|
10
|
+
documents: any[];
|
|
11
|
+
deviceId: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const formatSize = (bytes: number) => {
|
|
15
|
+
if (bytes === 0) return '0 Bytes'
|
|
16
|
+
const k = 1024
|
|
17
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
|
18
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
19
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const categoryLabels: Record<string, string> = {
|
|
23
|
+
user_manual: "User Manual",
|
|
24
|
+
service_manual: "Service Manual",
|
|
25
|
+
certificate: "Certificate",
|
|
26
|
+
calibration_report: "Calibration Report",
|
|
27
|
+
photo: "Photo",
|
|
28
|
+
other: "Other"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function DeviceDocumentsTab({ documents, deviceId }: DeviceDocumentsTabProps) {
|
|
32
|
+
return (
|
|
33
|
+
<div className="space-y-4">
|
|
34
|
+
<div className="flex justify-between items-center">
|
|
35
|
+
<h3 className="text-lg font-semibold">Device Documents</h3>
|
|
36
|
+
<Button variant="outline" size="sm" disabled>
|
|
37
|
+
Upload Document
|
|
38
|
+
</Button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{(!documents || documents.length === 0) ? (
|
|
42
|
+
<div className="text-center py-12 border rounded-lg bg-muted/20">
|
|
43
|
+
<FileText className="w-12 h-12 text-muted-foreground mx-auto mb-4 opacity-50" />
|
|
44
|
+
<h4 className="text-lg font-medium text-foreground">No documents found</h4>
|
|
45
|
+
<p className="text-muted-foreground text-sm mt-1">Upload manuals, certificates, and other device documentation here.</p>
|
|
46
|
+
</div>
|
|
47
|
+
) : (
|
|
48
|
+
<div className="border rounded-md">
|
|
49
|
+
<div className="grid grid-cols-12 gap-4 p-4 border-b bg-muted/50 font-medium text-sm">
|
|
50
|
+
<div className="col-span-4">File Name</div>
|
|
51
|
+
<div className="col-span-2">Category</div>
|
|
52
|
+
<div className="col-span-2">Size</div>
|
|
53
|
+
<div className="col-span-2">Visibility</div>
|
|
54
|
+
<div className="col-span-2 text-right">Actions</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="divide-y">
|
|
57
|
+
{documents.map((doc) => (
|
|
58
|
+
<div key={doc.id} className="grid grid-cols-12 gap-4 p-4 items-center text-sm hover:bg-muted/20 transition-colors">
|
|
59
|
+
<div className="col-span-4 font-medium flex items-center gap-2 truncate">
|
|
60
|
+
<FileText className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
61
|
+
<span className="truncate">{doc.fileName}</span>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="col-span-2">
|
|
64
|
+
<Badge variant="secondary" className="font-normal">
|
|
65
|
+
{categoryLabels[doc.category] || doc.category}
|
|
66
|
+
</Badge>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="col-span-2 text-muted-foreground">
|
|
69
|
+
{formatSize(doc.fileSize)}
|
|
70
|
+
</div>
|
|
71
|
+
<div className="col-span-2">
|
|
72
|
+
<Badge variant="outline" className={cn(
|
|
73
|
+
"font-normal capitalize",
|
|
74
|
+
doc.visibility === 'biomedical_only' ? 'text-purple-700 bg-purple-50 border-purple-200' : 'text-green-700 bg-green-50 border-green-200'
|
|
75
|
+
)}>
|
|
76
|
+
{doc.visibility.replace('_', ' ')}
|
|
77
|
+
</Badge>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="col-span-2 text-right">
|
|
80
|
+
<Button variant="ghost" size="icon" title="Download">
|
|
81
|
+
<Download className="w-4 h-4" />
|
|
82
|
+
<span className="sr-only">Download</span>
|
|
83
|
+
</Button>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Link from "next/link"
|
|
4
|
+
import { Wrench } from "lucide-react"
|
|
5
|
+
import { Card } from "@/components/ui/card"
|
|
6
|
+
import { Badge } from "@/components/ui/badge"
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
interface DeviceMaintenanceTabProps {
|
|
10
|
+
maintenanceTasks: any[];
|
|
11
|
+
deviceId: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const statusColors: Record<string, string> = {
|
|
15
|
+
draft: "bg-gray-100 text-gray-800 border-gray-200",
|
|
16
|
+
scheduled: "bg-blue-100 text-blue-800 border-blue-200",
|
|
17
|
+
assigned: "bg-indigo-100 text-indigo-800 border-indigo-200",
|
|
18
|
+
in_progress: "bg-yellow-100 text-yellow-800 border-yellow-200",
|
|
19
|
+
work_complete: "bg-emerald-100 text-emerald-800 border-emerald-200",
|
|
20
|
+
awaiting_review: "bg-purple-100 text-purple-800 border-purple-200",
|
|
21
|
+
awaiting_release: "bg-orange-100 text-orange-800 border-orange-200",
|
|
22
|
+
returned_for_rework: "bg-rose-100 text-rose-800 border-rose-200",
|
|
23
|
+
closed: "bg-green-100 text-green-800 border-green-200",
|
|
24
|
+
cancelled: "bg-gray-100 text-gray-800 border-gray-200",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function DeviceMaintenanceTab({ maintenanceTasks = [], deviceId }: DeviceMaintenanceTabProps) {
|
|
28
|
+
const total = maintenanceTasks.length
|
|
29
|
+
const completed = maintenanceTasks.filter(t => (t.statusCode || t.status) === "closed").length
|
|
30
|
+
const pending = total - completed
|
|
31
|
+
const overdue = maintenanceTasks.filter(t => t.dueDate && new Date(t.dueDate) < new Date() && (t.statusCode || t.status) !== "closed").length
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="space-y-6">
|
|
35
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
36
|
+
<Card className="p-4 flex flex-col items-center justify-center text-center">
|
|
37
|
+
<span className="text-2xl font-bold">{total}</span>
|
|
38
|
+
<span className="text-sm text-muted-foreground">Total Tasks</span>
|
|
39
|
+
</Card>
|
|
40
|
+
<Card className="p-4 flex flex-col items-center justify-center text-center">
|
|
41
|
+
<span className="text-2xl font-bold text-emerald-600">{completed}</span>
|
|
42
|
+
<span className="text-sm text-muted-foreground">Completed</span>
|
|
43
|
+
</Card>
|
|
44
|
+
<Card className="p-4 flex flex-col items-center justify-center text-center">
|
|
45
|
+
<span className="text-2xl font-bold text-blue-600">{pending}</span>
|
|
46
|
+
<span className="text-sm text-muted-foreground">Pending</span>
|
|
47
|
+
</Card>
|
|
48
|
+
<Card className="p-4 flex flex-col items-center justify-center text-center">
|
|
49
|
+
<span className="text-2xl font-bold text-rose-600">{overdue}</span>
|
|
50
|
+
<span className="text-sm text-muted-foreground">Overdue</span>
|
|
51
|
+
</Card>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="space-y-4">
|
|
55
|
+
<h3 className="text-lg font-semibold">Maintenance History</h3>
|
|
56
|
+
|
|
57
|
+
{maintenanceTasks.length === 0 ? (
|
|
58
|
+
<div className="text-center py-12 border rounded-lg bg-muted/20">
|
|
59
|
+
<Wrench className="w-12 h-12 text-muted-foreground mx-auto mb-4 opacity-50" />
|
|
60
|
+
<h4 className="text-lg font-medium text-foreground">No maintenance records</h4>
|
|
61
|
+
<p className="text-muted-foreground text-sm mt-1">This device has no recorded maintenance history.</p>
|
|
62
|
+
</div>
|
|
63
|
+
) : (
|
|
64
|
+
<div className="grid gap-3">
|
|
65
|
+
{maintenanceTasks.map(task => {
|
|
66
|
+
const statusKey = task.statusCode || task.status || "draft"
|
|
67
|
+
const typeLabel = (task.maintenanceType || task.type || "corrective").replace(/_/g, " ")
|
|
68
|
+
const assigneeName = task.assignedEngineer?.fullName || task.assignedEngineerName || "Unassigned"
|
|
69
|
+
const completedDate = task.completedAt || task.completedDate
|
|
70
|
+
const startDate = task.startedAt || task.startDate
|
|
71
|
+
const dueDate = task.dueDate
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Link key={task.id} href={`/maintenance/tasks/${task.id}`}>
|
|
75
|
+
<Card className="p-4 hover:bg-muted/50 transition-colors flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
76
|
+
<div className="space-y-1">
|
|
77
|
+
<div className="flex items-center gap-2">
|
|
78
|
+
<span className="font-mono text-sm text-muted-foreground">#{task.taskNumber}</span>
|
|
79
|
+
<span className="font-medium">{task.title}</span>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="text-sm text-muted-foreground">
|
|
82
|
+
Assigned to: {assigneeName}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="flex flex-wrap sm:flex-nowrap items-center gap-2">
|
|
87
|
+
<Badge variant="outline" className="capitalize">{typeLabel}</Badge>
|
|
88
|
+
<Badge variant="outline" className={cn("capitalize", statusColors[statusKey] || statusColors.draft)}>
|
|
89
|
+
{statusKey.replace(/_/g, " ")}
|
|
90
|
+
</Badge>
|
|
91
|
+
<div className="text-xs text-muted-foreground sm:text-right sm:w-28">
|
|
92
|
+
{completedDate ? (
|
|
93
|
+
<span>Done: {new Date(completedDate).toLocaleDateString()}</span>
|
|
94
|
+
) : startDate ? (
|
|
95
|
+
<span>Started: {new Date(startDate).toLocaleDateString()}</span>
|
|
96
|
+
) : dueDate ? (
|
|
97
|
+
<span>Due: {new Date(dueDate).toLocaleDateString()}</span>
|
|
98
|
+
) : (
|
|
99
|
+
<span>—</span>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</Card>
|
|
104
|
+
</Link>
|
|
105
|
+
)
|
|
106
|
+
})}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { QRCodeCanvas } from "qrcode.react"
|
|
5
|
+
import { QrCode, ExternalLink, Copy, Check } from "lucide-react"
|
|
6
|
+
import Link from "next/link"
|
|
7
|
+
import { toast } from "sonner"
|
|
8
|
+
|
|
9
|
+
interface DeviceMiniQrProps {
|
|
10
|
+
deviceId: string
|
|
11
|
+
opaqueReference: string
|
|
12
|
+
assetNumber?: string
|
|
13
|
+
deviceName?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function DeviceMiniQr({
|
|
17
|
+
deviceId,
|
|
18
|
+
opaqueReference,
|
|
19
|
+
assetNumber,
|
|
20
|
+
deviceName,
|
|
21
|
+
}: DeviceMiniQrProps) {
|
|
22
|
+
const [origin, setOrigin] = React.useState("")
|
|
23
|
+
const [copied, setCopied] = React.useState(false)
|
|
24
|
+
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
if (typeof window !== "undefined") {
|
|
27
|
+
setOrigin(window.location.origin)
|
|
28
|
+
}
|
|
29
|
+
}, [])
|
|
30
|
+
|
|
31
|
+
const scanUrl = `${origin || ""}/scan/${opaqueReference}`
|
|
32
|
+
|
|
33
|
+
const handleCopy = async (e: React.MouseEvent) => {
|
|
34
|
+
e.preventDefault()
|
|
35
|
+
e.stopPropagation()
|
|
36
|
+
try {
|
|
37
|
+
await navigator.clipboard.writeText(scanUrl)
|
|
38
|
+
setCopied(true)
|
|
39
|
+
toast.success("Scan link copied to clipboard")
|
|
40
|
+
setTimeout(() => setCopied(false), 2000)
|
|
41
|
+
} catch {
|
|
42
|
+
toast.error("Failed to copy link")
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex items-center gap-3.5 p-3 rounded-lg border bg-muted/40 hover:bg-muted/60 transition-colors">
|
|
48
|
+
{/* Mini QR Canvas with clickable link to print page */}
|
|
49
|
+
<Link
|
|
50
|
+
href={`/devices/${deviceId}/qr`}
|
|
51
|
+
className="bg-white p-1.5 rounded-md border shadow-2xs shrink-0 hover:ring-2 hover:ring-primary/40 transition-all cursor-pointer group"
|
|
52
|
+
title="Click to view & print full label"
|
|
53
|
+
>
|
|
54
|
+
<QRCodeCanvas
|
|
55
|
+
value={scanUrl}
|
|
56
|
+
size={72}
|
|
57
|
+
level="M"
|
|
58
|
+
includeMargin={false}
|
|
59
|
+
/>
|
|
60
|
+
</Link>
|
|
61
|
+
|
|
62
|
+
{/* Info & quick actions */}
|
|
63
|
+
<div className="flex-1 min-w-0 space-y-1">
|
|
64
|
+
<div className="flex items-center justify-between gap-1">
|
|
65
|
+
<div className="flex items-center gap-1.5 text-xs font-semibold text-foreground">
|
|
66
|
+
<QrCode className="w-3.5 h-3.5 text-primary" />
|
|
67
|
+
<span>Device QR Label</span>
|
|
68
|
+
</div>
|
|
69
|
+
<span className="font-mono text-[11px] text-muted-foreground bg-background px-1.5 py-0.5 rounded border">
|
|
70
|
+
{opaqueReference}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<p className="text-[11px] text-muted-foreground leading-tight">
|
|
75
|
+
Scan to access quick helpdesk & status
|
|
76
|
+
</p>
|
|
77
|
+
|
|
78
|
+
<div className="pt-1 flex items-center gap-3 text-xs">
|
|
79
|
+
<Link
|
|
80
|
+
href={`/devices/${deviceId}/qr`}
|
|
81
|
+
className="text-primary hover:underline font-medium inline-flex items-center gap-1 text-[11px]"
|
|
82
|
+
>
|
|
83
|
+
<span>View & Print</span>
|
|
84
|
+
<ExternalLink className="w-3 h-3" />
|
|
85
|
+
</Link>
|
|
86
|
+
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
onClick={handleCopy}
|
|
90
|
+
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-[11px] transition-colors"
|
|
91
|
+
>
|
|
92
|
+
{copied ? (
|
|
93
|
+
<>
|
|
94
|
+
<Check className="w-3 h-3 text-emerald-600" />
|
|
95
|
+
<span className="text-emerald-600 font-medium">Copied</span>
|
|
96
|
+
</>
|
|
97
|
+
) : (
|
|
98
|
+
<>
|
|
99
|
+
<Copy className="w-3 h-3" />
|
|
100
|
+
<span>Copy Link</span>
|
|
101
|
+
</>
|
|
102
|
+
)}
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { QrCodeDisplay } from "./qr-code-display"
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import { Printer, RefreshCw, QrCode } from "lucide-react"
|
|
8
|
+
import { generateNewQrLabel, recordQrPrint } from "@/lib/actions/qr"
|
|
9
|
+
import { useRouter } from "next/navigation"
|
|
10
|
+
import { toast } from "sonner"
|
|
11
|
+
|
|
12
|
+
export interface DeviceQrCardProps {
|
|
13
|
+
device: {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
internalCode: string;
|
|
17
|
+
assetNumber: string;
|
|
18
|
+
hospital?: { name?: string } | null;
|
|
19
|
+
department?: { name?: string } | null;
|
|
20
|
+
};
|
|
21
|
+
qrLabel: {
|
|
22
|
+
id: string;
|
|
23
|
+
opaqueReference: string;
|
|
24
|
+
labelStatus: string;
|
|
25
|
+
printCount?: number | null;
|
|
26
|
+
} | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function DeviceQrCard({ device, qrLabel }: DeviceQrCardProps) {
|
|
30
|
+
const router = useRouter()
|
|
31
|
+
const [isGenerating, setIsGenerating] = React.useState(false)
|
|
32
|
+
|
|
33
|
+
// Construct absolute URL for the QR code target
|
|
34
|
+
const [origin, setOrigin] = React.useState("")
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
if (typeof window !== "undefined") {
|
|
37
|
+
setOrigin(window.location.origin)
|
|
38
|
+
}
|
|
39
|
+
}, [])
|
|
40
|
+
|
|
41
|
+
const scanTargetUrl = qrLabel
|
|
42
|
+
? `${origin || ""}/scan/${qrLabel.opaqueReference}`
|
|
43
|
+
: ""
|
|
44
|
+
|
|
45
|
+
const handlePrint = async () => {
|
|
46
|
+
if (qrLabel?.id) {
|
|
47
|
+
await recordQrPrint(qrLabel.id)
|
|
48
|
+
}
|
|
49
|
+
window.print()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handleRegenerate = async () => {
|
|
53
|
+
if (!confirm("Are you sure you want to regenerate this QR label? The previous QR code will be revoked.")) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setIsGenerating(true)
|
|
58
|
+
try {
|
|
59
|
+
const res = await generateNewQrLabel(device.id, "User requested label replacement")
|
|
60
|
+
if (res.success) {
|
|
61
|
+
toast.success("New QR label generated successfully")
|
|
62
|
+
router.refresh()
|
|
63
|
+
} else {
|
|
64
|
+
toast.error((res as any).error || "Failed to generate QR label")
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
toast.error("An unexpected error occurred")
|
|
68
|
+
} finally {
|
|
69
|
+
setIsGenerating(false)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="grid gap-6 md:grid-cols-2 print:grid-cols-1">
|
|
75
|
+
{/* 1. Main QR Code & Actions Card */}
|
|
76
|
+
<Card className="print:hidden">
|
|
77
|
+
<CardHeader>
|
|
78
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
79
|
+
<QrCode className="w-5 h-5 text-primary" />
|
|
80
|
+
Active QR Code
|
|
81
|
+
</CardTitle>
|
|
82
|
+
</CardHeader>
|
|
83
|
+
<CardContent className="flex flex-col items-center gap-5">
|
|
84
|
+
{qrLabel ? (
|
|
85
|
+
<>
|
|
86
|
+
<QrCodeDisplay
|
|
87
|
+
value={scanTargetUrl || qrLabel.opaqueReference}
|
|
88
|
+
size={220}
|
|
89
|
+
assetNumber={device.assetNumber}
|
|
90
|
+
deviceName={device.name}
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
<div className="flex flex-wrap gap-2 justify-center w-full pt-2 border-t">
|
|
94
|
+
<Button variant="default" size="sm" onClick={handlePrint} className="gap-1.5">
|
|
95
|
+
<Printer className="w-4 h-4" /> Print Label
|
|
96
|
+
</Button>
|
|
97
|
+
<Button
|
|
98
|
+
variant="outline"
|
|
99
|
+
size="sm"
|
|
100
|
+
onClick={handleRegenerate}
|
|
101
|
+
disabled={isGenerating}
|
|
102
|
+
className="gap-1.5"
|
|
103
|
+
>
|
|
104
|
+
<RefreshCw className={`w-4 h-4 ${isGenerating ? "animate-spin" : ""}`} />
|
|
105
|
+
Regenerate
|
|
106
|
+
</Button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div className="text-xs text-muted-foreground space-y-1 text-center">
|
|
110
|
+
<p>
|
|
111
|
+
Opaque Reference: <span className="font-mono font-medium text-foreground">{qrLabel.opaqueReference}</span>
|
|
112
|
+
</p>
|
|
113
|
+
<p>Status: <span className="capitalize font-medium text-emerald-600 dark:text-emerald-400">{qrLabel.labelStatus}</span></p>
|
|
114
|
+
<p>Print Count: {qrLabel.printCount || 0}</p>
|
|
115
|
+
</div>
|
|
116
|
+
</>
|
|
117
|
+
) : (
|
|
118
|
+
<div className="text-center py-8">
|
|
119
|
+
<p className="text-muted-foreground mb-4">No active QR label for this device.</p>
|
|
120
|
+
<Button onClick={handleRegenerate} disabled={isGenerating}>
|
|
121
|
+
<RefreshCw className="w-4 h-4 mr-2" /> Generate QR Label
|
|
122
|
+
</Button>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</CardContent>
|
|
126
|
+
</Card>
|
|
127
|
+
|
|
128
|
+
{/* 2. Physical Thermal Label Preview (50mm x 30mm format) */}
|
|
129
|
+
<Card>
|
|
130
|
+
<CardHeader className="print:hidden">
|
|
131
|
+
<CardTitle className="text-base">Thermal Label Preview (50 × 30mm)</CardTitle>
|
|
132
|
+
</CardHeader>
|
|
133
|
+
<CardContent className="flex flex-col items-center justify-center p-6">
|
|
134
|
+
<div
|
|
135
|
+
id="printable-qr-label"
|
|
136
|
+
className="w-[280px] border-2 border-slate-900 rounded-lg p-3.5 bg-white text-slate-900 shadow-md flex flex-col justify-between aspect-[5/3]"
|
|
137
|
+
>
|
|
138
|
+
<div className="border-b pb-1">
|
|
139
|
+
<p className="text-[10px] font-semibold uppercase tracking-wider text-slate-500 truncate">
|
|
140
|
+
{device.hospital?.name || "Biomedical Engineering"}
|
|
141
|
+
</p>
|
|
142
|
+
<p className="text-sm font-bold truncate leading-tight mt-0.5">{device.name}</p>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="flex items-center justify-between gap-2 my-1">
|
|
146
|
+
<div className="space-y-0.5">
|
|
147
|
+
<p className="text-[11px] font-mono font-bold tracking-tight">
|
|
148
|
+
{device.assetNumber}
|
|
149
|
+
</p>
|
|
150
|
+
<p className="text-[9px] text-slate-600 font-mono">
|
|
151
|
+
{device.internalCode}
|
|
152
|
+
</p>
|
|
153
|
+
<p className="text-[8px] text-slate-500 truncate max-w-[120px]">
|
|
154
|
+
{device.department?.name || "General"}
|
|
155
|
+
</p>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{qrLabel ? (
|
|
159
|
+
<div className="p-1 bg-white shrink-0">
|
|
160
|
+
<QrCodeDisplay
|
|
161
|
+
value={scanTargetUrl || qrLabel.opaqueReference}
|
|
162
|
+
size={64}
|
|
163
|
+
showDownloadOptions={false}
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
<div className="w-16 h-16 bg-slate-100 flex items-center justify-center rounded text-[10px] text-slate-400">
|
|
168
|
+
QR
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div className="border-t pt-1 flex items-center justify-between text-[8px] text-slate-500">
|
|
174
|
+
<span>Scan to view status or report</span>
|
|
175
|
+
<span className="font-mono">{qrLabel?.opaqueReference.slice(0, 8)}</span>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<p className="text-xs text-muted-foreground mt-4 text-center print:hidden">
|
|
180
|
+
Optimized for 50mm × 30mm thermal label printers (Brother, Zebra, Dymo).
|
|
181
|
+
</p>
|
|
182
|
+
</CardContent>
|
|
183
|
+
</Card>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|