@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,722 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createTicket, getDeviceActiveTickets, getTicketCreationDefaults } from "@/lib/actions/tickets";
|
|
4
|
+
import { getDeviceById, getDevices } from "@/lib/actions/devices";
|
|
5
|
+
import { getEngineersTeam } from "@/lib/actions/engineers";
|
|
6
|
+
import { PageHeader } from "@/components/shared/page-header";
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
import { Input } from "@/components/ui/input";
|
|
10
|
+
import { Label } from "@/components/ui/label";
|
|
11
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
12
|
+
import { Badge } from "@/components/ui/badge";
|
|
13
|
+
import { DeviceStatusBadge } from "@/components/devices/device-badges";
|
|
14
|
+
import { TicketStatusBadge } from "@/components/tickets/ticket-badges";
|
|
15
|
+
import {
|
|
16
|
+
AlertTriangle,
|
|
17
|
+
Monitor,
|
|
18
|
+
Search,
|
|
19
|
+
CheckCircle,
|
|
20
|
+
Phone,
|
|
21
|
+
Clock,
|
|
22
|
+
Zap,
|
|
23
|
+
ShieldAlert,
|
|
24
|
+
ArrowLeft,
|
|
25
|
+
Building2,
|
|
26
|
+
UsersRound,
|
|
27
|
+
ExternalLink,
|
|
28
|
+
} from "lucide-react";
|
|
29
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
30
|
+
import Link from "next/link";
|
|
31
|
+
import { useState, useEffect, Suspense } from "react";
|
|
32
|
+
import { cn } from "@/lib/utils";
|
|
33
|
+
|
|
34
|
+
export default function CreateTicketPage() {
|
|
35
|
+
return (
|
|
36
|
+
<Suspense fallback={<div className="p-12 text-center text-muted-foreground">Loading helpdesk form...</div>}>
|
|
37
|
+
<CreateTicketContent />
|
|
38
|
+
</Suspense>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const QUICK_PROBLEM_CHIPS = [
|
|
43
|
+
{ label: "Won't turn on / Power issue", category: "power_issue" },
|
|
44
|
+
{ label: "Alarm constantly sounding", category: "alarm_problem" },
|
|
45
|
+
{ label: "Error code displayed", category: "display_interface" },
|
|
46
|
+
{ label: "Screen blank or flickering", category: "display_interface" },
|
|
47
|
+
{ label: "Battery won't charge", category: "power_issue" },
|
|
48
|
+
{ label: "Damaged cable / sensor / probe", category: "sensor_accessory" },
|
|
49
|
+
{ label: "Calibration or inaccurate readings", category: "performance_concern" },
|
|
50
|
+
{ label: "Physical drop / mechanical damage", category: "mechanical_damage" },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function CreateTicketContent() {
|
|
54
|
+
const router = useRouter();
|
|
55
|
+
const searchParams = useSearchParams();
|
|
56
|
+
const prefilledDeviceId = searchParams.get("deviceId");
|
|
57
|
+
const prefilledHospitalId = searchParams.get("hospitalId");
|
|
58
|
+
const prefilledDepartmentId = searchParams.get("departmentId");
|
|
59
|
+
const prefilledRoundVisitId = searchParams.get("roundVisitId");
|
|
60
|
+
const prefilledSource = searchParams.get("source") || (prefilledDeviceId ? "device_profile" : "manual_entry");
|
|
61
|
+
|
|
62
|
+
const [loading, setLoading] = useState(false);
|
|
63
|
+
const [error, setError] = useState("");
|
|
64
|
+
const [device, setDevice] = useState<any>(null);
|
|
65
|
+
const [activeTickets, setActiveTickets] = useState<any[]>([]);
|
|
66
|
+
|
|
67
|
+
// Organization and location defaults
|
|
68
|
+
const [orgDefaults, setOrgDefaults] = useState<{
|
|
69
|
+
organizationId: string;
|
|
70
|
+
defaultHospitalId: string;
|
|
71
|
+
defaultDepartmentId: string;
|
|
72
|
+
defaultHospitalName?: string;
|
|
73
|
+
defaultDepartmentName?: string;
|
|
74
|
+
isElevatedUser?: boolean;
|
|
75
|
+
hospitals: { id: string; name: string }[];
|
|
76
|
+
departments: { id: string; name: string; hospitalId: string }[];
|
|
77
|
+
userFullName: string;
|
|
78
|
+
} | null>(null);
|
|
79
|
+
|
|
80
|
+
const [selectedHospitalId, setSelectedHospitalId] = useState("");
|
|
81
|
+
const [selectedDepartmentId, setSelectedDepartmentId] = useState("");
|
|
82
|
+
const [selectedDeviceId, setSelectedDeviceId] = useState(prefilledDeviceId || "");
|
|
83
|
+
const [showLocationOverride, setShowLocationOverride] = useState(false);
|
|
84
|
+
|
|
85
|
+
// Form state
|
|
86
|
+
const [title, setTitle] = useState("");
|
|
87
|
+
const [description, setDescription] = useState("");
|
|
88
|
+
const [impact, setImpact] = useState<"device_usable" | "device_not_usable" | "patient_care_affected">("device_not_usable");
|
|
89
|
+
const [category, setCategory] = useState<string>("");
|
|
90
|
+
const [locationCorrection, setLocationCorrection] = useState("");
|
|
91
|
+
const [contactPhone, setContactPhone] = useState("");
|
|
92
|
+
|
|
93
|
+
// Search state
|
|
94
|
+
const [deviceSearch, setDeviceSearch] = useState("");
|
|
95
|
+
const [deviceResults, setDeviceResults] = useState<any[]>([]);
|
|
96
|
+
const [searching, setSearching] = useState(false);
|
|
97
|
+
|
|
98
|
+
// Engineers on duty
|
|
99
|
+
const [onDutyEngineers, setOnDutyEngineers] = useState<any[]>([]);
|
|
100
|
+
|
|
101
|
+
// Load defaults and prefilled device
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
async function loadInitialData() {
|
|
104
|
+
// 1. Load user creation defaults
|
|
105
|
+
const defRes = await getTicketCreationDefaults();
|
|
106
|
+
if (defRes.success && defRes.data) {
|
|
107
|
+
setOrgDefaults(defRes.data);
|
|
108
|
+
setSelectedHospitalId(prefilledHospitalId || defRes.data.defaultHospitalId);
|
|
109
|
+
setSelectedDepartmentId(prefilledDepartmentId || defRes.data.defaultDepartmentId);
|
|
110
|
+
if (defRes.data.userPhone || defRes.data.userExtension) {
|
|
111
|
+
setContactPhone(defRes.data.userPhone || defRes.data.userExtension || "");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. Load on-duty engineers
|
|
116
|
+
const engRes = await getEngineersTeam();
|
|
117
|
+
if (engRes.success && engRes.data) {
|
|
118
|
+
const available = engRes.data.filter((e: any) => e.dutyStatus === "on_duty" || e.dutyStatus === "on_call");
|
|
119
|
+
setOnDutyEngineers(available);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. Load prefilled device if present
|
|
123
|
+
if (prefilledDeviceId) {
|
|
124
|
+
const devRes = await getDeviceById(prefilledDeviceId);
|
|
125
|
+
if (devRes?.success && devRes.data) {
|
|
126
|
+
setDevice(devRes.data);
|
|
127
|
+
setSelectedDeviceId(prefilledDeviceId);
|
|
128
|
+
if (devRes.data.hospitalId) setSelectedHospitalId(devRes.data.hospitalId);
|
|
129
|
+
if (devRes.data.departmentId) setSelectedDepartmentId(devRes.data.departmentId);
|
|
130
|
+
|
|
131
|
+
// Check active tickets for duplicate warning
|
|
132
|
+
const actRes = await getDeviceActiveTickets(prefilledDeviceId);
|
|
133
|
+
if (actRes?.success && actRes.data) setActiveTickets(actRes.data as any[]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
loadInitialData();
|
|
138
|
+
}, [prefilledDeviceId]);
|
|
139
|
+
|
|
140
|
+
const handleDeviceSearch = async (val: string) => {
|
|
141
|
+
setDeviceSearch(val);
|
|
142
|
+
if (val.trim().length < 2) {
|
|
143
|
+
setDeviceResults([]);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
setSearching(true);
|
|
147
|
+
const res = await getDevices({ search: val, pageSize: 6 });
|
|
148
|
+
if (res?.success && res.data) {
|
|
149
|
+
setDeviceResults(res.data as any[]);
|
|
150
|
+
}
|
|
151
|
+
setSearching(false);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleSelectDevice = async (dev: any) => {
|
|
155
|
+
setDevice(dev);
|
|
156
|
+
setSelectedDeviceId(dev.id);
|
|
157
|
+
setDeviceResults([]);
|
|
158
|
+
setDeviceSearch("");
|
|
159
|
+
if (dev.hospitalId) setSelectedHospitalId(dev.hospitalId);
|
|
160
|
+
if (dev.departmentId) setSelectedDepartmentId(dev.departmentId);
|
|
161
|
+
|
|
162
|
+
const actRes = await getDeviceActiveTickets(dev.id);
|
|
163
|
+
if (actRes?.success && actRes.data) {
|
|
164
|
+
setActiveTickets(actRes.data as any[]);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleChipClick = (chip: { label: string; category: string }) => {
|
|
169
|
+
if (!title) {
|
|
170
|
+
setTitle(chip.label);
|
|
171
|
+
} else {
|
|
172
|
+
setTitle(`${title} — ${chip.label}`);
|
|
173
|
+
}
|
|
174
|
+
if (chip.category) {
|
|
175
|
+
setCategory(chip.category);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
setLoading(true);
|
|
182
|
+
setError("");
|
|
183
|
+
|
|
184
|
+
const orgId = device?.organizationId || orgDefaults?.organizationId;
|
|
185
|
+
const hospId = device?.hospitalId || selectedHospitalId || orgDefaults?.defaultHospitalId;
|
|
186
|
+
const deptId = device?.departmentId || selectedDepartmentId || orgDefaults?.defaultDepartmentId;
|
|
187
|
+
|
|
188
|
+
if (!orgId || !hospId || !deptId) {
|
|
189
|
+
setError("Please ensure hospital and department are selected.");
|
|
190
|
+
setLoading(false);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!title.trim() || title.trim().length < 5) {
|
|
195
|
+
setError("Please provide a descriptive problem title (at least 5 characters).");
|
|
196
|
+
setLoading(false);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const payload: any = {
|
|
201
|
+
organizationId: orgId,
|
|
202
|
+
hospitalId: hospId,
|
|
203
|
+
departmentId: deptId,
|
|
204
|
+
deviceId: selectedDeviceId || undefined,
|
|
205
|
+
source: prefilledSource,
|
|
206
|
+
ticketType: impact === "patient_care_affected" ? "urgent_equipment_concern" : "device_problem",
|
|
207
|
+
title: title.trim(),
|
|
208
|
+
description: description.trim() || title.trim(),
|
|
209
|
+
reportedImpact: impact,
|
|
210
|
+
reportedProblemCategory: category || undefined,
|
|
211
|
+
reportedLocationCorrection: locationCorrection.trim() || undefined,
|
|
212
|
+
requesterContactPhone: contactPhone.trim() || undefined,
|
|
213
|
+
requesterContactName: orgDefaults?.userFullName || undefined,
|
|
214
|
+
roundVisitId: prefilledRoundVisitId || undefined,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const result = await createTicket(payload);
|
|
218
|
+
if (result?.success && (result as any).data?.id) {
|
|
219
|
+
router.push(`/tickets/${(result as any).data.id}`);
|
|
220
|
+
} else {
|
|
221
|
+
setError((result as any)?.error || "Failed to create ticket. Please check the fields and try again.");
|
|
222
|
+
setLoading(false);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const availableDepts = orgDefaults?.departments.filter(
|
|
227
|
+
(d) => !selectedHospitalId || d.hospitalId === selectedHospitalId
|
|
228
|
+
) || [];
|
|
229
|
+
|
|
230
|
+
const currentHospitalName =
|
|
231
|
+
device?.hospital?.name ||
|
|
232
|
+
orgDefaults?.hospitals.find((h) => h.id === selectedHospitalId)?.name ||
|
|
233
|
+
orgDefaults?.defaultHospitalName ||
|
|
234
|
+
"General Hospital";
|
|
235
|
+
|
|
236
|
+
const currentDepartmentName =
|
|
237
|
+
device?.department?.name ||
|
|
238
|
+
orgDefaults?.departments.find((d) => d.id === selectedDepartmentId)?.name ||
|
|
239
|
+
orgDefaults?.defaultDepartmentName ||
|
|
240
|
+
"Clinical Department";
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div className="space-y-6 max-w-3xl mx-auto pb-12">
|
|
244
|
+
{/* Top Breadcrumb & Header */}
|
|
245
|
+
<div className="flex items-center gap-3">
|
|
246
|
+
<Button
|
|
247
|
+
type="button"
|
|
248
|
+
variant="ghost"
|
|
249
|
+
size="sm"
|
|
250
|
+
onClick={() => router.back()}
|
|
251
|
+
className="h-9 px-2 text-muted-foreground hover:text-foreground"
|
|
252
|
+
>
|
|
253
|
+
<ArrowLeft className="w-4 h-4 mr-1" />
|
|
254
|
+
Back
|
|
255
|
+
</Button>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<PageHeader
|
|
259
|
+
title="Report Equipment Problem"
|
|
260
|
+
description="Notify the Biomedical Engineering team for immediate inspection or repair"
|
|
261
|
+
/>
|
|
262
|
+
|
|
263
|
+
{error && (
|
|
264
|
+
<div className="p-4 rounded-xl border border-destructive/30 bg-destructive/10 text-destructive text-sm flex items-start gap-3">
|
|
265
|
+
<AlertTriangle className="w-5 h-5 shrink-0 mt-0.5" />
|
|
266
|
+
<div>
|
|
267
|
+
<p className="font-semibold">Unable to submit report</p>
|
|
268
|
+
<p className="text-xs mt-0.5">{error}</p>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
{/* ── 1. Device Selection Card ────────────────────────────────────── */}
|
|
274
|
+
{device ? (
|
|
275
|
+
<Card className="border-2 border-primary/20 shadow-sm overflow-hidden">
|
|
276
|
+
<CardHeader className="bg-primary/5 pb-3">
|
|
277
|
+
<div className="flex items-center justify-between">
|
|
278
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
279
|
+
<Monitor className="w-4 h-4 text-primary" />
|
|
280
|
+
Equipment Selected
|
|
281
|
+
</CardTitle>
|
|
282
|
+
<DeviceStatusBadge status={device.currentStatusCode} />
|
|
283
|
+
</div>
|
|
284
|
+
</CardHeader>
|
|
285
|
+
<CardContent className="pt-4 space-y-3">
|
|
286
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
|
287
|
+
<div>
|
|
288
|
+
<h3 className="text-lg font-bold text-foreground">{device.name}</h3>
|
|
289
|
+
<p className="text-xs text-muted-foreground font-mono">
|
|
290
|
+
Asset Tag: <span className="font-semibold text-foreground">{device.assetNumber}</span>
|
|
291
|
+
{device.serialNumber && ` · Serial: ${device.serialNumber}`}
|
|
292
|
+
</p>
|
|
293
|
+
</div>
|
|
294
|
+
{!prefilledDeviceId && (
|
|
295
|
+
<Button
|
|
296
|
+
type="button"
|
|
297
|
+
variant="outline"
|
|
298
|
+
size="sm"
|
|
299
|
+
onClick={() => {
|
|
300
|
+
setDevice(null);
|
|
301
|
+
setSelectedDeviceId("");
|
|
302
|
+
setActiveTickets([]);
|
|
303
|
+
}}
|
|
304
|
+
className="self-start sm:self-auto text-xs"
|
|
305
|
+
>
|
|
306
|
+
Change Device
|
|
307
|
+
</Button>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 text-xs text-muted-foreground bg-muted/40 p-2.5 rounded-lg border">
|
|
312
|
+
<div>
|
|
313
|
+
<span className="block text-[11px] font-semibold text-foreground">Hospital</span>
|
|
314
|
+
<span>{(device as any).hospital?.name || "General Hospital"}</span>
|
|
315
|
+
</div>
|
|
316
|
+
<div>
|
|
317
|
+
<span className="block text-[11px] font-semibold text-foreground">Department</span>
|
|
318
|
+
<span>{(device as any).department?.name || "Clinical Department"}</span>
|
|
319
|
+
</div>
|
|
320
|
+
<div>
|
|
321
|
+
<span className="block text-[11px] font-semibold text-foreground">Registered Room</span>
|
|
322
|
+
<span>{(device as any).location?.name || device.exactLocationDescription || "Room not set"}</span>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</CardContent>
|
|
326
|
+
</Card>
|
|
327
|
+
) : (
|
|
328
|
+
<Card className="shadow-sm">
|
|
329
|
+
<CardHeader className="pb-3">
|
|
330
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
331
|
+
<Search className="w-4 h-4 text-primary" />
|
|
332
|
+
Which device is having problems?
|
|
333
|
+
</CardTitle>
|
|
334
|
+
</CardHeader>
|
|
335
|
+
<CardContent className="space-y-4">
|
|
336
|
+
<div className="relative">
|
|
337
|
+
<Search className="w-4 h-4 absolute left-3 top-3 text-muted-foreground" />
|
|
338
|
+
<Input
|
|
339
|
+
placeholder="Search by asset tag (e.g. ICU-003), device name, or model..."
|
|
340
|
+
value={deviceSearch}
|
|
341
|
+
onChange={(e) => handleDeviceSearch(e.target.value)}
|
|
342
|
+
className="pl-9 pr-4 py-2 text-sm"
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
{searching && <p className="text-xs text-muted-foreground">Searching equipment catalog...</p>}
|
|
347
|
+
|
|
348
|
+
{deviceResults.length > 0 && (
|
|
349
|
+
<div className="border rounded-xl divide-y max-h-56 overflow-auto bg-card shadow-inner">
|
|
350
|
+
{deviceResults.map((dev: any) => (
|
|
351
|
+
<button
|
|
352
|
+
key={dev.id}
|
|
353
|
+
type="button"
|
|
354
|
+
onClick={() => handleSelectDevice(dev)}
|
|
355
|
+
className="w-full text-left p-3 hover:bg-muted/70 transition flex items-center justify-between gap-3"
|
|
356
|
+
>
|
|
357
|
+
<div className="min-w-0">
|
|
358
|
+
<p className="font-medium text-sm text-foreground truncate">{dev.name}</p>
|
|
359
|
+
<p className="text-xs text-muted-foreground font-mono">
|
|
360
|
+
Asset #{dev.assetNumber} · {dev.department?.name || "Dept"} · {dev.location?.name || "Room"}
|
|
361
|
+
</p>
|
|
362
|
+
</div>
|
|
363
|
+
<DeviceStatusBadge status={dev.currentStatusCode} />
|
|
364
|
+
</button>
|
|
365
|
+
))}
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
|
|
369
|
+
{/* Automatic Scope Confirmation & Optional Admin Switcher */}
|
|
370
|
+
<div className="pt-3 border-t space-y-3">
|
|
371
|
+
<div className="p-3.5 rounded-xl border bg-muted/30 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
372
|
+
<div className="flex items-center gap-3">
|
|
373
|
+
<div className="p-2 rounded-lg bg-primary/10 text-primary shrink-0">
|
|
374
|
+
<Building2 className="w-4 h-4" />
|
|
375
|
+
</div>
|
|
376
|
+
<div>
|
|
377
|
+
<p className="text-xs font-semibold text-foreground">
|
|
378
|
+
Assigned Location: {currentDepartmentName} · {currentHospitalName}
|
|
379
|
+
</p>
|
|
380
|
+
<p className="text-[11px] text-muted-foreground">
|
|
381
|
+
Pre-assigned from your user profile — no manual entry required.
|
|
382
|
+
</p>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<div className="flex items-center gap-2 self-start sm:self-auto">
|
|
387
|
+
<Button asChild variant="outline" size="sm" className="h-7 text-xs px-2.5">
|
|
388
|
+
<Link href="/my-department">
|
|
389
|
+
Browse Department Equipment →
|
|
390
|
+
</Link>
|
|
391
|
+
</Button>
|
|
392
|
+
{orgDefaults?.isElevatedUser && (
|
|
393
|
+
<Button
|
|
394
|
+
type="button"
|
|
395
|
+
variant="ghost"
|
|
396
|
+
size="sm"
|
|
397
|
+
onClick={() => setShowLocationOverride(!showLocationOverride)}
|
|
398
|
+
className="h-7 text-xs px-2 text-muted-foreground hover:text-foreground"
|
|
399
|
+
>
|
|
400
|
+
{showLocationOverride ? "Lock Location" : "Change (Admin)"}
|
|
401
|
+
</Button>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
{/* Only shown if elevated admin explicitly opens the switcher */}
|
|
407
|
+
{orgDefaults?.isElevatedUser && showLocationOverride && (
|
|
408
|
+
<div className="p-3.5 rounded-xl border bg-background space-y-3">
|
|
409
|
+
<p className="text-xs font-semibold text-muted-foreground">Administrative Location Override:</p>
|
|
410
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
411
|
+
<div>
|
|
412
|
+
<Label htmlFor="hosp-select" className="text-xs">Hospital</Label>
|
|
413
|
+
<select
|
|
414
|
+
id="hosp-select"
|
|
415
|
+
value={selectedHospitalId}
|
|
416
|
+
onChange={(e) => setSelectedHospitalId(e.target.value)}
|
|
417
|
+
className="w-full mt-1 rounded-lg border border-input bg-background px-3 py-2 text-sm"
|
|
418
|
+
>
|
|
419
|
+
<option value="">Select hospital...</option>
|
|
420
|
+
{orgDefaults?.hospitals.map((h) => (
|
|
421
|
+
<option key={h.id} value={h.id}>{h.name}</option>
|
|
422
|
+
))}
|
|
423
|
+
</select>
|
|
424
|
+
</div>
|
|
425
|
+
<div>
|
|
426
|
+
<Label htmlFor="dept-select" className="text-xs">Department</Label>
|
|
427
|
+
<select
|
|
428
|
+
id="dept-select"
|
|
429
|
+
value={selectedDepartmentId}
|
|
430
|
+
onChange={(e) => setSelectedDepartmentId(e.target.value)}
|
|
431
|
+
className="w-full mt-1 rounded-lg border border-input bg-background px-3 py-2 text-sm"
|
|
432
|
+
>
|
|
433
|
+
<option value="">Select department...</option>
|
|
434
|
+
{availableDepts.map((d) => (
|
|
435
|
+
<option key={d.id} value={d.id}>{d.name}</option>
|
|
436
|
+
))}
|
|
437
|
+
</select>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
)}
|
|
442
|
+
</div>
|
|
443
|
+
</CardContent>
|
|
444
|
+
</Card>
|
|
445
|
+
)}
|
|
446
|
+
|
|
447
|
+
{/* ── 2. Duplicate Ticket Warning ─────────────────────────────────── */}
|
|
448
|
+
{activeTickets.length > 0 && (
|
|
449
|
+
<Card className="border-amber-300 dark:border-amber-900 bg-amber-50/70 dark:bg-amber-950/20 shadow-sm">
|
|
450
|
+
<CardContent className="p-4">
|
|
451
|
+
<div className="flex items-start gap-3">
|
|
452
|
+
<AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400 shrink-0 mt-0.5" />
|
|
453
|
+
<div className="space-y-2 flex-1 min-w-0">
|
|
454
|
+
<div>
|
|
455
|
+
<h4 className="font-semibold text-sm text-amber-900 dark:text-amber-200">
|
|
456
|
+
Active Ticket Open on this Device ({activeTickets.length})
|
|
457
|
+
</h4>
|
|
458
|
+
<p className="text-xs text-amber-700 dark:text-amber-300 mt-0.5">
|
|
459
|
+
Engineering is already tracking an issue for this equipment. You can comment on the active ticket instead of creating a duplicate.
|
|
460
|
+
</p>
|
|
461
|
+
</div>
|
|
462
|
+
<div className="space-y-1.5">
|
|
463
|
+
{activeTickets.map((t: any) => (
|
|
464
|
+
<div
|
|
465
|
+
key={t.id}
|
|
466
|
+
className="flex items-center justify-between p-2 rounded-lg bg-card/80 border border-amber-200 dark:border-amber-900 text-xs gap-2"
|
|
467
|
+
>
|
|
468
|
+
<div className="truncate">
|
|
469
|
+
<span className="font-mono font-bold">{t.ticketNumber}</span>: {t.title}
|
|
470
|
+
</div>
|
|
471
|
+
<Link
|
|
472
|
+
href={`/tickets/${t.id}`}
|
|
473
|
+
className="font-semibold text-primary hover:underline flex items-center gap-1 shrink-0"
|
|
474
|
+
>
|
|
475
|
+
View Ticket
|
|
476
|
+
<ExternalLink className="w-3 h-3" />
|
|
477
|
+
</Link>
|
|
478
|
+
</div>
|
|
479
|
+
))}
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
</CardContent>
|
|
484
|
+
</Card>
|
|
485
|
+
)}
|
|
486
|
+
|
|
487
|
+
{/* ── 3. Problem Details Form ─────────────────────────────────────── */}
|
|
488
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
489
|
+
{/* Clinical Impact Selector */}
|
|
490
|
+
<Card className="shadow-sm">
|
|
491
|
+
<CardHeader className="pb-3">
|
|
492
|
+
<CardTitle className="text-base flex items-center justify-between">
|
|
493
|
+
<span>Clinical Urgency & Impact *</span>
|
|
494
|
+
<span className="text-xs font-normal text-muted-foreground">Select current state</span>
|
|
495
|
+
</CardTitle>
|
|
496
|
+
</CardHeader>
|
|
497
|
+
<CardContent className="space-y-3">
|
|
498
|
+
<div className="grid gap-2.5 sm:grid-cols-3">
|
|
499
|
+
{/* Option 1: Usable */}
|
|
500
|
+
<button
|
|
501
|
+
type="button"
|
|
502
|
+
onClick={() => setImpact("device_usable")}
|
|
503
|
+
className={cn(
|
|
504
|
+
"flex flex-col text-left p-3.5 rounded-xl border-2 transition-all",
|
|
505
|
+
impact === "device_usable"
|
|
506
|
+
? "border-green-600 bg-green-50/70 dark:bg-green-950/30 text-green-900 dark:text-green-200 shadow-sm"
|
|
507
|
+
: "border-border bg-card hover:bg-muted/40 text-muted-foreground"
|
|
508
|
+
)}
|
|
509
|
+
>
|
|
510
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
511
|
+
<span className="font-semibold text-sm text-foreground">Still Usable</span>
|
|
512
|
+
<span className="w-3 h-3 rounded-full bg-green-500" />
|
|
513
|
+
</div>
|
|
514
|
+
<p className="text-xs leading-relaxed">
|
|
515
|
+
Working with minor issue (alarm, accessory, display artifact). Safe for care.
|
|
516
|
+
</p>
|
|
517
|
+
</button>
|
|
518
|
+
|
|
519
|
+
{/* Option 2: Not Usable */}
|
|
520
|
+
<button
|
|
521
|
+
type="button"
|
|
522
|
+
onClick={() => setImpact("device_not_usable")}
|
|
523
|
+
className={cn(
|
|
524
|
+
"flex flex-col text-left p-3.5 rounded-xl border-2 transition-all",
|
|
525
|
+
impact === "device_not_usable"
|
|
526
|
+
? "border-amber-500 bg-amber-50/70 dark:bg-amber-950/30 text-amber-900 dark:text-amber-200 shadow-sm"
|
|
527
|
+
: "border-border bg-card hover:bg-muted/40 text-muted-foreground"
|
|
528
|
+
)}
|
|
529
|
+
>
|
|
530
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
531
|
+
<span className="font-semibold text-sm text-foreground">NOT Usable</span>
|
|
532
|
+
<span className="w-3 h-3 rounded-full bg-amber-500" />
|
|
533
|
+
</div>
|
|
534
|
+
<p className="text-xs leading-relaxed">
|
|
535
|
+
Device is inoperable or unsafe. Removed from patient use and waiting repair.
|
|
536
|
+
</p>
|
|
537
|
+
</button>
|
|
538
|
+
|
|
539
|
+
{/* Option 3: Patient Care Affected */}
|
|
540
|
+
<button
|
|
541
|
+
type="button"
|
|
542
|
+
onClick={() => setImpact("patient_care_affected")}
|
|
543
|
+
className={cn(
|
|
544
|
+
"flex flex-col text-left p-3.5 rounded-xl border-2 transition-all",
|
|
545
|
+
impact === "patient_care_affected"
|
|
546
|
+
? "border-red-600 bg-red-50/80 dark:bg-red-950/40 text-red-950 dark:text-red-100 shadow-md ring-2 ring-red-500/20"
|
|
547
|
+
: "border-border bg-card hover:bg-muted/40 text-muted-foreground"
|
|
548
|
+
)}
|
|
549
|
+
>
|
|
550
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
551
|
+
<span className="font-bold text-sm text-red-700 dark:text-red-400 flex items-center gap-1">
|
|
552
|
+
<Zap className="w-3.5 h-3.5" />
|
|
553
|
+
Patient Affected
|
|
554
|
+
</span>
|
|
555
|
+
<span className="w-3 h-3 rounded-full bg-red-600 animate-pulse" />
|
|
556
|
+
</div>
|
|
557
|
+
<p className="text-xs leading-relaxed text-red-800 dark:text-red-300">
|
|
558
|
+
CRITICAL: Procedure or patient monitoring interrupted. Immediate P1 dispatch.
|
|
559
|
+
</p>
|
|
560
|
+
</button>
|
|
561
|
+
</div>
|
|
562
|
+
</CardContent>
|
|
563
|
+
</Card>
|
|
564
|
+
|
|
565
|
+
{/* Problem Information */}
|
|
566
|
+
<Card className="shadow-sm">
|
|
567
|
+
<CardHeader className="pb-3">
|
|
568
|
+
<CardTitle className="text-base">What happened?</CardTitle>
|
|
569
|
+
</CardHeader>
|
|
570
|
+
<CardContent className="space-y-4">
|
|
571
|
+
{/* Quick Chips */}
|
|
572
|
+
<div>
|
|
573
|
+
<p className="text-xs text-muted-foreground mb-2">Tap a common symptom to quickly prefill:</p>
|
|
574
|
+
<div className="flex flex-wrap gap-1.5">
|
|
575
|
+
{QUICK_PROBLEM_CHIPS.map((chip) => (
|
|
576
|
+
<button
|
|
577
|
+
key={chip.label}
|
|
578
|
+
type="button"
|
|
579
|
+
onClick={() => handleChipClick(chip)}
|
|
580
|
+
className="text-xs px-2.5 py-1.5 rounded-lg border bg-muted/40 hover:bg-primary/10 hover:border-primary/40 transition text-muted-foreground hover:text-foreground font-medium"
|
|
581
|
+
>
|
|
582
|
+
+ {chip.label}
|
|
583
|
+
</button>
|
|
584
|
+
))}
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
|
|
588
|
+
<div>
|
|
589
|
+
<Label htmlFor="title" className="text-sm font-semibold">
|
|
590
|
+
Problem Summary *
|
|
591
|
+
</Label>
|
|
592
|
+
<Input
|
|
593
|
+
id="title"
|
|
594
|
+
value={title}
|
|
595
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
596
|
+
placeholder="e.g. Infusion pump beeping error code E-42"
|
|
597
|
+
required
|
|
598
|
+
className="mt-1.5"
|
|
599
|
+
/>
|
|
600
|
+
</div>
|
|
601
|
+
|
|
602
|
+
<div>
|
|
603
|
+
<Label htmlFor="description" className="text-sm font-semibold">
|
|
604
|
+
Detailed Observation (optional but helpful)
|
|
605
|
+
</Label>
|
|
606
|
+
<Textarea
|
|
607
|
+
id="description"
|
|
608
|
+
value={description}
|
|
609
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
610
|
+
rows={3}
|
|
611
|
+
placeholder="Describe what occurred: Started during morning rounds? Any alarms heard? Did restart resolve it?"
|
|
612
|
+
className="mt-1.5 text-sm"
|
|
613
|
+
/>
|
|
614
|
+
</div>
|
|
615
|
+
|
|
616
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-2">
|
|
617
|
+
<div>
|
|
618
|
+
<Label htmlFor="category" className="text-xs font-medium">
|
|
619
|
+
Technical Category
|
|
620
|
+
</Label>
|
|
621
|
+
<select
|
|
622
|
+
id="category"
|
|
623
|
+
value={category}
|
|
624
|
+
onChange={(e) => setCategory(e.target.value)}
|
|
625
|
+
className="w-full mt-1.5 rounded-lg border border-input bg-background px-3 py-2 text-sm"
|
|
626
|
+
>
|
|
627
|
+
<option value="">Auto-detected / Other</option>
|
|
628
|
+
<option value="power_issue">Power / Battery</option>
|
|
629
|
+
<option value="alarm_problem">Alarm Malfunction</option>
|
|
630
|
+
<option value="display_interface">Display / Screen</option>
|
|
631
|
+
<option value="sensor_accessory">Sensor / Cable / Probe</option>
|
|
632
|
+
<option value="mechanical_damage">Mechanical / Physical Damage</option>
|
|
633
|
+
<option value="performance_concern">Calibration / Accuracy</option>
|
|
634
|
+
<option value="software_configuration">Software / Settings</option>
|
|
635
|
+
<option value="other">General Other</option>
|
|
636
|
+
</select>
|
|
637
|
+
</div>
|
|
638
|
+
|
|
639
|
+
<div>
|
|
640
|
+
<Label htmlFor="locationCorrection" className="text-xs font-medium">
|
|
641
|
+
Current Room / Bed (if moved)
|
|
642
|
+
</Label>
|
|
643
|
+
<Input
|
|
644
|
+
id="locationCorrection"
|
|
645
|
+
value={locationCorrection}
|
|
646
|
+
onChange={(e) => setLocationCorrection(e.target.value)}
|
|
647
|
+
placeholder="e.g. Moved to Bed 4, ICU"
|
|
648
|
+
className="mt-1.5 text-sm"
|
|
649
|
+
/>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
|
|
653
|
+
<div>
|
|
654
|
+
<Label htmlFor="contactPhone" className="text-xs font-medium">
|
|
655
|
+
Your Contact Extension / Phone (for engineer callback)
|
|
656
|
+
</Label>
|
|
657
|
+
<div className="relative mt-1.5">
|
|
658
|
+
<Phone className="w-4 h-4 absolute left-3 top-3 text-muted-foreground" />
|
|
659
|
+
<Input
|
|
660
|
+
id="contactPhone"
|
|
661
|
+
value={contactPhone}
|
|
662
|
+
onChange={(e) => setContactPhone(e.target.value)}
|
|
663
|
+
placeholder="e.g. Ext. 3120 (Nurse Station) or mobile"
|
|
664
|
+
className="pl-9 text-sm"
|
|
665
|
+
/>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
</CardContent>
|
|
669
|
+
</Card>
|
|
670
|
+
|
|
671
|
+
{/* ── 4. Engineering Team Availability Info ───────────────────── */}
|
|
672
|
+
{onDutyEngineers.length > 0 && (
|
|
673
|
+
<Card className="border-purple-200 dark:border-purple-900 bg-purple-50/50 dark:bg-purple-950/20 shadow-sm">
|
|
674
|
+
<CardContent className="p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
675
|
+
<div className="flex items-center gap-3">
|
|
676
|
+
<div className="p-2.5 rounded-lg bg-purple-100 dark:bg-purple-900 text-purple-700 dark:text-purple-300">
|
|
677
|
+
<UsersRound className="w-5 h-5" />
|
|
678
|
+
</div>
|
|
679
|
+
<div>
|
|
680
|
+
<h4 className="text-sm font-semibold text-purple-900 dark:text-purple-200">
|
|
681
|
+
Biomedical Engineers Available Now
|
|
682
|
+
</h4>
|
|
683
|
+
<p className="text-xs text-purple-700 dark:text-purple-300 mt-0.5">
|
|
684
|
+
{onDutyEngineers.map((e) => `${e.fullName} (${e.extension ? `Ext. ${e.extension}` : e.jobTitle})`).join(" · ")}
|
|
685
|
+
</p>
|
|
686
|
+
</div>
|
|
687
|
+
</div>
|
|
688
|
+
<Button asChild variant="outline" size="sm" className="self-start sm:self-auto text-xs bg-card">
|
|
689
|
+
<Link href="/engineering-team">View Duty Roster</Link>
|
|
690
|
+
</Button>
|
|
691
|
+
</CardContent>
|
|
692
|
+
</Card>
|
|
693
|
+
)}
|
|
694
|
+
|
|
695
|
+
{/* Submit Actions */}
|
|
696
|
+
<div className="flex flex-col-reverse sm:flex-row justify-end gap-3 pt-2">
|
|
697
|
+
<Button
|
|
698
|
+
type="button"
|
|
699
|
+
variant="outline"
|
|
700
|
+
size="lg"
|
|
701
|
+
onClick={() => router.back()}
|
|
702
|
+
disabled={loading}
|
|
703
|
+
className="w-full sm:w-auto"
|
|
704
|
+
>
|
|
705
|
+
Cancel
|
|
706
|
+
</Button>
|
|
707
|
+
<Button
|
|
708
|
+
type="submit"
|
|
709
|
+
size="lg"
|
|
710
|
+
disabled={loading}
|
|
711
|
+
className={cn(
|
|
712
|
+
"w-full sm:w-auto min-w-[200px] font-semibold text-base",
|
|
713
|
+
impact === "patient_care_affected" && "bg-red-600 hover:bg-red-700 text-white"
|
|
714
|
+
)}
|
|
715
|
+
>
|
|
716
|
+
{loading ? "Transmitting Report..." : impact === "patient_care_affected" ? "Submit URGENT Report" : "Submit Problem Report"}
|
|
717
|
+
</Button>
|
|
718
|
+
</div>
|
|
719
|
+
</form>
|
|
720
|
+
</div>
|
|
721
|
+
);
|
|
722
|
+
}
|