forked from wangziqi/gongxue-base
feat: refine admin forms, attendance and finance workflows
Squash merge PR #23. Included changes: - complete occupancy check-in required fields/default payload - improve responsive admin management pages - fix attendance edge cases and attendance period config - refine wallet/finance-related workflow handling Checks: - npm run typecheck -w apps/admin - npm run typecheck -w apps/server
This commit is contained in:
@@ -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,14 +59,73 @@ const OccupanciesPage: React.FC = () => {
|
||||
const [batchCheckOutForm] = Form.useForm();
|
||||
const [availableBeds, setAvailableBeds] = useState<any[]>([]);
|
||||
const [availableLockers, setAvailableLockers] = useState<any[]>([]);
|
||||
const [availableResourcesLoading, setAvailableResourcesLoading] = useState(false);
|
||||
const [transferAvailableBeds, setTransferAvailableBeds] = useState<any[]>([]);
|
||||
const [transferAvailableLockers, setTransferAvailableLockers] = useState<any[]>([]);
|
||||
const [transferResourcesLoading, setTransferResourcesLoading] = useState(false);
|
||||
const selectedCheckInRoomId = Form.useWatch('roomId', checkInForm);
|
||||
const selectedTransferRoomId = Form.useWatch('newRoomId', transferForm);
|
||||
|
||||
const activeOccupancyByStudentId = useMemo(() => {
|
||||
const map = new Map<number, any>();
|
||||
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);
|
||||
try {
|
||||
const [occRes, stuRes, rmRes] = (await Promise.allSettled([
|
||||
api.get('/occupancies', { params: { active: showActive ? 'true' : undefined, dateFrom: dateRange?.[0]?.format('YYYY-MM-DD'), dateTo: dateRange?.[1]?.format('YYYY-MM-DD') } }),
|
||||
api.get('/occupancies', {
|
||||
params: {
|
||||
active: showActive ? 'true' : undefined,
|
||||
dateFrom: dateRange?.[0]?.format('YYYY-MM-DD'),
|
||||
dateTo: dateRange?.[1]?.format('YYYY-MM-DD'),
|
||||
},
|
||||
}),
|
||||
api.get('/students/basic-lookups'),
|
||||
api.get('/rooms/overview'),
|
||||
])) as PromiseSettledResult<any>[];
|
||||
@@ -94,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<any[]>(`/rooms/${roomId}/beds/available`),
|
||||
@@ -107,17 +166,25 @@ const OccupanciesPage: React.FC = () => {
|
||||
setAvailableBeds(beds);
|
||||
setAvailableLockers(lockers);
|
||||
if (beds.length === 1) checkInForm.setFieldValue('bedId', beds[0].id);
|
||||
} catch (e) { console.error(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<any[]>(`/rooms/${roomId}/beds/available`),
|
||||
@@ -126,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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -148,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();
|
||||
@@ -195,10 +254,7 @@ const OccupanciesPage: React.FC = () => {
|
||||
const values = await transferForm.validateFields();
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.put(
|
||||
`/occupancies/${transferModal.id}/transfer`,
|
||||
buildTransferPayload(values),
|
||||
);
|
||||
await api.put(`/occupancies/${transferModal.id}/transfer`, buildTransferPayload(values));
|
||||
message.success('换房成功');
|
||||
setTransferModal(null);
|
||||
transferForm.resetFields();
|
||||
@@ -246,83 +302,104 @@ const OccupanciesPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{ title: '学生', width: 120, render: (_: any, r: any) => r.student?.name || '-' },
|
||||
{ title: '宿舍', width: 120, render: (_: any, r: any) => r.room?.roomNumber || '-' },
|
||||
{ title: '床位', width: 80, render: (_: unknown, r: Record<string, unknown>) => (r.bed as Record<string, string> | undefined)?.bedNumber || '-' },
|
||||
{ title: '柜子', width: 80, render: (_: unknown, r: Record<string, unknown>) => (r.locker as Record<string, string> | undefined)?.lockerNumber || '-' },
|
||||
{ title: '入住日期', dataIndex: 'checkInDate', width: 110 },
|
||||
{ title: '计费起始', dataIndex: 'billingStartDate', width: 110 },
|
||||
{
|
||||
title: '退宿日期',
|
||||
dataIndex: 'checkOutDate',
|
||||
width: 110,
|
||||
render: (v: any) => v || <Tag color="green">在住</Tag>,
|
||||
},
|
||||
{ title: '计费截止', dataIndex: 'billingEndDate', render: (v: any) => v || '-' },
|
||||
{ title: '退宿原因', dataIndex: 'checkOutReason', render: (v: any) => v || '-' },
|
||||
{
|
||||
title: '操作',
|
||||
width: 220,
|
||||
render: (_: any, record: any) =>
|
||||
!record.checkOutDate ? (
|
||||
<Space>
|
||||
<PermissionButton
|
||||
permission="occupancy:checkout"
|
||||
size="small"
|
||||
icon={<LogoutOutlined />}
|
||||
onClick={() => {
|
||||
setCheckOutModal(record);
|
||||
checkOutForm.setFieldsValue({ checkOutDate: dayjs() });
|
||||
}}
|
||||
>
|
||||
退宿
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
permission="occupancy:transfer"
|
||||
size="small"
|
||||
icon={<SwapOutlined />}
|
||||
onClick={() => {
|
||||
setTransferAvailableBeds([]);
|
||||
setTransferAvailableLockers([]);
|
||||
transferForm.resetFields();
|
||||
setTransferModal(record);
|
||||
transferForm.setFieldsValue({ transferDate: dayjs() });
|
||||
}}
|
||||
>
|
||||
换房
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<Tag>已退宿</Tag>
|
||||
<Popconfirm
|
||||
title="确定归档此记录?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await api.delete(`/occupancies/${record.id}`);
|
||||
message.success('归档成功');
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '归档失败');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PermissionButton permission="occupancy:delete" size="small" danger icon={<InboxOutlined />}>
|
||||
归档
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{ title: '学生', width: 120, render: (_: any, r: any) => r.student?.name || '-' },
|
||||
{ title: '宿舍', width: 120, render: (_: any, r: any) => r.room?.roomNumber || '-' },
|
||||
{
|
||||
title: '床位',
|
||||
width: 80,
|
||||
render: (_: unknown, r: Record<string, unknown>) =>
|
||||
(r.bed as Record<string, string> | undefined)?.bedNumber || '-',
|
||||
},
|
||||
{
|
||||
title: '柜子',
|
||||
width: 80,
|
||||
render: (_: unknown, r: Record<string, unknown>) =>
|
||||
(r.locker as Record<string, string> | undefined)?.lockerNumber || '-',
|
||||
},
|
||||
{ title: '入住日期', dataIndex: 'checkInDate', width: 110 },
|
||||
{ title: '计费起始', dataIndex: 'billingStartDate', width: 110 },
|
||||
{
|
||||
title: '退宿日期',
|
||||
dataIndex: 'checkOutDate',
|
||||
width: 110,
|
||||
render: (v: any) => v || <Tag color="green">在住</Tag>,
|
||||
},
|
||||
{ title: '计费截止', dataIndex: 'billingEndDate', render: (v: any) => v || '-' },
|
||||
{ title: '退宿原因', dataIndex: 'checkOutReason', render: (v: any) => v || '-' },
|
||||
{
|
||||
title: '操作',
|
||||
width: 220,
|
||||
render: (_: any, record: any) =>
|
||||
!record.checkOutDate ? (
|
||||
<Space>
|
||||
<PermissionButton
|
||||
permission="occupancy:checkout"
|
||||
size="small"
|
||||
icon={<LogoutOutlined />}
|
||||
onClick={() => {
|
||||
setCheckOutModal(record);
|
||||
checkOutForm.setFieldsValue({ checkOutDate: dayjs() });
|
||||
}}
|
||||
>
|
||||
退宿
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
], [fetchData, setCheckOutModal, checkOutForm, setTransferModal, transferForm]);
|
||||
<PermissionButton
|
||||
permission="occupancy:transfer"
|
||||
size="small"
|
||||
icon={<SwapOutlined />}
|
||||
onClick={() => {
|
||||
setTransferAvailableBeds([]);
|
||||
setTransferAvailableLockers([]);
|
||||
transferForm.resetFields();
|
||||
setTransferModal(record);
|
||||
transferForm.setFieldsValue({ transferDate: dayjs() });
|
||||
}}
|
||||
>
|
||||
换房
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<Tag>已退宿</Tag>
|
||||
<Popconfirm
|
||||
title="确定归档此记录?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await api.delete(`/occupancies/${record.id}`);
|
||||
message.success('归档成功');
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '归档失败');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PermissionButton
|
||||
permission="occupancy:delete"
|
||||
size="small"
|
||||
danger
|
||||
icon={<InboxOutlined />}
|
||||
>
|
||||
归档
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
],
|
||||
[fetchData, setCheckOutModal, checkOutForm, setTransferModal, transferForm],
|
||||
);
|
||||
|
||||
const rowSelection = useMemo(() => ({
|
||||
selectedRowKeys,
|
||||
onChange: (keys: any[]) => setSelectedRowKeys(keys),
|
||||
// 「在住记录」Tab:禁用已退宿(防止误选用于批量退宿);「全部记录」Tab:均可选用于批量归档
|
||||
getCheckboxProps: (record: any) => (showActive ? { disabled: !!record.checkOutDate } : {}),
|
||||
}), [selectedRowKeys, showActive]);
|
||||
const rowSelection = useMemo(
|
||||
() => ({
|
||||
selectedRowKeys,
|
||||
onChange: (keys: any[]) => setSelectedRowKeys(keys),
|
||||
// 「在住记录」Tab:禁用已退宿(防止误选用于批量退宿);「全部记录」Tab:均可选用于批量归档
|
||||
getCheckboxProps: (record: any) => (showActive ? { disabled: !!record.checkOutDate } : {}),
|
||||
}),
|
||||
[selectedRowKeys, showActive],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -348,7 +425,14 @@ const OccupanciesPage: React.FC = () => {
|
||||
allowClear
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
<RangePicker value={dateRange} onChange={(dates) => { setDateRange(dates ? [dates[0], dates[1]] : null); }} placeholder={['入住开始', '入住结束']} style={{ width: 240 }} />
|
||||
<RangePicker
|
||||
value={dateRange}
|
||||
onChange={(dates) => {
|
||||
setDateRange(dates ? [dates[0], dates[1]] : null);
|
||||
}}
|
||||
placeholder={['入住开始', '入住结束']}
|
||||
style={{ width: 240 }}
|
||||
/>
|
||||
</Space>
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<PermissionButton
|
||||
@@ -357,7 +441,17 @@ const OccupanciesPage: React.FC = () => {
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
checkInForm.resetFields();
|
||||
checkInForm.setFieldsValue({ checkInDate: dayjs(), collectDeposit: true, depositAmount: 500 });
|
||||
setAvailableBeds([]);
|
||||
setAvailableLockers([]);
|
||||
setAvailableResourcesLoading(false);
|
||||
const today = dayjs();
|
||||
checkInForm.setFieldsValue({
|
||||
checkInDate: today,
|
||||
billingStartDate: today,
|
||||
stayType: 'short',
|
||||
collectDeposit: true,
|
||||
depositAmount: 500,
|
||||
});
|
||||
setCheckInModal(true);
|
||||
}}
|
||||
>
|
||||
@@ -432,26 +526,26 @@ const OccupanciesPage: React.FC = () => {
|
||||
导入时自动收押金
|
||||
{autoDeposit && (
|
||||
<Space.Compact>
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={0}
|
||||
value={depositAmount}
|
||||
onChange={(v) => setDepositAmount(v || 500)}
|
||||
style={{ width: 60 }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
padding: '0 8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
border: '1px solid #d9d9d9',
|
||||
backgroundColor: '#fafafa',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
元
|
||||
</span>
|
||||
</Space.Compact>
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={0}
|
||||
value={depositAmount}
|
||||
onChange={(v) => setDepositAmount(v || 500)}
|
||||
style={{ width: 60 }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
padding: '0 8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
border: '1px solid #d9d9d9',
|
||||
backgroundColor: '#fafafa',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
元
|
||||
</span>
|
||||
</Space.Compact>
|
||||
)}
|
||||
</span>
|
||||
</Space>
|
||||
@@ -524,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}
|
||||
@@ -541,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,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -559,18 +663,25 @@ const OccupanciesPage: React.FC = () => {
|
||||
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),
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="checkInDate" label="入住日期" rules={[{ required: true }]}>
|
||||
<Form.Item name="checkInDate" label="入住日期" rules={[{ required: true, message: '请选择入住日期' }]}>
|
||||
<DatePicker style={{ width: '100%' }} placeholder="选择入住日期" format="YYYY-MM-DD" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="billingStartDate"
|
||||
label="计费起始日"
|
||||
dependencies={["checkInDate"]}
|
||||
extra="默认与入住日期相同,可调整(如学生要求从次日开始计费)"
|
||||
rules={[
|
||||
{ required: true, message: '请选择计费起始日' },
|
||||
({ getFieldValue }) => ({
|
||||
validator: dateNotBefore(getFieldValue('checkInDate'), '计费起始日不能早于入住日期'),
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<DatePicker
|
||||
style={{ width: '100%' }}
|
||||
@@ -578,9 +689,8 @@ const OccupanciesPage: React.FC = () => {
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="stayType" label="入住类型">
|
||||
<Form.Item name="stayType" label="入住类型" rules={[{ required: true, message: '请选择入住类型' }]}>
|
||||
<Select
|
||||
allowClear
|
||||
options={[
|
||||
{ value: 'short', label: '短租' },
|
||||
{ value: 'long', label: '长租' },
|
||||
@@ -591,16 +701,25 @@ const OccupanciesPage: React.FC = () => {
|
||||
<Form.Item
|
||||
name="bedId"
|
||||
label="床位"
|
||||
rules={[{ required: true, message: '请选择床位' }]}
|
||||
rules={[
|
||||
{ required: true, message: '请选择床位' },
|
||||
{
|
||||
validator: (_: unknown, value?: number) =>
|
||||
!value || availableBeds.some((bed) => bed.id === value)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请选择当前宿舍下的可用床位')),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请先选择房间"
|
||||
disabled={availableBeds.length === 0}
|
||||
placeholder={selectedCheckInRoomId ? '请选择床位' : '请先选择房间'}
|
||||
loading={availableResourcesLoading}
|
||||
disabled={!selectedCheckInRoomId || availableResourcesLoading || availableBeds.length === 0}
|
||||
options={availableBeds.map((b) => ({
|
||||
value: b.id,
|
||||
label: b.bedNumber,
|
||||
}))}
|
||||
notFoundContent="该房间暂无可用床位"
|
||||
notFoundContent={selectedCheckInRoomId ? '该房间暂无可用床位' : '请先选择房间'}
|
||||
/>
|
||||
</Form.Item>
|
||||
{availableBeds.length > 0 && (
|
||||
@@ -608,14 +727,12 @@ const OccupanciesPage: React.FC = () => {
|
||||
空闲 {availableBeds.length} 张床位
|
||||
</div>
|
||||
)}
|
||||
<Form.Item
|
||||
name="lockerId"
|
||||
label="柜子(可选)"
|
||||
>
|
||||
<Form.Item name="lockerId" label="柜子(可选)">
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="可选分配柜子"
|
||||
disabled={availableLockers.length === 0}
|
||||
loading={availableResourcesLoading}
|
||||
disabled={!selectedCheckInRoomId || availableResourcesLoading || availableLockers.length === 0}
|
||||
options={availableLockers.map((l) => ({
|
||||
value: l.id,
|
||||
label: l.lockerNumber,
|
||||
@@ -630,7 +747,10 @@ const OccupanciesPage: React.FC = () => {
|
||||
>
|
||||
<Switch checkedChildren="已缴" unCheckedChildren="不缴" />
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, current) => prev.collectDeposit !== current.collectDeposit}>
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prev, current) => prev.collectDeposit !== current.collectDeposit}
|
||||
>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('collectDeposit') ? (
|
||||
<Form.Item
|
||||
@@ -638,12 +758,7 @@ const OccupanciesPage: React.FC = () => {
|
||||
label="押金金额"
|
||||
rules={[{ required: true, message: '请输入押金金额' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0.01}
|
||||
precision={2}
|
||||
addonAfter="元"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<InputNumber min={0.01} precision={2} addonAfter="元" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
) : null
|
||||
}
|
||||
@@ -664,10 +779,30 @@ const OccupanciesPage: React.FC = () => {
|
||||
confirmLoading={saving}
|
||||
>
|
||||
<Form form={checkOutForm} layout="vertical">
|
||||
<Form.Item name="checkOutDate" label="退宿日期" rules={[{ required: true }]}>
|
||||
<Form.Item
|
||||
name="checkOutDate"
|
||||
label="退宿日期"
|
||||
rules={[
|
||||
{ required: true, message: '请选择退宿日期' },
|
||||
{ validator: dateNotBefore(checkOutModal?.checkInDate, '退宿日期不能早于入住日期') },
|
||||
]}
|
||||
>
|
||||
<DatePicker style={{ width: '100%' }} placeholder="选择退宿日期" format="YYYY-MM-DD" />
|
||||
</Form.Item>
|
||||
<Form.Item name="billingEndDate" label="计费截止日" extra="默认与退宿日期相同">
|
||||
<Form.Item
|
||||
name="billingEndDate"
|
||||
label="计费截止日"
|
||||
dependencies={["checkOutDate"]}
|
||||
extra="默认与退宿日期相同"
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator: dateNotBefore(
|
||||
checkOutModal?.billingStartDate || checkOutModal?.checkInDate || getFieldValue('checkOutDate'),
|
||||
'计费截止日不能早于计费起始日',
|
||||
),
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<DatePicker
|
||||
style={{ width: '100%' }}
|
||||
placeholder="选择计费截止日"
|
||||
@@ -699,10 +834,34 @@ const OccupanciesPage: React.FC = () => {
|
||||
width={500}
|
||||
>
|
||||
<Form form={batchCheckOutForm} layout="vertical">
|
||||
<Form.Item name="checkOutDate" label="退宿日期" rules={[{ required: true }]}>
|
||||
<Form.Item
|
||||
name="checkOutDate"
|
||||
label="退宿日期"
|
||||
rules={[
|
||||
{ required: true, message: '请选择退宿日期' },
|
||||
{
|
||||
validator: dateNotBefore(
|
||||
latestSelectedCheckInDate,
|
||||
'退宿日期不能早于所选记录中最晚的入住日期',
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DatePicker style={{ width: '100%' }} placeholder="选择退宿日期" format="YYYY-MM-DD" />
|
||||
</Form.Item>
|
||||
<Form.Item name="billingEndDate" label="计费截止日" extra="默认与退宿日期相同">
|
||||
<Form.Item
|
||||
name="billingEndDate"
|
||||
label="计费截止日"
|
||||
extra="默认与退宿日期相同"
|
||||
rules={[
|
||||
{
|
||||
validator: dateNotBefore(
|
||||
latestSelectedBillingStartDate,
|
||||
'计费截止日不能早于所选记录中最晚的计费起始日',
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DatePicker
|
||||
style={{ width: '100%' }}
|
||||
placeholder="选择计费截止日"
|
||||
@@ -768,24 +927,33 @@ const OccupanciesPage: React.FC = () => {
|
||||
.filter((r: any) => r.id !== transferModal?.roomId)
|
||||
.map((r: any) => ({
|
||||
value: r.id,
|
||||
label: `${r.roomNumber} (${r.building || ''}) [${r.currentCount}/${r.capacity}]`,
|
||||
disabled: r.currentCount >= r.capacity,
|
||||
label: roomOptionLabel(r),
|
||||
disabled: !isRoomSelectable(r),
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="newBedId"
|
||||
label="目标床位"
|
||||
rules={[{ required: true, message: '请选择目标床位' }]}
|
||||
rules={[
|
||||
{ required: true, message: '请选择目标床位' },
|
||||
{
|
||||
validator: (_: unknown, value?: number) =>
|
||||
!value || transferAvailableBeds.some((bed) => bed.id === value)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请选择目标宿舍下的可用床位')),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请先选择目标宿舍"
|
||||
disabled={transferAvailableBeds.length === 0}
|
||||
placeholder={selectedTransferRoomId ? '请选择目标床位' : '请先选择目标宿舍'}
|
||||
loading={transferResourcesLoading}
|
||||
disabled={!selectedTransferRoomId || transferResourcesLoading || transferAvailableBeds.length === 0}
|
||||
options={transferAvailableBeds.map((bed) => ({
|
||||
value: bed.id,
|
||||
label: bed.bedNumber,
|
||||
}))}
|
||||
notFoundContent="目标宿舍暂无可用床位"
|
||||
notFoundContent={selectedTransferRoomId ? '目标宿舍暂无可用床位' : '请先选择目标宿舍'}
|
||||
/>
|
||||
</Form.Item>
|
||||
{transferAvailableBeds.length > 0 && (
|
||||
@@ -797,7 +965,8 @@ const OccupanciesPage: React.FC = () => {
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="可选分配目标宿舍柜子"
|
||||
disabled={transferAvailableLockers.length === 0}
|
||||
loading={transferResourcesLoading}
|
||||
disabled={!selectedTransferRoomId || transferResourcesLoading || transferAvailableLockers.length === 0}
|
||||
options={transferAvailableLockers.map((locker) => ({
|
||||
value: locker.id,
|
||||
label: locker.lockerNumber,
|
||||
@@ -805,17 +974,47 @@ const OccupanciesPage: React.FC = () => {
|
||||
notFoundContent="目标宿舍暂无可用柜子"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="transferDate" label="换房日期" rules={[{ required: true }]}>
|
||||
<Form.Item
|
||||
name="transferDate"
|
||||
label="换房日期"
|
||||
rules={[
|
||||
{ required: true, message: '请选择换房日期' },
|
||||
{ validator: dateNotBefore(transferModal?.checkInDate, '换房日期不能早于原入住日期') },
|
||||
]}
|
||||
>
|
||||
<DatePicker style={{ width: '100%' }} placeholder="选择换房日期" format="YYYY-MM-DD" />
|
||||
</Form.Item>
|
||||
<Form.Item name="oldBillingEndDate" label="旧房计费截止日" extra="默认为换房当天">
|
||||
<Form.Item
|
||||
name="oldBillingEndDate"
|
||||
label="旧房计费截止日"
|
||||
dependencies={["transferDate"]}
|
||||
extra="默认为换房当天"
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator: dateNotBefore(
|
||||
transferModal?.billingStartDate || transferModal?.checkInDate || getFieldValue('transferDate'),
|
||||
'旧房计费截止日不能早于计费起始日',
|
||||
),
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<DatePicker
|
||||
style={{ width: '100%' }}
|
||||
placeholder="选择旧房计费截止日"
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="newBillingStartDate" label="新房计费起始日" extra="默认为换房次日">
|
||||
<Form.Item
|
||||
name="newBillingStartDate"
|
||||
label="新房计费起始日"
|
||||
dependencies={["transferDate"]}
|
||||
extra="默认为换房次日"
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator: dateNotBefore(getFieldValue('transferDate'), '新房计费起始日不能早于换房日期'),
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<DatePicker
|
||||
style={{ width: '100%' }}
|
||||
placeholder="选择新房计费起始日"
|
||||
|
||||
Reference in New Issue
Block a user