@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,59 @@
|
|
|
1
|
+
import { auth } from '@/lib/auth';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { getNotifications, getUnreadCount } from '@/lib/actions/notifications';
|
|
4
|
+
import { NotificationPreferences } from '@/components/notifications/notification-preferences';
|
|
5
|
+
import { NotificationList } from './notification-list';
|
|
6
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
7
|
+
import { Bell, Sliders } from 'lucide-react';
|
|
8
|
+
|
|
9
|
+
export const dynamic = 'force-dynamic';
|
|
10
|
+
|
|
11
|
+
export default async function NotificationsPage() {
|
|
12
|
+
const session = await auth();
|
|
13
|
+
if (!session) redirect('/login');
|
|
14
|
+
|
|
15
|
+
const [initialNotifications, unreadCount] = await Promise.all([
|
|
16
|
+
getNotifications(50, 0),
|
|
17
|
+
getUnreadCount(),
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="space-y-6 max-w-4xl mx-auto">
|
|
22
|
+
<div className="flex items-center justify-between">
|
|
23
|
+
<div>
|
|
24
|
+
<h1 className="text-2xl font-bold tracking-tight">Notifications</h1>
|
|
25
|
+
<p className="text-sm text-muted-foreground">
|
|
26
|
+
Stay updated on your equipment, tickets, and maintenance tasks.
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<Tabs defaultValue="inbox" className="w-full">
|
|
32
|
+
<TabsList className="grid w-full grid-cols-2 max-w-xs">
|
|
33
|
+
<TabsTrigger value="inbox" className="flex items-center gap-2">
|
|
34
|
+
<Bell className="h-4 w-4" />
|
|
35
|
+
Inbox
|
|
36
|
+
{unreadCount > 0 && (
|
|
37
|
+
<span className="ml-1 rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary font-semibold">
|
|
38
|
+
{unreadCount}
|
|
39
|
+
</span>
|
|
40
|
+
)}
|
|
41
|
+
</TabsTrigger>
|
|
42
|
+
<TabsTrigger value="preferences" className="flex items-center gap-2">
|
|
43
|
+
<Sliders className="h-4 w-4" />
|
|
44
|
+
Preferences
|
|
45
|
+
</TabsTrigger>
|
|
46
|
+
</TabsList>
|
|
47
|
+
|
|
48
|
+
<TabsContent value="inbox" className="mt-6">
|
|
49
|
+
<NotificationList initialItems={initialNotifications} />
|
|
50
|
+
</TabsContent>
|
|
51
|
+
|
|
52
|
+
<TabsContent value="preferences" className="mt-6">
|
|
53
|
+
<NotificationPreferences />
|
|
54
|
+
</TabsContent>
|
|
55
|
+
</Tabs>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { auth } from '@/lib/auth';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { getCurrentUserProfile } from '@/lib/actions/users';
|
|
4
|
+
import { PageHeader } from '@/components/shared/page-header';
|
|
5
|
+
import { UserProfileView } from '@/components/profile/user-profile-view';
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
export default async function ProfilePage() {
|
|
10
|
+
const session = await auth();
|
|
11
|
+
if (!session?.user) redirect('/login');
|
|
12
|
+
|
|
13
|
+
const profileRes = await getCurrentUserProfile();
|
|
14
|
+
if (!profileRes.success || !profileRes.data) {
|
|
15
|
+
redirect('/dashboard');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="space-y-6 max-w-3xl mx-auto pb-12">
|
|
20
|
+
<PageHeader
|
|
21
|
+
title="My Profile & Clinical Details"
|
|
22
|
+
description="Your contact details, clinical speciality, and ward assignment"
|
|
23
|
+
/>
|
|
24
|
+
<UserProfileView profile={profileRes.data} />
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { auth } from '@/lib/auth';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { getCostReport } from '@/lib/actions/reports';
|
|
4
|
+
import { ReportTransparencyHeader } from '@/components/reports/transparency-header';
|
|
5
|
+
import { ReportSummaryCards } from '@/components/reports/summary-cards';
|
|
6
|
+
import { ReportExportButton } from '@/components/reports/export-button';
|
|
7
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
8
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
9
|
+
import { ArrowLeft, DollarSign, Wrench, Briefcase, Settings } from 'lucide-react';
|
|
10
|
+
import Link from 'next/link';
|
|
11
|
+
|
|
12
|
+
function formatCurrency(amount: number) {
|
|
13
|
+
return new Intl.NumberFormat('en-US', {
|
|
14
|
+
style: 'currency',
|
|
15
|
+
currency: 'USD',
|
|
16
|
+
}).format(amount || 0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default async function CostReportPage({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
|
|
20
|
+
const session = await auth();
|
|
21
|
+
if (!session) redirect('/login');
|
|
22
|
+
|
|
23
|
+
const resolvedParams = await searchParams;
|
|
24
|
+
|
|
25
|
+
const defaultStartDate = new Date();
|
|
26
|
+
defaultStartDate.setDate(defaultStartDate.getDate() - 90);
|
|
27
|
+
|
|
28
|
+
const filters = {
|
|
29
|
+
hospitalId: typeof resolvedParams.hospitalId === 'string' ? resolvedParams.hospitalId : undefined,
|
|
30
|
+
departmentId: typeof resolvedParams.departmentId === 'string' ? resolvedParams.departmentId : undefined,
|
|
31
|
+
startDate: typeof resolvedParams.startDate === 'string' ? resolvedParams.startDate : defaultStartDate.toISOString(),
|
|
32
|
+
endDate: typeof resolvedParams.endDate === 'string' ? resolvedParams.endDate : new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const data = await getCostReport(filters as any);
|
|
36
|
+
|
|
37
|
+
const getAmountByType = (type: string) => {
|
|
38
|
+
return data.byCostType.find(c => c.costType === type)?.totalAmount || 0;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const partsAmount = getAmountByType('parts');
|
|
42
|
+
const vendorAmount = getAmountByType('vendor_service');
|
|
43
|
+
const laborAmount = getAmountByType('labor');
|
|
44
|
+
|
|
45
|
+
const summaryItems = [
|
|
46
|
+
{ label: 'Total Cost', value: formatCurrency(data.totalCost), icon: DollarSign, color: 'text-primary' },
|
|
47
|
+
{ label: 'Parts', value: formatCurrency(partsAmount), icon: Settings },
|
|
48
|
+
{ label: 'Vendor Services', value: formatCurrency(vendorAmount), icon: Briefcase },
|
|
49
|
+
{ label: 'Labor', value: formatCurrency(laborAmount), icon: Wrench },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="space-y-6">
|
|
54
|
+
<div className="flex items-center gap-4">
|
|
55
|
+
<Link href="/reports" className="text-muted-foreground hover:text-foreground inline-flex items-center gap-2">
|
|
56
|
+
<ArrowLeft className="h-4 w-4" />
|
|
57
|
+
<span className="hidden sm:inline">Back to Reports</span>
|
|
58
|
+
</Link>
|
|
59
|
+
<h1 className="text-2xl font-bold tracking-tight">Maintenance Cost Report</h1>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<ReportTransparencyHeader
|
|
63
|
+
reportType="costs"
|
|
64
|
+
timezone="UTC"
|
|
65
|
+
dateRange={{ start: filters.startDate, end: filters.endDate }}
|
|
66
|
+
filters={filters as Record<string, string>}
|
|
67
|
+
generatedAt={new Date()}
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
<ReportSummaryCards items={summaryItems} />
|
|
71
|
+
|
|
72
|
+
{!data || data.byCostType.length === 0 ? (
|
|
73
|
+
<Card>
|
|
74
|
+
<CardContent className="p-6 text-center text-muted-foreground">
|
|
75
|
+
No cost data found matching the selected filters.
|
|
76
|
+
</CardContent>
|
|
77
|
+
</Card>
|
|
78
|
+
) : (
|
|
79
|
+
<>
|
|
80
|
+
<div className="grid gap-4 md:hidden">
|
|
81
|
+
{data.byCostType.map((item) => (
|
|
82
|
+
<Card key={item.costType}>
|
|
83
|
+
<CardContent className="p-4 flex justify-between items-center">
|
|
84
|
+
<div className="font-semibold capitalize">{item.costType?.replace('_', ' ') || 'Unknown'}</div>
|
|
85
|
+
<div className="font-medium">{formatCurrency(item.totalAmount)}</div>
|
|
86
|
+
</CardContent>
|
|
87
|
+
</Card>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="hidden md:block rounded-md border">
|
|
92
|
+
<Table>
|
|
93
|
+
<TableHeader>
|
|
94
|
+
<TableRow>
|
|
95
|
+
<TableHead>Cost Type</TableHead>
|
|
96
|
+
<TableHead className="text-right">Amount</TableHead>
|
|
97
|
+
<TableHead className="text-right">% of Total</TableHead>
|
|
98
|
+
</TableRow>
|
|
99
|
+
</TableHeader>
|
|
100
|
+
<TableBody>
|
|
101
|
+
{data.byCostType.map((item) => (
|
|
102
|
+
<TableRow key={item.costType}>
|
|
103
|
+
<TableCell className="font-medium capitalize">{item.costType?.replace('_', ' ') || 'Unknown'}</TableCell>
|
|
104
|
+
<TableCell className="text-right font-medium">{formatCurrency(item.totalAmount)}</TableCell>
|
|
105
|
+
<TableCell className="text-right">
|
|
106
|
+
{data.totalCost > 0 ? ((item.totalAmount / data.totalCost) * 100).toFixed(1) : 0}%
|
|
107
|
+
</TableCell>
|
|
108
|
+
</TableRow>
|
|
109
|
+
))}
|
|
110
|
+
<TableRow className="bg-muted/50 font-bold">
|
|
111
|
+
<TableCell>Total</TableCell>
|
|
112
|
+
<TableCell className="text-right">{formatCurrency(data.totalCost)}</TableCell>
|
|
113
|
+
<TableCell className="text-right">100%</TableCell>
|
|
114
|
+
</TableRow>
|
|
115
|
+
</TableBody>
|
|
116
|
+
</Table>
|
|
117
|
+
</div>
|
|
118
|
+
</>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
<div className="flex justify-end">
|
|
122
|
+
<ReportExportButton reportType="costs" filters={filters as Record<string, string>} />
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { auth } from '@/lib/auth';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { getReportHistory } from '@/lib/actions/report-history';
|
|
4
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
5
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
6
|
+
import { Badge } from '@/components/ui/badge';
|
|
7
|
+
import { ArrowLeft, Clock, FileText } from 'lucide-react';
|
|
8
|
+
import Link from 'next/link';
|
|
9
|
+
|
|
10
|
+
export default async function ReportHistoryPage() {
|
|
11
|
+
const session = await auth();
|
|
12
|
+
if (!session) redirect('/login');
|
|
13
|
+
|
|
14
|
+
const history = await getReportHistory();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="space-y-6">
|
|
18
|
+
<div className="flex items-center gap-4">
|
|
19
|
+
<Link href="/reports" className="text-muted-foreground hover:text-foreground inline-flex items-center gap-2">
|
|
20
|
+
<ArrowLeft className="h-4 w-4" />
|
|
21
|
+
<span className="hidden sm:inline">Back to Reports</span>
|
|
22
|
+
</Link>
|
|
23
|
+
<h1 className="text-2xl font-bold tracking-tight">Report History</h1>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
{!history || history.length === 0 ? (
|
|
27
|
+
<Card>
|
|
28
|
+
<CardContent className="p-12 text-center text-muted-foreground flex flex-col items-center justify-center">
|
|
29
|
+
<Clock className="h-12 w-12 mb-4 text-muted/50" />
|
|
30
|
+
<p className="text-lg font-medium mb-1 text-foreground">No reports have been generated yet.</p>
|
|
31
|
+
<p>Export a report to save it here.</p>
|
|
32
|
+
</CardContent>
|
|
33
|
+
</Card>
|
|
34
|
+
) : (
|
|
35
|
+
<>
|
|
36
|
+
<div className="grid gap-4 md:hidden">
|
|
37
|
+
{history.map((report) => (
|
|
38
|
+
<Card key={report.id}>
|
|
39
|
+
<CardContent className="p-4 space-y-3">
|
|
40
|
+
<div className="flex justify-between items-start">
|
|
41
|
+
<div>
|
|
42
|
+
<div className="font-semibold">{report.title}</div>
|
|
43
|
+
<div className="text-xs text-muted-foreground capitalize">{report.reportType.replace('_', ' ')}</div>
|
|
44
|
+
</div>
|
|
45
|
+
<Badge variant="outline">{report.fileFormat?.toUpperCase() || 'CSV'}</Badge>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="text-sm space-y-1">
|
|
49
|
+
{report.dateRangeStart && report.dateRangeEnd && (
|
|
50
|
+
<div className="text-muted-foreground">
|
|
51
|
+
Range: {new Date(report.dateRangeStart).toLocaleDateString()} - {new Date(report.dateRangeEnd).toLocaleDateString()}
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground pt-2 border-t mt-2">
|
|
55
|
+
<span>By: {report.generatedByUser?.fullName || 'Unknown'}</span>
|
|
56
|
+
<span>{new Date(report.generatedAt).toLocaleString()}</span>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</CardContent>
|
|
60
|
+
</Card>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="hidden md:block rounded-md border">
|
|
65
|
+
<Table>
|
|
66
|
+
<TableHeader>
|
|
67
|
+
<TableRow>
|
|
68
|
+
<TableHead>Report Details</TableHead>
|
|
69
|
+
<TableHead>Date Range</TableHead>
|
|
70
|
+
<TableHead>Generated By</TableHead>
|
|
71
|
+
<TableHead>Format</TableHead>
|
|
72
|
+
<TableHead className="text-right">Generated Date</TableHead>
|
|
73
|
+
</TableRow>
|
|
74
|
+
</TableHeader>
|
|
75
|
+
<TableBody>
|
|
76
|
+
{history.map((report) => (
|
|
77
|
+
<TableRow key={report.id}>
|
|
78
|
+
<TableCell>
|
|
79
|
+
<div className="font-medium">{report.title}</div>
|
|
80
|
+
<div className="text-xs text-muted-foreground capitalize">{report.reportType.replace('_', ' ')}</div>
|
|
81
|
+
</TableCell>
|
|
82
|
+
<TableCell className="text-muted-foreground text-sm">
|
|
83
|
+
{report.dateRangeStart && report.dateRangeEnd
|
|
84
|
+
? `${new Date(report.dateRangeStart).toLocaleDateString()} - ${new Date(report.dateRangeEnd).toLocaleDateString()}`
|
|
85
|
+
: 'N/A'
|
|
86
|
+
}
|
|
87
|
+
</TableCell>
|
|
88
|
+
<TableCell className="text-sm">{report.generatedByUser?.fullName || 'Unknown'}</TableCell>
|
|
89
|
+
<TableCell>
|
|
90
|
+
<Badge variant="outline">{report.fileFormat?.toUpperCase() || 'CSV'}</Badge>
|
|
91
|
+
</TableCell>
|
|
92
|
+
<TableCell className="text-right text-muted-foreground text-sm">
|
|
93
|
+
{new Date(report.generatedAt).toLocaleString()}
|
|
94
|
+
</TableCell>
|
|
95
|
+
</TableRow>
|
|
96
|
+
))}
|
|
97
|
+
</TableBody>
|
|
98
|
+
</Table>
|
|
99
|
+
</div>
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { auth } from '@/lib/auth';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { getInventoryReport } from '@/lib/actions/reports';
|
|
4
|
+
import { ReportTransparencyHeader } from '@/components/reports/transparency-header';
|
|
5
|
+
import { ReportSummaryCards } from '@/components/reports/summary-cards';
|
|
6
|
+
import { ReportExportButton } from '@/components/reports/export-button';
|
|
7
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
8
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
9
|
+
import { ArrowLeft, CheckCircle, Wrench, AlertTriangle, Monitor } from 'lucide-react';
|
|
10
|
+
import Link from 'next/link';
|
|
11
|
+
|
|
12
|
+
export default async function InventoryReportPage({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
|
|
13
|
+
const session = await auth();
|
|
14
|
+
if (!session) redirect('/login');
|
|
15
|
+
|
|
16
|
+
const resolvedParams = await searchParams;
|
|
17
|
+
|
|
18
|
+
const filters = {
|
|
19
|
+
groupBy: (typeof resolvedParams.groupBy === 'string' ? resolvedParams.groupBy : 'department') as any,
|
|
20
|
+
hospitalId: typeof resolvedParams.hospitalId === 'string' ? resolvedParams.hospitalId : undefined,
|
|
21
|
+
departmentId: typeof resolvedParams.departmentId === 'string' ? resolvedParams.departmentId : undefined,
|
|
22
|
+
deviceCategoryId: typeof resolvedParams.deviceCategoryId === 'string' ? resolvedParams.deviceCategoryId : undefined,
|
|
23
|
+
status: typeof resolvedParams.status === 'string' ? resolvedParams.status : undefined,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const data = await getInventoryReport(filters as any);
|
|
27
|
+
|
|
28
|
+
const totalDevices = data.reduce((acc, curr) => acc + curr.totalCount, 0);
|
|
29
|
+
const operationalDevices = data.reduce((acc, curr) => acc + curr.operationalCount, 0);
|
|
30
|
+
const maintenanceDevices = data.reduce((acc, curr) => acc + curr.maintenanceCount, 0);
|
|
31
|
+
const outOfServiceDevices = data.reduce((acc, curr) => acc + (curr.outOfServiceCount || 0), 0);
|
|
32
|
+
|
|
33
|
+
const summaryItems = [
|
|
34
|
+
{ label: 'Total Devices', value: totalDevices, icon: Monitor },
|
|
35
|
+
{ label: 'Operational', value: operationalDevices, icon: CheckCircle, color: 'text-green-600' },
|
|
36
|
+
{ label: 'Under Maintenance', value: maintenanceDevices, icon: Wrench, color: 'text-yellow-600' },
|
|
37
|
+
{ label: 'Out of Service', value: outOfServiceDevices, icon: AlertTriangle, color: 'text-red-600' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="space-y-6">
|
|
42
|
+
<div className="flex items-center gap-4">
|
|
43
|
+
<Link href="/reports" className="text-muted-foreground hover:text-foreground inline-flex items-center gap-2">
|
|
44
|
+
<ArrowLeft className="h-4 w-4" />
|
|
45
|
+
<span className="hidden sm:inline">Back to Reports</span>
|
|
46
|
+
</Link>
|
|
47
|
+
<h1 className="text-2xl font-bold tracking-tight">Inventory Report</h1>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<ReportTransparencyHeader
|
|
51
|
+
reportType="inventory"
|
|
52
|
+
timezone="UTC"
|
|
53
|
+
filters={filters as Record<string, string>}
|
|
54
|
+
generatedAt={new Date()}
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
<ReportSummaryCards items={summaryItems} />
|
|
58
|
+
|
|
59
|
+
{data.length === 0 ? (
|
|
60
|
+
<Card>
|
|
61
|
+
<CardContent className="p-6 text-center text-muted-foreground">
|
|
62
|
+
No inventory data found matching the selected filters.
|
|
63
|
+
</CardContent>
|
|
64
|
+
</Card>
|
|
65
|
+
) : (
|
|
66
|
+
<>
|
|
67
|
+
<div className="grid gap-4 md:hidden">
|
|
68
|
+
{data.map((item) => (
|
|
69
|
+
<Card key={String(item.groupId)}>
|
|
70
|
+
<CardContent className="p-4 space-y-2">
|
|
71
|
+
<div className="font-semibold">{item.groupId || 'Unknown Group'}</div>
|
|
72
|
+
<div className="text-sm grid grid-cols-2 gap-2">
|
|
73
|
+
<span className="text-green-600">Operational: {item.operationalCount}</span>
|
|
74
|
+
<span className="text-yellow-600">Maintenance: {item.maintenanceCount}</span>
|
|
75
|
+
<span className="text-red-600">Out of Service: {item.decommissionedCount}</span>
|
|
76
|
+
<span className="font-medium text-foreground">Total: {item.totalCount}</span>
|
|
77
|
+
</div>
|
|
78
|
+
</CardContent>
|
|
79
|
+
</Card>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div className="hidden md:block rounded-md border">
|
|
84
|
+
<Table>
|
|
85
|
+
<TableHeader>
|
|
86
|
+
<TableRow>
|
|
87
|
+
<TableHead className="w-[30%]">Group</TableHead>
|
|
88
|
+
<TableHead className="text-right">Operational</TableHead>
|
|
89
|
+
<TableHead className="text-right">Under Maintenance</TableHead>
|
|
90
|
+
<TableHead className="text-right">Out of Service</TableHead>
|
|
91
|
+
<TableHead className="text-right font-bold">Total</TableHead>
|
|
92
|
+
</TableRow>
|
|
93
|
+
</TableHeader>
|
|
94
|
+
<TableBody>
|
|
95
|
+
{data.map((item) => (
|
|
96
|
+
<TableRow key={String(item.groupId)}>
|
|
97
|
+
<TableCell className="font-medium">{item.groupId || 'Unknown Group'}</TableCell>
|
|
98
|
+
<TableCell className="text-right text-green-600">{item.operationalCount}</TableCell>
|
|
99
|
+
<TableCell className="text-right text-yellow-600">{item.maintenanceCount}</TableCell>
|
|
100
|
+
<TableCell className="text-right text-red-600">{item.decommissionedCount}</TableCell>
|
|
101
|
+
<TableCell className="text-right font-bold">{item.totalCount}</TableCell>
|
|
102
|
+
</TableRow>
|
|
103
|
+
))}
|
|
104
|
+
</TableBody>
|
|
105
|
+
</Table>
|
|
106
|
+
</div>
|
|
107
|
+
</>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
<div className="flex justify-end">
|
|
111
|
+
<ReportExportButton reportType="inventory" filters={filters as Record<string, string>} />
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { auth } from '@/lib/auth';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { getMaintenanceComplianceReport } from '@/lib/actions/reports';
|
|
4
|
+
import { ReportTransparencyHeader } from '@/components/reports/transparency-header';
|
|
5
|
+
import { ReportSummaryCards } from '@/components/reports/summary-cards';
|
|
6
|
+
import { ReportExportButton } from '@/components/reports/export-button';
|
|
7
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
8
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
9
|
+
import { ArrowLeft, Calendar, CheckCircle, AlertTriangle, Percent } from 'lucide-react';
|
|
10
|
+
import Link from 'next/link';
|
|
11
|
+
|
|
12
|
+
export default async function MaintenanceComplianceReportPage({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
|
|
13
|
+
const session = await auth();
|
|
14
|
+
if (!session) redirect('/login');
|
|
15
|
+
|
|
16
|
+
const resolvedParams = await searchParams;
|
|
17
|
+
|
|
18
|
+
const filters = {
|
|
19
|
+
hospitalId: typeof resolvedParams.hospitalId === 'string' ? resolvedParams.hospitalId : undefined,
|
|
20
|
+
departmentId: typeof resolvedParams.departmentId === 'string' ? resolvedParams.departmentId : undefined,
|
|
21
|
+
startDate: typeof resolvedParams.startDate === 'string' ? resolvedParams.startDate : undefined,
|
|
22
|
+
endDate: typeof resolvedParams.endDate === 'string' ? resolvedParams.endDate : undefined,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const data = await getMaintenanceComplianceReport(filters as any);
|
|
26
|
+
|
|
27
|
+
const summaryItems = [
|
|
28
|
+
{ label: 'Total Scheduled', value: data?.totalScheduled || 0, icon: Calendar },
|
|
29
|
+
{ label: 'Completed', value: data?.completed || 0, icon: CheckCircle, color: 'text-green-600' },
|
|
30
|
+
{ label: 'Overdue', value: data?.overdue || 0, icon: AlertTriangle, color: 'text-red-600' },
|
|
31
|
+
{ label: 'Compliance Rate', value: `${(data?.complianceRate || 0).toFixed(1)}%`, icon: Percent, color: 'text-primary' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="space-y-6">
|
|
36
|
+
<div className="flex items-center gap-4">
|
|
37
|
+
<Link href="/reports" className="text-muted-foreground hover:text-foreground inline-flex items-center gap-2">
|
|
38
|
+
<ArrowLeft className="h-4 w-4" />
|
|
39
|
+
<span className="hidden sm:inline">Back to Reports</span>
|
|
40
|
+
</Link>
|
|
41
|
+
<h1 className="text-2xl font-bold tracking-tight">PM Compliance Report</h1>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<ReportTransparencyHeader
|
|
45
|
+
reportType="maintenance"
|
|
46
|
+
timezone="UTC"
|
|
47
|
+
dateRange={{ start: filters.startDate, end: filters.endDate }}
|
|
48
|
+
filters={filters as Record<string, string>}
|
|
49
|
+
generatedAt={new Date()}
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<ReportSummaryCards items={summaryItems} />
|
|
53
|
+
|
|
54
|
+
{!data || data.totalScheduled === 0 ? (
|
|
55
|
+
<Card>
|
|
56
|
+
<CardContent className="p-6 text-center text-muted-foreground">
|
|
57
|
+
No maintenance tasks found matching the selected filters.
|
|
58
|
+
</CardContent>
|
|
59
|
+
</Card>
|
|
60
|
+
) : (
|
|
61
|
+
<>
|
|
62
|
+
<div className="my-6">
|
|
63
|
+
<h3 className="text-sm font-medium mb-2">Overall Compliance</h3>
|
|
64
|
+
<div className="w-full bg-secondary rounded-full h-4">
|
|
65
|
+
<div
|
|
66
|
+
className="bg-primary h-4 rounded-full transition-all"
|
|
67
|
+
style={{ width: `${Math.min(100, Math.max(0, data.complianceRate))}%` }}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
<p className="text-xs text-muted-foreground mt-2 text-right">
|
|
71
|
+
{data.complianceRate.toFixed(1)}% Completed
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div className="grid gap-4 md:hidden">
|
|
76
|
+
<Card>
|
|
77
|
+
<CardContent className="p-4 space-y-2">
|
|
78
|
+
<div className="font-semibold mb-2">Status Breakdown</div>
|
|
79
|
+
<div className="text-sm grid grid-cols-2 gap-2">
|
|
80
|
+
<span className="text-green-600 font-medium">Completed: {data.completed}</span>
|
|
81
|
+
<span className="text-red-600 font-medium">Overdue: {data.overdue}</span>
|
|
82
|
+
<span className="font-medium text-foreground col-span-2">Total Scheduled: {data.totalScheduled}</span>
|
|
83
|
+
</div>
|
|
84
|
+
</CardContent>
|
|
85
|
+
</Card>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="hidden md:block rounded-md border">
|
|
89
|
+
<Table>
|
|
90
|
+
<TableHeader>
|
|
91
|
+
<TableRow>
|
|
92
|
+
<TableHead>Status</TableHead>
|
|
93
|
+
<TableHead className="text-right">Count</TableHead>
|
|
94
|
+
<TableHead className="text-right">% of Total</TableHead>
|
|
95
|
+
</TableRow>
|
|
96
|
+
</TableHeader>
|
|
97
|
+
<TableBody>
|
|
98
|
+
<TableRow>
|
|
99
|
+
<TableCell className="font-medium text-green-600">Completed</TableCell>
|
|
100
|
+
<TableCell className="text-right">{data.completed}</TableCell>
|
|
101
|
+
<TableCell className="text-right">
|
|
102
|
+
{data.totalScheduled > 0 ? ((data.completed / data.totalScheduled) * 100).toFixed(1) : 0}%
|
|
103
|
+
</TableCell>
|
|
104
|
+
</TableRow>
|
|
105
|
+
<TableRow>
|
|
106
|
+
<TableCell className="font-medium text-red-600">Overdue</TableCell>
|
|
107
|
+
<TableCell className="text-right">{data.overdue}</TableCell>
|
|
108
|
+
<TableCell className="text-right">
|
|
109
|
+
{data.totalScheduled > 0 ? ((data.overdue / data.totalScheduled) * 100).toFixed(1) : 0}%
|
|
110
|
+
</TableCell>
|
|
111
|
+
</TableRow>
|
|
112
|
+
<TableRow>
|
|
113
|
+
<TableCell className="font-medium">Other</TableCell>
|
|
114
|
+
<TableCell className="text-right">{data.totalScheduled - data.completed - data.overdue}</TableCell>
|
|
115
|
+
<TableCell className="text-right">
|
|
116
|
+
{data.totalScheduled > 0 ? (((data.totalScheduled - data.completed - data.overdue) / data.totalScheduled) * 100).toFixed(1) : 0}%
|
|
117
|
+
</TableCell>
|
|
118
|
+
</TableRow>
|
|
119
|
+
<TableRow className="bg-muted/50 font-medium">
|
|
120
|
+
<TableCell>Total Scheduled</TableCell>
|
|
121
|
+
<TableCell className="text-right">{data.totalScheduled}</TableCell>
|
|
122
|
+
<TableCell className="text-right">100%</TableCell>
|
|
123
|
+
</TableRow>
|
|
124
|
+
</TableBody>
|
|
125
|
+
</Table>
|
|
126
|
+
</div>
|
|
127
|
+
</>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
<div className="flex justify-end">
|
|
131
|
+
<ReportExportButton reportType="maintenance" filters={filters as Record<string, string>} />
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|