@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,20 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Activity } from 'lucide-react'
|
|
3
|
+
import { ThemeToggle } from '@/components/theme/theme-toggle'
|
|
4
|
+
|
|
5
|
+
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="min-h-screen relative flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-950 p-4 transition-colors duration-200" suppressHydrationWarning>
|
|
8
|
+
<div className="absolute top-4 right-4 z-10">
|
|
9
|
+
<ThemeToggle />
|
|
10
|
+
</div>
|
|
11
|
+
<div className="mb-8 flex items-center justify-center gap-2 text-primary" suppressHydrationWarning>
|
|
12
|
+
<Activity className="h-10 w-10" />
|
|
13
|
+
<h1 className="text-3xl font-bold tracking-tight">BEMMS</h1>
|
|
14
|
+
</div>
|
|
15
|
+
<div className="w-full max-w-md" suppressHydrationWarning>
|
|
16
|
+
{children}
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { signIn } from 'next-auth/react'
|
|
5
|
+
import { useRouter } from 'next/navigation'
|
|
6
|
+
import { Eye, EyeOff, Loader2 } from 'lucide-react'
|
|
7
|
+
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
|
|
8
|
+
import { Input } from '@/components/ui/input'
|
|
9
|
+
import { Label } from '@/components/ui/label'
|
|
10
|
+
import { Button } from '@/components/ui/button'
|
|
11
|
+
|
|
12
|
+
export default function LoginPage() {
|
|
13
|
+
const router = useRouter()
|
|
14
|
+
const [email, setEmail] = useState('')
|
|
15
|
+
const [password, setPassword] = useState('')
|
|
16
|
+
const [showPassword, setShowPassword] = useState(false)
|
|
17
|
+
const [loading, setLoading] = useState(false)
|
|
18
|
+
const [error, setError] = useState('')
|
|
19
|
+
|
|
20
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
21
|
+
e.preventDefault()
|
|
22
|
+
setLoading(true)
|
|
23
|
+
setError('')
|
|
24
|
+
|
|
25
|
+
const cleanEmail = email.trim()
|
|
26
|
+
|
|
27
|
+
if (!cleanEmail || !password) {
|
|
28
|
+
setError('Please provide both username/email and password.')
|
|
29
|
+
setLoading(false)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const res = await signIn('credentials', {
|
|
35
|
+
email: cleanEmail,
|
|
36
|
+
password,
|
|
37
|
+
redirect: false,
|
|
38
|
+
})
|
|
39
|
+
if (res?.error) {
|
|
40
|
+
if (res.error.includes('DATABASE_CONNECTION_ERROR')) {
|
|
41
|
+
setError('Database connection error. Please verify the database service is running.')
|
|
42
|
+
} else {
|
|
43
|
+
setError('Invalid username, email, or password. Please verify your credentials.')
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
router.push('/dashboard')
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
setError('An error occurred. Please try again.')
|
|
50
|
+
} finally {
|
|
51
|
+
setLoading(false)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Card className="w-full shadow-lg border-border/40" suppressHydrationWarning>
|
|
57
|
+
<CardHeader className="space-y-1 text-center" suppressHydrationWarning>
|
|
58
|
+
<CardTitle className="text-2xl">Sign In</CardTitle>
|
|
59
|
+
<CardDescription>
|
|
60
|
+
Enter your username or email and password to access your account
|
|
61
|
+
</CardDescription>
|
|
62
|
+
</CardHeader>
|
|
63
|
+
|
|
64
|
+
<form onSubmit={handleSubmit} suppressHydrationWarning>
|
|
65
|
+
<CardContent className="space-y-4" suppressHydrationWarning>
|
|
66
|
+
{error && (
|
|
67
|
+
<div className="p-3 text-sm text-destructive-foreground bg-destructive/90 rounded-md">
|
|
68
|
+
{error}
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
<div className="space-y-2" suppressHydrationWarning>
|
|
72
|
+
<Label htmlFor="email">Username or Email</Label>
|
|
73
|
+
<Input
|
|
74
|
+
id="email"
|
|
75
|
+
name="email"
|
|
76
|
+
type="text"
|
|
77
|
+
autoComplete="username"
|
|
78
|
+
autoCapitalize="none"
|
|
79
|
+
autoCorrect="off"
|
|
80
|
+
placeholder="admin or email@hospital.org"
|
|
81
|
+
data-lpignore="true"
|
|
82
|
+
value={email}
|
|
83
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
84
|
+
suppressHydrationWarning
|
|
85
|
+
required
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
<div className="space-y-2" suppressHydrationWarning>
|
|
89
|
+
<Label htmlFor="password">Password</Label>
|
|
90
|
+
<div className="relative" suppressHydrationWarning>
|
|
91
|
+
<Input
|
|
92
|
+
id="password"
|
|
93
|
+
name="password"
|
|
94
|
+
type={showPassword ? 'text' : 'password'}
|
|
95
|
+
autoComplete="current-password"
|
|
96
|
+
data-lpignore="true"
|
|
97
|
+
value={password}
|
|
98
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
99
|
+
suppressHydrationWarning
|
|
100
|
+
required
|
|
101
|
+
/>
|
|
102
|
+
<Button
|
|
103
|
+
type="button"
|
|
104
|
+
variant="ghost"
|
|
105
|
+
size="icon"
|
|
106
|
+
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
|
107
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
108
|
+
tabIndex={-1}
|
|
109
|
+
>
|
|
110
|
+
{showPassword ? (
|
|
111
|
+
<EyeOff className="h-4 w-4 text-muted-foreground" />
|
|
112
|
+
) : (
|
|
113
|
+
<Eye className="h-4 w-4 text-muted-foreground" />
|
|
114
|
+
)}
|
|
115
|
+
<span className="sr-only">Toggle password visibility</span>
|
|
116
|
+
</Button>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</CardContent>
|
|
120
|
+
<CardFooter suppressHydrationWarning>
|
|
121
|
+
<Button className="w-full" type="submit" disabled={loading}>
|
|
122
|
+
{loading ? (
|
|
123
|
+
<>
|
|
124
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
125
|
+
Signing in...
|
|
126
|
+
</>
|
|
127
|
+
) : (
|
|
128
|
+
'Sign In'
|
|
129
|
+
)}
|
|
130
|
+
</Button>
|
|
131
|
+
</CardFooter>
|
|
132
|
+
</form>
|
|
133
|
+
</Card>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAuditLogs,
|
|
3
|
+
getAuditLogEntityTypes,
|
|
4
|
+
getAuditLogActors,
|
|
5
|
+
getAuditLogStats,
|
|
6
|
+
type AuditLogFilters as AuditFiltersType,
|
|
7
|
+
} from '@/lib/actions/audit-logs';
|
|
8
|
+
import { PageHeader } from '@/components/shared/page-header';
|
|
9
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
10
|
+
import { AuditLogFilters } from '@/components/admin/audit-log-filters';
|
|
11
|
+
import { AuditLogTable } from '@/components/admin/audit-log-table';
|
|
12
|
+
import { AuditLogExport } from '@/components/admin/audit-log-export';
|
|
13
|
+
import { Activity, Clock, AlertTriangle, Database } from 'lucide-react';
|
|
14
|
+
|
|
15
|
+
interface AuditLogsPageProps {
|
|
16
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default async function AuditLogsPage({ searchParams }: AuditLogsPageProps) {
|
|
20
|
+
const params = await searchParams;
|
|
21
|
+
|
|
22
|
+
const search = typeof params.search === 'string' ? params.search : undefined;
|
|
23
|
+
const entityType = typeof params.entityType === 'string' ? params.entityType : undefined;
|
|
24
|
+
const actionType = typeof params.actionType === 'string' ? params.actionType : undefined;
|
|
25
|
+
const actorId = typeof params.actorId === 'string' ? params.actorId : undefined;
|
|
26
|
+
const startDate = typeof params.startDate === 'string' ? params.startDate : undefined;
|
|
27
|
+
const endDate = typeof params.endDate === 'string' ? params.endDate : undefined;
|
|
28
|
+
const page = typeof params.page === 'string' ? Math.max(1, parseInt(params.page, 10) || 1) : 1;
|
|
29
|
+
const pageSize = typeof params.pageSize === 'string' ? Math.max(1, parseInt(params.pageSize, 10) || 20) : 20;
|
|
30
|
+
|
|
31
|
+
const filters: AuditFiltersType = {
|
|
32
|
+
search,
|
|
33
|
+
entityType,
|
|
34
|
+
actionType,
|
|
35
|
+
actorId,
|
|
36
|
+
startDate,
|
|
37
|
+
endDate,
|
|
38
|
+
page,
|
|
39
|
+
pageSize,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Fetch audit logs, distinct entity types, actors, and stats concurrently
|
|
43
|
+
const [logsRes, entityTypesRes, actorsRes, statsRes] = await Promise.all([
|
|
44
|
+
getAuditLogs(filters),
|
|
45
|
+
getAuditLogEntityTypes(),
|
|
46
|
+
getAuditLogActors(),
|
|
47
|
+
getAuditLogStats(),
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const logs = logsRes?.success && logsRes.data ? logsRes.data : [];
|
|
51
|
+
const pagination = logsRes?.pagination || {
|
|
52
|
+
page,
|
|
53
|
+
pageSize,
|
|
54
|
+
total: logs.length,
|
|
55
|
+
totalPages: 1,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const entityTypes = entityTypesRes?.success && entityTypesRes.data ? entityTypesRes.data : [];
|
|
59
|
+
const actors = actorsRes?.success && actorsRes.data ? actorsRes.data : [];
|
|
60
|
+
const stats = statsRes?.success && statsRes.data
|
|
61
|
+
? statsRes.data
|
|
62
|
+
: { total: 0, last24h: 0, last7d: 0, failedLogins24h: 0 };
|
|
63
|
+
|
|
64
|
+
const hasFailedLogins = stats.failedLogins24h > 0;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="space-y-6">
|
|
68
|
+
<PageHeader
|
|
69
|
+
title="Audit Logs"
|
|
70
|
+
description="Comprehensive audit trail and security event logs across system modules"
|
|
71
|
+
>
|
|
72
|
+
<AuditLogExport filters={filters} />
|
|
73
|
+
</PageHeader>
|
|
74
|
+
|
|
75
|
+
{/* 4 Stat Cards */}
|
|
76
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
77
|
+
{/* Total Logs */}
|
|
78
|
+
<Card className="border shadow-sm">
|
|
79
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
|
80
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
81
|
+
Total Logs
|
|
82
|
+
</CardTitle>
|
|
83
|
+
<Database className="h-4 w-4 text-muted-foreground" />
|
|
84
|
+
</CardHeader>
|
|
85
|
+
<CardContent>
|
|
86
|
+
<div className="text-2xl font-bold">{stats.total.toLocaleString()}</div>
|
|
87
|
+
<p className="text-xs text-muted-foreground mt-1">All recorded events</p>
|
|
88
|
+
</CardContent>
|
|
89
|
+
</Card>
|
|
90
|
+
|
|
91
|
+
{/* Last 24 Hours */}
|
|
92
|
+
<Card className="border shadow-sm">
|
|
93
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
|
94
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
95
|
+
Last 24 Hours
|
|
96
|
+
</CardTitle>
|
|
97
|
+
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
98
|
+
</CardHeader>
|
|
99
|
+
<CardContent>
|
|
100
|
+
<div className="text-2xl font-bold">{stats.last24h.toLocaleString()}</div>
|
|
101
|
+
<p className="text-xs text-muted-foreground mt-1">Recent system activities</p>
|
|
102
|
+
</CardContent>
|
|
103
|
+
</Card>
|
|
104
|
+
|
|
105
|
+
{/* Last 7 Days */}
|
|
106
|
+
<Card className="border shadow-sm">
|
|
107
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
|
108
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
109
|
+
Last 7 Days
|
|
110
|
+
</CardTitle>
|
|
111
|
+
<Activity className="h-4 w-4 text-muted-foreground" />
|
|
112
|
+
</CardHeader>
|
|
113
|
+
<CardContent>
|
|
114
|
+
<div className="text-2xl font-bold">{stats.last7d.toLocaleString()}</div>
|
|
115
|
+
<p className="text-xs text-muted-foreground mt-1">Weekly event volume</p>
|
|
116
|
+
</CardContent>
|
|
117
|
+
</Card>
|
|
118
|
+
|
|
119
|
+
{/* Failed Logins (24h) */}
|
|
120
|
+
<Card
|
|
121
|
+
className={`border shadow-sm transition-colors ${
|
|
122
|
+
hasFailedLogins
|
|
123
|
+
? 'border-red-300 bg-red-50/40 dark:border-red-900 dark:bg-red-950/20'
|
|
124
|
+
: ''
|
|
125
|
+
}`}
|
|
126
|
+
>
|
|
127
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
|
128
|
+
<CardTitle
|
|
129
|
+
className={`text-sm font-medium ${
|
|
130
|
+
hasFailedLogins ? 'text-red-700 dark:text-red-400' : 'text-muted-foreground'
|
|
131
|
+
}`}
|
|
132
|
+
>
|
|
133
|
+
Failed Logins (24h)
|
|
134
|
+
</CardTitle>
|
|
135
|
+
<AlertTriangle
|
|
136
|
+
className={`h-4 w-4 ${
|
|
137
|
+
hasFailedLogins
|
|
138
|
+
? 'text-red-600 dark:text-red-400'
|
|
139
|
+
: 'text-muted-foreground'
|
|
140
|
+
}`}
|
|
141
|
+
/>
|
|
142
|
+
</CardHeader>
|
|
143
|
+
<CardContent>
|
|
144
|
+
<div
|
|
145
|
+
className={`text-2xl font-bold ${
|
|
146
|
+
hasFailedLogins ? 'text-red-600 dark:text-red-400' : ''
|
|
147
|
+
}`}
|
|
148
|
+
>
|
|
149
|
+
{stats.failedLogins24h.toLocaleString()}
|
|
150
|
+
</div>
|
|
151
|
+
<p
|
|
152
|
+
className={`text-xs mt-1 ${
|
|
153
|
+
hasFailedLogins
|
|
154
|
+
? 'text-red-600/80 dark:text-red-400/80 font-medium'
|
|
155
|
+
: 'text-muted-foreground'
|
|
156
|
+
}`}
|
|
157
|
+
>
|
|
158
|
+
{hasFailedLogins ? 'Attention recommended' : 'No authentication alerts'}
|
|
159
|
+
</p>
|
|
160
|
+
</CardContent>
|
|
161
|
+
</Card>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Filters row */}
|
|
165
|
+
<AuditLogFilters
|
|
166
|
+
entityTypes={entityTypes}
|
|
167
|
+
actors={actors}
|
|
168
|
+
/>
|
|
169
|
+
|
|
170
|
+
{/* Audit Log Table with Pagination */}
|
|
171
|
+
<Card className="border shadow-sm">
|
|
172
|
+
<CardContent className="pt-6">
|
|
173
|
+
<AuditLogTable logs={logs} pagination={pagination} />
|
|
174
|
+
</CardContent>
|
|
175
|
+
</Card>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { redirect } from 'next/navigation';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { requireRole } from '@/lib/auth/rbac';
|
|
4
|
+
import { getExportSummary } from '@/lib/actions/data-management';
|
|
5
|
+
import { DataManagementTabs } from '@/components/admin/data-management/data-management-tabs';
|
|
6
|
+
|
|
7
|
+
export default async function AdminDataManagementPage() {
|
|
8
|
+
const session = await auth();
|
|
9
|
+
if (!session?.user) {
|
|
10
|
+
redirect('/login');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await requireRole('SYS_ADMIN', 'ORG_ADMIN', 'BIOMED_MGR');
|
|
15
|
+
} catch {
|
|
16
|
+
redirect('/dashboard');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const summaryRes = await getExportSummary();
|
|
20
|
+
const summary = summaryRes.success && summaryRes.data
|
|
21
|
+
? summaryRes.data
|
|
22
|
+
: {
|
|
23
|
+
devices: 0,
|
|
24
|
+
departments: 0,
|
|
25
|
+
categories: 0,
|
|
26
|
+
manufacturers: 0,
|
|
27
|
+
tickets: 0,
|
|
28
|
+
auditLogs: 0,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-6">
|
|
33
|
+
<DataManagementTabs summary={summary} />
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, use } from "react"
|
|
4
|
+
import { useRouter } from "next/navigation"
|
|
5
|
+
import { useForm } from "react-hook-form"
|
|
6
|
+
import { getDepartmentById, updateDepartment } from "@/lib/actions/departments"
|
|
7
|
+
import { getUsers } from "@/lib/actions/users"
|
|
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
13
|
+
import { Card, CardContent } from "@/components/ui/card"
|
|
14
|
+
import { ArrowLeft, Loader2, Save } from "lucide-react"
|
|
15
|
+
import Link from "next/link"
|
|
16
|
+
import { toast } from "sonner"
|
|
17
|
+
|
|
18
|
+
const DEPARTMENT_TYPES = [
|
|
19
|
+
"Emergency",
|
|
20
|
+
"Intensive Care",
|
|
21
|
+
"Surgical",
|
|
22
|
+
"Laboratory",
|
|
23
|
+
"Diagnostic & Imaging",
|
|
24
|
+
"Pediatrics",
|
|
25
|
+
"Dental",
|
|
26
|
+
"Clinical",
|
|
27
|
+
"Biomedical Engineering",
|
|
28
|
+
"Administration",
|
|
29
|
+
"Support Services",
|
|
30
|
+
"Other",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
export default function EditDepartmentPage({ params }: { params: Promise<{ deptId: string }> }) {
|
|
34
|
+
const resolvedParams = use(params)
|
|
35
|
+
const router = useRouter()
|
|
36
|
+
const [loading, setLoading] = useState(true)
|
|
37
|
+
const [saving, setSaving] = useState(false)
|
|
38
|
+
const [users, setUsers] = useState<any[]>([])
|
|
39
|
+
const [selectedManagerId, setSelectedManagerId] = useState<string>("none")
|
|
40
|
+
const [selectedType, setSelectedType] = useState<string>("")
|
|
41
|
+
const [selectedStatus, setSelectedStatus] = useState<string>("active")
|
|
42
|
+
|
|
43
|
+
const { register, handleSubmit, setValue } = useForm()
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
async function loadData() {
|
|
47
|
+
try {
|
|
48
|
+
const [deptRes, usersRes] = await Promise.all([
|
|
49
|
+
getDepartmentById(resolvedParams.deptId),
|
|
50
|
+
getUsers(),
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
if (usersRes.success && Array.isArray(usersRes.data)) {
|
|
54
|
+
setUsers(usersRes.data)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (deptRes.success && deptRes.data) {
|
|
58
|
+
const d = deptRes.data
|
|
59
|
+
setValue("name", d.name)
|
|
60
|
+
setValue("code", d.code)
|
|
61
|
+
setValue("departmentType", d.departmentType || "")
|
|
62
|
+
setValue("status", d.status || "active")
|
|
63
|
+
setSelectedType(d.departmentType || "")
|
|
64
|
+
setSelectedStatus(d.status || "active")
|
|
65
|
+
setSelectedManagerId(d.managerUserId || "none")
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error("Failed to load department data:", err)
|
|
69
|
+
toast.error("Failed to load department details")
|
|
70
|
+
} finally {
|
|
71
|
+
setLoading(false)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
loadData()
|
|
75
|
+
}, [resolvedParams.deptId, setValue])
|
|
76
|
+
|
|
77
|
+
const onSubmit = async (data: any) => {
|
|
78
|
+
setSaving(true)
|
|
79
|
+
try {
|
|
80
|
+
const payload = {
|
|
81
|
+
name: data.name,
|
|
82
|
+
code: data.code,
|
|
83
|
+
departmentType: selectedType || null,
|
|
84
|
+
status: selectedStatus || "active",
|
|
85
|
+
managerUserId: selectedManagerId === "none" ? null : selectedManagerId,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const res = await updateDepartment(resolvedParams.deptId, payload)
|
|
89
|
+
if (res.success) {
|
|
90
|
+
toast.success("Department updated successfully")
|
|
91
|
+
router.push(`/departments/${resolvedParams.deptId}`)
|
|
92
|
+
router.refresh()
|
|
93
|
+
} else {
|
|
94
|
+
toast.error((res as any).error || "Failed to update department")
|
|
95
|
+
}
|
|
96
|
+
} catch (err: any) {
|
|
97
|
+
toast.error(err.message || "An unexpected error occurred")
|
|
98
|
+
} finally {
|
|
99
|
+
setSaving(false)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (loading) {
|
|
104
|
+
return (
|
|
105
|
+
<div className="flex items-center justify-center min-h-[400px]">
|
|
106
|
+
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="space-y-6 max-w-2xl mx-auto">
|
|
113
|
+
<div className="flex items-center gap-3">
|
|
114
|
+
<Button variant="ghost" size="icon" asChild>
|
|
115
|
+
<Link href={`/departments/${resolvedParams.deptId}`}>
|
|
116
|
+
<ArrowLeft className="w-4 h-4" />
|
|
117
|
+
</Link>
|
|
118
|
+
</Button>
|
|
119
|
+
<PageHeader title="Edit Department" description="Update department configuration and assigned manager" />
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<Card>
|
|
123
|
+
<CardContent className="pt-6">
|
|
124
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
|
|
125
|
+
<div className="space-y-2">
|
|
126
|
+
<Label htmlFor="name">Department Name <span className="text-destructive">*</span></Label>
|
|
127
|
+
<Input id="name" {...register("name", { required: true })} placeholder="e.g. Emergency Department" />
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div className="space-y-2">
|
|
131
|
+
<Label htmlFor="code">Department Code <span className="text-destructive">*</span></Label>
|
|
132
|
+
<Input id="code" {...register("code", { required: true })} placeholder="e.g. EMERG" />
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div className="space-y-2">
|
|
136
|
+
<Label htmlFor="departmentType">Department Type</Label>
|
|
137
|
+
<Select value={selectedType} onValueChange={(val) => setSelectedType(val)}>
|
|
138
|
+
<SelectTrigger id="departmentType">
|
|
139
|
+
<SelectValue placeholder="Select Department Type" />
|
|
140
|
+
</SelectTrigger>
|
|
141
|
+
<SelectContent>
|
|
142
|
+
{DEPARTMENT_TYPES.map((t) => (
|
|
143
|
+
<SelectItem key={t} value={t}>{t}</SelectItem>
|
|
144
|
+
))}
|
|
145
|
+
</SelectContent>
|
|
146
|
+
</Select>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div className="space-y-2">
|
|
150
|
+
<Label htmlFor="manager">Department Manager (Head of Department)</Label>
|
|
151
|
+
<Select value={selectedManagerId} onValueChange={(val) => setSelectedManagerId(val)}>
|
|
152
|
+
<SelectTrigger id="manager">
|
|
153
|
+
<SelectValue placeholder="Assign a Department Manager" />
|
|
154
|
+
</SelectTrigger>
|
|
155
|
+
<SelectContent>
|
|
156
|
+
<SelectItem value="none">-- No Manager (Unassigned) --</SelectItem>
|
|
157
|
+
{users.map((u) => (
|
|
158
|
+
<SelectItem key={u.id} value={u.id}>
|
|
159
|
+
{u.fullName || u.email} ({u.email})
|
|
160
|
+
</SelectItem>
|
|
161
|
+
))}
|
|
162
|
+
</SelectContent>
|
|
163
|
+
</Select>
|
|
164
|
+
<p className="text-xs text-muted-foreground">
|
|
165
|
+
You can invite new department heads anytime under Administration → Users, then assign them here.
|
|
166
|
+
</p>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div className="space-y-2">
|
|
170
|
+
<Label htmlFor="status">Operational Status</Label>
|
|
171
|
+
<Select value={selectedStatus} onValueChange={(val) => setSelectedStatus(val)}>
|
|
172
|
+
<SelectTrigger id="status">
|
|
173
|
+
<SelectValue placeholder="Select Status" />
|
|
174
|
+
</SelectTrigger>
|
|
175
|
+
<SelectContent>
|
|
176
|
+
<SelectItem value="active">Active</SelectItem>
|
|
177
|
+
<SelectItem value="inactive">Inactive</SelectItem>
|
|
178
|
+
<SelectItem value="archived">Archived</SelectItem>
|
|
179
|
+
</SelectContent>
|
|
180
|
+
</Select>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div className="flex items-center gap-3 pt-4 border-t">
|
|
184
|
+
<Button type="submit" disabled={saving} className="min-h-[44px]">
|
|
185
|
+
{saving ? (
|
|
186
|
+
<>
|
|
187
|
+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
188
|
+
Saving...
|
|
189
|
+
</>
|
|
190
|
+
) : (
|
|
191
|
+
<>
|
|
192
|
+
<Save className="w-4 h-4 mr-2" />
|
|
193
|
+
Save Changes
|
|
194
|
+
</>
|
|
195
|
+
)}
|
|
196
|
+
</Button>
|
|
197
|
+
<Button variant="outline" asChild className="min-h-[44px]">
|
|
198
|
+
<Link href={`/departments/${resolvedParams.deptId}`}>Cancel</Link>
|
|
199
|
+
</Button>
|
|
200
|
+
</div>
|
|
201
|
+
</form>
|
|
202
|
+
</CardContent>
|
|
203
|
+
</Card>
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getDepartmentById } from "@/lib/actions/departments"
|
|
2
|
+
import { PageHeader } from "@/components/shared/page-header"
|
|
3
|
+
import { StatusBadge } from "@/components/shared/status-badge"
|
|
4
|
+
import { Button } from "@/components/ui/button"
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
6
|
+
import Link from "next/link"
|
|
7
|
+
import { Edit } from "lucide-react"
|
|
8
|
+
|
|
9
|
+
export default async function DepartmentDetailPage({ params }: { params: Promise<{ deptId: string }> }) {
|
|
10
|
+
const { deptId } = await params
|
|
11
|
+
const result = await getDepartmentById(deptId)
|
|
12
|
+
const dept = result?.success ? result.data : null
|
|
13
|
+
|
|
14
|
+
if (!dept) {
|
|
15
|
+
return <div>Department not found</div>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="space-y-6">
|
|
20
|
+
<PageHeader
|
|
21
|
+
title={dept.name}
|
|
22
|
+
description={`Code: ${dept.code} | Hospital: ${(dept as any).hospital?.name || dept.hospitalId}`}
|
|
23
|
+
>
|
|
24
|
+
<Button asChild variant="outline">
|
|
25
|
+
<Link href={`/admin/departments/${dept.id}/edit`}>
|
|
26
|
+
<Edit className="w-4 h-4 mr-2" />
|
|
27
|
+
Edit
|
|
28
|
+
</Link>
|
|
29
|
+
</Button>
|
|
30
|
+
</PageHeader>
|
|
31
|
+
|
|
32
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
33
|
+
<Card>
|
|
34
|
+
<CardHeader><CardTitle>Overview</CardTitle></CardHeader>
|
|
35
|
+
<CardContent className="space-y-4">
|
|
36
|
+
<div>
|
|
37
|
+
<span className="font-medium text-muted-foreground">Status: </span>
|
|
38
|
+
<StatusBadge status={dept.status} />
|
|
39
|
+
</div>
|
|
40
|
+
<div>
|
|
41
|
+
<span className="font-medium text-muted-foreground">Type: </span>
|
|
42
|
+
<span>{dept.departmentType || "N/A"}</span>
|
|
43
|
+
</div>
|
|
44
|
+
<div>
|
|
45
|
+
<span className="font-medium text-muted-foreground">Manager ID: </span>
|
|
46
|
+
<span>{(dept as any).manager?.fullName || dept.managerUserId || "None"}</span>
|
|
47
|
+
</div>
|
|
48
|
+
</CardContent>
|
|
49
|
+
</Card>
|
|
50
|
+
|
|
51
|
+
<Card>
|
|
52
|
+
<CardHeader><CardTitle>Locations & Devices</CardTitle></CardHeader>
|
|
53
|
+
<CardContent className="space-y-4">
|
|
54
|
+
<div>
|
|
55
|
+
<span className="font-medium text-muted-foreground">Linked Devices: </span>
|
|
56
|
+
<span>{(dept as any).linkedDevicesCount || 0}</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div>
|
|
59
|
+
<span className="font-medium text-muted-foreground">Locations: </span>
|
|
60
|
+
<ul className="list-disc pl-5 mt-2">
|
|
61
|
+
{((dept as any).locations)?.map((loc: any) => (
|
|
62
|
+
<li key={loc.id}>{loc.name}</li>
|
|
63
|
+
)) || <li>No locations defined.</li>}
|
|
64
|
+
</ul>
|
|
65
|
+
</div>
|
|
66
|
+
</CardContent>
|
|
67
|
+
</Card>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|