@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,408 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDeviceProfile,
|
|
3
|
+
getDeviceStatusHistory,
|
|
4
|
+
getDeviceLocationHistory,
|
|
5
|
+
getDeviceMaintenanceHistory,
|
|
6
|
+
getDeviceSignatures,
|
|
7
|
+
getDeviceDocuments,
|
|
8
|
+
} from "@/lib/actions/devices"
|
|
9
|
+
import { getActiveQrLabel } from "@/lib/actions/qr"
|
|
10
|
+
import { getDeviceActiveTickets, getDeviceTicketHistory } from "@/lib/actions/tickets"
|
|
11
|
+
import { PageHeader } from "@/components/shared/page-header"
|
|
12
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
13
|
+
import { Button } from "@/components/ui/button"
|
|
14
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
15
|
+
import { DeviceStatusBadge, CriticalityBadge, RiskClassBadge } from "@/components/devices/device-badges"
|
|
16
|
+
import { DeviceAvailabilityBanner } from "@/components/devices/device-availability-banner"
|
|
17
|
+
import { DeviceStatusTimeline, DeviceLocationTimeline } from "@/components/devices/device-timelines"
|
|
18
|
+
import { DeviceActionButtons } from "@/components/devices/device-action-buttons"
|
|
19
|
+
import { DeviceMaintenanceTab } from "@/components/devices/device-maintenance-tab"
|
|
20
|
+
import { DeviceTicketsTab } from "@/components/devices/device-tickets-tab"
|
|
21
|
+
import { DeviceSignaturesTab } from "@/components/devices/device-signatures-tab"
|
|
22
|
+
import { DeviceDocumentsTab } from "@/components/devices/device-documents-tab"
|
|
23
|
+
import { TicketStatusBadge } from "@/components/tickets/ticket-badges"
|
|
24
|
+
import { DeviceMiniQr } from "@/components/devices/device-mini-qr"
|
|
25
|
+
import { AlertTriangle, Wrench, ShieldCheck, FileText, Activity, MapPin, Ticket } from "lucide-react"
|
|
26
|
+
import Link from "next/link"
|
|
27
|
+
|
|
28
|
+
export default async function DeviceDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
|
29
|
+
const { id } = await params
|
|
30
|
+
return <DeviceDetailContent deviceId={id} />
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function DeviceDetailContent({ deviceId }: { deviceId: string }) {
|
|
34
|
+
const [
|
|
35
|
+
profileResult,
|
|
36
|
+
statusResult,
|
|
37
|
+
locationResult,
|
|
38
|
+
qrResult,
|
|
39
|
+
ticketsResult,
|
|
40
|
+
ticketHistoryResult,
|
|
41
|
+
maintenanceResult,
|
|
42
|
+
signaturesResult,
|
|
43
|
+
documentsResult,
|
|
44
|
+
] = await Promise.all([
|
|
45
|
+
getDeviceProfile(deviceId),
|
|
46
|
+
getDeviceStatusHistory(deviceId),
|
|
47
|
+
getDeviceLocationHistory(deviceId),
|
|
48
|
+
getActiveQrLabel(deviceId),
|
|
49
|
+
getDeviceActiveTickets(deviceId),
|
|
50
|
+
getDeviceTicketHistory(deviceId),
|
|
51
|
+
getDeviceMaintenanceHistory(deviceId),
|
|
52
|
+
getDeviceSignatures(deviceId),
|
|
53
|
+
getDeviceDocuments(deviceId),
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
const profile = profileResult?.success ? profileResult.data : null
|
|
57
|
+
const device = profile?.device
|
|
58
|
+
const statusHistory = (statusResult?.success && Array.isArray(statusResult.data)) ? statusResult.data : []
|
|
59
|
+
const locationHistory = (locationResult?.success && Array.isArray(locationResult.data)) ? locationResult.data : []
|
|
60
|
+
const qrLabel = qrResult?.success ? qrResult.data : null
|
|
61
|
+
const activeTickets = (ticketsResult?.success && Array.isArray(ticketsResult.data)) ? ticketsResult.data : []
|
|
62
|
+
const allTickets = (ticketHistoryResult?.success && Array.isArray(ticketHistoryResult.data)) ? ticketHistoryResult.data : []
|
|
63
|
+
const maintenanceTasks = maintenanceResult?.success ? (maintenanceResult.data?.tasks || []) : []
|
|
64
|
+
const signatures = signaturesResult?.success ? (signaturesResult.data?.signatures || []) : []
|
|
65
|
+
const signatureEvents = signaturesResult?.success ? (signaturesResult.data?.events || []) : []
|
|
66
|
+
const signedActivitiesCount = new Set(
|
|
67
|
+
signatures.map((s: any) => `${s.sourceType || s.entityType}:${s.entityId}`)
|
|
68
|
+
).size
|
|
69
|
+
const documents = (documentsResult?.success && Array.isArray(documentsResult.data)) ? documentsResult.data : []
|
|
70
|
+
|
|
71
|
+
if (!device) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="p-12 text-center">
|
|
74
|
+
<h2 className="text-xl font-semibold mb-2">Device Not Found</h2>
|
|
75
|
+
<p className="text-muted-foreground text-sm mb-4">
|
|
76
|
+
The requested device record could not be found or you do not have permission to view it.
|
|
77
|
+
</p>
|
|
78
|
+
<Link href="/devices">
|
|
79
|
+
<Button variant="outline">Back to Device Inventory</Button>
|
|
80
|
+
</Link>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const visibleSections = profile?.visibleSections || ["overview"]
|
|
86
|
+
const allowedActions = profile?.allowedActions || ["report_problem", "view_qr"]
|
|
87
|
+
|
|
88
|
+
const showTechnical = visibleSections.includes("technical")
|
|
89
|
+
const showMaintenance = visibleSections.includes("maintenance")
|
|
90
|
+
const showSignatures = visibleSections.includes("signatures")
|
|
91
|
+
const showStatusHistory = visibleSections.includes("status-history")
|
|
92
|
+
const showLocationHistory = visibleSections.includes("location-history")
|
|
93
|
+
const showDocuments = visibleSections.includes("documents")
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="space-y-6">
|
|
97
|
+
{/* Role-shaped Header with Action Buttons */}
|
|
98
|
+
<PageHeader
|
|
99
|
+
title={device.name}
|
|
100
|
+
description={`${device.internalCode} — Asset #${device.assetNumber}`}
|
|
101
|
+
>
|
|
102
|
+
<DeviceActionButtons
|
|
103
|
+
deviceId={device.id}
|
|
104
|
+
deviceName={device.name}
|
|
105
|
+
currentHospital={(device as any).hospital?.name || "Hospital"}
|
|
106
|
+
currentDepartment={(device as any).department?.name || "Department"}
|
|
107
|
+
currentLocation={(device as any).location?.name || device.exactLocationDescription || undefined}
|
|
108
|
+
currentStatus={device.currentStatusCode}
|
|
109
|
+
allowedActions={allowedActions}
|
|
110
|
+
/>
|
|
111
|
+
</PageHeader>
|
|
112
|
+
|
|
113
|
+
{/* Safety & Availability Banner */}
|
|
114
|
+
<DeviceAvailabilityBanner
|
|
115
|
+
status={device.currentStatusCode}
|
|
116
|
+
limitationsNote={device.statusLimitationsNote}
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
{/* Tabbed Navigation */}
|
|
120
|
+
<Tabs defaultValue="overview" className="space-y-6">
|
|
121
|
+
<TabsList className="flex flex-wrap h-auto p-1 gap-1">
|
|
122
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
123
|
+
{showTechnical && <TabsTrigger value="technical">Technical</TabsTrigger>}
|
|
124
|
+
<TabsTrigger value="tickets" className="gap-1.5">
|
|
125
|
+
<Ticket className="w-3.5 h-3.5" />
|
|
126
|
+
Tickets ({allTickets.length})
|
|
127
|
+
</TabsTrigger>
|
|
128
|
+
{showMaintenance && (
|
|
129
|
+
<TabsTrigger value="maintenance" className="gap-1.5">
|
|
130
|
+
<Wrench className="w-3.5 h-3.5" />
|
|
131
|
+
Maintenance ({maintenanceTasks.length})
|
|
132
|
+
</TabsTrigger>
|
|
133
|
+
)}
|
|
134
|
+
{showSignatures && (
|
|
135
|
+
<TabsTrigger value="signatures" className="gap-1.5">
|
|
136
|
+
<ShieldCheck className="w-3.5 h-3.5" />
|
|
137
|
+
Signatures ({signedActivitiesCount})
|
|
138
|
+
</TabsTrigger>
|
|
139
|
+
)}
|
|
140
|
+
{showStatusHistory && <TabsTrigger value="status-history">Status History</TabsTrigger>}
|
|
141
|
+
{showLocationHistory && <TabsTrigger value="location-history">Location History</TabsTrigger>}
|
|
142
|
+
{showDocuments && (
|
|
143
|
+
<TabsTrigger value="documents" className="gap-1.5">
|
|
144
|
+
<FileText className="w-3.5 h-3.5" />
|
|
145
|
+
Documents ({documents.length})
|
|
146
|
+
</TabsTrigger>
|
|
147
|
+
)}
|
|
148
|
+
</TabsList>
|
|
149
|
+
|
|
150
|
+
{/* 1. Overview Tab */}
|
|
151
|
+
<TabsContent value="overview" className="space-y-6">
|
|
152
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
153
|
+
{/* Identity Card */}
|
|
154
|
+
<Card>
|
|
155
|
+
<CardHeader className="pb-3">
|
|
156
|
+
<CardTitle className="text-base">Device Identity</CardTitle>
|
|
157
|
+
</CardHeader>
|
|
158
|
+
<CardContent className="space-y-4 text-sm">
|
|
159
|
+
{qrLabel && (
|
|
160
|
+
<DeviceMiniQr
|
|
161
|
+
deviceId={device.id}
|
|
162
|
+
opaqueReference={qrLabel.opaqueReference}
|
|
163
|
+
assetNumber={device.assetNumber}
|
|
164
|
+
deviceName={device.name}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
<div className="grid gap-3 pt-1">
|
|
169
|
+
<div className="flex justify-between">
|
|
170
|
+
<span className="text-muted-foreground">Internal Code</span>
|
|
171
|
+
<span className="font-mono font-medium">{device.internalCode}</span>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="flex justify-between">
|
|
174
|
+
<span className="text-muted-foreground">Asset Number</span>
|
|
175
|
+
<span className="font-mono font-medium">{device.assetNumber}</span>
|
|
176
|
+
</div>
|
|
177
|
+
<div className="flex justify-between">
|
|
178
|
+
<span className="text-muted-foreground">Serial Number</span>
|
|
179
|
+
<span>{device.serialNumber || "—"}</span>
|
|
180
|
+
</div>
|
|
181
|
+
<div className="flex justify-between">
|
|
182
|
+
<span className="text-muted-foreground">Category</span>
|
|
183
|
+
<span>{(device as any).deviceCategory?.name || "—"}</span>
|
|
184
|
+
</div>
|
|
185
|
+
<div className="flex justify-between">
|
|
186
|
+
<span className="text-muted-foreground">Manufacturer</span>
|
|
187
|
+
<span>{(device as any).manufacturer?.name || "—"}</span>
|
|
188
|
+
</div>
|
|
189
|
+
<div className="flex justify-between">
|
|
190
|
+
<span className="text-muted-foreground">Model</span>
|
|
191
|
+
<span>{(device as any).deviceModel?.modelName || device.modelNameFree || "—"}</span>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="flex justify-between items-center">
|
|
194
|
+
<span className="text-muted-foreground">Status</span>
|
|
195
|
+
<DeviceStatusBadge status={device.currentStatusCode} />
|
|
196
|
+
</div>
|
|
197
|
+
<div className="flex justify-between items-center">
|
|
198
|
+
<span className="text-muted-foreground">Criticality</span>
|
|
199
|
+
<CriticalityBadge level={device.criticalityLevel} />
|
|
200
|
+
</div>
|
|
201
|
+
<div className="flex justify-between items-center">
|
|
202
|
+
<span className="text-muted-foreground">Risk Class</span>
|
|
203
|
+
<RiskClassBadge classification={device.riskClassification} />
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</CardContent>
|
|
207
|
+
</Card>
|
|
208
|
+
|
|
209
|
+
{/* Location & Assignment Card */}
|
|
210
|
+
<Card>
|
|
211
|
+
<CardHeader><CardTitle className="text-base">Location & Assignment</CardTitle></CardHeader>
|
|
212
|
+
<CardContent className="grid gap-3 text-sm">
|
|
213
|
+
<div className="flex justify-between">
|
|
214
|
+
<span className="text-muted-foreground">Organization</span>
|
|
215
|
+
<span>{(device as any).organization?.name || "—"}</span>
|
|
216
|
+
</div>
|
|
217
|
+
<div className="flex justify-between">
|
|
218
|
+
<span className="text-muted-foreground">Hospital</span>
|
|
219
|
+
<span>{(device as any).hospital?.name || "—"}</span>
|
|
220
|
+
</div>
|
|
221
|
+
<div className="flex justify-between">
|
|
222
|
+
<span className="text-muted-foreground">Department</span>
|
|
223
|
+
<span>{(device as any).department?.name || "—"}</span>
|
|
224
|
+
</div>
|
|
225
|
+
<div className="flex justify-between">
|
|
226
|
+
<span className="text-muted-foreground">Location / Room</span>
|
|
227
|
+
<span>{(device as any).location?.name || device.exactLocationDescription || "—"}</span>
|
|
228
|
+
</div>
|
|
229
|
+
<div className="flex justify-between">
|
|
230
|
+
<span className="text-muted-foreground">Assigned Engineer</span>
|
|
231
|
+
<span>{(device as any).assignedEngineer?.fullName || "Unassigned"}</span>
|
|
232
|
+
</div>
|
|
233
|
+
{qrLabel && (
|
|
234
|
+
<div className="flex justify-between items-center">
|
|
235
|
+
<span className="text-muted-foreground">QR Reference</span>
|
|
236
|
+
<Link
|
|
237
|
+
href={`/devices/${device.id}/qr`}
|
|
238
|
+
className="font-mono text-xs text-primary hover:underline"
|
|
239
|
+
>
|
|
240
|
+
{qrLabel.opaqueReference}
|
|
241
|
+
</Link>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
{device.nextPmDueDate && (
|
|
245
|
+
<div className="flex justify-between">
|
|
246
|
+
<span className="text-muted-foreground">Next PM Due</span>
|
|
247
|
+
<span className="font-medium">{new Date(device.nextPmDueDate).toLocaleDateString()}</span>
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
</CardContent>
|
|
251
|
+
</Card>
|
|
252
|
+
|
|
253
|
+
{/* Purchase & Warranty Card */}
|
|
254
|
+
<Card className="md:col-span-2">
|
|
255
|
+
<CardHeader><CardTitle className="text-base">Purchase & Warranty</CardTitle></CardHeader>
|
|
256
|
+
<CardContent className="grid gap-3 sm:grid-cols-2 md:grid-cols-3 text-sm">
|
|
257
|
+
<div>
|
|
258
|
+
<span className="text-muted-foreground block text-xs">Purchase Date</span>
|
|
259
|
+
<span>{device.purchaseDate ? new Date(device.purchaseDate).toLocaleDateString() : "—"}</span>
|
|
260
|
+
</div>
|
|
261
|
+
<div>
|
|
262
|
+
<span className="text-muted-foreground block text-xs">Purchase Cost</span>
|
|
263
|
+
<span>{device.purchaseCost ? `${device.currency || "USD"} ${device.purchaseCost}` : "—"}</span>
|
|
264
|
+
</div>
|
|
265
|
+
<div>
|
|
266
|
+
<span className="text-muted-foreground block text-xs">Installation Date</span>
|
|
267
|
+
<span>{device.installationDate ? new Date(device.installationDate).toLocaleDateString() : "—"}</span>
|
|
268
|
+
</div>
|
|
269
|
+
<div>
|
|
270
|
+
<span className="text-muted-foreground block text-xs">Warranty Start</span>
|
|
271
|
+
<span>{device.warrantyStartDate ? new Date(device.warrantyStartDate).toLocaleDateString() : "—"}</span>
|
|
272
|
+
</div>
|
|
273
|
+
<div>
|
|
274
|
+
<span className="text-muted-foreground block text-xs">Warranty End</span>
|
|
275
|
+
<span>{device.warrantyEndDate ? new Date(device.warrantyEndDate).toLocaleDateString() : "—"}</span>
|
|
276
|
+
</div>
|
|
277
|
+
<div>
|
|
278
|
+
<span className="text-muted-foreground block text-xs">Lifecycle Status</span>
|
|
279
|
+
<span className="capitalize font-medium">{device.lifecycleStatus}</span>
|
|
280
|
+
</div>
|
|
281
|
+
</CardContent>
|
|
282
|
+
</Card>
|
|
283
|
+
|
|
284
|
+
{/* Active Tickets Display */}
|
|
285
|
+
{activeTickets.length > 0 && (
|
|
286
|
+
<Card className="md:col-span-2 border-amber-200 dark:border-amber-900/50">
|
|
287
|
+
<CardHeader className="bg-amber-50/60 dark:bg-amber-950/20 pb-4 border-b border-amber-100 dark:border-amber-900/40">
|
|
288
|
+
<CardTitle className="text-amber-900 dark:text-amber-300 text-base flex items-center gap-2">
|
|
289
|
+
<AlertTriangle className="w-4 h-4 text-amber-600" />
|
|
290
|
+
Active Service Tickets ({activeTickets.length})
|
|
291
|
+
</CardTitle>
|
|
292
|
+
</CardHeader>
|
|
293
|
+
<CardContent className="pt-4 grid gap-3">
|
|
294
|
+
{activeTickets.map((t: any) => (
|
|
295
|
+
<div
|
|
296
|
+
key={t.id}
|
|
297
|
+
className="flex flex-col md:flex-row md:items-center justify-between p-3.5 rounded-lg border bg-card gap-4"
|
|
298
|
+
>
|
|
299
|
+
<div className="space-y-1">
|
|
300
|
+
<Link
|
|
301
|
+
href={`/tickets/${t.id}`}
|
|
302
|
+
className="font-medium hover:underline text-primary flex items-center gap-2"
|
|
303
|
+
>
|
|
304
|
+
<span className="font-mono text-xs px-1.5 py-0.5 bg-muted rounded">
|
|
305
|
+
{t.ticketNumber}
|
|
306
|
+
</span>
|
|
307
|
+
{t.title}
|
|
308
|
+
</Link>
|
|
309
|
+
<p className="text-xs text-muted-foreground line-clamp-1">{t.description}</p>
|
|
310
|
+
</div>
|
|
311
|
+
<div className="flex items-center gap-3 shrink-0">
|
|
312
|
+
<TicketStatusBadge status={t.statusCode} />
|
|
313
|
+
<Button asChild variant="outline" size="sm">
|
|
314
|
+
<Link href={`/tickets/${t.id}`}>View Ticket</Link>
|
|
315
|
+
</Button>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
))}
|
|
319
|
+
</CardContent>
|
|
320
|
+
</Card>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
</TabsContent>
|
|
324
|
+
|
|
325
|
+
{/* 2. Technical Specifications Tab */}
|
|
326
|
+
{showTechnical && (
|
|
327
|
+
<TabsContent value="technical">
|
|
328
|
+
<Card>
|
|
329
|
+
<CardHeader><CardTitle className="text-base">Technical Specifications</CardTitle></CardHeader>
|
|
330
|
+
<CardContent>
|
|
331
|
+
{device.technicalSpecifications ? (
|
|
332
|
+
<pre className="bg-muted p-4 rounded-md text-xs font-mono overflow-auto max-h-96">
|
|
333
|
+
{JSON.stringify(device.technicalSpecifications, null, 2)}
|
|
334
|
+
</pre>
|
|
335
|
+
) : (
|
|
336
|
+
<p className="text-muted-foreground text-sm">No technical specifications recorded.</p>
|
|
337
|
+
)}
|
|
338
|
+
</CardContent>
|
|
339
|
+
</Card>
|
|
340
|
+
</TabsContent>
|
|
341
|
+
)}
|
|
342
|
+
|
|
343
|
+
{/* 3. Service Tickets History Tab */}
|
|
344
|
+
<TabsContent value="tickets">
|
|
345
|
+
<DeviceTicketsTab
|
|
346
|
+
tickets={allTickets}
|
|
347
|
+
deviceId={device.id}
|
|
348
|
+
deviceName={device.name}
|
|
349
|
+
/>
|
|
350
|
+
</TabsContent>
|
|
351
|
+
|
|
352
|
+
{/* 4. Maintenance Tab */}
|
|
353
|
+
{showMaintenance && (
|
|
354
|
+
<TabsContent value="maintenance">
|
|
355
|
+
<DeviceMaintenanceTab
|
|
356
|
+
maintenanceTasks={maintenanceTasks}
|
|
357
|
+
deviceId={device.id}
|
|
358
|
+
/>
|
|
359
|
+
</TabsContent>
|
|
360
|
+
)}
|
|
361
|
+
|
|
362
|
+
{/* 4. Signatures & Approvals Tab */}
|
|
363
|
+
{showSignatures && (
|
|
364
|
+
<TabsContent value="signatures">
|
|
365
|
+
<DeviceSignaturesTab
|
|
366
|
+
signatures={signatures}
|
|
367
|
+
signatureEvents={signatureEvents}
|
|
368
|
+
/>
|
|
369
|
+
</TabsContent>
|
|
370
|
+
)}
|
|
371
|
+
|
|
372
|
+
{/* 5. Status History Tab */}
|
|
373
|
+
{showStatusHistory && (
|
|
374
|
+
<TabsContent value="status-history">
|
|
375
|
+
<Card>
|
|
376
|
+
<CardHeader><CardTitle className="text-base">Status Change History</CardTitle></CardHeader>
|
|
377
|
+
<CardContent>
|
|
378
|
+
<DeviceStatusTimeline entries={statusHistory as any} />
|
|
379
|
+
</CardContent>
|
|
380
|
+
</Card>
|
|
381
|
+
</TabsContent>
|
|
382
|
+
)}
|
|
383
|
+
|
|
384
|
+
{/* 6. Location Transfer History Tab */}
|
|
385
|
+
{showLocationHistory && (
|
|
386
|
+
<TabsContent value="location-history">
|
|
387
|
+
<Card>
|
|
388
|
+
<CardHeader><CardTitle className="text-base">Location Transfer History</CardTitle></CardHeader>
|
|
389
|
+
<CardContent>
|
|
390
|
+
<DeviceLocationTimeline entries={locationHistory as any} />
|
|
391
|
+
</CardContent>
|
|
392
|
+
</Card>
|
|
393
|
+
</TabsContent>
|
|
394
|
+
)}
|
|
395
|
+
|
|
396
|
+
{/* 7. Documents Tab */}
|
|
397
|
+
{showDocuments && (
|
|
398
|
+
<TabsContent value="documents">
|
|
399
|
+
<DeviceDocumentsTab
|
|
400
|
+
documents={documents}
|
|
401
|
+
deviceId={device.id}
|
|
402
|
+
/>
|
|
403
|
+
</TabsContent>
|
|
404
|
+
)}
|
|
405
|
+
</Tabs>
|
|
406
|
+
</div>
|
|
407
|
+
)
|
|
408
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getDeviceById } from "@/lib/actions/devices"
|
|
2
|
+
import { getActiveQrLabel } from "@/lib/actions/qr"
|
|
3
|
+
import { PageHeader } from "@/components/shared/page-header"
|
|
4
|
+
import { Button } from "@/components/ui/button"
|
|
5
|
+
import { ArrowLeft } from "lucide-react"
|
|
6
|
+
import Link from "next/link"
|
|
7
|
+
import { use } from "react"
|
|
8
|
+
import { DeviceQrCard } from "@/components/devices/device-qr-card"
|
|
9
|
+
|
|
10
|
+
export default function DeviceQrPage({ params }: { params: Promise<{ id: string }> }) {
|
|
11
|
+
const { id } = use(params)
|
|
12
|
+
return <QrContent deviceId={id} />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function QrContent({ deviceId }: { deviceId: string }) {
|
|
16
|
+
const [deviceResult, qrResult] = await Promise.all([
|
|
17
|
+
getDeviceById(deviceId),
|
|
18
|
+
getActiveQrLabel(deviceId),
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
const device = deviceResult?.success ? deviceResult.data : null
|
|
22
|
+
const qrLabel = qrResult?.success ? qrResult.data : null
|
|
23
|
+
|
|
24
|
+
if (!device) {
|
|
25
|
+
return <div className="p-8 text-center text-muted-foreground">Device not found</div>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="space-y-6">
|
|
30
|
+
<PageHeader
|
|
31
|
+
title={`QR Code: ${device.name}`}
|
|
32
|
+
description={`${device.internalCode} — Asset #${device.assetNumber}`}
|
|
33
|
+
>
|
|
34
|
+
<Link href={`/devices/${deviceId}`} className="print:hidden">
|
|
35
|
+
<Button variant="outline" size="sm">
|
|
36
|
+
<ArrowLeft className="w-4 h-4 mr-2" /> Back to Device
|
|
37
|
+
</Button>
|
|
38
|
+
</Link>
|
|
39
|
+
</PageHeader>
|
|
40
|
+
|
|
41
|
+
<DeviceQrCard
|
|
42
|
+
device={{
|
|
43
|
+
id: device.id,
|
|
44
|
+
name: device.name,
|
|
45
|
+
internalCode: device.internalCode,
|
|
46
|
+
assetNumber: device.assetNumber,
|
|
47
|
+
hospital: (device as any).hospital,
|
|
48
|
+
department: (device as any).department,
|
|
49
|
+
}}
|
|
50
|
+
qrLabel={qrLabel || null}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|