@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,93 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { QrCode, PlusCircle, Inbox, Users, MonitorSmartphone, Monitor, Building2 } from "lucide-react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
|
|
6
|
+
interface ActionButtonsProps {
|
|
7
|
+
role: string;
|
|
8
|
+
triageCount?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ActionButtons({ role, triageCount = 0 }: ActionButtonsProps) {
|
|
12
|
+
const normalizedRole = (role || '').toUpperCase();
|
|
13
|
+
const isStaff = normalizedRole === 'STAFF' || normalizedRole === 'DEPT_MGR';
|
|
14
|
+
const isBiomed = normalizedRole.startsWith('BIOMED') || normalizedRole === 'BME';
|
|
15
|
+
const isAdmin = normalizedRole.includes('ADMIN');
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:flex-wrap">
|
|
19
|
+
<Button asChild size="lg" className="w-full sm:w-auto min-h-[44px]">
|
|
20
|
+
<Link href="/devices/scan">
|
|
21
|
+
<QrCode className="mr-2 h-5 w-5" />
|
|
22
|
+
Scan Device
|
|
23
|
+
</Link>
|
|
24
|
+
</Button>
|
|
25
|
+
|
|
26
|
+
{isStaff && (
|
|
27
|
+
<Button asChild variant="secondary" size="lg" className="w-full sm:w-auto min-h-[44px]">
|
|
28
|
+
<Link href="/tickets/new">
|
|
29
|
+
<PlusCircle className="mr-2 h-5 w-5" />
|
|
30
|
+
Report a Problem
|
|
31
|
+
</Link>
|
|
32
|
+
</Button>
|
|
33
|
+
)}
|
|
34
|
+
|
|
35
|
+
{isBiomed && (
|
|
36
|
+
<>
|
|
37
|
+
<Button asChild variant="secondary" size="lg" className="w-full sm:w-auto min-h-[44px] relative">
|
|
38
|
+
<Link href="/tickets/triage">
|
|
39
|
+
<Inbox className="mr-2 h-5 w-5" />
|
|
40
|
+
Triage Queue
|
|
41
|
+
{triageCount > 0 && (
|
|
42
|
+
<Badge variant="destructive" className="ml-2 absolute -top-2 -right-2">
|
|
43
|
+
{triageCount}
|
|
44
|
+
</Badge>
|
|
45
|
+
)}
|
|
46
|
+
</Link>
|
|
47
|
+
</Button>
|
|
48
|
+
<Button asChild variant="outline" size="lg" className="w-full sm:w-auto min-h-[44px]">
|
|
49
|
+
<Link href="/tickets">
|
|
50
|
+
<Inbox className="mr-2 h-5 w-5" />
|
|
51
|
+
Helpdesk Tickets
|
|
52
|
+
</Link>
|
|
53
|
+
</Button>
|
|
54
|
+
<Button asChild variant="outline" size="lg" className="w-full sm:w-auto min-h-[44px]">
|
|
55
|
+
<Link href="/devices">
|
|
56
|
+
<Monitor className="mr-2 h-5 w-5" />
|
|
57
|
+
Devices
|
|
58
|
+
</Link>
|
|
59
|
+
</Button>
|
|
60
|
+
<Button asChild variant="outline" size="lg" className="w-full sm:w-auto min-h-[44px]">
|
|
61
|
+
<Link href="/departments">
|
|
62
|
+
<Building2 className="mr-2 h-5 w-5" />
|
|
63
|
+
Departments
|
|
64
|
+
</Link>
|
|
65
|
+
</Button>
|
|
66
|
+
<Button asChild variant="outline" size="lg" className="w-full sm:w-auto min-h-[44px]">
|
|
67
|
+
<Link href="/team">
|
|
68
|
+
<Users className="mr-2 h-5 w-5" />
|
|
69
|
+
Engineering Team
|
|
70
|
+
</Link>
|
|
71
|
+
</Button>
|
|
72
|
+
</>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{isAdmin && (
|
|
76
|
+
<>
|
|
77
|
+
<Button asChild variant="outline" size="lg" className="w-full sm:w-auto min-h-[44px]">
|
|
78
|
+
<Link href="/admin/users">
|
|
79
|
+
<Users className="mr-2 h-5 w-5" />
|
|
80
|
+
Manage Users
|
|
81
|
+
</Link>
|
|
82
|
+
</Button>
|
|
83
|
+
<Button asChild variant="outline" size="lg" className="w-full sm:w-auto min-h-[44px]">
|
|
84
|
+
<Link href="/admin/devices">
|
|
85
|
+
<MonitorSmartphone className="mr-2 h-5 w-5" />
|
|
86
|
+
Manage Devices
|
|
87
|
+
</Link>
|
|
88
|
+
</Button>
|
|
89
|
+
</>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|
3
|
+
import { ShieldAlert, Users, Building, UserPlus } from "lucide-react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
|
|
6
|
+
interface AdminAlertsData {
|
|
7
|
+
usersWithoutRoles: number;
|
|
8
|
+
pendingInvitations: number;
|
|
9
|
+
departmentsWithoutManager: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AdminAlertsProps {
|
|
13
|
+
alerts: AdminAlertsData;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AdminAlerts({ alerts }: AdminAlertsProps) {
|
|
17
|
+
const hasAlerts = alerts.usersWithoutRoles > 0 || alerts.pendingInvitations > 0 || alerts.departmentsWithoutManager > 0;
|
|
18
|
+
|
|
19
|
+
if (!hasAlerts) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Card className="border-red-200">
|
|
25
|
+
<CardHeader className="pb-3 bg-red-50/50 rounded-t-xl border-b border-red-100">
|
|
26
|
+
<CardTitle className="text-lg flex items-center text-red-800">
|
|
27
|
+
<ShieldAlert className="w-5 h-5 mr-2 text-red-500" />
|
|
28
|
+
System Administration Alerts
|
|
29
|
+
</CardTitle>
|
|
30
|
+
</CardHeader>
|
|
31
|
+
<CardContent className="pt-4 space-y-3">
|
|
32
|
+
{alerts.usersWithoutRoles > 0 && (
|
|
33
|
+
<div className="flex items-center justify-between p-3 border rounded-lg bg-card min-h-[44px]">
|
|
34
|
+
<div className="flex items-center gap-3">
|
|
35
|
+
<div className="bg-red-100 dark:bg-red-950 p-2 rounded-full">
|
|
36
|
+
<Users className="w-4 h-4 text-red-700 dark:text-red-400" />
|
|
37
|
+
</div>
|
|
38
|
+
<div className="flex flex-col">
|
|
39
|
+
<span className="text-sm font-medium text-red-900 dark:text-red-300">{alerts.usersWithoutRoles} Users Without Roles</span>
|
|
40
|
+
<span className="text-xs text-red-700/80 dark:text-red-400/80">Users cannot access the system until roles are assigned.</span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<Button asChild size="sm" variant="outline" className="border-red-200 text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-950">
|
|
44
|
+
<Link href="/admin/users?filter=no-role">Fix</Link>
|
|
45
|
+
</Button>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
{alerts.pendingInvitations > 0 && (
|
|
50
|
+
<div className="flex items-center justify-between p-3 border rounded-lg bg-card min-h-[44px]">
|
|
51
|
+
<div className="flex items-center gap-3">
|
|
52
|
+
<div className="bg-yellow-100 dark:bg-yellow-950 p-2 rounded-full">
|
|
53
|
+
<UserPlus className="w-4 h-4 text-yellow-700 dark:text-yellow-400" />
|
|
54
|
+
</div>
|
|
55
|
+
<div className="flex flex-col">
|
|
56
|
+
<span className="text-sm font-medium text-yellow-900 dark:text-yellow-300">{alerts.pendingInvitations} Pending Invitations</span>
|
|
57
|
+
<span className="text-xs text-yellow-700/80 dark:text-yellow-400/80">Users have not yet accepted their invitations.</span>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<Button asChild size="sm" variant="outline" className="border-yellow-200 text-yellow-700 hover:bg-yellow-50 dark:border-yellow-800 dark:text-yellow-400 dark:hover:bg-yellow-950">
|
|
61
|
+
<Link href="/admin/users?status=invited">Review</Link>
|
|
62
|
+
</Button>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{alerts.departmentsWithoutManager > 0 && (
|
|
67
|
+
<div className="flex items-center justify-between p-3 border rounded-lg bg-card min-h-[44px]">
|
|
68
|
+
<div className="flex items-center gap-3">
|
|
69
|
+
<div className="bg-orange-100 dark:bg-orange-950 p-2 rounded-full">
|
|
70
|
+
<Building className="w-4 h-4 text-orange-700 dark:text-orange-400" />
|
|
71
|
+
</div>
|
|
72
|
+
<div className="flex flex-col">
|
|
73
|
+
<span className="text-sm font-medium text-orange-900 dark:text-orange-300">{alerts.departmentsWithoutManager} Departments Missing Manager</span>
|
|
74
|
+
<span className="text-xs text-orange-700/80 dark:text-orange-400/80">Some departments lack an assigned manager.</span>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<Button asChild size="sm" variant="outline" className="border-orange-200 text-orange-700 hover:bg-orange-50 dark:border-orange-800 dark:text-orange-400 dark:hover:bg-orange-950">
|
|
78
|
+
<Link href="/admin/departments?filter=no-manager">Manage</Link>
|
|
79
|
+
</Button>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</CardContent>
|
|
83
|
+
</Card>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|
3
|
+
import { Badge } from "@/components/ui/badge";
|
|
4
|
+
import { AlertTriangle, MonitorSmartphone } from "lucide-react";
|
|
5
|
+
import { StatusBadge } from "@/components/shared/status-badge";
|
|
6
|
+
|
|
7
|
+
interface UnavailableDevice {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
assetNumber: string;
|
|
11
|
+
currentStatusCode: string;
|
|
12
|
+
departmentName?: string | null;
|
|
13
|
+
locationDescription?: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AvailabilityAlertsProps {
|
|
17
|
+
devices: UnavailableDevice[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function AvailabilityAlerts({ devices }: AvailabilityAlertsProps) {
|
|
21
|
+
return (
|
|
22
|
+
<Card className="border-orange-200 dark:border-orange-900 shadow-sm">
|
|
23
|
+
<CardHeader className="pb-3 bg-orange-50/50 dark:bg-orange-950/30 rounded-t-xl border-b border-orange-100 dark:border-orange-900">
|
|
24
|
+
<CardTitle className="text-lg flex items-center text-orange-800 dark:text-orange-300">
|
|
25
|
+
<AlertTriangle className="w-5 h-5 mr-2 text-orange-500" />
|
|
26
|
+
Unavailable Devices
|
|
27
|
+
</CardTitle>
|
|
28
|
+
</CardHeader>
|
|
29
|
+
<CardContent className="pt-4">
|
|
30
|
+
{devices.length === 0 ? (
|
|
31
|
+
<div className="text-center py-6 text-sm text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/30 rounded-md border border-green-100 dark:border-green-900 flex flex-col items-center gap-2">
|
|
32
|
+
<MonitorSmartphone className="h-8 w-8 text-green-500 opacity-80" />
|
|
33
|
+
<p>All department devices are operational.</p>
|
|
34
|
+
</div>
|
|
35
|
+
) : (
|
|
36
|
+
<div className="space-y-3">
|
|
37
|
+
{devices.map((device) => (
|
|
38
|
+
<Link key={device.id} href={`/devices/${device.id}`} className="block">
|
|
39
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between p-3 border border-orange-100 dark:border-orange-900 bg-card rounded-lg hover:bg-orange-50/30 dark:hover:bg-orange-950/30 transition-colors gap-2 min-h-[44px]">
|
|
40
|
+
<div className="flex flex-col gap-1">
|
|
41
|
+
<div className="flex items-center gap-2">
|
|
42
|
+
<span className="text-sm font-semibold">{device.assetNumber}</span>
|
|
43
|
+
<StatusBadge status={device.currentStatusCode} />
|
|
44
|
+
</div>
|
|
45
|
+
<span className="text-sm font-medium">{device.name}</span>
|
|
46
|
+
<span className="text-xs text-muted-foreground flex gap-1 items-center">
|
|
47
|
+
{device.departmentName}
|
|
48
|
+
{device.locationDescription && (
|
|
49
|
+
<>
|
|
50
|
+
<span>•</span>
|
|
51
|
+
<span>{device.locationDescription}</span>
|
|
52
|
+
</>
|
|
53
|
+
)}
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</Link>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</CardContent>
|
|
62
|
+
</Card>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { formatDistanceToNow, isPast, isToday } from "date-fns";
|
|
3
|
+
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import { Wrench } from "lucide-react";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
interface MaintenanceTaskItem {
|
|
9
|
+
id: string;
|
|
10
|
+
taskNumber: string;
|
|
11
|
+
title: string;
|
|
12
|
+
statusCode: string;
|
|
13
|
+
dueDate: Date | null;
|
|
14
|
+
deviceName?: string | null;
|
|
15
|
+
maintenanceType: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface MaintenanceDueListProps {
|
|
19
|
+
tasks: MaintenanceTaskItem[];
|
|
20
|
+
title: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function MaintenanceDueList({ tasks, title }: MaintenanceDueListProps) {
|
|
24
|
+
return (
|
|
25
|
+
<Card>
|
|
26
|
+
<CardHeader className="pb-3">
|
|
27
|
+
<CardTitle className="text-lg flex items-center">
|
|
28
|
+
<Wrench className="w-5 h-5 mr-2" />
|
|
29
|
+
{title}
|
|
30
|
+
</CardTitle>
|
|
31
|
+
</CardHeader>
|
|
32
|
+
<CardContent>
|
|
33
|
+
{tasks.length === 0 ? (
|
|
34
|
+
<p className="text-muted-foreground text-sm py-4 text-center border rounded-md bg-muted/20">
|
|
35
|
+
No maintenance tasks due.
|
|
36
|
+
</p>
|
|
37
|
+
) : (
|
|
38
|
+
<div className="space-y-3">
|
|
39
|
+
{tasks.map((task) => {
|
|
40
|
+
let dueLabel = "Scheduled";
|
|
41
|
+
let dueColor = "bg-blue-100 text-blue-800 border-blue-200";
|
|
42
|
+
|
|
43
|
+
if (task.dueDate) {
|
|
44
|
+
const date = new Date(task.dueDate);
|
|
45
|
+
if (isPast(date) && !isToday(date)) {
|
|
46
|
+
dueLabel = "Overdue";
|
|
47
|
+
dueColor = "bg-red-100 text-red-800 border-red-200 font-medium";
|
|
48
|
+
} else if (isToday(date)) {
|
|
49
|
+
dueLabel = "Due Today";
|
|
50
|
+
dueColor = "bg-orange-100 text-orange-800 border-orange-200 font-medium";
|
|
51
|
+
} else {
|
|
52
|
+
dueLabel = "Due Soon";
|
|
53
|
+
dueColor = "bg-yellow-100 text-yellow-800 border-yellow-200";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Link key={task.id} href={`/maintenance/tasks/${task.id}`} className="block">
|
|
59
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between p-3 border rounded-lg hover:bg-muted/50 transition-colors gap-2 min-h-[44px]">
|
|
60
|
+
<div className="flex flex-col gap-1">
|
|
61
|
+
<div className="flex items-center gap-2">
|
|
62
|
+
<span className="text-sm font-semibold text-primary">{task.taskNumber}</span>
|
|
63
|
+
<Badge variant="outline" className={cn("text-xs font-normal", dueColor)}>
|
|
64
|
+
{dueLabel}
|
|
65
|
+
</Badge>
|
|
66
|
+
</div>
|
|
67
|
+
<span className="text-sm font-medium line-clamp-1">{task.title}</span>
|
|
68
|
+
{task.deviceName && (
|
|
69
|
+
<span className="text-xs text-muted-foreground line-clamp-1">{task.deviceName}</span>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
{task.dueDate && (
|
|
73
|
+
<div className="text-xs text-muted-foreground whitespace-nowrap self-start sm:self-auto">
|
|
74
|
+
{formatDistanceToNow(new Date(task.dueDate), { addSuffix: true })}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
</Link>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</CardContent>
|
|
84
|
+
</Card>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { isEngineerRoundEnabled, getTodayRound } from '@/lib/actions/engineer-rounds';
|
|
2
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Progress } from '@/components/ui/progress';
|
|
5
|
+
import { Route, CheckCircle2, Clock, ArrowRight, ShieldCheck } from 'lucide-react';
|
|
6
|
+
import Link from 'next/link';
|
|
7
|
+
|
|
8
|
+
export async function RoundWidget() {
|
|
9
|
+
const enabled = await isEngineerRoundEnabled();
|
|
10
|
+
if (!enabled) return null;
|
|
11
|
+
|
|
12
|
+
const result = await getTodayRound();
|
|
13
|
+
if (!result.success) return null;
|
|
14
|
+
|
|
15
|
+
// Off day
|
|
16
|
+
if (result.isOffDay) {
|
|
17
|
+
return (
|
|
18
|
+
<Card className="border-muted bg-muted/20">
|
|
19
|
+
<CardContent className="pt-4 pb-4 flex items-center justify-between">
|
|
20
|
+
<div className="flex items-center gap-3">
|
|
21
|
+
<div className="rounded-md bg-muted p-2 text-muted-foreground">
|
|
22
|
+
<Route className="h-5 w-5" />
|
|
23
|
+
</div>
|
|
24
|
+
<div>
|
|
25
|
+
<p className="text-sm font-medium">Daily Engineering Round</p>
|
|
26
|
+
<p className="text-xs text-muted-foreground">Not a scheduled working day</p>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<Link href="/rounds/history">
|
|
30
|
+
<Button variant="ghost" size="sm" className="gap-1 text-xs">
|
|
31
|
+
History <ArrowRight className="h-3 w-3" />
|
|
32
|
+
</Button>
|
|
33
|
+
</Link>
|
|
34
|
+
</CardContent>
|
|
35
|
+
</Card>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Can start round
|
|
40
|
+
if (result.canStart && !result.data) {
|
|
41
|
+
return (
|
|
42
|
+
<Card className="border-primary/20 bg-primary/5">
|
|
43
|
+
<CardContent className="pt-4 pb-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
44
|
+
<div className="flex items-center gap-3">
|
|
45
|
+
<div className="rounded-md bg-primary/10 p-2 text-primary">
|
|
46
|
+
<Route className="h-5 w-5" />
|
|
47
|
+
</div>
|
|
48
|
+
<div>
|
|
49
|
+
<p className="text-sm font-semibold">Today's Engineering Round</p>
|
|
50
|
+
<p className="text-xs text-muted-foreground">
|
|
51
|
+
{result.departments?.length || 0} departments scheduled · {result.shiftType} shift
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<Link href="/rounds">
|
|
56
|
+
<Button size="sm" className="gap-1.5 w-full sm:w-auto">
|
|
57
|
+
<ShieldCheck className="h-4 w-4" />
|
|
58
|
+
Start Round
|
|
59
|
+
</Button>
|
|
60
|
+
</Link>
|
|
61
|
+
</CardContent>
|
|
62
|
+
</Card>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Round in progress or completed
|
|
67
|
+
if (result.data) {
|
|
68
|
+
const round = result.data as any;
|
|
69
|
+
const isCompleted = round.status === 'completed';
|
|
70
|
+
const total = round.totalDepartments || round.departmentVisits?.length || 0;
|
|
71
|
+
const completed = round.completedDepartments || 0;
|
|
72
|
+
const progress = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Card className={isCompleted ? 'border-green-200 bg-green-50/50 dark:border-green-900/30 dark:bg-green-950/10' : 'border-blue-200 bg-blue-50/50 dark:border-blue-900/30 dark:bg-blue-950/10'}>
|
|
76
|
+
<CardContent className="pt-4 pb-4 space-y-2.5">
|
|
77
|
+
<div className="flex items-center justify-between">
|
|
78
|
+
<div className="flex items-center gap-2.5">
|
|
79
|
+
<div className={`rounded-md p-1.5 ${isCompleted ? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400' : 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400'}`}>
|
|
80
|
+
{isCompleted ? <CheckCircle2 className="h-4 w-4" /> : <Clock className="h-4 w-4" />}
|
|
81
|
+
</div>
|
|
82
|
+
<div>
|
|
83
|
+
<p className="text-sm font-semibold">
|
|
84
|
+
{isCompleted ? 'Daily Round Completed' : 'Daily Round In Progress'}
|
|
85
|
+
</p>
|
|
86
|
+
<p className="text-xs text-muted-foreground">
|
|
87
|
+
{completed} / {total} departments visited ({progress}%)
|
|
88
|
+
{round.totalIssuesReported > 0 ? ` · ${round.totalIssuesReported} issues reported` : ''}
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<Link href="/rounds">
|
|
94
|
+
<Button variant={isCompleted ? 'outline' : 'default'} size="sm" className="gap-1 text-xs">
|
|
95
|
+
{isCompleted ? 'View Round' : 'Continue Round'}
|
|
96
|
+
<ArrowRight className="h-3 w-3" />
|
|
97
|
+
</Button>
|
|
98
|
+
</Link>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{!isCompleted && (
|
|
102
|
+
<Progress value={progress} className="h-1.5" />
|
|
103
|
+
)}
|
|
104
|
+
</CardContent>
|
|
105
|
+
</Card>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
interface StatCardProps {
|
|
7
|
+
title: string;
|
|
8
|
+
value: number | string;
|
|
9
|
+
icon: ReactNode;
|
|
10
|
+
href: string;
|
|
11
|
+
color?: string;
|
|
12
|
+
trend?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function StatCard({ title, value, icon, href, trend, className }: StatCardProps) {
|
|
17
|
+
return (
|
|
18
|
+
<Link href={href} className="block min-h-[44px]">
|
|
19
|
+
<Card className={cn("hover:bg-muted/50 transition-colors cursor-pointer h-full", className)}>
|
|
20
|
+
<CardContent className="p-6">
|
|
21
|
+
<div className="flex items-center justify-between space-x-2">
|
|
22
|
+
<h3 className="text-sm font-medium text-muted-foreground">{title}</h3>
|
|
23
|
+
{icon}
|
|
24
|
+
</div>
|
|
25
|
+
<div className="flex flex-col mt-4">
|
|
26
|
+
<div className="text-3xl font-bold">{value}</div>
|
|
27
|
+
{trend && (
|
|
28
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
29
|
+
{trend}
|
|
30
|
+
</p>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
</CardContent>
|
|
34
|
+
</Card>
|
|
35
|
+
</Link>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { formatDistanceToNow } from "date-fns";
|
|
3
|
+
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import { Ticket } from "lucide-react";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
interface TicketItem {
|
|
9
|
+
id: string;
|
|
10
|
+
ticketNumber: string;
|
|
11
|
+
title: string;
|
|
12
|
+
statusCode: string;
|
|
13
|
+
priorityCode: string;
|
|
14
|
+
reportedAt: Date;
|
|
15
|
+
deviceName?: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface TicketListCompactProps {
|
|
19
|
+
tickets: TicketItem[];
|
|
20
|
+
title: string;
|
|
21
|
+
emptyMessage: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function TicketListCompact({ tickets, title, emptyMessage }: TicketListCompactProps) {
|
|
25
|
+
return (
|
|
26
|
+
<Card>
|
|
27
|
+
<CardHeader className="pb-3">
|
|
28
|
+
<CardTitle className="text-lg flex items-center">
|
|
29
|
+
<Ticket className="w-5 h-5 mr-2" />
|
|
30
|
+
{title}
|
|
31
|
+
</CardTitle>
|
|
32
|
+
</CardHeader>
|
|
33
|
+
<CardContent>
|
|
34
|
+
{tickets.length === 0 ? (
|
|
35
|
+
<p className="text-muted-foreground text-sm py-4 text-center border rounded-md bg-muted/20">
|
|
36
|
+
{emptyMessage}
|
|
37
|
+
</p>
|
|
38
|
+
) : (
|
|
39
|
+
<div className="space-y-3">
|
|
40
|
+
{tickets.map((ticket) => {
|
|
41
|
+
// Determine priority badge color
|
|
42
|
+
let priorityColor = "bg-gray-100 text-gray-800";
|
|
43
|
+
let priorityLabel = ticket.priorityCode;
|
|
44
|
+
if (ticket.priorityCode === 'p1_critical') {
|
|
45
|
+
priorityColor = "bg-red-100 text-red-800 border-red-200";
|
|
46
|
+
priorityLabel = "Critical";
|
|
47
|
+
} else if (ticket.priorityCode === 'p2_high') {
|
|
48
|
+
priorityColor = "bg-orange-100 text-orange-800 border-orange-200";
|
|
49
|
+
priorityLabel = "High";
|
|
50
|
+
} else if (ticket.priorityCode === 'p3_normal') {
|
|
51
|
+
priorityColor = "bg-blue-100 text-blue-800 border-blue-200";
|
|
52
|
+
priorityLabel = "Normal";
|
|
53
|
+
} else if (ticket.priorityCode === 'p4_low') {
|
|
54
|
+
priorityColor = "bg-gray-100 text-gray-800 border-gray-200";
|
|
55
|
+
priorityLabel = "Low";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Link key={ticket.id} href={`/tickets/${ticket.id}`} className="block">
|
|
60
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between p-3 border rounded-lg hover:bg-muted/50 transition-colors gap-2 min-h-[44px]">
|
|
61
|
+
<div className="flex flex-col gap-1">
|
|
62
|
+
<div className="flex items-center gap-2">
|
|
63
|
+
<span className="text-sm font-semibold text-primary">{ticket.ticketNumber}</span>
|
|
64
|
+
<Badge variant="outline" className={cn("text-xs font-normal", priorityColor)}>
|
|
65
|
+
{priorityLabel}
|
|
66
|
+
</Badge>
|
|
67
|
+
</div>
|
|
68
|
+
<span className="text-sm font-medium line-clamp-1">{ticket.title}</span>
|
|
69
|
+
{ticket.deviceName && (
|
|
70
|
+
<span className="text-xs text-muted-foreground line-clamp-1">{ticket.deviceName}</span>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
<div className="text-xs text-muted-foreground whitespace-nowrap self-start sm:self-auto">
|
|
74
|
+
{formatDistanceToNow(new Date(ticket.reportedAt), { addSuffix: true })}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</Link>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</CardContent>
|
|
83
|
+
</Card>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|