@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,184 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Clock, UserCheck, Zap } from 'lucide-react';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
interface WaitingForEngineerBannerProps {
|
|
7
|
+
/** When the ticket was reported */
|
|
8
|
+
reportedAt: Date | string;
|
|
9
|
+
/** The ticket's reported impact level */
|
|
10
|
+
reportedImpact?: string;
|
|
11
|
+
/** Whether to show in compact mode (for card lists) */
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
className?: string;
|
|
14
|
+
children?: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getWaitingDuration(reportedAt: Date | string): string {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
const reported = new Date(reportedAt).getTime();
|
|
20
|
+
const diffMs = now - reported;
|
|
21
|
+
const minutes = Math.floor(diffMs / (1000 * 60));
|
|
22
|
+
const hours = Math.floor(minutes / 60);
|
|
23
|
+
const days = Math.floor(hours / 24);
|
|
24
|
+
|
|
25
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
26
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
27
|
+
if (minutes > 0) return `${minutes}m`;
|
|
28
|
+
return 'just now';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Visual indicator that a ticket is waiting for an engineer to accept it.
|
|
33
|
+
* Used on ticket cards and ticket detail pages.
|
|
34
|
+
*/
|
|
35
|
+
export function WaitingForEngineerBanner({
|
|
36
|
+
reportedAt,
|
|
37
|
+
reportedImpact,
|
|
38
|
+
compact = false,
|
|
39
|
+
className,
|
|
40
|
+
children,
|
|
41
|
+
}: WaitingForEngineerBannerProps) {
|
|
42
|
+
const waitDuration = getWaitingDuration(reportedAt);
|
|
43
|
+
const isCritical = reportedImpact === 'patient_care_affected';
|
|
44
|
+
const isUrgent = reportedImpact === 'device_not_usable' || isCritical;
|
|
45
|
+
|
|
46
|
+
if (compact) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={cn(
|
|
50
|
+
'flex items-center gap-1.5 text-xs font-medium',
|
|
51
|
+
isCritical
|
|
52
|
+
? 'text-red-600 dark:text-red-400'
|
|
53
|
+
: isUrgent
|
|
54
|
+
? 'text-amber-600 dark:text-amber-400'
|
|
55
|
+
: 'text-blue-600 dark:text-blue-400',
|
|
56
|
+
className,
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
<Clock className="w-3 h-3 animate-pulse" />
|
|
60
|
+
<span>Waiting for engineer · {waitDuration}</span>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
className={cn(
|
|
68
|
+
'rounded-lg border p-4',
|
|
69
|
+
isCritical
|
|
70
|
+
? 'bg-red-50 border-red-300 dark:bg-red-950/30 dark:border-red-800'
|
|
71
|
+
: isUrgent
|
|
72
|
+
? 'bg-amber-50 border-amber-300 dark:bg-amber-950/30 dark:border-amber-800'
|
|
73
|
+
: 'bg-blue-50 border-blue-200 dark:bg-blue-950/30 dark:border-blue-800',
|
|
74
|
+
className,
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
|
78
|
+
<div className="flex items-start gap-3 flex-1 min-w-0">
|
|
79
|
+
<div
|
|
80
|
+
className={cn(
|
|
81
|
+
'p-2 rounded-lg shrink-0 mt-0.5 sm:mt-0',
|
|
82
|
+
isCritical
|
|
83
|
+
? 'bg-red-100 dark:bg-red-900/50'
|
|
84
|
+
: isUrgent
|
|
85
|
+
? 'bg-amber-100 dark:bg-amber-900/50'
|
|
86
|
+
: 'bg-blue-100 dark:bg-blue-900/50',
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{isCritical ? (
|
|
90
|
+
<Zap className="w-5 h-5 text-red-600 dark:text-red-400" />
|
|
91
|
+
) : (
|
|
92
|
+
<Clock className="w-5 h-5 text-amber-600 dark:text-amber-400 animate-pulse" />
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex-1 min-w-0">
|
|
96
|
+
<h4
|
|
97
|
+
className={cn(
|
|
98
|
+
'font-semibold text-sm',
|
|
99
|
+
isCritical
|
|
100
|
+
? 'text-red-800 dark:text-red-200'
|
|
101
|
+
: isUrgent
|
|
102
|
+
? 'text-amber-800 dark:text-amber-200'
|
|
103
|
+
: 'text-blue-800 dark:text-blue-200',
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
{isCritical
|
|
107
|
+
? '🚨 URGENT — Waiting for engineer to accept'
|
|
108
|
+
: '⏳ Waiting for engineer to accept'}
|
|
109
|
+
</h4>
|
|
110
|
+
<p
|
|
111
|
+
className={cn(
|
|
112
|
+
'text-xs mt-1',
|
|
113
|
+
isCritical
|
|
114
|
+
? 'text-red-700 dark:text-red-300'
|
|
115
|
+
: isUrgent
|
|
116
|
+
? 'text-amber-700 dark:text-amber-300'
|
|
117
|
+
: 'text-blue-700 dark:text-blue-300',
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
No engineer has accepted this ticket yet. Waiting for {waitDuration}.
|
|
121
|
+
{isCritical && ' This ticket affects patient care and requires immediate attention.'}
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
{children && (
|
|
126
|
+
<div className="shrink-0 self-end sm:self-center pl-10 sm:pl-0">
|
|
127
|
+
{children}
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Accepted by Engineer Banner ──────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
interface AcceptedByEngineerBannerProps {
|
|
138
|
+
engineerName: string;
|
|
139
|
+
acceptedAt: Date | string;
|
|
140
|
+
compact?: boolean;
|
|
141
|
+
className?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Shows that a ticket has been accepted by an engineer.
|
|
146
|
+
* Used on the staff ticket view to reassure the reporter.
|
|
147
|
+
*/
|
|
148
|
+
export function AcceptedByEngineerBanner({
|
|
149
|
+
engineerName,
|
|
150
|
+
acceptedAt,
|
|
151
|
+
compact = false,
|
|
152
|
+
className,
|
|
153
|
+
}: AcceptedByEngineerBannerProps) {
|
|
154
|
+
const acceptedDate = new Date(acceptedAt);
|
|
155
|
+
const timeStr = acceptedDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
156
|
+
const dateStr = acceptedDate.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
157
|
+
|
|
158
|
+
if (compact) {
|
|
159
|
+
return (
|
|
160
|
+
<div className={cn('flex items-center gap-1.5 text-xs font-medium text-green-600 dark:text-green-400', className)}>
|
|
161
|
+
<UserCheck className="w-3 h-3" />
|
|
162
|
+
<span>Accepted by {engineerName}</span>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className={cn('rounded-lg border bg-green-50 border-green-200 dark:bg-green-950/30 dark:border-green-800 p-4', className)}>
|
|
169
|
+
<div className="flex items-start gap-3">
|
|
170
|
+
<div className="p-2 rounded-lg bg-green-100 dark:bg-green-900/50 shrink-0">
|
|
171
|
+
<UserCheck className="w-5 h-5 text-green-600 dark:text-green-400" />
|
|
172
|
+
</div>
|
|
173
|
+
<div className="flex-1 min-w-0">
|
|
174
|
+
<h4 className="font-semibold text-sm text-green-800 dark:text-green-200">
|
|
175
|
+
✅ Engineer {engineerName} accepted this ticket
|
|
176
|
+
</h4>
|
|
177
|
+
<p className="text-xs text-green-700 dark:text-green-300 mt-1">
|
|
178
|
+
Accepted on {dateStr} at {timeStr}. Your reported issue is being investigated.
|
|
179
|
+
</p>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Auto-refreshes the tickets list view when new ticket notifications arrive,
|
|
8
|
+
* or periodically when the tab is visible.
|
|
9
|
+
*/
|
|
10
|
+
export function TicketListAutoRefresh({ pollIntervalMs = 10000 }: { pollIntervalMs?: number }) {
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
const lastRefreshRef = useRef(Date.now());
|
|
13
|
+
|
|
14
|
+
const triggerRefresh = () => {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
if (now - lastRefreshRef.current < 3000) return;
|
|
17
|
+
lastRefreshRef.current = now;
|
|
18
|
+
router.refresh();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
// 1. Listen for global notification event from NotificationBell
|
|
23
|
+
const handleNotification = (event: Event) => {
|
|
24
|
+
const customEvent = event as CustomEvent<any[]>;
|
|
25
|
+
const notifs = customEvent.detail;
|
|
26
|
+
if (!Array.isArray(notifs)) return;
|
|
27
|
+
|
|
28
|
+
const hasTicketEvent = notifs.some(
|
|
29
|
+
(n) =>
|
|
30
|
+
n.relatedEntityType === 'service_ticket' ||
|
|
31
|
+
(n.notificationType && n.notificationType.startsWith('ticket_'))
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (hasTicketEvent) {
|
|
35
|
+
triggerRefresh();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
window.addEventListener('bemms:notification', handleNotification);
|
|
40
|
+
|
|
41
|
+
// 2. Periodic refresh when tab is visible
|
|
42
|
+
const interval = setInterval(() => {
|
|
43
|
+
if (!document.hidden) {
|
|
44
|
+
triggerRefresh();
|
|
45
|
+
}
|
|
46
|
+
}, pollIntervalMs);
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
window.removeEventListener('bemms:notification', handleNotification);
|
|
50
|
+
clearInterval(interval);
|
|
51
|
+
};
|
|
52
|
+
}, [router, pollIntervalMs]);
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Badge } from '@/components/ui/badge';
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogTrigger,
|
|
13
|
+
} from '@/components/ui/dialog';
|
|
14
|
+
import {
|
|
15
|
+
User,
|
|
16
|
+
Phone,
|
|
17
|
+
Mail,
|
|
18
|
+
Building2,
|
|
19
|
+
Stethoscope,
|
|
20
|
+
IdCard,
|
|
21
|
+
MapPin,
|
|
22
|
+
ExternalLink,
|
|
23
|
+
ShieldCheck,
|
|
24
|
+
} from 'lucide-react';
|
|
25
|
+
|
|
26
|
+
interface ReporterInfo {
|
|
27
|
+
id?: string;
|
|
28
|
+
fullName?: string | null;
|
|
29
|
+
email?: string | null;
|
|
30
|
+
phone?: string | null;
|
|
31
|
+
jobTitle?: string | null;
|
|
32
|
+
employeeIdentifier?: string | null;
|
|
33
|
+
departmentName?: string | null;
|
|
34
|
+
hospitalName?: string | null;
|
|
35
|
+
reportedLocationCorrection?: string | null;
|
|
36
|
+
requesterContactName?: string | null;
|
|
37
|
+
requesterContactPhone?: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface TicketReporterProfileProps {
|
|
41
|
+
reporter: ReporterInfo;
|
|
42
|
+
isBiomedOrAdmin?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function TicketReporterProfile({
|
|
46
|
+
reporter,
|
|
47
|
+
isBiomedOrAdmin = false,
|
|
48
|
+
}: TicketReporterProfileProps) {
|
|
49
|
+
const [open, setOpen] = useState(false);
|
|
50
|
+
|
|
51
|
+
const name = reporter.fullName || reporter.requesterContactName || 'Unknown Reporter';
|
|
52
|
+
const jobTitle = reporter.jobTitle || 'Department Clinical Staff';
|
|
53
|
+
const phone = reporter.requesterContactPhone || reporter.phone;
|
|
54
|
+
const extension = reporter.employeeIdentifier;
|
|
55
|
+
const department = reporter.departmentName || 'Hospital Department';
|
|
56
|
+
const hospital = reporter.hospitalName || 'Hospital';
|
|
57
|
+
|
|
58
|
+
const initials = name
|
|
59
|
+
.split(' ')
|
|
60
|
+
.map((n) => n[0])
|
|
61
|
+
.join('')
|
|
62
|
+
.substring(0, 2)
|
|
63
|
+
.toUpperCase();
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="space-y-3">
|
|
67
|
+
{/* ── Reporter Summary Card in Sidebar ──────────────────────────── */}
|
|
68
|
+
<div className="p-3.5 rounded-xl border bg-muted/20 space-y-3">
|
|
69
|
+
<div className="flex items-start gap-3">
|
|
70
|
+
<Avatar className="h-10 w-10 shrink-0 border">
|
|
71
|
+
<AvatarFallback className="bg-primary/10 text-primary font-bold text-xs">
|
|
72
|
+
{initials}
|
|
73
|
+
</AvatarFallback>
|
|
74
|
+
</Avatar>
|
|
75
|
+
<div className="min-w-0 flex-1">
|
|
76
|
+
<p className="font-bold text-sm text-foreground truncate">{name}</p>
|
|
77
|
+
<p className="text-xs text-muted-foreground flex items-center gap-1 mt-0.5 truncate">
|
|
78
|
+
<Stethoscope className="w-3 h-3 text-primary shrink-0" />
|
|
79
|
+
<span>{jobTitle}</span>
|
|
80
|
+
</p>
|
|
81
|
+
<p className="text-[11px] text-muted-foreground truncate mt-0.5">
|
|
82
|
+
{department}
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Quick Call & Email actions */}
|
|
88
|
+
<div className="space-y-1.5 pt-2 border-t text-xs">
|
|
89
|
+
{phone && (
|
|
90
|
+
<a
|
|
91
|
+
href={`tel:${phone}`}
|
|
92
|
+
className="flex items-center gap-2 p-1 rounded-md hover:bg-muted text-primary font-medium transition"
|
|
93
|
+
>
|
|
94
|
+
<Phone className="w-3.5 h-3.5 text-green-600 shrink-0" />
|
|
95
|
+
<span className="truncate">{phone}</span>
|
|
96
|
+
</a>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{extension && (
|
|
100
|
+
<div className="flex items-center gap-2 p-1 text-muted-foreground">
|
|
101
|
+
<IdCard className="w-3.5 h-3.5 shrink-0" />
|
|
102
|
+
<span className="truncate">{extension}</span>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{reporter.email && (
|
|
107
|
+
<a
|
|
108
|
+
href={`mailto:${reporter.email}`}
|
|
109
|
+
className="flex items-center gap-2 p-1 rounded-md hover:bg-muted text-primary font-medium transition"
|
|
110
|
+
>
|
|
111
|
+
<Mail className="w-3.5 h-3.5 text-blue-600 shrink-0" />
|
|
112
|
+
<span className="truncate">{reporter.email}</span>
|
|
113
|
+
</a>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Dialog Trigger to view full reporter details */}
|
|
118
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
119
|
+
<DialogTrigger asChild>
|
|
120
|
+
<Button
|
|
121
|
+
variant="outline"
|
|
122
|
+
size="sm"
|
|
123
|
+
className="w-full text-xs h-8 gap-1.5 bg-card mt-1"
|
|
124
|
+
>
|
|
125
|
+
<User className="w-3.5 h-3.5" />
|
|
126
|
+
View Full Reporter Info
|
|
127
|
+
</Button>
|
|
128
|
+
</DialogTrigger>
|
|
129
|
+
|
|
130
|
+
<DialogContent className="max-w-md">
|
|
131
|
+
<DialogHeader>
|
|
132
|
+
<DialogTitle className="flex items-center gap-2 text-base">
|
|
133
|
+
<User className="w-4 h-4 text-primary" />
|
|
134
|
+
Department Reporter Information
|
|
135
|
+
</DialogTitle>
|
|
136
|
+
</DialogHeader>
|
|
137
|
+
|
|
138
|
+
<div className="space-y-4 pt-2">
|
|
139
|
+
{/* Header profile */}
|
|
140
|
+
<div className="flex items-center gap-3.5 p-3 rounded-xl bg-muted/30 border">
|
|
141
|
+
<Avatar className="h-12 w-12 border shadow-sm">
|
|
142
|
+
<AvatarFallback className="bg-primary text-primary-foreground font-bold text-base">
|
|
143
|
+
{initials}
|
|
144
|
+
</AvatarFallback>
|
|
145
|
+
</Avatar>
|
|
146
|
+
<div className="min-w-0 flex-1">
|
|
147
|
+
<h4 className="font-bold text-base text-foreground">{name}</h4>
|
|
148
|
+
<p className="text-xs text-muted-foreground flex items-center gap-1 font-medium mt-0.5">
|
|
149
|
+
<Stethoscope className="w-3.5 h-3.5 text-primary" />
|
|
150
|
+
{jobTitle}
|
|
151
|
+
</p>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Detail fields */}
|
|
156
|
+
<div className="grid grid-cols-2 gap-3 text-xs">
|
|
157
|
+
<div className="p-3 rounded-xl border bg-card space-y-1">
|
|
158
|
+
<span className="text-[10px] text-muted-foreground font-semibold uppercase">
|
|
159
|
+
Department
|
|
160
|
+
</span>
|
|
161
|
+
<p className="font-semibold text-foreground">{department}</p>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div className="p-3 rounded-xl border bg-card space-y-1">
|
|
165
|
+
<span className="text-[10px] text-muted-foreground font-semibold uppercase">
|
|
166
|
+
Hospital
|
|
167
|
+
</span>
|
|
168
|
+
<p className="font-semibold text-foreground">{hospital}</p>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div className="p-3 rounded-xl border bg-card space-y-1">
|
|
172
|
+
<span className="text-[10px] text-muted-foreground font-semibold uppercase">
|
|
173
|
+
Contact Phone
|
|
174
|
+
</span>
|
|
175
|
+
<p className="font-medium text-foreground">
|
|
176
|
+
{phone ? (
|
|
177
|
+
<a href={`tel:${phone}`} className="text-primary hover:underline">
|
|
178
|
+
{phone}
|
|
179
|
+
</a>
|
|
180
|
+
) : (
|
|
181
|
+
'Not provided'
|
|
182
|
+
)}
|
|
183
|
+
</p>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div className="p-3 rounded-xl border bg-card space-y-1">
|
|
187
|
+
<span className="text-[10px] text-muted-foreground font-semibold uppercase">
|
|
188
|
+
Hospital Extension / Ward
|
|
189
|
+
</span>
|
|
190
|
+
<p className="font-medium text-foreground">{extension || 'Not provided'}</p>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{reporter.email && (
|
|
195
|
+
<div className="p-3 rounded-xl border bg-card text-xs space-y-1">
|
|
196
|
+
<span className="text-[10px] text-muted-foreground font-semibold uppercase">
|
|
197
|
+
Official Email
|
|
198
|
+
</span>
|
|
199
|
+
<p className="font-medium text-foreground">
|
|
200
|
+
<a href={`mailto:${reporter.email}`} className="text-primary hover:underline">
|
|
201
|
+
{reporter.email}
|
|
202
|
+
</a>
|
|
203
|
+
</p>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
{reporter.reportedLocationCorrection && (
|
|
208
|
+
<div className="p-3 rounded-xl border bg-amber-50/50 dark:bg-amber-950/20 border-amber-200 dark:border-amber-800 text-xs space-y-1">
|
|
209
|
+
<span className="text-[10px] text-amber-800 dark:text-amber-300 font-semibold uppercase flex items-center gap-1">
|
|
210
|
+
<MapPin className="w-3 h-3" /> Reported Room / Bed Location
|
|
211
|
+
</span>
|
|
212
|
+
<p className="font-medium text-amber-950 dark:text-amber-100">
|
|
213
|
+
{reporter.reportedLocationCorrection}
|
|
214
|
+
</p>
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{/* Direct call action */}
|
|
219
|
+
{phone && (
|
|
220
|
+
<div className="pt-2">
|
|
221
|
+
<Button asChild className="w-full gap-2">
|
|
222
|
+
<a href={`tel:${phone}`}>
|
|
223
|
+
<Phone className="w-4 h-4" />
|
|
224
|
+
Call Reporter Now ({phone})
|
|
225
|
+
</a>
|
|
226
|
+
</Button>
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
</DialogContent>
|
|
231
|
+
</Dialog>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { SignatureTimeline } from "@/components/maintenance/signature-card"
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
5
|
+
import { ShieldCheck } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
interface TicketSignaturesPanelProps {
|
|
8
|
+
signatures: any[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function TicketSignaturesPanel({ signatures = [] }: TicketSignaturesPanelProps) {
|
|
12
|
+
if (!signatures || signatures.length === 0) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Card className="border-indigo-100 dark:border-indigo-950/60 bg-gradient-to-b from-indigo-50/20 to-transparent">
|
|
18
|
+
<CardHeader className="pb-3 border-b border-border/50">
|
|
19
|
+
<div className="flex items-center gap-2">
|
|
20
|
+
<ShieldCheck className="w-5 h-5 text-indigo-600 dark:text-indigo-400" />
|
|
21
|
+
<CardTitle className="text-base font-semibold">
|
|
22
|
+
Electronic Signatures ({signatures.length})
|
|
23
|
+
</CardTitle>
|
|
24
|
+
</div>
|
|
25
|
+
</CardHeader>
|
|
26
|
+
<CardContent className="pt-4">
|
|
27
|
+
<SignatureTimeline signatures={signatures} />
|
|
28
|
+
</CardContent>
|
|
29
|
+
</Card>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { TicketStatusBadge, TicketPriorityBadge } from "./ticket-badges"
|
|
2
|
+
import { DeviceStatusBadge } from "@/components/devices/device-badges"
|
|
3
|
+
import { User, Calendar } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
const eventTypeLabels: Record<string, string> = {
|
|
6
|
+
created: "Ticket Created",
|
|
7
|
+
acknowledged: "Acknowledged",
|
|
8
|
+
triaged: "Triaged",
|
|
9
|
+
assigned: "Assigned to Engineer",
|
|
10
|
+
reassigned: "Reassigned",
|
|
11
|
+
accepted: "Accepted by Engineer",
|
|
12
|
+
status_changed: "Status Changed",
|
|
13
|
+
priority_changed: "Priority Changed",
|
|
14
|
+
waiting_for_info: "Waiting for Information",
|
|
15
|
+
info_received: "Information Received",
|
|
16
|
+
maintenance_task_linked: "Maintenance Task Linked",
|
|
17
|
+
resolved: "Resolved",
|
|
18
|
+
closed: "Closed",
|
|
19
|
+
cancelled: "Cancelled",
|
|
20
|
+
reopened: "Reopened",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getEventDotClass(eventType: string): string {
|
|
24
|
+
switch (eventType) {
|
|
25
|
+
case 'status_changed':
|
|
26
|
+
// Amber/Orange for status updates
|
|
27
|
+
return 'bg-amber-500 ring-4 ring-amber-100 dark:ring-amber-950/60'
|
|
28
|
+
case 'waiting_for_info':
|
|
29
|
+
return 'bg-yellow-500 ring-4 ring-yellow-100 dark:ring-yellow-950/60'
|
|
30
|
+
case 'reopened':
|
|
31
|
+
return 'bg-orange-500 ring-4 ring-orange-100 dark:ring-orange-950/60'
|
|
32
|
+
case 'resolved':
|
|
33
|
+
return 'bg-emerald-500 ring-4 ring-emerald-100 dark:ring-emerald-950/60'
|
|
34
|
+
case 'closed':
|
|
35
|
+
return 'bg-slate-600 ring-4 ring-slate-100 dark:ring-slate-800'
|
|
36
|
+
case 'cancelled':
|
|
37
|
+
return 'bg-rose-400 ring-4 ring-rose-100 dark:ring-rose-950/60'
|
|
38
|
+
case 'created':
|
|
39
|
+
return 'bg-blue-500 ring-4 ring-blue-100 dark:ring-blue-950/60'
|
|
40
|
+
case 'acknowledged':
|
|
41
|
+
case 'triaged':
|
|
42
|
+
return 'bg-sky-500 ring-4 ring-sky-100 dark:ring-sky-950/60'
|
|
43
|
+
case 'assigned':
|
|
44
|
+
case 'reassigned':
|
|
45
|
+
return 'bg-violet-500 ring-4 ring-violet-100 dark:ring-violet-950/60'
|
|
46
|
+
case 'accepted':
|
|
47
|
+
return 'bg-indigo-500 ring-4 ring-indigo-100 dark:ring-indigo-950/60'
|
|
48
|
+
default:
|
|
49
|
+
return 'bg-amber-500 ring-4 ring-amber-100 dark:ring-amber-950/60'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface TimelineEntry {
|
|
54
|
+
id: string
|
|
55
|
+
eventType: string
|
|
56
|
+
previousValueJsonb?: any
|
|
57
|
+
newValueJsonb?: any
|
|
58
|
+
reason?: string | null
|
|
59
|
+
visibility: string
|
|
60
|
+
actor?: { fullName: string } | null
|
|
61
|
+
createdAt: Date | string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function TicketTimeline({ entries }: { entries: TimelineEntry[] }) {
|
|
65
|
+
if (!entries || entries.length === 0) {
|
|
66
|
+
return <p className="text-sm text-muted-foreground py-4">No timeline events recorded.</p>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="relative">
|
|
71
|
+
<div className="absolute left-4 top-0 bottom-0 w-px bg-border" />
|
|
72
|
+
<div className="space-y-6">
|
|
73
|
+
{entries.map((entry) => (
|
|
74
|
+
<div key={entry.id} className="relative pl-10">
|
|
75
|
+
<div
|
|
76
|
+
className={`absolute left-2.5 top-1.5 w-3 h-3 rounded-full border-2 border-background ${getEventDotClass(
|
|
77
|
+
entry.eventType
|
|
78
|
+
)}`}
|
|
79
|
+
/>
|
|
80
|
+
<div className="space-y-1">
|
|
81
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
82
|
+
<span className="font-medium text-sm">
|
|
83
|
+
{eventTypeLabels[entry.eventType] || entry.eventType}
|
|
84
|
+
</span>
|
|
85
|
+
{entry.visibility === 'internal' && (
|
|
86
|
+
<span className="text-xs bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded font-medium">
|
|
87
|
+
Internal
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
{entry.reason && (
|
|
92
|
+
<p className="text-sm text-foreground">{entry.reason}</p>
|
|
93
|
+
)}
|
|
94
|
+
{entry.newValueJsonb && typeof entry.newValueJsonb === 'object' && (
|
|
95
|
+
<div className="flex items-center gap-1.5 flex-wrap pt-0.5">
|
|
96
|
+
{entry.newValueJsonb.statusCode && (
|
|
97
|
+
<TicketStatusBadge status={entry.newValueJsonb.statusCode} />
|
|
98
|
+
)}
|
|
99
|
+
{entry.newValueJsonb.deviceStatusCode && (
|
|
100
|
+
<DeviceStatusBadge status={entry.newValueJsonb.deviceStatusCode} />
|
|
101
|
+
)}
|
|
102
|
+
{entry.newValueJsonb.priorityCode && (
|
|
103
|
+
<TicketPriorityBadge priority={entry.newValueJsonb.priorityCode} />
|
|
104
|
+
)}
|
|
105
|
+
{entry.newValueJsonb.partName && (
|
|
106
|
+
<span className="text-xs bg-amber-100 dark:bg-amber-950/60 text-amber-800 dark:text-amber-200 border border-amber-300 dark:border-amber-800 px-2 py-0.5 rounded font-medium inline-flex items-center gap-1">
|
|
107
|
+
📦 {entry.newValueJsonb.partName}
|
|
108
|
+
</span>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
113
|
+
<span className="flex items-center gap-1">
|
|
114
|
+
<Calendar className="w-3 h-3" />
|
|
115
|
+
{new Date(entry.createdAt).toLocaleString()}
|
|
116
|
+
</span>
|
|
117
|
+
{entry.actor && (
|
|
118
|
+
<span className="flex items-center gap-1">
|
|
119
|
+
<User className="w-3 h-3" />
|
|
120
|
+
{entry.actor.fullName}
|
|
121
|
+
</span>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
))}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|