@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,121 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Badge } from "@/components/ui/badge"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
import { Lock, ArrowRight, History } from "lucide-react"
|
|
7
|
+
|
|
8
|
+
export interface SignatureEventItem {
|
|
9
|
+
id: string
|
|
10
|
+
eventType: string
|
|
11
|
+
actorUserId?: string
|
|
12
|
+
timestamp: Date | string
|
|
13
|
+
previousStatus?: string | null
|
|
14
|
+
newStatus?: string | null
|
|
15
|
+
reason?: string | null
|
|
16
|
+
currentEventHash: string
|
|
17
|
+
previousEventHash?: string | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SignatureHistoryProps {
|
|
21
|
+
events: SignatureEventItem[]
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const EVENT_COLORS: Record<string, string> = {
|
|
26
|
+
created: "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-800",
|
|
27
|
+
approved: "bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-300 dark:border-emerald-800",
|
|
28
|
+
rejected: "bg-red-100 text-red-800 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-800",
|
|
29
|
+
amended: "bg-orange-100 text-orange-800 border-orange-200 dark:bg-orange-900/30 dark:text-orange-300 dark:border-orange-800",
|
|
30
|
+
voided: "bg-rose-100 text-rose-800 border-rose-200 dark:bg-rose-900/30 dark:text-rose-300 dark:border-rose-800",
|
|
31
|
+
reviewed: "bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-800",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const formatTime = (date: Date | string) => {
|
|
35
|
+
const d = new Date(date)
|
|
36
|
+
return d.toLocaleString("en-US", {
|
|
37
|
+
day: "2-digit",
|
|
38
|
+
month: "short",
|
|
39
|
+
year: "numeric",
|
|
40
|
+
hour: "2-digit",
|
|
41
|
+
minute: "2-digit",
|
|
42
|
+
second: "2-digit",
|
|
43
|
+
hour12: true,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function truncateHash(hash: string) {
|
|
48
|
+
if (!hash || hash.length < 16) return hash
|
|
49
|
+
return `${hash.substring(0, 8)}...${hash.substring(hash.length - 8)}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function SignatureHistory({ events, className }: SignatureHistoryProps) {
|
|
53
|
+
// Sort from newest to oldest
|
|
54
|
+
const sortedEvents = [...events].sort((a, b) => {
|
|
55
|
+
return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (sortedEvents.length === 0) {
|
|
59
|
+
return (
|
|
60
|
+
<div className={cn("text-center py-8 text-muted-foreground", className)}>
|
|
61
|
+
<History className="mx-auto h-8 w-8 opacity-40 mb-2" />
|
|
62
|
+
<p className="text-sm">No signature events recorded.</p>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className={cn("relative space-y-0 ml-3 sm:ml-4 border-l-2 border-muted", className)}>
|
|
69
|
+
{sortedEvents.map((event, idx) => {
|
|
70
|
+
const isLast = idx === sortedEvents.length - 1
|
|
71
|
+
const eventTypeClass = EVENT_COLORS[event.eventType.toLowerCase()] ||
|
|
72
|
+
"bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-700"
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div key={event.id} className="relative pl-6 sm:pl-8 pb-8 last:pb-0 group">
|
|
76
|
+
{/* Timeline Dot */}
|
|
77
|
+
<div className="absolute -left-[5px] top-1 h-2 w-2 rounded-full bg-border ring-4 ring-background group-hover:bg-primary transition-colors" />
|
|
78
|
+
|
|
79
|
+
<div className="space-y-2">
|
|
80
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
81
|
+
<Badge variant="outline" className={cn("capitalize text-[10px] font-semibold tracking-wide", eventTypeClass)}>
|
|
82
|
+
{event.eventType}
|
|
83
|
+
</Badge>
|
|
84
|
+
<span className="text-xs text-muted-foreground font-medium">
|
|
85
|
+
{formatTime(event.timestamp)}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{event.previousStatus && event.newStatus && (
|
|
90
|
+
<div className="flex items-center gap-2 text-sm text-foreground/80">
|
|
91
|
+
<span className="capitalize">{event.previousStatus}</span>
|
|
92
|
+
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
|
93
|
+
<span className="capitalize font-medium text-foreground">{event.newStatus}</span>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{event.reason && (
|
|
98
|
+
<p className="text-sm text-foreground bg-muted/40 p-2.5 rounded-md border border-border/50">
|
|
99
|
+
<span className="font-semibold text-xs text-muted-foreground block mb-1 uppercase tracking-wider">Reason</span>
|
|
100
|
+
{event.reason}
|
|
101
|
+
</p>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground/70 font-mono mt-1">
|
|
105
|
+
<Lock className="h-3 w-3" />
|
|
106
|
+
<span title={event.currentEventHash}>Hash: {truncateHash(event.currentEventHash)}</span>
|
|
107
|
+
{event.previousEventHash && (
|
|
108
|
+
<span title={event.previousEventHash}>
|
|
109
|
+
<ArrowRight className="h-2.5 w-2.5 inline mx-1" />
|
|
110
|
+
{truncateHash(event.previousEventHash)}
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
})}
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
5
|
+
import { Button } from "@/components/ui/button"
|
|
6
|
+
import { Badge } from "@/components/ui/badge"
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
import { CheckCircle2, Clock, Circle, XCircle, PenSquare } from "lucide-react"
|
|
9
|
+
|
|
10
|
+
export interface SignatureRequirement {
|
|
11
|
+
purpose: string
|
|
12
|
+
allowedRoles: string[]
|
|
13
|
+
required: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CompletedSignature {
|
|
17
|
+
id: string
|
|
18
|
+
purpose: string
|
|
19
|
+
signerName: string
|
|
20
|
+
signerRole: string
|
|
21
|
+
signedAt: Date | string
|
|
22
|
+
status: string
|
|
23
|
+
contentHash: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SignatureStatusPanelProps {
|
|
27
|
+
required: SignatureRequirement[]
|
|
28
|
+
completed: CompletedSignature[]
|
|
29
|
+
pending: SignatureRequirement[]
|
|
30
|
+
canSign: { purpose: string; allowed: boolean; reason?: string }[]
|
|
31
|
+
onSignClick?: (purpose: string) => void
|
|
32
|
+
className?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const formatPurpose = (purpose: string) => {
|
|
36
|
+
const map: Record<string, string> = {
|
|
37
|
+
perform: "Performer Signature",
|
|
38
|
+
review: "Reviewer Signature",
|
|
39
|
+
release: "Release Authority",
|
|
40
|
+
approve: "Approval",
|
|
41
|
+
reject: "Rejection",
|
|
42
|
+
}
|
|
43
|
+
if (map[purpose]) return map[purpose]
|
|
44
|
+
return purpose.charAt(0).toUpperCase() + purpose.slice(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const formatTime = (date: Date | string) => {
|
|
48
|
+
const d = new Date(date)
|
|
49
|
+
return d.toLocaleString("en-US", {
|
|
50
|
+
day: "2-digit",
|
|
51
|
+
month: "short",
|
|
52
|
+
year: "numeric",
|
|
53
|
+
hour: "2-digit",
|
|
54
|
+
minute: "2-digit",
|
|
55
|
+
hour12: false,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function SignatureStatusPanel({
|
|
60
|
+
required,
|
|
61
|
+
completed,
|
|
62
|
+
pending,
|
|
63
|
+
canSign,
|
|
64
|
+
onSignClick,
|
|
65
|
+
className,
|
|
66
|
+
}: SignatureStatusPanelProps) {
|
|
67
|
+
// Combine into an ordered list of steps based on the requirements
|
|
68
|
+
const steps = required.map((req) => {
|
|
69
|
+
const comp = completed.find((c) => c.purpose === req.purpose)
|
|
70
|
+
const isPending = pending.some((p) => p.purpose === req.purpose)
|
|
71
|
+
const signAuth = canSign.find((c) => c.purpose === req.purpose)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
requirement: req,
|
|
75
|
+
completed: comp,
|
|
76
|
+
isPending,
|
|
77
|
+
signAuth,
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Card className={cn("overflow-hidden", className)}>
|
|
83
|
+
<CardHeader className="bg-muted/40 pb-3 pt-4 px-4 border-b">
|
|
84
|
+
<CardTitle className="text-sm font-semibold flex items-center gap-2">
|
|
85
|
+
Signature Status
|
|
86
|
+
</CardTitle>
|
|
87
|
+
</CardHeader>
|
|
88
|
+
<CardContent className="p-0">
|
|
89
|
+
<div className="flex flex-col divide-y">
|
|
90
|
+
{steps.map((step, idx) => {
|
|
91
|
+
const purposeLabel = formatPurpose(step.requirement.purpose)
|
|
92
|
+
|
|
93
|
+
if (step.completed) {
|
|
94
|
+
const isRejected = step.completed.status === 'rejected'
|
|
95
|
+
return (
|
|
96
|
+
<div key={idx} className="flex items-start gap-3 p-4 bg-card">
|
|
97
|
+
{isRejected ? (
|
|
98
|
+
<XCircle className="h-5 w-5 text-destructive shrink-0 mt-0.5" />
|
|
99
|
+
) : (
|
|
100
|
+
<CheckCircle2 className="h-5 w-5 text-emerald-500 shrink-0 mt-0.5" />
|
|
101
|
+
)}
|
|
102
|
+
<div className="min-w-0 flex-1 space-y-1">
|
|
103
|
+
<p className="text-sm font-medium text-foreground">
|
|
104
|
+
{purposeLabel}
|
|
105
|
+
</p>
|
|
106
|
+
<p className="text-xs text-muted-foreground truncate">
|
|
107
|
+
{step.completed.signerName}, {step.completed.signerRole} • {formatTime(step.completed.signedAt)}
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (step.isPending) {
|
|
115
|
+
return (
|
|
116
|
+
<div key={idx} className="flex items-start gap-3 p-4 bg-muted/20">
|
|
117
|
+
<Clock className="h-5 w-5 text-amber-500 shrink-0 mt-0.5" />
|
|
118
|
+
<div className="min-w-0 flex-1 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
119
|
+
<div className="space-y-1">
|
|
120
|
+
<p className="text-sm font-medium text-foreground">
|
|
121
|
+
{purposeLabel}
|
|
122
|
+
</p>
|
|
123
|
+
<p className="text-xs text-muted-foreground">
|
|
124
|
+
{step.signAuth?.allowed
|
|
125
|
+
? "Awaiting your signature"
|
|
126
|
+
: step.signAuth?.reason || "Awaiting review"}
|
|
127
|
+
</p>
|
|
128
|
+
</div>
|
|
129
|
+
{step.signAuth?.allowed && onSignClick && (
|
|
130
|
+
<Button
|
|
131
|
+
size="sm"
|
|
132
|
+
onClick={() => onSignClick(step.requirement.purpose)}
|
|
133
|
+
className="shrink-0 h-8 gap-1.5"
|
|
134
|
+
>
|
|
135
|
+
<PenSquare className="h-3.5 w-3.5" />
|
|
136
|
+
Sign
|
|
137
|
+
</Button>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Not yet required
|
|
145
|
+
return (
|
|
146
|
+
<div key={idx} className="flex items-start gap-3 p-4 opacity-60">
|
|
147
|
+
<Circle className="h-5 w-5 text-muted-foreground shrink-0 mt-0.5" />
|
|
148
|
+
<div className="min-w-0 flex-1 space-y-1">
|
|
149
|
+
<p className="text-sm font-medium text-foreground">
|
|
150
|
+
{purposeLabel}
|
|
151
|
+
</p>
|
|
152
|
+
<p className="text-xs text-muted-foreground">
|
|
153
|
+
Not yet required (waiting for previous steps)
|
|
154
|
+
</p>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
})}
|
|
159
|
+
</div>
|
|
160
|
+
</CardContent>
|
|
161
|
+
</Card>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
|
4
|
+
import { Badge } from '@/components/ui/badge';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Building2, Phone, PhoneCall, Wrench, Shield, CheckCircle2 } from 'lucide-react';
|
|
7
|
+
import Link from 'next/link';
|
|
8
|
+
import { EngineerDutyStatus } from '@/lib/db/schema/engineer-profiles';
|
|
9
|
+
|
|
10
|
+
interface DepartmentCoverageProps {
|
|
11
|
+
coverage: {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
code: string;
|
|
15
|
+
deviceCount: number;
|
|
16
|
+
primaryEngineer: {
|
|
17
|
+
id: string;
|
|
18
|
+
fullName: string;
|
|
19
|
+
jobTitle: string;
|
|
20
|
+
extension: string | null;
|
|
21
|
+
mobileNumber: string | null;
|
|
22
|
+
dutyStatus: EngineerDutyStatus;
|
|
23
|
+
} | null;
|
|
24
|
+
}[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function DepartmentCoverage({ coverage }: DepartmentCoverageProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="space-y-4">
|
|
30
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
|
31
|
+
<div>
|
|
32
|
+
<h3 className="text-base font-semibold">Hospital Department Engineering Assignments</h3>
|
|
33
|
+
<p className="text-xs text-muted-foreground">
|
|
34
|
+
Each hospital department is assigned a designated biomedical engineer for routine inspections, PM compliance, and rapid fault resolution.
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
|
|
40
|
+
{coverage.map((dept) => {
|
|
41
|
+
const eng = dept.primaryEngineer;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Card key={dept.id} className="border-border/70 shadow-sm flex flex-col justify-between hover:border-primary/40 transition-colors">
|
|
45
|
+
<CardHeader className="pb-3">
|
|
46
|
+
<div className="flex items-start justify-between gap-2">
|
|
47
|
+
<div>
|
|
48
|
+
<Badge variant="outline" className="text-[10px] font-mono mb-1">
|
|
49
|
+
{dept.code}
|
|
50
|
+
</Badge>
|
|
51
|
+
<CardTitle className="text-base font-semibold leading-tight">{dept.name}</CardTitle>
|
|
52
|
+
<span className="text-xs text-muted-foreground flex items-center gap-1 mt-1">
|
|
53
|
+
<Wrench className="w-3 h-3 text-primary" />
|
|
54
|
+
{dept.deviceCount} Medical Equipment Assets
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</CardHeader>
|
|
59
|
+
|
|
60
|
+
<CardContent className="space-y-3 text-xs flex-1">
|
|
61
|
+
<div className="p-3 rounded-lg bg-muted/40 border border-border/50 space-y-2">
|
|
62
|
+
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider block">
|
|
63
|
+
Designated BME Lead
|
|
64
|
+
</span>
|
|
65
|
+
|
|
66
|
+
{eng ? (
|
|
67
|
+
<div>
|
|
68
|
+
<div className="flex items-center justify-between">
|
|
69
|
+
<p className="font-semibold text-sm">{eng.fullName}</p>
|
|
70
|
+
{eng.dutyStatus === 'on_duty' ? (
|
|
71
|
+
<Badge className="bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 border-emerald-500/30 text-[10px] py-0 px-1.5 font-normal">
|
|
72
|
+
🟢 On Duty
|
|
73
|
+
</Badge>
|
|
74
|
+
) : eng.dutyStatus === 'on_call' ? (
|
|
75
|
+
<Badge className="bg-amber-500/15 text-amber-700 dark:text-amber-400 border-amber-500/30 text-[10px] py-0 px-1.5 font-normal">
|
|
76
|
+
📞 On Call
|
|
77
|
+
</Badge>
|
|
78
|
+
) : (
|
|
79
|
+
<Badge variant="outline" className="text-muted-foreground text-[10px] py-0 px-1.5">
|
|
80
|
+
⚪ Off Duty
|
|
81
|
+
</Badge>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
<p className="text-[11px] text-muted-foreground mt-0.5">{eng.jobTitle}</p>
|
|
85
|
+
|
|
86
|
+
<div className="flex items-center gap-3 mt-2 text-xs pt-2 border-t border-border/40">
|
|
87
|
+
{eng.extension && (
|
|
88
|
+
<span className="flex items-center gap-1 font-medium text-foreground">
|
|
89
|
+
<PhoneCall className="w-3 h-3 text-primary" />
|
|
90
|
+
Ext: {eng.extension}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
{eng.mobileNumber && (
|
|
94
|
+
<a
|
|
95
|
+
href={`tel:${eng.mobileNumber}`}
|
|
96
|
+
className="flex items-center gap-1 text-primary hover:underline font-medium"
|
|
97
|
+
>
|
|
98
|
+
<Phone className="w-3 h-3" />
|
|
99
|
+
{eng.mobileNumber}
|
|
100
|
+
</a>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
) : (
|
|
105
|
+
<p className="text-muted-foreground italic text-xs">General biomedical team pool</p>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</CardContent>
|
|
109
|
+
|
|
110
|
+
<div className="p-3 pt-0 flex gap-2">
|
|
111
|
+
<Button asChild variant="outline" size="sm" className="flex-1 text-xs h-8">
|
|
112
|
+
<Link href={`/departments/${dept.id}`}>
|
|
113
|
+
<Building2 className="w-3 h-3 mr-1" />
|
|
114
|
+
View Dept
|
|
115
|
+
</Link>
|
|
116
|
+
</Button>
|
|
117
|
+
|
|
118
|
+
{eng?.mobileNumber ? (
|
|
119
|
+
<Button asChild size="sm" className="flex-1 text-xs h-8 gap-1.5">
|
|
120
|
+
<a href={`tel:${eng.mobileNumber}`}>
|
|
121
|
+
<PhoneCall className="w-3 h-3" />
|
|
122
|
+
Contact Lead
|
|
123
|
+
</a>
|
|
124
|
+
</Button>
|
|
125
|
+
) : (
|
|
126
|
+
<Button asChild size="sm" className="flex-1 text-xs h-8 gap-1.5">
|
|
127
|
+
<Link href={`/tickets/new?departmentId=${dept.id}`}>
|
|
128
|
+
<Wrench className="w-3 h-3" />
|
|
129
|
+
Report Fault
|
|
130
|
+
</Link>
|
|
131
|
+
</Button>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</Card>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
} from '@/components/ui/dialog';
|
|
12
|
+
import { Button } from '@/components/ui/button';
|
|
13
|
+
import { Input } from '@/components/ui/input';
|
|
14
|
+
import { Label } from '@/components/ui/label';
|
|
15
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
16
|
+
import { EngineerTeamMember, updateEngineerProfile } from '@/lib/actions/engineers';
|
|
17
|
+
import { toast } from 'sonner';
|
|
18
|
+
|
|
19
|
+
interface EditEngineerDialogProps {
|
|
20
|
+
engineer: EngineerTeamMember | null;
|
|
21
|
+
open: boolean;
|
|
22
|
+
onOpenChange: (open: boolean) => void;
|
|
23
|
+
onSuccess: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ALL_DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
27
|
+
|
|
28
|
+
export function EditEngineerDialog({
|
|
29
|
+
engineer,
|
|
30
|
+
open,
|
|
31
|
+
onOpenChange,
|
|
32
|
+
onSuccess,
|
|
33
|
+
}: EditEngineerDialogProps) {
|
|
34
|
+
const [jobTitle, setJobTitle] = useState(engineer?.jobTitle || '');
|
|
35
|
+
const [specialization, setSpecialization] = useState(engineer?.specialization || '');
|
|
36
|
+
const [extension, setExtension] = useState(engineer?.extension || '');
|
|
37
|
+
const [mobileNumber, setMobileNumber] = useState(engineer?.mobileNumber || '');
|
|
38
|
+
const [shiftHours, setShiftHours] = useState(engineer?.shiftHours || '08:00 AM - 04:00 PM');
|
|
39
|
+
const [workingDays, setWorkingDays] = useState<string[]>(engineer?.workingDays || []);
|
|
40
|
+
const [onCallDays, setOnCallDays] = useState<string[]>(engineer?.onCallDays || []);
|
|
41
|
+
const [notes, setNotes] = useState(engineer?.notes || '');
|
|
42
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (engineer) {
|
|
46
|
+
setJobTitle(engineer.jobTitle || '');
|
|
47
|
+
setSpecialization(engineer.specialization || '');
|
|
48
|
+
setExtension(engineer.extension || '');
|
|
49
|
+
setMobileNumber(engineer.mobileNumber || '');
|
|
50
|
+
setShiftHours(engineer.shiftHours || '08:00 AM - 04:00 PM');
|
|
51
|
+
setWorkingDays(engineer.workingDays || []);
|
|
52
|
+
setOnCallDays(engineer.onCallDays || []);
|
|
53
|
+
setNotes(engineer.notes || '');
|
|
54
|
+
}
|
|
55
|
+
}, [engineer]);
|
|
56
|
+
|
|
57
|
+
if (!engineer) return null;
|
|
58
|
+
|
|
59
|
+
const toggleWorkingDay = (day: string) => {
|
|
60
|
+
setWorkingDays((prev) =>
|
|
61
|
+
prev.includes(day) ? prev.filter((d) => d !== day) : [...prev, day]
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const toggleOnCallDay = (day: string) => {
|
|
66
|
+
setOnCallDays((prev) =>
|
|
67
|
+
prev.includes(day) ? prev.filter((d) => d !== day) : [...prev, day]
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
if (!engineer) return;
|
|
74
|
+
setIsSubmitting(true);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const res = await updateEngineerProfile(engineer.id, {
|
|
78
|
+
jobTitle,
|
|
79
|
+
specialization,
|
|
80
|
+
extension,
|
|
81
|
+
mobileNumber,
|
|
82
|
+
shiftHours,
|
|
83
|
+
workingDays,
|
|
84
|
+
onCallDays,
|
|
85
|
+
notes,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (res.success) {
|
|
89
|
+
toast.success(`Updated profile for ${engineer.fullName}`);
|
|
90
|
+
onOpenChange(false);
|
|
91
|
+
onSuccess();
|
|
92
|
+
} else {
|
|
93
|
+
toast.error(res.error || 'Failed to update engineer profile');
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
toast.error('An unexpected error occurred');
|
|
97
|
+
} finally {
|
|
98
|
+
setIsSubmitting(false);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
104
|
+
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
|
|
105
|
+
<DialogHeader>
|
|
106
|
+
<DialogTitle>Edit Engineer Schedule & Info</DialogTitle>
|
|
107
|
+
<DialogDescription>
|
|
108
|
+
Update contact info, shift hours, and working days for <strong>{engineer.fullName}</strong>.
|
|
109
|
+
</DialogDescription>
|
|
110
|
+
</DialogHeader>
|
|
111
|
+
|
|
112
|
+
<form onSubmit={handleSubmit} className="space-y-4 text-xs">
|
|
113
|
+
<div className="grid grid-cols-2 gap-3">
|
|
114
|
+
<div className="space-y-1.5">
|
|
115
|
+
<Label className="text-xs">Job Title</Label>
|
|
116
|
+
<Input
|
|
117
|
+
value={jobTitle}
|
|
118
|
+
onChange={(e) => setJobTitle(e.target.value)}
|
|
119
|
+
placeholder="e.g. Lead Biomedical Engineer"
|
|
120
|
+
required
|
|
121
|
+
className="h-8 text-xs"
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="space-y-1.5">
|
|
125
|
+
<Label className="text-xs">Specialization</Label>
|
|
126
|
+
<Input
|
|
127
|
+
value={specialization}
|
|
128
|
+
onChange={(e) => setSpecialization(e.target.value)}
|
|
129
|
+
placeholder="e.g. Critical Care Systems"
|
|
130
|
+
className="h-8 text-xs"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div className="grid grid-cols-2 gap-3">
|
|
136
|
+
<div className="space-y-1.5">
|
|
137
|
+
<Label className="text-xs">Internal Phone Extension</Label>
|
|
138
|
+
<Input
|
|
139
|
+
value={extension}
|
|
140
|
+
onChange={(e) => setExtension(e.target.value)}
|
|
141
|
+
placeholder="e.g. 201"
|
|
142
|
+
className="h-8 text-xs font-mono"
|
|
143
|
+
/>
|
|
144
|
+
</div>
|
|
145
|
+
<div className="space-y-1.5">
|
|
146
|
+
<Label className="text-xs">Direct Mobile Number</Label>
|
|
147
|
+
<Input
|
|
148
|
+
value={mobileNumber}
|
|
149
|
+
onChange={(e) => setMobileNumber(e.target.value)}
|
|
150
|
+
placeholder="e.g. +249 91 234 5678"
|
|
151
|
+
className="h-8 text-xs font-mono"
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div className="space-y-1.5">
|
|
157
|
+
<Label className="text-xs">Shift Hours</Label>
|
|
158
|
+
<Input
|
|
159
|
+
value={shiftHours}
|
|
160
|
+
onChange={(e) => setShiftHours(e.target.value)}
|
|
161
|
+
placeholder="e.g. 08:00 AM - 04:00 PM"
|
|
162
|
+
className="h-8 text-xs"
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div className="space-y-1.5">
|
|
167
|
+
<Label className="text-xs font-semibold">Scheduled Working Days (On-Site)</Label>
|
|
168
|
+
<div className="flex flex-wrap gap-1.5">
|
|
169
|
+
{ALL_DAYS.map((day) => {
|
|
170
|
+
const checked = workingDays.includes(day);
|
|
171
|
+
return (
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
key={day}
|
|
175
|
+
onClick={() => toggleWorkingDay(day)}
|
|
176
|
+
className={`px-2.5 py-1 rounded text-xs font-medium border transition-colors ${
|
|
177
|
+
checked
|
|
178
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
179
|
+
: 'bg-muted/40 text-muted-foreground border-border/70 hover:bg-muted'
|
|
180
|
+
}`}
|
|
181
|
+
>
|
|
182
|
+
{day}
|
|
183
|
+
</button>
|
|
184
|
+
);
|
|
185
|
+
})}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div className="space-y-1.5">
|
|
190
|
+
<Label className="text-xs font-semibold">Emergency On-Call Days</Label>
|
|
191
|
+
<div className="flex flex-wrap gap-1.5">
|
|
192
|
+
{ALL_DAYS.map((day) => {
|
|
193
|
+
const checked = onCallDays.includes(day);
|
|
194
|
+
return (
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
key={day}
|
|
198
|
+
onClick={() => toggleOnCallDay(day)}
|
|
199
|
+
className={`px-2.5 py-1 rounded text-xs font-medium border transition-colors ${
|
|
200
|
+
checked
|
|
201
|
+
? 'bg-amber-500 text-white border-amber-600'
|
|
202
|
+
: 'bg-muted/40 text-muted-foreground border-border/70 hover:bg-muted'
|
|
203
|
+
}`}
|
|
204
|
+
>
|
|
205
|
+
{day} 📞
|
|
206
|
+
</button>
|
|
207
|
+
);
|
|
208
|
+
})}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div className="space-y-1.5">
|
|
213
|
+
<Label className="text-xs">Clinical Engineering Notes / Certifications</Label>
|
|
214
|
+
<Textarea
|
|
215
|
+
value={notes}
|
|
216
|
+
onChange={(e) => setNotes(e.target.value)}
|
|
217
|
+
placeholder="e.g. Lead responder for OR 1 & 2. Certified on Dräger anesthesia workstations."
|
|
218
|
+
rows={2}
|
|
219
|
+
className="text-xs resize-none"
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<DialogFooter className="pt-2">
|
|
224
|
+
<Button
|
|
225
|
+
type="button"
|
|
226
|
+
variant="outline"
|
|
227
|
+
size="sm"
|
|
228
|
+
onClick={() => onOpenChange(false)}
|
|
229
|
+
disabled={isSubmitting}
|
|
230
|
+
>
|
|
231
|
+
Cancel
|
|
232
|
+
</Button>
|
|
233
|
+
<Button type="submit" size="sm" disabled={isSubmitting}>
|
|
234
|
+
{isSubmitting ? 'Saving...' : 'Save Changes'}
|
|
235
|
+
</Button>
|
|
236
|
+
</DialogFooter>
|
|
237
|
+
</form>
|
|
238
|
+
</DialogContent>
|
|
239
|
+
</Dialog>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|