@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,281 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useTransition } from 'react';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
6
|
+
import { Progress } from '@/components/ui/progress';
|
|
7
|
+
import { DepartmentVisitSheet } from './department-visit-sheet';
|
|
8
|
+
import { completeRound } from '@/lib/actions/engineer-rounds';
|
|
9
|
+
import { toast } from 'sonner';
|
|
10
|
+
import { useRouter } from 'next/navigation';
|
|
11
|
+
import {
|
|
12
|
+
CheckCircle2, Circle, Clock, AlertTriangle, PartyPopper,
|
|
13
|
+
FileText, Loader2, SkipForward,
|
|
14
|
+
} from 'lucide-react';
|
|
15
|
+
import { cn } from '@/lib/utils';
|
|
16
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
17
|
+
|
|
18
|
+
interface Visit {
|
|
19
|
+
id: string;
|
|
20
|
+
departmentId: string;
|
|
21
|
+
departmentName: string;
|
|
22
|
+
sequence: number;
|
|
23
|
+
status: string;
|
|
24
|
+
startedAt: string | null;
|
|
25
|
+
completedAt: string | null;
|
|
26
|
+
ticketCount: number;
|
|
27
|
+
notes: {
|
|
28
|
+
id: string;
|
|
29
|
+
noteText: string;
|
|
30
|
+
deviceMentions: any[];
|
|
31
|
+
author?: { id: string; fullName: string };
|
|
32
|
+
}[];
|
|
33
|
+
department: { id: string; name: string; code: string };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface RoundData {
|
|
37
|
+
id: string;
|
|
38
|
+
roundDate: string;
|
|
39
|
+
shiftType: string;
|
|
40
|
+
status: string;
|
|
41
|
+
startedAt: string | null;
|
|
42
|
+
completedAt: string | null;
|
|
43
|
+
totalDepartments: number;
|
|
44
|
+
completedDepartments: number;
|
|
45
|
+
totalIssuesReported: number;
|
|
46
|
+
totalNotes: number;
|
|
47
|
+
hospitalId: string;
|
|
48
|
+
hospital: { id: string; name: string };
|
|
49
|
+
engineer: { id: string; fullName: string };
|
|
50
|
+
departmentVisits: Visit[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface RoundDepartmentListProps {
|
|
54
|
+
round: RoundData;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function RoundDepartmentList({ round: initialRound }: RoundDepartmentListProps) {
|
|
58
|
+
const router = useRouter();
|
|
59
|
+
const [selectedVisit, setSelectedVisit] = useState<Visit | null>(null);
|
|
60
|
+
const [sheetOpen, setSheetOpen] = useState(false);
|
|
61
|
+
const [isPending, startTransition] = useTransition();
|
|
62
|
+
|
|
63
|
+
const round = initialRound;
|
|
64
|
+
const visits = round.departmentVisits;
|
|
65
|
+
const completedCount = visits.filter(v => v.status === 'completed').length;
|
|
66
|
+
const progress = visits.length > 0 ? (completedCount / visits.length) * 100 : 0;
|
|
67
|
+
const allCompleted = completedCount === visits.length;
|
|
68
|
+
const isCompleted = round.status === 'completed';
|
|
69
|
+
|
|
70
|
+
const handleVisitClick = (visit: Visit) => {
|
|
71
|
+
setSelectedVisit(visit);
|
|
72
|
+
setSheetOpen(true);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleCompleteRound = () => {
|
|
76
|
+
startTransition(async () => {
|
|
77
|
+
const result = await completeRound({ roundId: round.id });
|
|
78
|
+
if (result.success) {
|
|
79
|
+
toast.success('Round completed successfully!');
|
|
80
|
+
router.refresh();
|
|
81
|
+
} else {
|
|
82
|
+
toast.error(result.error || 'Failed to complete round');
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const formatTime = (ts: string | null) => {
|
|
88
|
+
if (!ts) return null;
|
|
89
|
+
return new Date(ts).toLocaleTimeString('en-US', {
|
|
90
|
+
hour: '2-digit',
|
|
91
|
+
minute: '2-digit',
|
|
92
|
+
hour12: true,
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="space-y-4">
|
|
98
|
+
{/* Progress header */}
|
|
99
|
+
<Card>
|
|
100
|
+
<CardContent className="pt-6">
|
|
101
|
+
<div className="flex items-center justify-between mb-2">
|
|
102
|
+
<span className="text-sm font-medium">
|
|
103
|
+
{completedCount} / {visits.length} Departments
|
|
104
|
+
</span>
|
|
105
|
+
<span className="text-sm text-muted-foreground">
|
|
106
|
+
{Math.round(progress)}%
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
<Progress value={progress} className="h-2" />
|
|
110
|
+
{round.startedAt && (
|
|
111
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
112
|
+
Started {formatTime(round.startedAt)} · {round.shiftType} shift
|
|
113
|
+
</p>
|
|
114
|
+
)}
|
|
115
|
+
</CardContent>
|
|
116
|
+
</Card>
|
|
117
|
+
|
|
118
|
+
{/* Department List (To-Do Style) */}
|
|
119
|
+
<div className="space-y-2">
|
|
120
|
+
{visits.map((visit) => {
|
|
121
|
+
const isVisitCompleted = visit.status === 'completed';
|
|
122
|
+
const isSkipped = visit.status === 'skipped';
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<button
|
|
126
|
+
key={visit.id}
|
|
127
|
+
onClick={() => handleVisitClick(visit)}
|
|
128
|
+
className={cn(
|
|
129
|
+
'w-full flex items-start gap-3 p-4 rounded-lg border transition-all text-left',
|
|
130
|
+
isVisitCompleted
|
|
131
|
+
? 'bg-green-50 border-green-200 dark:bg-green-950/20 dark:border-green-900/40'
|
|
132
|
+
: isSkipped
|
|
133
|
+
? 'bg-muted/30 border-muted opacity-60'
|
|
134
|
+
: 'bg-card border-border hover:border-primary/40 hover:shadow-sm active:scale-[0.99]',
|
|
135
|
+
)}
|
|
136
|
+
disabled={isCompleted}
|
|
137
|
+
>
|
|
138
|
+
{/* Status Icon */}
|
|
139
|
+
<div className="mt-0.5 shrink-0">
|
|
140
|
+
{isVisitCompleted ? (
|
|
141
|
+
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
|
142
|
+
) : isSkipped ? (
|
|
143
|
+
<SkipForward className="h-5 w-5 text-muted-foreground" />
|
|
144
|
+
) : (
|
|
145
|
+
<Circle className="h-5 w-5 text-muted-foreground/40" />
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Content */}
|
|
150
|
+
<div className="flex-1 min-w-0">
|
|
151
|
+
<div className="flex items-center justify-between gap-2">
|
|
152
|
+
<span className={cn(
|
|
153
|
+
'font-medium',
|
|
154
|
+
isVisitCompleted && 'text-green-700 dark:text-green-400',
|
|
155
|
+
isSkipped && 'line-through text-muted-foreground',
|
|
156
|
+
)}>
|
|
157
|
+
{visit.departmentName}
|
|
158
|
+
</span>
|
|
159
|
+
{isVisitCompleted && visit.completedAt && (
|
|
160
|
+
<span className="text-xs text-muted-foreground shrink-0">
|
|
161
|
+
{formatTime(visit.completedAt)}
|
|
162
|
+
</span>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* Visit details */}
|
|
167
|
+
{isVisitCompleted && (
|
|
168
|
+
<div className="flex flex-wrap items-center gap-2 mt-1">
|
|
169
|
+
{visit.notes.length > 0 && (
|
|
170
|
+
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
|
|
171
|
+
<FileText className="h-3 w-3" />
|
|
172
|
+
{visit.notes.length} note{visit.notes.length !== 1 ? 's' : ''}
|
|
173
|
+
</span>
|
|
174
|
+
)}
|
|
175
|
+
{visit.ticketCount > 0 && (
|
|
176
|
+
<span className="inline-flex items-center gap-1 text-xs text-yellow-600 dark:text-yellow-400">
|
|
177
|
+
<AlertTriangle className="h-3 w-3" />
|
|
178
|
+
{visit.ticketCount} issue{visit.ticketCount !== 1 ? 's' : ''}
|
|
179
|
+
</span>
|
|
180
|
+
)}
|
|
181
|
+
{visit.notes.length === 0 && visit.ticketCount === 0 && (
|
|
182
|
+
<span className="text-xs text-green-600/70 dark:text-green-500/70">
|
|
183
|
+
No issues
|
|
184
|
+
</span>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{/* First note preview */}
|
|
190
|
+
{visit.notes.length > 0 && (
|
|
191
|
+
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">
|
|
192
|
+
"{visit.notes[0].noteText}"
|
|
193
|
+
</p>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</button>
|
|
197
|
+
);
|
|
198
|
+
})}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{/* Complete Round Button */}
|
|
202
|
+
{!isCompleted && (
|
|
203
|
+
<div className="pt-2">
|
|
204
|
+
{allCompleted ? (
|
|
205
|
+
<Card className="border-green-200 bg-green-50 dark:border-green-900/40 dark:bg-green-950/20">
|
|
206
|
+
<CardContent className="pt-6 text-center space-y-3">
|
|
207
|
+
<PartyPopper className="h-8 w-8 mx-auto text-green-500" />
|
|
208
|
+
<div>
|
|
209
|
+
<h3 className="font-semibold text-green-700 dark:text-green-400">
|
|
210
|
+
All departments visited!
|
|
211
|
+
</h3>
|
|
212
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
213
|
+
{round.totalIssuesReported} issue{round.totalIssuesReported !== 1 ? 's' : ''} reported · {round.totalNotes} note{round.totalNotes !== 1 ? 's' : ''}
|
|
214
|
+
</p>
|
|
215
|
+
</div>
|
|
216
|
+
<Button
|
|
217
|
+
size="lg"
|
|
218
|
+
className="w-full gap-2"
|
|
219
|
+
onClick={handleCompleteRound}
|
|
220
|
+
disabled={isPending}
|
|
221
|
+
>
|
|
222
|
+
{isPending ? (
|
|
223
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
224
|
+
) : (
|
|
225
|
+
<CheckCircle2 className="h-5 w-5" />
|
|
226
|
+
)}
|
|
227
|
+
Complete Round
|
|
228
|
+
</Button>
|
|
229
|
+
</CardContent>
|
|
230
|
+
</Card>
|
|
231
|
+
) : (
|
|
232
|
+
<Button
|
|
233
|
+
variant="outline"
|
|
234
|
+
size="lg"
|
|
235
|
+
className="w-full gap-2"
|
|
236
|
+
onClick={handleCompleteRound}
|
|
237
|
+
disabled={isPending}
|
|
238
|
+
>
|
|
239
|
+
{isPending ? (
|
|
240
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
241
|
+
) : (
|
|
242
|
+
<CheckCircle2 className="h-5 w-5" />
|
|
243
|
+
)}
|
|
244
|
+
Complete Round ({completedCount}/{visits.length} done)
|
|
245
|
+
</Button>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
|
|
250
|
+
{/* Completed Summary */}
|
|
251
|
+
{isCompleted && (
|
|
252
|
+
<Card className="border-green-200 bg-green-50 dark:border-green-900/40 dark:bg-green-950/20">
|
|
253
|
+
<CardContent className="pt-6 text-center">
|
|
254
|
+
<CheckCircle2 className="h-8 w-8 mx-auto text-green-500 mb-2" />
|
|
255
|
+
<h3 className="font-semibold text-green-700 dark:text-green-400">
|
|
256
|
+
Round Completed
|
|
257
|
+
</h3>
|
|
258
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
259
|
+
{completedCount}/{visits.length} departments · {round.totalIssuesReported} issues · {round.totalNotes} notes
|
|
260
|
+
</p>
|
|
261
|
+
{round.startedAt && round.completedAt && (
|
|
262
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
263
|
+
{formatTime(round.startedAt)} — {formatTime(round.completedAt)}
|
|
264
|
+
</p>
|
|
265
|
+
)}
|
|
266
|
+
</CardContent>
|
|
267
|
+
</Card>
|
|
268
|
+
)}
|
|
269
|
+
|
|
270
|
+
{/* Department Visit Sheet */}
|
|
271
|
+
<DepartmentVisitSheet
|
|
272
|
+
open={sheetOpen}
|
|
273
|
+
onOpenChange={setSheetOpen}
|
|
274
|
+
visit={selectedVisit}
|
|
275
|
+
roundId={round.id}
|
|
276
|
+
hospitalId={round.hospitalId}
|
|
277
|
+
onCompleted={() => router.refresh()}
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useTransition } from 'react';
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { RoundCalendar } from './round-calendar';
|
|
6
|
+
import { getRoundCalendarData } from '@/lib/actions/engineer-rounds';
|
|
7
|
+
import {
|
|
8
|
+
CheckCircle2, XCircle, Clock, UserX, Loader2, AlertTriangle, FileText,
|
|
9
|
+
} from 'lucide-react';
|
|
10
|
+
import { cn } from '@/lib/utils';
|
|
11
|
+
import Link from 'next/link';
|
|
12
|
+
|
|
13
|
+
interface RoundHistoryClientProps {
|
|
14
|
+
initialHistory: any[];
|
|
15
|
+
initialCalendar: any;
|
|
16
|
+
initialCompliance: any;
|
|
17
|
+
isManager: boolean;
|
|
18
|
+
currentUserId: string;
|
|
19
|
+
pagination: any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function RoundHistoryClient({
|
|
23
|
+
initialHistory,
|
|
24
|
+
initialCalendar,
|
|
25
|
+
initialCompliance,
|
|
26
|
+
isManager,
|
|
27
|
+
currentUserId,
|
|
28
|
+
pagination,
|
|
29
|
+
}: RoundHistoryClientProps) {
|
|
30
|
+
const [calendarData, setCalendarData] = useState(initialCalendar);
|
|
31
|
+
const [isPending, startTransition] = useTransition();
|
|
32
|
+
|
|
33
|
+
const handleMonthChange = (month: number, year: number) => {
|
|
34
|
+
startTransition(async () => {
|
|
35
|
+
const result = await getRoundCalendarData({
|
|
36
|
+
engineerId: currentUserId,
|
|
37
|
+
month,
|
|
38
|
+
year,
|
|
39
|
+
});
|
|
40
|
+
if (result.success) {
|
|
41
|
+
setCalendarData(result.data);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const statusIcons: Record<string, any> = {
|
|
47
|
+
completed: <CheckCircle2 className="h-4 w-4 text-green-500" />,
|
|
48
|
+
in_progress: <Clock className="h-4 w-4 text-blue-500 animate-pulse" />,
|
|
49
|
+
missed: <XCircle className="h-4 w-4 text-red-500" />,
|
|
50
|
+
scheduled: <Clock className="h-4 w-4 text-muted-foreground" />,
|
|
51
|
+
cancelled: <XCircle className="h-4 w-4 text-muted-foreground" />,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const formatTime = (ts: string | null) => {
|
|
55
|
+
if (!ts) return '';
|
|
56
|
+
return new Date(ts).toLocaleTimeString('en-US', {
|
|
57
|
+
hour: '2-digit', minute: '2-digit', hour12: true,
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="space-y-6">
|
|
63
|
+
{/* Calendar */}
|
|
64
|
+
{calendarData && (
|
|
65
|
+
<Card>
|
|
66
|
+
<CardHeader>
|
|
67
|
+
<CardTitle className="text-base">Round Activity</CardTitle>
|
|
68
|
+
</CardHeader>
|
|
69
|
+
<CardContent className="relative">
|
|
70
|
+
{isPending && (
|
|
71
|
+
<div className="absolute inset-0 bg-background/50 flex items-center justify-center z-10 rounded-lg">
|
|
72
|
+
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
<RoundCalendar
|
|
76
|
+
month={calendarData.month}
|
|
77
|
+
year={calendarData.year}
|
|
78
|
+
calendarDays={calendarData.calendarDays}
|
|
79
|
+
summary={calendarData.summary}
|
|
80
|
+
onMonthChange={handleMonthChange}
|
|
81
|
+
/>
|
|
82
|
+
</CardContent>
|
|
83
|
+
</Card>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{/* Manager: Team Compliance */}
|
|
87
|
+
{isManager && initialCompliance && (
|
|
88
|
+
<Card>
|
|
89
|
+
<CardHeader>
|
|
90
|
+
<CardTitle className="text-base">Team Compliance Today</CardTitle>
|
|
91
|
+
</CardHeader>
|
|
92
|
+
<CardContent>
|
|
93
|
+
{/* Summary Stats */}
|
|
94
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4">
|
|
95
|
+
<div className="rounded-lg bg-green-50 dark:bg-green-950/20 p-3 text-center">
|
|
96
|
+
<p className="text-2xl font-bold text-green-600 dark:text-green-400">
|
|
97
|
+
{initialCompliance.summary.completed}
|
|
98
|
+
</p>
|
|
99
|
+
<p className="text-xs text-muted-foreground">Completed</p>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="rounded-lg bg-blue-50 dark:bg-blue-950/20 p-3 text-center">
|
|
102
|
+
<p className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
|
103
|
+
{initialCompliance.summary.inProgress}
|
|
104
|
+
</p>
|
|
105
|
+
<p className="text-xs text-muted-foreground">In Progress</p>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="rounded-lg bg-red-50 dark:bg-red-950/20 p-3 text-center">
|
|
108
|
+
<p className="text-2xl font-bold text-red-600 dark:text-red-400">
|
|
109
|
+
{initialCompliance.summary.missed}
|
|
110
|
+
</p>
|
|
111
|
+
<p className="text-xs text-muted-foreground">Missed</p>
|
|
112
|
+
</div>
|
|
113
|
+
<div className="rounded-lg bg-muted p-3 text-center">
|
|
114
|
+
<p className="text-2xl font-bold">
|
|
115
|
+
{initialCompliance.summary.complianceRate}%
|
|
116
|
+
</p>
|
|
117
|
+
<p className="text-xs text-muted-foreground">Compliance</p>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Engineer list */}
|
|
122
|
+
<div className="space-y-1.5">
|
|
123
|
+
{initialCompliance.team.map((member: any) => (
|
|
124
|
+
<div
|
|
125
|
+
key={member.engineerId}
|
|
126
|
+
className="flex items-center gap-3 py-2 px-3 rounded-md hover:bg-muted/50"
|
|
127
|
+
>
|
|
128
|
+
{member.status === 'completed' ? (
|
|
129
|
+
<CheckCircle2 className="h-4 w-4 text-green-500 shrink-0" />
|
|
130
|
+
) : member.status === 'in_progress' ? (
|
|
131
|
+
<Clock className="h-4 w-4 text-blue-500 shrink-0" />
|
|
132
|
+
) : member.status === 'missed' ? (
|
|
133
|
+
<XCircle className="h-4 w-4 text-red-500 shrink-0" />
|
|
134
|
+
) : member.status === 'off_duty' ? (
|
|
135
|
+
<UserX className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
136
|
+
) : (
|
|
137
|
+
<Clock className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
138
|
+
)}
|
|
139
|
+
<div className="flex-1 min-w-0">
|
|
140
|
+
<p className="text-sm font-medium truncate">{member.engineerName}</p>
|
|
141
|
+
<p className="text-xs text-muted-foreground">{member.hospitalName}</p>
|
|
142
|
+
</div>
|
|
143
|
+
<span className={cn(
|
|
144
|
+
'text-xs font-medium capitalize',
|
|
145
|
+
member.status === 'completed' && 'text-green-600 dark:text-green-400',
|
|
146
|
+
member.status === 'in_progress' && 'text-blue-600 dark:text-blue-400',
|
|
147
|
+
member.status === 'missed' && 'text-red-600 dark:text-red-400',
|
|
148
|
+
member.status === 'off_duty' && 'text-muted-foreground',
|
|
149
|
+
)}>
|
|
150
|
+
{member.status?.replace(/_/g, ' ')}
|
|
151
|
+
</span>
|
|
152
|
+
</div>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
</CardContent>
|
|
156
|
+
</Card>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{/* History List */}
|
|
160
|
+
<Card>
|
|
161
|
+
<CardHeader>
|
|
162
|
+
<CardTitle className="text-base">Recent Rounds</CardTitle>
|
|
163
|
+
</CardHeader>
|
|
164
|
+
<CardContent>
|
|
165
|
+
{initialHistory.length === 0 ? (
|
|
166
|
+
<p className="text-sm text-muted-foreground text-center py-4">
|
|
167
|
+
No rounds found
|
|
168
|
+
</p>
|
|
169
|
+
) : (
|
|
170
|
+
<div className="space-y-2">
|
|
171
|
+
{initialHistory.map((round: any) => (
|
|
172
|
+
<Link
|
|
173
|
+
key={round.id}
|
|
174
|
+
href={`/rounds/${round.id}`}
|
|
175
|
+
className="flex items-start gap-3 py-3 px-3 rounded-lg hover:bg-muted/50 transition-colors"
|
|
176
|
+
>
|
|
177
|
+
{statusIcons[round.status] || statusIcons.scheduled}
|
|
178
|
+
<div className="flex-1 min-w-0">
|
|
179
|
+
<div className="flex items-center justify-between gap-2">
|
|
180
|
+
<span className="text-sm font-medium">
|
|
181
|
+
{round.engineer?.fullName || 'Unknown'}
|
|
182
|
+
</span>
|
|
183
|
+
<span className="text-xs text-muted-foreground capitalize">
|
|
184
|
+
{round.shiftType}
|
|
185
|
+
</span>
|
|
186
|
+
</div>
|
|
187
|
+
<p className="text-xs text-muted-foreground">
|
|
188
|
+
{new Date(round.roundDate + 'T00:00:00').toLocaleDateString('en-US', {
|
|
189
|
+
weekday: 'short', month: 'short', day: 'numeric',
|
|
190
|
+
})}
|
|
191
|
+
{' · '}
|
|
192
|
+
{round.hospital?.name}
|
|
193
|
+
</p>
|
|
194
|
+
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
|
|
195
|
+
<span>{round.completedDepartments}/{round.totalDepartments} depts</span>
|
|
196
|
+
{round.totalIssuesReported > 0 && (
|
|
197
|
+
<span className="flex items-center gap-1 text-yellow-600 dark:text-yellow-400">
|
|
198
|
+
<AlertTriangle className="h-3 w-3" />
|
|
199
|
+
{round.totalIssuesReported} issues
|
|
200
|
+
</span>
|
|
201
|
+
)}
|
|
202
|
+
{round.totalNotes > 0 && (
|
|
203
|
+
<span className="flex items-center gap-1">
|
|
204
|
+
<FileText className="h-3 w-3" />
|
|
205
|
+
{round.totalNotes} notes
|
|
206
|
+
</span>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</Link>
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</CardContent>
|
|
215
|
+
</Card>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useTransition } from 'react';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
6
|
+
import { Input } from '@/components/ui/input';
|
|
7
|
+
import { Label } from '@/components/ui/label';
|
|
8
|
+
import {
|
|
9
|
+
Dialog, DialogContent, DialogHeader, DialogTitle,
|
|
10
|
+
DialogDescription, DialogFooter,
|
|
11
|
+
} from '@/components/ui/dialog';
|
|
12
|
+
import { startRound } from '@/lib/actions/engineer-rounds';
|
|
13
|
+
import { toast } from 'sonner';
|
|
14
|
+
import { useRouter } from 'next/navigation';
|
|
15
|
+
import { ShieldCheck, Loader2, Building2, Calendar, Clock, MapPin } from 'lucide-react';
|
|
16
|
+
|
|
17
|
+
interface StartRoundCardProps {
|
|
18
|
+
hospitalId: string;
|
|
19
|
+
hospitalName?: string;
|
|
20
|
+
departments: { id: string; name: string; code: string }[];
|
|
21
|
+
shiftType: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function StartRoundCard({
|
|
25
|
+
hospitalId,
|
|
26
|
+
hospitalName,
|
|
27
|
+
departments,
|
|
28
|
+
shiftType,
|
|
29
|
+
}: StartRoundCardProps) {
|
|
30
|
+
const router = useRouter();
|
|
31
|
+
const [showSignDialog, setShowSignDialog] = useState(false);
|
|
32
|
+
const [password, setPassword] = useState('');
|
|
33
|
+
const [isPending, startTransition] = useTransition();
|
|
34
|
+
|
|
35
|
+
const today = new Date();
|
|
36
|
+
const dateStr = today.toLocaleDateString('en-US', {
|
|
37
|
+
weekday: 'long',
|
|
38
|
+
year: 'numeric',
|
|
39
|
+
month: 'long',
|
|
40
|
+
day: 'numeric',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const handleStartRound = () => {
|
|
44
|
+
if (!password.trim()) {
|
|
45
|
+
toast.error('Please enter your password');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
startTransition(async () => {
|
|
50
|
+
const result = await startRound({
|
|
51
|
+
hospitalId,
|
|
52
|
+
shiftType,
|
|
53
|
+
password,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (result.success) {
|
|
57
|
+
toast.success('Round started! Good luck on your round.');
|
|
58
|
+
setShowSignDialog(false);
|
|
59
|
+
setPassword('');
|
|
60
|
+
router.refresh();
|
|
61
|
+
} else {
|
|
62
|
+
toast.error(result.error || 'Failed to start round');
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<Card className="border-primary/20 shadow-sm">
|
|
70
|
+
<CardHeader className="pb-3">
|
|
71
|
+
<CardTitle className="text-xl">Start Daily Round</CardTitle>
|
|
72
|
+
</CardHeader>
|
|
73
|
+
<CardContent className="space-y-4">
|
|
74
|
+
<div className="grid gap-3 text-sm">
|
|
75
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
76
|
+
<Calendar className="h-4 w-4 shrink-0" />
|
|
77
|
+
<span>{dateStr}</span>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
80
|
+
<Building2 className="h-4 w-4 shrink-0" />
|
|
81
|
+
<span>{hospitalName || 'Your Hospital'}</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
84
|
+
<Clock className="h-4 w-4 shrink-0" />
|
|
85
|
+
<span className="capitalize">{shiftType} Shift</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
88
|
+
<MapPin className="h-4 w-4 shrink-0" />
|
|
89
|
+
<span>{departments.length} department{departments.length !== 1 ? 's' : ''} to visit</span>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Department Preview */}
|
|
94
|
+
<div className="flex flex-wrap gap-1.5 pt-1">
|
|
95
|
+
{departments.map(dept => (
|
|
96
|
+
<span
|
|
97
|
+
key={dept.id}
|
|
98
|
+
className="inline-flex items-center text-xs bg-muted px-2 py-1 rounded-md"
|
|
99
|
+
>
|
|
100
|
+
{dept.name}
|
|
101
|
+
</span>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<Button
|
|
106
|
+
size="lg"
|
|
107
|
+
className="w-full gap-2 text-base h-12"
|
|
108
|
+
onClick={() => setShowSignDialog(true)}
|
|
109
|
+
>
|
|
110
|
+
<ShieldCheck className="h-5 w-5" />
|
|
111
|
+
Sign & Start Round
|
|
112
|
+
</Button>
|
|
113
|
+
</CardContent>
|
|
114
|
+
</Card>
|
|
115
|
+
|
|
116
|
+
{/* E-Signature Dialog */}
|
|
117
|
+
<Dialog open={showSignDialog} onOpenChange={setShowSignDialog}>
|
|
118
|
+
<DialogContent className="sm:max-w-md">
|
|
119
|
+
<DialogHeader>
|
|
120
|
+
<DialogTitle className="flex items-center gap-2">
|
|
121
|
+
<ShieldCheck className="h-5 w-5 text-primary" />
|
|
122
|
+
Electronic Signature
|
|
123
|
+
</DialogTitle>
|
|
124
|
+
<DialogDescription>
|
|
125
|
+
Confirm your identity to start the daily engineering round.
|
|
126
|
+
This action will be recorded with your electronic signature.
|
|
127
|
+
</DialogDescription>
|
|
128
|
+
</DialogHeader>
|
|
129
|
+
|
|
130
|
+
<div className="space-y-4 py-2">
|
|
131
|
+
<div className="rounded-md bg-muted/50 p-3 text-sm space-y-1">
|
|
132
|
+
<p><span className="font-medium">Action:</span> Start Daily Engineering Round</p>
|
|
133
|
+
<p><span className="font-medium">Date:</span> {dateStr}</p>
|
|
134
|
+
<p><span className="font-medium">Shift:</span> <span className="capitalize">{shiftType}</span></p>
|
|
135
|
+
<p><span className="font-medium">Departments:</span> {departments.length}</p>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div>
|
|
139
|
+
<Label htmlFor="sig-password">Enter your password to confirm</Label>
|
|
140
|
+
<Input
|
|
141
|
+
id="sig-password"
|
|
142
|
+
type="password"
|
|
143
|
+
placeholder="Your password"
|
|
144
|
+
value={password}
|
|
145
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
146
|
+
onKeyDown={(e) => {
|
|
147
|
+
if (e.key === 'Enter') handleStartRound();
|
|
148
|
+
}}
|
|
149
|
+
disabled={isPending}
|
|
150
|
+
className="mt-1.5"
|
|
151
|
+
autoFocus
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<DialogFooter>
|
|
157
|
+
<Button
|
|
158
|
+
variant="outline"
|
|
159
|
+
onClick={() => {
|
|
160
|
+
setShowSignDialog(false);
|
|
161
|
+
setPassword('');
|
|
162
|
+
}}
|
|
163
|
+
disabled={isPending}
|
|
164
|
+
>
|
|
165
|
+
Cancel
|
|
166
|
+
</Button>
|
|
167
|
+
<Button
|
|
168
|
+
onClick={handleStartRound}
|
|
169
|
+
disabled={isPending || !password.trim()}
|
|
170
|
+
className="gap-2"
|
|
171
|
+
>
|
|
172
|
+
{isPending ? (
|
|
173
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
174
|
+
) : (
|
|
175
|
+
<ShieldCheck className="h-4 w-4" />
|
|
176
|
+
)}
|
|
177
|
+
Confirm & Start
|
|
178
|
+
</Button>
|
|
179
|
+
</DialogFooter>
|
|
180
|
+
</DialogContent>
|
|
181
|
+
</Dialog>
|
|
182
|
+
</>
|
|
183
|
+
);
|
|
184
|
+
}
|