@omersx/bemms 0.0.0-stage → 0.1.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,197 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
|
|
6
|
+
interface TicketAutoRefreshProps {
|
|
7
|
+
ticketId: string;
|
|
8
|
+
initialUpdatedAt?: string | Date | null;
|
|
9
|
+
initialStatusCode?: string;
|
|
10
|
+
initialCommentCount?: number;
|
|
11
|
+
initialAssignedEngineerUserId?: string | null;
|
|
12
|
+
pollIntervalMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Live synchronization component for the ticket detail view.
|
|
17
|
+
*
|
|
18
|
+
* Provides instantaneous updates without full page reload:
|
|
19
|
+
* 1. Listens for window 'bemms:notification' events dispatched by NotificationBell (SSE & Push).
|
|
20
|
+
* 2. Directly connects to SSE as a fallback stream for instant pushes.
|
|
21
|
+
* 3. Fast-polls /api/tickets/[id]/live-sync (default 3.5s) when the tab is visible.
|
|
22
|
+
* 4. Refreshes immediately when the user switches back to this browser tab.
|
|
23
|
+
*/
|
|
24
|
+
export function TicketAutoRefresh({
|
|
25
|
+
ticketId,
|
|
26
|
+
initialUpdatedAt,
|
|
27
|
+
initialStatusCode,
|
|
28
|
+
initialCommentCount = 0,
|
|
29
|
+
initialAssignedEngineerUserId,
|
|
30
|
+
pollIntervalMs = 3500,
|
|
31
|
+
}: TicketAutoRefreshProps) {
|
|
32
|
+
const router = useRouter();
|
|
33
|
+
|
|
34
|
+
const lastKnownRef = useRef({
|
|
35
|
+
updatedAt: initialUpdatedAt ? new Date(initialUpdatedAt).toISOString() : '',
|
|
36
|
+
statusCode: initialStatusCode || '',
|
|
37
|
+
commentCount: initialCommentCount,
|
|
38
|
+
assignedEngineerUserId: initialAssignedEngineerUserId || null,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const lastRefreshTimestampRef = useRef<number>(Date.now());
|
|
42
|
+
const isRefreshingRef = useRef<boolean>(false);
|
|
43
|
+
|
|
44
|
+
// Sync ref with server props whenever page re-renders
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
lastKnownRef.current = {
|
|
47
|
+
updatedAt: initialUpdatedAt ? new Date(initialUpdatedAt).toISOString() : '',
|
|
48
|
+
statusCode: initialStatusCode || '',
|
|
49
|
+
commentCount: initialCommentCount,
|
|
50
|
+
assignedEngineerUserId: initialAssignedEngineerUserId || null,
|
|
51
|
+
};
|
|
52
|
+
isRefreshingRef.current = false;
|
|
53
|
+
}, [initialUpdatedAt, initialStatusCode, initialCommentCount, initialAssignedEngineerUserId]);
|
|
54
|
+
|
|
55
|
+
// Safe throttled refresh
|
|
56
|
+
const triggerRefresh = () => {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
if (now - lastRefreshTimestampRef.current < 1500) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
lastRefreshTimestampRef.current = now;
|
|
62
|
+
isRefreshingRef.current = true;
|
|
63
|
+
router.refresh();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// 1. Listen for local notification events dispatched from NotificationBell
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const handleNotification = (event: Event) => {
|
|
69
|
+
const customEvent = event as CustomEvent<any[]>;
|
|
70
|
+
const notifications = customEvent.detail;
|
|
71
|
+
if (!Array.isArray(notifications)) return;
|
|
72
|
+
|
|
73
|
+
const isForThisTicket = notifications.some(
|
|
74
|
+
(n) =>
|
|
75
|
+
(n.relatedEntityType === 'service_ticket' && n.relatedEntityId === ticketId) ||
|
|
76
|
+
(n.actionUrl && n.actionUrl.includes(`/tickets/${ticketId}`)) ||
|
|
77
|
+
(n.data && n.data.ticketId === ticketId)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (isForThisTicket) {
|
|
81
|
+
triggerRefresh();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
window.addEventListener('bemms:notification', handleNotification);
|
|
86
|
+
return () => {
|
|
87
|
+
window.removeEventListener('bemms:notification', handleNotification);
|
|
88
|
+
};
|
|
89
|
+
}, [ticketId]);
|
|
90
|
+
|
|
91
|
+
// 2. Resilient SSE direct listener
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
let eventSource: EventSource | null = null;
|
|
94
|
+
let reconnectTimeout: NodeJS.Timeout;
|
|
95
|
+
|
|
96
|
+
const connectSSE = () => {
|
|
97
|
+
try {
|
|
98
|
+
eventSource = new EventSource('/api/notifications/sse');
|
|
99
|
+
|
|
100
|
+
eventSource.onmessage = (event) => {
|
|
101
|
+
try {
|
|
102
|
+
const data = JSON.parse(event.data);
|
|
103
|
+
if (data.type === 'new_notifications' && Array.isArray(data.notifications)) {
|
|
104
|
+
const matched = data.notifications.some(
|
|
105
|
+
(n: any) =>
|
|
106
|
+
(n.relatedEntityType === 'service_ticket' && n.relatedEntityId === ticketId) ||
|
|
107
|
+
(n.actionUrl && n.actionUrl.includes(`/tickets/${ticketId}`))
|
|
108
|
+
);
|
|
109
|
+
if (matched) {
|
|
110
|
+
triggerRefresh();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// Ignore parse errors
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
eventSource.onerror = () => {
|
|
119
|
+
eventSource?.close();
|
|
120
|
+
reconnectTimeout = setTimeout(connectSSE, 20000);
|
|
121
|
+
};
|
|
122
|
+
} catch {
|
|
123
|
+
// Ignore SSE connection errors
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
connectSSE();
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
eventSource?.close();
|
|
131
|
+
clearTimeout(reconnectTimeout);
|
|
132
|
+
};
|
|
133
|
+
}, [ticketId]);
|
|
134
|
+
|
|
135
|
+
// 3. Fast live-sync polling (checks updatedAt, statusCode, commentCount)
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
let isCancelled = false;
|
|
138
|
+
|
|
139
|
+
const checkLiveSync = async () => {
|
|
140
|
+
// Don't poll if document is hidden or refresh is pending
|
|
141
|
+
if (document.hidden || isRefreshingRef.current) return;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const res = await fetch(`/api/tickets/${ticketId}/live-sync`, {
|
|
145
|
+
cache: 'no-store',
|
|
146
|
+
headers: { 'Cache-Control': 'no-cache' },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!res.ok || isCancelled) return;
|
|
150
|
+
|
|
151
|
+
const data = await res.json();
|
|
152
|
+
const current = lastKnownRef.current;
|
|
153
|
+
|
|
154
|
+
const hasChanged =
|
|
155
|
+
(data.updatedAt && current.updatedAt && data.updatedAt !== current.updatedAt) ||
|
|
156
|
+
(data.statusCode && current.statusCode && data.statusCode !== current.statusCode) ||
|
|
157
|
+
(typeof data.commentCount === 'number' && data.commentCount !== current.commentCount) ||
|
|
158
|
+
(data.assignedEngineerUserId !== undefined &&
|
|
159
|
+
data.assignedEngineerUserId !== current.assignedEngineerUserId);
|
|
160
|
+
|
|
161
|
+
if (hasChanged) {
|
|
162
|
+
lastKnownRef.current = {
|
|
163
|
+
updatedAt: data.updatedAt || current.updatedAt,
|
|
164
|
+
statusCode: data.statusCode || current.statusCode,
|
|
165
|
+
commentCount: data.commentCount ?? current.commentCount,
|
|
166
|
+
assignedEngineerUserId:
|
|
167
|
+
data.assignedEngineerUserId !== undefined
|
|
168
|
+
? data.assignedEngineerUserId
|
|
169
|
+
: current.assignedEngineerUserId,
|
|
170
|
+
};
|
|
171
|
+
triggerRefresh();
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// Silently handle polling network blips
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const intervalId = setInterval(checkLiveSync, pollIntervalMs);
|
|
179
|
+
|
|
180
|
+
// Refresh immediately when tab gains focus / becomes visible
|
|
181
|
+
const handleVisibilityChange = () => {
|
|
182
|
+
if (!document.hidden) {
|
|
183
|
+
checkLiveSync();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
188
|
+
|
|
189
|
+
return () => {
|
|
190
|
+
isCancelled = true;
|
|
191
|
+
clearInterval(intervalId);
|
|
192
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
193
|
+
};
|
|
194
|
+
}, [ticketId, pollIntervalMs]);
|
|
195
|
+
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
import { Badge } from "@/components/ui/badge"
|
|
3
|
+
|
|
4
|
+
const statusConfig: Record<string, { label: string; className: string }> = {
|
|
5
|
+
new: { label: "New", className: "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-950 dark:text-blue-300 dark:border-blue-800" },
|
|
6
|
+
acknowledged: { label: "Acknowledged", className: "bg-sky-100 text-sky-800 border-sky-200 dark:bg-sky-950 dark:text-sky-300 dark:border-sky-800" },
|
|
7
|
+
in_triage: { label: "Under Assessment", className: "bg-indigo-100 text-indigo-800 border-indigo-200 dark:bg-indigo-950 dark:text-indigo-300 dark:border-indigo-800" },
|
|
8
|
+
in_progress: { label: "In Progress", className: "bg-violet-100 text-violet-800 border-violet-200 dark:bg-violet-950 dark:text-violet-300 dark:border-violet-800" },
|
|
9
|
+
waiting_requester: { label: "Info Needed", className: "bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-800" },
|
|
10
|
+
waiting_parts_vendor: { label: "Waiting Parts", className: "bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-950 dark:text-yellow-300 dark:border-yellow-800" },
|
|
11
|
+
resolved: { label: "Resolved", className: "bg-green-100 text-green-800 border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-800" },
|
|
12
|
+
closed: { label: "Closed", className: "bg-slate-100 text-slate-600 border-slate-200 dark:bg-slate-950 dark:text-slate-300 dark:border-slate-800" },
|
|
13
|
+
cancelled: { label: "Cancelled", className: "bg-gray-100 text-gray-500 border-gray-200 line-through dark:bg-gray-950 dark:text-gray-400 dark:border-gray-800" },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const priorityConfig: Record<string, { label: string; className: string }> = {
|
|
17
|
+
p1_critical: { label: "P1 Critical", className: "bg-red-100 text-red-800 border-red-300 font-bold dark:bg-red-950 dark:text-red-300 dark:border-red-800" },
|
|
18
|
+
p2_high: { label: "P2 High", className: "bg-orange-100 text-orange-800 border-orange-200 dark:bg-orange-950 dark:text-orange-300 dark:border-orange-800" },
|
|
19
|
+
p3_normal: { label: "P3 Normal", className: "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950 dark:text-blue-300 dark:border-blue-800" },
|
|
20
|
+
p4_low: { label: "P4 Low", className: "bg-gray-50 text-gray-600 border-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-700" },
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const impactConfig: Record<string, { label: string; className: string }> = {
|
|
24
|
+
device_usable: { label: "Device Usable", className: "bg-green-50 text-green-700 border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-800" },
|
|
25
|
+
device_not_usable: { label: "Device Not Usable", className: "bg-orange-50 text-orange-700 border-orange-200 dark:bg-orange-950 dark:text-orange-300 dark:border-orange-800" },
|
|
26
|
+
patient_care_affected: { label: "Patient Care Affected", className: "bg-red-50 text-red-800 border-red-300 font-bold dark:bg-red-950 dark:text-red-300 dark:border-red-800" },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function TicketStatusBadge({ status }: { status?: string | null }) {
|
|
30
|
+
const config = statusConfig[status || ''] || { label: status || 'Unknown', className: 'bg-muted text-muted-foreground' }
|
|
31
|
+
return <Badge variant="outline" className={cn("font-medium", config.className)}>{config.label}</Badge>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function TicketPriorityBadge({ priority }: { priority?: string | null }) {
|
|
35
|
+
const config = priorityConfig[priority || ''] || { label: priority || 'N/A', className: 'bg-muted text-muted-foreground' }
|
|
36
|
+
return <Badge variant="outline" className={cn("font-medium text-xs", config.className)}>{config.label}</Badge>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function TicketImpactBadge({ impact }: { impact?: string | null }) {
|
|
40
|
+
const config = impactConfig[impact || ''] || { label: impact || 'N/A', className: 'bg-muted text-muted-foreground' }
|
|
41
|
+
return <Badge variant="outline" className={cn("font-medium text-xs", config.className)}>{config.label}</Badge>
|
|
42
|
+
}
|
|
43
|
+
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Link from "next/link"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
import { TicketPriorityBadge, TicketStatusBadge } from "./ticket-badges"
|
|
6
|
+
import { WaitingForEngineerBanner } from "./ticket-dispatch-status"
|
|
7
|
+
import { Monitor, MapPin, User, Calendar, Package } from "lucide-react"
|
|
8
|
+
|
|
9
|
+
export interface TicketItem {
|
|
10
|
+
id: string
|
|
11
|
+
ticketNumber: string
|
|
12
|
+
title: string
|
|
13
|
+
description?: string | null
|
|
14
|
+
statusCode: string
|
|
15
|
+
priorityCode: string
|
|
16
|
+
reportedImpact?: string | null
|
|
17
|
+
waitingReason?: string | null
|
|
18
|
+
waitingDependencyType?: string | null
|
|
19
|
+
reportedAt?: Date | string | null
|
|
20
|
+
createdAt?: Date | string | null
|
|
21
|
+
device?: {
|
|
22
|
+
id?: string
|
|
23
|
+
name: string
|
|
24
|
+
assetNumber?: string | null
|
|
25
|
+
currentStatusCode?: string | null
|
|
26
|
+
} | null
|
|
27
|
+
hospital?: {
|
|
28
|
+
id?: string
|
|
29
|
+
name: string
|
|
30
|
+
} | null
|
|
31
|
+
department?: {
|
|
32
|
+
id?: string
|
|
33
|
+
name: string
|
|
34
|
+
} | null
|
|
35
|
+
reportedByUser?: {
|
|
36
|
+
id?: string
|
|
37
|
+
fullName?: string | null
|
|
38
|
+
} | null
|
|
39
|
+
assignedEngineer?: {
|
|
40
|
+
id?: string
|
|
41
|
+
fullName?: string | null
|
|
42
|
+
} | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getTicketStatusStyle(statusCode: string) {
|
|
46
|
+
switch (statusCode) {
|
|
47
|
+
case "new":
|
|
48
|
+
case "acknowledged":
|
|
49
|
+
case "in_triage":
|
|
50
|
+
return {
|
|
51
|
+
rail: "border-l-rose-500",
|
|
52
|
+
bg: "bg-rose-50/35 dark:bg-rose-950/20 hover:bg-rose-50/70 dark:hover:bg-rose-950/30",
|
|
53
|
+
border: "border-rose-200/80 dark:border-rose-900/40",
|
|
54
|
+
label: "Attention Needed",
|
|
55
|
+
dot: "bg-rose-500",
|
|
56
|
+
}
|
|
57
|
+
case "in_progress":
|
|
58
|
+
return {
|
|
59
|
+
rail: "border-l-amber-500",
|
|
60
|
+
bg: "bg-amber-50/35 dark:bg-amber-950/20 hover:bg-amber-50/70 dark:hover:bg-amber-950/30",
|
|
61
|
+
border: "border-amber-200/80 dark:border-amber-900/40",
|
|
62
|
+
label: "In Progress",
|
|
63
|
+
dot: "bg-amber-500",
|
|
64
|
+
}
|
|
65
|
+
case "waiting_parts_vendor":
|
|
66
|
+
case "waiting_requester":
|
|
67
|
+
return {
|
|
68
|
+
rail: "border-l-orange-500",
|
|
69
|
+
bg: "bg-orange-50/35 dark:bg-orange-950/20 hover:bg-orange-50/70 dark:hover:bg-orange-950/30",
|
|
70
|
+
border: "border-orange-200/80 dark:border-orange-900/40",
|
|
71
|
+
label: "Waiting for Parts",
|
|
72
|
+
dot: "bg-orange-500",
|
|
73
|
+
}
|
|
74
|
+
case "resolved":
|
|
75
|
+
return {
|
|
76
|
+
rail: "border-l-emerald-500",
|
|
77
|
+
bg: "bg-emerald-50/35 dark:bg-emerald-950/20 hover:bg-emerald-50/70 dark:hover:bg-emerald-950/30",
|
|
78
|
+
border: "border-emerald-200/80 dark:border-emerald-900/40",
|
|
79
|
+
label: "Resolved",
|
|
80
|
+
dot: "bg-emerald-500",
|
|
81
|
+
}
|
|
82
|
+
case "closed":
|
|
83
|
+
return {
|
|
84
|
+
rail: "border-l-slate-400 dark:border-l-slate-600",
|
|
85
|
+
bg: "bg-slate-50/50 dark:bg-slate-900/20 hover:bg-slate-100/50 dark:hover:bg-slate-900/40",
|
|
86
|
+
border: "border-slate-200/80 dark:border-slate-800/40",
|
|
87
|
+
label: "Closed",
|
|
88
|
+
dot: "bg-slate-400",
|
|
89
|
+
}
|
|
90
|
+
case "cancelled":
|
|
91
|
+
default:
|
|
92
|
+
return {
|
|
93
|
+
rail: "border-l-zinc-300 dark:border-l-zinc-700",
|
|
94
|
+
bg: "bg-zinc-50/30 dark:bg-zinc-900/10 hover:bg-zinc-100/40 dark:hover:bg-zinc-900/30",
|
|
95
|
+
border: "border-zinc-200/80 dark:border-zinc-800/40",
|
|
96
|
+
label: "Cancelled",
|
|
97
|
+
dot: "bg-zinc-400",
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function TicketCard({ ticket }: { ticket: TicketItem }) {
|
|
103
|
+
const style = getTicketStatusStyle(ticket.statusCode)
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Link
|
|
107
|
+
href={`/tickets/${ticket.id}`}
|
|
108
|
+
className={cn(
|
|
109
|
+
"group relative flex flex-col justify-between rounded-lg border border-l-4 transition-all duration-200 p-4 shadow-sm hover:shadow-md",
|
|
110
|
+
style.rail,
|
|
111
|
+
style.bg,
|
|
112
|
+
style.border
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
<div>
|
|
116
|
+
{/* Top Header: Ticket Number & Badges */}
|
|
117
|
+
<div className="flex items-center justify-between gap-2 mb-2">
|
|
118
|
+
<span className="font-mono text-xs font-bold text-primary group-hover:underline">
|
|
119
|
+
{ticket.ticketNumber}
|
|
120
|
+
</span>
|
|
121
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
122
|
+
<TicketPriorityBadge priority={ticket.priorityCode} />
|
|
123
|
+
<TicketStatusBadge status={ticket.statusCode} />
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Title */}
|
|
128
|
+
<h3 className="font-semibold text-base text-foreground group-hover:text-primary transition-colors line-clamp-1 mb-2.5">
|
|
129
|
+
{ticket.title}
|
|
130
|
+
</h3>
|
|
131
|
+
|
|
132
|
+
{/* Device & Location Meta */}
|
|
133
|
+
<div className="space-y-1.5 text-xs text-muted-foreground mb-3">
|
|
134
|
+
<div className="flex items-center gap-1.5 truncate">
|
|
135
|
+
<Monitor className="w-3.5 h-3.5 shrink-0 text-slate-500" />
|
|
136
|
+
<span className="font-medium text-foreground truncate">
|
|
137
|
+
{ticket.device?.name || "No linked device"}
|
|
138
|
+
</span>
|
|
139
|
+
{ticket.device?.assetNumber && (
|
|
140
|
+
<span className="font-mono text-[11px] text-muted-foreground shrink-0">
|
|
141
|
+
({ticket.device.assetNumber})
|
|
142
|
+
</span>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{(ticket.hospital?.name || ticket.department?.name) && (
|
|
147
|
+
<div className="flex items-center gap-1.5 truncate">
|
|
148
|
+
<MapPin className="w-3.5 h-3.5 shrink-0 text-slate-500" />
|
|
149
|
+
<span className="truncate">
|
|
150
|
+
{ticket.hospital?.name ? `${ticket.hospital.name} / ` : ""}
|
|
151
|
+
{ticket.department?.name || "Unassigned Dept"}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Waiting for Spare Parts Pill */}
|
|
158
|
+
{ticket.statusCode === "waiting_parts_vendor" && ticket.waitingReason && (
|
|
159
|
+
<div className="mb-3 rounded bg-orange-100/80 dark:bg-orange-950/60 px-2.5 py-1.5 text-xs text-orange-950 dark:text-orange-200 border border-orange-200 dark:border-orange-800/60 flex items-center gap-1.5">
|
|
160
|
+
<Package className="w-3.5 h-3.5 shrink-0 text-orange-600 dark:text-orange-400" />
|
|
161
|
+
<span className="truncate font-medium">On Order: {ticket.waitingReason}</span>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* Footer: Assignee & Date */}
|
|
167
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground pt-3 border-t border-border/50 mt-1">
|
|
168
|
+
<div className="flex items-center gap-1.5 truncate">
|
|
169
|
+
<User className="w-3.5 h-3.5 shrink-0 text-slate-400" />
|
|
170
|
+
<span className="truncate">
|
|
171
|
+
{ticket.assignedEngineer?.fullName ? (
|
|
172
|
+
<span className="text-foreground font-medium">
|
|
173
|
+
{ticket.assignedEngineer.fullName}
|
|
174
|
+
</span>
|
|
175
|
+
) : ["new", "acknowledged", "in_triage"].includes(ticket.statusCode) ? (
|
|
176
|
+
<WaitingForEngineerBanner
|
|
177
|
+
compact
|
|
178
|
+
reportedAt={ticket.reportedAt || ticket.createdAt || new Date()}
|
|
179
|
+
reportedImpact={ticket.reportedImpact || undefined}
|
|
180
|
+
/>
|
|
181
|
+
) : (
|
|
182
|
+
<span className="text-rose-600 dark:text-rose-400 font-semibold">
|
|
183
|
+
Unassigned
|
|
184
|
+
</span>
|
|
185
|
+
)}
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
<div className="flex items-center gap-1 text-[11px] shrink-0">
|
|
189
|
+
<Calendar className="w-3 h-3 text-slate-400" />
|
|
190
|
+
<span>
|
|
191
|
+
{new Date(ticket.reportedAt || ticket.createdAt || Date.now()).toLocaleDateString()}
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</Link>
|
|
196
|
+
)
|
|
197
|
+
}
|