@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,467 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
6
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
7
|
+
import { Badge } from '@/components/ui/badge';
|
|
8
|
+
import { Button } from '@/components/ui/button';
|
|
9
|
+
import { Input } from '@/components/ui/input';
|
|
10
|
+
import { DeviceStatusBadge, CriticalityBadge } from '@/components/devices/device-badges';
|
|
11
|
+
import { TicketStatusBadge, TicketPriorityBadge } from '@/components/tickets/ticket-badges';
|
|
12
|
+
import {
|
|
13
|
+
Monitor,
|
|
14
|
+
Ticket,
|
|
15
|
+
MapPin,
|
|
16
|
+
Search,
|
|
17
|
+
Plus,
|
|
18
|
+
QrCode,
|
|
19
|
+
Edit,
|
|
20
|
+
ArrowLeft,
|
|
21
|
+
ChevronRight,
|
|
22
|
+
User,
|
|
23
|
+
AlertTriangle,
|
|
24
|
+
Clock,
|
|
25
|
+
} from 'lucide-react';
|
|
26
|
+
import { cn } from '@/lib/utils';
|
|
27
|
+
import { getDepartmentVisuals } from './department-visuals';
|
|
28
|
+
|
|
29
|
+
interface DepartmentDetailViewProps {
|
|
30
|
+
department: {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
code: string;
|
|
34
|
+
departmentType?: string | null;
|
|
35
|
+
status?: string | null;
|
|
36
|
+
hospitalName?: string | null;
|
|
37
|
+
managerName?: string | null;
|
|
38
|
+
organizationId?: string;
|
|
39
|
+
};
|
|
40
|
+
devices: any[];
|
|
41
|
+
tickets: any[];
|
|
42
|
+
locations: any[];
|
|
43
|
+
stats: {
|
|
44
|
+
totalDevices: number;
|
|
45
|
+
operationalDevices: number;
|
|
46
|
+
outOfServiceDevices: number;
|
|
47
|
+
activeTicketsCount: number;
|
|
48
|
+
operationalRate: number;
|
|
49
|
+
};
|
|
50
|
+
isAdmin: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function DepartmentDetailView({
|
|
54
|
+
department,
|
|
55
|
+
devices,
|
|
56
|
+
tickets,
|
|
57
|
+
locations,
|
|
58
|
+
stats,
|
|
59
|
+
isAdmin,
|
|
60
|
+
}: DepartmentDetailViewProps) {
|
|
61
|
+
const visual = getDepartmentVisuals(department.name, department.code, department.departmentType);
|
|
62
|
+
const DeptIcon = visual.icon;
|
|
63
|
+
|
|
64
|
+
const [deviceSearch, setDeviceSearch] = useState('');
|
|
65
|
+
const [deviceStatusFilter, setDeviceStatusFilter] = useState<string>('all');
|
|
66
|
+
const [ticketSearch, setTicketSearch] = useState('');
|
|
67
|
+
|
|
68
|
+
const filteredDevices = useMemo(() => {
|
|
69
|
+
return devices.filter((device) => {
|
|
70
|
+
// Status filter
|
|
71
|
+
if (deviceStatusFilter !== 'all') {
|
|
72
|
+
if (deviceStatusFilter === 'operational' && device.currentStatusCode !== 'operational') {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (
|
|
76
|
+
deviceStatusFilter === 'issues' &&
|
|
77
|
+
!['out_of_service', 'under_repair', 'under_maintenance'].includes(device.currentStatusCode)
|
|
78
|
+
) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Text search
|
|
84
|
+
if (deviceSearch.trim()) {
|
|
85
|
+
const q = deviceSearch.toLowerCase().trim();
|
|
86
|
+
const matchesName = device.name?.toLowerCase().includes(q) || false;
|
|
87
|
+
const matchesAsset = device.assetNumber?.toLowerCase().includes(q) || false;
|
|
88
|
+
const matchesCode = device.internalCode?.toLowerCase().includes(q) || false;
|
|
89
|
+
const matchesCategory = device.deviceCategory?.name?.toLowerCase().includes(q) || false;
|
|
90
|
+
const matchesModel = device.deviceModel?.modelName?.toLowerCase().includes(q) || false;
|
|
91
|
+
const matchesLocation = device.location?.name?.toLowerCase().includes(q) || false;
|
|
92
|
+
return matchesName || matchesAsset || matchesCode || matchesCategory || matchesModel || matchesLocation;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return true;
|
|
96
|
+
});
|
|
97
|
+
}, [devices, deviceSearch, deviceStatusFilter]);
|
|
98
|
+
|
|
99
|
+
const filteredTickets = useMemo(() => {
|
|
100
|
+
return tickets.filter((ticket) => {
|
|
101
|
+
if (!ticketSearch.trim()) return true;
|
|
102
|
+
const q = ticketSearch.toLowerCase().trim();
|
|
103
|
+
const matchesNum = ticket.ticketNumber?.toLowerCase().includes(q) || false;
|
|
104
|
+
const matchesTitle = ticket.title?.toLowerCase().includes(q) || false;
|
|
105
|
+
const matchesDevice = ticket.device?.name?.toLowerCase().includes(q) || false;
|
|
106
|
+
return matchesNum || matchesTitle || matchesDevice;
|
|
107
|
+
});
|
|
108
|
+
}, [tickets, ticketSearch]);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="space-y-6 max-w-7xl mx-auto pb-12">
|
|
112
|
+
{/* Breadcrumb & Navigation */}
|
|
113
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
114
|
+
<Link
|
|
115
|
+
href="/departments"
|
|
116
|
+
className="inline-flex items-center gap-1 hover:text-foreground transition-colors min-h-[36px] py-1"
|
|
117
|
+
>
|
|
118
|
+
<ArrowLeft className="w-4 h-4" />
|
|
119
|
+
<span>All Departments</span>
|
|
120
|
+
</Link>
|
|
121
|
+
<span>/</span>
|
|
122
|
+
<span className="text-foreground font-medium truncate">{department.name}</span>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{/* Department Summary Header Card */}
|
|
126
|
+
<Card className="border shadow-sm overflow-hidden">
|
|
127
|
+
<CardContent className="p-5 sm:p-6 space-y-6">
|
|
128
|
+
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
|
129
|
+
<div className="flex items-start gap-3.5">
|
|
130
|
+
<div className={cn("p-3 rounded-2xl shrink-0 mt-0.5", visual.detailWrapperClass)}>
|
|
131
|
+
<DeptIcon className="w-7 h-7" />
|
|
132
|
+
</div>
|
|
133
|
+
<div className="space-y-1">
|
|
134
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
135
|
+
<h1 className="text-xl sm:text-2xl font-bold tracking-tight">{department.name}</h1>
|
|
136
|
+
<Badge variant="outline" className="font-mono text-xs">
|
|
137
|
+
{department.code}
|
|
138
|
+
</Badge>
|
|
139
|
+
<Badge variant="outline" className={cn("text-xs font-medium", visual.badgeClass)}>
|
|
140
|
+
{department.departmentType || visual.badgeLabel}
|
|
141
|
+
</Badge>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div className="flex flex-wrap items-center gap-y-1 gap-x-3 text-xs sm:text-sm text-muted-foreground">
|
|
145
|
+
{department.hospitalName && (
|
|
146
|
+
<span className="flex items-center gap-1">
|
|
147
|
+
<MapPin className="w-3.5 h-3.5" />
|
|
148
|
+
{department.hospitalName}
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
{department.managerName && (
|
|
152
|
+
<span className="flex items-center gap-1">
|
|
153
|
+
<User className="w-3.5 h-3.5" />
|
|
154
|
+
Manager: {department.managerName}
|
|
155
|
+
</span>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Actions */}
|
|
162
|
+
<div className="flex flex-wrap items-center gap-2 self-stretch sm:self-auto">
|
|
163
|
+
<Button asChild size="sm" variant="outline" className="min-h-[40px] flex-1 sm:flex-none">
|
|
164
|
+
<Link href="/scan">
|
|
165
|
+
<QrCode className="w-4 h-4 mr-1.5" />
|
|
166
|
+
Scan QR
|
|
167
|
+
</Link>
|
|
168
|
+
</Button>
|
|
169
|
+
<Button asChild size="sm" className="min-h-[40px] flex-1 sm:flex-none">
|
|
170
|
+
<Link href="/tickets/create">
|
|
171
|
+
<Plus className="w-4 h-4 mr-1.5" />
|
|
172
|
+
Report Issue
|
|
173
|
+
</Link>
|
|
174
|
+
</Button>
|
|
175
|
+
{isAdmin && (
|
|
176
|
+
<Button asChild size="sm" variant="ghost" className="min-h-[40px]">
|
|
177
|
+
<Link href={`/admin/departments/${department.id}/edit`}>
|
|
178
|
+
<Edit className="w-4 h-4" />
|
|
179
|
+
</Link>
|
|
180
|
+
</Button>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Operational Metrics Bar */}
|
|
186
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 pt-2 border-t">
|
|
187
|
+
<div className="p-3 bg-muted/40 rounded-xl border">
|
|
188
|
+
<div className="text-xs text-muted-foreground">Total Equipment</div>
|
|
189
|
+
<div className="text-2xl font-bold mt-1 text-primary">{stats.totalDevices}</div>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="p-3 bg-emerald-50/50 dark:bg-emerald-950/20 rounded-xl border border-emerald-100 dark:border-emerald-900/30">
|
|
192
|
+
<div className="text-xs text-emerald-700 dark:text-emerald-400">Operational</div>
|
|
193
|
+
<div className="text-2xl font-bold mt-1 text-emerald-600 dark:text-emerald-400">
|
|
194
|
+
{stats.operationalDevices}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
<div className={`p-3 rounded-xl border ${
|
|
198
|
+
stats.outOfServiceDevices > 0
|
|
199
|
+
? 'bg-red-50/50 dark:bg-red-950/20 border-red-100 dark:border-red-900/30'
|
|
200
|
+
: 'bg-muted/40'
|
|
201
|
+
}`}>
|
|
202
|
+
<div className={`text-xs ${stats.outOfServiceDevices > 0 ? 'text-red-700 dark:text-red-400' : 'text-muted-foreground'}`}>
|
|
203
|
+
Down / Repair
|
|
204
|
+
</div>
|
|
205
|
+
<div className={`text-2xl font-bold mt-1 ${stats.outOfServiceDevices > 0 ? 'text-red-600 dark:text-red-400' : 'text-muted-foreground'}`}>
|
|
206
|
+
{stats.outOfServiceDevices}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
<div className={`p-3 rounded-xl border ${
|
|
210
|
+
stats.activeTicketsCount > 0
|
|
211
|
+
? 'bg-amber-50/50 dark:bg-amber-950/20 border-amber-100 dark:border-amber-900/30'
|
|
212
|
+
: 'bg-muted/40'
|
|
213
|
+
}`}>
|
|
214
|
+
<div className={`text-xs ${stats.activeTicketsCount > 0 ? 'text-amber-700 dark:text-amber-400' : 'text-muted-foreground'}`}>
|
|
215
|
+
Active Tickets
|
|
216
|
+
</div>
|
|
217
|
+
<div className={`text-2xl font-bold mt-1 ${stats.activeTicketsCount > 0 ? 'text-amber-600 dark:text-amber-400' : 'text-muted-foreground'}`}>
|
|
218
|
+
{stats.activeTicketsCount}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</CardContent>
|
|
223
|
+
</Card>
|
|
224
|
+
|
|
225
|
+
{/* Segmented Tabs: Devices | Tickets | Locations */}
|
|
226
|
+
<Tabs defaultValue="devices" className="space-y-4">
|
|
227
|
+
<TabsList className="grid w-full grid-cols-3 max-w-md h-11 p-1 bg-muted/70">
|
|
228
|
+
<TabsTrigger value="devices" className="text-xs sm:text-sm gap-1.5">
|
|
229
|
+
<Monitor className="w-4 h-4" />
|
|
230
|
+
<span>Devices</span>
|
|
231
|
+
<span className="text-[11px] px-1.5 py-0.2 rounded-full bg-primary/10 text-primary font-medium">
|
|
232
|
+
{devices.length}
|
|
233
|
+
</span>
|
|
234
|
+
</TabsTrigger>
|
|
235
|
+
<TabsTrigger value="tickets" className="text-xs sm:text-sm gap-1.5">
|
|
236
|
+
<Ticket className="w-4 h-4" />
|
|
237
|
+
<span>Tickets</span>
|
|
238
|
+
{tickets.length > 0 && (
|
|
239
|
+
<span className="text-[11px] px-1.5 py-0.2 rounded-full bg-amber-100 dark:bg-amber-900 text-amber-700 dark:text-amber-300 font-medium">
|
|
240
|
+
{tickets.length}
|
|
241
|
+
</span>
|
|
242
|
+
)}
|
|
243
|
+
</TabsTrigger>
|
|
244
|
+
<TabsTrigger value="locations" className="text-xs sm:text-sm gap-1.5">
|
|
245
|
+
<MapPin className="w-4 h-4" />
|
|
246
|
+
<span>Rooms</span>
|
|
247
|
+
<span className="text-[11px] px-1.5 py-0.2 rounded-full bg-muted font-medium">
|
|
248
|
+
{locations.length}
|
|
249
|
+
</span>
|
|
250
|
+
</TabsTrigger>
|
|
251
|
+
</TabsList>
|
|
252
|
+
|
|
253
|
+
{/* ── Tab 1: Devices ── */}
|
|
254
|
+
<TabsContent value="devices" className="space-y-4 focus:outline-none">
|
|
255
|
+
{/* Controls: Search & Status filter chips */}
|
|
256
|
+
<div className="flex flex-col sm:flex-row sm:items-center gap-3 justify-between">
|
|
257
|
+
<div className="relative flex-1 max-w-md">
|
|
258
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
259
|
+
<Input
|
|
260
|
+
placeholder="Search department devices by name, asset #, category..."
|
|
261
|
+
value={deviceSearch}
|
|
262
|
+
onChange={(e) => setDeviceSearch(e.target.value)}
|
|
263
|
+
className="pl-9 bg-card"
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<div className="flex items-center gap-1.5 overflow-x-auto pb-1 sm:pb-0 scrollbar-none">
|
|
268
|
+
<button
|
|
269
|
+
onClick={() => setDeviceStatusFilter('all')}
|
|
270
|
+
className={`px-3 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-colors min-h-[36px] ${
|
|
271
|
+
deviceStatusFilter === 'all'
|
|
272
|
+
? 'bg-primary text-primary-foreground shadow-sm'
|
|
273
|
+
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
|
274
|
+
}`}
|
|
275
|
+
>
|
|
276
|
+
All ({devices.length})
|
|
277
|
+
</button>
|
|
278
|
+
<button
|
|
279
|
+
onClick={() => setDeviceStatusFilter('operational')}
|
|
280
|
+
className={`px-3 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-colors min-h-[36px] ${
|
|
281
|
+
deviceStatusFilter === 'operational'
|
|
282
|
+
? 'bg-emerald-600 text-white shadow-sm'
|
|
283
|
+
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
|
284
|
+
}`}
|
|
285
|
+
>
|
|
286
|
+
Operational ({stats.operationalDevices})
|
|
287
|
+
</button>
|
|
288
|
+
<button
|
|
289
|
+
onClick={() => setDeviceStatusFilter('issues')}
|
|
290
|
+
className={`px-3 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-colors min-h-[36px] ${
|
|
291
|
+
deviceStatusFilter === 'issues'
|
|
292
|
+
? 'bg-red-600 text-white shadow-sm'
|
|
293
|
+
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
|
294
|
+
}`}
|
|
295
|
+
>
|
|
296
|
+
Down / Repair ({stats.outOfServiceDevices})
|
|
297
|
+
</button>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{/* Devices List */}
|
|
302
|
+
{filteredDevices.length === 0 ? (
|
|
303
|
+
<Card className="border-dashed">
|
|
304
|
+
<CardContent className="py-12 text-center space-y-3">
|
|
305
|
+
<Monitor className="w-10 h-10 mx-auto text-muted-foreground/50" />
|
|
306
|
+
<p className="text-base font-medium">No equipment found</p>
|
|
307
|
+
<p className="text-sm text-muted-foreground">
|
|
308
|
+
No devices match your search or filter in this department.
|
|
309
|
+
</p>
|
|
310
|
+
</CardContent>
|
|
311
|
+
</Card>
|
|
312
|
+
) : (
|
|
313
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
314
|
+
{filteredDevices.map((device) => (
|
|
315
|
+
<Link
|
|
316
|
+
key={device.id}
|
|
317
|
+
href={`/devices/${device.id}`}
|
|
318
|
+
className="group block focus:outline-none"
|
|
319
|
+
>
|
|
320
|
+
<Card className="h-full border transition-all duration-200 hover:shadow-md hover:border-primary/40 group-active:scale-[0.99] cursor-pointer">
|
|
321
|
+
<CardContent className="p-4 space-y-3">
|
|
322
|
+
{/* Top row: Code + Status Badge */}
|
|
323
|
+
<div className="flex items-start justify-between gap-2">
|
|
324
|
+
<span className="font-mono text-xs font-semibold text-primary group-hover:underline">
|
|
325
|
+
{device.internalCode || device.assetNumber}
|
|
326
|
+
</span>
|
|
327
|
+
<DeviceStatusBadge status={device.currentStatusCode} />
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
{/* Device Name & Details */}
|
|
331
|
+
<div>
|
|
332
|
+
<h4 className="font-semibold text-sm leading-snug group-hover:text-primary transition-colors line-clamp-1">
|
|
333
|
+
{device.name}
|
|
334
|
+
</h4>
|
|
335
|
+
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-1">
|
|
336
|
+
{device.manufacturer?.name || 'Unknown Mfg'} {device.modelNumber ? `• ${device.modelNumber}` : ''}
|
|
337
|
+
</p>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
{/* Bottom row: Asset Tag + Location + Criticality */}
|
|
341
|
+
<div className="flex items-center justify-between text-xs pt-2 border-t text-muted-foreground">
|
|
342
|
+
<span className="truncate">
|
|
343
|
+
{device.location?.name || 'Main Dept Area'}
|
|
344
|
+
</span>
|
|
345
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
346
|
+
{device.criticalityLevel && (
|
|
347
|
+
<CriticalityBadge level={device.criticalityLevel} />
|
|
348
|
+
)}
|
|
349
|
+
<ChevronRight className="w-3.5 h-3.5 text-muted-foreground/60 group-hover:text-primary group-hover:translate-x-0.5 transition-all" />
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
</CardContent>
|
|
353
|
+
</Card>
|
|
354
|
+
</Link>
|
|
355
|
+
))}
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
</TabsContent>
|
|
359
|
+
|
|
360
|
+
{/* ── Tab 2: Active Tickets ── */}
|
|
361
|
+
<TabsContent value="tickets" className="space-y-4 focus:outline-none">
|
|
362
|
+
<div className="relative max-w-md">
|
|
363
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
364
|
+
<Input
|
|
365
|
+
placeholder="Search tickets by number, title, device..."
|
|
366
|
+
value={ticketSearch}
|
|
367
|
+
onChange={(e) => setTicketSearch(e.target.value)}
|
|
368
|
+
className="pl-9 bg-card"
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
{filteredTickets.length === 0 ? (
|
|
373
|
+
<Card className="border-dashed">
|
|
374
|
+
<CardContent className="py-12 text-center space-y-3">
|
|
375
|
+
<Ticket className="w-10 h-10 mx-auto text-muted-foreground/50" />
|
|
376
|
+
<p className="text-base font-medium">No active tickets</p>
|
|
377
|
+
<p className="text-sm text-muted-foreground">
|
|
378
|
+
All equipment in this department is free of open service tickets.
|
|
379
|
+
</p>
|
|
380
|
+
<Button asChild size="sm" className="mt-2">
|
|
381
|
+
<Link href="/tickets/create">Report an Issue</Link>
|
|
382
|
+
</Button>
|
|
383
|
+
</CardContent>
|
|
384
|
+
</Card>
|
|
385
|
+
) : (
|
|
386
|
+
<div className="space-y-3">
|
|
387
|
+
{filteredTickets.map((ticket) => (
|
|
388
|
+
<Link
|
|
389
|
+
key={ticket.id}
|
|
390
|
+
href={`/tickets/${ticket.id}`}
|
|
391
|
+
className="block group focus:outline-none"
|
|
392
|
+
>
|
|
393
|
+
<Card className="border hover:shadow-sm hover:border-primary/40 transition-all p-4 group-active:scale-[0.99]">
|
|
394
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
395
|
+
<div className="space-y-1.5">
|
|
396
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
397
|
+
<span className="font-mono text-xs font-semibold text-primary">
|
|
398
|
+
{ticket.ticketNumber}
|
|
399
|
+
</span>
|
|
400
|
+
<TicketPriorityBadge priority={ticket.priorityCode} />
|
|
401
|
+
<TicketStatusBadge status={ticket.statusCode} />
|
|
402
|
+
</div>
|
|
403
|
+
<h4 className="font-medium text-sm text-foreground group-hover:text-primary transition-colors">
|
|
404
|
+
{ticket.title}
|
|
405
|
+
</h4>
|
|
406
|
+
{ticket.device && (
|
|
407
|
+
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
|
408
|
+
<Monitor className="w-3 h-3" />
|
|
409
|
+
{ticket.device.name} ({ticket.device.assetNumber})
|
|
410
|
+
</p>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground self-start sm:self-auto shrink-0">
|
|
415
|
+
<Clock className="w-3.5 h-3.5" />
|
|
416
|
+
<span>{new Date(ticket.reportedAt).toLocaleDateString()}</span>
|
|
417
|
+
<ChevronRight className="w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors ml-1" />
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</Card>
|
|
421
|
+
</Link>
|
|
422
|
+
))}
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
</TabsContent>
|
|
426
|
+
|
|
427
|
+
{/* ── Tab 3: Locations / Rooms ── */}
|
|
428
|
+
<TabsContent value="locations" className="space-y-4 focus:outline-none">
|
|
429
|
+
{locations.length === 0 ? (
|
|
430
|
+
<Card className="border-dashed">
|
|
431
|
+
<CardContent className="py-12 text-center space-y-3">
|
|
432
|
+
<MapPin className="w-10 h-10 mx-auto text-muted-foreground/50" />
|
|
433
|
+
<p className="text-base font-medium">No specific rooms configured</p>
|
|
434
|
+
<p className="text-sm text-muted-foreground">
|
|
435
|
+
All devices in this department are currently registered under the main department floor.
|
|
436
|
+
</p>
|
|
437
|
+
</CardContent>
|
|
438
|
+
</Card>
|
|
439
|
+
) : (
|
|
440
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
441
|
+
{locations.map((loc) => {
|
|
442
|
+
const roomDevices = devices.filter((d) => d.locationId === loc.id);
|
|
443
|
+
return (
|
|
444
|
+
<Card key={loc.id} className="border p-4">
|
|
445
|
+
<div className="flex items-start justify-between gap-2">
|
|
446
|
+
<div className="space-y-1">
|
|
447
|
+
<div className="flex items-center gap-1.5">
|
|
448
|
+
<MapPin className="w-4 h-4 text-primary" />
|
|
449
|
+
<h4 className="font-semibold text-sm">{loc.name}</h4>
|
|
450
|
+
</div>
|
|
451
|
+
{loc.code && <p className="font-mono text-xs text-muted-foreground">{loc.code}</p>}
|
|
452
|
+
</div>
|
|
453
|
+
<Badge variant="secondary" className="text-xs">
|
|
454
|
+
{roomDevices.length} {roomDevices.length === 1 ? 'Device' : 'Devices'}
|
|
455
|
+
</Badge>
|
|
456
|
+
</div>
|
|
457
|
+
</Card>
|
|
458
|
+
);
|
|
459
|
+
})}
|
|
460
|
+
</div>
|
|
461
|
+
)}
|
|
462
|
+
</TabsContent>
|
|
463
|
+
</Tabs>
|
|
464
|
+
</div>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { LucideIcon } from 'lucide-react';
|
|
2
|
+
import {
|
|
3
|
+
Siren,
|
|
4
|
+
Activity,
|
|
5
|
+
Scissors,
|
|
6
|
+
Microscope,
|
|
7
|
+
Scan,
|
|
8
|
+
Baby,
|
|
9
|
+
Smile,
|
|
10
|
+
Building2,
|
|
11
|
+
} from 'lucide-react';
|
|
12
|
+
|
|
13
|
+
export interface DepartmentVisualConfig {
|
|
14
|
+
icon: LucideIcon;
|
|
15
|
+
label: string;
|
|
16
|
+
badgeLabel: string;
|
|
17
|
+
iconWrapperClass: string;
|
|
18
|
+
detailWrapperClass: string;
|
|
19
|
+
badgeClass: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Maps hospital departments to dedicated clinical icons and color palettes.
|
|
24
|
+
* Specifically configured for the 7 primary hospital departments:
|
|
25
|
+
* 1. Emergency Department -> Siren (Rose/Red)
|
|
26
|
+
* 2. Intensive Care Unit (ICU) -> Activity (Cyan/Teal)
|
|
27
|
+
* 3. Surgery & Operating Theatre -> Scissors (Indigo/Violet)
|
|
28
|
+
* 4. Clinical Laboratory -> Microscope (Emerald/Green)
|
|
29
|
+
* 5. Radiology & Medical Imaging -> Scan (Sky/Blue)
|
|
30
|
+
* 6. Pediatrics -> Baby (Amber/Orange)
|
|
31
|
+
* 7. Dental Clinic -> Smile (Teal/Emerald)
|
|
32
|
+
*/
|
|
33
|
+
export function getDepartmentVisuals(
|
|
34
|
+
name: string,
|
|
35
|
+
code?: string | null,
|
|
36
|
+
type?: string | null
|
|
37
|
+
): DepartmentVisualConfig {
|
|
38
|
+
const text = `${name} ${code || ''} ${type || ''}`.toLowerCase();
|
|
39
|
+
|
|
40
|
+
// 1. Emergency Department
|
|
41
|
+
if (
|
|
42
|
+
text.includes('emerg') ||
|
|
43
|
+
text.includes('trauma') ||
|
|
44
|
+
text.includes('er') ||
|
|
45
|
+
text.includes('urgenc')
|
|
46
|
+
) {
|
|
47
|
+
return {
|
|
48
|
+
icon: Siren,
|
|
49
|
+
label: 'Emergency',
|
|
50
|
+
badgeLabel: 'Emergency',
|
|
51
|
+
iconWrapperClass:
|
|
52
|
+
'bg-rose-100 text-rose-600 dark:bg-rose-950/50 dark:text-rose-400 group-hover:bg-rose-600 group-hover:text-white',
|
|
53
|
+
detailWrapperClass: 'bg-rose-100 text-rose-600 dark:bg-rose-950/60 dark:text-rose-400',
|
|
54
|
+
badgeClass:
|
|
55
|
+
'border-rose-200 text-rose-700 bg-rose-50 dark:border-rose-800 dark:text-rose-300 dark:bg-rose-950/40',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Intensive Care Unit (ICU)
|
|
60
|
+
if (
|
|
61
|
+
text.includes('icu') ||
|
|
62
|
+
text.includes('intensive') ||
|
|
63
|
+
text.includes('critical') ||
|
|
64
|
+
text.includes('ccu')
|
|
65
|
+
) {
|
|
66
|
+
return {
|
|
67
|
+
icon: Activity,
|
|
68
|
+
label: 'Intensive Care',
|
|
69
|
+
badgeLabel: 'Critical Care',
|
|
70
|
+
iconWrapperClass:
|
|
71
|
+
'bg-cyan-100 text-cyan-700 dark:bg-cyan-950/50 dark:text-cyan-400 group-hover:bg-cyan-600 group-hover:text-white',
|
|
72
|
+
detailWrapperClass: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-950/60 dark:text-cyan-400',
|
|
73
|
+
badgeClass:
|
|
74
|
+
'border-cyan-200 text-cyan-700 bg-cyan-50 dark:border-cyan-800 dark:text-cyan-300 dark:bg-cyan-950/40',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Surgery & Operating Theatre
|
|
79
|
+
if (
|
|
80
|
+
text.includes('surg') ||
|
|
81
|
+
text.includes('operat') ||
|
|
82
|
+
text.includes('theatre') ||
|
|
83
|
+
text.includes('theater') ||
|
|
84
|
+
text.includes('or')
|
|
85
|
+
) {
|
|
86
|
+
return {
|
|
87
|
+
icon: Scissors,
|
|
88
|
+
label: 'Surgery',
|
|
89
|
+
badgeLabel: 'Surgical Unit',
|
|
90
|
+
iconWrapperClass:
|
|
91
|
+
'bg-indigo-100 text-indigo-700 dark:bg-indigo-950/50 dark:text-indigo-400 group-hover:bg-indigo-600 group-hover:text-white',
|
|
92
|
+
detailWrapperClass: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-950/60 dark:text-indigo-400',
|
|
93
|
+
badgeClass:
|
|
94
|
+
'border-indigo-200 text-indigo-700 bg-indigo-50 dark:border-indigo-800 dark:text-indigo-300 dark:bg-indigo-950/40',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 4. Clinical Laboratory
|
|
99
|
+
if (
|
|
100
|
+
text.includes('lab') ||
|
|
101
|
+
text.includes('patholog') ||
|
|
102
|
+
text.includes('hematol') ||
|
|
103
|
+
text.includes('microbiol')
|
|
104
|
+
) {
|
|
105
|
+
return {
|
|
106
|
+
icon: Microscope,
|
|
107
|
+
label: 'Laboratory',
|
|
108
|
+
badgeLabel: 'Diagnostics',
|
|
109
|
+
iconWrapperClass:
|
|
110
|
+
'bg-emerald-100 text-emerald-700 dark:bg-emerald-950/50 dark:text-emerald-400 group-hover:bg-emerald-600 group-hover:text-white',
|
|
111
|
+
detailWrapperClass: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-400',
|
|
112
|
+
badgeClass:
|
|
113
|
+
'border-emerald-200 text-emerald-700 bg-emerald-50 dark:border-emerald-800 dark:text-emerald-300 dark:bg-emerald-950/40',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 5. Radiology & Medical Imaging
|
|
118
|
+
if (
|
|
119
|
+
text.includes('radio') ||
|
|
120
|
+
text.includes('imag') ||
|
|
121
|
+
text.includes('x-ray') ||
|
|
122
|
+
text.includes('xray') ||
|
|
123
|
+
text.includes('mri') ||
|
|
124
|
+
text.includes('ct scan') ||
|
|
125
|
+
text.includes('ultrasound')
|
|
126
|
+
) {
|
|
127
|
+
return {
|
|
128
|
+
icon: Scan,
|
|
129
|
+
label: 'Radiology',
|
|
130
|
+
badgeLabel: 'Imaging',
|
|
131
|
+
iconWrapperClass:
|
|
132
|
+
'bg-sky-100 text-sky-700 dark:bg-sky-950/50 dark:text-sky-400 group-hover:bg-sky-600 group-hover:text-white',
|
|
133
|
+
detailWrapperClass: 'bg-sky-100 text-sky-700 dark:bg-sky-950/60 dark:text-sky-400',
|
|
134
|
+
badgeClass:
|
|
135
|
+
'border-sky-200 text-sky-700 bg-sky-50 dark:border-sky-800 dark:text-sky-300 dark:bg-sky-950/40',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 6. Pediatrics
|
|
140
|
+
if (
|
|
141
|
+
text.includes('pedia') ||
|
|
142
|
+
text.includes('child') ||
|
|
143
|
+
text.includes('baby') ||
|
|
144
|
+
text.includes('neonat') ||
|
|
145
|
+
text.includes('nicu')
|
|
146
|
+
) {
|
|
147
|
+
return {
|
|
148
|
+
icon: Baby,
|
|
149
|
+
label: 'Pediatrics',
|
|
150
|
+
badgeLabel: 'Pediatric Care',
|
|
151
|
+
iconWrapperClass:
|
|
152
|
+
'bg-amber-100 text-amber-700 dark:bg-amber-950/50 dark:text-amber-400 group-hover:bg-amber-600 group-hover:text-white',
|
|
153
|
+
detailWrapperClass: 'bg-amber-100 text-amber-700 dark:bg-amber-950/60 dark:text-amber-400',
|
|
154
|
+
badgeClass:
|
|
155
|
+
'border-amber-200 text-amber-700 bg-amber-50 dark:border-amber-800 dark:text-amber-300 dark:bg-amber-950/40',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 7. Dental Clinic
|
|
160
|
+
if (
|
|
161
|
+
text.includes('dent') ||
|
|
162
|
+
text.includes('tooth') ||
|
|
163
|
+
text.includes('teeth') ||
|
|
164
|
+
text.includes('orthodon') ||
|
|
165
|
+
text.includes('oral')
|
|
166
|
+
) {
|
|
167
|
+
return {
|
|
168
|
+
icon: Smile,
|
|
169
|
+
label: 'Dental',
|
|
170
|
+
badgeLabel: 'Dental Care',
|
|
171
|
+
iconWrapperClass:
|
|
172
|
+
'bg-teal-100 text-teal-700 dark:bg-teal-950/50 dark:text-teal-400 group-hover:bg-teal-600 group-hover:text-white',
|
|
173
|
+
detailWrapperClass: 'bg-teal-100 text-teal-700 dark:bg-teal-950/60 dark:text-teal-400',
|
|
174
|
+
badgeClass:
|
|
175
|
+
'border-teal-200 text-teal-700 bg-teal-50 dark:border-teal-800 dark:text-teal-300 dark:bg-teal-950/40',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Default fallback
|
|
180
|
+
return {
|
|
181
|
+
icon: Building2,
|
|
182
|
+
label: 'Department',
|
|
183
|
+
badgeLabel: 'Clinical',
|
|
184
|
+
iconWrapperClass:
|
|
185
|
+
'bg-primary/10 text-primary group-hover:bg-primary group-hover:text-primary-foreground',
|
|
186
|
+
detailWrapperClass: 'bg-primary/10 text-primary',
|
|
187
|
+
badgeClass: 'bg-secondary text-secondary-foreground',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|