@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,955 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { useState, useTransition } from "react"
|
|
5
|
+
import { useRouter } from "next/navigation"
|
|
6
|
+
import Link from "next/link"
|
|
7
|
+
import { Button } from "@/components/ui/button"
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogDescription,
|
|
12
|
+
DialogFooter,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
} from "@/components/ui/dialog"
|
|
16
|
+
import { Label } from "@/components/ui/label"
|
|
17
|
+
import { Input } from "@/components/ui/input"
|
|
18
|
+
import { Textarea } from "@/components/ui/textarea"
|
|
19
|
+
import {
|
|
20
|
+
Select,
|
|
21
|
+
SelectContent,
|
|
22
|
+
SelectItem,
|
|
23
|
+
SelectTrigger,
|
|
24
|
+
SelectValue,
|
|
25
|
+
} from "@/components/ui/select"
|
|
26
|
+
import {
|
|
27
|
+
startMaintenanceTask,
|
|
28
|
+
markTaskWaiting,
|
|
29
|
+
completeTaskWork,
|
|
30
|
+
cancelMaintenanceTask,
|
|
31
|
+
} from "@/lib/actions/maintenance-tasks"
|
|
32
|
+
import {
|
|
33
|
+
reviewMaintenanceRecord,
|
|
34
|
+
releaseDeviceMaintenance,
|
|
35
|
+
signMaintenanceRecord,
|
|
36
|
+
} from "@/lib/actions/signatures"
|
|
37
|
+
import {
|
|
38
|
+
Play,
|
|
39
|
+
Pause,
|
|
40
|
+
CheckCheck,
|
|
41
|
+
FileSignature,
|
|
42
|
+
Eye,
|
|
43
|
+
Sparkles,
|
|
44
|
+
RotateCcw,
|
|
45
|
+
Ban,
|
|
46
|
+
Loader2,
|
|
47
|
+
AlertCircle,
|
|
48
|
+
ShieldCheck,
|
|
49
|
+
Wrench,
|
|
50
|
+
} from "lucide-react"
|
|
51
|
+
|
|
52
|
+
interface TaskActionBarProps {
|
|
53
|
+
taskId: string
|
|
54
|
+
taskNumber: string
|
|
55
|
+
statusCode: string
|
|
56
|
+
deviceId: string
|
|
57
|
+
resultRecordId?: string | null
|
|
58
|
+
currentUserId?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function TaskActionBar({
|
|
62
|
+
taskId,
|
|
63
|
+
taskNumber,
|
|
64
|
+
statusCode,
|
|
65
|
+
deviceId,
|
|
66
|
+
resultRecordId,
|
|
67
|
+
currentUserId,
|
|
68
|
+
}: TaskActionBarProps) {
|
|
69
|
+
const router = useRouter()
|
|
70
|
+
const [isPending, startTransition] = useTransition()
|
|
71
|
+
const [error, setError] = useState<string | null>(null)
|
|
72
|
+
|
|
73
|
+
// Dialog States
|
|
74
|
+
const [waitingDialogOpen, setWaitingDialogOpen] = useState(false)
|
|
75
|
+
const [completeDialogOpen, setCompleteDialogOpen] = useState(false)
|
|
76
|
+
const [reviewDialogOpen, setReviewDialogOpen] = useState(false)
|
|
77
|
+
const [releaseDialogOpen, setReleaseDialogOpen] = useState(false)
|
|
78
|
+
const [cancelDialogOpen, setCancelDialogOpen] = useState(false)
|
|
79
|
+
const [signDialogOpen, setSignDialogOpen] = useState(false)
|
|
80
|
+
|
|
81
|
+
// Form States - Mark Waiting
|
|
82
|
+
const [waitingDependencyType, setWaitingDependencyType] = useState<
|
|
83
|
+
"parts" | "requester" | "vendor" | "department_access"
|
|
84
|
+
>("parts")
|
|
85
|
+
const [waitingReason, setWaitingReason] = useState("")
|
|
86
|
+
|
|
87
|
+
// Form States - Complete Work
|
|
88
|
+
const [diagnosis, setDiagnosis] = useState("")
|
|
89
|
+
const [rootCause, setRootCause] = useState("")
|
|
90
|
+
const [workPerformed, setWorkPerformed] = useState("")
|
|
91
|
+
const [findings, setFindings] = useState("")
|
|
92
|
+
const [recommendations, setRecommendations] = useState("")
|
|
93
|
+
const [finalResultCode, setFinalResultCode] = useState<string>("passed")
|
|
94
|
+
const [finalDeviceStatusCode, setFinalDeviceStatusCode] = useState<string>("awaiting_release")
|
|
95
|
+
|
|
96
|
+
// Form States - Review Work
|
|
97
|
+
const [reviewDecision, setReviewDecision] = useState<"approve" | "reject">("approve")
|
|
98
|
+
const [reviewNotes, setReviewNotes] = useState("")
|
|
99
|
+
const [rejectionReason, setRejectionReason] = useState("")
|
|
100
|
+
const [reviewPassword, setReviewPassword] = useState("")
|
|
101
|
+
|
|
102
|
+
// Form States - Release Device
|
|
103
|
+
const [releaseStatus, setReleaseStatus] = useState<"operational" | "operational_with_limitations">(
|
|
104
|
+
"operational"
|
|
105
|
+
)
|
|
106
|
+
const [releaseNotes, setReleaseNotes] = useState("")
|
|
107
|
+
const [releasePassword, setReleasePassword] = useState("")
|
|
108
|
+
|
|
109
|
+
// Form States - Sign & Submit
|
|
110
|
+
const [signPassword, setSignPassword] = useState("")
|
|
111
|
+
const [signComments, setSignComments] = useState("")
|
|
112
|
+
|
|
113
|
+
// Form States - Cancel Task
|
|
114
|
+
const [cancellationReason, setCancellationReason] = useState("")
|
|
115
|
+
|
|
116
|
+
// Handlers
|
|
117
|
+
const handleStartWork = () => {
|
|
118
|
+
setError(null)
|
|
119
|
+
startTransition(async () => {
|
|
120
|
+
const res = await startMaintenanceTask(taskId)
|
|
121
|
+
if (res?.success) {
|
|
122
|
+
router.refresh()
|
|
123
|
+
} else {
|
|
124
|
+
setError(res?.error || "Failed to start maintenance work")
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const handleMarkWaiting = (e: React.FormEvent) => {
|
|
130
|
+
e.preventDefault()
|
|
131
|
+
if (!waitingReason.trim() || waitingReason.trim().length < 5) {
|
|
132
|
+
setError("Waiting reason must be at least 5 characters")
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setError(null)
|
|
137
|
+
startTransition(async () => {
|
|
138
|
+
const res = await markTaskWaiting(taskId, {
|
|
139
|
+
waitingDependencyType,
|
|
140
|
+
waitingReason: waitingReason.trim(),
|
|
141
|
+
})
|
|
142
|
+
if (res?.success) {
|
|
143
|
+
setWaitingDialogOpen(false)
|
|
144
|
+
setWaitingReason("")
|
|
145
|
+
router.refresh()
|
|
146
|
+
} else {
|
|
147
|
+
setError(res?.error || "Failed to update task to waiting state")
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const handleCompleteWork = (e: React.FormEvent) => {
|
|
153
|
+
e.preventDefault()
|
|
154
|
+
if (!workPerformed.trim() || workPerformed.trim().length < 10) {
|
|
155
|
+
setError("Work performed description must be at least 10 characters")
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
setError(null)
|
|
160
|
+
startTransition(async () => {
|
|
161
|
+
const res = await completeTaskWork(taskId, {
|
|
162
|
+
diagnosis: diagnosis.trim() || undefined,
|
|
163
|
+
rootCause: rootCause.trim() || undefined,
|
|
164
|
+
workPerformed: workPerformed.trim(),
|
|
165
|
+
findings: findings.trim() || undefined,
|
|
166
|
+
recommendations: recommendations.trim() || undefined,
|
|
167
|
+
finalResultCode: finalResultCode as any,
|
|
168
|
+
finalDeviceStatusCode: finalDeviceStatusCode as any,
|
|
169
|
+
})
|
|
170
|
+
if (res?.success) {
|
|
171
|
+
setCompleteDialogOpen(false)
|
|
172
|
+
router.refresh()
|
|
173
|
+
} else {
|
|
174
|
+
setError(res?.error || "Failed to record completed work")
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const handleSignAndSubmit = (e: React.FormEvent) => {
|
|
180
|
+
e.preventDefault()
|
|
181
|
+
if (!signPassword) {
|
|
182
|
+
setError("Password is required for electronic signature authentication")
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
setError(null)
|
|
187
|
+
startTransition(async () => {
|
|
188
|
+
const res = await signMaintenanceRecord(
|
|
189
|
+
taskId,
|
|
190
|
+
{
|
|
191
|
+
diagnosis: diagnosis.trim() || undefined,
|
|
192
|
+
rootCause: rootCause.trim() || undefined,
|
|
193
|
+
workPerformed: workPerformed.trim() || "Maintenance work completed per checklist and standard procedure.",
|
|
194
|
+
findings: findings.trim() || undefined,
|
|
195
|
+
recommendations: recommendations.trim() || undefined,
|
|
196
|
+
finalResultCode: finalResultCode || "passed",
|
|
197
|
+
finalDeviceStatusCode: finalDeviceStatusCode || "awaiting_release",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
signaturePurpose: "perform",
|
|
201
|
+
authMethod: "password_reauth",
|
|
202
|
+
password: signPassword,
|
|
203
|
+
comments: signComments.trim() || undefined,
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if (res?.success) {
|
|
208
|
+
setSignDialogOpen(false)
|
|
209
|
+
setSignPassword("")
|
|
210
|
+
setSignComments("")
|
|
211
|
+
router.refresh()
|
|
212
|
+
} else {
|
|
213
|
+
setError(res?.error || "Failed to electronically sign and submit record")
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const handleReviewWork = (e: React.FormEvent) => {
|
|
219
|
+
e.preventDefault()
|
|
220
|
+
if (!reviewPassword) {
|
|
221
|
+
setError("Password is required for electronic review signature")
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
if (reviewDecision === "reject" && (!rejectionReason.trim() || rejectionReason.trim().length < 5)) {
|
|
225
|
+
setError("Please provide a reason for rejecting the maintenance work (min 5 characters)")
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
setError(null)
|
|
230
|
+
startTransition(async () => {
|
|
231
|
+
const res = await reviewMaintenanceRecord(taskId, {
|
|
232
|
+
decision: reviewDecision,
|
|
233
|
+
reviewNotes: reviewNotes.trim() || undefined,
|
|
234
|
+
rejectionReason: rejectionReason.trim() || undefined,
|
|
235
|
+
password: reviewPassword,
|
|
236
|
+
})
|
|
237
|
+
if (res?.success) {
|
|
238
|
+
setReviewDialogOpen(false)
|
|
239
|
+
setReviewPassword("")
|
|
240
|
+
setReviewNotes("")
|
|
241
|
+
setRejectionReason("")
|
|
242
|
+
router.refresh()
|
|
243
|
+
} else {
|
|
244
|
+
setError(res?.error || "Failed to submit technical review")
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const handleReleaseDevice = (e: React.FormEvent) => {
|
|
250
|
+
e.preventDefault()
|
|
251
|
+
if (!releasePassword) {
|
|
252
|
+
setError("Password is required for release authorization signature")
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
setError(null)
|
|
257
|
+
startTransition(async () => {
|
|
258
|
+
const res = await releaseDeviceMaintenance(taskId, {
|
|
259
|
+
finalDeviceStatusCode: releaseStatus,
|
|
260
|
+
releaseNotes: releaseNotes.trim() || undefined,
|
|
261
|
+
password: releasePassword,
|
|
262
|
+
})
|
|
263
|
+
if (res?.success) {
|
|
264
|
+
setReleaseDialogOpen(false)
|
|
265
|
+
setReleasePassword("")
|
|
266
|
+
setReleaseNotes("")
|
|
267
|
+
router.refresh()
|
|
268
|
+
} else {
|
|
269
|
+
setError(res?.error || "Failed to authorize device release")
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const handleCancelTask = (e: React.FormEvent) => {
|
|
275
|
+
e.preventDefault()
|
|
276
|
+
if (!cancellationReason.trim() || cancellationReason.trim().length < 10) {
|
|
277
|
+
setError("Cancellation reason must be at least 10 characters")
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
setError(null)
|
|
282
|
+
startTransition(async () => {
|
|
283
|
+
const res = await cancelMaintenanceTask(taskId, {
|
|
284
|
+
cancellationReason: cancellationReason.trim(),
|
|
285
|
+
})
|
|
286
|
+
if (res?.success) {
|
|
287
|
+
setCancelDialogOpen(false)
|
|
288
|
+
setCancellationReason("")
|
|
289
|
+
router.refresh()
|
|
290
|
+
} else {
|
|
291
|
+
setError(res?.error || "Failed to cancel task")
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div className="flex flex-col gap-2">
|
|
298
|
+
{error && (
|
|
299
|
+
<div className="flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 p-3 text-xs text-destructive">
|
|
300
|
+
<AlertCircle className="h-4 w-4 shrink-0" />
|
|
301
|
+
<span>{error}</span>
|
|
302
|
+
<Button
|
|
303
|
+
type="button"
|
|
304
|
+
variant="ghost"
|
|
305
|
+
size="sm"
|
|
306
|
+
onClick={() => setError(null)}
|
|
307
|
+
className="ml-auto h-6 text-xs text-destructive hover:bg-transparent"
|
|
308
|
+
>
|
|
309
|
+
Dismiss
|
|
310
|
+
</Button>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
315
|
+
{/* State: ASSIGNED -> Start Work */}
|
|
316
|
+
{statusCode === "assigned" && (
|
|
317
|
+
<Button
|
|
318
|
+
type="button"
|
|
319
|
+
size="sm"
|
|
320
|
+
onClick={handleStartWork}
|
|
321
|
+
disabled={isPending}
|
|
322
|
+
className="bg-indigo-600 hover:bg-indigo-700 text-white gap-1.5 shadow-sm"
|
|
323
|
+
>
|
|
324
|
+
{isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />}
|
|
325
|
+
Start Work
|
|
326
|
+
</Button>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
{/* State: IN_PROGRESS -> Mark Waiting & Complete Work */}
|
|
330
|
+
{statusCode === "in_progress" && (
|
|
331
|
+
<>
|
|
332
|
+
<Button
|
|
333
|
+
type="button"
|
|
334
|
+
variant="outline"
|
|
335
|
+
size="sm"
|
|
336
|
+
onClick={() => {
|
|
337
|
+
setError(null)
|
|
338
|
+
setWaitingDialogOpen(true)
|
|
339
|
+
}}
|
|
340
|
+
disabled={isPending}
|
|
341
|
+
className="gap-1.5 border-amber-300 text-amber-800 dark:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-950/40"
|
|
342
|
+
>
|
|
343
|
+
<Pause className="h-4 w-4" />
|
|
344
|
+
Mark Waiting
|
|
345
|
+
</Button>
|
|
346
|
+
|
|
347
|
+
<Button
|
|
348
|
+
type="button"
|
|
349
|
+
size="sm"
|
|
350
|
+
onClick={() => {
|
|
351
|
+
setError(null)
|
|
352
|
+
setCompleteDialogOpen(true)
|
|
353
|
+
}}
|
|
354
|
+
disabled={isPending}
|
|
355
|
+
className="bg-emerald-600 hover:bg-emerald-700 text-white gap-1.5 shadow-sm"
|
|
356
|
+
>
|
|
357
|
+
<CheckCheck className="h-4 w-4" />
|
|
358
|
+
Complete Work
|
|
359
|
+
</Button>
|
|
360
|
+
</>
|
|
361
|
+
)}
|
|
362
|
+
|
|
363
|
+
{/* State: WAITING_DEPENDENCY -> Resume Work */}
|
|
364
|
+
{statusCode === "waiting_dependency" && (
|
|
365
|
+
<Button
|
|
366
|
+
type="button"
|
|
367
|
+
size="sm"
|
|
368
|
+
onClick={handleStartWork}
|
|
369
|
+
disabled={isPending}
|
|
370
|
+
className="bg-indigo-600 hover:bg-indigo-700 text-white gap-1.5 shadow-sm"
|
|
371
|
+
>
|
|
372
|
+
{isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <RotateCcw className="h-4 w-4" />}
|
|
373
|
+
Resume Work
|
|
374
|
+
</Button>
|
|
375
|
+
)}
|
|
376
|
+
|
|
377
|
+
{/* State: RETURNED_FOR_REWORK -> Resume Work */}
|
|
378
|
+
{statusCode === "returned_for_rework" && (
|
|
379
|
+
<>
|
|
380
|
+
<Button
|
|
381
|
+
type="button"
|
|
382
|
+
size="sm"
|
|
383
|
+
onClick={handleStartWork}
|
|
384
|
+
disabled={isPending}
|
|
385
|
+
className="bg-indigo-600 hover:bg-indigo-700 text-white gap-1.5 shadow-sm"
|
|
386
|
+
>
|
|
387
|
+
{isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Wrench className="h-4 w-4" />}
|
|
388
|
+
Resume Work
|
|
389
|
+
</Button>
|
|
390
|
+
|
|
391
|
+
<Button
|
|
392
|
+
type="button"
|
|
393
|
+
size="sm"
|
|
394
|
+
onClick={() => {
|
|
395
|
+
setError(null)
|
|
396
|
+
setCompleteDialogOpen(true)
|
|
397
|
+
}}
|
|
398
|
+
disabled={isPending}
|
|
399
|
+
className="bg-emerald-600 hover:bg-emerald-700 text-white gap-1.5 shadow-sm"
|
|
400
|
+
>
|
|
401
|
+
<CheckCheck className="h-4 w-4" />
|
|
402
|
+
Re-Complete Work
|
|
403
|
+
</Button>
|
|
404
|
+
</>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
{/* State: WORK_COMPLETE -> Sign & Submit Link and Action */}
|
|
408
|
+
{statusCode === "work_complete" && (
|
|
409
|
+
<div className="flex items-center gap-2">
|
|
410
|
+
<Button
|
|
411
|
+
type="button"
|
|
412
|
+
size="sm"
|
|
413
|
+
onClick={() => {
|
|
414
|
+
setError(null)
|
|
415
|
+
setSignDialogOpen(true)
|
|
416
|
+
}}
|
|
417
|
+
disabled={isPending}
|
|
418
|
+
className="bg-cyan-700 hover:bg-cyan-800 text-white gap-1.5 shadow-sm"
|
|
419
|
+
>
|
|
420
|
+
<FileSignature className="h-4 w-4" />
|
|
421
|
+
Sign & Submit
|
|
422
|
+
</Button>
|
|
423
|
+
<Button variant="outline" size="sm" asChild>
|
|
424
|
+
<Link href={`/maintenance/tasks/${taskId}/sign`} className="gap-1.5">
|
|
425
|
+
<ShieldCheck className="h-4 w-4 text-primary" />
|
|
426
|
+
Sign Page
|
|
427
|
+
</Link>
|
|
428
|
+
</Button>
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
431
|
+
|
|
432
|
+
{/* State: AWAITING_REVIEW -> Review Work */}
|
|
433
|
+
{statusCode === "awaiting_review" && (
|
|
434
|
+
<Button
|
|
435
|
+
type="button"
|
|
436
|
+
size="sm"
|
|
437
|
+
onClick={() => {
|
|
438
|
+
setError(null)
|
|
439
|
+
setReviewDialogOpen(true)
|
|
440
|
+
}}
|
|
441
|
+
disabled={isPending}
|
|
442
|
+
className="bg-purple-600 hover:bg-purple-700 text-white gap-1.5 shadow-sm"
|
|
443
|
+
>
|
|
444
|
+
<Eye className="h-4 w-4" />
|
|
445
|
+
Review Work
|
|
446
|
+
</Button>
|
|
447
|
+
)}
|
|
448
|
+
|
|
449
|
+
{/* State: AWAITING_RELEASE -> Release Device */}
|
|
450
|
+
{statusCode === "awaiting_release" && (
|
|
451
|
+
<Button
|
|
452
|
+
type="button"
|
|
453
|
+
size="sm"
|
|
454
|
+
onClick={() => {
|
|
455
|
+
setError(null)
|
|
456
|
+
setReleaseDialogOpen(true)
|
|
457
|
+
}}
|
|
458
|
+
disabled={isPending}
|
|
459
|
+
className="bg-emerald-600 hover:bg-emerald-700 text-white gap-1.5 shadow-sm"
|
|
460
|
+
>
|
|
461
|
+
<Sparkles className="h-4 w-4" />
|
|
462
|
+
Release Device
|
|
463
|
+
</Button>
|
|
464
|
+
)}
|
|
465
|
+
|
|
466
|
+
{/* Optional Cancel Action for Active Stages */}
|
|
467
|
+
{["draft", "assigned", "in_progress", "waiting_dependency"].includes(statusCode) && (
|
|
468
|
+
<Button
|
|
469
|
+
type="button"
|
|
470
|
+
variant="ghost"
|
|
471
|
+
size="sm"
|
|
472
|
+
onClick={() => {
|
|
473
|
+
setError(null)
|
|
474
|
+
setCancelDialogOpen(true)
|
|
475
|
+
}}
|
|
476
|
+
disabled={isPending}
|
|
477
|
+
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10 text-xs"
|
|
478
|
+
>
|
|
479
|
+
<Ban className="h-3.5 w-3.5 mr-1" />
|
|
480
|
+
Cancel Task
|
|
481
|
+
</Button>
|
|
482
|
+
)}
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
{/* ── 1. Dialog: Mark Waiting ───────────────────────────────────────── */}
|
|
486
|
+
<Dialog open={waitingDialogOpen} onOpenChange={setWaitingDialogOpen}>
|
|
487
|
+
<DialogContent className="sm:max-w-md">
|
|
488
|
+
<DialogHeader>
|
|
489
|
+
<DialogTitle>Pause Task — Waiting on Dependency</DialogTitle>
|
|
490
|
+
<DialogDescription>
|
|
491
|
+
Specify the dependency type and detailed justification for pausing maintenance work.
|
|
492
|
+
</DialogDescription>
|
|
493
|
+
</DialogHeader>
|
|
494
|
+
|
|
495
|
+
<form onSubmit={handleMarkWaiting} className="space-y-4 pt-2">
|
|
496
|
+
<div className="space-y-1.5">
|
|
497
|
+
<Label htmlFor="dependency-type">Dependency Category</Label>
|
|
498
|
+
<Select
|
|
499
|
+
value={waitingDependencyType}
|
|
500
|
+
onValueChange={(val: any) => setWaitingDependencyType(val)}
|
|
501
|
+
>
|
|
502
|
+
<SelectTrigger id="dependency-type">
|
|
503
|
+
<SelectValue placeholder="Select dependency" />
|
|
504
|
+
</SelectTrigger>
|
|
505
|
+
<SelectContent>
|
|
506
|
+
<SelectItem value="parts">Waiting for Spare Parts</SelectItem>
|
|
507
|
+
<SelectItem value="vendor">Waiting for External Vendor Service</SelectItem>
|
|
508
|
+
<SelectItem value="requester">Waiting for Clinical Department / Requester</SelectItem>
|
|
509
|
+
<SelectItem value="department_access">Waiting for Physical / Room Access</SelectItem>
|
|
510
|
+
</SelectContent>
|
|
511
|
+
</Select>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
<div className="space-y-1.5">
|
|
515
|
+
<Label htmlFor="waiting-reason">Reason & Details</Label>
|
|
516
|
+
<Textarea
|
|
517
|
+
id="waiting-reason"
|
|
518
|
+
placeholder="e.g. Awaiting delivery of power supply unit PN-4402 from OEM supplier."
|
|
519
|
+
value={waitingReason}
|
|
520
|
+
onChange={(e) => setWaitingReason(e.target.value)}
|
|
521
|
+
rows={3}
|
|
522
|
+
required
|
|
523
|
+
/>
|
|
524
|
+
</div>
|
|
525
|
+
|
|
526
|
+
<DialogFooter className="gap-2 sm:gap-0 pt-2">
|
|
527
|
+
<Button
|
|
528
|
+
type="button"
|
|
529
|
+
variant="outline"
|
|
530
|
+
onClick={() => setWaitingDialogOpen(false)}
|
|
531
|
+
disabled={isPending}
|
|
532
|
+
>
|
|
533
|
+
Cancel
|
|
534
|
+
</Button>
|
|
535
|
+
<Button type="submit" disabled={isPending} className="bg-amber-600 hover:bg-amber-700 text-white">
|
|
536
|
+
{isPending ? <Loader2 className="h-4 w-4 animate-spin mr-1.5" /> : null}
|
|
537
|
+
Confirm Pause
|
|
538
|
+
</Button>
|
|
539
|
+
</DialogFooter>
|
|
540
|
+
</form>
|
|
541
|
+
</DialogContent>
|
|
542
|
+
</Dialog>
|
|
543
|
+
|
|
544
|
+
{/* ── 2. Dialog: Complete Work ───────────────────────────────────────── */}
|
|
545
|
+
<Dialog open={completeDialogOpen} onOpenChange={setCompleteDialogOpen}>
|
|
546
|
+
<DialogContent className="sm:max-w-lg max-h-[90vh] overflow-y-auto">
|
|
547
|
+
<DialogHeader>
|
|
548
|
+
<DialogTitle>Complete Maintenance Work</DialogTitle>
|
|
549
|
+
<DialogDescription>
|
|
550
|
+
Record the technical execution details, diagnosis findings, and initial result evaluation.
|
|
551
|
+
</DialogDescription>
|
|
552
|
+
</DialogHeader>
|
|
553
|
+
|
|
554
|
+
<form onSubmit={handleCompleteWork} className="space-y-4 pt-2">
|
|
555
|
+
<div className="space-y-1.5">
|
|
556
|
+
<Label htmlFor="work-performed">
|
|
557
|
+
Work Performed <span className="text-destructive">*</span>
|
|
558
|
+
</Label>
|
|
559
|
+
<Textarea
|
|
560
|
+
id="work-performed"
|
|
561
|
+
placeholder="Describe all maintenance, calibration, tests, and repairs executed..."
|
|
562
|
+
value={workPerformed}
|
|
563
|
+
onChange={(e) => setWorkPerformed(e.target.value)}
|
|
564
|
+
rows={3}
|
|
565
|
+
required
|
|
566
|
+
/>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
570
|
+
<div className="space-y-1.5">
|
|
571
|
+
<Label htmlFor="diagnosis">Diagnosis / Initial Condition</Label>
|
|
572
|
+
<Input
|
|
573
|
+
id="diagnosis"
|
|
574
|
+
placeholder="Root technical assessment"
|
|
575
|
+
value={diagnosis}
|
|
576
|
+
onChange={(e) => setDiagnosis(e.target.value)}
|
|
577
|
+
/>
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
<div className="space-y-1.5">
|
|
581
|
+
<Label htmlFor="root-cause">Root Cause</Label>
|
|
582
|
+
<Input
|
|
583
|
+
id="root-cause"
|
|
584
|
+
placeholder="e.g. Component wear, electrical surge"
|
|
585
|
+
value={rootCause}
|
|
586
|
+
onChange={(e) => setRootCause(e.target.value)}
|
|
587
|
+
/>
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
592
|
+
<div className="space-y-1.5">
|
|
593
|
+
<Label htmlFor="final-result-code">Final Test Result</Label>
|
|
594
|
+
<Select value={finalResultCode} onValueChange={setFinalResultCode}>
|
|
595
|
+
<SelectTrigger id="final-result-code">
|
|
596
|
+
<SelectValue />
|
|
597
|
+
</SelectTrigger>
|
|
598
|
+
<SelectContent>
|
|
599
|
+
<SelectItem value="passed">Passed</SelectItem>
|
|
600
|
+
<SelectItem value="passed_with_limitations">Passed with Limitations</SelectItem>
|
|
601
|
+
<SelectItem value="failed">Failed</SelectItem>
|
|
602
|
+
<SelectItem value="no_fault_found">No Fault Found</SelectItem>
|
|
603
|
+
<SelectItem value="decommission_recommended">Decommission Recommended</SelectItem>
|
|
604
|
+
<SelectItem value="requires_external_service">Requires External Service</SelectItem>
|
|
605
|
+
</SelectContent>
|
|
606
|
+
</Select>
|
|
607
|
+
</div>
|
|
608
|
+
|
|
609
|
+
<div className="space-y-1.5">
|
|
610
|
+
<Label htmlFor="device-status-code">Proposed Device Status</Label>
|
|
611
|
+
<Select value={finalDeviceStatusCode} onValueChange={setFinalDeviceStatusCode}>
|
|
612
|
+
<SelectTrigger id="device-status-code">
|
|
613
|
+
<SelectValue />
|
|
614
|
+
</SelectTrigger>
|
|
615
|
+
<SelectContent>
|
|
616
|
+
<SelectItem value="awaiting_release">Awaiting Release</SelectItem>
|
|
617
|
+
<SelectItem value="operational">Operational</SelectItem>
|
|
618
|
+
<SelectItem value="operational_with_limitations">Limited Use</SelectItem>
|
|
619
|
+
<SelectItem value="under_repair">Under Repair</SelectItem>
|
|
620
|
+
<SelectItem value="out_of_service">Out of Service</SelectItem>
|
|
621
|
+
</SelectContent>
|
|
622
|
+
</Select>
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
<div className="space-y-1.5">
|
|
627
|
+
<Label htmlFor="findings">Key Findings & Observations</Label>
|
|
628
|
+
<Input
|
|
629
|
+
id="findings"
|
|
630
|
+
placeholder="Observed test parameters, physical checks"
|
|
631
|
+
value={findings}
|
|
632
|
+
onChange={(e) => setFindings(e.target.value)}
|
|
633
|
+
/>
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
<div className="space-y-1.5">
|
|
637
|
+
<Label htmlFor="recommendations">Recommendations for Next Service</Label>
|
|
638
|
+
<Input
|
|
639
|
+
id="recommendations"
|
|
640
|
+
placeholder="Preventive maintenance advice, parts to monitor"
|
|
641
|
+
value={recommendations}
|
|
642
|
+
onChange={(e) => setRecommendations(e.target.value)}
|
|
643
|
+
/>
|
|
644
|
+
</div>
|
|
645
|
+
|
|
646
|
+
<DialogFooter className="gap-2 sm:gap-0 pt-3">
|
|
647
|
+
<Button
|
|
648
|
+
type="button"
|
|
649
|
+
variant="outline"
|
|
650
|
+
onClick={() => setCompleteDialogOpen(false)}
|
|
651
|
+
disabled={isPending}
|
|
652
|
+
>
|
|
653
|
+
Cancel
|
|
654
|
+
</Button>
|
|
655
|
+
<Button type="submit" disabled={isPending} className="bg-emerald-600 hover:bg-emerald-700 text-white">
|
|
656
|
+
{isPending ? <Loader2 className="h-4 w-4 animate-spin mr-1.5" /> : null}
|
|
657
|
+
Save & Complete Work
|
|
658
|
+
</Button>
|
|
659
|
+
</DialogFooter>
|
|
660
|
+
</form>
|
|
661
|
+
</DialogContent>
|
|
662
|
+
</Dialog>
|
|
663
|
+
|
|
664
|
+
{/* ── 3. Dialog: Quick Sign & Submit ─────────────────────────────────── */}
|
|
665
|
+
<Dialog open={signDialogOpen} onOpenChange={setSignDialogOpen}>
|
|
666
|
+
<DialogContent className="sm:max-w-md">
|
|
667
|
+
<DialogHeader>
|
|
668
|
+
<DialogTitle className="flex items-center gap-2">
|
|
669
|
+
<ShieldCheck className="h-5 w-5 text-primary" />
|
|
670
|
+
Sign & Submit Maintenance Record
|
|
671
|
+
</DialogTitle>
|
|
672
|
+
<DialogDescription>
|
|
673
|
+
Electronically attest that the recorded maintenance work is accurate and complete.
|
|
674
|
+
</DialogDescription>
|
|
675
|
+
</DialogHeader>
|
|
676
|
+
|
|
677
|
+
<form onSubmit={handleSignAndSubmit} className="space-y-4 pt-2">
|
|
678
|
+
<blockquote className="rounded-md border-l-2 border-primary/50 bg-muted/40 p-3 text-xs italic text-foreground/90">
|
|
679
|
+
"I confirm that I performed the recorded maintenance/inspection and the details entered are accurate."
|
|
680
|
+
</blockquote>
|
|
681
|
+
|
|
682
|
+
<div className="space-y-1.5">
|
|
683
|
+
<Label htmlFor="sign-comments">Signer Comments (Optional)</Label>
|
|
684
|
+
<Textarea
|
|
685
|
+
id="sign-comments"
|
|
686
|
+
placeholder="Additional notes or context for the technical reviewer..."
|
|
687
|
+
value={signComments}
|
|
688
|
+
onChange={(e) => setSignComments(e.target.value)}
|
|
689
|
+
rows={2}
|
|
690
|
+
/>
|
|
691
|
+
</div>
|
|
692
|
+
|
|
693
|
+
<div className="space-y-1.5">
|
|
694
|
+
<Label htmlFor="sign-password">
|
|
695
|
+
Re-Authenticate Password <span className="text-destructive">*</span>
|
|
696
|
+
</Label>
|
|
697
|
+
<Input
|
|
698
|
+
id="sign-password"
|
|
699
|
+
type="password"
|
|
700
|
+
placeholder="Enter your account password"
|
|
701
|
+
value={signPassword}
|
|
702
|
+
onChange={(e) => setSignPassword(e.target.value)}
|
|
703
|
+
required
|
|
704
|
+
autoFocus
|
|
705
|
+
/>
|
|
706
|
+
</div>
|
|
707
|
+
|
|
708
|
+
<DialogFooter className="gap-2 sm:gap-0 pt-2">
|
|
709
|
+
<Button
|
|
710
|
+
type="button"
|
|
711
|
+
variant="outline"
|
|
712
|
+
onClick={() => setSignDialogOpen(false)}
|
|
713
|
+
disabled={isPending}
|
|
714
|
+
>
|
|
715
|
+
Cancel
|
|
716
|
+
</Button>
|
|
717
|
+
<Button type="submit" disabled={isPending} className="bg-cyan-700 hover:bg-cyan-800 text-white">
|
|
718
|
+
{isPending ? <Loader2 className="h-4 w-4 animate-spin mr-1.5" /> : <FileSignature className="h-4 w-4 mr-1.5" />}
|
|
719
|
+
Sign Record
|
|
720
|
+
</Button>
|
|
721
|
+
</DialogFooter>
|
|
722
|
+
</form>
|
|
723
|
+
</DialogContent>
|
|
724
|
+
</Dialog>
|
|
725
|
+
|
|
726
|
+
{/* ── 4. Dialog: Technical Review ────────────────────────────────────── */}
|
|
727
|
+
<Dialog open={reviewDialogOpen} onOpenChange={setReviewDialogOpen}>
|
|
728
|
+
<DialogContent className="sm:max-w-md">
|
|
729
|
+
<DialogHeader>
|
|
730
|
+
<DialogTitle className="flex items-center gap-2">
|
|
731
|
+
<Eye className="h-5 w-5 text-purple-600" />
|
|
732
|
+
Technical Peer Review
|
|
733
|
+
</DialogTitle>
|
|
734
|
+
<DialogDescription>
|
|
735
|
+
Review checklist findings, test measurements, and parts used to approve or return for rework.
|
|
736
|
+
</DialogDescription>
|
|
737
|
+
</DialogHeader>
|
|
738
|
+
|
|
739
|
+
<form onSubmit={handleReviewWork} className="space-y-4 pt-2">
|
|
740
|
+
<div className="space-y-1.5">
|
|
741
|
+
<Label htmlFor="review-decision">Review Decision</Label>
|
|
742
|
+
<Select
|
|
743
|
+
value={reviewDecision}
|
|
744
|
+
onValueChange={(val: any) => setReviewDecision(val)}
|
|
745
|
+
>
|
|
746
|
+
<SelectTrigger id="review-decision">
|
|
747
|
+
<SelectValue />
|
|
748
|
+
</SelectTrigger>
|
|
749
|
+
<SelectContent>
|
|
750
|
+
<SelectItem value="approve">Approve Work (Proceed to Release)</SelectItem>
|
|
751
|
+
<SelectItem value="reject">Reject & Return for Rework</SelectItem>
|
|
752
|
+
</SelectContent>
|
|
753
|
+
</Select>
|
|
754
|
+
</div>
|
|
755
|
+
|
|
756
|
+
{reviewDecision === "approve" ? (
|
|
757
|
+
<>
|
|
758
|
+
<blockquote className="rounded-md border-l-2 border-purple-500 bg-purple-50/40 dark:bg-purple-950/20 p-3 text-xs italic text-foreground/90">
|
|
759
|
+
"I have reviewed the findings, checklist, test results, and parts used, and verify technical completeness."
|
|
760
|
+
</blockquote>
|
|
761
|
+
<div className="space-y-1.5">
|
|
762
|
+
<Label htmlFor="review-notes">Review Notes (Optional)</Label>
|
|
763
|
+
<Textarea
|
|
764
|
+
id="review-notes"
|
|
765
|
+
placeholder="Comments regarding technical verification..."
|
|
766
|
+
value={reviewNotes}
|
|
767
|
+
onChange={(e) => setReviewNotes(e.target.value)}
|
|
768
|
+
rows={2}
|
|
769
|
+
/>
|
|
770
|
+
</div>
|
|
771
|
+
</>
|
|
772
|
+
) : (
|
|
773
|
+
<div className="space-y-1.5">
|
|
774
|
+
<Label htmlFor="rejection-reason">
|
|
775
|
+
Rework Justification <span className="text-destructive">*</span>
|
|
776
|
+
</Label>
|
|
777
|
+
<Textarea
|
|
778
|
+
id="rejection-reason"
|
|
779
|
+
placeholder="Explain why this work was rejected and what specific corrections are needed..."
|
|
780
|
+
value={rejectionReason}
|
|
781
|
+
onChange={(e) => setRejectionReason(e.target.value)}
|
|
782
|
+
rows={3}
|
|
783
|
+
required
|
|
784
|
+
/>
|
|
785
|
+
</div>
|
|
786
|
+
)}
|
|
787
|
+
|
|
788
|
+
<div className="space-y-1.5">
|
|
789
|
+
<Label htmlFor="review-password">
|
|
790
|
+
Re-Authenticate Password <span className="text-destructive">*</span>
|
|
791
|
+
</Label>
|
|
792
|
+
<Input
|
|
793
|
+
id="review-password"
|
|
794
|
+
type="password"
|
|
795
|
+
placeholder="Enter password to sign review"
|
|
796
|
+
value={reviewPassword}
|
|
797
|
+
onChange={(e) => setReviewPassword(e.target.value)}
|
|
798
|
+
required
|
|
799
|
+
/>
|
|
800
|
+
</div>
|
|
801
|
+
|
|
802
|
+
<DialogFooter className="gap-2 sm:gap-0 pt-2">
|
|
803
|
+
<Button
|
|
804
|
+
type="button"
|
|
805
|
+
variant="outline"
|
|
806
|
+
onClick={() => setReviewDialogOpen(false)}
|
|
807
|
+
disabled={isPending}
|
|
808
|
+
>
|
|
809
|
+
Cancel
|
|
810
|
+
</Button>
|
|
811
|
+
<Button
|
|
812
|
+
type="submit"
|
|
813
|
+
disabled={isPending}
|
|
814
|
+
className={
|
|
815
|
+
reviewDecision === "approve"
|
|
816
|
+
? "bg-purple-600 hover:bg-purple-700 text-white"
|
|
817
|
+
: "bg-rose-600 hover:bg-rose-700 text-white"
|
|
818
|
+
}
|
|
819
|
+
>
|
|
820
|
+
{isPending ? <Loader2 className="h-4 w-4 animate-spin mr-1.5" /> : null}
|
|
821
|
+
{reviewDecision === "approve" ? "Sign & Approve" : "Return for Rework"}
|
|
822
|
+
</Button>
|
|
823
|
+
</DialogFooter>
|
|
824
|
+
</form>
|
|
825
|
+
</DialogContent>
|
|
826
|
+
</Dialog>
|
|
827
|
+
|
|
828
|
+
{/* ── 5. Dialog: Release Device ──────────────────────────────────────── */}
|
|
829
|
+
<Dialog open={releaseDialogOpen} onOpenChange={setReleaseDialogOpen}>
|
|
830
|
+
<DialogContent className="sm:max-w-md">
|
|
831
|
+
<DialogHeader>
|
|
832
|
+
<DialogTitle className="flex items-center gap-2">
|
|
833
|
+
<Sparkles className="h-5 w-5 text-emerald-600" />
|
|
834
|
+
Authorize Device Release
|
|
835
|
+
</DialogTitle>
|
|
836
|
+
<DialogDescription>
|
|
837
|
+
Authorize return of this medical device to active clinical service and finalize maintenance records.
|
|
838
|
+
</DialogDescription>
|
|
839
|
+
</DialogHeader>
|
|
840
|
+
|
|
841
|
+
<form onSubmit={handleReleaseDevice} className="space-y-4 pt-2">
|
|
842
|
+
<blockquote className="rounded-md border-l-2 border-emerald-500 bg-emerald-50/40 dark:bg-emerald-950/20 p-3 text-xs italic text-foreground/90">
|
|
843
|
+
"I authorize the release of this medical device for clinical use with the specified status."
|
|
844
|
+
</blockquote>
|
|
845
|
+
|
|
846
|
+
<div className="space-y-1.5">
|
|
847
|
+
<Label htmlFor="release-status">Final Clinical Device Status</Label>
|
|
848
|
+
<Select
|
|
849
|
+
value={releaseStatus}
|
|
850
|
+
onValueChange={(val: any) => setReleaseStatus(val)}
|
|
851
|
+
>
|
|
852
|
+
<SelectTrigger id="release-status">
|
|
853
|
+
<SelectValue />
|
|
854
|
+
</SelectTrigger>
|
|
855
|
+
<SelectContent>
|
|
856
|
+
<SelectItem value="operational">Operational (Full Clinical Use)</SelectItem>
|
|
857
|
+
<SelectItem value="operational_with_limitations">Operational with Limitations</SelectItem>
|
|
858
|
+
</SelectContent>
|
|
859
|
+
</Select>
|
|
860
|
+
</div>
|
|
861
|
+
|
|
862
|
+
<div className="space-y-1.5">
|
|
863
|
+
<Label htmlFor="release-notes">Release Notes / Clinical Guidance (Optional)</Label>
|
|
864
|
+
<Textarea
|
|
865
|
+
id="release-notes"
|
|
866
|
+
placeholder="Guidance for clinical operators upon return..."
|
|
867
|
+
value={releaseNotes}
|
|
868
|
+
onChange={(e) => setReleaseNotes(e.target.value)}
|
|
869
|
+
rows={2}
|
|
870
|
+
/>
|
|
871
|
+
</div>
|
|
872
|
+
|
|
873
|
+
<div className="space-y-1.5">
|
|
874
|
+
<Label htmlFor="release-password">
|
|
875
|
+
Re-Authenticate Password <span className="text-destructive">*</span>
|
|
876
|
+
</Label>
|
|
877
|
+
<Input
|
|
878
|
+
id="release-password"
|
|
879
|
+
type="password"
|
|
880
|
+
placeholder="Enter password to authorize release"
|
|
881
|
+
value={releasePassword}
|
|
882
|
+
onChange={(e) => setReleasePassword(e.target.value)}
|
|
883
|
+
required
|
|
884
|
+
/>
|
|
885
|
+
</div>
|
|
886
|
+
|
|
887
|
+
<DialogFooter className="gap-2 sm:gap-0 pt-2">
|
|
888
|
+
<Button
|
|
889
|
+
type="button"
|
|
890
|
+
variant="outline"
|
|
891
|
+
onClick={() => setReleaseDialogOpen(false)}
|
|
892
|
+
disabled={isPending}
|
|
893
|
+
>
|
|
894
|
+
Cancel
|
|
895
|
+
</Button>
|
|
896
|
+
<Button type="submit" disabled={isPending} className="bg-emerald-600 hover:bg-emerald-700 text-white">
|
|
897
|
+
{isPending ? <Loader2 className="h-4 w-4 animate-spin mr-1.5" /> : null}
|
|
898
|
+
Sign & Release Device
|
|
899
|
+
</Button>
|
|
900
|
+
</DialogFooter>
|
|
901
|
+
</form>
|
|
902
|
+
</DialogContent>
|
|
903
|
+
</Dialog>
|
|
904
|
+
|
|
905
|
+
{/* ── 6. Dialog: Cancel Task ─────────────────────────────────────────── */}
|
|
906
|
+
<Dialog open={cancelDialogOpen} onOpenChange={setCancelDialogOpen}>
|
|
907
|
+
<DialogContent className="sm:max-w-md">
|
|
908
|
+
<DialogHeader>
|
|
909
|
+
<DialogTitle className="flex items-center gap-2 text-destructive">
|
|
910
|
+
<Ban className="h-5 w-5" />
|
|
911
|
+
Cancel Maintenance Task
|
|
912
|
+
</DialogTitle>
|
|
913
|
+
<DialogDescription>
|
|
914
|
+
Are you sure you want to cancel this maintenance work order? This action is audited and cannot be undone.
|
|
915
|
+
</DialogDescription>
|
|
916
|
+
</DialogHeader>
|
|
917
|
+
|
|
918
|
+
<form onSubmit={handleCancelTask} className="space-y-4 pt-2">
|
|
919
|
+
<div className="space-y-1.5">
|
|
920
|
+
<Label htmlFor="cancel-reason">
|
|
921
|
+
Cancellation Reason <span className="text-destructive">*</span>
|
|
922
|
+
</Label>
|
|
923
|
+
<Textarea
|
|
924
|
+
id="cancel-reason"
|
|
925
|
+
placeholder="Provide a detailed explanation for cancelling this task (min 10 characters)..."
|
|
926
|
+
value={cancellationReason}
|
|
927
|
+
onChange={(e) => setCancellationReason(e.target.value)}
|
|
928
|
+
rows={3}
|
|
929
|
+
required
|
|
930
|
+
/>
|
|
931
|
+
</div>
|
|
932
|
+
|
|
933
|
+
<DialogFooter className="gap-2 sm:gap-0 pt-2">
|
|
934
|
+
<Button
|
|
935
|
+
type="button"
|
|
936
|
+
variant="outline"
|
|
937
|
+
onClick={() => setCancelDialogOpen(false)}
|
|
938
|
+
disabled={isPending}
|
|
939
|
+
>
|
|
940
|
+
Keep Task
|
|
941
|
+
</Button>
|
|
942
|
+
<Button type="submit" disabled={isPending} variant="destructive">
|
|
943
|
+
{isPending ? <Loader2 className="h-4 w-4 animate-spin mr-1.5" /> : null}
|
|
944
|
+
Confirm Cancellation
|
|
945
|
+
</Button>
|
|
946
|
+
</DialogFooter>
|
|
947
|
+
</form>
|
|
948
|
+
</DialogContent>
|
|
949
|
+
</Dialog>
|
|
950
|
+
</div>
|
|
951
|
+
)
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
export default TaskActionBar
|
|
955
|
+
|