@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,462 @@
|
|
|
1
|
+
import { auth } from '@/lib/auth';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { hasRole } from '@/lib/auth/rbac';
|
|
4
|
+
import {
|
|
5
|
+
getDashboardStats,
|
|
6
|
+
getMyTickets,
|
|
7
|
+
getDepartmentAlerts,
|
|
8
|
+
getTriageQueue,
|
|
9
|
+
getMyAssignedWork,
|
|
10
|
+
getMaintenanceDueSummary,
|
|
11
|
+
getAwaitingReview,
|
|
12
|
+
getDevicesOutOfService,
|
|
13
|
+
getAdminAlerts,
|
|
14
|
+
} from '@/lib/actions/dashboard';
|
|
15
|
+
import { getEngineersTeam } from '@/lib/actions/engineers';
|
|
16
|
+
import { StatCard } from '@/components/dashboard/stat-card';
|
|
17
|
+
import { ActionButtons } from '@/components/dashboard/action-buttons';
|
|
18
|
+
import { RoundWidget } from '@/components/dashboard/round-widget';
|
|
19
|
+
import { TicketListCompact } from '@/components/dashboard/ticket-list-compact';
|
|
20
|
+
import { MaintenanceDueList } from '@/components/dashboard/maintenance-due-list';
|
|
21
|
+
import { AvailabilityAlerts } from '@/components/dashboard/availability-alerts';
|
|
22
|
+
import { AdminAlerts } from '@/components/dashboard/admin-alerts';
|
|
23
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
24
|
+
import { Button } from '@/components/ui/button';
|
|
25
|
+
import Link from 'next/link';
|
|
26
|
+
import {
|
|
27
|
+
Monitor, TicketCheck, Wrench, AlertTriangle,
|
|
28
|
+
ShieldAlert, ClipboardCheck, Clock, UsersRound, MessageCircle,
|
|
29
|
+
} from 'lucide-react';
|
|
30
|
+
|
|
31
|
+
export default async function DashboardPage() {
|
|
32
|
+
const session = await auth();
|
|
33
|
+
if (!session?.user) redirect('/login');
|
|
34
|
+
|
|
35
|
+
const user = session.user as {
|
|
36
|
+
id: string;
|
|
37
|
+
fullName: string;
|
|
38
|
+
roles: string[];
|
|
39
|
+
organizationId: string | null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const roles = user.roles || [];
|
|
43
|
+
const firstName = user.fullName?.split(' ')[0] || 'there';
|
|
44
|
+
|
|
45
|
+
const isBME = hasRole(roles, 'BIOMED_ENG', 'BIOMED_TECH', 'BIOMED_MGR');
|
|
46
|
+
const isManager = hasRole(roles, 'BIOMED_MGR', 'HOSP_ADMIN', 'ORG_ADMIN');
|
|
47
|
+
const isAdmin = hasRole(roles, 'SYS_ADMIN', 'ORG_ADMIN');
|
|
48
|
+
const isStaff = hasRole(roles, 'STAFF', 'DEPT_MGR');
|
|
49
|
+
|
|
50
|
+
// Fetch data based on role
|
|
51
|
+
const [stats] = await Promise.all([
|
|
52
|
+
getDashboardStats(),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="space-y-6 max-w-5xl mx-auto">
|
|
57
|
+
{/* Greeting */}
|
|
58
|
+
<div>
|
|
59
|
+
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">
|
|
60
|
+
Welcome back, {firstName}
|
|
61
|
+
</h1>
|
|
62
|
+
<p className="text-muted-foreground mt-1 text-sm">
|
|
63
|
+
{roles.map(r => r.replace(/_/g, ' ')).join(' · ')}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{/* Staff Dashboard */}
|
|
68
|
+
{isStaff && <StaffDashboard stats={stats} userId={user.id} />}
|
|
69
|
+
|
|
70
|
+
{/* BME Dashboard */}
|
|
71
|
+
{isBME && !isManager && <BMEDashboard stats={stats} />}
|
|
72
|
+
|
|
73
|
+
{/* Manager Dashboard */}
|
|
74
|
+
{isManager && <ManagerDashboard stats={stats} />}
|
|
75
|
+
|
|
76
|
+
{/* Admin Dashboard */}
|
|
77
|
+
{isAdmin && <AdminDashboard stats={stats} />}
|
|
78
|
+
|
|
79
|
+
{/* Fallback if no specific role matched */}
|
|
80
|
+
{!isStaff && !isBME && !isManager && !isAdmin && (
|
|
81
|
+
<GenericDashboard stats={stats} />
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Staff Dashboard ─────────────────────────────────────────────────────
|
|
88
|
+
async function StaffDashboard({ stats, userId }: { stats: any; userId: string }) {
|
|
89
|
+
const [myTickets, alerts, teamRes] = await Promise.all([
|
|
90
|
+
getMyTickets(5),
|
|
91
|
+
getDepartmentAlerts(),
|
|
92
|
+
getEngineersTeam(),
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const engineers = teamRes.data || [];
|
|
96
|
+
const onDutyEngineers = engineers.filter((e: any) => e.dutyStatus === 'on_duty' || e.dutyStatus === 'on_call');
|
|
97
|
+
const responseNeededTickets = myTickets.filter((t: any) => t.statusCode === 'waiting_requester');
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
{/* Quick Actions - full width on mobile */}
|
|
102
|
+
<ActionButtons role="staff" />
|
|
103
|
+
|
|
104
|
+
{/* Action Required: Response Requested on Tickets */}
|
|
105
|
+
{responseNeededTickets.length > 0 && (
|
|
106
|
+
<Card className="border-amber-300 dark:border-amber-900 bg-amber-50/70 dark:bg-amber-950/20 shadow-sm overflow-hidden">
|
|
107
|
+
<CardContent className="p-4 flex items-start gap-3">
|
|
108
|
+
<MessageCircle className="w-5 h-5 text-amber-600 dark:text-amber-400 shrink-0 mt-0.5" />
|
|
109
|
+
<div className="flex-1 min-w-0">
|
|
110
|
+
<h4 className="font-semibold text-sm text-amber-900 dark:text-amber-200">
|
|
111
|
+
Action Required: Response Requested ({responseNeededTickets.length})
|
|
112
|
+
</h4>
|
|
113
|
+
<p className="text-xs text-amber-700 dark:text-amber-300 mt-0.5">
|
|
114
|
+
Biomedical Engineering needs additional information from you regarding reported issues.
|
|
115
|
+
</p>
|
|
116
|
+
<div className="mt-2 space-y-1">
|
|
117
|
+
{responseNeededTickets.map((t: any) => (
|
|
118
|
+
<Link
|
|
119
|
+
key={t.id}
|
|
120
|
+
href={`/tickets/${t.id}`}
|
|
121
|
+
className="block p-2 rounded-lg bg-card/80 border border-amber-200 dark:border-amber-800 text-xs hover:border-amber-400 transition"
|
|
122
|
+
>
|
|
123
|
+
<span className="font-mono font-bold">{t.ticketNumber}</span>: {t.title} → Tap to reply
|
|
124
|
+
</Link>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</CardContent>
|
|
129
|
+
</Card>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{/* Department Availability Alerts */}
|
|
133
|
+
{alerts.length > 0 && (
|
|
134
|
+
<AvailabilityAlerts devices={alerts} />
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{/* Biomedical Engineering Coverage / On-Duty Team Widget */}
|
|
138
|
+
<Card className="border shadow-sm">
|
|
139
|
+
<CardContent className="p-4">
|
|
140
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
141
|
+
<div className="flex items-center gap-3">
|
|
142
|
+
<div className="p-2.5 rounded-lg bg-purple-50 dark:bg-purple-950 text-purple-700 dark:text-purple-300">
|
|
143
|
+
<UsersRound className="w-5 h-5" />
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<h3 className="font-semibold text-sm">Biomedical Engineering Support</h3>
|
|
147
|
+
<p className="text-xs text-muted-foreground">
|
|
148
|
+
{onDutyEngineers.length > 0
|
|
149
|
+
? `${onDutyEngineers.length} engineer(s) available in hospital today`
|
|
150
|
+
: 'Biomedical support on duty & on call'}
|
|
151
|
+
</p>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
<Button asChild variant="outline" size="sm" className="self-start sm:self-auto text-xs">
|
|
155
|
+
<Link href="/engineering-team">View Team & Schedules →</Link>
|
|
156
|
+
</Button>
|
|
157
|
+
</div>
|
|
158
|
+
{onDutyEngineers.length > 0 && (
|
|
159
|
+
<div className="mt-3 pt-3 border-t grid sm:grid-cols-2 md:grid-cols-3 gap-2">
|
|
160
|
+
{onDutyEngineers.slice(0, 3).map((eng: any) => (
|
|
161
|
+
<div key={eng.id} className="flex items-center gap-2 p-2 rounded-lg bg-muted/40 text-xs">
|
|
162
|
+
<span className="w-2 h-2 rounded-full bg-green-500 shrink-0" />
|
|
163
|
+
<span className="font-medium truncate">{eng.fullName}</span>
|
|
164
|
+
{eng.extension ? (
|
|
165
|
+
<span className="text-muted-foreground ml-auto shrink-0 font-mono">Ext. {eng.extension}</span>
|
|
166
|
+
) : eng.mobileNumber ? (
|
|
167
|
+
<span className="text-muted-foreground ml-auto shrink-0">{eng.mobileNumber}</span>
|
|
168
|
+
) : null}
|
|
169
|
+
</div>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</CardContent>
|
|
174
|
+
</Card>
|
|
175
|
+
|
|
176
|
+
{/* My Tickets */}
|
|
177
|
+
<TicketListCompact
|
|
178
|
+
tickets={myTickets}
|
|
179
|
+
title="My Recent Tickets"
|
|
180
|
+
emptyMessage="No tickets need your attention."
|
|
181
|
+
/>
|
|
182
|
+
|
|
183
|
+
{/* Stats row */}
|
|
184
|
+
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
|
185
|
+
<StatCard
|
|
186
|
+
title="Open Tickets"
|
|
187
|
+
value={stats?.openTickets || 0}
|
|
188
|
+
icon={<TicketCheck className="h-4 w-4 text-orange-500" />}
|
|
189
|
+
href="/tickets"
|
|
190
|
+
/>
|
|
191
|
+
<StatCard
|
|
192
|
+
title="Devices Available"
|
|
193
|
+
value={stats?.activeDevices || 0}
|
|
194
|
+
icon={<Monitor className="h-4 w-4 text-green-500" />}
|
|
195
|
+
href="/my-department"
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
</>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── BME Dashboard ────────────────────────────────────────────────────────
|
|
203
|
+
async function BMEDashboard({ stats }: { stats: any }) {
|
|
204
|
+
const [triage, assigned, maintenanceDue, review, outOfService] =
|
|
205
|
+
await Promise.all([
|
|
206
|
+
getTriageQueue(5),
|
|
207
|
+
getMyAssignedWork(5),
|
|
208
|
+
getMaintenanceDueSummary(),
|
|
209
|
+
getAwaitingReview(5),
|
|
210
|
+
getDevicesOutOfService(5),
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<>
|
|
215
|
+
{/* Critical alert cards */}
|
|
216
|
+
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
|
217
|
+
{(stats?.criticalTickets || 0) > 0 && (
|
|
218
|
+
<StatCard
|
|
219
|
+
title="Critical Tickets"
|
|
220
|
+
value={stats.criticalTickets}
|
|
221
|
+
icon={<ShieldAlert className="h-4 w-4 text-red-500" />}
|
|
222
|
+
href="/tickets?priority=p1_critical"
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
<StatCard
|
|
226
|
+
title="Untriaged"
|
|
227
|
+
value={triage.length}
|
|
228
|
+
icon={<AlertTriangle className="h-4 w-4 text-amber-500" />}
|
|
229
|
+
href="/tickets/triage"
|
|
230
|
+
/>
|
|
231
|
+
<StatCard
|
|
232
|
+
title="Due / Overdue"
|
|
233
|
+
value={`${maintenanceDue.due_today + maintenanceDue.overdue}`}
|
|
234
|
+
icon={<Clock className={`h-4 w-4 ${maintenanceDue.overdue > 0 ? 'text-red-500' : 'text-yellow-500'}`} />}
|
|
235
|
+
href="/maintenance/tasks?due=today"
|
|
236
|
+
trend={maintenanceDue.overdue > 0 ? `${maintenanceDue.overdue} overdue` : undefined}
|
|
237
|
+
/>
|
|
238
|
+
<StatCard
|
|
239
|
+
title="Out of Service"
|
|
240
|
+
value={stats?.outOfService || 0}
|
|
241
|
+
icon={<AlertTriangle className="h-4 w-4 text-red-500" />}
|
|
242
|
+
href="/devices?status=out_of_service"
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{/* Quick Actions */}
|
|
247
|
+
<ActionButtons role="bme" triageCount={triage.length} />
|
|
248
|
+
|
|
249
|
+
{/* Daily Round Widget */}
|
|
250
|
+
<RoundWidget />
|
|
251
|
+
|
|
252
|
+
{/* Assigned Work */}
|
|
253
|
+
<TicketListCompact
|
|
254
|
+
tickets={assigned
|
|
255
|
+
.filter((a) => a.type === 'ticket')
|
|
256
|
+
.map((a) => ({
|
|
257
|
+
id: a.id,
|
|
258
|
+
ticketNumber: a.number,
|
|
259
|
+
title: a.title,
|
|
260
|
+
statusCode: a.statusCode,
|
|
261
|
+
priorityCode: a.priorityCode,
|
|
262
|
+
reportedAt: a.date || new Date(),
|
|
263
|
+
deviceName: a.deviceName,
|
|
264
|
+
}))}
|
|
265
|
+
title="My Assigned Tickets"
|
|
266
|
+
emptyMessage="No assigned tickets."
|
|
267
|
+
/>
|
|
268
|
+
|
|
269
|
+
{/* Maintenance Due */}
|
|
270
|
+
<MaintenanceDueList
|
|
271
|
+
tasks={assigned
|
|
272
|
+
.filter((a) => a.type === 'task')
|
|
273
|
+
.map((a) => ({
|
|
274
|
+
id: a.id,
|
|
275
|
+
taskNumber: a.number,
|
|
276
|
+
title: a.title,
|
|
277
|
+
statusCode: a.statusCode,
|
|
278
|
+
dueDate: a.date || null,
|
|
279
|
+
deviceName: a.deviceName,
|
|
280
|
+
maintenanceType: 'preventive_maintenance',
|
|
281
|
+
}))}
|
|
282
|
+
title="My Maintenance Tasks"
|
|
283
|
+
/>
|
|
284
|
+
|
|
285
|
+
{/* Awaiting Review */}
|
|
286
|
+
{review.length > 0 && (
|
|
287
|
+
<MaintenanceDueList
|
|
288
|
+
tasks={review}
|
|
289
|
+
title={`Awaiting Review (${review.length})`}
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
{/* Out of Service Devices */}
|
|
294
|
+
{outOfService.length > 0 && (
|
|
295
|
+
<AvailabilityAlerts devices={outOfService} />
|
|
296
|
+
)}
|
|
297
|
+
</>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Manager Dashboard ────────────────────────────────────────────────────
|
|
302
|
+
async function ManagerDashboard({ stats }: { stats: any }) {
|
|
303
|
+
const [triage, review, maintenanceDue, outOfService] = await Promise.all([
|
|
304
|
+
getTriageQueue(5),
|
|
305
|
+
getAwaitingReview(5),
|
|
306
|
+
getMaintenanceDueSummary(),
|
|
307
|
+
getDevicesOutOfService(10),
|
|
308
|
+
]);
|
|
309
|
+
|
|
310
|
+
const complianceRate = stats?.pmComplianceRate || 100;
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<>
|
|
314
|
+
{/* Exception-first metric cards */}
|
|
315
|
+
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
|
316
|
+
<StatCard
|
|
317
|
+
title="Critical Tickets"
|
|
318
|
+
value={stats?.criticalTickets || 0}
|
|
319
|
+
icon={<ShieldAlert className="h-4 w-4 text-red-500" />}
|
|
320
|
+
href="/tickets?priority=p1_critical"
|
|
321
|
+
/>
|
|
322
|
+
<StatCard
|
|
323
|
+
title="Overdue Maintenance"
|
|
324
|
+
value={maintenanceDue.overdue}
|
|
325
|
+
icon={<Clock className={`h-4 w-4 ${maintenanceDue.overdue > 0 ? 'text-red-500' : 'text-green-500'}`} />}
|
|
326
|
+
href="/maintenance/tasks?due=overdue"
|
|
327
|
+
/>
|
|
328
|
+
<StatCard
|
|
329
|
+
title="Out of Service"
|
|
330
|
+
value={stats?.outOfService || 0}
|
|
331
|
+
icon={<AlertTriangle className="h-4 w-4 text-red-500" />}
|
|
332
|
+
href="/devices?status=out_of_service"
|
|
333
|
+
/>
|
|
334
|
+
<StatCard
|
|
335
|
+
title="PM Compliance"
|
|
336
|
+
value={`${complianceRate}%`}
|
|
337
|
+
icon={<ClipboardCheck className={`h-4 w-4 ${complianceRate >= 90 ? 'text-green-500' : 'text-amber-500'}`} />}
|
|
338
|
+
href="/reports/maintenance"
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
{/* Overall stats */}
|
|
343
|
+
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
|
344
|
+
<StatCard
|
|
345
|
+
title="Total Devices"
|
|
346
|
+
value={stats?.totalDevices || 0}
|
|
347
|
+
icon={<Monitor className="h-4 w-4 text-blue-500" />}
|
|
348
|
+
href="/devices"
|
|
349
|
+
/>
|
|
350
|
+
<StatCard
|
|
351
|
+
title="Open Tickets"
|
|
352
|
+
value={stats?.openTickets || 0}
|
|
353
|
+
icon={<TicketCheck className="h-4 w-4 text-orange-500" />}
|
|
354
|
+
href="/tickets?status=open"
|
|
355
|
+
/>
|
|
356
|
+
<StatCard
|
|
357
|
+
title="Untriaged"
|
|
358
|
+
value={triage.length}
|
|
359
|
+
icon={<AlertTriangle className="h-4 w-4 text-amber-500" />}
|
|
360
|
+
href="/tickets/triage"
|
|
361
|
+
/>
|
|
362
|
+
<StatCard
|
|
363
|
+
title="Awaiting Review"
|
|
364
|
+
value={review.length}
|
|
365
|
+
icon={<Wrench className="h-4 w-4 text-purple-500" />}
|
|
366
|
+
href="/maintenance/tasks?status=awaiting_review"
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
{/* Daily Round Widget */}
|
|
371
|
+
<RoundWidget />
|
|
372
|
+
|
|
373
|
+
{/* Triage Queue */}
|
|
374
|
+
{triage.length > 0 && (
|
|
375
|
+
<TicketListCompact
|
|
376
|
+
tickets={triage}
|
|
377
|
+
title="Triage Queue"
|
|
378
|
+
emptyMessage="All tickets have been triaged."
|
|
379
|
+
/>
|
|
380
|
+
)}
|
|
381
|
+
|
|
382
|
+
{/* Awaiting Review */}
|
|
383
|
+
{review.length > 0 && (
|
|
384
|
+
<MaintenanceDueList
|
|
385
|
+
tasks={review}
|
|
386
|
+
title="Awaiting Review / Release"
|
|
387
|
+
/>
|
|
388
|
+
)}
|
|
389
|
+
|
|
390
|
+
{/* Out of Service */}
|
|
391
|
+
{outOfService.length > 0 && (
|
|
392
|
+
<AvailabilityAlerts devices={outOfService} />
|
|
393
|
+
)}
|
|
394
|
+
</>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ── Admin Dashboard ──────────────────────────────────────────────────────
|
|
399
|
+
async function AdminDashboard({ stats }: { stats: any }) {
|
|
400
|
+
const adminAlerts = await getAdminAlerts();
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<>
|
|
404
|
+
{/* Admin Alerts */}
|
|
405
|
+
<AdminAlerts alerts={adminAlerts} />
|
|
406
|
+
|
|
407
|
+
{/* System Overview */}
|
|
408
|
+
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
|
409
|
+
<StatCard
|
|
410
|
+
title="Total Devices"
|
|
411
|
+
value={stats?.totalDevices || 0}
|
|
412
|
+
icon={<Monitor className="h-4 w-4 text-blue-500" />}
|
|
413
|
+
href="/devices"
|
|
414
|
+
/>
|
|
415
|
+
<StatCard
|
|
416
|
+
title="Open Tickets"
|
|
417
|
+
value={stats?.openTickets || 0}
|
|
418
|
+
icon={<TicketCheck className="h-4 w-4 text-orange-500" />}
|
|
419
|
+
href="/tickets"
|
|
420
|
+
/>
|
|
421
|
+
<StatCard
|
|
422
|
+
title="Out of Service"
|
|
423
|
+
value={stats?.outOfService || 0}
|
|
424
|
+
icon={<AlertTriangle className="h-4 w-4 text-red-500" />}
|
|
425
|
+
href="/devices?status=out_of_service"
|
|
426
|
+
/>
|
|
427
|
+
<StatCard
|
|
428
|
+
title="PM Compliance"
|
|
429
|
+
value={`${stats?.pmComplianceRate || 100}%`}
|
|
430
|
+
icon={<ClipboardCheck className="h-4 w-4 text-green-500" />}
|
|
431
|
+
href="/reports/maintenance"
|
|
432
|
+
/>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
{/* Quick Actions */}
|
|
436
|
+
<ActionButtons role="admin" />
|
|
437
|
+
</>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ── Generic Fallback ─────────────────────────────────────────────────────
|
|
442
|
+
function GenericDashboard({ stats }: { stats: any }) {
|
|
443
|
+
return (
|
|
444
|
+
<>
|
|
445
|
+
<ActionButtons role="staff" />
|
|
446
|
+
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
|
447
|
+
<StatCard
|
|
448
|
+
title="Total Devices"
|
|
449
|
+
value={stats?.totalDevices || 0}
|
|
450
|
+
icon={<Monitor className="h-4 w-4 text-blue-500" />}
|
|
451
|
+
href="/devices"
|
|
452
|
+
/>
|
|
453
|
+
<StatCard
|
|
454
|
+
title="Open Tickets"
|
|
455
|
+
value={stats?.openTickets || 0}
|
|
456
|
+
icon={<TicketCheck className="h-4 w-4 text-orange-500" />}
|
|
457
|
+
href="/tickets"
|
|
458
|
+
/>
|
|
459
|
+
</div>
|
|
460
|
+
</>
|
|
461
|
+
);
|
|
462
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { auth } from '@/lib/auth';
|
|
2
|
+
import { redirect, notFound } from 'next/navigation';
|
|
3
|
+
import { getDepartmentDetails } from '@/lib/actions/departments';
|
|
4
|
+
import { DepartmentDetailView } from '@/components/departments/department-detail-view';
|
|
5
|
+
import { hasRole } from '@/lib/auth/rbac';
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
export default async function DepartmentPage({
|
|
10
|
+
params,
|
|
11
|
+
}: {
|
|
12
|
+
params: Promise<{ deptId: string }>;
|
|
13
|
+
}) {
|
|
14
|
+
const session = await auth();
|
|
15
|
+
if (!session?.user) redirect('/login');
|
|
16
|
+
|
|
17
|
+
const { deptId } = await params;
|
|
18
|
+
const result = await getDepartmentDetails(deptId);
|
|
19
|
+
|
|
20
|
+
if (!result.success || !result.data) {
|
|
21
|
+
notFound();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const user = session.user as { roles?: string[] };
|
|
25
|
+
const isAdmin = hasRole(user.roles || [], 'SYS_ADMIN', 'ORG_ADMIN', 'HOSP_ADMIN');
|
|
26
|
+
|
|
27
|
+
const { department, devices, tickets, locations, stats } = result.data;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<DepartmentDetailView
|
|
31
|
+
department={department}
|
|
32
|
+
devices={devices}
|
|
33
|
+
tickets={tickets}
|
|
34
|
+
locations={locations}
|
|
35
|
+
stats={stats}
|
|
36
|
+
isAdmin={isAdmin}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|