@omersx/bemms 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +203 -2
- package/drizzle.config.ts +11 -0
- package/next.config.ts +51 -0
- package/package.json +99 -3
- package/postcss.config.mjs +7 -0
- package/public/hospital_demo_devices.csv +32 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon-maskable-512.png +0 -0
- package/public/manifest.webmanifest +31 -0
- package/public/sw.js +177 -0
- package/src/app/(auth)/layout.tsx +20 -0
- package/src/app/(auth)/login/page.tsx +135 -0
- package/src/app/(dashboard)/admin/audit-logs/page.tsx +178 -0
- package/src/app/(dashboard)/admin/data/page.tsx +37 -0
- package/src/app/(dashboard)/admin/departments/[deptId]/edit/page.tsx +206 -0
- package/src/app/(dashboard)/admin/departments/[deptId]/page.tsx +71 -0
- package/src/app/(dashboard)/admin/departments/new/page.tsx +6 -0
- package/src/app/(dashboard)/admin/departments/page.tsx +6 -0
- package/src/app/(dashboard)/admin/general/page.tsx +374 -0
- package/src/app/(dashboard)/admin/hospitals/[hospitalId]/edit/page.tsx +304 -0
- package/src/app/(dashboard)/admin/hospitals/[hospitalId]/page.tsx +94 -0
- package/src/app/(dashboard)/admin/hospitals/new/page.tsx +302 -0
- package/src/app/(dashboard)/admin/hospitals/page.tsx +6 -0
- package/src/app/(dashboard)/admin/layout.tsx +49 -0
- package/src/app/(dashboard)/admin/organizations/[orgId]/edit/page.tsx +172 -0
- package/src/app/(dashboard)/admin/organizations/[orgId]/page.tsx +82 -0
- package/src/app/(dashboard)/admin/organizations/new/page.tsx +150 -0
- package/src/app/(dashboard)/admin/organizations/page.tsx +6 -0
- package/src/app/(dashboard)/admin/page.tsx +258 -0
- package/src/app/(dashboard)/admin/roles/page.tsx +153 -0
- package/src/app/(dashboard)/admin/settings/page.tsx +69 -0
- package/src/app/(dashboard)/admin/users/[userId]/edit/page.tsx +153 -0
- package/src/app/(dashboard)/admin/users/[userId]/page.tsx +317 -0
- package/src/app/(dashboard)/admin/users/invite/page.tsx +3 -0
- package/src/app/(dashboard)/admin/users/new/page.tsx +5 -0
- package/src/app/(dashboard)/admin/users/page.tsx +23 -0
- package/src/app/(dashboard)/dashboard/page.tsx +462 -0
- package/src/app/(dashboard)/departments/[deptId]/page.tsx +39 -0
- package/src/app/(dashboard)/departments/new/page.tsx +330 -0
- package/src/app/(dashboard)/departments/page.tsx +52 -0
- package/src/app/(dashboard)/devices/[id]/edit/page.tsx +131 -0
- package/src/app/(dashboard)/devices/[id]/page.tsx +408 -0
- package/src/app/(dashboard)/devices/[id]/qr/page.tsx +54 -0
- package/src/app/(dashboard)/devices/new/page.tsx +382 -0
- package/src/app/(dashboard)/devices/page.tsx +80 -0
- package/src/app/(dashboard)/engineering-team/page.tsx +41 -0
- package/src/app/(dashboard)/layout.tsx +43 -0
- package/src/app/(dashboard)/maintenance/checklists/page.tsx +57 -0
- package/src/app/(dashboard)/maintenance/page.tsx +231 -0
- package/src/app/(dashboard)/maintenance/plans/page.tsx +142 -0
- package/src/app/(dashboard)/maintenance/tasks/[id]/page.tsx +997 -0
- package/src/app/(dashboard)/maintenance/tasks/page.tsx +119 -0
- package/src/app/(dashboard)/more/page.tsx +310 -0
- package/src/app/(dashboard)/my-department/page.tsx +151 -0
- package/src/app/(dashboard)/notifications/notification-list.tsx +103 -0
- package/src/app/(dashboard)/notifications/page.tsx +59 -0
- package/src/app/(dashboard)/profile/page.tsx +27 -0
- package/src/app/(dashboard)/reports/costs/page.tsx +127 -0
- package/src/app/(dashboard)/reports/history/page.tsx +105 -0
- package/src/app/(dashboard)/reports/inventory/page.tsx +116 -0
- package/src/app/(dashboard)/reports/maintenance/page.tsx +136 -0
- package/src/app/(dashboard)/reports/page.tsx +120 -0
- package/src/app/(dashboard)/reports/tickets/page.tsx +139 -0
- package/src/app/(dashboard)/reports/workload/page.tsx +135 -0
- package/src/app/(dashboard)/rounds/[id]/page.tsx +197 -0
- package/src/app/(dashboard)/rounds/history/page.tsx +82 -0
- package/src/app/(dashboard)/rounds/page.tsx +107 -0
- package/src/app/(dashboard)/scan/[code]/page.tsx +154 -0
- package/src/app/(dashboard)/scan/page.tsx +73 -0
- package/src/app/(dashboard)/team/page.tsx +42 -0
- package/src/app/(dashboard)/tickets/[id]/page.tsx +465 -0
- package/src/app/(dashboard)/tickets/create/page.tsx +722 -0
- package/src/app/(dashboard)/tickets/page.tsx +55 -0
- package/src/app/(dashboard)/tickets/triage/page.tsx +161 -0
- package/src/app/api/admin/data/export/route.ts +316 -0
- package/src/app/api/auth/[...nextauth]/route.ts +3 -0
- package/src/app/api/health/route.ts +46 -0
- package/src/app/api/notifications/sse/route.ts +91 -0
- package/src/app/api/notifications/subscribe/route.ts +54 -0
- package/src/app/api/notifications/vapid-key/route.ts +7 -0
- package/src/app/api/reports/export/route.ts +102 -0
- package/src/app/api/tickets/[id]/attachments/route.ts +129 -0
- package/src/app/api/tickets/[id]/live-sync/route.ts +52 -0
- package/src/app/error.tsx +54 -0
- package/src/app/globals.css +89 -0
- package/src/app/layout.tsx +59 -0
- package/src/app/not-found.tsx +32 -0
- package/src/app/page.tsx +5 -0
- package/src/components/admin/add-department-user-dialog.tsx +759 -0
- package/src/components/admin/add-engineer-dialog.tsx +773 -0
- package/src/components/admin/add-user-type-dialog.tsx +89 -0
- package/src/components/admin/audit-log-export.tsx +113 -0
- package/src/components/admin/audit-log-filters.tsx +255 -0
- package/src/components/admin/audit-log-table.tsx +383 -0
- package/src/components/admin/data-management/danger-zone.tsx +301 -0
- package/src/components/admin/data-management/data-management-tabs.tsx +61 -0
- package/src/components/admin/data-management/export-center.tsx +301 -0
- package/src/components/admin/data-management/import-wizard.tsx +558 -0
- package/src/components/admin/settings-forms/features-form.tsx +115 -0
- package/src/components/admin/settings-forms/health-panel.tsx +182 -0
- package/src/components/admin/settings-forms/localization-form.tsx +203 -0
- package/src/components/admin/settings-forms/retention-form.tsx +164 -0
- package/src/components/admin/settings-forms/security-form.tsx +186 -0
- package/src/components/admin/system-settings-tabs.tsx +88 -0
- package/src/components/admin/user-role-dialog.tsx +293 -0
- package/src/components/admin/user-status-dialog.tsx +166 -0
- package/src/components/admin/users-page-client.tsx +514 -0
- package/src/components/dashboard/action-buttons.tsx +93 -0
- package/src/components/dashboard/admin-alerts.tsx +86 -0
- package/src/components/dashboard/availability-alerts.tsx +65 -0
- package/src/components/dashboard/maintenance-due-list.tsx +87 -0
- package/src/components/dashboard/round-widget.tsx +110 -0
- package/src/components/dashboard/stat-card.tsx +38 -0
- package/src/components/dashboard/ticket-list-compact.tsx +86 -0
- package/src/components/departments/department-cards-view.tsx +244 -0
- package/src/components/departments/department-detail-view.tsx +467 -0
- package/src/components/departments/department-visuals.tsx +190 -0
- package/src/components/departments/my-department-view.tsx +333 -0
- package/src/components/devices/change-status-dialog.tsx +146 -0
- package/src/components/devices/device-action-buttons.tsx +125 -0
- package/src/components/devices/device-availability-banner.tsx +59 -0
- package/src/components/devices/device-badges.tsx +55 -0
- package/src/components/devices/device-documents-tab.tsx +93 -0
- package/src/components/devices/device-maintenance-tab.tsx +113 -0
- package/src/components/devices/device-mini-qr.tsx +109 -0
- package/src/components/devices/device-qr-card.tsx +187 -0
- package/src/components/devices/device-signatures-tab.tsx +607 -0
- package/src/components/devices/device-tickets-tab.tsx +139 -0
- package/src/components/devices/device-timelines.tsx +124 -0
- package/src/components/devices/qr-camera-scanner.tsx +167 -0
- package/src/components/devices/qr-code-display.tsx +139 -0
- package/src/components/devices/quick-add-category-dialog.tsx +135 -0
- package/src/components/devices/quick-add-manufacturer-dialog.tsx +212 -0
- package/src/components/devices/transfer-device-dialog.tsx +190 -0
- package/src/components/layout/header.tsx +80 -0
- package/src/components/layout/mobile-nav.tsx +91 -0
- package/src/components/layout/more-item-badge.tsx +15 -0
- package/src/components/layout/nav-badge.tsx +72 -0
- package/src/components/layout/sidebar.tsx +297 -0
- package/src/components/maintenance/maintenance-badges.tsx +579 -0
- package/src/components/maintenance/maintenance-nav.tsx +107 -0
- package/src/components/maintenance/parts-costs-manager.tsx +909 -0
- package/src/components/maintenance/signature-card.tsx +413 -0
- package/src/components/maintenance/task-action-bar.tsx +955 -0
- package/src/components/notifications/notification-bell.tsx +367 -0
- package/src/components/notifications/notification-item.tsx +93 -0
- package/src/components/notifications/notification-preferences.tsx +182 -0
- package/src/components/profile/user-profile-view.tsx +626 -0
- package/src/components/providers/nav-counts-provider.tsx +235 -0
- package/src/components/providers/session-provider.tsx +7 -0
- package/src/components/providers/theme-provider.tsx +124 -0
- package/src/components/pwa/install-prompt.tsx +82 -0
- package/src/components/pwa/sw-register.tsx +43 -0
- package/src/components/reports/export-button.tsx +62 -0
- package/src/components/reports/filter-chips.tsx +47 -0
- package/src/components/reports/summary-cards.tsx +40 -0
- package/src/components/reports/transparency-header.tsx +54 -0
- package/src/components/rounds/department-visit-sheet.tsx +199 -0
- package/src/components/rounds/mention-textarea.tsx +187 -0
- package/src/components/rounds/round-calendar.tsx +233 -0
- package/src/components/rounds/round-department-list.tsx +281 -0
- package/src/components/rounds/round-history-client.tsx +218 -0
- package/src/components/rounds/start-round-card.tsx +184 -0
- package/src/components/shared/confirm-dialog.tsx +67 -0
- package/src/components/shared/data-table.tsx +104 -0
- package/src/components/shared/page-header.tsx +20 -0
- package/src/components/shared/sign-out-button.tsx +52 -0
- package/src/components/shared/status-badge.tsx +42 -0
- package/src/components/shared/table-search.tsx +55 -0
- package/src/components/signatures/sign-action-dialog.tsx +253 -0
- package/src/components/signatures/signature-history.tsx +121 -0
- package/src/components/signatures/signature-status-panel.tsx +164 -0
- package/src/components/team/department-coverage.tsx +141 -0
- package/src/components/team/edit-engineer-dialog.tsx +242 -0
- package/src/components/team/engineer-card.tsx +180 -0
- package/src/components/team/engineer-details-dialog.tsx +432 -0
- package/src/components/team/schedule-calendar.tsx +204 -0
- package/src/components/team/staff-engineer-team-view.tsx +368 -0
- package/src/components/team/team-header-stats.tsx +88 -0
- package/src/components/team/team-page-client.tsx +216 -0
- package/src/components/theme/theme-selector-card.tsx +63 -0
- package/src/components/theme/theme-toggle.tsx +65 -0
- package/src/components/tickets/staff-status-label.tsx +58 -0
- package/src/components/tickets/staff-ticket-card.tsx +109 -0
- package/src/components/tickets/staff-ticket-progress-tracker.tsx +405 -0
- package/src/components/tickets/staff-tickets-view.tsx +182 -0
- package/src/components/tickets/ticket-action-buttons.tsx +873 -0
- package/src/components/tickets/ticket-auto-refresh.tsx +197 -0
- package/src/components/tickets/ticket-badges.tsx +43 -0
- package/src/components/tickets/ticket-card.tsx +197 -0
- package/src/components/tickets/ticket-cards-view.tsx +395 -0
- package/src/components/tickets/ticket-comment-section.tsx +684 -0
- package/src/components/tickets/ticket-dispatch-status.tsx +184 -0
- package/src/components/tickets/ticket-list-auto-refresh.tsx +55 -0
- package/src/components/tickets/ticket-reporter-profile.tsx +235 -0
- package/src/components/tickets/ticket-signatures-panel.tsx +32 -0
- package/src/components/tickets/ticket-timeline.tsx +131 -0
- package/src/components/tickets/ticket-triage-panel.tsx +124 -0
- package/src/components/tickets/tickets-nav.tsx +107 -0
- package/src/components/ui/avatar.tsx +43 -0
- package/src/components/ui/badge.tsx +34 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/card.tsx +77 -0
- package/src/components/ui/checkbox.tsx +31 -0
- package/src/components/ui/dialog.tsx +123 -0
- package/src/components/ui/dropdown-menu.tsx +126 -0
- package/src/components/ui/form.tsx +179 -0
- package/src/components/ui/input.tsx +24 -0
- package/src/components/ui/label.tsx +27 -0
- package/src/components/ui/popover.tsx +34 -0
- package/src/components/ui/progress.tsx +36 -0
- package/src/components/ui/scroll-area.tsx +47 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/sheet.tsx +131 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +121 -0
- package/src/components/ui/tabs.tsx +62 -0
- package/src/components/ui/textarea.tsx +23 -0
- package/src/components/ui/tooltip.tsx +27 -0
- package/src/lib/actions/__tests__/devices.test.ts +176 -0
- package/src/lib/actions/__tests__/signatures.test.ts +303 -0
- package/src/lib/actions/__tests__/tickets.test.ts +243 -0
- package/src/lib/actions/audit-logs.ts +298 -0
- package/src/lib/actions/checklists.ts +429 -0
- package/src/lib/actions/dashboard.ts +358 -0
- package/src/lib/actions/data-management.ts +723 -0
- package/src/lib/actions/departments.ts +369 -0
- package/src/lib/actions/device-categories.ts +117 -0
- package/src/lib/actions/devices.ts +748 -0
- package/src/lib/actions/engineer-rounds.ts +973 -0
- package/src/lib/actions/engineers.ts +285 -0
- package/src/lib/actions/hospitals.ts +148 -0
- package/src/lib/actions/locations.ts +115 -0
- package/src/lib/actions/maintenance-plans.ts +316 -0
- package/src/lib/actions/maintenance-tasks.ts +415 -0
- package/src/lib/actions/manufacturers.ts +132 -0
- package/src/lib/actions/nav-counts.ts +144 -0
- package/src/lib/actions/notifications.ts +136 -0
- package/src/lib/actions/organizations.ts +99 -0
- package/src/lib/actions/qr.ts +147 -0
- package/src/lib/actions/report-history.ts +77 -0
- package/src/lib/actions/reports.ts +192 -0
- package/src/lib/actions/roles.ts +32 -0
- package/src/lib/actions/signature-auth.ts +377 -0
- package/src/lib/actions/signatures.ts +532 -0
- package/src/lib/actions/system-settings.ts +274 -0
- package/src/lib/actions/tickets.ts +1445 -0
- package/src/lib/actions/users.ts +740 -0
- package/src/lib/audit/index.ts +103 -0
- package/src/lib/auth/__tests__/rbac.test.ts +33 -0
- package/src/lib/auth/index.ts +338 -0
- package/src/lib/auth/password.ts +10 -0
- package/src/lib/auth/rbac.ts +103 -0
- package/src/lib/auth/types.ts +57 -0
- package/src/lib/constants/departments.ts +16 -0
- package/src/lib/constants/manufacturers.ts +32 -0
- package/src/lib/db/index.ts +23 -0
- package/src/lib/db/schema/audit-logs.ts +42 -0
- package/src/lib/db/schema/departments.ts +39 -0
- package/src/lib/db/schema/device-categories.ts +38 -0
- package/src/lib/db/schema/devices.ts +195 -0
- package/src/lib/db/schema/engineer-profiles.ts +63 -0
- package/src/lib/db/schema/engineer-rounds.ts +115 -0
- package/src/lib/db/schema/enums.ts +148 -0
- package/src/lib/db/schema/hospitals.ts +34 -0
- package/src/lib/db/schema/index.ts +24 -0
- package/src/lib/db/schema/locations.ts +46 -0
- package/src/lib/db/schema/maintenance-plans.ts +99 -0
- package/src/lib/db/schema/maintenance-records.ts +161 -0
- package/src/lib/db/schema/maintenance-tasks.ts +196 -0
- package/src/lib/db/schema/manufacturers.ts +60 -0
- package/src/lib/db/schema/notifications.ts +48 -0
- package/src/lib/db/schema/organizations.ts +24 -0
- package/src/lib/db/schema/report-history.ts +28 -0
- package/src/lib/db/schema/roles.ts +59 -0
- package/src/lib/db/schema/sessions.ts +46 -0
- package/src/lib/db/schema/signature-events.ts +48 -0
- package/src/lib/db/schema/signature-policies.ts +60 -0
- package/src/lib/db/schema/signatures.ts +87 -0
- package/src/lib/db/schema/tickets.ts +169 -0
- package/src/lib/db/schema/user-assignments.ts +85 -0
- package/src/lib/db/schema/users.ts +39 -0
- package/src/lib/db/seed.ts +131 -0
- package/src/lib/logger/__tests__/logger.test.ts +154 -0
- package/src/lib/logger/formatters.ts +159 -0
- package/src/lib/logger/index.ts +184 -0
- package/src/lib/logger/types.ts +48 -0
- package/src/lib/notifications/create.ts +71 -0
- package/src/lib/notifications/dispatcher.ts +490 -0
- package/src/lib/notifications/index.ts +7 -0
- package/src/lib/notifications/push-sender.ts +128 -0
- package/src/lib/notifications/push.ts +64 -0
- package/src/lib/notifications/triggers.ts +166 -0
- package/src/lib/utils/__tests__/crypto.test.ts +30 -0
- package/src/lib/utils/__tests__/export.test.ts +55 -0
- package/src/lib/utils/__tests__/nav-badge.test.ts +147 -0
- package/src/lib/utils/__tests__/staff-status.test.ts +50 -0
- package/src/lib/utils/attestations.ts +19 -0
- package/src/lib/utils/crypto.ts +18 -0
- package/src/lib/utils/csv-parser.ts +181 -0
- package/src/lib/utils/export.ts +45 -0
- package/src/lib/utils/nav-badge.ts +225 -0
- package/src/lib/utils/sound.ts +152 -0
- package/src/lib/utils/staff-status.ts +115 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validators/__tests__/auth.test.ts +127 -0
- package/src/lib/validators/__tests__/data-management.test.ts +182 -0
- package/src/lib/validators/__tests__/devices-profile.test.ts +129 -0
- package/src/lib/validators/__tests__/devices.test.ts +98 -0
- package/src/lib/validators/__tests__/engineer-rounds.test.ts +170 -0
- package/src/lib/validators/__tests__/maintenance.test.ts +178 -0
- package/src/lib/validators/__tests__/signatures.test.ts +154 -0
- package/src/lib/validators/__tests__/system-settings.test.ts +119 -0
- package/src/lib/validators/__tests__/tickets.test.ts +176 -0
- package/src/lib/validators/__tests__/users.test.ts +92 -0
- package/src/lib/validators/auth.ts +32 -0
- package/src/lib/validators/data-management.ts +67 -0
- package/src/lib/validators/departments.ts +14 -0
- package/src/lib/validators/devices.ts +94 -0
- package/src/lib/validators/engineer-rounds.ts +70 -0
- package/src/lib/validators/hospitals.ts +17 -0
- package/src/lib/validators/locations.ts +15 -0
- package/src/lib/validators/maintenance.ts +211 -0
- package/src/lib/validators/organizations.ts +11 -0
- package/src/lib/validators/reports.ts +31 -0
- package/src/lib/validators/signatures.ts +49 -0
- package/src/lib/validators/system-settings.ts +55 -0
- package/src/lib/validators/tickets.ts +125 -0
- package/src/lib/validators/users.ts +43 -0
- package/src/middleware.ts +45 -0
- package/tailwind.config.ts +76 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Calendar as CalendarIcon, Clock, PhoneCall, Radio, ChevronLeft, ChevronRight, User } from 'lucide-react';
|
|
8
|
+
import { EngineerTeamMember } from '@/lib/actions/engineers';
|
|
9
|
+
|
|
10
|
+
interface ScheduleCalendarProps {
|
|
11
|
+
engineers: EngineerTeamMember[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DAYS_OF_WEEK = [
|
|
15
|
+
{ key: 'Sun', label: 'Sunday', arabic: 'الأحد' },
|
|
16
|
+
{ key: 'Mon', label: 'Monday', arabic: 'الاثنين' },
|
|
17
|
+
{ key: 'Tue', label: 'Tuesday', arabic: 'الثلاثاء' },
|
|
18
|
+
{ key: 'Wed', label: 'Wednesday', arabic: 'الأربعاء' },
|
|
19
|
+
{ key: 'Thu', label: 'Thursday', arabic: 'الخميس' },
|
|
20
|
+
{ key: 'Fri', label: 'Friday', arabic: 'الجمعة' },
|
|
21
|
+
{ key: 'Sat', label: 'Saturday', arabic: 'السبت' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function ScheduleCalendar({ engineers }: ScheduleCalendarProps) {
|
|
25
|
+
// Current day of the week
|
|
26
|
+
const todayIndex = new Date().getDay(); // 0 is Sunday
|
|
27
|
+
const todayKey = DAYS_OF_WEEK[todayIndex].key;
|
|
28
|
+
|
|
29
|
+
const [selectedDay, setSelectedDay] = useState<string>(todayKey);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-6">
|
|
33
|
+
{/* Overview Banner */}
|
|
34
|
+
<Card className="bg-gradient-to-r from-primary/10 via-primary/5 to-transparent border-primary/20">
|
|
35
|
+
<CardHeader className="pb-3">
|
|
36
|
+
<div className="flex items-center justify-between">
|
|
37
|
+
<div className="space-y-1">
|
|
38
|
+
<CardTitle className="text-lg flex items-center gap-2">
|
|
39
|
+
<CalendarIcon className="w-5 h-5 text-primary" />
|
|
40
|
+
Hospital Biomedical Duty Roster & On-Call Schedule
|
|
41
|
+
</CardTitle>
|
|
42
|
+
<CardDescription>
|
|
43
|
+
Regular clinical engineering coverage hours: Sunday to Thursday (08:00 AM – 04:00 PM). Emergency on-call is active 24/7.
|
|
44
|
+
</CardDescription>
|
|
45
|
+
</div>
|
|
46
|
+
<Badge variant="outline" className="bg-background/80 text-xs gap-1.5 py-1 px-2.5 font-medium">
|
|
47
|
+
<Clock className="w-3.5 h-3.5 text-primary" />
|
|
48
|
+
Standard Shift: 8 hrs
|
|
49
|
+
</Badge>
|
|
50
|
+
</div>
|
|
51
|
+
</CardHeader>
|
|
52
|
+
</Card>
|
|
53
|
+
|
|
54
|
+
{/* Day Selector Pills */}
|
|
55
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-7 gap-2.5">
|
|
56
|
+
{DAYS_OF_WEEK.map((day) => {
|
|
57
|
+
const isToday = day.key === todayKey;
|
|
58
|
+
const isSelected = day.key === selectedDay;
|
|
59
|
+
const onDutyCount = engineers.filter((e) => e.workingDays.includes(day.key)).length;
|
|
60
|
+
const onCallCount = engineers.filter((e) => e.onCallDays.includes(day.key)).length;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<button
|
|
64
|
+
key={day.key}
|
|
65
|
+
onClick={() => setSelectedDay(day.key)}
|
|
66
|
+
className={`p-3 rounded-xl border text-center transition-all flex flex-col items-center justify-between min-h-[90px] relative ${
|
|
67
|
+
isSelected
|
|
68
|
+
? 'border-primary bg-primary text-primary-foreground shadow-md'
|
|
69
|
+
: isToday
|
|
70
|
+
? 'border-emerald-500/50 bg-emerald-50/50 dark:bg-emerald-950/20 text-foreground'
|
|
71
|
+
: 'border-border/60 bg-card hover:border-primary/40 text-foreground'
|
|
72
|
+
}`}
|
|
73
|
+
>
|
|
74
|
+
{isToday && (
|
|
75
|
+
<span
|
|
76
|
+
className={`absolute -top-2 left-1/2 -translate-x-1/2 text-[9px] px-1.5 py-0.2 rounded-full font-bold uppercase tracking-wider ${
|
|
77
|
+
isSelected ? 'bg-background text-primary' : 'bg-emerald-600 text-white'
|
|
78
|
+
}`}
|
|
79
|
+
>
|
|
80
|
+
Today
|
|
81
|
+
</span>
|
|
82
|
+
)}
|
|
83
|
+
<span className={`text-xs font-semibold ${isSelected ? 'text-primary-foreground' : 'text-muted-foreground'}`}>
|
|
84
|
+
{day.key}
|
|
85
|
+
</span>
|
|
86
|
+
<span className="text-sm font-bold my-1">{day.label.slice(0, 3)}</span>
|
|
87
|
+
|
|
88
|
+
<div className="flex items-center gap-1.5 text-[10px] mt-1">
|
|
89
|
+
{onDutyCount > 0 && (
|
|
90
|
+
<span
|
|
91
|
+
className={`px-1.5 py-0.5 rounded font-medium ${
|
|
92
|
+
isSelected ? 'bg-primary-foreground/20 text-primary-foreground' : 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400'
|
|
93
|
+
}`}
|
|
94
|
+
>
|
|
95
|
+
🟢 {onDutyCount}
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
98
|
+
{onCallCount > 0 && (
|
|
99
|
+
<span
|
|
100
|
+
className={`px-1.5 py-0.5 rounded font-medium ${
|
|
101
|
+
isSelected ? 'bg-primary-foreground/20 text-primary-foreground' : 'bg-amber-500/15 text-amber-700 dark:text-amber-400'
|
|
102
|
+
}`}
|
|
103
|
+
>
|
|
104
|
+
📞 {onCallCount}
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</button>
|
|
109
|
+
);
|
|
110
|
+
})}
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* Selected Day Roster Details */}
|
|
114
|
+
<Card className="border-border/70 shadow-sm">
|
|
115
|
+
<CardHeader className="pb-3 border-b bg-muted/20">
|
|
116
|
+
<div className="flex items-center justify-between">
|
|
117
|
+
<div>
|
|
118
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
119
|
+
<span>{DAYS_OF_WEEK.find((d) => d.key === selectedDay)?.label} Roster</span>
|
|
120
|
+
<span className="text-xs text-muted-foreground font-normal">
|
|
121
|
+
({DAYS_OF_WEEK.find((d) => d.key === selectedDay)?.arabic})
|
|
122
|
+
</span>
|
|
123
|
+
{selectedDay === todayKey && (
|
|
124
|
+
<Badge className="bg-emerald-500 text-white text-[10px] ml-2">Active Today</Badge>
|
|
125
|
+
)}
|
|
126
|
+
</CardTitle>
|
|
127
|
+
<CardDescription className="text-xs mt-0.5">
|
|
128
|
+
Engineers scheduled for hospital on-site duty and emergency on-call rotation.
|
|
129
|
+
</CardDescription>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</CardHeader>
|
|
133
|
+
|
|
134
|
+
<CardContent className="p-0 divide-y divide-border/60">
|
|
135
|
+
{engineers.map((eng) => {
|
|
136
|
+
const isWorking = eng.workingDays.includes(selectedDay);
|
|
137
|
+
const isOnCall = eng.onCallDays.includes(selectedDay);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div key={eng.id} className="p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4 hover:bg-muted/10 transition-colors">
|
|
141
|
+
<div className="flex items-center gap-3">
|
|
142
|
+
<div className="w-10 h-10 rounded-full bg-primary/10 text-primary font-bold flex items-center justify-center shrink-0 border border-primary/20">
|
|
143
|
+
{eng.fullName.replace('Eng. ', '').split(' ').map((n) => n[0]).join('').slice(0, 2)}
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<div className="flex items-center gap-2">
|
|
147
|
+
<h4 className="font-semibold text-sm">{eng.fullName}</h4>
|
|
148
|
+
<Badge variant="outline" className="text-[10px] py-0 px-1.5 font-normal">
|
|
149
|
+
{eng.jobTitle}
|
|
150
|
+
</Badge>
|
|
151
|
+
</div>
|
|
152
|
+
<p className="text-xs text-muted-foreground mt-0.5">{eng.specialization}</p>
|
|
153
|
+
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
|
|
154
|
+
{eng.extension && (
|
|
155
|
+
<span>Ext: <strong className="text-foreground">{eng.extension}</strong></span>
|
|
156
|
+
)}
|
|
157
|
+
{eng.mobileNumber && (
|
|
158
|
+
<span>Mobile: <strong className="text-foreground">{eng.mobileNumber}</strong></span>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div className="flex items-center gap-3 shrink-0 self-end sm:self-center">
|
|
165
|
+
{isWorking ? (
|
|
166
|
+
<div className="text-right">
|
|
167
|
+
<Badge className="bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 border-emerald-500/30 gap-1.5 font-medium">
|
|
168
|
+
<Radio className="w-3 h-3 text-emerald-500" />
|
|
169
|
+
On-Site Duty ({eng.shiftHours || '08:00 - 16:00'})
|
|
170
|
+
</Badge>
|
|
171
|
+
<p className="text-[11px] text-muted-foreground mt-0.5">Hospital Workshop & Wards</p>
|
|
172
|
+
</div>
|
|
173
|
+
) : isOnCall ? (
|
|
174
|
+
<div className="text-right">
|
|
175
|
+
<Badge className="bg-amber-500/15 text-amber-700 dark:text-amber-400 border-amber-500/30 gap-1.5 font-medium">
|
|
176
|
+
<PhoneCall className="w-3 h-3 text-amber-500 animate-pulse" />
|
|
177
|
+
Emergency On-Call (24h)
|
|
178
|
+
</Badge>
|
|
179
|
+
<p className="text-[11px] text-muted-foreground mt-0.5">Urgent response within 30 min</p>
|
|
180
|
+
</div>
|
|
181
|
+
) : (
|
|
182
|
+
<Badge variant="outline" className="text-muted-foreground gap-1.5">
|
|
183
|
+
Off Duty / Rest
|
|
184
|
+
</Badge>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{eng.mobileNumber && (
|
|
188
|
+
<Button asChild size="sm" variant="outline" className="h-8 gap-1.5 text-xs">
|
|
189
|
+
<a href={`tel:${eng.mobileNumber}`}>
|
|
190
|
+
<PhoneCall className="w-3.5 h-3.5 text-primary" />
|
|
191
|
+
Call
|
|
192
|
+
</a>
|
|
193
|
+
</Button>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
})}
|
|
199
|
+
</CardContent>
|
|
200
|
+
</Card>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
7
|
+
import {
|
|
8
|
+
Phone,
|
|
9
|
+
Mail,
|
|
10
|
+
Clock,
|
|
11
|
+
CheckCircle,
|
|
12
|
+
AlertCircle,
|
|
13
|
+
Calendar,
|
|
14
|
+
Wrench,
|
|
15
|
+
Moon,
|
|
16
|
+
} from 'lucide-react';
|
|
17
|
+
import { cn } from '@/lib/utils';
|
|
18
|
+
|
|
19
|
+
interface EngineerInfo {
|
|
20
|
+
id: string;
|
|
21
|
+
userId: string;
|
|
22
|
+
fullName: string;
|
|
23
|
+
email: string;
|
|
24
|
+
avatarUrl: string | null;
|
|
25
|
+
jobTitle: string;
|
|
26
|
+
specialization: string | null;
|
|
27
|
+
extension: string | null;
|
|
28
|
+
mobileNumber: string | null;
|
|
29
|
+
dutyStatus: string;
|
|
30
|
+
shiftHours: string | null;
|
|
31
|
+
workingDays: string[];
|
|
32
|
+
onCallDays: string[];
|
|
33
|
+
coveredDepartments: { id: string; name: string; code: string }[];
|
|
34
|
+
hospitalName: string | null;
|
|
35
|
+
activeTicketsCount: number;
|
|
36
|
+
activeMaintenanceCount: number;
|
|
37
|
+
assignedDevicesCount: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface StaffEngineerTeamViewProps {
|
|
41
|
+
engineers: EngineerInfo[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
45
|
+
|
|
46
|
+
const DUTY_STATUS_CONFIG: Record<
|
|
47
|
+
string,
|
|
48
|
+
{
|
|
49
|
+
label: string;
|
|
50
|
+
color: string;
|
|
51
|
+
bgColor: string;
|
|
52
|
+
borderColor: string;
|
|
53
|
+
icon: typeof CheckCircle;
|
|
54
|
+
}
|
|
55
|
+
> = {
|
|
56
|
+
on_duty: {
|
|
57
|
+
label: 'On Duty',
|
|
58
|
+
color: 'text-green-700 dark:text-green-300',
|
|
59
|
+
bgColor: 'bg-green-50 dark:bg-green-950/30',
|
|
60
|
+
borderColor: 'border-green-200 dark:border-green-800',
|
|
61
|
+
icon: CheckCircle,
|
|
62
|
+
},
|
|
63
|
+
on_call: {
|
|
64
|
+
label: 'On Call',
|
|
65
|
+
color: 'text-blue-700 dark:text-blue-300',
|
|
66
|
+
bgColor: 'bg-blue-50 dark:bg-blue-950/30',
|
|
67
|
+
borderColor: 'border-blue-200 dark:border-blue-800',
|
|
68
|
+
icon: Phone,
|
|
69
|
+
},
|
|
70
|
+
in_maintenance: {
|
|
71
|
+
label: 'In Maintenance',
|
|
72
|
+
color: 'text-amber-700 dark:text-amber-300',
|
|
73
|
+
bgColor: 'bg-amber-50 dark:bg-amber-950/30',
|
|
74
|
+
borderColor: 'border-amber-200 dark:border-amber-800',
|
|
75
|
+
icon: Wrench,
|
|
76
|
+
},
|
|
77
|
+
off_duty: {
|
|
78
|
+
label: 'Off Duty',
|
|
79
|
+
color: 'text-gray-500 dark:text-gray-400',
|
|
80
|
+
bgColor: 'bg-gray-50 dark:bg-gray-900/30',
|
|
81
|
+
borderColor: 'border-gray-200 dark:border-gray-700',
|
|
82
|
+
icon: Moon,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export function StaffEngineerTeamView({ engineers }: StaffEngineerTeamViewProps) {
|
|
87
|
+
const [filter, setFilter] = useState<'all' | 'available'>('all');
|
|
88
|
+
|
|
89
|
+
const availableEngineers = engineers.filter(
|
|
90
|
+
(e) => e.dutyStatus === 'on_duty' || e.dutyStatus === 'on_call'
|
|
91
|
+
);
|
|
92
|
+
const filteredEngineers =
|
|
93
|
+
filter === 'available' ? availableEngineers : engineers;
|
|
94
|
+
|
|
95
|
+
const todayIndex = new Date().getDay();
|
|
96
|
+
const todayName = DAYS_OF_WEEK[todayIndex];
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="space-y-5">
|
|
100
|
+
{/* Summary Strip */}
|
|
101
|
+
<div className="flex items-center gap-3 flex-wrap">
|
|
102
|
+
<div className="flex items-center gap-2 bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-800 rounded-lg px-3 py-2">
|
|
103
|
+
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" />
|
|
104
|
+
<span className="text-sm font-semibold text-green-700 dark:text-green-300">
|
|
105
|
+
{availableEngineers.length} available today
|
|
106
|
+
</span>
|
|
107
|
+
</div>
|
|
108
|
+
<span className="text-sm text-muted-foreground">
|
|
109
|
+
{engineers.length} total engineers
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* Filter */}
|
|
114
|
+
<div className="flex gap-2">
|
|
115
|
+
<button
|
|
116
|
+
onClick={() => setFilter('all')}
|
|
117
|
+
className={cn(
|
|
118
|
+
'text-xs px-3 py-1.5 rounded-full border font-medium transition-colors',
|
|
119
|
+
filter === 'all'
|
|
120
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
121
|
+
: 'bg-card text-muted-foreground border-border hover:bg-muted'
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
All ({engineers.length})
|
|
125
|
+
</button>
|
|
126
|
+
<button
|
|
127
|
+
onClick={() => setFilter('available')}
|
|
128
|
+
className={cn(
|
|
129
|
+
'text-xs px-3 py-1.5 rounded-full border font-medium transition-colors',
|
|
130
|
+
filter === 'available'
|
|
131
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
132
|
+
: 'bg-card text-muted-foreground border-border hover:bg-muted'
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
Available Now ({availableEngineers.length})
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Engineer Cards */}
|
|
140
|
+
{filteredEngineers.length === 0 ? (
|
|
141
|
+
<div className="text-center py-12 border rounded-lg bg-muted/20">
|
|
142
|
+
<p className="text-muted-foreground text-sm">
|
|
143
|
+
No engineers are currently available.
|
|
144
|
+
</p>
|
|
145
|
+
</div>
|
|
146
|
+
) : (
|
|
147
|
+
<div className="space-y-4">
|
|
148
|
+
{filteredEngineers.map((eng) => (
|
|
149
|
+
<EngineerCard
|
|
150
|
+
key={eng.id}
|
|
151
|
+
engineer={eng}
|
|
152
|
+
todayName={todayName}
|
|
153
|
+
/>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function EngineerCard({
|
|
162
|
+
engineer,
|
|
163
|
+
todayName,
|
|
164
|
+
}: {
|
|
165
|
+
engineer: EngineerInfo;
|
|
166
|
+
todayName: string;
|
|
167
|
+
}) {
|
|
168
|
+
const statusConfig = DUTY_STATUS_CONFIG[engineer.dutyStatus] || DUTY_STATUS_CONFIG.off_duty;
|
|
169
|
+
const StatusIcon = statusConfig.icon;
|
|
170
|
+
const initials = engineer.fullName
|
|
171
|
+
.split(' ')
|
|
172
|
+
.map((n) => n[0])
|
|
173
|
+
.join('')
|
|
174
|
+
.substring(0, 2)
|
|
175
|
+
.toUpperCase();
|
|
176
|
+
|
|
177
|
+
const isAvailable =
|
|
178
|
+
engineer.dutyStatus === 'on_duty' || engineer.dutyStatus === 'on_call';
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<Card
|
|
182
|
+
className={cn(
|
|
183
|
+
'shadow-sm overflow-hidden',
|
|
184
|
+
isAvailable
|
|
185
|
+
? 'border-green-200 dark:border-green-900'
|
|
186
|
+
: 'opacity-75'
|
|
187
|
+
)}
|
|
188
|
+
>
|
|
189
|
+
{/* Status Banner */}
|
|
190
|
+
<div
|
|
191
|
+
className={cn(
|
|
192
|
+
'px-4 py-2 flex items-center gap-2 text-xs font-semibold border-b',
|
|
193
|
+
statusConfig.bgColor,
|
|
194
|
+
statusConfig.borderColor,
|
|
195
|
+
statusConfig.color
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
<StatusIcon className="w-3.5 h-3.5" />
|
|
199
|
+
{statusConfig.label}
|
|
200
|
+
{engineer.shiftHours && (
|
|
201
|
+
<span className="text-muted-foreground font-normal ml-auto">
|
|
202
|
+
{engineer.shiftHours}
|
|
203
|
+
</span>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<CardContent className="p-4">
|
|
208
|
+
{/* Name and Role */}
|
|
209
|
+
<div className="flex items-start gap-3">
|
|
210
|
+
<Avatar className="h-12 w-12 shrink-0">
|
|
211
|
+
<AvatarFallback
|
|
212
|
+
className={cn(
|
|
213
|
+
'text-sm font-bold',
|
|
214
|
+
isAvailable
|
|
215
|
+
? 'bg-primary/10 text-primary'
|
|
216
|
+
: 'bg-muted text-muted-foreground'
|
|
217
|
+
)}
|
|
218
|
+
>
|
|
219
|
+
{initials}
|
|
220
|
+
</AvatarFallback>
|
|
221
|
+
</Avatar>
|
|
222
|
+
<div className="min-w-0 flex-1">
|
|
223
|
+
<h3 className="font-semibold text-base">{engineer.fullName}</h3>
|
|
224
|
+
<p className="text-sm text-muted-foreground">{engineer.jobTitle}</p>
|
|
225
|
+
{engineer.specialization && (
|
|
226
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
227
|
+
{engineer.specialization}
|
|
228
|
+
</p>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{/* Contact Info */}
|
|
234
|
+
<div className="mt-4 space-y-2">
|
|
235
|
+
{engineer.mobileNumber && (
|
|
236
|
+
<a
|
|
237
|
+
href={`tel:${engineer.mobileNumber}`}
|
|
238
|
+
className="flex items-center gap-2 text-sm text-primary hover:underline min-h-[36px]"
|
|
239
|
+
>
|
|
240
|
+
<Phone className="w-4 h-4 shrink-0" />
|
|
241
|
+
{engineer.mobileNumber}
|
|
242
|
+
</a>
|
|
243
|
+
)}
|
|
244
|
+
{engineer.extension && (
|
|
245
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
246
|
+
<Phone className="w-4 h-4 shrink-0" />
|
|
247
|
+
Ext. {engineer.extension}
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
<a
|
|
251
|
+
href={`mailto:${engineer.email}`}
|
|
252
|
+
className="flex items-center gap-2 text-sm text-primary hover:underline min-h-[36px]"
|
|
253
|
+
>
|
|
254
|
+
<Mail className="w-4 h-4 shrink-0" />
|
|
255
|
+
{engineer.email}
|
|
256
|
+
</a>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{/* Weekly Schedule Calendar */}
|
|
260
|
+
<div className="mt-4">
|
|
261
|
+
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
|
262
|
+
<Calendar className="w-3.5 h-3.5 inline mr-1" />
|
|
263
|
+
Weekly Schedule
|
|
264
|
+
</p>
|
|
265
|
+
<div className="grid grid-cols-7 gap-1">
|
|
266
|
+
{DAYS_OF_WEEK.map((day) => {
|
|
267
|
+
const isWorkDay = engineer.workingDays.includes(day);
|
|
268
|
+
const isOnCallDay = engineer.onCallDays.includes(day);
|
|
269
|
+
const isToday = day === todayName;
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div
|
|
273
|
+
key={day}
|
|
274
|
+
className={cn(
|
|
275
|
+
'flex flex-col items-center py-1.5 rounded-lg text-center border',
|
|
276
|
+
isToday && 'ring-2 ring-primary ring-offset-1',
|
|
277
|
+
isWorkDay
|
|
278
|
+
? 'bg-green-50 dark:bg-green-950/30 border-green-200 dark:border-green-800'
|
|
279
|
+
: isOnCallDay
|
|
280
|
+
? 'bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-800'
|
|
281
|
+
: 'bg-muted/30 border-border'
|
|
282
|
+
)}
|
|
283
|
+
>
|
|
284
|
+
<span
|
|
285
|
+
className={cn(
|
|
286
|
+
'text-[10px] font-bold leading-none',
|
|
287
|
+
isToday
|
|
288
|
+
? 'text-primary'
|
|
289
|
+
: isWorkDay
|
|
290
|
+
? 'text-green-700 dark:text-green-300'
|
|
291
|
+
: isOnCallDay
|
|
292
|
+
? 'text-blue-700 dark:text-blue-300'
|
|
293
|
+
: 'text-muted-foreground'
|
|
294
|
+
)}
|
|
295
|
+
>
|
|
296
|
+
{day}
|
|
297
|
+
</span>
|
|
298
|
+
<span className="text-[10px] mt-0.5 leading-none">
|
|
299
|
+
{isWorkDay ? '✓' : isOnCallDay ? '📞' : '—'}
|
|
300
|
+
</span>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
})}
|
|
304
|
+
</div>
|
|
305
|
+
<div className="flex items-center gap-3 mt-2 text-[11px] text-muted-foreground">
|
|
306
|
+
<span className="flex items-center gap-1">
|
|
307
|
+
<span className="w-2 h-2 rounded-full bg-green-500 inline-block" />
|
|
308
|
+
Working
|
|
309
|
+
</span>
|
|
310
|
+
<span className="flex items-center gap-1">
|
|
311
|
+
<span className="w-2 h-2 rounded-full bg-blue-500 inline-block" />
|
|
312
|
+
On Call
|
|
313
|
+
</span>
|
|
314
|
+
<span className="flex items-center gap-1">
|
|
315
|
+
<span className="w-2 h-2 rounded-full bg-muted inline-block border" />
|
|
316
|
+
Off
|
|
317
|
+
</span>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
{/* Covered Departments */}
|
|
322
|
+
{engineer.coveredDepartments.length > 0 && (
|
|
323
|
+
<div className="mt-4">
|
|
324
|
+
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1.5">
|
|
325
|
+
Covers Departments
|
|
326
|
+
</p>
|
|
327
|
+
<div className="flex flex-wrap gap-1">
|
|
328
|
+
{engineer.coveredDepartments.map((dept) => (
|
|
329
|
+
<Badge
|
|
330
|
+
key={dept.id}
|
|
331
|
+
variant="secondary"
|
|
332
|
+
className="text-xs"
|
|
333
|
+
>
|
|
334
|
+
{dept.name}
|
|
335
|
+
</Badge>
|
|
336
|
+
))}
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
|
|
341
|
+
{/* Current Workload */}
|
|
342
|
+
{(engineer.activeTicketsCount > 0 ||
|
|
343
|
+
engineer.activeMaintenanceCount > 0) && (
|
|
344
|
+
<div className="mt-4 pt-3 border-t border-border">
|
|
345
|
+
<p className="text-xs text-muted-foreground">
|
|
346
|
+
Currently handling:{' '}
|
|
347
|
+
{engineer.activeTicketsCount > 0 && (
|
|
348
|
+
<span className="font-medium text-foreground">
|
|
349
|
+
{engineer.activeTicketsCount} ticket
|
|
350
|
+
{engineer.activeTicketsCount !== 1 ? 's' : ''}
|
|
351
|
+
</span>
|
|
352
|
+
)}
|
|
353
|
+
{engineer.activeTicketsCount > 0 &&
|
|
354
|
+
engineer.activeMaintenanceCount > 0 &&
|
|
355
|
+
' and '}
|
|
356
|
+
{engineer.activeMaintenanceCount > 0 && (
|
|
357
|
+
<span className="font-medium text-foreground">
|
|
358
|
+
{engineer.activeMaintenanceCount} maintenance task
|
|
359
|
+
{engineer.activeMaintenanceCount !== 1 ? 's' : ''}
|
|
360
|
+
</span>
|
|
361
|
+
)}
|
|
362
|
+
</p>
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
</CardContent>
|
|
366
|
+
</Card>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Card } from '@/components/ui/card';
|
|
4
|
+
import { Users, Radio, PhoneCall, Wrench, ShieldCheck, Moon } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface TeamHeaderStatsProps {
|
|
7
|
+
stats: {
|
|
8
|
+
totalEngineers: number;
|
|
9
|
+
onDutyToday: number;
|
|
10
|
+
onCallToday: number;
|
|
11
|
+
inMaintenanceToday: number;
|
|
12
|
+
offDutyToday: number;
|
|
13
|
+
totalActiveWorkOrders: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function TeamHeaderStats({ stats }: TeamHeaderStatsProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3">
|
|
20
|
+
{/* Total Engineers */}
|
|
21
|
+
<Card className="p-4 bg-card border-border/70 shadow-sm flex items-center justify-between">
|
|
22
|
+
<div>
|
|
23
|
+
<p className="text-xs font-medium text-muted-foreground">Total Team</p>
|
|
24
|
+
<p className="text-2xl font-bold mt-0.5">{stats.totalEngineers}</p>
|
|
25
|
+
<span className="text-[11px] text-muted-foreground">Biomedical Engineers</span>
|
|
26
|
+
</div>
|
|
27
|
+
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
|
28
|
+
<Users className="w-5 h-5" />
|
|
29
|
+
</div>
|
|
30
|
+
</Card>
|
|
31
|
+
|
|
32
|
+
{/* On Duty */}
|
|
33
|
+
<Card className="p-4 bg-emerald-50/50 dark:bg-emerald-950/20 border-emerald-200/60 dark:border-emerald-900/40 shadow-sm flex items-center justify-between">
|
|
34
|
+
<div>
|
|
35
|
+
<p className="text-xs font-medium text-emerald-800 dark:text-emerald-300">On Duty Today</p>
|
|
36
|
+
<div className="flex items-center gap-1.5 mt-0.5">
|
|
37
|
+
<span className="relative flex h-2.5 w-2.5">
|
|
38
|
+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
|
39
|
+
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
|
|
40
|
+
</span>
|
|
41
|
+
<p className="text-2xl font-bold text-emerald-700 dark:text-emerald-400">{stats.onDutyToday}</p>
|
|
42
|
+
</div>
|
|
43
|
+
<span className="text-[11px] text-emerald-700/80 dark:text-emerald-400/80">In Hospital / Ready</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="w-10 h-10 rounded-full bg-emerald-100 dark:bg-emerald-900/50 flex items-center justify-center text-emerald-600 dark:text-emerald-400">
|
|
46
|
+
<Radio className="w-5 h-5" />
|
|
47
|
+
</div>
|
|
48
|
+
</Card>
|
|
49
|
+
|
|
50
|
+
{/* On Call */}
|
|
51
|
+
<Card className="p-4 bg-amber-50/50 dark:bg-amber-950/20 border-amber-200/60 dark:border-amber-900/40 shadow-sm flex items-center justify-between">
|
|
52
|
+
<div>
|
|
53
|
+
<p className="text-xs font-medium text-amber-800 dark:text-amber-300">On-Call Shift</p>
|
|
54
|
+
<p className="text-2xl font-bold text-amber-700 dark:text-amber-400 mt-0.5">{stats.onCallToday}</p>
|
|
55
|
+
<span className="text-[11px] text-amber-700/80 dark:text-amber-400/80">Emergency Responder</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="w-10 h-10 rounded-full bg-amber-100 dark:bg-amber-900/50 flex items-center justify-center text-amber-600 dark:text-amber-400">
|
|
58
|
+
<PhoneCall className="w-5 h-5" />
|
|
59
|
+
</div>
|
|
60
|
+
</Card>
|
|
61
|
+
|
|
62
|
+
{/* Active Work Orders */}
|
|
63
|
+
<Card className="p-4 bg-blue-50/50 dark:bg-blue-950/20 border-blue-200/60 dark:border-blue-900/40 shadow-sm flex items-center justify-between">
|
|
64
|
+
<div>
|
|
65
|
+
<p className="text-xs font-medium text-blue-800 dark:text-blue-300">Active Workloads</p>
|
|
66
|
+
<p className="text-2xl font-bold text-blue-700 dark:text-blue-400 mt-0.5">{stats.totalActiveWorkOrders}</p>
|
|
67
|
+
<span className="text-[11px] text-blue-700/80 dark:text-blue-400/80">Open Tasks & Tickets</span>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center text-blue-600 dark:text-blue-400">
|
|
70
|
+
<Wrench className="w-5 h-5" />
|
|
71
|
+
</div>
|
|
72
|
+
</Card>
|
|
73
|
+
|
|
74
|
+
{/* Off Duty */}
|
|
75
|
+
<Card className="p-4 bg-slate-50 dark:bg-slate-900/40 border-border/70 shadow-sm col-span-2 md:col-span-1 flex items-center justify-between">
|
|
76
|
+
<div>
|
|
77
|
+
<p className="text-xs font-medium text-muted-foreground">Off Duty</p>
|
|
78
|
+
<p className="text-2xl font-bold text-muted-foreground mt-0.5">{stats.offDutyToday}</p>
|
|
79
|
+
<span className="text-[11px] text-muted-foreground">Rest / Post-Call</span>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center text-muted-foreground">
|
|
82
|
+
<Moon className="w-5 h-5" />
|
|
83
|
+
</div>
|
|
84
|
+
</Card>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|