636 lines
25 KiB
TypeScript
636 lines
25 KiB
TypeScript
"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 { Textarea } from "@/components/ui/input";
|
|
import { Select, type SelectOption } from "@/components/ui/select";
|
|
import { Table } from "@/components/ui/table";
|
|
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<string, BedOperationalContext>;
|
|
initialRooms: Room[];
|
|
initialBeds: FacilityBed[];
|
|
initialAdmissions: Admission[];
|
|
initialElders: Elder[];
|
|
permissions: Permission[];
|
|
};
|
|
|
|
type WorkspaceTab = "overview" | "operations" | "history";
|
|
|
|
const workspaceTabs: Array<{ value: WorkspaceTab; label: string }> = [
|
|
{ value: "overview", label: "概览" },
|
|
{ value: "operations", label: "办理" },
|
|
{ value: "history", label: "记录" },
|
|
];
|
|
|
|
const bedStatusLabels: Record<BedStatus, string> = {
|
|
available: "空闲",
|
|
occupied: "占用",
|
|
maintenance: "维修",
|
|
disabled: "停用",
|
|
};
|
|
|
|
const admissionStatusLabels: Record<Admission["status"], string> = {
|
|
active: "在住",
|
|
transferred: "已换床",
|
|
discharged: "已退住",
|
|
};
|
|
|
|
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 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 })),
|
|
];
|
|
}
|
|
|
|
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<WorkspaceTab>("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 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 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)}`,
|
|
})),
|
|
"选择在住记录",
|
|
);
|
|
|
|
async function refreshData(): Promise<void> {
|
|
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<HTMLFormElement>): Promise<void> {
|
|
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 submitTransfer(event: FormEvent<HTMLFormElement>): Promise<void> {
|
|
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 submitDischarge(event: FormEvent<HTMLFormElement>): Promise<void> {
|
|
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);
|
|
}
|
|
|
|
return (
|
|
<div className="mx-auto flex w-full max-w-7xl flex-col gap-5">
|
|
<section className="flex border-b pb-4">
|
|
<div className="flex flex-wrap gap-2" role="tablist" aria-label="床位工作区">
|
|
{workspaceTabs.map((tab) => (
|
|
<Button
|
|
aria-selected={activeTab === tab.value}
|
|
key={tab.value}
|
|
onClick={() => setActiveTab(tab.value)}
|
|
role="tab"
|
|
type="button"
|
|
variant={activeTab === tab.value ? "default" : "outline"}
|
|
>
|
|
{tab.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{message ? (
|
|
<p className="rounded-md bg-secondary px-3 py-2 text-sm text-secondary-foreground" role="status">
|
|
{message}
|
|
</p>
|
|
) : null}
|
|
|
|
{activeTab === "overview" ? (
|
|
<>
|
|
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
|
<div>
|
|
<CardDescription>房间</CardDescription>
|
|
<CardTitle className="mt-2 text-3xl">{rooms.length}</CardTitle>
|
|
</div>
|
|
<DoorOpen className="size-5 text-primary" aria-hidden="true" />
|
|
</CardHeader>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
|
<div>
|
|
<CardDescription>床位</CardDescription>
|
|
<CardTitle className="mt-2 text-3xl">{beds.length}</CardTitle>
|
|
</div>
|
|
<BedDouble className="size-5 text-primary" aria-hidden="true" />
|
|
</CardHeader>
|
|
<CardContent className="pt-0">
|
|
<p className="text-sm text-muted-foreground">占用率 {occupancyRate}%</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className={maintenanceBeds > 0 ? "border-amber-200 bg-amber-50/70" : undefined}>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
|
<div>
|
|
<CardDescription className={maintenanceBeds > 0 ? "text-amber-700" : undefined}>维修床位</CardDescription>
|
|
<CardTitle className={maintenanceBeds > 0 ? "mt-2 text-3xl text-amber-700" : "mt-2 text-3xl"}>
|
|
{maintenanceBeds}
|
|
</CardTitle>
|
|
</div>
|
|
<Wrench className={maintenanceBeds > 0 ? "size-5 text-amber-700" : "size-5 text-primary"} aria-hidden="true" />
|
|
</CardHeader>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
|
<div>
|
|
<CardDescription>在住</CardDescription>
|
|
<CardTitle className="mt-2 text-3xl">{activeAdmissions.length}</CardTitle>
|
|
</div>
|
|
<History className="size-5 text-primary" aria-hidden="true" />
|
|
</CardHeader>
|
|
</Card>
|
|
</section>
|
|
|
|
<section className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>房间台账</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="overflow-x-auto rounded-md border">
|
|
<Table className="w-full min-w-[620px] text-sm">
|
|
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
|
|
<Table.Row>
|
|
<Table.Head className="px-4 py-3 font-medium">房间</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">编号</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">容量</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">床位</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">状态</Table.Head>
|
|
</Table.Row>
|
|
</Table.Header>
|
|
<Table.Body className="divide-y bg-card">
|
|
{rooms.map((room) => (
|
|
<Table.Row key={room.id}>
|
|
<Table.Cell className="px-4 py-3 font-medium">{room.name}</Table.Cell>
|
|
<Table.Cell className="px-4 py-3 text-muted-foreground">{room.code}</Table.Cell>
|
|
<Table.Cell className="px-4 py-3">{room.capacity}</Table.Cell>
|
|
<Table.Cell className="px-4 py-3">
|
|
{room.occupiedBedCount}/{room.bedCount}
|
|
</Table.Cell>
|
|
<Table.Cell className="px-4 py-3">
|
|
<Badge variant={room.status === "active" ? "success" : "danger"}>{room.status}</Badge>
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
))}
|
|
{rooms.length === 0 ? (
|
|
<Table.Row>
|
|
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
|
|
暂无房间主数据
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
) : null}
|
|
</Table.Body>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>床位状态</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="overflow-x-auto rounded-md border">
|
|
<Table className="w-full min-w-[700px] text-sm">
|
|
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
|
|
<Table.Row>
|
|
<Table.Head className="px-4 py-3 font-medium">房间</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">床位</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">状态</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">当前老人</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">入住上下文</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">备注</Table.Head>
|
|
</Table.Row>
|
|
</Table.Header>
|
|
<Table.Body className="divide-y bg-card">
|
|
{beds.map((bed) => {
|
|
const context = bedContexts[bed.id];
|
|
return (
|
|
<Table.Row key={bed.id}>
|
|
<Table.Cell className="px-4 py-3">{bed.roomName}</Table.Cell>
|
|
<Table.Cell className="px-4 py-3 font-medium">{bed.code}</Table.Cell>
|
|
<Table.Cell className="px-4 py-3">
|
|
<Badge variant={bedBadgeVariant(bed.status)}>{bedStatusLabels[bed.status]}</Badge>
|
|
</Table.Cell>
|
|
<Table.Cell className="px-4 py-3">
|
|
<OccupantSummary context={context} fallbackName={bed.currentElderName} />
|
|
</Table.Cell>
|
|
<Table.Cell className="px-4 py-3">
|
|
<ContextLines lines={context?.lines ?? []} />
|
|
</Table.Cell>
|
|
<Table.Cell className="px-4 py-3 text-muted-foreground">{bed.notes || "-"}</Table.Cell>
|
|
</Table.Row>
|
|
);
|
|
})}
|
|
{beds.length === 0 ? (
|
|
<Table.Row>
|
|
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
|
|
暂无床位主数据
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
) : null}
|
|
</Table.Body>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
</>
|
|
) : null}
|
|
|
|
{activeTab === "operations" ? (
|
|
<section className="grid gap-4 xl:grid-cols-3">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<UserCheck className="size-5 text-primary" aria-hidden="true" />
|
|
办理入住
|
|
</CardTitle>
|
|
<CardDescription>选择未在住老人和空闲床位。</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form className="grid gap-3" onSubmit={(event) => void submitAdmission(event)}>
|
|
<Select
|
|
aria-label="入住老人"
|
|
disabled={!canManageAdmissions || isPending}
|
|
onValueChange={setAdmitElderId}
|
|
options={elderOptions}
|
|
value={admitElderId}
|
|
/>
|
|
<Select
|
|
aria-label="入住房间床位"
|
|
disabled={!canManageAdmissions || isPending}
|
|
onValueChange={setAdmitBedId}
|
|
options={bedOptions}
|
|
value={admitBedId}
|
|
/>
|
|
<Textarea
|
|
aria-label="入住备注"
|
|
disabled={!canManageAdmissions || isPending}
|
|
onChange={(event) => setAdmitNotes(event.target.value)}
|
|
placeholder="备注"
|
|
value={admitNotes}
|
|
/>
|
|
<Button disabled={!canManageAdmissions || isPending} type="submit">
|
|
<UserCheck className="size-4" aria-hidden="true" />
|
|
办理入住
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<MoveRight className="size-5 text-primary" aria-hidden="true" />
|
|
办理换床
|
|
</CardTitle>
|
|
<CardDescription>换床会释放原床位并占用新床位。</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form className="grid gap-3" onSubmit={(event) => void submitTransfer(event)}>
|
|
<Select
|
|
aria-label="换床入住记录"
|
|
disabled={!canManageAdmissions || isPending}
|
|
onValueChange={setTransferAdmissionId}
|
|
options={activeAdmissionOptions}
|
|
value={transferAdmissionId}
|
|
/>
|
|
<Select
|
|
aria-label="换床新床位"
|
|
disabled={!canManageAdmissions || isPending}
|
|
onValueChange={setTransferBedId}
|
|
options={bedOptions}
|
|
value={transferBedId}
|
|
/>
|
|
<Textarea
|
|
aria-label="换床备注"
|
|
disabled={!canManageAdmissions || isPending}
|
|
onChange={(event) => setTransferNotes(event.target.value)}
|
|
placeholder="备注"
|
|
value={transferNotes}
|
|
/>
|
|
<Button disabled={!canManageAdmissions || isPending} type="submit" variant="outline">
|
|
<MoveRight className="size-4" aria-hidden="true" />
|
|
办理换床
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<UserMinus className="size-5 text-primary" aria-hidden="true" />
|
|
办理退住
|
|
</CardTitle>
|
|
<CardDescription>退住会关闭入住记录并释放床位。</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form className="grid gap-3" onSubmit={(event) => void submitDischarge(event)}>
|
|
<Select
|
|
aria-label="退住入住记录"
|
|
disabled={!canManageAdmissions || isPending}
|
|
onValueChange={setDischargeAdmissionId}
|
|
options={activeAdmissionOptions}
|
|
value={dischargeAdmissionId}
|
|
/>
|
|
<Textarea
|
|
aria-label="退住备注"
|
|
disabled={!canManageAdmissions || isPending}
|
|
onChange={(event) => setDischargeNotes(event.target.value)}
|
|
placeholder="备注"
|
|
value={dischargeNotes}
|
|
/>
|
|
<Button disabled={!canManageAdmissions || isPending} type="submit" variant="destructive">
|
|
<UserMinus className="size-4" aria-hidden="true" />
|
|
办理退住
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
) : null}
|
|
|
|
{activeTab === "history" ? (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>入住、换床与退住记录</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="overflow-x-auto rounded-md border">
|
|
<Table className="w-full min-w-[820px] text-sm">
|
|
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
|
|
<Table.Row>
|
|
<Table.Head className="px-4 py-3 font-medium">老人</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">床位</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">状态</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">入住时间</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">结束时间</Table.Head>
|
|
<Table.Head className="px-4 py-3 font-medium">备注</Table.Head>
|
|
</Table.Row>
|
|
</Table.Header>
|
|
<Table.Body className="divide-y bg-card">
|
|
{admissions.map((admission) => (
|
|
<Table.Row key={admission.id}>
|
|
<Table.Cell className="px-4 py-3 font-medium">{admission.elderName}</Table.Cell>
|
|
<Table.Cell className="px-4 py-3">{admissionBedLabel(admission)}</Table.Cell>
|
|
<Table.Cell className="px-4 py-3">
|
|
<Badge variant={admission.status === "active" ? "success" : "secondary"}>
|
|
{admissionStatusLabels[admission.status]}
|
|
</Badge>
|
|
</Table.Cell>
|
|
<Table.Cell className="px-4 py-3 text-muted-foreground">{formatDateTime(admission.admittedAt)}</Table.Cell>
|
|
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
|
{admission.dischargedAt ? formatDateTime(admission.dischargedAt) : "-"}
|
|
</Table.Cell>
|
|
<Table.Cell className="px-4 py-3 text-muted-foreground">{admission.notes || "-"}</Table.Cell>
|
|
</Table.Row>
|
|
))}
|
|
{admissions.length === 0 ? (
|
|
<Table.Row>
|
|
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
|
|
暂无入住记录
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
) : null}
|
|
</Table.Body>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function contextToneClass(tone: OperationalContextLine["tone"]): string {
|
|
if (tone === "danger") {
|
|
return "text-destructive";
|
|
}
|
|
if (tone === "warning") {
|
|
return "text-amber-700";
|
|
}
|
|
return "text-muted-foreground";
|
|
}
|
|
|
|
function ContextLines({ lines }: { lines: OperationalContextLine[] }): React.ReactElement {
|
|
if (lines.length === 0) {
|
|
return <span className="text-muted-foreground">-</span>;
|
|
}
|
|
|
|
return (
|
|
<div className="grid gap-1">
|
|
{lines.map((line) => (
|
|
<p className={`max-w-[260px] truncate text-xs ${contextToneClass(line.tone)}`} key={`${line.kind}-${line.label}-${line.value}`}>
|
|
<span className="font-medium">{line.label}</span> {line.value}
|
|
</p>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function OccupantSummary({
|
|
context,
|
|
fallbackName,
|
|
}: {
|
|
context: BedOperationalContext | undefined;
|
|
fallbackName: string | undefined;
|
|
}): React.ReactElement {
|
|
if (!context?.occupant) {
|
|
return <span className="text-muted-foreground">{fallbackName ?? "-"}</span>;
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<p className="font-medium">{context.occupant.elderName}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{context.occupant.careLevel} / {context.occupant.status}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|