@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,405 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { getStaffStatus, getStaffStatusStyle } from '@/lib/utils/staff-status';
|
|
6
|
+
import {
|
|
7
|
+
CheckCircle2,
|
|
8
|
+
Clock,
|
|
9
|
+
Wrench,
|
|
10
|
+
Package,
|
|
11
|
+
MessageCircle,
|
|
12
|
+
CheckCircle,
|
|
13
|
+
AlertTriangle,
|
|
14
|
+
User,
|
|
15
|
+
Phone,
|
|
16
|
+
Mail,
|
|
17
|
+
ShieldCheck,
|
|
18
|
+
Send,
|
|
19
|
+
} from 'lucide-react';
|
|
20
|
+
import { Button } from '@/components/ui/button';
|
|
21
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
22
|
+
import { useState } from 'react';
|
|
23
|
+
import { addTicketComment } from '@/lib/actions/tickets';
|
|
24
|
+
|
|
25
|
+
interface StaffTicketProgressTrackerProps {
|
|
26
|
+
ticket: {
|
|
27
|
+
id: string;
|
|
28
|
+
ticketNumber: string;
|
|
29
|
+
statusCode: string;
|
|
30
|
+
reportedAt?: Date | string | null;
|
|
31
|
+
createdAt?: Date | string | null;
|
|
32
|
+
resolvedAt?: Date | string | null;
|
|
33
|
+
waitingReason?: string | null;
|
|
34
|
+
waitingDependencyType?: string | null;
|
|
35
|
+
assignedEngineerUserId?: string | null;
|
|
36
|
+
assignedEngineer?: {
|
|
37
|
+
fullName?: string | null;
|
|
38
|
+
email?: string | null;
|
|
39
|
+
phone?: string | null;
|
|
40
|
+
} | null;
|
|
41
|
+
finalDeviceStatusCode?: string | null;
|
|
42
|
+
resolutionSummary?: string | null;
|
|
43
|
+
};
|
|
44
|
+
engineerContact?: {
|
|
45
|
+
mobileNumber?: string | null;
|
|
46
|
+
extension?: string | null;
|
|
47
|
+
dutyStatus?: string | null;
|
|
48
|
+
} | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface Step {
|
|
52
|
+
id: string;
|
|
53
|
+
label: string;
|
|
54
|
+
sublabel: string;
|
|
55
|
+
status: 'complete' | 'current' | 'upcoming' | 'alert';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function StaffTicketProgressTracker({
|
|
59
|
+
ticket,
|
|
60
|
+
engineerContact,
|
|
61
|
+
}: StaffTicketProgressTrackerProps) {
|
|
62
|
+
const [replyText, setReplyText] = useState('');
|
|
63
|
+
const [submittingReply, setSubmittingReply] = useState(false);
|
|
64
|
+
const [replySuccess, setReplySuccess] = useState(false);
|
|
65
|
+
|
|
66
|
+
const statusInfo = getStaffStatus(ticket.statusCode);
|
|
67
|
+
const statusStyle = getStaffStatusStyle(ticket.statusCode);
|
|
68
|
+
|
|
69
|
+
const isResolved = ['resolved', 'closed'].includes(ticket.statusCode);
|
|
70
|
+
const isWaitingParts = ticket.statusCode === 'waiting_parts_vendor';
|
|
71
|
+
const isWaitingResponse = ticket.statusCode === 'waiting_requester';
|
|
72
|
+
|
|
73
|
+
// Compute 5 linear steps for clinical staff
|
|
74
|
+
const steps: Step[] = useMemo(() => {
|
|
75
|
+
// Stage 1: Submitted
|
|
76
|
+
// Stage 2: Reviewed
|
|
77
|
+
// Stage 3: Assigned to Engineer
|
|
78
|
+
// Stage 4: In Repair / Hold
|
|
79
|
+
// Stage 5: Resolved
|
|
80
|
+
|
|
81
|
+
const code = ticket.statusCode;
|
|
82
|
+
|
|
83
|
+
let s1: Step['status'] = 'complete';
|
|
84
|
+
let s2: Step['status'] = 'upcoming';
|
|
85
|
+
let s3: Step['status'] = 'upcoming';
|
|
86
|
+
let s4: Step['status'] = 'upcoming';
|
|
87
|
+
let s5: Step['status'] = 'upcoming';
|
|
88
|
+
|
|
89
|
+
if (code === 'new') {
|
|
90
|
+
s1 = 'complete';
|
|
91
|
+
s2 = 'current';
|
|
92
|
+
} else if (code === 'acknowledged' || code === 'in_triage') {
|
|
93
|
+
s1 = 'complete';
|
|
94
|
+
s2 = 'complete';
|
|
95
|
+
s3 = 'current';
|
|
96
|
+
} else if (code === 'assigned') {
|
|
97
|
+
s1 = 'complete';
|
|
98
|
+
s2 = 'complete';
|
|
99
|
+
s3 = 'complete';
|
|
100
|
+
s4 = 'current';
|
|
101
|
+
} else if (code === 'in_progress') {
|
|
102
|
+
s1 = 'complete';
|
|
103
|
+
s2 = 'complete';
|
|
104
|
+
s3 = 'complete';
|
|
105
|
+
s4 = 'current';
|
|
106
|
+
} else if (code === 'waiting_parts_vendor') {
|
|
107
|
+
s1 = 'complete';
|
|
108
|
+
s2 = 'complete';
|
|
109
|
+
s3 = 'complete';
|
|
110
|
+
s4 = 'alert';
|
|
111
|
+
} else if (code === 'waiting_requester') {
|
|
112
|
+
s1 = 'complete';
|
|
113
|
+
s2 = 'complete';
|
|
114
|
+
s3 = 'complete';
|
|
115
|
+
s4 = 'alert';
|
|
116
|
+
} else if (isResolved) {
|
|
117
|
+
s1 = 'complete';
|
|
118
|
+
s2 = 'complete';
|
|
119
|
+
s3 = 'complete';
|
|
120
|
+
s4 = 'complete';
|
|
121
|
+
s5 = 'complete';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
id: '1',
|
|
127
|
+
label: '1. Submitted',
|
|
128
|
+
sublabel: ticket.reportedAt
|
|
129
|
+
? new Date(ticket.reportedAt).toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
130
|
+
: 'Logged',
|
|
131
|
+
status: s1,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: '2',
|
|
135
|
+
label: '2. Reviewed',
|
|
136
|
+
sublabel: 'Biomedical Triage',
|
|
137
|
+
status: s2,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: '3',
|
|
141
|
+
label: '3. Assigned',
|
|
142
|
+
sublabel: ticket.assignedEngineer?.fullName
|
|
143
|
+
? ticket.assignedEngineer.fullName.split(' ')[0]
|
|
144
|
+
: 'Pending engineer',
|
|
145
|
+
status: s3,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: '4',
|
|
149
|
+
label: isWaitingParts
|
|
150
|
+
? '4. Parts On Order'
|
|
151
|
+
: isWaitingResponse
|
|
152
|
+
? '4. Needs Reply'
|
|
153
|
+
: '4. Being Repaired',
|
|
154
|
+
sublabel: isWaitingParts
|
|
155
|
+
? 'Waiting parts'
|
|
156
|
+
: isWaitingResponse
|
|
157
|
+
? 'Doctor reply needed'
|
|
158
|
+
: 'Active repair',
|
|
159
|
+
status: s4,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: '5',
|
|
163
|
+
label: '5. Resolved',
|
|
164
|
+
sublabel: isResolved ? 'Ready for clinical use' : 'Pending testing',
|
|
165
|
+
status: s5,
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
}, [ticket.statusCode, ticket.reportedAt, ticket.assignedEngineer, isWaitingParts, isWaitingResponse, isResolved]);
|
|
169
|
+
|
|
170
|
+
const handleSendReply = async () => {
|
|
171
|
+
if (!replyText.trim()) return;
|
|
172
|
+
setSubmittingReply(true);
|
|
173
|
+
const res = await addTicketComment(ticket.id, {
|
|
174
|
+
body: replyText.trim(),
|
|
175
|
+
visibility: 'public',
|
|
176
|
+
});
|
|
177
|
+
if (res?.success) {
|
|
178
|
+
setReplySuccess(true);
|
|
179
|
+
setReplyText('');
|
|
180
|
+
}
|
|
181
|
+
setSubmittingReply(false);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div className="space-y-4">
|
|
186
|
+
{/* ── Visual Stepper Bar ─────────────────────────────────────────── */}
|
|
187
|
+
<div className="bg-card rounded-2xl border p-4 sm:p-5 shadow-sm space-y-4">
|
|
188
|
+
<div className="flex items-center justify-between flex-wrap gap-2 pb-3 border-b border-border/60">
|
|
189
|
+
<div>
|
|
190
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
191
|
+
Repair Progress Tracker
|
|
192
|
+
</span>
|
|
193
|
+
<h3 className="text-base font-bold text-foreground flex items-center gap-2 mt-0.5">
|
|
194
|
+
<span>{statusInfo.icon}</span>
|
|
195
|
+
<span>{statusInfo.label}</span>
|
|
196
|
+
</h3>
|
|
197
|
+
</div>
|
|
198
|
+
<span
|
|
199
|
+
className={cn(
|
|
200
|
+
'text-xs px-2.5 py-1 rounded-full font-medium border',
|
|
201
|
+
statusStyle.bg,
|
|
202
|
+
statusStyle.text,
|
|
203
|
+
statusStyle.border
|
|
204
|
+
)}
|
|
205
|
+
>
|
|
206
|
+
{statusInfo.description || statusInfo.label}
|
|
207
|
+
</span>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{/* Stepper Dots & Line */}
|
|
211
|
+
<div className="grid grid-cols-5 gap-1 pt-1">
|
|
212
|
+
{steps.map((step, idx) => {
|
|
213
|
+
const isCompleted = step.status === 'complete';
|
|
214
|
+
const isCurrent = step.status === 'current';
|
|
215
|
+
const isAlert = step.status === 'alert';
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div key={step.id} className="flex flex-col items-center text-center relative group">
|
|
219
|
+
{/* Connecting Line between steps */}
|
|
220
|
+
{idx < steps.length - 1 && (
|
|
221
|
+
<div
|
|
222
|
+
className={cn(
|
|
223
|
+
'hidden sm:block absolute top-3.5 left-1/2 w-full h-0.5 -z-0',
|
|
224
|
+
isCompleted ? 'bg-primary' : 'bg-muted'
|
|
225
|
+
)}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
{/* Step Circle */}
|
|
230
|
+
<div
|
|
231
|
+
className={cn(
|
|
232
|
+
'w-7 h-7 sm:w-8 sm:h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all relative z-10',
|
|
233
|
+
isCompleted && 'bg-primary text-primary-foreground shadow-sm',
|
|
234
|
+
isCurrent && 'bg-primary/20 text-primary border-2 border-primary ring-4 ring-primary/10 animate-pulse',
|
|
235
|
+
isAlert && 'bg-amber-500 text-white ring-4 ring-amber-500/20 shadow-sm animate-bounce',
|
|
236
|
+
step.status === 'upcoming' && 'bg-muted text-muted-foreground border'
|
|
237
|
+
)}
|
|
238
|
+
>
|
|
239
|
+
{isCompleted ? (
|
|
240
|
+
<CheckCircle2 className="w-4 h-4" />
|
|
241
|
+
) : isAlert ? (
|
|
242
|
+
'!'
|
|
243
|
+
) : (
|
|
244
|
+
<span>{step.id}</span>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{/* Step Label */}
|
|
249
|
+
<div className="mt-2 min-w-0 px-0.5">
|
|
250
|
+
<p
|
|
251
|
+
className={cn(
|
|
252
|
+
'text-[11px] sm:text-xs font-semibold truncate leading-tight',
|
|
253
|
+
(isCurrent || isAlert) && 'text-primary font-bold',
|
|
254
|
+
isCompleted && 'text-foreground',
|
|
255
|
+
step.status === 'upcoming' && 'text-muted-foreground'
|
|
256
|
+
)}
|
|
257
|
+
>
|
|
258
|
+
{step.label}
|
|
259
|
+
</p>
|
|
260
|
+
<p className="text-[10px] text-muted-foreground truncate hidden sm:block mt-0.5">
|
|
261
|
+
{step.sublabel}
|
|
262
|
+
</p>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
})}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
{/* ── Status-Specific Clinical Guidance Cards ────────────────────── */}
|
|
271
|
+
|
|
272
|
+
{/* 1. If Waiting for Response from Staff */}
|
|
273
|
+
{isWaitingResponse && (
|
|
274
|
+
<div className="bg-amber-50 dark:bg-amber-950/40 border-2 border-amber-300 dark:border-amber-700 rounded-2xl p-4 sm:p-5 shadow-sm space-y-3">
|
|
275
|
+
<div className="flex items-start gap-3">
|
|
276
|
+
<div className="p-2 bg-amber-500/10 text-amber-700 dark:text-amber-300 rounded-xl">
|
|
277
|
+
<MessageCircle className="w-5 h-5" />
|
|
278
|
+
</div>
|
|
279
|
+
<div>
|
|
280
|
+
<h4 className="font-bold text-sm text-amber-900 dark:text-amber-100">
|
|
281
|
+
Biomedical Engineering has asked a question
|
|
282
|
+
</h4>
|
|
283
|
+
<p className="text-xs text-amber-800 dark:text-amber-200 mt-0.5 leading-relaxed">
|
|
284
|
+
The technician needs more details from the ward staff to proceed with repairing this device. Please provide the requested answer below.
|
|
285
|
+
</p>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
{replySuccess ? (
|
|
290
|
+
<div className="p-3 bg-green-100 dark:bg-green-950 text-green-800 dark:text-green-200 rounded-xl text-xs font-medium flex items-center gap-2">
|
|
291
|
+
<CheckCircle className="w-4 h-4 text-green-600" />
|
|
292
|
+
Thank you! Your reply was sent directly to the biomedical team.
|
|
293
|
+
</div>
|
|
294
|
+
) : (
|
|
295
|
+
<div className="space-y-2 pt-1">
|
|
296
|
+
<Textarea
|
|
297
|
+
placeholder="Type your response to the engineer here (e.g. 'Yes, the alarm was sounding every 30 seconds from pump channel 2')..."
|
|
298
|
+
value={replyText}
|
|
299
|
+
onChange={(e) => setReplyText(e.target.value)}
|
|
300
|
+
rows={2}
|
|
301
|
+
className="bg-card text-xs sm:text-sm"
|
|
302
|
+
/>
|
|
303
|
+
<div className="flex justify-end">
|
|
304
|
+
<Button
|
|
305
|
+
size="sm"
|
|
306
|
+
onClick={handleSendReply}
|
|
307
|
+
disabled={submittingReply || !replyText.trim()}
|
|
308
|
+
className="bg-amber-600 hover:bg-amber-700 text-white text-xs gap-1.5"
|
|
309
|
+
>
|
|
310
|
+
<Send className="w-3.5 h-3.5" />
|
|
311
|
+
{submittingReply ? 'Sending...' : 'Send Answer to Engineer'}
|
|
312
|
+
</Button>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{/* 2. If Waiting for Spare Parts */}
|
|
320
|
+
{isWaitingParts && (
|
|
321
|
+
<div className="bg-amber-50/60 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800 rounded-2xl p-4 shadow-sm flex items-start gap-3">
|
|
322
|
+
<div className="p-2 bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300 rounded-xl">
|
|
323
|
+
<Package className="w-5 h-5" />
|
|
324
|
+
</div>
|
|
325
|
+
<div className="space-y-1">
|
|
326
|
+
<h4 className="font-bold text-sm text-amber-950 dark:text-amber-100">
|
|
327
|
+
Spare Parts On Order
|
|
328
|
+
</h4>
|
|
329
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
330
|
+
Required component: <strong className="text-foreground">{ticket.waitingReason || 'Replacement part ordered from vendor'}</strong>.
|
|
331
|
+
Repair will resume as soon as the package arrives at the hospital biomedical workshop.
|
|
332
|
+
</p>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
|
|
337
|
+
{/* 3. If Assigned to Engineer */}
|
|
338
|
+
{ticket.assignedEngineer?.fullName && (
|
|
339
|
+
<div className="bg-blue-50/50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900/60 rounded-2xl p-4 shadow-sm flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
340
|
+
<div className="flex items-center gap-3">
|
|
341
|
+
<div className="w-11 h-11 rounded-full bg-blue-100 dark:bg-blue-900/80 text-blue-700 dark:text-blue-300 font-bold text-sm flex items-center justify-center shrink-0">
|
|
342
|
+
{ticket.assignedEngineer.fullName.substring(0, 2).toUpperCase()}
|
|
343
|
+
</div>
|
|
344
|
+
<div>
|
|
345
|
+
<div className="flex items-center gap-2">
|
|
346
|
+
<h4 className="font-bold text-sm text-foreground">
|
|
347
|
+
{ticket.assignedEngineer.fullName}
|
|
348
|
+
</h4>
|
|
349
|
+
<span className="text-[10px] bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-1.5 py-0.2 rounded font-medium">
|
|
350
|
+
Assigned Engineer
|
|
351
|
+
</span>
|
|
352
|
+
</div>
|
|
353
|
+
<p className="text-xs text-muted-foreground">
|
|
354
|
+
Biomedical Engineering · Handling your device repair
|
|
355
|
+
</p>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
{/* Quick Call & Email actions */}
|
|
360
|
+
<div className="flex items-center gap-2 pt-2 sm:pt-0 border-t sm:border-t-0 border-border/40">
|
|
361
|
+
{(engineerContact?.extension || engineerContact?.mobileNumber || ticket.assignedEngineer.phone) && (
|
|
362
|
+
<Button asChild variant="outline" size="sm" className="h-8 text-xs gap-1.5 bg-card">
|
|
363
|
+
<a
|
|
364
|
+
href={`tel:${
|
|
365
|
+
engineerContact?.mobileNumber ||
|
|
366
|
+
engineerContact?.extension ||
|
|
367
|
+
ticket.assignedEngineer.phone
|
|
368
|
+
}`}
|
|
369
|
+
>
|
|
370
|
+
<Phone className="w-3.5 h-3.5 text-green-600" />
|
|
371
|
+
<span>Call {engineerContact?.extension ? `Ext. ${engineerContact.extension}` : 'Engineer'}</span>
|
|
372
|
+
</a>
|
|
373
|
+
</Button>
|
|
374
|
+
)}
|
|
375
|
+
{ticket.assignedEngineer.email && (
|
|
376
|
+
<Button asChild variant="outline" size="sm" className="h-8 text-xs gap-1.5 bg-card">
|
|
377
|
+
<a href={`mailto:${ticket.assignedEngineer.email}`}>
|
|
378
|
+
<Mail className="w-3.5 h-3.5 text-blue-600" />
|
|
379
|
+
<span>Email</span>
|
|
380
|
+
</a>
|
|
381
|
+
</Button>
|
|
382
|
+
)}
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
)}
|
|
386
|
+
|
|
387
|
+
{/* 4. If Resolved */}
|
|
388
|
+
{isResolved && (
|
|
389
|
+
<div className="bg-green-50/70 dark:bg-green-950/30 border border-green-200 dark:border-green-800 rounded-2xl p-4 shadow-sm flex items-start gap-3">
|
|
390
|
+
<div className="p-2 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded-xl">
|
|
391
|
+
<ShieldCheck className="w-5 h-5" />
|
|
392
|
+
</div>
|
|
393
|
+
<div>
|
|
394
|
+
<h4 className="font-bold text-sm text-green-950 dark:text-green-100">
|
|
395
|
+
Equipment Tested & Cleared for Clinical Use
|
|
396
|
+
</h4>
|
|
397
|
+
<p className="text-xs text-green-800 dark:text-green-300 mt-0.5 leading-relaxed">
|
|
398
|
+
Biomedical repair work is complete. The equipment has passed functional safety testing and is safe for bedside deployment.
|
|
399
|
+
</p>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { PageHeader } from '@/components/shared/page-header';
|
|
6
|
+
import { StaffTicketCard, StaffTicketItem } from '@/components/tickets/staff-ticket-card';
|
|
7
|
+
import { Button } from '@/components/ui/button';
|
|
8
|
+
import { Input } from '@/components/ui/input';
|
|
9
|
+
import { PlusCircle, Search, TicketCheck, MessageCircle, CheckCircle, Wrench } from 'lucide-react';
|
|
10
|
+
import { cn } from '@/lib/utils';
|
|
11
|
+
|
|
12
|
+
interface StaffTicketsViewProps {
|
|
13
|
+
tickets: StaffTicketItem[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type TicketFilter = 'all' | 'active' | 'needs_response' | 'resolved';
|
|
17
|
+
|
|
18
|
+
export function StaffTicketsView({ tickets }: StaffTicketsViewProps) {
|
|
19
|
+
const [filter, setFilter] = useState<TicketFilter>('all');
|
|
20
|
+
const [search, setSearch] = useState('');
|
|
21
|
+
|
|
22
|
+
const counts = useMemo(() => {
|
|
23
|
+
const total = tickets.length;
|
|
24
|
+
const needsResponse = tickets.filter((t) => t.statusCode === 'waiting_requester').length;
|
|
25
|
+
const resolved = tickets.filter((t) => ['resolved', 'closed'].includes(t.statusCode)).length;
|
|
26
|
+
const active = total - resolved;
|
|
27
|
+
return { total, active, needsResponse, resolved };
|
|
28
|
+
}, [tickets]);
|
|
29
|
+
|
|
30
|
+
const filteredTickets = useMemo(() => {
|
|
31
|
+
let result = tickets;
|
|
32
|
+
|
|
33
|
+
// Filter by tab
|
|
34
|
+
if (filter === 'active') {
|
|
35
|
+
result = result.filter((t) => !['resolved', 'closed', 'cancelled'].includes(t.statusCode));
|
|
36
|
+
} else if (filter === 'needs_response') {
|
|
37
|
+
result = result.filter((t) => t.statusCode === 'waiting_requester');
|
|
38
|
+
} else if (filter === 'resolved') {
|
|
39
|
+
result = result.filter((t) => ['resolved', 'closed'].includes(t.statusCode));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Filter by search query
|
|
43
|
+
if (search.trim()) {
|
|
44
|
+
const q = search.toLowerCase();
|
|
45
|
+
result = result.filter(
|
|
46
|
+
(t) =>
|
|
47
|
+
t.ticketNumber.toLowerCase().includes(q) ||
|
|
48
|
+
t.title.toLowerCase().includes(q) ||
|
|
49
|
+
(t.deviceName && t.deviceName.toLowerCase().includes(q)) ||
|
|
50
|
+
(t.assignedEngineerName && t.assignedEngineerName.toLowerCase().includes(q))
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return result;
|
|
55
|
+
}, [tickets, filter, search]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="space-y-6 max-w-3xl mx-auto pb-10">
|
|
59
|
+
<PageHeader
|
|
60
|
+
title="My Tickets"
|
|
61
|
+
description="Monitor biomedical repair progress and communicate with engineers"
|
|
62
|
+
>
|
|
63
|
+
<Link href="/tickets/create">
|
|
64
|
+
<Button size="lg" className="min-h-[44px]">
|
|
65
|
+
<PlusCircle className="mr-2 h-5 w-5" />
|
|
66
|
+
Report Problem
|
|
67
|
+
</Button>
|
|
68
|
+
</Link>
|
|
69
|
+
</PageHeader>
|
|
70
|
+
|
|
71
|
+
{/* Action banner if any tickets need response */}
|
|
72
|
+
{counts.needsResponse > 0 && filter !== 'needs_response' && (
|
|
73
|
+
<div className="bg-amber-50 dark:bg-amber-950/40 border border-amber-300 dark:border-amber-800 rounded-xl p-3.5 flex items-center justify-between gap-3 text-xs">
|
|
74
|
+
<div className="flex items-center gap-2 text-amber-900 dark:text-amber-200">
|
|
75
|
+
<MessageCircle className="w-4 h-4 text-amber-600 shrink-0" />
|
|
76
|
+
<span>
|
|
77
|
+
<strong>{counts.needsResponse} ticket(s)</strong> require your response so engineering can proceed.
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
<button
|
|
81
|
+
onClick={() => setFilter('needs_response')}
|
|
82
|
+
className="text-primary font-semibold hover:underline shrink-0"
|
|
83
|
+
>
|
|
84
|
+
Show Tickets →
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{/* Search Input */}
|
|
90
|
+
<div className="relative">
|
|
91
|
+
<Search className="w-4 h-4 absolute left-3 top-3 text-muted-foreground" />
|
|
92
|
+
<Input
|
|
93
|
+
placeholder="Search by ticket number, issue, or equipment..."
|
|
94
|
+
value={search}
|
|
95
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
96
|
+
className="pl-9 pr-4 text-sm bg-card"
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Filter Tabs / Pills */}
|
|
101
|
+
<div className="flex gap-2 overflow-x-auto pb-1">
|
|
102
|
+
<button
|
|
103
|
+
onClick={() => setFilter('all')}
|
|
104
|
+
className={cn(
|
|
105
|
+
'text-xs px-3.5 py-1.5 rounded-full border font-medium transition-all whitespace-nowrap',
|
|
106
|
+
filter === 'all'
|
|
107
|
+
? 'bg-primary text-primary-foreground border-primary shadow-sm'
|
|
108
|
+
: 'bg-card text-muted-foreground border-border hover:bg-muted'
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
All ({counts.total})
|
|
112
|
+
</button>
|
|
113
|
+
|
|
114
|
+
<button
|
|
115
|
+
onClick={() => setFilter('active')}
|
|
116
|
+
className={cn(
|
|
117
|
+
'text-xs px-3.5 py-1.5 rounded-full border font-medium transition-all whitespace-nowrap flex items-center gap-1.5',
|
|
118
|
+
filter === 'active'
|
|
119
|
+
? 'bg-primary text-primary-foreground border-primary shadow-sm'
|
|
120
|
+
: 'bg-card text-muted-foreground border-border hover:bg-muted'
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
<Wrench className="w-3 h-3" />
|
|
124
|
+
Active Repair ({counts.active})
|
|
125
|
+
</button>
|
|
126
|
+
|
|
127
|
+
{counts.needsResponse > 0 && (
|
|
128
|
+
<button
|
|
129
|
+
onClick={() => setFilter('needs_response')}
|
|
130
|
+
className={cn(
|
|
131
|
+
'text-xs px-3.5 py-1.5 rounded-full border font-medium transition-all whitespace-nowrap flex items-center gap-1.5',
|
|
132
|
+
filter === 'needs_response'
|
|
133
|
+
? 'bg-amber-600 text-white border-amber-600 shadow-sm'
|
|
134
|
+
: 'bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-200 border-amber-300 hover:bg-amber-100'
|
|
135
|
+
)}
|
|
136
|
+
>
|
|
137
|
+
<MessageCircle className="w-3 h-3" />
|
|
138
|
+
Needs Reply ({counts.needsResponse})
|
|
139
|
+
</button>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
<button
|
|
143
|
+
onClick={() => setFilter('resolved')}
|
|
144
|
+
className={cn(
|
|
145
|
+
'text-xs px-3.5 py-1.5 rounded-full border font-medium transition-all whitespace-nowrap flex items-center gap-1.5',
|
|
146
|
+
filter === 'resolved'
|
|
147
|
+
? 'bg-primary text-primary-foreground border-primary shadow-sm'
|
|
148
|
+
: 'bg-card text-muted-foreground border-border hover:bg-muted'
|
|
149
|
+
)}
|
|
150
|
+
>
|
|
151
|
+
<CheckCircle className="w-3 h-3" />
|
|
152
|
+
Resolved ({counts.resolved})
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Ticket List */}
|
|
157
|
+
{filteredTickets.length === 0 ? (
|
|
158
|
+
<div className="text-center py-12 border rounded-2xl bg-card shadow-sm space-y-2">
|
|
159
|
+
<TicketCheck className="w-8 h-8 text-muted-foreground mx-auto" />
|
|
160
|
+
<p className="text-muted-foreground text-sm font-medium">
|
|
161
|
+
{search
|
|
162
|
+
? 'No tickets match your search.'
|
|
163
|
+
: filter === 'needs_response'
|
|
164
|
+
? 'No tickets currently waiting for your response.'
|
|
165
|
+
: filter === 'active'
|
|
166
|
+
? 'No active tickets in progress.'
|
|
167
|
+
: "You haven't reported any equipment problems yet."}
|
|
168
|
+
</p>
|
|
169
|
+
<p className="text-muted-foreground text-xs">
|
|
170
|
+
Scan a device QR code or click "Report Problem" above to submit an issue.
|
|
171
|
+
</p>
|
|
172
|
+
</div>
|
|
173
|
+
) : (
|
|
174
|
+
<div className="space-y-3">
|
|
175
|
+
{filteredTickets.map((ticket) => (
|
|
176
|
+
<StaffTicketCard key={ticket.id} ticket={ticket} />
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|