refactor: resolve remaining field audit issues
This commit is contained in:
@@ -15,7 +15,7 @@ import {
|
||||
Tooltip,
|
||||
Empty,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, UploadOutlined, DeleteOutlined, FileTextOutlined } from '@ant-design/icons';
|
||||
import { PlusOutlined, UploadOutlined, DeleteOutlined, FileTextOutlined, StopOutlined, CheckOutlined } from '@ant-design/icons';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import api from '../../api';
|
||||
import { downloadBlob } from '../../utils/download';
|
||||
@@ -39,6 +39,7 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
const [editing, setEditing] = useState<any>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [filterMonth, setFilterMonth] = useState<Dayjs | null>(null);
|
||||
const [filterStatus, setFilterStatus] = useState<string | undefined>();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [unavailableDates, setUnavailableDates] = useState<Set<string>>(new Set());
|
||||
@@ -48,20 +49,22 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
const selectedClassroomId = Form.useWatch('classroomId', form);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!searchText) return data;
|
||||
const s = searchText.toLowerCase();
|
||||
return data.filter((r: any) => {
|
||||
if (filterStatus && r.effectiveStatus !== filterStatus) return false;
|
||||
if (!searchText) return true;
|
||||
const s = searchText.toLowerCase();
|
||||
const matchClassroom = r.classroom?.name?.toLowerCase().includes(s);
|
||||
const matchOrganization = r.lesseeOrganization?.name?.toLowerCase().includes(s);
|
||||
return matchClassroom || matchOrganization;
|
||||
});
|
||||
}, [data, searchText]);
|
||||
}, [data, searchText, filterStatus]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: any = {};
|
||||
if (filterMonth) params.month = filterMonth.format('YYYY-MM');
|
||||
params.includeEnded = true;
|
||||
const res: any = await api.get('/classroom-rentals', { params });
|
||||
setData(res);
|
||||
} catch (e: any) {
|
||||
@@ -215,6 +218,16 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRentalAction = async (id: number, action: 'cancel' | 'end') => {
|
||||
try {
|
||||
await api.put(`/classroom-rentals/${id}/${action}`);
|
||||
message.success(action === 'cancel' ? '租赁已取消' : '租赁已结束');
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadContract = async (id: number, filename?: string) => {
|
||||
try {
|
||||
await downloadBlob(`/classroom-rentals/${id}/contract`, filename || `contract-${id}.pdf`);
|
||||
@@ -308,6 +321,19 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
width: 100,
|
||||
render: (v: any) => (v ? `¥${v}` : '-'),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'effectiveStatus',
|
||||
width: 90,
|
||||
render: (status: string) => {
|
||||
const config: Record<string, { text: string; color: string }> = {
|
||||
active: { text: '进行中', color: 'green' },
|
||||
ended: { text: '已结束', color: 'default' },
|
||||
cancelled: { text: '已取消', color: 'red' },
|
||||
};
|
||||
return <Tag color={config[status]?.color}>{config[status]?.text || status}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '合同',
|
||||
width: 120,
|
||||
@@ -364,21 +390,30 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
width: 150,
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
<PermissionButton
|
||||
permission="rental:edit"
|
||||
size="small"
|
||||
onClick={() => openEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
<Popconfirm
|
||||
title="确定删除该租赁订单?合同文件将一并删除。"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<PermissionButton permission="rental:delete" size="small" danger>
|
||||
删除
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
{record.effectiveStatus === 'active' && (
|
||||
<>
|
||||
<PermissionButton permission="rental:edit" size="small" onClick={() => openEdit(record)}>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
<Popconfirm title="确定取消该租赁?" onConfirm={() => handleRentalAction(record.id, 'cancel')}>
|
||||
<PermissionButton permission="rental:edit" size="small" danger icon={<StopOutlined />}>
|
||||
取消
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
{!dayjs(record.startDate).isAfter(dayjs(), 'day') && (
|
||||
<Popconfirm title="确定今天结束该租赁?" onConfirm={() => handleRentalAction(record.id, 'end')}>
|
||||
<PermissionButton permission="rental:edit" size="small" icon={<CheckOutlined />}>
|
||||
结束
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{record.effectiveStatus !== 'active' && (
|
||||
<Popconfirm title="确定删除该租赁订单?合同文件将一并删除。" onConfirm={() => handleDelete(record.id)}>
|
||||
<PermissionButton permission="rental:delete" size="small" danger>删除</PermissionButton>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -415,6 +450,18 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
allowClear
|
||||
format="YYYY-MM"
|
||||
/>
|
||||
<Select
|
||||
placeholder="状态"
|
||||
allowClear
|
||||
style={{ width: 110 }}
|
||||
value={filterStatus}
|
||||
onChange={setFilterStatus}
|
||||
options={[
|
||||
{ value: 'active', label: '进行中' },
|
||||
{ value: 'ended', label: '已结束' },
|
||||
{ value: 'cancelled', label: '已取消' },
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
<PermissionButton
|
||||
permission="rental:create"
|
||||
@@ -459,7 +506,7 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
optionFilterProp="label"
|
||||
placeholder="选择教室"
|
||||
onChange={handleClassroomChange}
|
||||
options={classrooms.map((c) => ({
|
||||
options={classrooms.filter((c) => c.status === 'available').map((c) => ({
|
||||
value: c.id,
|
||||
label: `${c.building ? c.building + ' · ' : ''}${c.name}(${c.roomType})`,
|
||||
}))}
|
||||
|
||||
@@ -61,7 +61,7 @@ const ClassroomsPage: React.FC = () => {
|
||||
const filteredData = useMemo(() => {
|
||||
let result = data;
|
||||
if (searchText) { const s = searchText.toLowerCase(); result = result.filter((d: Record<string, unknown>) => (typeof d.name === 'string' && d.name.toLowerCase().includes(s)) || (typeof d.building === 'string' && d.building.toLowerCase().includes(s))); }
|
||||
if (filterStatus) result = result.filter((d: Record<string, unknown>) => d.status === filterStatus);
|
||||
if (filterStatus) result = result.filter((d: Record<string, unknown>) => d.effectiveStatus === filterStatus);
|
||||
return result;
|
||||
}, [data, searchText, filterStatus]);
|
||||
|
||||
@@ -157,11 +157,11 @@ const ClassroomsPage: React.FC = () => {
|
||||
{
|
||||
title: '状态', width: 100,
|
||||
dataIndex: 'status',
|
||||
render: (s: string, record: { currentUsage?: CurrentUsage | null }) => {
|
||||
const effectiveStatus = record.currentUsage ? 'in_use' : s;
|
||||
render: (_s: string, record: { effectiveStatus?: string; status: string; currentUsage?: CurrentUsage | null }) => {
|
||||
const effectiveStatus = record.effectiveStatus || record.status;
|
||||
return (
|
||||
<Tooltip title={record.currentUsage ? `${record.currentUsage.title} (${record.currentUsage.startTime}-${record.currentUsage.endTime})` : undefined}>
|
||||
<Tag color={statusMap[effectiveStatus]?.color}>{statusMap[effectiveStatus]?.text || s}</Tag>
|
||||
<Tag color={statusMap[effectiveStatus]?.color}>{statusMap[effectiveStatus]?.text || effectiveStatus}</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
@@ -228,7 +228,7 @@ const ClassroomsPage: React.FC = () => {
|
||||
if (!e.target.value) setSearchText('');
|
||||
}}
|
||||
/>
|
||||
<Select placeholder="状态" allowClear style={{ width: 110 }} value={filterStatus} onChange={setFilterStatus} options={[{value:'available',label:'可用'},{value:'in_use',label:'使用中'},{value:'reserved',label:'已预留'},{value:'maintenance',label:'维护中'}]} />
|
||||
<Select placeholder="状态" allowClear style={{ width: 110 }} value={filterStatus} onChange={setFilterStatus} options={[{value:'available',label:'可用'},{value:'in_use',label:'使用中'},{value:'reserved',label:'已预留'},{value:'maintenance',label:'维护中'},{value:'archived',label:'已归档'}]} />
|
||||
<Button
|
||||
type={showArchived ? 'primary' : 'default'}
|
||||
onClick={() => setShowArchived(!showArchived)}
|
||||
@@ -323,6 +323,16 @@ const ClassroomsPage: React.FC = () => {
|
||||
<Form.Item name="capacity" label="容量">
|
||||
<InputNumber min={1} max={500} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
{editing && (
|
||||
<Form.Item name="status" label="基础状态">
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'available', label: '可用' },
|
||||
{ value: 'maintenance', label: '维护中' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="notes" label="备注">
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -159,13 +159,17 @@ const RoomsPage: React.FC = () => {
|
||||
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields();
|
||||
const payload = {
|
||||
...values,
|
||||
gender: values.gender === '__unset__' ? null : values.gender,
|
||||
};
|
||||
setSaving(true);
|
||||
try {
|
||||
if (editing) {
|
||||
await api.put(`/rooms/${editing.id}`, values);
|
||||
await api.put(`/rooms/${editing.id}`, payload);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await api.post('/rooms', values);
|
||||
await api.post('/rooms', payload);
|
||||
message.success('创建成功');
|
||||
}
|
||||
setModalOpen(false);
|
||||
@@ -341,7 +345,8 @@ const RoomsPage: React.FC = () => {
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
width: 80,
|
||||
render: (v: any) => (v ? <Tag color={v === '男' ? 'blue' : 'pink'}>{v}</Tag> : '-'),
|
||||
render: (v: string | null) =>
|
||||
v ? <Tag color={v === '男' ? 'blue' : 'pink'}>{v}生宿舍</Tag> : '未指定',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
@@ -386,7 +391,7 @@ const RoomsPage: React.FC = () => {
|
||||
onClick={() => {
|
||||
const rec = record as { id: number };
|
||||
setEditing(rec);
|
||||
form.setFieldsValue(rec);
|
||||
form.setFieldsValue({ ...rec, gender: (record as any).gender ?? '__unset__' });
|
||||
setModalOpen(true);
|
||||
}}
|
||||
>
|
||||
@@ -468,6 +473,7 @@ const RoomsPage: React.FC = () => {
|
||||
onClick={() => {
|
||||
setEditing(null);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ gender: '__unset__' });
|
||||
setModalOpen(true);
|
||||
}}
|
||||
>
|
||||
@@ -571,6 +577,15 @@ const RoomsPage: React.FC = () => {
|
||||
placeholder="留空自动解析"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="gender" label="宿舍性别">
|
||||
<Select
|
||||
options={[
|
||||
{ value: '男', label: '男生宿舍' },
|
||||
{ value: '女', label: '女生宿舍' },
|
||||
{ value: '__unset__', label: '未指定(首位入住者确定)' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="rentalCategory" label="租赁类别">
|
||||
<Select
|
||||
allowClear
|
||||
@@ -617,6 +632,7 @@ const RoomsPage: React.FC = () => {
|
||||
<div><strong>楼栋:</strong>{drawerRoom.building || '-'}</div>
|
||||
<div><strong>楼层:</strong>{drawerRoom.floor ?? '-'}</div>
|
||||
<div><strong>类型:</strong>{drawerRoom.roomType || '-'}</div>
|
||||
<div><strong>宿舍性别:</strong>{drawerRoom.gender ? `${drawerRoom.gender}生宿舍` : '未指定'}</div>
|
||||
<div><strong>额定人数:</strong>{drawerRoom.capacity}</div>
|
||||
<div><strong>租赁类别:</strong>{drawerRoom.rentalCategory === 'long' ? '长租' : '短租'}</div>
|
||||
<div><strong>月租金:</strong>{drawerRoom.monthlyRate ? `¥${drawerRoom.monthlyRate}` : '-'}</div>
|
||||
|
||||
Reference in New Issue
Block a user