@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,139 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Link from "next/link"
|
|
4
|
+
import { Ticket, Plus, AlertCircle, Clock, CheckCircle2, User, ChevronRight } from "lucide-react"
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import { TicketStatusBadge, TicketPriorityBadge } from "@/components/tickets/ticket-badges"
|
|
8
|
+
|
|
9
|
+
interface DeviceTicketsTabProps {
|
|
10
|
+
tickets: any[]
|
|
11
|
+
deviceId: string
|
|
12
|
+
deviceName: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function DeviceTicketsTab({ tickets = [], deviceId, deviceName }: DeviceTicketsTabProps) {
|
|
16
|
+
const total = tickets.length
|
|
17
|
+
const openCount = tickets.filter(t => !["resolved", "closed", "cancelled"].includes(t.statusCode)).length
|
|
18
|
+
const inProgressCount = tickets.filter(t => t.statusCode === "in_progress").length
|
|
19
|
+
const resolvedCount = tickets.filter(t => ["resolved", "closed"].includes(t.statusCode)).length
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="space-y-6">
|
|
23
|
+
{/* 1. Summary Metrics */}
|
|
24
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
25
|
+
<Card className="p-4 flex flex-col items-center justify-center text-center">
|
|
26
|
+
<span className="text-2xl font-bold">{total}</span>
|
|
27
|
+
<span className="text-xs text-muted-foreground mt-0.5">Total Tickets</span>
|
|
28
|
+
</Card>
|
|
29
|
+
<Card className="p-4 flex flex-col items-center justify-center text-center">
|
|
30
|
+
<span className="text-2xl font-bold text-amber-600">{openCount}</span>
|
|
31
|
+
<span className="text-xs text-muted-foreground mt-0.5">Open / Active</span>
|
|
32
|
+
</Card>
|
|
33
|
+
<Card className="p-4 flex flex-col items-center justify-center text-center">
|
|
34
|
+
<span className="text-2xl font-bold text-blue-600">{inProgressCount}</span>
|
|
35
|
+
<span className="text-xs text-muted-foreground mt-0.5">In Progress</span>
|
|
36
|
+
</Card>
|
|
37
|
+
<Card className="p-4 flex flex-col items-center justify-center text-center">
|
|
38
|
+
<span className="text-2xl font-bold text-emerald-600">{resolvedCount}</span>
|
|
39
|
+
<span className="text-xs text-muted-foreground mt-0.5">Resolved / Closed</span>
|
|
40
|
+
</Card>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* 2. Ticket History Section Header & Action */}
|
|
44
|
+
<div className="flex items-center justify-between">
|
|
45
|
+
<div>
|
|
46
|
+
<h3 className="text-base font-semibold">Service & Problem History</h3>
|
|
47
|
+
<p className="text-xs text-muted-foreground">
|
|
48
|
+
All incident reports and service tickets submitted for this device
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
<Button asChild size="sm" className="gap-1.5">
|
|
52
|
+
<Link href={`/tickets/create?deviceId=${deviceId}&source=device_profile`}>
|
|
53
|
+
<Plus className="w-4 h-4" />
|
|
54
|
+
Report a Problem
|
|
55
|
+
</Link>
|
|
56
|
+
</Button>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* 3. Ticket List */}
|
|
60
|
+
{tickets.length === 0 ? (
|
|
61
|
+
<div className="text-center py-12 border rounded-lg bg-muted/20 space-y-3">
|
|
62
|
+
<Ticket className="w-12 h-12 text-muted-foreground mx-auto opacity-40" />
|
|
63
|
+
<div>
|
|
64
|
+
<h4 className="text-sm font-medium text-foreground">No Tickets Reported</h4>
|
|
65
|
+
<p className="text-xs text-muted-foreground max-w-sm mx-auto mt-1">
|
|
66
|
+
This equipment currently has no reported problems or maintenance tickets in the registry.
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
<Button asChild variant="outline" size="sm" className="mt-2">
|
|
70
|
+
<Link href={`/tickets/create?deviceId=${deviceId}&source=device_profile`}>
|
|
71
|
+
<Plus className="w-4 h-4 mr-1.5" />
|
|
72
|
+
Report First Ticket
|
|
73
|
+
</Link>
|
|
74
|
+
</Button>
|
|
75
|
+
</div>
|
|
76
|
+
) : (
|
|
77
|
+
<div className="space-y-3">
|
|
78
|
+
{tickets.map((ticket) => {
|
|
79
|
+
const reportedDate = ticket.reportedAt || ticket.createdAt
|
|
80
|
+
const formattedDate = reportedDate
|
|
81
|
+
? new Date(reportedDate).toISOString().replace("T", " ").substring(0, 16)
|
|
82
|
+
: "Unknown date"
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Link
|
|
86
|
+
key={ticket.id}
|
|
87
|
+
href={`/tickets/${ticket.id}`}
|
|
88
|
+
className="block group"
|
|
89
|
+
>
|
|
90
|
+
<Card className="transition-all hover:border-primary/50 hover:shadow-xs">
|
|
91
|
+
<CardContent className="p-4 sm:p-5">
|
|
92
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
93
|
+
{/* Left: Info */}
|
|
94
|
+
<div className="space-y-1.5 flex-1 min-w-0">
|
|
95
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
96
|
+
<span className="font-mono text-xs font-semibold text-primary">
|
|
97
|
+
{ticket.ticketNumber}
|
|
98
|
+
</span>
|
|
99
|
+
<TicketPriorityBadge priority={ticket.priorityCode} />
|
|
100
|
+
<TicketStatusBadge status={ticket.statusCode} />
|
|
101
|
+
</div>
|
|
102
|
+
<h4 className="font-medium text-sm text-foreground group-hover:text-primary transition-colors line-clamp-1">
|
|
103
|
+
{ticket.title}
|
|
104
|
+
</h4>
|
|
105
|
+
{ticket.description && (
|
|
106
|
+
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
107
|
+
{ticket.description}
|
|
108
|
+
</p>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Right: Meta & Arrow */}
|
|
113
|
+
<div className="flex sm:flex-col items-center sm:items-end justify-between sm:justify-center text-xs text-muted-foreground shrink-0 border-t sm:border-t-0 pt-2 sm:pt-0 gap-1.5">
|
|
114
|
+
<div className="flex items-center gap-1">
|
|
115
|
+
<Clock className="w-3.5 h-3.5" />
|
|
116
|
+
<span suppressHydrationWarning>{formattedDate} UTC</span>
|
|
117
|
+
</div>
|
|
118
|
+
{ticket.assignedEngineer?.fullName && (
|
|
119
|
+
<div className="flex items-center gap-1 text-foreground/80">
|
|
120
|
+
<User className="w-3.5 h-3.5 text-muted-foreground" />
|
|
121
|
+
<span>{ticket.assignedEngineer.fullName}</span>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
<span className="text-primary group-hover:translate-x-0.5 transition-transform hidden sm:inline-flex items-center gap-0.5 text-xs font-medium">
|
|
125
|
+
View details <ChevronRight className="w-3.5 h-3.5" />
|
|
126
|
+
</span>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</CardContent>
|
|
130
|
+
</Card>
|
|
131
|
+
</Link>
|
|
132
|
+
)
|
|
133
|
+
})}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { DeviceStatusBadge } from "./device-badges"
|
|
2
|
+
import { User, Calendar } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
interface StatusEntry {
|
|
5
|
+
id: string
|
|
6
|
+
previousStatusCode?: string | null
|
|
7
|
+
newStatusCode: string
|
|
8
|
+
reason?: string | null
|
|
9
|
+
notes?: string | null
|
|
10
|
+
effectiveAt: Date | string
|
|
11
|
+
changedByUser?: { fullName: string } | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface LocationEntry {
|
|
15
|
+
id: string
|
|
16
|
+
newHospital?: { name: string } | null
|
|
17
|
+
newDepartment?: { name: string } | null
|
|
18
|
+
transferReason: string
|
|
19
|
+
notes?: string | null
|
|
20
|
+
effectiveAt: Date | string
|
|
21
|
+
changedByUser?: { fullName: string } | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function DeviceStatusTimeline({ entries }: { entries: StatusEntry[] }) {
|
|
25
|
+
if (!entries || entries.length === 0) {
|
|
26
|
+
return <p className="text-sm text-muted-foreground py-4">No status history recorded.</p>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="relative">
|
|
31
|
+
<div className="absolute left-4 top-0 bottom-0 w-px bg-border" />
|
|
32
|
+
<div className="space-y-6">
|
|
33
|
+
{entries.map((entry) => (
|
|
34
|
+
<div key={entry.id} className="relative pl-10">
|
|
35
|
+
<div className="absolute left-2.5 top-1.5 w-3 h-3 rounded-full bg-primary border-2 border-background" />
|
|
36
|
+
<div className="space-y-1">
|
|
37
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
38
|
+
{entry.previousStatusCode && (
|
|
39
|
+
<>
|
|
40
|
+
<DeviceStatusBadge status={entry.previousStatusCode} />
|
|
41
|
+
<span className="text-muted-foreground">→</span>
|
|
42
|
+
</>
|
|
43
|
+
)}
|
|
44
|
+
<DeviceStatusBadge status={entry.newStatusCode} />
|
|
45
|
+
</div>
|
|
46
|
+
{entry.reason && (
|
|
47
|
+
<p className="text-sm text-foreground">{entry.reason}</p>
|
|
48
|
+
)}
|
|
49
|
+
{entry.notes && (
|
|
50
|
+
<p className="text-xs text-muted-foreground italic">{entry.notes}</p>
|
|
51
|
+
)}
|
|
52
|
+
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
53
|
+
<span className="flex items-center gap-1">
|
|
54
|
+
<Calendar className="w-3 h-3" />
|
|
55
|
+
{new Date(entry.effectiveAt).toLocaleString()}
|
|
56
|
+
</span>
|
|
57
|
+
{entry.changedByUser && (
|
|
58
|
+
<span className="flex items-center gap-1">
|
|
59
|
+
<User className="w-3 h-3" />
|
|
60
|
+
{entry.changedByUser.fullName}
|
|
61
|
+
</span>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const transferReasonLabels: Record<string, string> = {
|
|
73
|
+
relocation: "Relocation",
|
|
74
|
+
temporary_loan: "Temporary Loan",
|
|
75
|
+
department_change: "Department Change",
|
|
76
|
+
correction: "Correction",
|
|
77
|
+
replacement: "Replacement",
|
|
78
|
+
workshop: "To Workshop",
|
|
79
|
+
storage: "To Storage",
|
|
80
|
+
other: "Other",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function DeviceLocationTimeline({ entries }: { entries: LocationEntry[] }) {
|
|
84
|
+
if (!entries || entries.length === 0) {
|
|
85
|
+
return <p className="text-sm text-muted-foreground py-4">No location history recorded.</p>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="relative">
|
|
90
|
+
<div className="absolute left-4 top-0 bottom-0 w-px bg-border" />
|
|
91
|
+
<div className="space-y-6">
|
|
92
|
+
{entries.map((entry) => (
|
|
93
|
+
<div key={entry.id} className="relative pl-10">
|
|
94
|
+
<div className="absolute left-2.5 top-1.5 w-3 h-3 rounded-full bg-blue-500 border-2 border-background" />
|
|
95
|
+
<div className="space-y-1">
|
|
96
|
+
<p className="text-sm font-medium">
|
|
97
|
+
{entry.newHospital?.name || "Unknown Hospital"} — {entry.newDepartment?.name || "Unknown Department"}
|
|
98
|
+
</p>
|
|
99
|
+
<p className="text-xs text-muted-foreground">
|
|
100
|
+
Reason: {transferReasonLabels[entry.transferReason] || entry.transferReason}
|
|
101
|
+
</p>
|
|
102
|
+
{entry.notes && (
|
|
103
|
+
<p className="text-xs text-muted-foreground italic">{entry.notes}</p>
|
|
104
|
+
)}
|
|
105
|
+
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
106
|
+
<span className="flex items-center gap-1">
|
|
107
|
+
<Calendar className="w-3 h-3" />
|
|
108
|
+
{new Date(entry.effectiveAt).toLocaleString()}
|
|
109
|
+
</span>
|
|
110
|
+
{entry.changedByUser && (
|
|
111
|
+
<span className="flex items-center gap-1">
|
|
112
|
+
<User className="w-3 h-3" />
|
|
113
|
+
{entry.changedByUser.fullName}
|
|
114
|
+
</span>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Button } from "@/components/ui/button"
|
|
5
|
+
import { Camera, CameraOff, QrCode, Loader2 } from "lucide-react"
|
|
6
|
+
import { useRouter } from "next/navigation"
|
|
7
|
+
import { toast } from "sonner"
|
|
8
|
+
import { playQrBeep, unlockAudio } from "@/lib/utils/sound"
|
|
9
|
+
|
|
10
|
+
export function QrCameraScanner() {
|
|
11
|
+
const router = useRouter()
|
|
12
|
+
const [isScanning, setIsScanning] = React.useState(false)
|
|
13
|
+
const [hasCamera, setHasCamera] = React.useState<boolean | null>(null)
|
|
14
|
+
const [errorMsg, setErrorMsg] = React.useState<string | null>(null)
|
|
15
|
+
const scannerRef = React.useRef<any>(null)
|
|
16
|
+
|
|
17
|
+
// Handle scanned text
|
|
18
|
+
const handleScanSuccess = React.useCallback(
|
|
19
|
+
(decodedText: string) => {
|
|
20
|
+
// Stop scanner immediately
|
|
21
|
+
if (scannerRef.current) {
|
|
22
|
+
scannerRef.current.clear().catch(() => {})
|
|
23
|
+
}
|
|
24
|
+
setIsScanning(false)
|
|
25
|
+
|
|
26
|
+
// Play beep sound to indicate successful capture (works on native devices)
|
|
27
|
+
playQrBeep()
|
|
28
|
+
|
|
29
|
+
toast.success("QR Code detected!")
|
|
30
|
+
|
|
31
|
+
// Process decoded text
|
|
32
|
+
const cleanText = decodedText.trim()
|
|
33
|
+
|
|
34
|
+
// 1. If it's a full URL containing /scan/
|
|
35
|
+
if (cleanText.includes("/scan/")) {
|
|
36
|
+
const parts = cleanText.split("/scan/")
|
|
37
|
+
const code = parts[parts.length - 1].split("?")[0].replace(/\/$/, "")
|
|
38
|
+
router.push(`/scan/${code}`)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. If it's directly an opaque reference
|
|
43
|
+
if (cleanText.startsWith("dv_")) {
|
|
44
|
+
router.push(`/scan/${cleanText}`)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Fallback: Search by asset number
|
|
49
|
+
router.push(`/devices?search=${encodeURIComponent(cleanText)}`)
|
|
50
|
+
},
|
|
51
|
+
[router]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const startScanner = async () => {
|
|
55
|
+
setErrorMsg(null)
|
|
56
|
+
setIsScanning(true)
|
|
57
|
+
|
|
58
|
+
// Unlock Web Audio on user gesture so beep can play on mobile devices
|
|
59
|
+
unlockAudio()
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const { Html5QrcodeScanner } = await import("html5-qrcode")
|
|
63
|
+
|
|
64
|
+
// Small delay to ensure the container div exists
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
try {
|
|
67
|
+
const scanner = new Html5QrcodeScanner(
|
|
68
|
+
"bemms-qr-reader",
|
|
69
|
+
{
|
|
70
|
+
fps: 10,
|
|
71
|
+
qrbox: { width: 250, height: 250 },
|
|
72
|
+
aspectRatio: 1.0,
|
|
73
|
+
showTorchButtonIfSupported: true,
|
|
74
|
+
},
|
|
75
|
+
false
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
scannerRef.current = scanner
|
|
79
|
+
|
|
80
|
+
scanner.render(
|
|
81
|
+
(decodedText) => handleScanSuccess(decodedText),
|
|
82
|
+
(error) => {
|
|
83
|
+
// Non-fatal scanning frames error, can be safely ignored
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
setErrorMsg(err.message || "Failed to initialize camera")
|
|
88
|
+
setIsScanning(false)
|
|
89
|
+
}
|
|
90
|
+
}, 100)
|
|
91
|
+
} catch (err: any) {
|
|
92
|
+
setErrorMsg("Camera scanner library could not be loaded")
|
|
93
|
+
setIsScanning(false)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const stopScanner = () => {
|
|
98
|
+
if (scannerRef.current) {
|
|
99
|
+
scannerRef.current.clear().catch(() => {})
|
|
100
|
+
scannerRef.current = null
|
|
101
|
+
}
|
|
102
|
+
setIsScanning(false)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
React.useEffect(() => {
|
|
106
|
+
return () => {
|
|
107
|
+
if (scannerRef.current) {
|
|
108
|
+
scannerRef.current.clear().catch(() => {})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}, [])
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="space-y-4">
|
|
115
|
+
{/* Scanner Viewport Container */}
|
|
116
|
+
<div className="relative aspect-square max-w-sm mx-auto bg-muted rounded-xl flex flex-col items-center justify-center border-2 border-dashed overflow-hidden">
|
|
117
|
+
{isScanning ? (
|
|
118
|
+
<div id="bemms-qr-reader" className="w-full h-full" />
|
|
119
|
+
) : (
|
|
120
|
+
<div className="flex flex-col items-center justify-center p-6 text-center gap-3">
|
|
121
|
+
<div className="h-16 w-16 rounded-full bg-primary/10 text-primary flex items-center justify-center">
|
|
122
|
+
<QrCode className="w-8 h-8" />
|
|
123
|
+
</div>
|
|
124
|
+
<div className="space-y-1">
|
|
125
|
+
<p className="font-semibold text-sm">Camera QR Scanner</p>
|
|
126
|
+
<p className="text-xs text-muted-foreground max-w-xs">
|
|
127
|
+
Position the BEMMS device QR code within camera view to open the profile.
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{errorMsg && (
|
|
132
|
+
<p className="text-xs text-destructive font-medium px-2 bg-destructive/10 py-1 rounded">
|
|
133
|
+
{errorMsg}
|
|
134
|
+
</p>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
<Button
|
|
138
|
+
type="button"
|
|
139
|
+
variant="default"
|
|
140
|
+
onClick={startScanner}
|
|
141
|
+
className="mt-2 gap-2"
|
|
142
|
+
>
|
|
143
|
+
<Camera className="w-4 h-4" />
|
|
144
|
+
Enable Camera
|
|
145
|
+
</Button>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{isScanning && (
|
|
151
|
+
<div className="flex justify-center">
|
|
152
|
+
<Button
|
|
153
|
+
type="button"
|
|
154
|
+
variant="outline"
|
|
155
|
+
size="sm"
|
|
156
|
+
onClick={stopScanner}
|
|
157
|
+
className="gap-2 text-destructive hover:bg-destructive/10"
|
|
158
|
+
>
|
|
159
|
+
<CameraOff className="w-4 h-4" />
|
|
160
|
+
Stop Camera
|
|
161
|
+
</Button>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react"
|
|
5
|
+
import { Button } from "@/components/ui/button"
|
|
6
|
+
import { Download, Copy, Check, QrCode } from "lucide-react"
|
|
7
|
+
import { toast } from "sonner"
|
|
8
|
+
import { cn } from "@/lib/utils"
|
|
9
|
+
|
|
10
|
+
export interface QrCodeDisplayProps {
|
|
11
|
+
value: string;
|
|
12
|
+
size?: number;
|
|
13
|
+
assetNumber?: string;
|
|
14
|
+
deviceName?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
showDownloadOptions?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function QrCodeDisplay({
|
|
20
|
+
value,
|
|
21
|
+
size = 200,
|
|
22
|
+
assetNumber = "device",
|
|
23
|
+
deviceName = "Medical Device",
|
|
24
|
+
className,
|
|
25
|
+
showDownloadOptions = true,
|
|
26
|
+
}: QrCodeDisplayProps) {
|
|
27
|
+
const [copied, setCopied] = React.useState(false)
|
|
28
|
+
const canvasRef = React.useRef<HTMLDivElement>(null)
|
|
29
|
+
|
|
30
|
+
const handleDownloadPng = () => {
|
|
31
|
+
if (!canvasRef.current) return
|
|
32
|
+
const canvas = canvasRef.current.querySelector("canvas")
|
|
33
|
+
if (!canvas) {
|
|
34
|
+
toast.error("Could not find QR canvas element")
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Create full printable resolution image with border & label
|
|
40
|
+
const padding = 24
|
|
41
|
+
const textHeight = 48
|
|
42
|
+
const exportCanvas = document.createElement("canvas")
|
|
43
|
+
exportCanvas.width = canvas.width + padding * 2
|
|
44
|
+
exportCanvas.height = canvas.height + padding * 2 + textHeight
|
|
45
|
+
const ctx = exportCanvas.getContext("2d")
|
|
46
|
+
|
|
47
|
+
if (ctx) {
|
|
48
|
+
// Fill white background
|
|
49
|
+
ctx.fillStyle = "#ffffff"
|
|
50
|
+
ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height)
|
|
51
|
+
|
|
52
|
+
// Draw QR
|
|
53
|
+
ctx.drawImage(canvas, padding, padding)
|
|
54
|
+
|
|
55
|
+
// Draw label text
|
|
56
|
+
ctx.fillStyle = "#0f172a"
|
|
57
|
+
ctx.font = "bold 14px sans-serif"
|
|
58
|
+
ctx.textAlign = "center"
|
|
59
|
+
ctx.fillText(assetNumber, exportCanvas.width / 2, canvas.height + padding + 18)
|
|
60
|
+
|
|
61
|
+
ctx.fillStyle = "#64748b"
|
|
62
|
+
ctx.font = "11px sans-serif"
|
|
63
|
+
ctx.fillText(deviceName.slice(0, 30), exportCanvas.width / 2, canvas.height + padding + 36)
|
|
64
|
+
|
|
65
|
+
const link = document.createElement("a")
|
|
66
|
+
const safeName = assetNumber.replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
67
|
+
link.download = `BEMMS_QR_${safeName}.png`
|
|
68
|
+
link.href = exportCanvas.toDataURL("image/png")
|
|
69
|
+
link.click()
|
|
70
|
+
toast.success("QR code image downloaded successfully")
|
|
71
|
+
}
|
|
72
|
+
} catch (err: any) {
|
|
73
|
+
toast.error("Failed to download image: " + err.message)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const handleCopyUrl = async () => {
|
|
78
|
+
try {
|
|
79
|
+
await navigator.clipboard.writeText(value)
|
|
80
|
+
setCopied(true)
|
|
81
|
+
toast.success("QR URL copied to clipboard")
|
|
82
|
+
setTimeout(() => setCopied(false), 2000)
|
|
83
|
+
} catch {
|
|
84
|
+
toast.error("Failed to copy to clipboard")
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className={cn("flex flex-col items-center gap-4", className)}>
|
|
90
|
+
<div
|
|
91
|
+
ref={canvasRef}
|
|
92
|
+
className="bg-white p-4 rounded-xl shadow-xs border flex items-center justify-center"
|
|
93
|
+
>
|
|
94
|
+
<QRCodeCanvas
|
|
95
|
+
value={value}
|
|
96
|
+
size={size}
|
|
97
|
+
level="H"
|
|
98
|
+
includeMargin={false}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{showDownloadOptions && (
|
|
103
|
+
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
104
|
+
<Button
|
|
105
|
+
type="button"
|
|
106
|
+
variant="outline"
|
|
107
|
+
size="sm"
|
|
108
|
+
onClick={handleDownloadPng}
|
|
109
|
+
className="gap-1.5"
|
|
110
|
+
>
|
|
111
|
+
<Download className="w-4 h-4 text-primary" />
|
|
112
|
+
<span>Download PNG</span>
|
|
113
|
+
</Button>
|
|
114
|
+
|
|
115
|
+
<Button
|
|
116
|
+
type="button"
|
|
117
|
+
variant="ghost"
|
|
118
|
+
size="sm"
|
|
119
|
+
onClick={handleCopyUrl}
|
|
120
|
+
className="gap-1.5"
|
|
121
|
+
>
|
|
122
|
+
{copied ? (
|
|
123
|
+
<>
|
|
124
|
+
<Check className="w-4 h-4 text-emerald-600" />
|
|
125
|
+
<span className="text-emerald-600">Copied</span>
|
|
126
|
+
</>
|
|
127
|
+
) : (
|
|
128
|
+
<>
|
|
129
|
+
<Copy className="w-4 h-4" />
|
|
130
|
+
<span>Copy Link</span>
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|