@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,382 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { createDevice } from "@/lib/actions/devices"
|
|
4
|
+
import { getDeviceCategories } from "@/lib/actions/device-categories"
|
|
5
|
+
import { getManufacturers } from "@/lib/actions/manufacturers"
|
|
6
|
+
import { getHospitals } from "@/lib/actions/hospitals"
|
|
7
|
+
import { getDepartments } from "@/lib/actions/departments"
|
|
8
|
+
import { PageHeader } from "@/components/shared/page-header"
|
|
9
|
+
import { Button } from "@/components/ui/button"
|
|
10
|
+
import { Input } from "@/components/ui/input"
|
|
11
|
+
import { Label } from "@/components/ui/label"
|
|
12
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
13
|
+
import { useRouter } from "next/navigation"
|
|
14
|
+
import { useState, useEffect } from "react"
|
|
15
|
+
import { QuickAddCategoryDialog } from "@/components/devices/quick-add-category-dialog"
|
|
16
|
+
import { QuickAddManufacturerDialog } from "@/components/devices/quick-add-manufacturer-dialog"
|
|
17
|
+
import { toast } from "sonner"
|
|
18
|
+
import { Loader2, ChevronDown, ChevronUp, Sparkles } from "lucide-react"
|
|
19
|
+
|
|
20
|
+
export default function NewDevicePage() {
|
|
21
|
+
const router = useRouter()
|
|
22
|
+
const [loading, setLoading] = useState(false)
|
|
23
|
+
const [error, setError] = useState("")
|
|
24
|
+
const [deviceName, setDeviceName] = useState("")
|
|
25
|
+
const [categories, setCategories] = useState<any[]>([])
|
|
26
|
+
const [selectedCategoryId, setSelectedCategoryId] = useState("")
|
|
27
|
+
const [manufacturers, setManufacturers] = useState<any[]>([])
|
|
28
|
+
const [selectedManufacturerId, setSelectedManufacturerId] = useState("")
|
|
29
|
+
const [hospitals, setHospitals] = useState<any[]>([])
|
|
30
|
+
const [selectedHospitalId, setSelectedHospitalId] = useState("")
|
|
31
|
+
const [departments, setDepartments] = useState<any[]>([])
|
|
32
|
+
const [selectedDepartmentId, setSelectedDepartmentId] = useState("")
|
|
33
|
+
const [organizationId, setOrganizationId] = useState("")
|
|
34
|
+
const [loadingDepts, setLoadingDepts] = useState(false)
|
|
35
|
+
const [showAdvanced, setShowAdvanced] = useState(false)
|
|
36
|
+
|
|
37
|
+
// Track auto-detected category name for helpful feedback
|
|
38
|
+
const [detectedCategoryName, setDetectedCategoryName] = useState("")
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
async function loadData() {
|
|
42
|
+
try {
|
|
43
|
+
const [catRes, mfrRes, hospRes] = await Promise.all([
|
|
44
|
+
getDeviceCategories(),
|
|
45
|
+
getManufacturers(),
|
|
46
|
+
getHospitals(),
|
|
47
|
+
])
|
|
48
|
+
if (catRes?.success && catRes.data) setCategories(catRes.data as any[])
|
|
49
|
+
if (mfrRes?.success && mfrRes.data) setManufacturers(mfrRes.data as any[])
|
|
50
|
+
if (hospRes?.success && hospRes.data && Array.isArray(hospRes.data)) {
|
|
51
|
+
const hospList = hospRes.data as any[]
|
|
52
|
+
setHospitals(hospList)
|
|
53
|
+
if (hospList.length > 0) {
|
|
54
|
+
const firstHosp = hospList[0]
|
|
55
|
+
setSelectedHospitalId(firstHosp.id)
|
|
56
|
+
setOrganizationId(firstHosp.organizationId)
|
|
57
|
+
setLoadingDepts(true)
|
|
58
|
+
const deptRes = await getDepartments({ hospitalId: firstHosp.id })
|
|
59
|
+
if (deptRes?.success && deptRes.data) setDepartments(deptRes.data as any[])
|
|
60
|
+
setLoadingDepts(false)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error("Failed to load initial data:", err)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
loadData()
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
const handleHospitalChange = async (hospitalId: string) => {
|
|
71
|
+
setSelectedHospitalId(hospitalId)
|
|
72
|
+
setSelectedDepartmentId("")
|
|
73
|
+
if (!hospitalId) { setDepartments([]); setOrganizationId(""); return }
|
|
74
|
+
const hosp = hospitals.find((h: any) => h.id === hospitalId)
|
|
75
|
+
if (hosp) setOrganizationId(hosp.organizationId)
|
|
76
|
+
setLoadingDepts(true)
|
|
77
|
+
try {
|
|
78
|
+
const deptRes = await getDepartments({ hospitalId })
|
|
79
|
+
if (deptRes?.success && deptRes.data) setDepartments(deptRes.data as any[])
|
|
80
|
+
else setDepartments([])
|
|
81
|
+
} catch { setDepartments([]) }
|
|
82
|
+
finally { setLoadingDepts(false) }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Smart auto-detection when user types device name
|
|
86
|
+
const handleNameChange = (val: string) => {
|
|
87
|
+
setDeviceName(val)
|
|
88
|
+
if (!val.trim()) {
|
|
89
|
+
setDetectedCategoryName("")
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const lower = val.toLowerCase()
|
|
94
|
+
const matched = categories.find((c: any) => {
|
|
95
|
+
const catLower = c.name.toLowerCase()
|
|
96
|
+
const codeLower = (c.code || "").toLowerCase()
|
|
97
|
+
return lower.includes(catLower) || catLower.includes(lower) || lower.includes(codeLower)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (matched) {
|
|
101
|
+
setSelectedCategoryId(matched.id)
|
|
102
|
+
setDetectedCategoryName(matched.name)
|
|
103
|
+
} else {
|
|
104
|
+
setDetectedCategoryName("")
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
109
|
+
e.preventDefault()
|
|
110
|
+
setLoading(true)
|
|
111
|
+
setError("")
|
|
112
|
+
|
|
113
|
+
if (!organizationId) { setError("Please select a hospital first."); setLoading(false); return }
|
|
114
|
+
if (!selectedDepartmentId) { setError("Please select a department."); setLoading(false); return }
|
|
115
|
+
|
|
116
|
+
const fd = new FormData(e.currentTarget)
|
|
117
|
+
const data: any = {}
|
|
118
|
+
fd.forEach((v, k) => { if (v) data[k] = v })
|
|
119
|
+
|
|
120
|
+
data.name = deviceName
|
|
121
|
+
data.organizationId = organizationId
|
|
122
|
+
data.hospitalId = selectedHospitalId
|
|
123
|
+
data.departmentId = selectedDepartmentId
|
|
124
|
+
if (selectedCategoryId) data.deviceCategoryId = selectedCategoryId
|
|
125
|
+
if (selectedManufacturerId) data.manufacturerId = selectedManufacturerId
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const result = await createDevice(data)
|
|
129
|
+
if (result?.success) {
|
|
130
|
+
toast.success("Device registered successfully!")
|
|
131
|
+
router.push(`/devices/${(result as any).data.id}`)
|
|
132
|
+
} else {
|
|
133
|
+
const errMsg = (result as any)?.error || "Failed to create device"
|
|
134
|
+
setError(errMsg)
|
|
135
|
+
toast.error(errMsg)
|
|
136
|
+
}
|
|
137
|
+
} catch (err: any) {
|
|
138
|
+
const errMsg = err.message || "An unexpected error occurred"
|
|
139
|
+
setError(errMsg)
|
|
140
|
+
toast.error(errMsg)
|
|
141
|
+
} finally {
|
|
142
|
+
setLoading(false)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div className="space-y-6 max-w-5xl">
|
|
148
|
+
<PageHeader
|
|
149
|
+
title="Add New Device"
|
|
150
|
+
description="Register a new medical device in the hospital inventory"
|
|
151
|
+
/>
|
|
152
|
+
|
|
153
|
+
{error && <div className="bg-destructive/10 text-destructive p-3 rounded-md text-sm">{error}</div>}
|
|
154
|
+
|
|
155
|
+
<form onSubmit={handleSubmit}>
|
|
156
|
+
<input type="hidden" name="organizationId" value={organizationId} />
|
|
157
|
+
|
|
158
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
159
|
+
{/* ── Card 1: Device Info ─────────────────────────────────── */}
|
|
160
|
+
<Card>
|
|
161
|
+
<CardHeader><CardTitle>Device Information</CardTitle></CardHeader>
|
|
162
|
+
<CardContent className="space-y-4">
|
|
163
|
+
<div>
|
|
164
|
+
<Label htmlFor="name">Device Name *</Label>
|
|
165
|
+
<Input
|
|
166
|
+
id="name"
|
|
167
|
+
name="name"
|
|
168
|
+
value={deviceName}
|
|
169
|
+
onChange={(e) => handleNameChange(e.target.value)}
|
|
170
|
+
placeholder="e.g. Infusion Pump 1, Defibrillator A..."
|
|
171
|
+
required
|
|
172
|
+
/>
|
|
173
|
+
{detectedCategoryName && (
|
|
174
|
+
<p className="flex items-center gap-1.5 text-xs text-emerald-600 dark:text-emerald-400 mt-1.5 font-medium">
|
|
175
|
+
<Sparkles className="w-3.5 h-3.5" />
|
|
176
|
+
Auto-detected type: {detectedCategoryName}
|
|
177
|
+
</p>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
<div>
|
|
181
|
+
<Label htmlFor="assetNumber">Asset Number *</Label>
|
|
182
|
+
<Input id="assetNumber" name="assetNumber" placeholder="e.g. AST-2026-001" required />
|
|
183
|
+
</div>
|
|
184
|
+
<div>
|
|
185
|
+
<Label htmlFor="serialNumber">Serial Number (Optional)</Label>
|
|
186
|
+
<Input id="serialNumber" name="serialNumber" placeholder="Manufacturer serial #" />
|
|
187
|
+
</div>
|
|
188
|
+
</CardContent>
|
|
189
|
+
</Card>
|
|
190
|
+
|
|
191
|
+
{/* ── Card 2: Location ───────────────────────────────────── */}
|
|
192
|
+
<Card>
|
|
193
|
+
<CardHeader><CardTitle>Location</CardTitle></CardHeader>
|
|
194
|
+
<CardContent className="space-y-4">
|
|
195
|
+
<div>
|
|
196
|
+
<Label htmlFor="hospitalId">Hospital *</Label>
|
|
197
|
+
<select
|
|
198
|
+
id="hospitalId" name="hospitalId" required
|
|
199
|
+
value={selectedHospitalId}
|
|
200
|
+
onChange={(e) => handleHospitalChange(e.target.value)}
|
|
201
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
202
|
+
>
|
|
203
|
+
<option value="">Select hospital...</option>
|
|
204
|
+
{hospitals.map((h: any) => (
|
|
205
|
+
<option key={h.id} value={h.id}>{h.name}</option>
|
|
206
|
+
))}
|
|
207
|
+
</select>
|
|
208
|
+
</div>
|
|
209
|
+
<div>
|
|
210
|
+
<div className="flex items-center justify-between mb-1">
|
|
211
|
+
<Label htmlFor="departmentId">Department *</Label>
|
|
212
|
+
{loadingDepts && (
|
|
213
|
+
<span className="flex items-center text-xs text-muted-foreground gap-1">
|
|
214
|
+
<Loader2 className="w-3 h-3 animate-spin" /> Loading...
|
|
215
|
+
</span>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
<select
|
|
219
|
+
id="departmentId" name="departmentId" required
|
|
220
|
+
value={selectedDepartmentId}
|
|
221
|
+
onChange={(e) => setSelectedDepartmentId(e.target.value)}
|
|
222
|
+
disabled={!selectedHospitalId || loadingDepts}
|
|
223
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-50"
|
|
224
|
+
>
|
|
225
|
+
<option value="">
|
|
226
|
+
{loadingDepts ? "Loading..." : !selectedHospitalId
|
|
227
|
+
? "Select a hospital first..." : departments.length === 0
|
|
228
|
+
? "No departments found" : "Select department..."}
|
|
229
|
+
</option>
|
|
230
|
+
{departments.map((d: any) => (
|
|
231
|
+
<option key={d.id} value={d.id}>{d.name}</option>
|
|
232
|
+
))}
|
|
233
|
+
</select>
|
|
234
|
+
</div>
|
|
235
|
+
<div>
|
|
236
|
+
<Label htmlFor="exactLocationDescription">Exact Location (Optional)</Label>
|
|
237
|
+
<Input id="exactLocationDescription" name="exactLocationDescription"
|
|
238
|
+
placeholder="e.g. Room 302, Bay 4" />
|
|
239
|
+
</div>
|
|
240
|
+
</CardContent>
|
|
241
|
+
</Card>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{/* ── Optional Advanced Details Toggle ─────────────────────── */}
|
|
245
|
+
<div className="mt-6">
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
onClick={() => setShowAdvanced(!showAdvanced)}
|
|
249
|
+
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors py-1 font-medium"
|
|
250
|
+
>
|
|
251
|
+
{showAdvanced ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
|
252
|
+
{showAdvanced ? "Hide" : "Show"} Advanced Details
|
|
253
|
+
<span className="text-xs text-muted-foreground/75 font-normal">(device type override, manufacturer, warranty...)</span>
|
|
254
|
+
</button>
|
|
255
|
+
|
|
256
|
+
{showAdvanced && (
|
|
257
|
+
<div className="grid gap-6 md:grid-cols-2 mt-4">
|
|
258
|
+
<Card>
|
|
259
|
+
<CardHeader><CardTitle className="text-base">Type & Classification (Optional)</CardTitle></CardHeader>
|
|
260
|
+
<CardContent className="space-y-4">
|
|
261
|
+
<div>
|
|
262
|
+
<div className="flex items-center justify-between mb-1">
|
|
263
|
+
<Label htmlFor="deviceCategoryId">Device Type</Label>
|
|
264
|
+
<QuickAddCategoryDialog
|
|
265
|
+
onSuccess={(newCat) => {
|
|
266
|
+
setCategories((prev) => [newCat, ...prev])
|
|
267
|
+
setSelectedCategoryId(newCat.id)
|
|
268
|
+
setDetectedCategoryName(newCat.name)
|
|
269
|
+
}}
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
<select
|
|
273
|
+
id="deviceCategoryId"
|
|
274
|
+
name="deviceCategoryId"
|
|
275
|
+
value={selectedCategoryId}
|
|
276
|
+
onChange={(e) => {
|
|
277
|
+
setSelectedCategoryId(e.target.value)
|
|
278
|
+
const c = categories.find((cat: any) => cat.id === e.target.value)
|
|
279
|
+
setDetectedCategoryName(c ? c.name : "")
|
|
280
|
+
}}
|
|
281
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
282
|
+
>
|
|
283
|
+
<option value="">Auto-assign (Default: General Medical Equipment)</option>
|
|
284
|
+
{categories.map((c: any) => (
|
|
285
|
+
<option key={c.id} value={c.id}>{c.name}</option>
|
|
286
|
+
))}
|
|
287
|
+
</select>
|
|
288
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
289
|
+
Leave empty to auto-detect from name or use default.
|
|
290
|
+
</p>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div>
|
|
294
|
+
<div className="flex items-center justify-between mb-1">
|
|
295
|
+
<Label htmlFor="manufacturerId">Manufacturer</Label>
|
|
296
|
+
<QuickAddManufacturerDialog
|
|
297
|
+
onSuccess={(newMfr) => {
|
|
298
|
+
setManufacturers((prev) => [newMfr, ...prev])
|
|
299
|
+
setSelectedManufacturerId(newMfr.id)
|
|
300
|
+
}}
|
|
301
|
+
/>
|
|
302
|
+
</div>
|
|
303
|
+
<select
|
|
304
|
+
id="manufacturerId" name="manufacturerId"
|
|
305
|
+
value={selectedManufacturerId}
|
|
306
|
+
onChange={(e) => setSelectedManufacturerId(e.target.value)}
|
|
307
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
308
|
+
>
|
|
309
|
+
<option value="">Select manufacturer...</option>
|
|
310
|
+
{manufacturers.map((m: any) => (
|
|
311
|
+
<option key={m.id} value={m.id}>
|
|
312
|
+
{m.name} {m.country ? `(${m.country})` : ""}
|
|
313
|
+
</option>
|
|
314
|
+
))}
|
|
315
|
+
</select>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<div>
|
|
319
|
+
<Label htmlFor="modelNameFree">Model Name</Label>
|
|
320
|
+
<Input id="modelNameFree" name="modelNameFree" placeholder="Model name/number" />
|
|
321
|
+
</div>
|
|
322
|
+
</CardContent>
|
|
323
|
+
</Card>
|
|
324
|
+
|
|
325
|
+
<Card>
|
|
326
|
+
<CardHeader>
|
|
327
|
+
<CardTitle className="text-base">Purchase & Warranty (Optional)</CardTitle>
|
|
328
|
+
</CardHeader>
|
|
329
|
+
<CardContent className="space-y-4">
|
|
330
|
+
<div className="grid grid-cols-2 gap-3">
|
|
331
|
+
<div>
|
|
332
|
+
<Label htmlFor="purchaseDate">Purchase Date</Label>
|
|
333
|
+
<Input id="purchaseDate" name="purchaseDate" type="date" />
|
|
334
|
+
</div>
|
|
335
|
+
<div>
|
|
336
|
+
<Label htmlFor="installationDate">Installation Date</Label>
|
|
337
|
+
<Input id="installationDate" name="installationDate" type="date" />
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<div className="grid grid-cols-2 gap-3">
|
|
342
|
+
<div>
|
|
343
|
+
<Label htmlFor="purchaseCost">Purchase Cost</Label>
|
|
344
|
+
<Input id="purchaseCost" name="purchaseCost" type="number" step="0.01" placeholder="0.00" />
|
|
345
|
+
</div>
|
|
346
|
+
<div>
|
|
347
|
+
<Label htmlFor="currency">Currency</Label>
|
|
348
|
+
<Input id="currency" name="currency" defaultValue="USD" placeholder="USD" />
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<div className="grid grid-cols-2 gap-3">
|
|
353
|
+
<div>
|
|
354
|
+
<Label htmlFor="warrantyStartDate">Warranty Start</Label>
|
|
355
|
+
<Input id="warrantyStartDate" name="warrantyStartDate" type="date" />
|
|
356
|
+
</div>
|
|
357
|
+
<div>
|
|
358
|
+
<Label htmlFor="warrantyEndDate">Warranty End Date</Label>
|
|
359
|
+
<Input id="warrantyEndDate" name="warrantyEndDate" type="date" />
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</CardContent>
|
|
363
|
+
</Card>
|
|
364
|
+
</div>
|
|
365
|
+
)}
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<div className="flex justify-end gap-3 mt-6">
|
|
369
|
+
<Button type="button" variant="outline" onClick={() => router.back()}>Cancel</Button>
|
|
370
|
+
<Button type="submit" disabled={loading} className="min-h-[44px] px-6">
|
|
371
|
+
{loading ? (
|
|
372
|
+
<><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Registering...</>
|
|
373
|
+
) : (
|
|
374
|
+
"Register Device"
|
|
375
|
+
)}
|
|
376
|
+
</Button>
|
|
377
|
+
</div>
|
|
378
|
+
</form>
|
|
379
|
+
</div>
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getDevices } from "@/lib/actions/devices"
|
|
2
|
+
import { PageHeader } from "@/components/shared/page-header"
|
|
3
|
+
import { DataTable } from "@/components/shared/data-table"
|
|
4
|
+
import { DeviceStatusBadge, CriticalityBadge } from "@/components/devices/device-badges"
|
|
5
|
+
import { Button } from "@/components/ui/button"
|
|
6
|
+
import { Plus, QrCode } from "lucide-react"
|
|
7
|
+
import Link from "next/link"
|
|
8
|
+
|
|
9
|
+
export const dynamic = 'force-dynamic'
|
|
10
|
+
|
|
11
|
+
export default async function DevicesPage() {
|
|
12
|
+
const result = await getDevices()
|
|
13
|
+
const devicesList = (result?.success && Array.isArray(result.data)) ? result.data : []
|
|
14
|
+
|
|
15
|
+
const columns = [
|
|
16
|
+
{ key: "internalCode", header: "Code", render: (item: any) => (
|
|
17
|
+
<Link href={`/devices/${item.id}`} className="text-primary hover:underline font-mono text-sm">
|
|
18
|
+
{item.internalCode}
|
|
19
|
+
</Link>
|
|
20
|
+
)},
|
|
21
|
+
{ key: "name", header: "Device Name", render: (item: any) => (
|
|
22
|
+
<Link href={`/devices/${item.id}`} className="font-medium hover:underline">
|
|
23
|
+
{item.name}
|
|
24
|
+
</Link>
|
|
25
|
+
)},
|
|
26
|
+
{ key: "assetNumber", header: "Asset #" },
|
|
27
|
+
{ key: "category", header: "Category", render: (item: any) => item.deviceCategory?.name || "—" },
|
|
28
|
+
{ key: "manufacturer", header: "Manufacturer", render: (item: any) => item.manufacturer?.name || "—" },
|
|
29
|
+
{ key: "department", header: "Department", render: (item: any) => (
|
|
30
|
+
<span className="text-sm">
|
|
31
|
+
{[item.hospital?.name, item.department?.name].filter(Boolean).join(" / ") || "—"}
|
|
32
|
+
</span>
|
|
33
|
+
)},
|
|
34
|
+
{ key: "location", header: "Location", render: (item: any) => item.location?.name || item.exactLocationDescription || "—" },
|
|
35
|
+
{ key: "status", header: "Status", render: (item: any) => <DeviceStatusBadge status={item.currentStatusCode} /> },
|
|
36
|
+
{ key: "criticality", header: "Criticality", render: (item: any) => <CriticalityBadge level={item.criticalityLevel} /> },
|
|
37
|
+
{ key: "actions", header: "", render: (item: any) => (
|
|
38
|
+
<div className="flex gap-1">
|
|
39
|
+
<Button asChild variant="ghost" size="sm" title="View & Print QR">
|
|
40
|
+
<Link href={`/devices/${item.id}/qr`}>
|
|
41
|
+
<QrCode className="w-4 h-4" />
|
|
42
|
+
</Link>
|
|
43
|
+
</Button>
|
|
44
|
+
</div>
|
|
45
|
+
)},
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="space-y-6">
|
|
50
|
+
<PageHeader
|
|
51
|
+
title="Device Inventory"
|
|
52
|
+
description="Manage medical equipment and device lifecycle"
|
|
53
|
+
>
|
|
54
|
+
<div className="flex gap-2">
|
|
55
|
+
<Button asChild variant="outline">
|
|
56
|
+
<Link href="/scan">
|
|
57
|
+
<QrCode className="w-4 h-4 mr-2" />
|
|
58
|
+
Scan QR
|
|
59
|
+
</Link>
|
|
60
|
+
</Button>
|
|
61
|
+
<Button asChild>
|
|
62
|
+
<Link href="/devices/new">
|
|
63
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
64
|
+
Add Device
|
|
65
|
+
</Link>
|
|
66
|
+
</Button>
|
|
67
|
+
</div>
|
|
68
|
+
</PageHeader>
|
|
69
|
+
|
|
70
|
+
<DataTable
|
|
71
|
+
data={devicesList}
|
|
72
|
+
columns={columns}
|
|
73
|
+
searchKey="name"
|
|
74
|
+
searchPlaceholder="Search devices by name, asset #, serial #..."
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { auth } from '@/lib/auth';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { getEngineersTeam } from '@/lib/actions/engineers';
|
|
4
|
+
import { PageHeader } from '@/components/shared/page-header';
|
|
5
|
+
import { StaffEngineerTeamView } from '@/components/team/staff-engineer-team-view';
|
|
6
|
+
|
|
7
|
+
import { Button } from '@/components/ui/button';
|
|
8
|
+
import Link from 'next/link';
|
|
9
|
+
import { UsersRound } from 'lucide-react';
|
|
10
|
+
import { hasRole } from '@/lib/auth/rbac';
|
|
11
|
+
|
|
12
|
+
export const dynamic = 'force-dynamic';
|
|
13
|
+
|
|
14
|
+
export default async function EngineeringTeamPage() {
|
|
15
|
+
const session = await auth();
|
|
16
|
+
if (!session?.user) redirect('/login');
|
|
17
|
+
|
|
18
|
+
const teamRes = await getEngineersTeam();
|
|
19
|
+
const members = teamRes.data || [];
|
|
20
|
+
const userRoles = (session.user as any).roles || [];
|
|
21
|
+
const isBiomedOrAdmin = hasRole(userRoles, 'BIOMED_ENG', 'BIOMED_TECH', 'BIOMED_MGR', 'SYS_ADMIN', 'ORG_ADMIN');
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="space-y-6 max-w-3xl mx-auto">
|
|
25
|
+
<PageHeader
|
|
26
|
+
title="Our Engineering Team"
|
|
27
|
+
description="Biomedical engineers available to support your department"
|
|
28
|
+
>
|
|
29
|
+
{isBiomedOrAdmin && (
|
|
30
|
+
<Button asChild variant="outline" size="sm">
|
|
31
|
+
<Link href="/team">
|
|
32
|
+
<UsersRound className="w-4 h-4 mr-2" />
|
|
33
|
+
Duty Schedule & Coverage
|
|
34
|
+
</Link>
|
|
35
|
+
</Button>
|
|
36
|
+
)}
|
|
37
|
+
</PageHeader>
|
|
38
|
+
<StaffEngineerTeamView engineers={members} />
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { auth } from '@/lib/auth';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { Sidebar } from '@/components/layout/sidebar';
|
|
4
|
+
import { MobileNav } from '@/components/layout/mobile-nav';
|
|
5
|
+
import { Header } from '@/components/layout/header';
|
|
6
|
+
import { NavCountsProvider } from '@/components/providers/nav-counts-provider';
|
|
7
|
+
|
|
8
|
+
export default async function DashboardLayout({
|
|
9
|
+
children,
|
|
10
|
+
}: {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}) {
|
|
13
|
+
const session = await auth();
|
|
14
|
+
if (!session?.user) {
|
|
15
|
+
redirect('/login');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const sessionUser = session.user as
|
|
19
|
+
| { name?: string; fullName?: string; email?: string; roles?: string[] }
|
|
20
|
+
| undefined;
|
|
21
|
+
|
|
22
|
+
const roles = sessionUser?.roles || [];
|
|
23
|
+
|
|
24
|
+
const user = {
|
|
25
|
+
name: sessionUser?.fullName || sessionUser?.name || 'Administrator',
|
|
26
|
+
role: roles[0] ? roles[0].replace(/_/g, ' ') : 'Admin',
|
|
27
|
+
email: sessionUser?.email || 'admin@hospital.org',
|
|
28
|
+
roles,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<NavCountsProvider>
|
|
33
|
+
<div className="flex min-h-screen bg-slate-50/50 dark:bg-slate-950/50">
|
|
34
|
+
<Sidebar user={user} />
|
|
35
|
+
<div className="flex-1 flex flex-col min-w-0 md:ml-64 transition-all duration-300">
|
|
36
|
+
<Header user={user} />
|
|
37
|
+
<main className="flex-1 p-4 md:p-6 pb-24 md:pb-6">{children}</main>
|
|
38
|
+
</div>
|
|
39
|
+
<MobileNav roles={roles} />
|
|
40
|
+
</div>
|
|
41
|
+
</NavCountsProvider>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { listChecklistTemplates } from "@/lib/actions/checklists"
|
|
2
|
+
import { PageHeader } from "@/components/shared/page-header"
|
|
3
|
+
import { MaintenanceNav } from "@/components/maintenance/maintenance-nav"
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
5
|
+
import { Button } from "@/components/ui/button"
|
|
6
|
+
import { ClipboardCheck, Plus } from "lucide-react"
|
|
7
|
+
import Link from "next/link"
|
|
8
|
+
|
|
9
|
+
export default async function ChecklistTemplatesPage() {
|
|
10
|
+
return (
|
|
11
|
+
<div className="space-y-6">
|
|
12
|
+
<PageHeader
|
|
13
|
+
title="Checklist Templates"
|
|
14
|
+
description="Standardized inspection checklists, test procedures, and maintenance protocols"
|
|
15
|
+
>
|
|
16
|
+
<Link href="/maintenance/checklists/new">
|
|
17
|
+
<Button>
|
|
18
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
19
|
+
New Template
|
|
20
|
+
</Button>
|
|
21
|
+
</Link>
|
|
22
|
+
</PageHeader>
|
|
23
|
+
|
|
24
|
+
<MaintenanceNav />
|
|
25
|
+
|
|
26
|
+
<Card className="border-dashed">
|
|
27
|
+
<CardHeader className="text-center pb-2">
|
|
28
|
+
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 text-primary">
|
|
29
|
+
<ClipboardCheck className="h-6 w-6" />
|
|
30
|
+
</div>
|
|
31
|
+
<CardTitle className="text-xl">
|
|
32
|
+
Checklist template management coming soon
|
|
33
|
+
</CardTitle>
|
|
34
|
+
<CardDescription className="max-w-md mx-auto">
|
|
35
|
+
Design version-controlled maintenance checklists, safety inspection routines, and calibration steps with required pass/fail criteria and numerical measurements.
|
|
36
|
+
</CardDescription>
|
|
37
|
+
</CardHeader>
|
|
38
|
+
<CardContent className="flex flex-col items-center justify-center pb-8 pt-4">
|
|
39
|
+
<div className="flex gap-3">
|
|
40
|
+
<Link href="/maintenance/checklists/new">
|
|
41
|
+
<Button variant="outline" className="gap-2">
|
|
42
|
+
<Plus className="w-4 h-4" />
|
|
43
|
+
Create Template
|
|
44
|
+
</Button>
|
|
45
|
+
</Link>
|
|
46
|
+
<Link href="/maintenance">
|
|
47
|
+
<Button variant="ghost">
|
|
48
|
+
Back to Maintenance
|
|
49
|
+
</Button>
|
|
50
|
+
</Link>
|
|
51
|
+
</div>
|
|
52
|
+
</CardContent>
|
|
53
|
+
</Card>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|