From 74396633023b1a2f01a9e38f3a8ea701548bf37c Mon Sep 17 00:00:00 2001 From: wangziqi Date: Sat, 18 Jul 2026 20:40:18 +0800 Subject: [PATCH] fix(admin): complete occupancy check-in form payload --- apps/admin/src/pages/Occupancies/index.tsx | 285 ++++++++++++++---- .../occupancy-form.integration.test.ts | 50 ++- .../src/pages/Occupancies/occupancy-form.ts | 29 ++ 3 files changed, 306 insertions(+), 58 deletions(-) diff --git a/apps/admin/src/pages/Occupancies/index.tsx b/apps/admin/src/pages/Occupancies/index.tsx index c106e5b..06d0644 100644 --- a/apps/admin/src/pages/Occupancies/index.tsx +++ b/apps/admin/src/pages/Occupancies/index.tsx @@ -26,13 +26,13 @@ import { DownloadOutlined, ExportOutlined, } from '@ant-design/icons'; -import dayjs from 'dayjs'; +import dayjs, { type Dayjs } from 'dayjs'; import api from '../../api'; import { downloadBlob } from '../../utils/download'; import { maskPhone, maskIdNumber } from '../../utils/sensitive'; import PermissionButton from '../../components/PermissionButton'; import { message } from '../../ui/app-message'; -import { buildTransferPayload } from './occupancy-form'; +import { buildCheckInPayload, buildTransferPayload } from './occupancy-form'; const { RangePicker } = DatePicker; @@ -59,8 +59,61 @@ const OccupanciesPage: React.FC = () => { const [batchCheckOutForm] = Form.useForm(); const [availableBeds, setAvailableBeds] = useState([]); const [availableLockers, setAvailableLockers] = useState([]); + const [availableResourcesLoading, setAvailableResourcesLoading] = useState(false); const [transferAvailableBeds, setTransferAvailableBeds] = useState([]); const [transferAvailableLockers, setTransferAvailableLockers] = useState([]); + const [transferResourcesLoading, setTransferResourcesLoading] = useState(false); + const selectedCheckInRoomId = Form.useWatch('roomId', checkInForm); + const selectedTransferRoomId = Form.useWatch('newRoomId', transferForm); + + const activeOccupancyByStudentId = useMemo(() => { + const map = new Map(); + data.forEach((item) => { + if (!item.checkOutDate && item.status !== 'archived') map.set(item.studentId, item); + }); + return map; + }, [data]); + + const isRoomSelectable = useCallback((room: any) => { + const currentCount = Number(room.currentCount || 0); + const capacity = Number(room.capacity || 0); + return room.status !== 'archived' && room.status !== 'maintenance' && currentCount < capacity; + }, []); + + const roomOptionLabel = useCallback((room: any) => { + const base = `${room.roomNumber} (${room.building || ''}) [${room.currentCount}/${room.capacity}]`; + if (room.status === 'maintenance') return `${base} · 维修中`; + if (room.status === 'archived') return `${base} · 已归档`; + if (Number(room.currentCount || 0) >= Number(room.capacity || 0)) return `${base} · 已满`; + return base; + }, []); + + const selectedBatchRecords = useMemo( + () => data.filter((item) => selectedRowKeys.includes(item.id) && !item.checkOutDate), + [data, selectedRowKeys], + ); + const latestSelectedCheckInDate = useMemo( + () => selectedBatchRecords.map((item) => item.checkInDate).filter(Boolean).sort().at(-1), + [selectedBatchRecords], + ); + const latestSelectedBillingStartDate = useMemo( + () => + selectedBatchRecords + .map((item) => item.billingStartDate || item.checkInDate) + .filter(Boolean) + .sort() + .at(-1), + [selectedBatchRecords], + ); + + const dateNotBefore = (start: string | Dayjs | null | undefined, messageText: string) => + (_: unknown, value?: Dayjs | null) => { + if (!value || !start) return Promise.resolve(); + const startDate = dayjs.isDayjs(start) ? start : dayjs(start); + return value.isBefore(startDate, 'day') + ? Promise.reject(new Error(messageText)) + : Promise.resolve(); + }; const fetchData = useCallback(async () => { setLoading(true); @@ -100,11 +153,11 @@ const OccupanciesPage: React.FC = () => { const handleRoomChange = async (roomId: number) => { checkInForm.setFieldValue('bedId', undefined); checkInForm.setFieldValue('lockerId', undefined); - if (!roomId) { - setAvailableBeds([]); - setAvailableLockers([]); - return; - } + setAvailableBeds([]); + setAvailableLockers([]); + if (!roomId) return; + + setAvailableResourcesLoading(true); try { const [beds, lockers] = await Promise.all([ api.get(`/rooms/${roomId}/beds/available`), @@ -113,19 +166,25 @@ const OccupanciesPage: React.FC = () => { setAvailableBeds(beds); setAvailableLockers(lockers); if (beds.length === 1) checkInForm.setFieldValue('bedId', beds[0].id); - } catch (e) { + if (beds.length === 0) message.warning('该宿舍暂无可用床位,请先在宿舍详情添加或释放床位'); + } catch (e: any) { console.error(e); + setAvailableBeds([]); + setAvailableLockers([]); + message.error(e?.message || '宿舍床位和柜子加载失败'); + } finally { + setAvailableResourcesLoading(false); } }; const handleTransferRoomChange = async (roomId: number) => { transferForm.setFieldValue('newBedId', undefined); transferForm.setFieldValue('newLockerId', undefined); - if (!roomId) { - setTransferAvailableBeds([]); - setTransferAvailableLockers([]); - return; - } + setTransferAvailableBeds([]); + setTransferAvailableLockers([]); + if (!roomId) return; + + setTransferResourcesLoading(true); try { const [beds, lockers] = await Promise.all([ api.get(`/rooms/${roomId}/beds/available`), @@ -134,11 +193,14 @@ const OccupanciesPage: React.FC = () => { setTransferAvailableBeds(beds); setTransferAvailableLockers(lockers); if (beds.length === 1) transferForm.setFieldValue('newBedId', beds[0].id); - } catch (e) { + if (beds.length === 0) message.warning('目标宿舍暂无可用床位,请先在宿舍详情添加或释放床位'); + } catch (e: any) { console.error(e); setTransferAvailableBeds([]); setTransferAvailableLockers([]); - message.error('目标宿舍床位和柜子加载失败'); + message.error(e?.message || '目标宿舍床位和柜子加载失败'); + } finally { + setTransferResourcesLoading(false); } }; @@ -156,18 +218,7 @@ const OccupanciesPage: React.FC = () => { const values = await checkInForm.validateFields(); setSaving(true); try { - await api.post('/occupancies/check-in', { - studentId: values.studentId, - roomId: values.roomId, - checkInDate: values.checkInDate.format('YYYY-MM-DD'), - billingStartDate: values.billingStartDate?.format('YYYY-MM-DD'), - stayType: values.stayType, - collectDeposit: values.collectDeposit, - depositAmount: values.collectDeposit ? values.depositAmount : undefined, - notes: values.notes, - bedId: values.bedId, - lockerId: values.lockerId || undefined, - }); + await api.post('/occupancies/check-in', buildCheckInPayload(values)); message.success('入住登记成功'); setCheckInModal(false); checkInForm.resetFields(); @@ -390,8 +441,14 @@ const OccupanciesPage: React.FC = () => { icon={} onClick={() => { checkInForm.resetFields(); + setAvailableBeds([]); + setAvailableLockers([]); + setAvailableResourcesLoading(false); + const today = dayjs(); checkInForm.setFieldsValue({ - checkInDate: dayjs(), + checkInDate: today, + billingStartDate: today, + stayType: 'short', collectDeposit: true, depositAmount: 500, }); @@ -561,7 +618,12 @@ const OccupanciesPage: React.FC = () => { title="入住登记" open={checkInModal} onOk={handleCheckIn} - onCancel={() => setCheckInModal(false)} + onCancel={() => { + setCheckInModal(false); + setAvailableBeds([]); + setAvailableLockers([]); + setAvailableResourcesLoading(false); + }} okText="确认入住" confirmLoading={saving} width={500} @@ -578,10 +640,15 @@ const OccupanciesPage: React.FC = () => { placeholder="搜索并选择学生" options={students .filter((s: any) => s.status === 'active') - .map((s: any) => ({ - value: s.id, - label: `${s.name} (${s.idNumber ? maskIdNumber(s.idNumber) : s.phone ? maskPhone(s.phone) : ''})`, - }))} + .map((s: any) => { + const activeOccupancy = activeOccupancyByStudentId.get(s.id); + const identifier = s.idNumber ? maskIdNumber(s.idNumber) : s.phone ? maskPhone(s.phone) : ''; + return { + value: s.id, + label: `${s.name} (${identifier})${activeOccupancy ? ` · 已入住${activeOccupancy.room?.roomNumber ? ` ${activeOccupancy.room.roomNumber}` : ''}` : ''}`, + disabled: !!activeOccupancy, + }; + })} /> { onChange={handleRoomChange} options={rooms.map((r) => ({ value: r.id, - label: `${r.roomNumber} (${r.building || ''}) [${r.currentCount}/${r.capacity}]`, - disabled: r.currentCount >= r.capacity, + label: roomOptionLabel(r), + disabled: !isRoomSelectable(r), }))} /> - + ({ + validator: dateNotBefore(getFieldValue('checkInDate'), '计费起始日不能早于入住日期'), + }), + ]} > { format="YYYY-MM-DD" /> - + ({ value: b.id, label: b.bedNumber, }))} - notFoundContent="该房间暂无可用床位" + notFoundContent={selectedCheckInRoomId ? '该房间暂无可用床位' : '请先选择房间'} /> {availableBeds.length > 0 && ( @@ -645,7 +731,8 @@ const OccupanciesPage: React.FC = () => { ({ value: bed.id, label: bed.bedNumber, }))} - notFoundContent="目标宿舍暂无可用床位" + notFoundContent={selectedTransferRoomId ? '目标宿舍暂无可用床位' : '请先选择目标宿舍'} /> {transferAvailableBeds.length > 0 && ( @@ -825,7 +965,8 @@ const OccupanciesPage: React.FC = () => {