"use client"; import { FormEvent, useMemo, useState } from "react"; import { BedDouble, DoorOpen, History, MoveRight, UserCheck, UserMinus, Wrench } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog } from "@/components/ui/dialog"; import { Textarea } from "@/components/ui/input"; import { Select, type SelectOption } from "@/components/ui/select"; import { Table } from "@/components/ui/table"; import { cn } from "@/lib/utils"; import type { ApiResult } from "@/modules/core/server/api"; import type { Admission, BedStatus, FacilityBed, Permission, Room } from "@/modules/core/types"; import type { Elder } from "@/modules/elders/types"; import type { BedOperationalContext, OperationalContextLine } from "@/modules/operations/types"; type BedsWorkspaceProps = { bedContexts?: Record; initialRooms: Room[]; initialBeds: FacilityBed[]; initialAdmissions: Admission[]; initialElders: Elder[]; permissions: Permission[]; }; type WorkspaceTab = "overview" | "operations" | "history"; type BedDetailTab = "context" | "records" | "actions"; type RoomView = { room: Room; beds: FacilityBed[]; }; const workspaceTabs: Array<{ value: WorkspaceTab; label: string }> = [ { value: "overview", label: "概览" }, { value: "operations", label: "办理" }, { value: "history", label: "记录" }, ]; const bedDetailTabs: Array<{ value: BedDetailTab; label: string }> = [ { value: "context", label: "上下文" }, { value: "records", label: "流水记录" }, { value: "actions", label: "专项办理" }, ]; const bedStatusLabels: Record = { available: "空闲", occupied: "占用", maintenance: "维修", disabled: "停用", }; const admissionStatusLabels: Record = { active: "在住", transferred: "已换床", discharged: "已退住", }; const contextKindLabels: Record = { admission: "入住", care: "照护", health: "健康", incident: "预警", }; function formatDateTime(value: string): string { return new Date(value).toLocaleString("zh-CN"); } function bedBadgeVariant(status: BedStatus): "success" | "secondary" | "warning" | "danger" { switch (status) { case "available": return "success"; case "occupied": return "secondary"; case "maintenance": return "warning"; case "disabled": return "danger"; } } function admissionBedLabel(admission: Admission): string { return `${admission.roomName}-${admission.bedCode}`; } function buildRoomViews(rooms: Room[], beds: FacilityBed[]): RoomView[] { const bedsByRoomId = new Map(); for (const bed of beds) { const roomBeds = bedsByRoomId.get(bed.roomId) ?? []; roomBeds.push(bed); bedsByRoomId.set(bed.roomId, roomBeds); } return [...rooms] .sort((first, second) => first.code.localeCompare(second.code, "zh-CN", { numeric: true })) .map((room) => ({ room, beds: (bedsByRoomId.get(room.id) ?? []).sort((first, second) => first.code.localeCompare(second.code, "zh-CN", { numeric: true }), ), })); } function makeSelectOptions(items: Array<{ id: string; label: string }>, emptyLabel: string): SelectOption[] { return [ { value: "", label: emptyLabel, disabled: true }, ...items.map((item) => ({ value: item.id, label: item.label })), ]; } function bedTileClass(status: BedStatus): string { switch (status) { case "available": return "border-emerald-200 bg-emerald-50 text-emerald-900 hover:border-emerald-400"; case "occupied": return "border-zinc-200 bg-zinc-50 text-zinc-900 hover:border-zinc-400"; case "maintenance": return "border-amber-200 bg-amber-50 text-amber-950 hover:border-amber-400"; case "disabled": return "border-rose-200 bg-rose-50 text-rose-950 hover:border-rose-400"; } } function statusDotClass(status: BedStatus): string { switch (status) { case "available": return "bg-emerald-500"; case "occupied": return "bg-zinc-500"; case "maintenance": return "bg-amber-500"; case "disabled": return "bg-rose-500"; } } export function BedsWorkspace({ bedContexts = {}, initialRooms, initialBeds, initialAdmissions, initialElders, permissions, }: BedsWorkspaceProps): React.ReactElement { const [rooms, setRooms] = useState(initialRooms); const [beds, setBeds] = useState(initialBeds); const [admissions, setAdmissions] = useState(initialAdmissions); const [elders, setElders] = useState(initialElders); const [activeTab, setActiveTab] = useState("overview"); const [message, setMessage] = useState(""); const [isPending, setIsPending] = useState(false); const [admitElderId, setAdmitElderId] = useState(""); const [admitBedId, setAdmitBedId] = useState(""); const [admitNotes, setAdmitNotes] = useState(""); const [transferAdmissionId, setTransferAdmissionId] = useState(""); const [transferBedId, setTransferBedId] = useState(""); const [transferNotes, setTransferNotes] = useState(""); const [dischargeAdmissionId, setDischargeAdmissionId] = useState(""); const [dischargeNotes, setDischargeNotes] = useState(""); const [selectedBedId, setSelectedBedId] = useState(""); const [bedDetailTab, setBedDetailTab] = useState("context"); const canManageAdmissions = permissions.includes("admission:manage"); const activeAdmissions = useMemo(() => admissions.filter((admission) => admission.status === "active"), [admissions]); const activeElderIds = useMemo(() => new Set(activeAdmissions.map((admission) => admission.elderId)), [activeAdmissions]); const availableBeds = useMemo(() => beds.filter((bed) => bed.status === "available"), [beds]); const occupiedBeds = beds.filter((bed) => bed.status === "occupied").length; const maintenanceBeds = beds.filter((bed) => bed.status === "maintenance").length; const occupancyRate = beds.length > 0 ? Math.round((occupiedBeds / beds.length) * 100) : 0; const admissionEligibleElders = elders.filter((elder) => !activeElderIds.has(elder.id)); const roomViews = useMemo(() => buildRoomViews(rooms, beds), [rooms, beds]); const selectedBed = useMemo(() => beds.find((bed) => bed.id === selectedBedId), [beds, selectedBedId]); const selectedAdmission = selectedBed ? activeAdmissions.find((admission) => admission.bedId === selectedBed.id) : undefined; const selectedBedAdmissions = selectedBed ? admissions.filter((admission) => admission.bedId === selectedBed.id) : []; const selectedBedContext = selectedBed ? bedContexts[selectedBed.id] : undefined; const elderOptions = makeSelectOptions( admissionEligibleElders.map((elder) => ({ id: elder.id, label: `${elder.name} / ${elder.phone}` })), "选择老人", ); const bedOptions = makeSelectOptions( availableBeds.map((bed) => ({ id: bed.id, label: `${bed.roomName}-${bed.code}` })), "选择空闲床位", ); const activeAdmissionOptions = makeSelectOptions( activeAdmissions.map((admission) => ({ id: admission.id, label: `${admission.elderName} / ${admissionBedLabel(admission)}`, })), "选择在住记录", ); function openBedDetail(bed: FacilityBed, initialTab: BedDetailTab = "context"): void { setSelectedBedId(bed.id); setBedDetailTab(initialTab); } function startAdmissionFromBed(bed: FacilityBed): void { setAdmitBedId(bed.id); setSelectedBedId(bed.id); setBedDetailTab("actions"); } async function refreshData(): Promise { const [eldersResponse, roomsResponse, bedsResponse, admissionsResponse] = await Promise.all([ fetch("/api/elders"), fetch("/api/facilities/rooms"), fetch("/api/facilities/beds"), fetch("/api/admissions"), ]); const [eldersResult, roomsResult, bedsResult, admissionsResult] = (await Promise.all([ eldersResponse.json(), roomsResponse.json(), bedsResponse.json(), admissionsResponse.json(), ])) as [ ApiResult<{ elders: Elder[] }>, ApiResult<{ rooms: Room[] }>, ApiResult<{ beds: FacilityBed[] }>, ApiResult<{ admissions: Admission[] }>, ]; if (eldersResult.success) { setElders(eldersResult.elders); } if (roomsResult.success) { setRooms(roomsResult.rooms); } if (bedsResult.success) { setBeds(bedsResult.beds); } if (admissionsResult.success) { setAdmissions(admissionsResult.admissions); } } async function submitAdmission(event: FormEvent): Promise { event.preventDefault(); if (!canManageAdmissions) { setMessage("当前角色无权办理入住"); return; } if (!admitElderId || !admitBedId) { setMessage("请选择老人和空闲床位"); return; } setIsPending(true); setMessage(""); const response = await fetch("/api/admissions", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ elderId: admitElderId, bedId: admitBedId, notes: admitNotes }), }); const result = (await response.json()) as ApiResult<{ admission: Admission }>; setIsPending(false); if (!result.success) { setMessage(result.reason); return; } setAdmitElderId(""); setAdmitBedId(""); setAdmitNotes(""); await refreshData(); setMessage(result.reason); } async function submitAdmissionForBed(event: FormEvent, bed: FacilityBed): Promise { event.preventDefault(); if (!canManageAdmissions) { setMessage("当前角色无权办理入住"); return; } if (bed.status !== "available") { setMessage("当前床位不可办理入住"); return; } if (!admitElderId) { setMessage("请选择老人"); return; } setIsPending(true); setMessage(""); const response = await fetch("/api/admissions", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ elderId: admitElderId, bedId: bed.id, notes: admitNotes }), }); const result = (await response.json()) as ApiResult<{ admission: Admission }>; setIsPending(false); if (!result.success) { setMessage(result.reason); return; } setAdmitElderId(""); setAdmitBedId(""); setAdmitNotes(""); await refreshData(); setMessage(result.reason); } async function submitTransfer(event: FormEvent): Promise { event.preventDefault(); const admission = activeAdmissions.find((item) => item.id === transferAdmissionId); if (!canManageAdmissions) { setMessage("当前角色无权办理换床"); return; } if (!admission || !transferBedId) { setMessage("请选择在住记录和新床位"); return; } setIsPending(true); setMessage(""); const response = await fetch("/api/admissions", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ elderId: admission.elderId, bedId: transferBedId, notes: transferNotes || "办理换床" }), }); const result = (await response.json()) as ApiResult<{ admission: Admission }>; setIsPending(false); if (!result.success) { setMessage(result.reason); return; } setTransferAdmissionId(""); setTransferBedId(""); setTransferNotes(""); await refreshData(); setMessage("换床已办理"); } async function submitTransferForAdmission(event: FormEvent, admission: Admission): Promise { event.preventDefault(); if (!canManageAdmissions) { setMessage("当前角色无权办理换床"); return; } if (!transferBedId) { setMessage("请选择新床位"); return; } setIsPending(true); setMessage(""); const response = await fetch("/api/admissions", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ elderId: admission.elderId, bedId: transferBedId, notes: transferNotes || "办理换床" }), }); const result = (await response.json()) as ApiResult<{ admission: Admission }>; setIsPending(false); if (!result.success) { setMessage(result.reason); return; } setTransferAdmissionId(""); setTransferBedId(""); setTransferNotes(""); await refreshData(); setMessage("换床已办理"); } async function submitDischarge(event: FormEvent): Promise { event.preventDefault(); if (!canManageAdmissions) { setMessage("当前角色无权办理退住"); return; } if (!dischargeAdmissionId) { setMessage("请选择在住记录"); return; } setIsPending(true); setMessage(""); const response = await fetch(`/api/admissions/${dischargeAdmissionId}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ notes: dischargeNotes }), }); const result = (await response.json()) as ApiResult<{ admission: Admission }>; setIsPending(false); if (!result.success) { setMessage(result.reason); return; } setDischargeAdmissionId(""); setDischargeNotes(""); await refreshData(); setMessage(result.reason); } async function submitDischargeForAdmission(event: FormEvent, admission: Admission): Promise { event.preventDefault(); if (!canManageAdmissions) { setMessage("当前角色无权办理退住"); return; } setIsPending(true); setMessage(""); const response = await fetch(`/api/admissions/${admission.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ notes: dischargeNotes }), }); const result = (await response.json()) as ApiResult<{ admission: Admission }>; setIsPending(false); if (!result.success) { setMessage(result.reason); return; } setDischargeAdmissionId(""); setDischargeNotes(""); await refreshData(); setMessage(result.reason); } return (
{workspaceTabs.map((tab) => ( ))}
{message ? (

{message}

) : null} {activeTab === "overview" ? ( <>
房间 {rooms.length}
床位 {beds.length}

占用率 {occupancyRate}%

0 ? "border-amber-200 bg-amber-50/70" : undefined}>
0 ? "text-amber-700" : undefined}>维修床位 0 ? "mt-2 text-3xl text-amber-700" : "mt-2 text-3xl"}> {maintenanceBeds}
0 ? "size-5 text-amber-700" : "size-5 text-primary"} aria-hidden="true" />
在住 {activeAdmissions.length}
房间格子视图 按房间查看床位占用、维修、停用和待入住空床。 {roomViews.length > 0 ? (
{roomViews.map((view) => ( ))}
) : (

暂无房间主数据

)}
快速办理 空床格子可直接进入入住办理。
void submitAdmission(event)}>