@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,216 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
5
|
+
import { Input } from '@/components/ui/input';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Badge } from '@/components/ui/badge';
|
|
8
|
+
import { Users, Calendar as CalendarIcon, Building2, Search, Filter } from 'lucide-react';
|
|
9
|
+
import { TeamHeaderStats } from './team-header-stats';
|
|
10
|
+
import { EngineerCard } from './engineer-card';
|
|
11
|
+
import { ScheduleCalendar } from './schedule-calendar';
|
|
12
|
+
import { DepartmentCoverage } from './department-coverage';
|
|
13
|
+
import { EditEngineerDialog } from './edit-engineer-dialog';
|
|
14
|
+
import { EngineerDetailsDialog } from './engineer-details-dialog';
|
|
15
|
+
import { EngineerTeamMember } from '@/lib/actions/engineers';
|
|
16
|
+
import { useRouter } from 'next/navigation';
|
|
17
|
+
|
|
18
|
+
interface TeamPageClientProps {
|
|
19
|
+
initialMembers: EngineerTeamMember[];
|
|
20
|
+
stats: {
|
|
21
|
+
totalEngineers: number;
|
|
22
|
+
onDutyToday: number;
|
|
23
|
+
onCallToday: number;
|
|
24
|
+
inMaintenanceToday: number;
|
|
25
|
+
offDutyToday: number;
|
|
26
|
+
totalActiveWorkOrders: number;
|
|
27
|
+
};
|
|
28
|
+
coverage: {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
code: string;
|
|
32
|
+
deviceCount: number;
|
|
33
|
+
primaryEngineer: any;
|
|
34
|
+
}[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function TeamPageClient({ initialMembers, stats, coverage }: TeamPageClientProps) {
|
|
38
|
+
const router = useRouter();
|
|
39
|
+
const [search, setSearch] = useState('');
|
|
40
|
+
const [statusFilter, setStatusFilter] = useState<string>('all');
|
|
41
|
+
const [selectedEngineer, setSelectedEngineer] = useState<EngineerTeamMember | null>(null);
|
|
42
|
+
const [viewEngineer, setViewEngineer] = useState<EngineerTeamMember | null>(null);
|
|
43
|
+
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
|
44
|
+
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
|
45
|
+
|
|
46
|
+
const filteredMembers = initialMembers.filter((m) => {
|
|
47
|
+
const matchesSearch =
|
|
48
|
+
m.fullName.toLowerCase().includes(search.toLowerCase()) ||
|
|
49
|
+
m.jobTitle.toLowerCase().includes(search.toLowerCase()) ||
|
|
50
|
+
(m.specialization && m.specialization.toLowerCase().includes(search.toLowerCase())) ||
|
|
51
|
+
(m.extension && m.extension.includes(search));
|
|
52
|
+
|
|
53
|
+
const matchesStatus =
|
|
54
|
+
statusFilter === 'all' ||
|
|
55
|
+
m.dutyStatus === statusFilter;
|
|
56
|
+
|
|
57
|
+
return matchesSearch && matchesStatus;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const handleEdit = (engineer: EngineerTeamMember) => {
|
|
61
|
+
setSelectedEngineer(engineer);
|
|
62
|
+
setIsEditDialogOpen(true);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleCardClick = (engineer: EngineerTeamMember) => {
|
|
66
|
+
setViewEngineer(engineer);
|
|
67
|
+
setIsDetailsOpen(true);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="space-y-6">
|
|
72
|
+
{/* Top Header */}
|
|
73
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
74
|
+
<div>
|
|
75
|
+
<h1 className="text-2xl font-bold tracking-tight">Biomedical Engineering Team & Roster</h1>
|
|
76
|
+
<p className="text-sm text-muted-foreground mt-0.5">
|
|
77
|
+
Clinical engineering staff directory, real-time hospital duty status, and department coverage roster.
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Top Stats Cards */}
|
|
83
|
+
<TeamHeaderStats stats={stats} />
|
|
84
|
+
|
|
85
|
+
{/* Main Tabs */}
|
|
86
|
+
<Tabs defaultValue="directory" className="space-y-5">
|
|
87
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
88
|
+
<TabsList className="grid grid-cols-3 w-full sm:w-auto h-9">
|
|
89
|
+
<TabsTrigger value="directory" className="text-xs gap-1.5 px-3">
|
|
90
|
+
<Users className="w-3.5 h-3.5" />
|
|
91
|
+
Team Directory
|
|
92
|
+
</TabsTrigger>
|
|
93
|
+
<TabsTrigger value="schedule" className="text-xs gap-1.5 px-3">
|
|
94
|
+
<CalendarIcon className="w-3.5 h-3.5" />
|
|
95
|
+
Duty Schedule
|
|
96
|
+
</TabsTrigger>
|
|
97
|
+
<TabsTrigger value="coverage" className="text-xs gap-1.5 px-3">
|
|
98
|
+
<Building2 className="w-3.5 h-3.5" />
|
|
99
|
+
Dept. Coverage
|
|
100
|
+
</TabsTrigger>
|
|
101
|
+
</TabsList>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Tab 1: Directory */}
|
|
105
|
+
<TabsContent value="directory" className="space-y-4 m-0">
|
|
106
|
+
{/* Filters Bar */}
|
|
107
|
+
<div className="flex flex-col md:flex-row items-stretch md:items-center justify-between gap-3">
|
|
108
|
+
<div className="relative w-full md:max-w-md">
|
|
109
|
+
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
110
|
+
<Input
|
|
111
|
+
placeholder="Search engineer by name, title, specialization, or extension..."
|
|
112
|
+
value={search}
|
|
113
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
114
|
+
className="pl-9 h-9 text-xs"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="flex items-center gap-1.5 overflow-x-auto pb-1 md:pb-0 shrink-0">
|
|
119
|
+
<Button
|
|
120
|
+
variant={statusFilter === 'all' ? 'secondary' : 'ghost'}
|
|
121
|
+
size="sm"
|
|
122
|
+
onClick={() => setStatusFilter('all')}
|
|
123
|
+
className="text-xs h-8"
|
|
124
|
+
>
|
|
125
|
+
All ({initialMembers.length})
|
|
126
|
+
</Button>
|
|
127
|
+
<Button
|
|
128
|
+
variant={statusFilter === 'on_duty' ? 'secondary' : 'ghost'}
|
|
129
|
+
size="sm"
|
|
130
|
+
onClick={() => setStatusFilter('on_duty')}
|
|
131
|
+
className="text-xs h-8 text-emerald-700 dark:text-emerald-400 gap-1.5"
|
|
132
|
+
>
|
|
133
|
+
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
|
134
|
+
On Duty ({stats.onDutyToday})
|
|
135
|
+
</Button>
|
|
136
|
+
<Button
|
|
137
|
+
variant={statusFilter === 'on_call' ? 'secondary' : 'ghost'}
|
|
138
|
+
size="sm"
|
|
139
|
+
onClick={() => setStatusFilter('on_call')}
|
|
140
|
+
className="text-xs h-8 text-amber-700 dark:text-amber-400 gap-1.5"
|
|
141
|
+
>
|
|
142
|
+
<span className="w-2 h-2 rounded-full bg-amber-500" />
|
|
143
|
+
On Call ({stats.onCallToday})
|
|
144
|
+
</Button>
|
|
145
|
+
<Button
|
|
146
|
+
variant={statusFilter === 'off_duty' ? 'secondary' : 'ghost'}
|
|
147
|
+
size="sm"
|
|
148
|
+
onClick={() => setStatusFilter('off_duty')}
|
|
149
|
+
className="text-xs h-8 text-muted-foreground gap-1.5"
|
|
150
|
+
>
|
|
151
|
+
<span className="w-2 h-2 rounded-full bg-slate-400" />
|
|
152
|
+
Off Duty ({stats.offDutyToday})
|
|
153
|
+
</Button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Engineers Cards Grid */}
|
|
158
|
+
{filteredMembers.length > 0 ? (
|
|
159
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
160
|
+
{filteredMembers.map((member) => (
|
|
161
|
+
<EngineerCard
|
|
162
|
+
key={member.id}
|
|
163
|
+
engineer={member}
|
|
164
|
+
onClick={handleCardClick}
|
|
165
|
+
onEdit={handleEdit}
|
|
166
|
+
/>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
) : (
|
|
170
|
+
<div className="text-center py-12 border border-dashed rounded-xl space-y-2 bg-muted/10">
|
|
171
|
+
<Users className="w-10 h-10 text-muted-foreground mx-auto" />
|
|
172
|
+
<p className="font-semibold text-sm">No engineers match your filter</p>
|
|
173
|
+
<p className="text-xs text-muted-foreground">Try clearing your search query or status filter.</p>
|
|
174
|
+
<Button variant="outline" size="sm" onClick={() => { setSearch(''); setStatusFilter('all'); }}>
|
|
175
|
+
Reset Filters
|
|
176
|
+
</Button>
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
</TabsContent>
|
|
180
|
+
|
|
181
|
+
{/* Tab 2: Duty Calendar */}
|
|
182
|
+
<TabsContent value="schedule" className="space-y-4 m-0">
|
|
183
|
+
<ScheduleCalendar engineers={initialMembers} />
|
|
184
|
+
</TabsContent>
|
|
185
|
+
|
|
186
|
+
{/* Tab 3: Department Coverage */}
|
|
187
|
+
<TabsContent value="coverage" className="space-y-4 m-0">
|
|
188
|
+
<DepartmentCoverage coverage={coverage} />
|
|
189
|
+
</TabsContent>
|
|
190
|
+
</Tabs>
|
|
191
|
+
|
|
192
|
+
{/* Full Engineer Details Dialog */}
|
|
193
|
+
{viewEngineer && (
|
|
194
|
+
<EngineerDetailsDialog
|
|
195
|
+
engineer={viewEngineer}
|
|
196
|
+
open={isDetailsOpen}
|
|
197
|
+
onOpenChange={setIsDetailsOpen}
|
|
198
|
+
onEdit={handleEdit}
|
|
199
|
+
onStatusUpdated={() => router.refresh()}
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{/* Edit Engineer Dialog */}
|
|
204
|
+
{selectedEngineer && (
|
|
205
|
+
<EditEngineerDialog
|
|
206
|
+
key={selectedEngineer.id}
|
|
207
|
+
engineer={selectedEngineer}
|
|
208
|
+
open={isEditDialogOpen}
|
|
209
|
+
onOpenChange={setIsEditDialogOpen}
|
|
210
|
+
onSuccess={() => router.refresh()}
|
|
211
|
+
/>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useTheme, type Theme } from '@/components/providers/theme-provider'
|
|
5
|
+
import { Sun, Moon, Monitor } from 'lucide-react'
|
|
6
|
+
import { cn } from '@/lib/utils'
|
|
7
|
+
|
|
8
|
+
export function ThemeSelectorCard() {
|
|
9
|
+
const { theme, setTheme } = useTheme()
|
|
10
|
+
const [mounted, setMounted] = React.useState(false)
|
|
11
|
+
|
|
12
|
+
React.useEffect(() => {
|
|
13
|
+
setMounted(true)
|
|
14
|
+
}, [])
|
|
15
|
+
|
|
16
|
+
const options: { value: Theme; label: string; icon: React.ComponentType<{ className?: string }> }[] = [
|
|
17
|
+
{ value: 'light', label: 'Light', icon: Sun },
|
|
18
|
+
{ value: 'dark', label: 'Dark', icon: Moon },
|
|
19
|
+
{ value: 'system', label: 'System', icon: Monitor },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="space-y-2">
|
|
24
|
+
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground px-1">
|
|
25
|
+
Appearance & Theme
|
|
26
|
+
</h3>
|
|
27
|
+
<div className="bg-card rounded-xl border p-3 sm:p-4 shadow-sm">
|
|
28
|
+
<div className="flex items-center justify-between gap-2">
|
|
29
|
+
<div className="flex flex-col">
|
|
30
|
+
<span className="text-sm font-medium leading-snug">Interface Theme</span>
|
|
31
|
+
<span className="text-xs text-muted-foreground">
|
|
32
|
+
Select your preferred visual style or sync with device settings
|
|
33
|
+
</span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div className="grid grid-cols-3 gap-2 mt-3.5">
|
|
38
|
+
{options.map((opt) => {
|
|
39
|
+
const Icon = opt.icon
|
|
40
|
+
const isSelected = mounted && theme === opt.value
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<button
|
|
44
|
+
key={opt.value}
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={() => setTheme(opt.value)}
|
|
47
|
+
className={cn(
|
|
48
|
+
"flex items-center justify-center gap-2 p-2.5 rounded-lg border text-xs font-medium transition-all min-h-[44px]",
|
|
49
|
+
isSelected
|
|
50
|
+
? "bg-primary text-primary-foreground border-primary shadow-sm"
|
|
51
|
+
: "bg-muted/40 hover:bg-muted text-muted-foreground hover:text-foreground border-transparent"
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
<Icon className={cn("w-4 h-4", isSelected ? "text-primary-foreground" : "")} />
|
|
55
|
+
<span>{opt.label}</span>
|
|
56
|
+
</button>
|
|
57
|
+
)
|
|
58
|
+
})}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useTheme } from '@/components/providers/theme-provider'
|
|
5
|
+
import { Moon, Sun, Monitor, Check } from 'lucide-react'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from '@/components/ui/dropdown-menu'
|
|
13
|
+
|
|
14
|
+
export function ThemeToggle() {
|
|
15
|
+
const { setTheme, theme } = useTheme()
|
|
16
|
+
const [mounted, setMounted] = React.useState(false)
|
|
17
|
+
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
setMounted(true)
|
|
20
|
+
}, [])
|
|
21
|
+
|
|
22
|
+
if (!mounted) {
|
|
23
|
+
return (
|
|
24
|
+
<Button variant="ghost" size="icon" className="h-9 w-9">
|
|
25
|
+
<Sun className="h-4 w-4" />
|
|
26
|
+
<span className="sr-only">Toggle theme</span>
|
|
27
|
+
</Button>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<DropdownMenu>
|
|
33
|
+
<DropdownMenuTrigger asChild>
|
|
34
|
+
<Button variant="ghost" size="icon" className="h-9 w-9">
|
|
35
|
+
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
36
|
+
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
37
|
+
<span className="sr-only">Toggle theme</span>
|
|
38
|
+
</Button>
|
|
39
|
+
</DropdownMenuTrigger>
|
|
40
|
+
<DropdownMenuContent align="end">
|
|
41
|
+
<DropdownMenuItem onClick={() => setTheme('light')} className="cursor-pointer justify-between">
|
|
42
|
+
<div className="flex items-center">
|
|
43
|
+
<Sun className="mr-2 h-4 w-4" />
|
|
44
|
+
<span>Light</span>
|
|
45
|
+
</div>
|
|
46
|
+
{theme === 'light' && <Check className="h-4 w-4 text-primary ml-2" />}
|
|
47
|
+
</DropdownMenuItem>
|
|
48
|
+
<DropdownMenuItem onClick={() => setTheme('dark')} className="cursor-pointer justify-between">
|
|
49
|
+
<div className="flex items-center">
|
|
50
|
+
<Moon className="mr-2 h-4 w-4" />
|
|
51
|
+
<span>Dark</span>
|
|
52
|
+
</div>
|
|
53
|
+
{theme === 'dark' && <Check className="h-4 w-4 text-primary ml-2" />}
|
|
54
|
+
</DropdownMenuItem>
|
|
55
|
+
<DropdownMenuItem onClick={() => setTheme('system')} className="cursor-pointer justify-between">
|
|
56
|
+
<div className="flex items-center">
|
|
57
|
+
<Monitor className="mr-2 h-4 w-4" />
|
|
58
|
+
<span>System</span>
|
|
59
|
+
</div>
|
|
60
|
+
{theme === 'system' && <Check className="h-4 w-4 text-primary ml-2" />}
|
|
61
|
+
</DropdownMenuItem>
|
|
62
|
+
</DropdownMenuContent>
|
|
63
|
+
</DropdownMenu>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { Badge } from '@/components/ui/badge';
|
|
3
|
+
import { getStaffStatus, getStaffStatusStyle } from '@/lib/utils/staff-status';
|
|
4
|
+
|
|
5
|
+
interface StaffStatusLabelProps {
|
|
6
|
+
statusCode: string;
|
|
7
|
+
showIcon?: boolean;
|
|
8
|
+
size?: 'sm' | 'default';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Staff-facing ticket status badge.
|
|
13
|
+
* Shows plain-language labels like "Being repaired" instead of "in_progress".
|
|
14
|
+
* Used in staff dashboards, ticket lists, and ticket detail pages.
|
|
15
|
+
*/
|
|
16
|
+
export function StaffStatusLabel({ statusCode, showIcon = true, size = 'default' }: StaffStatusLabelProps) {
|
|
17
|
+
const info = getStaffStatus(statusCode);
|
|
18
|
+
const style = getStaffStatusStyle(statusCode);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Badge
|
|
22
|
+
variant="outline"
|
|
23
|
+
className={cn(
|
|
24
|
+
'font-medium whitespace-nowrap',
|
|
25
|
+
style.bg,
|
|
26
|
+
style.text,
|
|
27
|
+
style.border,
|
|
28
|
+
size === 'sm' && 'text-xs px-1.5 py-0',
|
|
29
|
+
)}
|
|
30
|
+
>
|
|
31
|
+
{showIcon && <span className="mr-1">{info.icon}</span>}
|
|
32
|
+
{info.label}
|
|
33
|
+
</Badge>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Larger staff status display with description.
|
|
39
|
+
* Used on ticket detail pages for staff.
|
|
40
|
+
*/
|
|
41
|
+
export function StaffStatusBanner({ statusCode }: { statusCode: string }) {
|
|
42
|
+
const info = getStaffStatus(statusCode);
|
|
43
|
+
const style = getStaffStatusStyle(statusCode);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className={cn('rounded-lg border px-4 py-3', style.bg, style.border)}>
|
|
47
|
+
<div className="flex items-center gap-2">
|
|
48
|
+
<span className="text-lg">{info.icon}</span>
|
|
49
|
+
<div>
|
|
50
|
+
<p className={cn('font-semibold text-sm', style.text)}>{info.label}</p>
|
|
51
|
+
{info.description && (
|
|
52
|
+
<p className="text-xs text-muted-foreground mt-0.5">{info.description}</p>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { getStaffStatus, getStaffStatusStyle } from '@/lib/utils/staff-status';
|
|
6
|
+
import { Monitor, User, MessageCircle } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export interface StaffTicketItem {
|
|
9
|
+
id: string;
|
|
10
|
+
ticketNumber: string;
|
|
11
|
+
title: string;
|
|
12
|
+
statusCode: string;
|
|
13
|
+
reportedAt?: Date | string | null;
|
|
14
|
+
updatedAt?: Date | string | null;
|
|
15
|
+
deviceName?: string | null;
|
|
16
|
+
deviceAssetNumber?: string | null;
|
|
17
|
+
assignedEngineerName?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function StaffTicketCard({ ticket }: { ticket: StaffTicketItem }) {
|
|
21
|
+
const status = getStaffStatus(ticket.statusCode);
|
|
22
|
+
const style = getStaffStatusStyle(ticket.statusCode);
|
|
23
|
+
const needsResponse = ticket.statusCode === 'waiting_requester';
|
|
24
|
+
const isResolved = ['resolved', 'closed'].includes(ticket.statusCode);
|
|
25
|
+
|
|
26
|
+
const updatedText = ticket.updatedAt
|
|
27
|
+
? formatRelativeTime(new Date(ticket.updatedAt))
|
|
28
|
+
: ticket.reportedAt
|
|
29
|
+
? formatRelativeTime(new Date(ticket.reportedAt))
|
|
30
|
+
: '';
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Link
|
|
34
|
+
href={`/tickets/${ticket.id}`}
|
|
35
|
+
className={cn(
|
|
36
|
+
'block bg-card rounded-xl border shadow-sm overflow-hidden transition-all hover:shadow-md',
|
|
37
|
+
needsResponse && 'border-amber-300 dark:border-amber-800',
|
|
38
|
+
isResolved && 'opacity-75',
|
|
39
|
+
!needsResponse && !isResolved && 'border-border hover:border-primary/50',
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
{/* Response requested banner */}
|
|
43
|
+
{needsResponse && (
|
|
44
|
+
<div className="bg-amber-50 dark:bg-amber-950/30 px-4 py-1.5 text-xs font-semibold text-amber-700 dark:text-amber-300 flex items-center gap-1.5 border-b border-amber-200 dark:border-amber-800">
|
|
45
|
+
<MessageCircle className="w-3.5 h-3.5" />
|
|
46
|
+
Response requested — tap to reply
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
|
|
50
|
+
<div className="p-4">
|
|
51
|
+
{/* Title and status */}
|
|
52
|
+
<div className="flex items-start justify-between gap-2">
|
|
53
|
+
<div className="min-w-0">
|
|
54
|
+
<p className="font-semibold text-sm line-clamp-1">{ticket.title}</p>
|
|
55
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
56
|
+
{ticket.ticketNumber}
|
|
57
|
+
{ticket.deviceName && (
|
|
58
|
+
<> · {ticket.deviceName}</>
|
|
59
|
+
)}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
<span
|
|
63
|
+
className={cn(
|
|
64
|
+
'text-xs px-2 py-0.5 rounded-full font-medium whitespace-nowrap border',
|
|
65
|
+
style.bg,
|
|
66
|
+
style.text,
|
|
67
|
+
style.border
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{status.icon} {status.label}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Engineer and time */}
|
|
75
|
+
<div className="mt-2 flex items-center gap-4 text-xs text-muted-foreground">
|
|
76
|
+
{ticket.assignedEngineerName ? (
|
|
77
|
+
<span className="flex items-center gap-1">
|
|
78
|
+
<User className="w-3 h-3" />
|
|
79
|
+
{ticket.assignedEngineerName}
|
|
80
|
+
</span>
|
|
81
|
+
) : (
|
|
82
|
+
<span className="flex items-center gap-1 text-muted-foreground/70">
|
|
83
|
+
<User className="w-3 h-3" />
|
|
84
|
+
Awaiting assignment
|
|
85
|
+
</span>
|
|
86
|
+
)}
|
|
87
|
+
{updatedText && (
|
|
88
|
+
<span>Updated {updatedText}</span>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</Link>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Simple relative time formatter */
|
|
97
|
+
function formatRelativeTime(date: Date): string {
|
|
98
|
+
const now = new Date();
|
|
99
|
+
const diffMs = now.getTime() - date.getTime();
|
|
100
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
101
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
102
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
103
|
+
|
|
104
|
+
if (diffMins < 1) return 'just now';
|
|
105
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
106
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
107
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
108
|
+
return date.toLocaleDateString();
|
|
109
|
+
}
|