@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,333 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Input } from '@/components/ui/input';
|
|
8
|
+
import { Badge } from '@/components/ui/badge';
|
|
9
|
+
import { DeviceStatusBadge } from '@/components/devices/device-badges';
|
|
10
|
+
import { PageHeader } from '@/components/shared/page-header';
|
|
11
|
+
import {
|
|
12
|
+
Search,
|
|
13
|
+
Monitor,
|
|
14
|
+
AlertTriangle,
|
|
15
|
+
Wrench,
|
|
16
|
+
CheckCircle,
|
|
17
|
+
PlusCircle,
|
|
18
|
+
TicketCheck,
|
|
19
|
+
} from 'lucide-react';
|
|
20
|
+
import { cn } from '@/lib/utils';
|
|
21
|
+
|
|
22
|
+
interface DeviceItem {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
assetNumber: string;
|
|
26
|
+
currentStatusCode: string;
|
|
27
|
+
locationName: string;
|
|
28
|
+
categoryName: string;
|
|
29
|
+
manufacturerName: string;
|
|
30
|
+
activeTickets: {
|
|
31
|
+
id: string;
|
|
32
|
+
ticketNumber: string;
|
|
33
|
+
title: string;
|
|
34
|
+
statusCode: string;
|
|
35
|
+
}[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface MyDepartmentViewProps {
|
|
39
|
+
department: {
|
|
40
|
+
name: string;
|
|
41
|
+
hospitalName: string;
|
|
42
|
+
};
|
|
43
|
+
devices: DeviceItem[];
|
|
44
|
+
stats: {
|
|
45
|
+
total: number;
|
|
46
|
+
operational: number;
|
|
47
|
+
underMaintenance: number;
|
|
48
|
+
outOfService: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type StatusFilter = 'all' | 'operational' | 'maintenance' | 'out_of_service';
|
|
53
|
+
|
|
54
|
+
export function MyDepartmentView({
|
|
55
|
+
department,
|
|
56
|
+
devices,
|
|
57
|
+
stats,
|
|
58
|
+
}: MyDepartmentViewProps) {
|
|
59
|
+
const [search, setSearch] = useState('');
|
|
60
|
+
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
|
61
|
+
|
|
62
|
+
const filteredDevices = useMemo(() => {
|
|
63
|
+
let filtered = devices;
|
|
64
|
+
|
|
65
|
+
// Search filter
|
|
66
|
+
if (search.trim()) {
|
|
67
|
+
const q = search.toLowerCase();
|
|
68
|
+
filtered = filtered.filter(
|
|
69
|
+
(d) =>
|
|
70
|
+
d.name.toLowerCase().includes(q) ||
|
|
71
|
+
d.assetNumber.toLowerCase().includes(q) ||
|
|
72
|
+
d.locationName.toLowerCase().includes(q) ||
|
|
73
|
+
d.categoryName.toLowerCase().includes(q) ||
|
|
74
|
+
d.manufacturerName.toLowerCase().includes(q)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Status filter
|
|
79
|
+
if (statusFilter === 'operational') {
|
|
80
|
+
filtered = filtered.filter(
|
|
81
|
+
(d) => d.currentStatusCode === 'operational'
|
|
82
|
+
);
|
|
83
|
+
} else if (statusFilter === 'maintenance') {
|
|
84
|
+
filtered = filtered.filter((d) =>
|
|
85
|
+
[
|
|
86
|
+
'under_repair',
|
|
87
|
+
'under_maintenance',
|
|
88
|
+
'waiting_for_parts',
|
|
89
|
+
'under_investigation',
|
|
90
|
+
].includes(d.currentStatusCode)
|
|
91
|
+
);
|
|
92
|
+
} else if (statusFilter === 'out_of_service') {
|
|
93
|
+
filtered = filtered.filter((d) =>
|
|
94
|
+
['out_of_service', 'decommissioned'].includes(d.currentStatusCode)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Sort: unavailable devices first
|
|
99
|
+
return filtered.sort((a, b) => {
|
|
100
|
+
const priority: Record<string, number> = {
|
|
101
|
+
out_of_service: 0,
|
|
102
|
+
decommissioned: 1,
|
|
103
|
+
under_repair: 2,
|
|
104
|
+
under_maintenance: 3,
|
|
105
|
+
waiting_for_parts: 4,
|
|
106
|
+
under_investigation: 5,
|
|
107
|
+
operational: 6,
|
|
108
|
+
};
|
|
109
|
+
return (
|
|
110
|
+
(priority[a.currentStatusCode] ?? 10) -
|
|
111
|
+
(priority[b.currentStatusCode] ?? 10)
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
}, [devices, search, statusFilter]);
|
|
115
|
+
|
|
116
|
+
const filters: { key: StatusFilter; label: string; count: number }[] = [
|
|
117
|
+
{ key: 'all', label: 'All', count: stats.total },
|
|
118
|
+
{ key: 'operational', label: 'Available', count: stats.operational },
|
|
119
|
+
{
|
|
120
|
+
key: 'maintenance',
|
|
121
|
+
label: 'Under Maint.',
|
|
122
|
+
count: stats.underMaintenance,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
key: 'out_of_service',
|
|
126
|
+
label: 'Out of Service',
|
|
127
|
+
count: stats.outOfService,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="space-y-6 max-w-3xl mx-auto">
|
|
133
|
+
<PageHeader
|
|
134
|
+
title="My Department"
|
|
135
|
+
description={`${department.name} \u00B7 ${department.hospitalName}`}
|
|
136
|
+
>
|
|
137
|
+
<Link href="/tickets/create">
|
|
138
|
+
<Button size="lg" className="min-h-[44px]">
|
|
139
|
+
<PlusCircle className="mr-2 h-5 w-5" />
|
|
140
|
+
Report Problem
|
|
141
|
+
</Button>
|
|
142
|
+
</Link>
|
|
143
|
+
</PageHeader>
|
|
144
|
+
|
|
145
|
+
{/* Summary Stats */}
|
|
146
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
147
|
+
<Card className="shadow-sm">
|
|
148
|
+
<CardContent className="p-3 text-center">
|
|
149
|
+
<p className="text-2xl font-bold">{stats.total}</p>
|
|
150
|
+
<p className="text-xs text-muted-foreground">Total Devices</p>
|
|
151
|
+
</CardContent>
|
|
152
|
+
</Card>
|
|
153
|
+
<Card className="shadow-sm border-green-200 dark:border-green-900 bg-green-50/50 dark:bg-green-950/20">
|
|
154
|
+
<CardContent className="p-3 text-center">
|
|
155
|
+
<p className="text-2xl font-bold text-green-700 dark:text-green-400">
|
|
156
|
+
{stats.operational}
|
|
157
|
+
</p>
|
|
158
|
+
<p className="text-xs text-green-600 dark:text-green-500">
|
|
159
|
+
Available
|
|
160
|
+
</p>
|
|
161
|
+
</CardContent>
|
|
162
|
+
</Card>
|
|
163
|
+
<Card className="shadow-sm border-amber-200 dark:border-amber-900 bg-amber-50/50 dark:bg-amber-950/20">
|
|
164
|
+
<CardContent className="p-3 text-center">
|
|
165
|
+
<p className="text-2xl font-bold text-amber-700 dark:text-amber-400">
|
|
166
|
+
{stats.underMaintenance}
|
|
167
|
+
</p>
|
|
168
|
+
<p className="text-xs text-amber-600 dark:text-amber-500">
|
|
169
|
+
Maintenance
|
|
170
|
+
</p>
|
|
171
|
+
</CardContent>
|
|
172
|
+
</Card>
|
|
173
|
+
<Card className="shadow-sm border-red-200 dark:border-red-900 bg-red-50/50 dark:bg-red-950/20">
|
|
174
|
+
<CardContent className="p-3 text-center">
|
|
175
|
+
<p className="text-2xl font-bold text-red-700 dark:text-red-400">
|
|
176
|
+
{stats.outOfService}
|
|
177
|
+
</p>
|
|
178
|
+
<p className="text-xs text-red-600 dark:text-red-500">
|
|
179
|
+
Out of Service
|
|
180
|
+
</p>
|
|
181
|
+
</CardContent>
|
|
182
|
+
</Card>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Search */}
|
|
186
|
+
<div className="relative">
|
|
187
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
188
|
+
<Input
|
|
189
|
+
placeholder="Search devices by name, asset number, location..."
|
|
190
|
+
value={search}
|
|
191
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
192
|
+
className="pl-10"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Filter Chips */}
|
|
197
|
+
<div className="flex gap-2 flex-wrap">
|
|
198
|
+
{filters.map((f) => (
|
|
199
|
+
<button
|
|
200
|
+
key={f.key}
|
|
201
|
+
onClick={() => setStatusFilter(f.key)}
|
|
202
|
+
className={cn(
|
|
203
|
+
'text-xs px-3 py-1.5 rounded-full border font-medium transition-colors whitespace-nowrap',
|
|
204
|
+
statusFilter === f.key
|
|
205
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
206
|
+
: 'bg-card text-muted-foreground border-border hover:bg-muted'
|
|
207
|
+
)}
|
|
208
|
+
>
|
|
209
|
+
{f.label} ({f.count})
|
|
210
|
+
</button>
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{/* Device List */}
|
|
215
|
+
{filteredDevices.length === 0 ? (
|
|
216
|
+
<div className="text-center py-12 border rounded-lg bg-muted/20">
|
|
217
|
+
<Monitor className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
|
|
218
|
+
<p className="text-sm text-muted-foreground">
|
|
219
|
+
{search
|
|
220
|
+
? 'No devices match your search.'
|
|
221
|
+
: 'No devices in this category.'}
|
|
222
|
+
</p>
|
|
223
|
+
</div>
|
|
224
|
+
) : (
|
|
225
|
+
<div className="space-y-3">
|
|
226
|
+
{filteredDevices.map((device) => (
|
|
227
|
+
<StaffDeviceCard key={device.id} device={device} />
|
|
228
|
+
))}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function StaffDeviceCard({ device }: { device: DeviceItem }) {
|
|
236
|
+
const isOOS = ['out_of_service', 'decommissioned'].includes(
|
|
237
|
+
device.currentStatusCode
|
|
238
|
+
);
|
|
239
|
+
const isMaint = [
|
|
240
|
+
'under_repair',
|
|
241
|
+
'under_maintenance',
|
|
242
|
+
'waiting_for_parts',
|
|
243
|
+
'under_investigation',
|
|
244
|
+
].includes(device.currentStatusCode);
|
|
245
|
+
const hasActiveTicket = device.activeTickets.length > 0;
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<Card
|
|
249
|
+
className={cn(
|
|
250
|
+
'shadow-sm overflow-hidden',
|
|
251
|
+
isOOS && 'border-red-200 dark:border-red-900',
|
|
252
|
+
isMaint && 'border-amber-200 dark:border-amber-900'
|
|
253
|
+
)}
|
|
254
|
+
>
|
|
255
|
+
{/* Status Banner for non-operational devices */}
|
|
256
|
+
{isOOS && (
|
|
257
|
+
<div className="bg-red-50 dark:bg-red-950/30 px-4 py-2 text-xs font-semibold text-red-700 dark:text-red-300 flex items-center gap-1.5 border-b border-red-200 dark:border-red-900">
|
|
258
|
+
<AlertTriangle className="w-3.5 h-3.5" />
|
|
259
|
+
OUT OF SERVICE — Do not use
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
{isMaint && (
|
|
263
|
+
<div className="bg-amber-50 dark:bg-amber-950/30 px-4 py-2 text-xs font-semibold text-amber-700 dark:text-amber-300 flex items-center gap-1.5 border-b border-amber-200 dark:border-amber-900">
|
|
264
|
+
<Wrench className="w-3.5 h-3.5" />
|
|
265
|
+
UNDER MAINTENANCE
|
|
266
|
+
</div>
|
|
267
|
+
)}
|
|
268
|
+
|
|
269
|
+
<CardContent className="p-4">
|
|
270
|
+
{/* Device Info */}
|
|
271
|
+
<div className="flex items-start justify-between gap-3">
|
|
272
|
+
<div className="min-w-0">
|
|
273
|
+
<h4 className="font-semibold text-sm truncate">{device.name}</h4>
|
|
274
|
+
<p className="text-xs text-muted-foreground truncate">
|
|
275
|
+
{device.assetNumber}
|
|
276
|
+
{device.locationName && ` · ${device.locationName}`}
|
|
277
|
+
</p>
|
|
278
|
+
{device.manufacturerName && (
|
|
279
|
+
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
|
280
|
+
{device.manufacturerName}
|
|
281
|
+
{device.categoryName && ` · ${device.categoryName}`}
|
|
282
|
+
</p>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
<DeviceStatusBadge status={device.currentStatusCode} />
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
{/* Active Ticket Alert */}
|
|
289
|
+
{hasActiveTicket && (
|
|
290
|
+
<div className="mt-3 space-y-1.5">
|
|
291
|
+
{device.activeTickets.map((ticket) => (
|
|
292
|
+
<Link
|
|
293
|
+
key={ticket.id}
|
|
294
|
+
href={`/tickets/${ticket.id}`}
|
|
295
|
+
className="flex items-center gap-2 text-xs bg-amber-50 dark:bg-amber-950/30 rounded-lg px-2.5 py-1.5 border border-amber-200 dark:border-amber-800 text-amber-800 dark:text-amber-200 hover:bg-amber-100 dark:hover:bg-amber-950/50 transition-colors"
|
|
296
|
+
>
|
|
297
|
+
<TicketCheck className="w-3.5 h-3.5 shrink-0" />
|
|
298
|
+
<span className="truncate">
|
|
299
|
+
<strong>{ticket.ticketNumber}</strong> — {ticket.title}
|
|
300
|
+
</span>
|
|
301
|
+
</Link>
|
|
302
|
+
))}
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
|
|
306
|
+
{/* Actions */}
|
|
307
|
+
<div className="mt-3 flex gap-2">
|
|
308
|
+
{hasActiveTicket ? (
|
|
309
|
+
<Button asChild variant="outline" size="sm" className="min-h-[36px]">
|
|
310
|
+
<Link href={`/tickets/${device.activeTickets[0].id}`}>
|
|
311
|
+
<TicketCheck className="mr-1.5 h-3.5 w-3.5" />
|
|
312
|
+
View Ticket
|
|
313
|
+
</Link>
|
|
314
|
+
</Button>
|
|
315
|
+
) : (
|
|
316
|
+
<Button asChild size="sm" className="min-h-[36px]">
|
|
317
|
+
<Link href={`/tickets/create?deviceId=${device.id}`}>
|
|
318
|
+
<PlusCircle className="mr-1.5 h-3.5 w-3.5" />
|
|
319
|
+
Report Problem
|
|
320
|
+
</Link>
|
|
321
|
+
</Button>
|
|
322
|
+
)}
|
|
323
|
+
<Button asChild variant="outline" size="sm" className="min-h-[36px]">
|
|
324
|
+
<Link href={`/devices/${device.id}`}>
|
|
325
|
+
<Monitor className="mr-1.5 h-3.5 w-3.5" />
|
|
326
|
+
View Device
|
|
327
|
+
</Link>
|
|
328
|
+
</Button>
|
|
329
|
+
</div>
|
|
330
|
+
</CardContent>
|
|
331
|
+
</Card>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { useRouter } from "next/navigation"
|
|
5
|
+
import { toast } from "sonner"
|
|
6
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
|
7
|
+
import { Button } from "@/components/ui/button"
|
|
8
|
+
import { Input } from "@/components/ui/input"
|
|
9
|
+
import { Label } from "@/components/ui/label"
|
|
10
|
+
import { Textarea } from "@/components/ui/textarea"
|
|
11
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
12
|
+
import { changeDeviceStatus } from "@/lib/actions/devices"
|
|
13
|
+
import { DeviceStatusBadge } from "./device-badges"
|
|
14
|
+
|
|
15
|
+
interface ChangeStatusDialogProps {
|
|
16
|
+
open: boolean;
|
|
17
|
+
onOpenChange: (open: boolean) => void;
|
|
18
|
+
deviceId: string;
|
|
19
|
+
deviceName: string;
|
|
20
|
+
currentStatus: string;
|
|
21
|
+
onSuccess: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ChangeStatusDialog({
|
|
25
|
+
open,
|
|
26
|
+
onOpenChange,
|
|
27
|
+
deviceId,
|
|
28
|
+
deviceName,
|
|
29
|
+
currentStatus,
|
|
30
|
+
onSuccess
|
|
31
|
+
}: ChangeStatusDialogProps) {
|
|
32
|
+
const router = useRouter()
|
|
33
|
+
const [loading, setLoading] = useState(false)
|
|
34
|
+
|
|
35
|
+
const [status, setStatus] = useState("")
|
|
36
|
+
const [reason, setReason] = useState("")
|
|
37
|
+
const [limitationsNote, setLimitationsNote] = useState("")
|
|
38
|
+
const [notes, setNotes] = useState("")
|
|
39
|
+
|
|
40
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
41
|
+
e.preventDefault()
|
|
42
|
+
if (!status || !reason) {
|
|
43
|
+
toast.error("Please fill in all required fields")
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setLoading(true)
|
|
48
|
+
try {
|
|
49
|
+
await changeDeviceStatus(deviceId, {
|
|
50
|
+
newStatusCode: status as any,
|
|
51
|
+
reason,
|
|
52
|
+
notes,
|
|
53
|
+
limitationsNote: status === 'operational_with_limitations' ? limitationsNote : undefined,
|
|
54
|
+
})
|
|
55
|
+
toast.success("Device status updated successfully")
|
|
56
|
+
onSuccess()
|
|
57
|
+
onOpenChange(false)
|
|
58
|
+
router.refresh()
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(error)
|
|
61
|
+
toast.error("Failed to update status")
|
|
62
|
+
} finally {
|
|
63
|
+
setLoading(false)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
69
|
+
<DialogContent className="sm:max-w-[500px]">
|
|
70
|
+
<form onSubmit={handleSubmit}>
|
|
71
|
+
<DialogHeader>
|
|
72
|
+
<DialogTitle>Change Device Status</DialogTitle>
|
|
73
|
+
</DialogHeader>
|
|
74
|
+
<div className="grid gap-4 py-4">
|
|
75
|
+
<div className="flex items-center gap-2 mb-2">
|
|
76
|
+
<span className="text-sm font-medium">Current Status:</span>
|
|
77
|
+
<DeviceStatusBadge status={currentStatus} />
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div className="grid gap-2">
|
|
81
|
+
<Label htmlFor="status">New Status *</Label>
|
|
82
|
+
<Select value={status} onValueChange={setStatus} required>
|
|
83
|
+
<SelectTrigger>
|
|
84
|
+
<SelectValue placeholder="Select new status" />
|
|
85
|
+
</SelectTrigger>
|
|
86
|
+
<SelectContent>
|
|
87
|
+
<SelectItem value="operational">Operational</SelectItem>
|
|
88
|
+
<SelectItem value="operational_with_limitations">Operational with Limitations</SelectItem>
|
|
89
|
+
<SelectItem value="under_maintenance">Under Maintenance</SelectItem>
|
|
90
|
+
<SelectItem value="under_repair">Under Repair</SelectItem>
|
|
91
|
+
<SelectItem value="waiting_for_parts">Waiting for Parts</SelectItem>
|
|
92
|
+
<SelectItem value="awaiting_release">Awaiting Release</SelectItem>
|
|
93
|
+
<SelectItem value="out_of_service">Out of Service</SelectItem>
|
|
94
|
+
<SelectItem value="standby">Standby</SelectItem>
|
|
95
|
+
</SelectContent>
|
|
96
|
+
</Select>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="grid gap-2">
|
|
100
|
+
<Label htmlFor="reason">Reason for Change *</Label>
|
|
101
|
+
<Input
|
|
102
|
+
id="reason"
|
|
103
|
+
placeholder="Brief reason for status change"
|
|
104
|
+
value={reason}
|
|
105
|
+
onChange={(e) => setReason(e.target.value)}
|
|
106
|
+
required
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{status === 'operational_with_limitations' && (
|
|
111
|
+
<div className="grid gap-2">
|
|
112
|
+
<Label htmlFor="limitations">Limitations Note *</Label>
|
|
113
|
+
<Textarea
|
|
114
|
+
id="limitations"
|
|
115
|
+
placeholder="Describe the operational limitations"
|
|
116
|
+
value={limitationsNote}
|
|
117
|
+
onChange={(e) => setLimitationsNote(e.target.value)}
|
|
118
|
+
required
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
<div className="grid gap-2">
|
|
124
|
+
<Label htmlFor="notes">Additional Notes (Optional)</Label>
|
|
125
|
+
<Textarea
|
|
126
|
+
id="notes"
|
|
127
|
+
placeholder="Any other relevant details"
|
|
128
|
+
value={notes}
|
|
129
|
+
onChange={(e) => setNotes(e.target.value)}
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<DialogFooter>
|
|
134
|
+
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
|
135
|
+
Cancel
|
|
136
|
+
</Button>
|
|
137
|
+
<Button type="submit" disabled={loading}>
|
|
138
|
+
{loading ? "Updating..." : "Update Status"}
|
|
139
|
+
</Button>
|
|
140
|
+
</DialogFooter>
|
|
141
|
+
</form>
|
|
142
|
+
</DialogContent>
|
|
143
|
+
</Dialog>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Button } from "@/components/ui/button"
|
|
5
|
+
import { AlertTriangle, QrCode, Edit, ArrowLeftRight, Activity } from "lucide-react"
|
|
6
|
+
import Link from "next/link"
|
|
7
|
+
import { TransferDeviceDialog } from "./transfer-device-dialog"
|
|
8
|
+
import { ChangeStatusDialog } from "./change-status-dialog"
|
|
9
|
+
import { useRouter } from "next/navigation"
|
|
10
|
+
|
|
11
|
+
export interface DeviceActionButtonsProps {
|
|
12
|
+
deviceId: string;
|
|
13
|
+
deviceName: string;
|
|
14
|
+
currentHospital: string;
|
|
15
|
+
currentDepartment: string;
|
|
16
|
+
currentLocation?: string;
|
|
17
|
+
currentStatus: string;
|
|
18
|
+
allowedActions?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function DeviceActionButtons({
|
|
22
|
+
deviceId,
|
|
23
|
+
deviceName,
|
|
24
|
+
currentHospital,
|
|
25
|
+
currentDepartment,
|
|
26
|
+
currentLocation,
|
|
27
|
+
currentStatus,
|
|
28
|
+
allowedActions = ["report_problem", "view_qr", "edit", "transfer", "change_status"],
|
|
29
|
+
}: DeviceActionButtonsProps) {
|
|
30
|
+
const router = useRouter()
|
|
31
|
+
const [transferOpen, setTransferOpen] = React.useState(false)
|
|
32
|
+
const [statusOpen, setStatusOpen] = React.useState(false)
|
|
33
|
+
|
|
34
|
+
const canReport = allowedActions.includes("report_problem")
|
|
35
|
+
const canViewQr = allowedActions.includes("view_qr")
|
|
36
|
+
const canEdit = allowedActions.includes("edit")
|
|
37
|
+
const canTransfer = allowedActions.includes("transfer")
|
|
38
|
+
const canChangeStatus = allowedActions.includes("change_status")
|
|
39
|
+
|
|
40
|
+
const handleRefresh = () => {
|
|
41
|
+
router.refresh()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
47
|
+
{canReport && (
|
|
48
|
+
<Link href={`/tickets/create?deviceId=${deviceId}&source=device_profile`}>
|
|
49
|
+
<Button
|
|
50
|
+
variant="default"
|
|
51
|
+
size="sm"
|
|
52
|
+
className="bg-amber-600 hover:bg-amber-700 text-white font-medium"
|
|
53
|
+
>
|
|
54
|
+
<AlertTriangle className="w-4 h-4 mr-1.5" />
|
|
55
|
+
Report Problem
|
|
56
|
+
</Button>
|
|
57
|
+
</Link>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{canViewQr && (
|
|
61
|
+
<Link href={`/devices/${deviceId}/qr`}>
|
|
62
|
+
<Button variant="outline" size="sm">
|
|
63
|
+
<QrCode className="w-4 h-4 mr-1.5" />
|
|
64
|
+
QR Code
|
|
65
|
+
</Button>
|
|
66
|
+
</Link>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{canEdit && (
|
|
70
|
+
<Link href={`/devices/${deviceId}/edit`}>
|
|
71
|
+
<Button variant="outline" size="sm">
|
|
72
|
+
<Edit className="w-4 h-4 mr-1.5" />
|
|
73
|
+
Edit
|
|
74
|
+
</Button>
|
|
75
|
+
</Link>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{canTransfer && (
|
|
79
|
+
<Button
|
|
80
|
+
variant="outline"
|
|
81
|
+
size="sm"
|
|
82
|
+
onClick={() => setTransferOpen(true)}
|
|
83
|
+
>
|
|
84
|
+
<ArrowLeftRight className="w-4 h-4 mr-1.5" />
|
|
85
|
+
Transfer
|
|
86
|
+
</Button>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{canChangeStatus && (
|
|
90
|
+
<Button
|
|
91
|
+
variant="outline"
|
|
92
|
+
size="sm"
|
|
93
|
+
onClick={() => setStatusOpen(true)}
|
|
94
|
+
>
|
|
95
|
+
<Activity className="w-4 h-4 mr-1.5" />
|
|
96
|
+
Change Status
|
|
97
|
+
</Button>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Transfer Dialog */}
|
|
102
|
+
<TransferDeviceDialog
|
|
103
|
+
open={transferOpen}
|
|
104
|
+
onOpenChange={setTransferOpen}
|
|
105
|
+
deviceId={deviceId}
|
|
106
|
+
deviceName={deviceName}
|
|
107
|
+
currentHospital={currentHospital}
|
|
108
|
+
currentDepartment={currentDepartment}
|
|
109
|
+
currentLocation={currentLocation}
|
|
110
|
+
onSuccess={handleRefresh}
|
|
111
|
+
/>
|
|
112
|
+
|
|
113
|
+
{/* Status Change Dialog */}
|
|
114
|
+
<ChangeStatusDialog
|
|
115
|
+
open={statusOpen}
|
|
116
|
+
onOpenChange={setStatusOpen}
|
|
117
|
+
deviceId={deviceId}
|
|
118
|
+
deviceName={deviceName}
|
|
119
|
+
currentStatus={currentStatus}
|
|
120
|
+
onSuccess={handleRefresh}
|
|
121
|
+
/>
|
|
122
|
+
</>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { AlertTriangle, CheckCircle, AlertCircle, Wrench, Clock } from "lucide-react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
const bannerConfig: Record<string, { icon: any; bg: string; border: string; text: string; message: string }> = {
|
|
5
|
+
operational: {
|
|
6
|
+
icon: CheckCircle, bg: "bg-green-50", border: "border-green-300", text: "text-green-800",
|
|
7
|
+
message: "This device is operational and available for use.",
|
|
8
|
+
},
|
|
9
|
+
operational_with_limitations: {
|
|
10
|
+
icon: AlertTriangle, bg: "bg-amber-50", border: "border-amber-300", text: "text-amber-800",
|
|
11
|
+
message: "This device is operational with limitations. Check notes below before use.",
|
|
12
|
+
},
|
|
13
|
+
under_maintenance: {
|
|
14
|
+
icon: Wrench, bg: "bg-blue-50", border: "border-blue-300", text: "text-blue-800",
|
|
15
|
+
message: "This device is currently under maintenance. Do not use until released by Biomedical Engineering.",
|
|
16
|
+
},
|
|
17
|
+
under_repair: {
|
|
18
|
+
icon: Wrench, bg: "bg-orange-50", border: "border-orange-300", text: "text-orange-800",
|
|
19
|
+
message: "This device is under repair. Do not use until released by Biomedical Engineering.",
|
|
20
|
+
},
|
|
21
|
+
waiting_for_parts: {
|
|
22
|
+
icon: Clock, bg: "bg-yellow-50", border: "border-yellow-300", text: "text-yellow-800",
|
|
23
|
+
message: "This device is waiting for parts. Do not use.",
|
|
24
|
+
},
|
|
25
|
+
awaiting_release: {
|
|
26
|
+
icon: Clock, bg: "bg-purple-50", border: "border-purple-300", text: "text-purple-800",
|
|
27
|
+
message: "This device is awaiting release after maintenance. Do not use until formally released.",
|
|
28
|
+
},
|
|
29
|
+
out_of_service: {
|
|
30
|
+
icon: AlertCircle, bg: "bg-red-50", border: "border-red-300", text: "text-red-800",
|
|
31
|
+
message: "OUT OF SERVICE — Do not use until released by Biomedical Engineering.",
|
|
32
|
+
},
|
|
33
|
+
standby: {
|
|
34
|
+
icon: Clock, bg: "bg-gray-50", border: "border-gray-300", text: "text-gray-800",
|
|
35
|
+
message: "This device is on standby.",
|
|
36
|
+
},
|
|
37
|
+
decommissioned: {
|
|
38
|
+
icon: AlertCircle, bg: "bg-slate-100", border: "border-slate-300", text: "text-slate-600",
|
|
39
|
+
message: "This device has been decommissioned and is no longer in service.",
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function DeviceAvailabilityBanner({ status, limitationsNote }: { status: string; limitationsNote?: string | null }) {
|
|
44
|
+
const config = bannerConfig[status] || bannerConfig.operational
|
|
45
|
+
const Icon = config.icon
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={cn("rounded-lg border-2 p-4 flex items-start gap-3", config.bg, config.border)}>
|
|
49
|
+
<Icon className={cn("w-6 h-6 mt-0.5 flex-shrink-0", config.text)} />
|
|
50
|
+
<div>
|
|
51
|
+
<p className={cn("font-semibold text-base", config.text)}>{config.message}</p>
|
|
52
|
+
{limitationsNote && status === 'operational_with_limitations' && (
|
|
53
|
+
<p className={cn("mt-1 text-sm", config.text)}>Note: {limitationsNote}</p>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|