@@ -6,9 +6,11 @@ import { BedDouble, DoorOpen, History, MoveRight, UserCheck, UserMinus, Wrench }
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" ;
@@ -24,6 +26,11 @@ type BedsWorkspaceProps = {
} ;
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 : "概览" } ,
@@ -31,6 +38,12 @@ const workspaceTabs: Array<{ value: WorkspaceTab; label: string }> = [
{ value : "history" , label : "记录" } ,
] ;
const bedDetailTabs : Array < { value : BedDetailTab ; label : string } > = [
{ value : "context" , label : "上下文" } ,
{ value : "records" , label : "流水记录" } ,
{ value : "actions" , label : "专项办理" } ,
] ;
const bedStatusLabels : Record < BedStatus , string > = {
available : "空闲" ,
occupied : "占用" ,
@@ -44,6 +57,13 @@ const admissionStatusLabels: Record<Admission["status"], string> = {
discharged : "已退住" ,
} ;
const contextKindLabels : Record < OperationalContextLine [ "kind" ] , string > = {
admission : "入住" ,
care : "照护" ,
health : "健康" ,
incident : "预警" ,
} ;
function formatDateTime ( value : string ) : string {
return new Date ( value ) . toLocaleString ( "zh-CN" ) ;
}
@@ -65,6 +85,24 @@ function admissionBedLabel(admission: Admission): string {
return ` ${ admission . roomName } - ${ admission . bedCode } ` ;
}
function buildRoomViews ( rooms : Room [ ] , beds : FacilityBed [ ] ) : RoomView [ ] {
const bedsByRoomId = new Map < string , FacilityBed [ ] > ( ) ;
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 } ,
@@ -72,6 +110,32 @@ function makeSelectOptions(items: Array<{ id: string; label: string }>, emptyLab
] ;
}
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 ,
@@ -95,6 +159,8 @@ export function BedsWorkspace({
const [ transferNotes , setTransferNotes ] = useState ( "" ) ;
const [ dischargeAdmissionId , setDischargeAdmissionId ] = useState ( "" ) ;
const [ dischargeNotes , setDischargeNotes ] = useState ( "" ) ;
const [ selectedBedId , setSelectedBedId ] = useState ( "" ) ;
const [ bedDetailTab , setBedDetailTab ] = useState < BedDetailTab > ( "context" ) ;
const canManageAdmissions = permissions . includes ( "admission:manage" ) ;
const activeAdmissions = useMemo ( ( ) = > admissions . filter ( ( admission ) = > admission . status === "active" ) , [ admissions ] ) ;
@@ -104,6 +170,15 @@ export function BedsWorkspace({
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 } ` } ) ) ,
@@ -121,6 +196,17 @@ export function BedsWorkspace({
"选择在住记录" ,
) ;
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 < void > {
const [ eldersResponse , roomsResponse , bedsResponse , admissionsResponse ] = await Promise . all ( [
fetch ( "/api/elders" ) ,
@@ -187,6 +273,43 @@ export function BedsWorkspace({
setMessage ( result . reason ) ;
}
async function submitAdmissionForBed ( event : FormEvent < HTMLFormElement > , bed : FacilityBed ) : Promise < void > {
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 < HTMLFormElement > ) : Promise < void > {
event . preventDefault ( ) ;
const admission = activeAdmissions . find ( ( item ) = > item . id === transferAdmissionId ) ;
@@ -221,6 +344,39 @@ export function BedsWorkspace({
setMessage ( "换床已办理" ) ;
}
async function submitTransferForAdmission ( event : FormEvent < HTMLFormElement > , admission : Admission ) : Promise < void > {
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 < HTMLFormElement > ) : Promise < void > {
event . preventDefault ( ) ;
if ( ! canManageAdmissions ) {
@@ -253,6 +409,34 @@ export function BedsWorkspace({
setMessage ( result . reason ) ;
}
async function submitDischargeForAdmission ( event : FormEvent < HTMLFormElement > , admission : Admission ) : Promise < void > {
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 (
< div className = "mx-auto flex w-full max-w-7xl flex-col gap-5" >
< section className = "flex border-b pb-4" >
@@ -324,97 +508,65 @@ export function BedsWorkspace({
< / Card >
< / section >
< section className = "grid gap-4 xl:grid-cols-[0.9fr_1.1fr ]" >
< section className = "grid gap-4 xl:grid-cols-[minmax(0,1fr)_20rem ]" >
< Card >
< CardHeader >
< CardTitle > 房 间 台 账 < / CardTitle >
< CardTitle > 房 间 格 子 视 图 < / CardTitle >
< CardDescription > 按 房 间 查 看 床 位 占 用 、 维 修 、 停 用 和 待 入 住 空 床 。 < / CardDescription >
< / 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.Hea d>
< Table.Head className = "px-4 py-3 font-medium" > 床 位 < / Table.Head >
< Table.Head className = "px-4 py-3 font-medium" > 状 态 < / Table.Hea d>
< / Table.Ro w>
< / 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 className = "grid gap-4" >
< StatusLegend / >
{ roomViews . length > 0 ? (
< div className = "grid gap-4 lg:grid-cols-2 " >
{ roomViews . map ( ( view ) = > (
< RoomGrid
bedContexts = { bedContexts }
key = { view . room . i d}
onSelectBed = { openBedDetail }
onStartAdmission = { startAdmissionFromBe d}
roomView = { vie w }
/ >
) ) }
< / div >
) : (
< p className = "rounded-md border border-dashed px-4 py-8 text-center text-sm text-muted-foreground" > 暂 无 房 间 主 数 据 < / p >
) }
< / CardContent >
< / Card >
< Card >
< CardHeader >
< CardTitle > 床 位 状 态 < / CardTitle >
< CardTitle > 快 速 办 理 < / CardTitle >
< CardDescription > 空 床 格 子 可 直 接 进 入 入 住 办 理 。 < / CardDescription >
< / 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.Hea d >
< Table.Head className = "px-4 py-3 font-medium" > 床 位 < / Table.Head >
< Table.Head className = "px-4 py-3 font-medium" > 状 态 < / Table.Hea d >
< 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 >
< form className = "grid gap-3" onSubmit = { ( event ) = > void submitAdmission ( event ) } >
< Select
aria-label = "入住老人"
disabled = { ! canManageAdmissions || isPending }
onValueChange = { setAdmitElderI d}
options = { elderOptions }
value = { admitElderI d}
/ >
< 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 >
< / section >
@@ -583,6 +735,435 @@ export function BedsWorkspace({
< / CardContent >
< / Card >
) : null }
< BedDetailDialog
activeAdmission = { selectedAdmission }
activeTab = { bedDetailTab }
admissions = { selectedBedAdmissions }
bed = { selectedBed }
bedOptions = { bedOptions }
canManageAdmissions = { canManageAdmissions }
context = { selectedBedContext }
dischargeNotes = { dischargeNotes }
elderOptions = { elderOptions }
isPending = { isPending }
notes = { admitNotes }
onClose = { ( ) = > setSelectedBedId ( "" ) }
onDischargeNotesChange = { setDischargeNotes }
onElderChange = { setAdmitElderId }
onNotesChange = { setAdmitNotes }
onSubmitAdmission = { submitAdmissionForBed }
onSubmitDischarge = { submitDischargeForAdmission }
onSubmitTransfer = { submitTransferForAdmission }
onTabChange = { setBedDetailTab }
onTransferBedChange = { setTransferBedId }
onTransferNotesChange = { setTransferNotes }
selectedElderId = { admitElderId }
selectedTransferBedId = { transferBedId }
transferNotes = { transferNotes }
/ >
< / div >
) ;
}
function StatusLegend ( ) : React . ReactElement {
return (
< div className = "flex flex-wrap gap-3 text-xs text-muted-foreground" aria-label = "床位状态图例" >
{ ( [ "available" , "occupied" , "maintenance" , "disabled" ] as BedStatus [ ] ) . map ( ( status ) = > (
< span className = "inline-flex items-center gap-2" key = { status } >
< span className = { cn ( "size-2 rounded-full" , statusDotClass ( status ) ) } aria-hidden = "true" / >
{ bedStatusLabels [ status ] }
< / span >
) ) }
< / div >
) ;
}
function RoomGrid ( {
bedContexts ,
onSelectBed ,
onStartAdmission ,
roomView ,
} : {
bedContexts : Record < string , BedOperationalContext > ;
onSelectBed : ( bed : FacilityBed , initialTab? : BedDetailTab ) = > void ;
onStartAdmission : ( bed : FacilityBed ) = > void ;
roomView : RoomView ;
} ) : React . ReactElement {
const availableCount = roomView . beds . filter ( ( bed ) = > bed . status === "available" ) . length ;
const maintenanceCount = roomView . beds . filter ( ( bed ) = > bed . status === "maintenance" ) . length ;
return (
< article className = "rounded-lg border border-kumo-line bg-kumo-base p-4" >
< header className = "flex items-start justify-between gap-3" >
< div className = "min-w-0" >
< h3 className = "truncate text-sm font-semibold text-kumo-strong" > { roomView . room . name } < / h3 >
< p className = "mt-1 text-xs text-muted-foreground" >
{ roomView . room . code } · { roomView . room . occupiedBedCount } / { roomView . room . bedCount } 在 住 · 容 量 { roomView . room . capacity }
< / p >
< / div >
< Badge variant = { roomView . room . status === "active" ? "success" : "danger" } >
{ roomView . room . status === "active" ? "启用" : "停用" }
< / Badge >
< / header >
< div className = "mt-4 grid grid-cols-2 gap-2 sm:grid-cols-3" >
{ roomView . beds . map ( ( bed ) = > (
< BedTile
bed = { bed }
context = { bedContexts [ bed . id ] }
key = { bed . id }
onSelect = { ( ) = > onSelectBed ( bed ) }
onStartAdmission = { ( ) = > onStartAdmission ( bed ) }
/ >
) ) }
{ roomView . beds . length === 0 ? (
< p className = "col-span-full rounded-md border border-dashed px-3 py-5 text-center text-xs text-muted-foreground" > 暂 无 床 位 < / p >
) : null }
< / div >
< footer className = "mt-4 flex flex-wrap gap-2 text-xs text-muted-foreground" >
< span > { availableCount } 空 闲 < / span >
< span > { maintenanceCount } 维 修 < / span >
< / footer >
< / article >
) ;
}
function BedTile ( {
bed ,
context ,
onSelect ,
onStartAdmission ,
} : {
bed : FacilityBed ;
context : BedOperationalContext | undefined ;
onSelect : ( ) = > void ;
onStartAdmission : ( ) = > void ;
} ) : React . ReactElement {
const occupantName = context ? . occupant ? . elderName ? ? bed . currentElderName ;
const bedActionLabel = bed . status === "available" ? "可办理入住" : bedStatusLabels [ bed . status ] ;
return (
< button
className = { cn (
"grid min-h-28 content-between rounded-md border p-3 text-left transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary" ,
bedTileClass ( bed . status ) ,
) }
onClick = { bed . status === "available" ? onStartAdmission : onSelect }
type = "button"
>
< span className = "flex items-start justify-between gap-2" >
< span className = "min-w-0" >
< span className = "block truncate text-sm font-semibold" > { bed . code } < / span >
< span className = "mt-1 block text-xs opacity-75" > { bedStatusLabels [ bed . status ] } < / span >
< / span >
< span className = { cn ( "mt-1 size-2 shrink-0 rounded-full" , statusDotClass ( bed . status ) ) } aria-hidden = "true" / >
< / span >
< span className = "min-w-0" >
< span className = "block truncate text-xs font-medium" > { occupantName ? ? bedActionLabel } < / span >
< span className = "mt-1 block truncate text-[11px] opacity-75" > { bed . notes || "无备注" } < / span >
< / span >
< / button >
) ;
}
function BedDetailDialog ( {
activeAdmission ,
activeTab ,
admissions ,
bed ,
bedOptions ,
canManageAdmissions ,
context ,
dischargeNotes ,
elderOptions ,
isPending ,
notes ,
onClose ,
onDischargeNotesChange ,
onElderChange ,
onNotesChange ,
onSubmitAdmission ,
onSubmitDischarge ,
onSubmitTransfer ,
onTabChange ,
onTransferBedChange ,
onTransferNotesChange ,
selectedElderId ,
selectedTransferBedId ,
transferNotes ,
} : {
activeAdmission : Admission | undefined ;
activeTab : BedDetailTab ;
admissions : Admission [ ] ;
bed : FacilityBed | undefined ;
bedOptions : SelectOption [ ] ;
canManageAdmissions : boolean ;
context : BedOperationalContext | undefined ;
dischargeNotes : string ;
elderOptions : SelectOption [ ] ;
isPending : boolean ;
notes : string ;
onClose : ( ) = > void ;
onDischargeNotesChange : ( value : string ) = > void ;
onElderChange : ( value : string ) = > void ;
onNotesChange : ( value : string ) = > void ;
onSubmitAdmission : ( event : FormEvent < HTMLFormElement > , bed : FacilityBed ) = > Promise < void > ;
onSubmitDischarge : ( event : FormEvent < HTMLFormElement > , admission : Admission ) = > Promise < void > ;
onSubmitTransfer : ( event : FormEvent < HTMLFormElement > , admission : Admission ) = > Promise < void > ;
onTabChange : ( value : BedDetailTab ) = > void ;
onTransferBedChange : ( value : string ) = > void ;
onTransferNotesChange : ( value : string ) = > void ;
selectedElderId : string ;
selectedTransferBedId : string ;
transferNotes : string ;
} ) : React . ReactElement {
const title = bed ? ` ${ bed . roomName } · ${ bed . code } ` : "床位详情" ;
const description = bed ? ` ${ bedStatusLabels [ bed . status ] } · ${ bed . notes || "无备注" } ` : undefined ;
return (
< Dialog description = { description } onClose = { onClose } open = { Boolean ( bed ) } title = { title } width = "wide" >
{ bed ? (
< div className = "grid gap-5" >
< div className = "flex flex-wrap gap-2" role = "tablist" aria-label = "床位详情" >
{ bedDetailTabs . map ( ( tab ) = > (
< Button
aria-selected = { activeTab === tab . value }
key = { tab . value }
onClick = { ( ) = > onTabChange ( tab . value ) }
role = "tab"
size = "sm"
type = "button"
variant = { activeTab === tab . value ? "default" : "outline" }
>
{ tab . label }
< / Button >
) ) }
< / div >
{ activeTab === "context" ? < BedContextPanel bed = { bed } context = { context } / > : null }
{ activeTab === "records" ? < AdmissionRecords admissions = { admissions } / > : null }
{ activeTab === "actions" ? (
< BedActionsPanel
activeAdmission = { activeAdmission }
bed = { bed }
bedOptions = { bedOptions }
canManageAdmissions = { canManageAdmissions }
dischargeNotes = { dischargeNotes }
elderOptions = { elderOptions }
isPending = { isPending }
notes = { notes }
onDischargeNotesChange = { onDischargeNotesChange }
onElderChange = { onElderChange }
onNotesChange = { onNotesChange }
onSubmitAdmission = { onSubmitAdmission }
onSubmitDischarge = { onSubmitDischarge }
onSubmitTransfer = { onSubmitTransfer }
onTransferBedChange = { onTransferBedChange }
onTransferNotesChange = { onTransferNotesChange }
selectedElderId = { selectedElderId }
selectedTransferBedId = { selectedTransferBedId }
transferNotes = { transferNotes }
/ >
) : null }
< / div >
) : null }
< / Dialog >
) ;
}
function BedContextPanel ( {
bed ,
context ,
} : {
bed : FacilityBed ;
context : BedOperationalContext | undefined ;
} ) : React . ReactElement {
return (
< div className = "grid gap-4 lg:grid-cols-[18rem_minmax(0,1fr)]" >
< section className = "rounded-lg border border-kumo-line p-4" >
< p className = "text-xs text-muted-foreground" > 当 前 老 人 < / p >
< div className = "mt-3" >
< OccupantSummary context = { context } fallbackName = { bed . currentElderName } / >
< / div >
< dl className = "mt-4 grid gap-3 text-sm" >
< div >
< dt className = "text-xs text-muted-foreground" > 状 态 < / dt >
< dd className = "mt-1" >
< Badge variant = { bedBadgeVariant ( bed . status ) } > { bedStatusLabels [ bed . status ] } < / Badge >
< / dd >
< / div >
< div >
< dt className = "text-xs text-muted-foreground" > 备 注 < / dt >
< dd className = "mt-1 text-kumo-strong" > { bed . notes || "-" } < / dd >
< / div >
< / dl >
< / section >
< section className = "rounded-lg border border-kumo-line p-4" >
< h3 className = "text-sm font-semibold text-kumo-strong" > 运 营 上 下 文 < / h3 >
< div className = "mt-3" >
< ContextLines lines = { context ? . lines ? ? [ ] } expanded / >
< / div >
< / section >
< / div >
) ;
}
function AdmissionRecords ( { admissions } : { admissions : Admission [ ] } ) : React . ReactElement {
return (
< div className = "overflow-x-auto rounded-md border" >
< Table className = "w-full min-w-[720px] 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" >
{ 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" >
< 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 = { 5 } >
暂 无 该 床 位 流 水 记 录
< / Table.Cell >
< / Table.Row >
) : null }
< / Table.Body >
< / Table >
< / div >
) ;
}
function BedActionsPanel ( {
activeAdmission ,
bed ,
bedOptions ,
canManageAdmissions ,
dischargeNotes ,
elderOptions ,
isPending ,
notes ,
onDischargeNotesChange ,
onElderChange ,
onNotesChange ,
onSubmitAdmission ,
onSubmitDischarge ,
onSubmitTransfer ,
onTransferBedChange ,
onTransferNotesChange ,
selectedElderId ,
selectedTransferBedId ,
transferNotes ,
} : {
activeAdmission : Admission | undefined ;
bed : FacilityBed ;
bedOptions : SelectOption [ ] ;
canManageAdmissions : boolean ;
dischargeNotes : string ;
elderOptions : SelectOption [ ] ;
isPending : boolean ;
notes : string ;
onDischargeNotesChange : ( value : string ) = > void ;
onElderChange : ( value : string ) = > void ;
onNotesChange : ( value : string ) = > void ;
onSubmitAdmission : ( event : FormEvent < HTMLFormElement > , bed : FacilityBed ) = > Promise < void > ;
onSubmitDischarge : ( event : FormEvent < HTMLFormElement > , admission : Admission ) = > Promise < void > ;
onSubmitTransfer : ( event : FormEvent < HTMLFormElement > , admission : Admission ) = > Promise < void > ;
onTransferBedChange : ( value : string ) = > void ;
onTransferNotesChange : ( value : string ) = > void ;
selectedElderId : string ;
selectedTransferBedId : string ;
transferNotes : string ;
} ) : React . ReactElement {
if ( bed . status === "available" ) {
return (
< form className = "grid gap-3 rounded-lg border border-kumo-line p-4" onSubmit = { ( event ) = > void onSubmitAdmission ( event , bed ) } >
< h3 className = "text-sm font-semibold text-kumo-strong" > 办 理 入 住 < / h3 >
< Select
aria-label = "入住老人"
disabled = { ! canManageAdmissions || isPending }
onValueChange = { onElderChange }
options = { elderOptions }
value = { selectedElderId }
/ >
< Textarea
aria-label = "入住备注"
disabled = { ! canManageAdmissions || isPending }
onChange = { ( event ) = > onNotesChange ( event . target . value ) }
placeholder = "备注"
value = { notes }
/ >
< Button disabled = { ! canManageAdmissions || isPending } type = "submit" >
< UserCheck className = "size-4" aria-hidden = "true" / >
办 理 入 住
< / Button >
< / form >
) ;
}
if ( ! activeAdmission ) {
return < p className = "rounded-md border border-dashed px-4 py-8 text-center text-sm text-muted-foreground" > 该 床 位 当 前 不 能 办 理 入 住 操 作 < / p > ;
}
return (
< div className = "grid gap-4 lg:grid-cols-2" >
< form className = "grid gap-3 rounded-lg border border-kumo-line p-4" onSubmit = { ( event ) = > void onSubmitTransfer ( event , activeAdmission ) } >
< h3 className = "text-sm font-semibold text-kumo-strong" > 办 理 换 床 < / h3 >
< p className = "text-xs text-muted-foreground" >
当 前 : { activeAdmission . elderName } / { admissionBedLabel ( activeAdmission ) }
< / p >
< Select
aria-label = "换床新床位"
disabled = { ! canManageAdmissions || isPending }
onValueChange = { onTransferBedChange }
options = { bedOptions }
value = { selectedTransferBedId }
/ >
< Textarea
aria-label = "换床备注"
disabled = { ! canManageAdmissions || isPending }
onChange = { ( event ) = > onTransferNotesChange ( event . target . value ) }
placeholder = "备注"
value = { transferNotes }
/ >
< Button disabled = { ! canManageAdmissions || isPending } type = "submit" variant = "outline" >
< MoveRight className = "size-4" aria-hidden = "true" / >
办 理 换 床
< / Button >
< / form >
< form className = "grid gap-3 rounded-lg border border-kumo-line p-4" onSubmit = { ( event ) = > void onSubmitDischarge ( event , activeAdmission ) } >
< h3 className = "text-sm font-semibold text-kumo-strong" > 办 理 退 住 < / h3 >
< p className = "text-xs text-muted-foreground" > 退 住 会 关 闭 当 前 入 住 记 录 并 释 放 该 床 位 。 < / p >
< Textarea
aria-label = "退住备注"
disabled = { ! canManageAdmissions || isPending }
onChange = { ( event ) = > onDischargeNotesChange ( event . target . value ) }
placeholder = "备注"
value = { dischargeNotes }
/ >
< Button disabled = { ! canManageAdmissions || isPending } type = "submit" variant = "destructive" >
< UserMinus className = "size-4" aria-hidden = "true" / >
办 理 退 住
< / Button >
< / form >
< / div >
) ;
}
@@ -597,7 +1178,7 @@ function contextToneClass(tone: OperationalContextLine["tone"]): string {
return "text-muted-foreground" ;
}
function ContextLines ( { lines } : { lines : OperationalContextLine [ ] } ) : React . ReactElement {
function ContextLines ( { expanded = false , lines } : { expanded? : boolean ; lines : OperationalContextLine [ ] } ) : React . ReactElement {
if ( lines . length === 0 ) {
return < span className = "text-muted-foreground" > - < / span > ;
}
@@ -605,8 +1186,11 @@ function ContextLines({ lines }: { lines: OperationalContextLine[] }): React.Rea
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
className = { cn ( expanded ? "text-sm" : "max-w-[260px] truncate text-xs" , contextToneClass ( line . tone ) ) }
key = { ` ${ line . kind } - ${ line . label } - ${ line . value } ` }
>
< span className = "font-medium" > { contextKindLabels [ line . kind ] } · { line . label } < / span > { line . value }
< / p >
) ) }
< / div >