feat: 为批量归档补充批量恢复
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
UploadOutlined,
|
||||
DownloadOutlined,
|
||||
ExportOutlined,
|
||||
UndoOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import dayjs, { type Dayjs } from 'dayjs';
|
||||
import api from '../../api';
|
||||
@@ -34,6 +35,7 @@ import PermissionButton from '../../components/PermissionButton';
|
||||
import { message } from '../../ui/app-message';
|
||||
import { buildCheckInPayload, buildTransferPayload } from './occupancy-form';
|
||||
import { usePermission } from '../../hooks/usePermission';
|
||||
import { occupancyParamsForView, occupancyViewPolicy, type OccupancyView } from '../archive-view';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
@@ -50,7 +52,8 @@ const OccupanciesPage: React.FC = () => {
|
||||
const [checkInModal, setCheckInModal] = useState(false);
|
||||
const [checkOutModal, setCheckOutModal] = useState<any>(null);
|
||||
const [transferModal, setTransferModal] = useState<any>(null);
|
||||
const [showActive, setShowActive] = useState(true);
|
||||
const [viewMode, setViewMode] = useState<OccupancyView>('active');
|
||||
const viewPolicy = occupancyViewPolicy(viewMode);
|
||||
const [autoDeposit, setAutoDeposit] = useState(true);
|
||||
const [depositAmount, setDepositAmount] = useState(500);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
@@ -73,10 +76,30 @@ const OccupanciesPage: React.FC = () => {
|
||||
const selectedTransferRoomId = Form.useWatch('newRoomId', transferForm);
|
||||
|
||||
// Close modals when the user loses the required permission
|
||||
useEffect(() => { if (!canCheckIn) { setCheckInModal(false); checkInForm.resetFields(); } }, [canCheckIn, checkInForm]);
|
||||
useEffect(() => { if (!canCheckOut && checkOutModal) { setCheckOutModal(null); checkOutForm.resetFields(); } }, [canCheckOut, checkOutModal, checkOutForm]);
|
||||
useEffect(() => { if (!canCheckOut) { setBatchCheckOutModal(false); batchCheckOutForm.resetFields(); } }, [canCheckOut, batchCheckOutForm]);
|
||||
useEffect(() => { if (!canTransfer && transferModal) { setTransferModal(null); transferForm.resetFields(); } }, [canTransfer, transferModal, transferForm]);
|
||||
useEffect(() => {
|
||||
if (!canCheckIn) {
|
||||
setCheckInModal(false);
|
||||
checkInForm.resetFields();
|
||||
}
|
||||
}, [canCheckIn, checkInForm]);
|
||||
useEffect(() => {
|
||||
if (!canCheckOut && checkOutModal) {
|
||||
setCheckOutModal(null);
|
||||
checkOutForm.resetFields();
|
||||
}
|
||||
}, [canCheckOut, checkOutModal, checkOutForm]);
|
||||
useEffect(() => {
|
||||
if (!canCheckOut) {
|
||||
setBatchCheckOutModal(false);
|
||||
batchCheckOutForm.resetFields();
|
||||
}
|
||||
}, [canCheckOut, batchCheckOutForm]);
|
||||
useEffect(() => {
|
||||
if (!canTransfer && transferModal) {
|
||||
setTransferModal(null);
|
||||
transferForm.resetFields();
|
||||
}
|
||||
}, [canTransfer, transferModal, transferForm]);
|
||||
|
||||
const activeOccupancyByStudentId = useMemo(() => {
|
||||
const map = new Map<number, any>();
|
||||
@@ -139,7 +162,7 @@ const OccupanciesPage: React.FC = () => {
|
||||
const [occRes, stuRes, rmRes] = (await Promise.allSettled([
|
||||
api.get('/occupancies', {
|
||||
params: {
|
||||
active: showActive ? 'true' : undefined,
|
||||
...occupancyParamsForView(viewMode),
|
||||
dateFrom: dateRange?.[0]?.format('YYYY-MM-DD'),
|
||||
dateTo: dateRange?.[1]?.format('YYYY-MM-DD'),
|
||||
},
|
||||
@@ -161,7 +184,7 @@ const OccupanciesPage: React.FC = () => {
|
||||
message.error('数据加载异常');
|
||||
}
|
||||
setLoading(false);
|
||||
}, [showActive, dateRange]);
|
||||
}, [viewMode, dateRange]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@@ -285,6 +308,7 @@ const OccupanciesPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleBatchCheckOut = async () => {
|
||||
if (batchLoading) return;
|
||||
const values = await batchCheckOutForm.validateFields();
|
||||
setBatchLoading(true);
|
||||
try {
|
||||
@@ -307,6 +331,7 @@ const OccupanciesPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
if (batchLoading) return;
|
||||
setBatchLoading(true);
|
||||
try {
|
||||
const res: any = await api.post('/occupancies/batch-delete', { ids: selectedRowKeys });
|
||||
@@ -320,6 +345,26 @@ const OccupanciesPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchRestore = async () => {
|
||||
if (batchLoading) return;
|
||||
setBatchLoading(true);
|
||||
try {
|
||||
const res = await api.put<{ restored: number; skipped: number }>(
|
||||
'/occupancies/batch-restore',
|
||||
{ ids: selectedRowKeys },
|
||||
);
|
||||
message.success(
|
||||
`已恢复 ${res.restored} 条入住记录${res.skipped ? `,跳过 ${res.skipped} 条` : ''}`,
|
||||
);
|
||||
setSelectedRowKeys([]);
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '批量恢复失败');
|
||||
} finally {
|
||||
setBatchLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{ title: '学生', width: 120, render: (_: any, r: any) => r.student?.name || '-' },
|
||||
@@ -350,7 +395,9 @@ const OccupanciesPage: React.FC = () => {
|
||||
title: '操作',
|
||||
width: 220,
|
||||
render: (_: any, record: any) =>
|
||||
!record.checkOutDate ? (
|
||||
viewPolicy.readonly ? (
|
||||
<Tag color="#999">已归档</Tag>
|
||||
) : !record.checkOutDate ? (
|
||||
<Space>
|
||||
<PermissionButton
|
||||
permission="occupancy:checkout"
|
||||
@@ -394,11 +441,7 @@ const OccupanciesPage: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<InboxOutlined />}
|
||||
>
|
||||
<Button size="small" danger icon={<InboxOutlined />}>
|
||||
归档
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
@@ -407,17 +450,25 @@ const OccupanciesPage: React.FC = () => {
|
||||
),
|
||||
},
|
||||
],
|
||||
[fetchData, setCheckOutModal, checkOutForm, setTransferModal, transferForm],
|
||||
[
|
||||
fetchData,
|
||||
setCheckOutModal,
|
||||
checkOutForm,
|
||||
setTransferModal,
|
||||
transferForm,
|
||||
viewPolicy.readonly,
|
||||
],
|
||||
);
|
||||
|
||||
const rowSelection = useMemo(
|
||||
() => ({
|
||||
selectedRowKeys,
|
||||
onChange: (keys: any[]) => setSelectedRowKeys(keys),
|
||||
// 「在住记录」Tab:禁用已退宿(防止误选用于批量退宿);「全部记录」Tab:均可选用于批量归档
|
||||
getCheckboxProps: (record: any) => (showActive ? { disabled: !!record.checkOutDate } : {}),
|
||||
// 「在住记录」视图禁用已退宿;其余视图中的记录均可选择。
|
||||
getCheckboxProps: (record: any) =>
|
||||
viewMode === 'active' ? { disabled: !!record.checkOutDate } : {},
|
||||
}),
|
||||
[selectedRowKeys, showActive],
|
||||
[selectedRowKeys, viewMode],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -432,12 +483,24 @@ const OccupanciesPage: React.FC = () => {
|
||||
/>
|
||||
<div className="responsive-toolbar">
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<Button type={showActive ? 'primary' : 'default'} onClick={() => setShowActive(true)}>
|
||||
<Button
|
||||
type={viewMode === 'active' ? 'primary' : 'default'}
|
||||
onClick={() => setViewMode('active')}
|
||||
>
|
||||
在住记录
|
||||
</Button>
|
||||
<Button type={!showActive ? 'primary' : 'default'} onClick={() => setShowActive(false)}>
|
||||
<Button
|
||||
type={viewMode === 'all' ? 'primary' : 'default'}
|
||||
onClick={() => setViewMode('all')}
|
||||
>
|
||||
全部记录
|
||||
</Button>
|
||||
<Button
|
||||
type={viewMode === 'archived' ? 'primary' : 'default'}
|
||||
onClick={() => setViewMode('archived')}
|
||||
>
|
||||
已归档
|
||||
</Button>
|
||||
<Input.Search
|
||||
placeholder="搜索学生姓名或房间号"
|
||||
onSearch={setSearchText}
|
||||
@@ -454,29 +517,31 @@ const OccupanciesPage: React.FC = () => {
|
||||
/>
|
||||
</Space>
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<PermissionButton
|
||||
permission="occupancy:checkin"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
checkInForm.resetFields();
|
||||
setAvailableBeds([]);
|
||||
setAvailableLockers([]);
|
||||
setAvailableResourcesLoading(false);
|
||||
const today = dayjs();
|
||||
checkInForm.setFieldsValue({
|
||||
checkInDate: today,
|
||||
billingStartDate: today,
|
||||
stayType: 'short',
|
||||
collectDeposit: true,
|
||||
depositAmount: 500,
|
||||
});
|
||||
setCheckInModal(true);
|
||||
}}
|
||||
>
|
||||
入住登记
|
||||
</PermissionButton>
|
||||
{canCheckIn ? (
|
||||
{viewMode !== 'archived' ? (
|
||||
<PermissionButton
|
||||
permission="occupancy:checkin"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
checkInForm.resetFields();
|
||||
setAvailableBeds([]);
|
||||
setAvailableLockers([]);
|
||||
setAvailableResourcesLoading(false);
|
||||
const today = dayjs();
|
||||
checkInForm.setFieldsValue({
|
||||
checkInDate: today,
|
||||
billingStartDate: today,
|
||||
stayType: 'short',
|
||||
collectDeposit: true,
|
||||
depositAmount: 500,
|
||||
});
|
||||
setCheckInModal(true);
|
||||
}}
|
||||
>
|
||||
入住登记
|
||||
</PermissionButton>
|
||||
) : null}
|
||||
{viewMode !== 'archived' && canCheckIn ? (
|
||||
<>
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
@@ -547,30 +612,34 @@ const OccupanciesPage: React.FC = () => {
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
<PermissionButton
|
||||
permission="occupancy:view"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => {
|
||||
downloadBlob('/occupancies/template', '入住名单导入模板.xlsx').catch(() =>
|
||||
message.error('下载失败'),
|
||||
);
|
||||
}}
|
||||
>
|
||||
下载模板
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
permission="occupancy:view"
|
||||
icon={<ExportOutlined />}
|
||||
onClick={() => {
|
||||
const params = showActive ? '?active=true' : '';
|
||||
const filename = showActive ? '在住记录.xlsx' : '全部入住记录.xlsx';
|
||||
downloadBlob('/occupancies/export' + params, filename).catch(() =>
|
||||
message.error('导出失败'),
|
||||
);
|
||||
}}
|
||||
>
|
||||
导出记录
|
||||
</PermissionButton>
|
||||
{viewMode !== 'archived' ? (
|
||||
<PermissionButton
|
||||
permission="occupancy:view"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => {
|
||||
downloadBlob('/occupancies/template', '入住名单导入模板.xlsx').catch(() =>
|
||||
message.error('下载失败'),
|
||||
);
|
||||
}}
|
||||
>
|
||||
下载模板
|
||||
</PermissionButton>
|
||||
) : null}
|
||||
{viewMode !== 'archived' ? (
|
||||
<PermissionButton
|
||||
permission="occupancy:view"
|
||||
icon={<ExportOutlined />}
|
||||
onClick={() => {
|
||||
const params = viewMode === 'active' ? '?active=true' : '';
|
||||
const filename = viewMode === 'active' ? '在住记录.xlsx' : '全部入住记录.xlsx';
|
||||
downloadBlob('/occupancies/export' + params, filename).catch(() =>
|
||||
message.error('导出失败'),
|
||||
);
|
||||
}}
|
||||
>
|
||||
导出记录
|
||||
</PermissionButton>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
{selectedRowKeys.length > 0 && (
|
||||
@@ -578,7 +647,7 @@ const OccupanciesPage: React.FC = () => {
|
||||
title={
|
||||
<span>
|
||||
已选 <strong>{selectedRowKeys.length}</strong> 条记录
|
||||
{showActive ? (
|
||||
{viewPolicy.batchAction === 'checkout' ? (
|
||||
<PermissionButton
|
||||
permission="occupancy:checkout"
|
||||
type="primary"
|
||||
@@ -594,7 +663,7 @@ const OccupanciesPage: React.FC = () => {
|
||||
>
|
||||
批量退宿
|
||||
</PermissionButton>
|
||||
) : (
|
||||
) : viewPolicy.batchAction === 'archive' ? (
|
||||
canDelete ? (
|
||||
<Popconfirm
|
||||
title={`确定归档选中的 ${selectedRowKeys.length} 条入住记录?在住记录会自动跳过`}
|
||||
@@ -613,7 +682,24 @@ const OccupanciesPage: React.FC = () => {
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null
|
||||
)}
|
||||
) : canDelete ? (
|
||||
<Popconfirm
|
||||
title={`确定恢复选中的 ${selectedRowKeys.length} 条入住记录?`}
|
||||
onConfirm={handleBatchRestore}
|
||||
okText="恢复"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<UndoOutlined />}
|
||||
style={{ marginLeft: 12 }}
|
||||
loading={batchLoading}
|
||||
>
|
||||
批量恢复
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
<Button size="small" onClick={() => setSelectedRowKeys([])} style={{ marginLeft: 8 }}>
|
||||
取消选择
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user