@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,330 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
import { useRouter } from "next/navigation"
|
|
5
|
+
import { createDepartment } from "@/lib/actions/departments"
|
|
6
|
+
import { getOrganizations } from "@/lib/actions/organizations"
|
|
7
|
+
import { getHospitals } from "@/lib/actions/hospitals"
|
|
8
|
+
import { getUsers } from "@/lib/actions/users"
|
|
9
|
+
import { PageHeader } from "@/components/shared/page-header"
|
|
10
|
+
import { Button } from "@/components/ui/button"
|
|
11
|
+
import { Input } from "@/components/ui/input"
|
|
12
|
+
import { Label } from "@/components/ui/label"
|
|
13
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
14
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
|
15
|
+
import { ArrowLeft, Building2, Loader2, Check } from "lucide-react"
|
|
16
|
+
import Link from "next/link"
|
|
17
|
+
import { toast } from "sonner"
|
|
18
|
+
|
|
19
|
+
const DEPARTMENT_TYPES = [
|
|
20
|
+
{ value: "Clinical", label: "Clinical" },
|
|
21
|
+
{ value: "Surgical", label: "Surgical" },
|
|
22
|
+
{ value: "Intensive Care", label: "Intensive Care (ICU)" },
|
|
23
|
+
{ value: "Emergency", label: "Emergency" },
|
|
24
|
+
{ value: "Diagnostic & Imaging", label: "Diagnostic & Imaging" },
|
|
25
|
+
{ value: "Laboratory", label: "Laboratory" },
|
|
26
|
+
{ value: "Pharmacy", label: "Pharmacy" },
|
|
27
|
+
{ value: "Biomedical Engineering", label: "Biomedical Engineering" },
|
|
28
|
+
{ value: "Support Services", label: "Support Services" },
|
|
29
|
+
{ value: "Administration", label: "Administration" },
|
|
30
|
+
{ value: "Other", label: "Other" },
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
export default function NewDepartmentPage() {
|
|
34
|
+
const router = useRouter()
|
|
35
|
+
const [loading, setLoading] = useState(false)
|
|
36
|
+
const [organizations, setOrganizations] = useState<any[]>([])
|
|
37
|
+
const [hospitals, setHospitals] = useState<any[]>([])
|
|
38
|
+
const [users, setUsers] = useState<any[]>([])
|
|
39
|
+
|
|
40
|
+
const [organizationId, setOrganizationId] = useState("")
|
|
41
|
+
const [hospitalId, setHospitalId] = useState("")
|
|
42
|
+
const [name, setName] = useState("")
|
|
43
|
+
const [code, setCode] = useState("")
|
|
44
|
+
const [departmentType, setDepartmentType] = useState("")
|
|
45
|
+
const [managerUserId, setManagerUserId] = useState<string>("none")
|
|
46
|
+
|
|
47
|
+
// Load organizations and users on mount
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
async function loadInitialData() {
|
|
50
|
+
try {
|
|
51
|
+
const [orgRes, usersRes] = await Promise.all([
|
|
52
|
+
getOrganizations(),
|
|
53
|
+
getUsers(),
|
|
54
|
+
])
|
|
55
|
+
if (orgRes?.success && Array.isArray(orgRes.data)) {
|
|
56
|
+
setOrganizations(orgRes.data)
|
|
57
|
+
// If only one org, auto-select it
|
|
58
|
+
if (orgRes.data.length === 1) {
|
|
59
|
+
setOrganizationId(orgRes.data[0].id)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (usersRes?.success && Array.isArray(usersRes.data)) {
|
|
63
|
+
setUsers(usersRes.data)
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error("Failed to load initial data:", err)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
loadInitialData()
|
|
70
|
+
}, [])
|
|
71
|
+
|
|
72
|
+
// Load hospitals when organization is selected
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
async function loadHospitals() {
|
|
75
|
+
if (organizationId) {
|
|
76
|
+
try {
|
|
77
|
+
const res = await getHospitals(organizationId)
|
|
78
|
+
if (res?.success && Array.isArray(res.data)) {
|
|
79
|
+
setHospitals(res.data)
|
|
80
|
+
// If only one hospital, auto-select it
|
|
81
|
+
if (res.data.length === 1) {
|
|
82
|
+
setHospitalId(res.data[0].id)
|
|
83
|
+
} else {
|
|
84
|
+
setHospitalId("")
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error("Failed to load hospitals:", err)
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
setHospitals([])
|
|
92
|
+
setHospitalId("")
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
loadHospitals()
|
|
96
|
+
}, [organizationId])
|
|
97
|
+
|
|
98
|
+
// Auto-generate code from department name if code is empty
|
|
99
|
+
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
100
|
+
const val = e.target.value
|
|
101
|
+
setName(val)
|
|
102
|
+
if (!code || code === name.slice(0, 4).toUpperCase()) {
|
|
103
|
+
setCode(val.replace(/[^a-zA-Z0-9]/g, "").slice(0, 6).toUpperCase())
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
108
|
+
e.preventDefault()
|
|
109
|
+
|
|
110
|
+
if (!organizationId) {
|
|
111
|
+
toast.error("Please select an organization")
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
if (!hospitalId) {
|
|
115
|
+
toast.error("Please select a hospital")
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
if (!name.trim()) {
|
|
119
|
+
toast.error("Please enter a department name")
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
if (!code.trim()) {
|
|
123
|
+
toast.error("Please enter a department code")
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
setLoading(true)
|
|
128
|
+
try {
|
|
129
|
+
const res = await createDepartment({
|
|
130
|
+
organizationId,
|
|
131
|
+
hospitalId,
|
|
132
|
+
name: name.trim(),
|
|
133
|
+
code: code.trim().toUpperCase(),
|
|
134
|
+
departmentType: departmentType || undefined,
|
|
135
|
+
managerUserId: managerUserId === "none" ? undefined : managerUserId,
|
|
136
|
+
status: "active",
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
if (res.success) {
|
|
140
|
+
toast.success("Department created successfully!")
|
|
141
|
+
router.push("/departments")
|
|
142
|
+
router.refresh()
|
|
143
|
+
} else {
|
|
144
|
+
toast.error((res as any).error || "Failed to create department")
|
|
145
|
+
}
|
|
146
|
+
} catch (err: any) {
|
|
147
|
+
toast.error(err.message || "An unexpected error occurred")
|
|
148
|
+
} finally {
|
|
149
|
+
setLoading(false)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div className="space-y-6 max-w-2xl mx-auto pb-12">
|
|
155
|
+
<div className="flex items-center gap-2">
|
|
156
|
+
<Link
|
|
157
|
+
href="/departments"
|
|
158
|
+
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
159
|
+
>
|
|
160
|
+
<ArrowLeft className="w-4 h-4 mr-1.5" />
|
|
161
|
+
Back to Departments
|
|
162
|
+
</Link>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<PageHeader
|
|
166
|
+
title="Add Department"
|
|
167
|
+
description="Create a new clinical or operational hospital department"
|
|
168
|
+
/>
|
|
169
|
+
|
|
170
|
+
<Card className="shadow-xs border">
|
|
171
|
+
<CardHeader className="pb-4">
|
|
172
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
173
|
+
<Building2 className="w-5 h-5 text-primary" />
|
|
174
|
+
Department Details
|
|
175
|
+
</CardTitle>
|
|
176
|
+
<CardDescription>
|
|
177
|
+
Specify the organization, hospital location, and operational category for the department.
|
|
178
|
+
</CardDescription>
|
|
179
|
+
</CardHeader>
|
|
180
|
+
<CardContent>
|
|
181
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
182
|
+
{/* Organization Select */}
|
|
183
|
+
<div className="space-y-2">
|
|
184
|
+
<Label htmlFor="organization">
|
|
185
|
+
Organization <span className="text-destructive">*</span>
|
|
186
|
+
</Label>
|
|
187
|
+
<Select value={organizationId} onValueChange={setOrganizationId}>
|
|
188
|
+
<SelectTrigger id="organization">
|
|
189
|
+
<SelectValue placeholder="Select Organization" />
|
|
190
|
+
</SelectTrigger>
|
|
191
|
+
<SelectContent>
|
|
192
|
+
{organizations.map((org) => (
|
|
193
|
+
<SelectItem key={org.id} value={org.id}>
|
|
194
|
+
{org.name}
|
|
195
|
+
</SelectItem>
|
|
196
|
+
))}
|
|
197
|
+
</SelectContent>
|
|
198
|
+
</Select>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{/* Hospital Select (cascading) */}
|
|
202
|
+
<div className="space-y-2">
|
|
203
|
+
<Label htmlFor="hospital">
|
|
204
|
+
Hospital <span className="text-destructive">*</span>
|
|
205
|
+
</Label>
|
|
206
|
+
<Select
|
|
207
|
+
value={hospitalId}
|
|
208
|
+
onValueChange={setHospitalId}
|
|
209
|
+
disabled={!organizationId || hospitals.length === 0}
|
|
210
|
+
>
|
|
211
|
+
<SelectTrigger id="hospital">
|
|
212
|
+
<SelectValue
|
|
213
|
+
placeholder={
|
|
214
|
+
!organizationId
|
|
215
|
+
? "Select an organization first"
|
|
216
|
+
: hospitals.length === 0
|
|
217
|
+
? "No hospitals available"
|
|
218
|
+
: "Select Hospital"
|
|
219
|
+
}
|
|
220
|
+
/>
|
|
221
|
+
</SelectTrigger>
|
|
222
|
+
<SelectContent>
|
|
223
|
+
{hospitals.map((h) => (
|
|
224
|
+
<SelectItem key={h.id} value={h.id}>
|
|
225
|
+
{h.name}
|
|
226
|
+
</SelectItem>
|
|
227
|
+
))}
|
|
228
|
+
</SelectContent>
|
|
229
|
+
</Select>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Department Name */}
|
|
233
|
+
<div className="space-y-2">
|
|
234
|
+
<Label htmlFor="name">
|
|
235
|
+
Department Name <span className="text-destructive">*</span>
|
|
236
|
+
</Label>
|
|
237
|
+
<Input
|
|
238
|
+
id="name"
|
|
239
|
+
value={name}
|
|
240
|
+
onChange={handleNameChange}
|
|
241
|
+
placeholder="e.g. Intensive Care Unit, Cardiology, Radiology"
|
|
242
|
+
required
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{/* Department Code */}
|
|
247
|
+
<div className="space-y-2">
|
|
248
|
+
<Label htmlFor="code">
|
|
249
|
+
Department Code <span className="text-destructive">*</span>
|
|
250
|
+
</Label>
|
|
251
|
+
<Input
|
|
252
|
+
id="code"
|
|
253
|
+
value={code}
|
|
254
|
+
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
|
255
|
+
placeholder="e.g. ICU, CARD, RAD"
|
|
256
|
+
className="font-mono uppercase"
|
|
257
|
+
maxLength={20}
|
|
258
|
+
required
|
|
259
|
+
/>
|
|
260
|
+
<p className="text-xs text-muted-foreground">
|
|
261
|
+
Short unique identifier for asset codes and reporting (2-20 characters).
|
|
262
|
+
</p>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Department Type */}
|
|
266
|
+
<div className="space-y-2">
|
|
267
|
+
<Label htmlFor="type">Department Type</Label>
|
|
268
|
+
<Select value={departmentType} onValueChange={setDepartmentType}>
|
|
269
|
+
<SelectTrigger id="type">
|
|
270
|
+
<SelectValue placeholder="Select Department Type (optional)" />
|
|
271
|
+
</SelectTrigger>
|
|
272
|
+
<SelectContent>
|
|
273
|
+
{DEPARTMENT_TYPES.map((type) => (
|
|
274
|
+
<SelectItem key={type.value} value={type.value}>
|
|
275
|
+
{type.label}
|
|
276
|
+
</SelectItem>
|
|
277
|
+
))}
|
|
278
|
+
</SelectContent>
|
|
279
|
+
</Select>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{/* Department Manager */}
|
|
283
|
+
<div className="space-y-2">
|
|
284
|
+
<Label htmlFor="manager">Department Manager (Head of Department)</Label>
|
|
285
|
+
<Select value={managerUserId} onValueChange={setManagerUserId}>
|
|
286
|
+
<SelectTrigger id="manager">
|
|
287
|
+
<SelectValue placeholder="Assign a Department Manager (optional)" />
|
|
288
|
+
</SelectTrigger>
|
|
289
|
+
<SelectContent>
|
|
290
|
+
<SelectItem value="none">-- No Manager (Unassigned) --</SelectItem>
|
|
291
|
+
{users.map((u) => (
|
|
292
|
+
<SelectItem key={u.id} value={u.id}>
|
|
293
|
+
{u.fullName || u.email} ({u.email})
|
|
294
|
+
</SelectItem>
|
|
295
|
+
))}
|
|
296
|
+
</SelectContent>
|
|
297
|
+
</Select>
|
|
298
|
+
<p className="text-xs text-muted-foreground">
|
|
299
|
+
Optional. You can assign or change the department manager anytime later.
|
|
300
|
+
</p>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
{/* Form Actions */}
|
|
304
|
+
<div className="flex items-center justify-end gap-3 pt-4 border-t">
|
|
305
|
+
<Link href="/departments">
|
|
306
|
+
<Button type="button" variant="outline" disabled={loading}>
|
|
307
|
+
Cancel
|
|
308
|
+
</Button>
|
|
309
|
+
</Link>
|
|
310
|
+
<Button type="submit" disabled={loading} className="gap-2">
|
|
311
|
+
{loading ? (
|
|
312
|
+
<>
|
|
313
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
314
|
+
Creating...
|
|
315
|
+
</>
|
|
316
|
+
) : (
|
|
317
|
+
<>
|
|
318
|
+
<Check className="w-4 h-4" />
|
|
319
|
+
Create Department
|
|
320
|
+
</>
|
|
321
|
+
)}
|
|
322
|
+
</Button>
|
|
323
|
+
</div>
|
|
324
|
+
</form>
|
|
325
|
+
</CardContent>
|
|
326
|
+
</Card>
|
|
327
|
+
</div>
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { auth } from '@/lib/auth';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { getDepartmentsOverview } from '@/lib/actions/departments';
|
|
4
|
+
import { PageHeader } from '@/components/shared/page-header';
|
|
5
|
+
import { DepartmentCardsView } from '@/components/departments/department-cards-view';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Plus, QrCode } from 'lucide-react';
|
|
8
|
+
import Link from 'next/link';
|
|
9
|
+
import { hasRole } from '@/lib/auth/rbac';
|
|
10
|
+
|
|
11
|
+
export const dynamic = 'force-dynamic';
|
|
12
|
+
|
|
13
|
+
export default async function DepartmentsPage() {
|
|
14
|
+
const session = await auth();
|
|
15
|
+
if (!session?.user) redirect('/login');
|
|
16
|
+
|
|
17
|
+
const user = session.user as { roles?: string[] };
|
|
18
|
+
const isAdmin = hasRole(user.roles || [], 'SYS_ADMIN', 'ORG_ADMIN', 'HOSP_ADMIN');
|
|
19
|
+
|
|
20
|
+
const result = await getDepartmentsOverview();
|
|
21
|
+
const departments = result.success && result.data ? result.data.departments : [];
|
|
22
|
+
const hospitals = result.success && result.data ? result.data.hospitals : [];
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="space-y-6 max-w-7xl mx-auto">
|
|
26
|
+
<PageHeader
|
|
27
|
+
title="Departments"
|
|
28
|
+
description="Browse clinical departments, equipment health, and device allocations"
|
|
29
|
+
>
|
|
30
|
+
<div className="flex gap-2">
|
|
31
|
+
<Button asChild variant="outline" className="min-h-[44px]">
|
|
32
|
+
<Link href="/scan">
|
|
33
|
+
<QrCode className="w-4 h-4 mr-2" />
|
|
34
|
+
Scan QR
|
|
35
|
+
</Link>
|
|
36
|
+
</Button>
|
|
37
|
+
{isAdmin && (
|
|
38
|
+
<Button asChild className="min-h-[44px]">
|
|
39
|
+
<Link href="/departments/new">
|
|
40
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
41
|
+
Add Department
|
|
42
|
+
</Link>
|
|
43
|
+
</Button>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
</PageHeader>
|
|
47
|
+
|
|
48
|
+
<DepartmentCardsView departments={departments} hospitals={hospitals} />
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { updateDevice } from "@/lib/actions/devices"
|
|
4
|
+
import { getDeviceById } from "@/lib/actions/devices"
|
|
5
|
+
import { PageHeader } from "@/components/shared/page-header"
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import { Input } from "@/components/ui/input"
|
|
8
|
+
import { Label } from "@/components/ui/label"
|
|
9
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
10
|
+
import { useRouter } from "next/navigation"
|
|
11
|
+
import { useState, useEffect, use } from "react"
|
|
12
|
+
|
|
13
|
+
export default function EditDevicePage({ params }: { params: Promise<{ id: string }> }) {
|
|
14
|
+
const { id } = use(params)
|
|
15
|
+
const router = useRouter()
|
|
16
|
+
const [loading, setLoading] = useState(false)
|
|
17
|
+
const [error, setError] = useState("")
|
|
18
|
+
const [device, setDevice] = useState<any>(null)
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
async function load() {
|
|
22
|
+
const res = await getDeviceById(id)
|
|
23
|
+
if (res?.success && res.data) setDevice(res.data)
|
|
24
|
+
}
|
|
25
|
+
load()
|
|
26
|
+
}, [id])
|
|
27
|
+
|
|
28
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
29
|
+
e.preventDefault()
|
|
30
|
+
setLoading(true)
|
|
31
|
+
setError("")
|
|
32
|
+
const fd = new FormData(e.currentTarget)
|
|
33
|
+
const data: any = {}
|
|
34
|
+
fd.forEach((v, k) => { if (v) data[k] = v })
|
|
35
|
+
|
|
36
|
+
const result = await updateDevice(id, data)
|
|
37
|
+
if (result?.success) {
|
|
38
|
+
router.push(`/devices/${id}`)
|
|
39
|
+
} else {
|
|
40
|
+
setError((result as any)?.error || "Failed to update device")
|
|
41
|
+
}
|
|
42
|
+
setLoading(false)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!device) return <div className="p-8 text-center text-muted-foreground">Loading...</div>
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="space-y-6">
|
|
49
|
+
<PageHeader title={`Edit: ${device.name}`} description={`${device.internalCode}`} />
|
|
50
|
+
{error && <div className="bg-destructive/10 text-destructive p-3 rounded-md text-sm">{error}</div>}
|
|
51
|
+
|
|
52
|
+
<form onSubmit={handleSubmit}>
|
|
53
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
54
|
+
<Card>
|
|
55
|
+
<CardHeader><CardTitle>Device Identity</CardTitle></CardHeader>
|
|
56
|
+
<CardContent className="space-y-4">
|
|
57
|
+
<div>
|
|
58
|
+
<Label htmlFor="name">Device Name</Label>
|
|
59
|
+
<Input id="name" name="name" defaultValue={device.name} />
|
|
60
|
+
</div>
|
|
61
|
+
<div>
|
|
62
|
+
<Label htmlFor="assetNumber">Asset Number</Label>
|
|
63
|
+
<Input id="assetNumber" name="assetNumber" defaultValue={device.assetNumber} />
|
|
64
|
+
</div>
|
|
65
|
+
<div>
|
|
66
|
+
<Label htmlFor="serialNumber">Serial Number</Label>
|
|
67
|
+
<Input id="serialNumber" name="serialNumber" defaultValue={device.serialNumber || ""} />
|
|
68
|
+
</div>
|
|
69
|
+
<div>
|
|
70
|
+
<Label htmlFor="modelNameFree">Model Name</Label>
|
|
71
|
+
<Input id="modelNameFree" name="modelNameFree" defaultValue={device.modelNameFree || ""} />
|
|
72
|
+
</div>
|
|
73
|
+
</CardContent>
|
|
74
|
+
</Card>
|
|
75
|
+
|
|
76
|
+
<Card>
|
|
77
|
+
<CardHeader><CardTitle>Classification</CardTitle></CardHeader>
|
|
78
|
+
<CardContent className="space-y-4">
|
|
79
|
+
<div>
|
|
80
|
+
<Label htmlFor="criticalityLevel">Criticality Level</Label>
|
|
81
|
+
<select id="criticalityLevel" name="criticalityLevel" defaultValue={device.criticalityLevel || "medium"}
|
|
82
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
|
83
|
+
<option value="critical">Critical</option>
|
|
84
|
+
<option value="high">High</option>
|
|
85
|
+
<option value="medium">Medium</option>
|
|
86
|
+
<option value="low">Low</option>
|
|
87
|
+
</select>
|
|
88
|
+
</div>
|
|
89
|
+
<div>
|
|
90
|
+
<Label htmlFor="riskClassification">Risk Classification</Label>
|
|
91
|
+
<select id="riskClassification" name="riskClassification" defaultValue={device.riskClassification || ""}
|
|
92
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
|
93
|
+
<option value="">Select...</option>
|
|
94
|
+
<option value="class_i">Class I</option>
|
|
95
|
+
<option value="class_iia">Class IIa</option>
|
|
96
|
+
<option value="class_iib">Class IIb</option>
|
|
97
|
+
<option value="class_iii">Class III</option>
|
|
98
|
+
</select>
|
|
99
|
+
</div>
|
|
100
|
+
<div>
|
|
101
|
+
<Label htmlFor="exactLocationDescription">Exact Location</Label>
|
|
102
|
+
<Input id="exactLocationDescription" name="exactLocationDescription" defaultValue={device.exactLocationDescription || ""} />
|
|
103
|
+
</div>
|
|
104
|
+
</CardContent>
|
|
105
|
+
</Card>
|
|
106
|
+
|
|
107
|
+
<Card className="md:col-span-2">
|
|
108
|
+
<CardHeader><CardTitle>Warranty</CardTitle></CardHeader>
|
|
109
|
+
<CardContent className="grid gap-4 md:grid-cols-3">
|
|
110
|
+
<div>
|
|
111
|
+
<Label htmlFor="warrantyStartDate">Warranty Start</Label>
|
|
112
|
+
<Input id="warrantyStartDate" name="warrantyStartDate" type="date" defaultValue={device.warrantyStartDate || ""} />
|
|
113
|
+
</div>
|
|
114
|
+
<div>
|
|
115
|
+
<Label htmlFor="warrantyEndDate">Warranty End</Label>
|
|
116
|
+
<Input id="warrantyEndDate" name="warrantyEndDate" type="date" defaultValue={device.warrantyEndDate || ""} />
|
|
117
|
+
</div>
|
|
118
|
+
</CardContent>
|
|
119
|
+
</Card>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div className="flex justify-end gap-3 mt-6">
|
|
123
|
+
<Button type="button" variant="outline" onClick={() => router.back()}>Cancel</Button>
|
|
124
|
+
<Button type="submit" disabled={loading}>
|
|
125
|
+
{loading ? "Saving..." : "Save Changes"}
|
|
126
|
+
</Button>
|
|
127
|
+
</div>
|
|
128
|
+
</form>
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|