- 通知中心改为游标分页 + 加载更多,历史通知不再被 50 条上限截断; 加载失败显示错误态与重试 - AI 流式请求 401 被登出时,登录页提示"登录已过期",不再无声踢出 - AI 附件/引用来源打开失败时给出明确错误提示,不再"点了没反应" - 新增 useDownload hook:统一导出/下载的防重复、loading 与成功/失败反馈; 接入学生/账单/房间/入住/费用五个页面的模板下载与导出按钮 - 学生页移除不检查响应状态的私有下载实现,统一走 downloadBlob
635 lines
22 KiB
TypeScript
635 lines
22 KiB
TypeScript
// aislop-ignore-file: duplicate-block -- 表格/表单声明结构相似且参数不同,渲染逻辑已共享组件化
|
||
import React, { useState, useMemo, useCallback } from 'react';
|
||
import { useNavigate } from 'react-router';
|
||
import { Alert, App, Form } from 'antd';
|
||
import dayjs, { type Dayjs } from 'dayjs';
|
||
import api from '../../api';
|
||
import { message } from '../../ui/app-message';
|
||
import { buildCheckInPayload, buildTransferPayload } from './occupancy-form';
|
||
import { buildOccupancyColumns } from './OccupancyColumns';
|
||
import type { OccupancyRow } from './OccupancyColumns';
|
||
import {
|
||
BatchCheckOutModal,
|
||
CheckInModal,
|
||
CheckOutModal,
|
||
TransferModal,
|
||
} from './OccupancyModals';
|
||
import { usePermission } from '../../hooks/usePermission';
|
||
import { occupancyParamsForView, occupancyViewPolicy, type OccupancyView } from '../archive-view';
|
||
import { useQuery } from '@tanstack/react-query';
|
||
import { validateResponse } from '../../utils/validate';
|
||
import { occupanciesSchema } from '../../api/schemas';
|
||
import { OccupanciesTableArea } from './OccupanciesTableArea';
|
||
import { OccupanciesToolbar } from './OccupanciesToolbar';
|
||
import { useOccupancyMutations } from './useOccupancyMutations';
|
||
import { QueryErrorState } from '../../components/QueryState';
|
||
import { NextStepHint } from '../../components/NextStepHint';
|
||
import { useVisibleRefetch } from '../../hooks/usePageVisible';
|
||
import { useDownload } from '../../hooks/useDownload';
|
||
|
||
interface StudentLookupRow {
|
||
id: number;
|
||
name: string;
|
||
studentNo?: string;
|
||
idNumber?: string;
|
||
phone?: string;
|
||
status?: string;
|
||
}
|
||
|
||
interface RoomOverviewRow {
|
||
id: number;
|
||
roomNumber: string;
|
||
building?: string;
|
||
capacity?: number;
|
||
currentCount?: number;
|
||
floor?: number | null;
|
||
roomType?: string;
|
||
status?: string;
|
||
}
|
||
|
||
const OccupanciesPage: React.FC = () => {
|
||
const { modal } = App.useApp();
|
||
const navigate = useNavigate();
|
||
const { hasPermission, permissionsReady } = usePermission();
|
||
const canCheckIn = permissionsReady && hasPermission('occupancy:checkin');
|
||
const canCheckOut = permissionsReady && hasPermission('occupancy:checkout');
|
||
const canTransfer = permissionsReady && hasPermission('occupancy:transfer');
|
||
const canDelete = permissionsReady && hasPermission('occupancy:delete');
|
||
const canPurge = permissionsReady && hasPermission('occupancy:purge');
|
||
const [checkInModal, setCheckInModal] = useState(false);
|
||
const [checkOutModal, setCheckOutModal] = useState<any>(null);
|
||
const [transferModal, setTransferModal] = useState<any>(null);
|
||
// 入住成功后的「下一步」引导提示
|
||
const [nextStepHint, setNextStepHint] = useState<'billing' | null>(null);
|
||
const [viewMode, setViewMode] = useState<OccupancyView>('active');
|
||
const viewPolicy = occupancyViewPolicy(viewMode);
|
||
const [autoDeposit, setAutoDeposit] = useState(true);
|
||
const [depositAmount, setDepositAmount] = useState(500);
|
||
const [searchText, setSearchText] = useState('');
|
||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null] | null>(null);
|
||
const [batchCheckOutModal, setBatchCheckOutModal] = useState(false);
|
||
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
||
|
||
const changeViewMode = (mode: OccupancyView) => {
|
||
setViewMode(mode);
|
||
setSelectedRowKeys([]);
|
||
};
|
||
const changeDateRange = (dates: [dayjs.Dayjs | null, dayjs.Dayjs | null] | null) => {
|
||
setDateRange(dates);
|
||
setSelectedRowKeys([]);
|
||
};
|
||
|
||
const {
|
||
data: fetchResult = { data: [], students: [], rooms: [] },
|
||
isLoading,
|
||
isFetching,
|
||
isError,
|
||
refetch,
|
||
} = useQuery<{ data: OccupancyRow[]; students: StudentLookupRow[]; rooms: RoomOverviewRow[] }>({
|
||
queryKey: ['occupancies', viewMode, dateRange],
|
||
queryFn: async () => {
|
||
const [occRes, stuRes, rmRes] = await Promise.allSettled([
|
||
api.get<OccupancyRow[]>('/occupancies', {
|
||
params: {
|
||
...occupancyParamsForView(viewMode),
|
||
dateFrom: dateRange?.[0]?.format('YYYY-MM-DD'),
|
||
dateTo: dateRange?.[1]?.format('YYYY-MM-DD'),
|
||
},
|
||
}),
|
||
api.get<StudentLookupRow[]>('/students/basic-lookups'),
|
||
api.get<RoomOverviewRow[]>('/rooms/overview'),
|
||
]);
|
||
const labels = ['入住数据', '学生列表', '房间列表'];
|
||
[stuRes, rmRes].forEach((res, i) => {
|
||
if (res.status === 'rejected') {
|
||
message.warning(`${labels[i + 1]}加载失败`);
|
||
}
|
||
});
|
||
if (occRes.status === 'rejected') {
|
||
throw occRes.reason;
|
||
}
|
||
return {
|
||
data: validateResponse<OccupancyRow[]>(occupanciesSchema, occRes.value),
|
||
students: stuRes.status === 'fulfilled' ? stuRes.value : [],
|
||
rooms: rmRes.status === 'fulfilled' ? rmRes.value : [],
|
||
};
|
||
},
|
||
});
|
||
const data = fetchResult.data;
|
||
const students = fetchResult.students;
|
||
const rooms = fetchResult.rooms;
|
||
const loading = isLoading || isFetching;
|
||
// RouteKeeper 保活页面切回时刷新入住列表
|
||
useVisibleRefetch(['occupancies']);
|
||
|
||
const {
|
||
checkInMutation,
|
||
checkOutMutation,
|
||
transferMutation,
|
||
batchCheckOutMutation,
|
||
batchDeleteMutation,
|
||
batchRestoreMutation,
|
||
archiveMutation,
|
||
purgeMutation,
|
||
batchPurgeMutation,
|
||
importMutation,
|
||
} = useOccupancyMutations();
|
||
const [saving, setSaving] = useState(false);
|
||
const [batchLoading, setBatchLoading] = useState(false);
|
||
const [checkInForm] = Form.useForm();
|
||
const [checkOutForm] = Form.useForm();
|
||
const [transferForm] = Form.useForm();
|
||
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, OccupancyRow>();
|
||
data.forEach((item) => {
|
||
if (!item.checkOutDate && item.status !== 'archived') map.set(item.studentId, item);
|
||
});
|
||
return map;
|
||
}, [data]);
|
||
|
||
const isRoomSelectable = useCallback((room: RoomOverviewRow) => {
|
||
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: RoomOverviewRow) => {
|
||
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((date): date is string => Boolean(date))
|
||
.reduce((latest: string | undefined, date) =>
|
||
!latest || date > latest ? date : latest,
|
||
undefined),
|
||
[selectedBatchRecords],
|
||
);
|
||
const latestSelectedBillingStartDate = useMemo(
|
||
() =>
|
||
selectedBatchRecords
|
||
.map((item) => item.billingStartDate || item.checkInDate)
|
||
.filter((date): date is string => Boolean(date))
|
||
.reduce((latest: string | undefined, date) =>
|
||
!latest || date > latest ? date : latest,
|
||
undefined),
|
||
[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 handleRoomChange = async (roomId: number) => {
|
||
checkInForm.setFieldValue('bedId', undefined);
|
||
checkInForm.setFieldValue('lockerId', undefined);
|
||
setAvailableBeds([]);
|
||
setAvailableLockers([]);
|
||
if (!roomId) return;
|
||
setAvailableResourcesLoading(true);
|
||
try {
|
||
const [beds, lockers] = await Promise.all([
|
||
api.get<any[]>(`/rooms/${roomId}/beds/available`),
|
||
api.get<any[]>(`/rooms/${roomId}/lockers/available`),
|
||
]);
|
||
setAvailableBeds(beds);
|
||
setAvailableLockers(lockers);
|
||
if (beds.length === 1) checkInForm.setFieldValue('bedId', beds[0].id);
|
||
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);
|
||
setTransferAvailableBeds([]);
|
||
setTransferAvailableLockers([]);
|
||
if (!roomId) return;
|
||
setTransferResourcesLoading(true);
|
||
try {
|
||
const [beds, lockers] = await Promise.all([
|
||
api.get<any[]>(`/rooms/${roomId}/beds/available`),
|
||
api.get<any[]>(`/rooms/${roomId}/lockers/available`),
|
||
]);
|
||
setTransferAvailableBeds(beds);
|
||
setTransferAvailableLockers(lockers);
|
||
if (beds.length === 1) transferForm.setFieldValue('newBedId', beds[0].id);
|
||
if (beds.length === 0) message.warning('目标宿舍暂无可用床位,请先在宿舍详情添加或释放床位');
|
||
} catch (e: any) {
|
||
console.error(e);
|
||
setTransferAvailableBeds([]);
|
||
setTransferAvailableLockers([]);
|
||
message.error(e?.message || '目标宿舍床位和柜子加载失败');
|
||
} finally {
|
||
setTransferResourcesLoading(false);
|
||
}
|
||
};
|
||
|
||
const filteredData = useMemo(() => {
|
||
if (!searchText) return data;
|
||
const keyword = searchText.toLowerCase();
|
||
return data.filter(
|
||
(r: any) =>
|
||
r.student?.name?.toLowerCase().includes(keyword) ||
|
||
r.room?.roomNumber?.toLowerCase().includes(keyword),
|
||
);
|
||
}, [data, searchText]);
|
||
|
||
const handleCheckIn = async () => {
|
||
const values = await checkInForm.validateFields();
|
||
setSaving(true);
|
||
try {
|
||
await checkInMutation.mutateAsync(buildCheckInPayload(values));
|
||
message.success('入住登记成功');
|
||
setCheckInModal(false);
|
||
checkInForm.resetFields();
|
||
// 引导业务闭环的下一步:录费用 → 生成账单
|
||
setNextStepHint('billing');
|
||
} catch {
|
||
// 错误提示由 useApiMutation 统一处理
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleCheckOut = async () => {
|
||
const values = await checkOutForm.validateFields();
|
||
setSaving(true);
|
||
try {
|
||
await checkOutMutation.mutateAsync({
|
||
id: checkOutModal.id,
|
||
payload: {
|
||
checkOutDate: values.checkOutDate.format('YYYY-MM-DD'),
|
||
billingEndDate: values.billingEndDate?.format('YYYY-MM-DD'),
|
||
checkOutReason: values.checkOutReason,
|
||
},
|
||
});
|
||
message.success('退宿成功');
|
||
setCheckOutModal(null);
|
||
checkOutForm.resetFields();
|
||
} catch {
|
||
// 错误提示由 useApiMutation 统一处理
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleTransfer = async () => {
|
||
const values = await transferForm.validateFields();
|
||
setSaving(true);
|
||
try {
|
||
await transferMutation.mutateAsync({
|
||
id: transferModal.id,
|
||
payload: buildTransferPayload(values),
|
||
});
|
||
message.success('换房成功');
|
||
setTransferModal(null);
|
||
transferForm.resetFields();
|
||
} catch {
|
||
// 错误提示由 useApiMutation 统一处理
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleBatchCheckOut = async () => {
|
||
if (batchLoading) return;
|
||
const values = await batchCheckOutForm.validateFields();
|
||
setBatchLoading(true);
|
||
try {
|
||
const res: any = await batchCheckOutMutation.mutateAsync({
|
||
ids: selectedRowKeys,
|
||
checkOutDate: values.checkOutDate.format('YYYY-MM-DD'),
|
||
billingEndDate: values.billingEndDate?.format('YYYY-MM-DD'),
|
||
checkOutReason: values.checkOutReason,
|
||
});
|
||
message.success(res.message || `已成功退宿 ${res.success} 人`);
|
||
setBatchCheckOutModal(false);
|
||
batchCheckOutForm.resetFields();
|
||
setSelectedRowKeys([]);
|
||
} catch {
|
||
// 错误提示由 useApiMutation 统一处理
|
||
} finally {
|
||
setBatchLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleBatchDelete = async () => {
|
||
if (batchLoading) return;
|
||
setBatchLoading(true);
|
||
try {
|
||
const res: any = await batchDeleteMutation.mutateAsync(selectedRowKeys);
|
||
message.success(res?.message || `已归档 ${selectedRowKeys.length} 条`);
|
||
setSelectedRowKeys([]);
|
||
} catch {
|
||
// 错误提示由 useApiMutation 统一处理
|
||
} finally {
|
||
setBatchLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleBatchRestore = async () => {
|
||
if (batchLoading) return;
|
||
setBatchLoading(true);
|
||
try {
|
||
const res = await batchRestoreMutation.mutateAsync(selectedRowKeys);
|
||
message.success(
|
||
`已恢复 ${res.restored} 条入住记录${res.skipped ? `,跳过 ${res.skipped} 条` : ''}`,
|
||
);
|
||
setSelectedRowKeys([]);
|
||
} catch {
|
||
// 错误提示由 useApiMutation 统一处理
|
||
} finally {
|
||
setBatchLoading(false);
|
||
}
|
||
};
|
||
|
||
const handlePurge = useCallback(
|
||
(id: number, studentName: string) => {
|
||
modal.confirm({
|
||
title: `永久删除入住记录(${studentName})?`,
|
||
content: '删除后不可恢复,该入住记录将被物理删除。确定继续?',
|
||
okText: '永久删除',
|
||
okButtonProps: { danger: true },
|
||
cancelText: '取消',
|
||
onOk: async () => {
|
||
try {
|
||
await purgeMutation.mutateAsync(id);
|
||
message.success('已永久删除(不可恢复)');
|
||
} catch {
|
||
// 错误提示由 useApiMutation 统一处理
|
||
}
|
||
},
|
||
});
|
||
},
|
||
[modal, purgeMutation],
|
||
);
|
||
|
||
const handleBatchPurge = async () => {
|
||
if (batchLoading) return;
|
||
setBatchLoading(true);
|
||
try {
|
||
const res: any = await batchPurgeMutation.mutateAsync(selectedRowKeys);
|
||
message.success(res?.message || `已永久删除 ${selectedRowKeys.length} 条`);
|
||
setSelectedRowKeys([]);
|
||
} catch {
|
||
// 错误提示由 useApiMutation 统一处理
|
||
} finally {
|
||
setBatchLoading(false);
|
||
}
|
||
};
|
||
|
||
const columns = useMemo(
|
||
() =>
|
||
buildOccupancyColumns({
|
||
readonly: viewPolicy.readonly,
|
||
canPurge,
|
||
canDelete,
|
||
onPurge: handlePurge,
|
||
onArchive: (id) => archiveMutation.mutateAsync(id),
|
||
onCheckOut: (record) => {
|
||
setCheckOutModal(record);
|
||
checkOutForm.setFieldsValue({ checkOutDate: dayjs() });
|
||
},
|
||
onTransfer: (record) => {
|
||
setTransferAvailableBeds([]);
|
||
setTransferAvailableLockers([]);
|
||
transferForm.resetFields();
|
||
setTransferModal(record);
|
||
transferForm.setFieldsValue({ transferDate: dayjs() });
|
||
},
|
||
}),
|
||
[
|
||
viewPolicy.readonly,
|
||
canPurge,
|
||
canDelete,
|
||
handlePurge,
|
||
archiveMutation,
|
||
checkOutForm,
|
||
transferForm,
|
||
],
|
||
);
|
||
|
||
const rowSelection = useMemo(
|
||
() => ({
|
||
selectedRowKeys,
|
||
onChange: (keys: any[]) => setSelectedRowKeys(keys),
|
||
getCheckboxProps: (record: any) =>
|
||
viewMode === 'active' ? { disabled: !!record.checkOutDate } : {},
|
||
}),
|
||
[selectedRowKeys, viewMode],
|
||
);
|
||
|
||
const { downloading: templateDownloading, run: runTemplateDownload } = useDownload();
|
||
const { downloading: exportDownloading, run: runExportDownload } = useDownload();
|
||
|
||
return (
|
||
<div>
|
||
<Alert
|
||
title="一站式导入"
|
||
description="导入入住名单时会优先按手机号关联已有学生,所属机构自动取学生档案;未找到学生或宿舍时会自动创建。后续仅需在此页面处理换房/退宿等日常操作即可。"
|
||
type="info"
|
||
showIcon
|
||
closable
|
||
style={{ marginBottom: 16 }}
|
||
/>
|
||
<OccupanciesToolbar
|
||
viewMode={viewMode}
|
||
onChangeViewMode={changeViewMode}
|
||
onSearch={setSearchText}
|
||
dateRange={dateRange}
|
||
onChangeDateRange={changeDateRange}
|
||
canCheckIn={canCheckIn}
|
||
onCheckIn={() => {
|
||
checkInForm.resetFields();
|
||
setAvailableBeds([]);
|
||
setAvailableLockers([]);
|
||
setAvailableResourcesLoading(false);
|
||
const today = dayjs();
|
||
checkInForm.setFieldsValue({
|
||
checkInDate: today,
|
||
billingStartDate: today,
|
||
stayType: 'short',
|
||
collectDeposit: true,
|
||
depositAmount: 500,
|
||
});
|
||
setCheckInModal(true);
|
||
}}
|
||
onImport={async ({ file, onSuccess, onError }: any) => {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
const params = new URLSearchParams();
|
||
if (autoDeposit) { params.set('autoDeposit', 'true'); params.set('depositAmount', String(depositAmount)); }
|
||
try {
|
||
const res: any = await importMutation.mutateAsync({
|
||
formData,
|
||
params: params.toString(),
|
||
});
|
||
if (res.errors?.length > 0) {
|
||
modal.warning({
|
||
title: res.message,
|
||
content: res.errors.join('\n'),
|
||
width: 500,
|
||
});
|
||
} else {
|
||
message.success(res.message);
|
||
}
|
||
onSuccess?.(res);
|
||
} catch (e) {
|
||
onError?.(e as Error);
|
||
}
|
||
}}
|
||
autoDeposit={autoDeposit}
|
||
onAutoDepositChange={setAutoDeposit}
|
||
depositAmount={depositAmount}
|
||
onDepositAmountChange={setDepositAmount}
|
||
onDownloadTemplate={() => {
|
||
void runTemplateDownload('/occupancies/template', '入住名单导入模板.xlsx', {
|
||
successMsg: '模板已下载',
|
||
errorMsg: '下载失败',
|
||
});
|
||
}}
|
||
templateLoading={templateDownloading}
|
||
onExport={() => {
|
||
const params = viewMode === 'active' ? '?active=true' : '';
|
||
const filename = viewMode === 'active' ? '在住记录.xlsx' : '全部入住记录.xlsx';
|
||
void runExportDownload('/occupancies/export' + params, filename, {
|
||
successMsg: '入住记录已导出',
|
||
errorMsg: '导出失败',
|
||
});
|
||
}}
|
||
exportLoading={exportDownloading}
|
||
/>
|
||
{nextStepHint === 'billing' && (
|
||
<NextStepHint
|
||
title="入住登记完成"
|
||
description="接下来可以为该学生录入公共/个人费用,再生成账单完成计费闭环。"
|
||
action={{ label: '去费用管理', onClick: () => navigate('/expenses') }}
|
||
onClose={() => setNextStepHint(null)}
|
||
/>
|
||
)}
|
||
{isError ? (
|
||
<QueryErrorState
|
||
title="入住数据加载失败"
|
||
description="请检查网络后重试。"
|
||
onRetry={() => void refetch()}
|
||
/>
|
||
) : (
|
||
<OccupanciesTableArea
|
||
columns={columns}
|
||
data={filteredData}
|
||
loading={loading}
|
||
selectedRowKeys={selectedRowKeys}
|
||
rowSelection={rowSelection}
|
||
batchAction={viewPolicy.batchAction}
|
||
canDelete={canDelete}
|
||
canPurge={canPurge}
|
||
batchLoading={batchLoading}
|
||
onBatchCheckOut={() => {
|
||
batchCheckOutForm.resetFields();
|
||
batchCheckOutForm.setFieldsValue({ checkOutDate: dayjs() });
|
||
setBatchCheckOutModal(true);
|
||
}}
|
||
onBatchDelete={handleBatchDelete}
|
||
onBatchRestore={handleBatchRestore}
|
||
onBatchPurge={handleBatchPurge}
|
||
onClearSelection={() => setSelectedRowKeys([])}
|
||
/>
|
||
)}
|
||
|
||
<CheckInModal
|
||
open={checkInModal}
|
||
canCheckIn={canCheckIn}
|
||
saving={saving}
|
||
form={checkInForm}
|
||
students={students}
|
||
activeOccupancyByStudentId={activeOccupancyByStudentId}
|
||
rooms={rooms}
|
||
roomOptionLabel={roomOptionLabel}
|
||
isRoomSelectable={isRoomSelectable}
|
||
onRoomChange={handleRoomChange}
|
||
availableBeds={availableBeds}
|
||
availableLockers={availableLockers}
|
||
availableResourcesLoading={availableResourcesLoading}
|
||
selectedCheckInRoomId={selectedCheckInRoomId}
|
||
dateNotBefore={dateNotBefore}
|
||
onOk={handleCheckIn}
|
||
onCancel={() => { setCheckInModal(false); setAvailableBeds([]); setAvailableLockers([]); setAvailableResourcesLoading(false); }}
|
||
/>
|
||
<CheckOutModal
|
||
record={checkOutModal}
|
||
canCheckOut={canCheckOut}
|
||
saving={saving}
|
||
form={checkOutForm}
|
||
dateNotBefore={dateNotBefore}
|
||
onOk={handleCheckOut}
|
||
onCancel={() => setCheckOutModal(null)}
|
||
/>
|
||
<BatchCheckOutModal
|
||
open={batchCheckOutModal}
|
||
canCheckOut={canCheckOut}
|
||
selectedRowKeys={selectedRowKeys}
|
||
latestSelectedCheckInDate={latestSelectedCheckInDate}
|
||
latestSelectedBillingStartDate={latestSelectedBillingStartDate}
|
||
data={data}
|
||
form={batchCheckOutForm}
|
||
dateNotBefore={dateNotBefore}
|
||
onOk={handleBatchCheckOut}
|
||
onCancel={() => setBatchCheckOutModal(false)}
|
||
/>
|
||
<TransferModal
|
||
record={transferModal}
|
||
canTransfer={canTransfer}
|
||
saving={saving}
|
||
form={transferForm}
|
||
rooms={rooms}
|
||
roomOptionLabel={roomOptionLabel}
|
||
isRoomSelectable={isRoomSelectable}
|
||
onRoomChange={handleTransferRoomChange}
|
||
transferAvailableBeds={transferAvailableBeds}
|
||
transferAvailableLockers={transferAvailableLockers}
|
||
transferResourcesLoading={transferResourcesLoading}
|
||
selectedTransferRoomId={selectedTransferRoomId}
|
||
dateNotBefore={dateNotBefore}
|
||
onOk={handleTransfer}
|
||
onCancel={() => { setTransferModal(null); transferForm.resetFields(); setTransferAvailableBeds([]); setTransferAvailableLockers([]); }}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default OccupanciesPage;
|