forked from wangziqi/gongxue-base
817 lines
27 KiB
TypeScript
817 lines
27 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||
import {
|
||
Table,
|
||
Modal,
|
||
Form,
|
||
Select,
|
||
DatePicker,
|
||
InputNumber,
|
||
Input,
|
||
Space,
|
||
Tag,
|
||
Popconfirm,
|
||
Card,
|
||
Empty,
|
||
} from 'antd';
|
||
import { PlusOutlined, InboxOutlined, DollarOutlined, TeamOutlined } from '@ant-design/icons';
|
||
import dayjs from 'dayjs';
|
||
import api from '../../api';
|
||
import PermissionButton from '../../components/PermissionButton';
|
||
import EditableCell from '../../components/EditableCell';
|
||
import { message } from '../../ui/app-message';
|
||
import { buildDepositStudentOptions, type DepositStudentLookup } from './deposit-student-option';
|
||
|
||
const statusMap: Record<string, { text: string; color: string }> = {
|
||
paid: { text: '有余额', color: 'green' },
|
||
refunded: { text: '已全退', color: 'blue' },
|
||
depleted: { text: '已扣完', color: 'red' },
|
||
};
|
||
|
||
const installmentStatusMap: Record<string, { text: string; color: string }> = {
|
||
pending: { text: '待缴', color: 'orange' },
|
||
paid: { text: '已缴', color: 'green' },
|
||
};
|
||
|
||
const roomTypeOptions = [
|
||
{ value: '单人间', label: '单人间' },
|
||
{ value: '四人间', label: '四人间' },
|
||
];
|
||
|
||
const suggestedDepositByRoomType: Record<string, number> = {
|
||
单人间: 200,
|
||
四人间: 100,
|
||
};
|
||
|
||
interface DepositRecord {
|
||
id: number;
|
||
studentId: number;
|
||
amount: number;
|
||
status: string;
|
||
paidDate: string;
|
||
refundDate?: string | null;
|
||
notes?: string | null;
|
||
installments?: Array<{
|
||
id: number;
|
||
amount: number;
|
||
dueDate: string;
|
||
paidDate?: string | null;
|
||
status: string;
|
||
}>;
|
||
student?: DepositStudentLookup;
|
||
}
|
||
|
||
interface EligibleStudent {
|
||
studentId: number;
|
||
studentName: string;
|
||
studentNo?: string | null;
|
||
roomId: number;
|
||
roomNumber: string;
|
||
building?: string | null;
|
||
roomType?: string | null;
|
||
capacity: number;
|
||
depositAmount: number;
|
||
}
|
||
|
||
const isFormValidationError = (error: unknown) =>
|
||
typeof error === 'object' &&
|
||
error !== null &&
|
||
Array.isArray((error as { errorFields?: unknown }).errorFields);
|
||
|
||
const DepositsPage: React.FC = () => {
|
||
const [data, setData] = useState<DepositRecord[]>([]);
|
||
const [students, setStudents] = useState<DepositStudentLookup[]>([]);
|
||
const [eligibleStudents, setEligibleStudents] = useState<EligibleStudent[]>([]);
|
||
const [selectedEligibleStudentIds, setSelectedEligibleStudentIds] = useState<number[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [eligibleLoading, setEligibleLoading] = useState(false);
|
||
const [createModal, setCreateModal] = useState(false);
|
||
const [batchModal, setBatchModal] = useState(false);
|
||
const [refundModal, setRefundModal] = useState<DepositRecord | null>(null);
|
||
const [detailModal, setDetailModal] = useState<DepositRecord | null>(null);
|
||
const [installmentModal, setInstallmentModal] = useState<number | null>(null);
|
||
const [createForm] = Form.useForm();
|
||
const [batchForm] = Form.useForm();
|
||
const [refundForm] = Form.useForm();
|
||
const [installmentForm] = Form.useForm();
|
||
const [searchText, setSearchText] = useState('');
|
||
const [filterStatus, setFilterStatus] = useState<string | undefined>(undefined);
|
||
const [filterRoomType, setFilterRoomType] = useState<string | undefined>(undefined);
|
||
const [batchRoomType, setBatchRoomType] = useState<string>('四人间');
|
||
const [saving, setSaving] = useState(false);
|
||
|
||
const fetchData = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
const [d, s] = await Promise.all([
|
||
api.get<DepositRecord[]>('/deposits'),
|
||
api.get<DepositStudentLookup[]>('/deposits/student-lookups'),
|
||
]);
|
||
setData(d);
|
||
setStudents(s);
|
||
} catch (e: any) {
|
||
message.error(e?.message || '加载失败,请稍后重试');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
const fetchEligibleStudents = useCallback(async (roomType?: string) => {
|
||
setEligibleLoading(true);
|
||
try {
|
||
const params = roomType ? `?roomType=${encodeURIComponent(roomType)}` : '';
|
||
const rows = await api.get<EligibleStudent[]>(`/deposits/eligible-students${params}`);
|
||
setEligibleStudents(rows);
|
||
setSelectedEligibleStudentIds(rows.map((item) => item.studentId));
|
||
} catch (e: any) {
|
||
message.error(e?.message || '加载在住人员失败');
|
||
} finally {
|
||
setEligibleLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
fetchData();
|
||
}, [fetchData]);
|
||
|
||
useEffect(() => {
|
||
fetchEligibleStudents(filterRoomType);
|
||
}, [fetchEligibleStudents, filterRoomType]);
|
||
|
||
const depositByStudentId = useMemo(() => {
|
||
const map = new Map<number, DepositRecord>();
|
||
data.forEach((item) => map.set(item.studentId, item));
|
||
return map;
|
||
}, [data]);
|
||
|
||
const filteredData = useMemo(() => {
|
||
if (filterRoomType) {
|
||
const s = searchText.trim().toLowerCase();
|
||
return eligibleStudents
|
||
.filter(
|
||
(item) =>
|
||
!s ||
|
||
item.studentName.toLowerCase().includes(s) ||
|
||
item.studentNo?.toLowerCase().includes(s),
|
||
)
|
||
.map((item) => {
|
||
const deposit = depositByStudentId.get(item.studentId);
|
||
return {
|
||
id: deposit?.id ?? `eligible-${item.studentId}`,
|
||
studentId: item.studentId,
|
||
amount: deposit?.amount ?? item.depositAmount ?? 0,
|
||
status: deposit?.status ?? 'unpaid',
|
||
paidDate: deposit?.paidDate ?? '',
|
||
refundDate: deposit?.refundDate,
|
||
notes: deposit?.notes,
|
||
installments: deposit?.installments ?? [],
|
||
student: {
|
||
id: item.studentId,
|
||
name: item.studentName,
|
||
studentNo: item.studentNo,
|
||
roomType: item.roomType,
|
||
},
|
||
roomNumber: item.roomNumber,
|
||
building: item.building,
|
||
roomType: item.roomType,
|
||
};
|
||
});
|
||
}
|
||
|
||
return data.filter((d) => {
|
||
if (searchText) {
|
||
const s = searchText.toLowerCase();
|
||
if (!d.student?.name?.toLowerCase().includes(s)) return false;
|
||
}
|
||
if (filterStatus && d.status !== filterStatus) return false;
|
||
return true;
|
||
});
|
||
}, [data, depositByStudentId, eligibleStudents, filterRoomType, filterStatus, searchText]);
|
||
|
||
const studentOptions = useMemo(() => buildDepositStudentOptions(students), [students]);
|
||
|
||
const openBatchModal = (roomType = filterRoomType || '四人间') => {
|
||
const amount = suggestedDepositByRoomType[roomType] ?? 100;
|
||
setBatchRoomType(roomType);
|
||
batchForm.resetFields();
|
||
batchForm.setFieldsValue({ roomType, amount, paidDate: dayjs() });
|
||
setBatchModal(true);
|
||
fetchEligibleStudents(roomType);
|
||
};
|
||
|
||
const handleBatchRoomTypeChange = (roomType: string) => {
|
||
setBatchRoomType(roomType);
|
||
batchForm.setFieldsValue({
|
||
amount: suggestedDepositByRoomType[roomType] ?? batchForm.getFieldValue('amount') ?? 100,
|
||
});
|
||
fetchEligibleStudents(roomType);
|
||
};
|
||
|
||
const handleCreate = async () => {
|
||
setSaving(true);
|
||
try {
|
||
const values = await createForm.validateFields();
|
||
await api.post('/deposits', {
|
||
studentId: values.studentId,
|
||
amount: values.amount,
|
||
paidDate: values.paidDate.format('YYYY-MM-DD'),
|
||
notes: values.notes,
|
||
});
|
||
message.success('押金金额已增加');
|
||
setCreateModal(false);
|
||
createForm.resetFields();
|
||
fetchData();
|
||
fetchEligibleStudents(filterRoomType);
|
||
} catch (e: any) {
|
||
if (!isFormValidationError(e)) {
|
||
message.error(e?.message || '操作失败');
|
||
}
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleBatchCreate = async () => {
|
||
if (selectedEligibleStudentIds.length === 0) {
|
||
message.warning('请选择至少一名学生');
|
||
return;
|
||
}
|
||
setSaving(true);
|
||
try {
|
||
const values = await batchForm.validateFields();
|
||
await api.post('/deposits/batch', {
|
||
studentIds: selectedEligibleStudentIds,
|
||
amount: values.amount,
|
||
paidDate: values.paidDate.format('YYYY-MM-DD'),
|
||
notes: values.notes,
|
||
roomType: values.roomType,
|
||
});
|
||
message.success(`已为 ${selectedEligibleStudentIds.length} 人批量收取押金`);
|
||
setBatchModal(false);
|
||
batchForm.resetFields();
|
||
await fetchData();
|
||
fetchEligibleStudents(filterRoomType);
|
||
} catch (e: any) {
|
||
if (!isFormValidationError(e)) {
|
||
message.error(e?.message || '操作失败');
|
||
}
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleRefund = async () => {
|
||
if (!refundModal) return;
|
||
setSaving(true);
|
||
try {
|
||
const values = await refundForm.validateFields();
|
||
await api.put(`/deposits/${refundModal.id}/refund`, {
|
||
refundDate: values.refundDate.format('YYYY-MM-DD'),
|
||
notes: values.notes,
|
||
});
|
||
message.success('退还操作完成');
|
||
setRefundModal(null);
|
||
refundForm.resetFields();
|
||
fetchData();
|
||
fetchEligibleStudents(filterRoomType);
|
||
} catch (e: any) {
|
||
if (!isFormValidationError(e)) {
|
||
message.error(e?.message || '操作失败');
|
||
}
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleAddInstallment = async () => {
|
||
if (installmentModal == null) return;
|
||
try {
|
||
const values = await installmentForm.validateFields();
|
||
await api.post(`/deposits/${installmentModal}/installments`, {
|
||
amount: values.amount,
|
||
dueDate: values.dueDate.format('YYYY-MM-DD'),
|
||
});
|
||
message.success('分期已添加');
|
||
setInstallmentModal(null);
|
||
installmentForm.resetFields();
|
||
fetchData();
|
||
} catch (e: any) {
|
||
if (!isFormValidationError(e)) {
|
||
message.error(e?.message || '操作失败');
|
||
}
|
||
}
|
||
};
|
||
|
||
const handlePayInstallment = async (installmentId: number) => {
|
||
try {
|
||
await api.put(`/deposits/installments/${installmentId}`, {
|
||
paidDate: dayjs().format('YYYY-MM-DD'),
|
||
status: 'paid',
|
||
});
|
||
message.success('分期已标记为已缴');
|
||
fetchData();
|
||
} catch (e: any) {
|
||
message.error(e?.message || '操作失败');
|
||
}
|
||
};
|
||
|
||
const saveInstallmentCell = async (
|
||
installmentId: number,
|
||
field: 'status' | 'paidDate',
|
||
value: unknown,
|
||
) => {
|
||
await api.put(`/deposits/installments/${installmentId}`, { [field]: value });
|
||
message.success('分期记录已保存');
|
||
if (detailModal) {
|
||
const refreshed = await api.get<DepositRecord>(`/deposits/${detailModal.id}`);
|
||
setDetailModal(refreshed);
|
||
}
|
||
await fetchData();
|
||
};
|
||
|
||
const handleDeleteInstallment = async (installmentId: number) => {
|
||
try {
|
||
await api.delete(`/deposits/installments/${installmentId}`);
|
||
message.success('分期已归档');
|
||
fetchData();
|
||
} catch (e: any) {
|
||
message.error(e?.message || '操作失败');
|
||
}
|
||
};
|
||
|
||
const columns = useMemo(
|
||
() => [
|
||
{ title: '学生', width: 120, render: (_: unknown, r: any) => r.student?.name || '-' },
|
||
{
|
||
title: '当前可用押金',
|
||
dataIndex: 'amount',
|
||
width: 130,
|
||
render: (v: number) => `¥${Number(v || 0).toFixed(2)}`,
|
||
},
|
||
{
|
||
title: '房间',
|
||
width: 120,
|
||
render: (_: unknown, r: any) =>
|
||
r.roomNumber ? `${r.building ? `${r.building}-` : ''}${r.roomNumber}` : '-',
|
||
},
|
||
{ title: '房型', dataIndex: 'roomType', width: 100, render: (v: string) => v || '-' },
|
||
{ title: '最近收取日期', dataIndex: 'paidDate', width: 120, render: (v: string) => v || '-' },
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
render: (s: string) =>
|
||
s === 'unpaid' ? (
|
||
<Tag color="default">未缴</Tag>
|
||
) : (
|
||
<Tag color={statusMap[s]?.color}>{statusMap[s]?.text || s}</Tag>
|
||
),
|
||
},
|
||
{ title: '退还日期', dataIndex: 'refundDate', width: 110, render: (v: unknown) => v || '-' },
|
||
{ title: '备注', dataIndex: 'notes', width: 120, render: (v: unknown) => v || '-' },
|
||
{
|
||
title: '操作',
|
||
width: 240,
|
||
render: (_: unknown, record: any) => {
|
||
const hasDeposit = typeof record.id === 'number';
|
||
return (
|
||
<Space>
|
||
{hasDeposit && (
|
||
<PermissionButton
|
||
permission="deposit:view"
|
||
size="small"
|
||
onClick={() => {
|
||
setDetailModal(record);
|
||
}}
|
||
>
|
||
详情
|
||
</PermissionButton>
|
||
)}
|
||
{record.status === 'paid' && hasDeposit && (
|
||
<PermissionButton
|
||
permission="deposit:refund"
|
||
size="small"
|
||
type="primary"
|
||
onClick={() => {
|
||
setRefundModal(record);
|
||
refundForm.setFieldsValue({ refundDate: dayjs() });
|
||
}}
|
||
>
|
||
退还
|
||
</PermissionButton>
|
||
)}
|
||
{hasDeposit && (
|
||
<Popconfirm
|
||
title="确定归档?"
|
||
onConfirm={async () => {
|
||
try {
|
||
await api.delete(`/deposits/${record.id}`);
|
||
message.success('归档成功');
|
||
fetchData();
|
||
fetchEligibleStudents(filterRoomType);
|
||
} catch (e: any) {
|
||
message.error(e?.message || '归档失败');
|
||
}
|
||
}}
|
||
>
|
||
<PermissionButton
|
||
permission="deposit:delete"
|
||
size="small"
|
||
danger
|
||
icon={<InboxOutlined />}
|
||
>
|
||
归档
|
||
</PermissionButton>
|
||
</Popconfirm>
|
||
)}
|
||
</Space>
|
||
);
|
||
},
|
||
},
|
||
],
|
||
[fetchData, fetchEligibleStudents, filterRoomType, refundForm],
|
||
);
|
||
|
||
const eligibleColumns = [
|
||
{
|
||
title: '学生',
|
||
render: (_: unknown, r: EligibleStudent) =>
|
||
`${r.studentName} (${r.studentNo || `#${r.studentId}`})`,
|
||
},
|
||
{
|
||
title: '房间',
|
||
render: (_: unknown, r: EligibleStudent) =>
|
||
`${r.building ? `${r.building}-` : ''}${r.roomNumber}`,
|
||
},
|
||
{ title: '房型', dataIndex: 'roomType' },
|
||
{
|
||
title: '当前押金',
|
||
dataIndex: 'depositAmount',
|
||
render: (v: number) => `¥${Number(v || 0).toFixed(2)}`,
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<div
|
||
style={{
|
||
marginBottom: 16,
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
flexWrap: 'wrap',
|
||
gap: 8,
|
||
}}
|
||
>
|
||
<Space wrap>
|
||
<Input.Search
|
||
placeholder="搜索学生姓名/学号"
|
||
allowClear
|
||
style={{ width: 180 }}
|
||
onSearch={(v) => setSearchText(v)}
|
||
onChange={(e) => {
|
||
setSearchText(e.target.value);
|
||
}}
|
||
/>
|
||
<Select
|
||
placeholder="房型筛选"
|
||
allowClear
|
||
style={{ width: 130 }}
|
||
value={filterRoomType}
|
||
onChange={(v) => setFilterRoomType(v)}
|
||
options={roomTypeOptions}
|
||
/>
|
||
<Select
|
||
placeholder="状态筛选"
|
||
allowClear
|
||
style={{ width: 120 }}
|
||
value={filterStatus}
|
||
disabled={!!filterRoomType}
|
||
onChange={(v) => setFilterStatus(v)}
|
||
options={[
|
||
{ value: 'paid', label: '有余额' },
|
||
{ value: 'refunded', label: '已全退' },
|
||
{ value: 'depleted', label: '已扣完' },
|
||
]}
|
||
/>
|
||
</Space>
|
||
<Space wrap>
|
||
<PermissionButton
|
||
permission="deposit:create"
|
||
icon={<TeamOutlined />}
|
||
onClick={() => openBatchModal()}
|
||
>
|
||
按房型批量收取
|
||
</PermissionButton>
|
||
<PermissionButton
|
||
permission="deposit:create"
|
||
type="primary"
|
||
icon={<PlusOutlined />}
|
||
onClick={() => {
|
||
createForm.resetFields();
|
||
createForm.setFieldsValue({ amount: 500, paidDate: dayjs() });
|
||
setCreateModal(true);
|
||
}}
|
||
>
|
||
收取押金
|
||
</PermissionButton>
|
||
</Space>
|
||
</div>
|
||
<Table
|
||
columns={columns}
|
||
dataSource={filteredData}
|
||
rowKey="id"
|
||
loading={loading || (!!filterRoomType && eligibleLoading)}
|
||
scroll={{ x: 1200 }}
|
||
pagination={{
|
||
defaultPageSize: 15,
|
||
showSizeChanger: true,
|
||
pageSizeOptions: [15, 30, 50, 100],
|
||
showTotal: (total) => `共 ${total} 条`,
|
||
}}
|
||
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||
/>
|
||
|
||
{/* Batch Create Modal */}
|
||
<Modal
|
||
title="按房型批量收取押金"
|
||
open={batchModal}
|
||
onOk={handleBatchCreate}
|
||
onCancel={() => setBatchModal(false)}
|
||
okText="确认批量收取"
|
||
confirmLoading={saving}
|
||
okButtonProps={{ disabled: selectedEligibleStudentIds.length === 0 }}
|
||
width={760}
|
||
>
|
||
<Form form={batchForm} layout="vertical">
|
||
<Space style={{ width: '100%' }} align="start" wrap>
|
||
<Form.Item
|
||
name="roomType"
|
||
label="房型"
|
||
rules={[{ required: true, message: '请选择房型' }]}
|
||
>
|
||
<Select
|
||
style={{ width: 140 }}
|
||
options={roomTypeOptions}
|
||
onChange={handleBatchRoomTypeChange}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="amount"
|
||
label="每人收取金额(元)"
|
||
rules={[{ required: true, message: '请输入金额' }]}
|
||
>
|
||
<InputNumber min={0.01} precision={2} style={{ width: 180 }} />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="paidDate"
|
||
label="收取日期"
|
||
rules={[{ required: true, message: '请选择日期' }]}
|
||
>
|
||
<DatePicker style={{ width: 180 }} placeholder="选择收取日期" format="YYYY-MM-DD" />
|
||
</Form.Item>
|
||
</Space>
|
||
<Form.Item name="notes" label="备注">
|
||
<Input.TextArea rows={2} placeholder={`${batchRoomType}押金`} />
|
||
</Form.Item>
|
||
</Form>
|
||
<div style={{ marginBottom: 8 }}>
|
||
已选择 <strong>{selectedEligibleStudentIds.length}</strong> / {eligibleStudents.length} 人
|
||
{suggestedDepositByRoomType[batchRoomType] && (
|
||
<span style={{ color: '#999', marginLeft: 8 }}>
|
||
建议金额:¥{suggestedDepositByRoomType[batchRoomType]}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<Table
|
||
size="small"
|
||
columns={eligibleColumns}
|
||
dataSource={eligibleStudents}
|
||
rowKey="studentId"
|
||
loading={eligibleLoading}
|
||
locale={{ emptyText: <Empty description="暂无符合条件的在住人员" /> }}
|
||
pagination={{ pageSize: 6, showSizeChanger: false }}
|
||
rowSelection={{
|
||
selectedRowKeys: selectedEligibleStudentIds,
|
||
onChange: (keys) => setSelectedEligibleStudentIds(keys as number[]),
|
||
}}
|
||
/>
|
||
</Modal>
|
||
|
||
{/* Create Modal */}
|
||
<Modal
|
||
title="收取押金"
|
||
open={createModal}
|
||
onOk={handleCreate}
|
||
onCancel={() => setCreateModal(false)}
|
||
okText="确认"
|
||
confirmLoading={saving}
|
||
>
|
||
<Form form={createForm} layout="vertical">
|
||
<Form.Item
|
||
name="studentId"
|
||
label="学生"
|
||
rules={[{ required: true, message: '请选择学生' }]}
|
||
>
|
||
<Select
|
||
showSearch
|
||
optionFilterProp="label"
|
||
placeholder="搜索并选择学生"
|
||
options={studentOptions}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="amount" label="本次收取金额(元)" rules={[{ required: true }]}>
|
||
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="paidDate" label="收取日期" rules={[{ required: true }]}>
|
||
<DatePicker style={{ width: '100%' }} placeholder="选择收取日期" format="YYYY-MM-DD" />
|
||
</Form.Item>
|
||
<Form.Item name="notes" label="备注">
|
||
<Input.TextArea rows={2} />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
{/* Refund Modal */}
|
||
<Modal
|
||
title={`退还押金 - ${refundModal?.student?.name}`}
|
||
open={!!refundModal}
|
||
onOk={handleRefund}
|
||
onCancel={() => setRefundModal(null)}
|
||
okText="确认退还"
|
||
confirmLoading={saving}
|
||
>
|
||
<Form form={refundForm} layout="vertical">
|
||
<div style={{ marginBottom: 16, padding: 12, background: '#f5f5f5', borderRadius: 8 }}>
|
||
当前可用押金: <strong>¥{Number(refundModal?.amount || 0).toFixed(2)}</strong>
|
||
</div>
|
||
<Form.Item name="refundDate" label="退还日期" rules={[{ required: true }]}>
|
||
<DatePicker style={{ width: '100%' }} placeholder="选择退还日期" format="YYYY-MM-DD" />
|
||
</Form.Item>
|
||
<Form.Item name="notes" label="备注">
|
||
<Input.TextArea rows={2} />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
{/* Detail Modal */}
|
||
<Modal
|
||
title={`押金详情 - ${detailModal?.student?.name}`}
|
||
open={!!detailModal}
|
||
onCancel={() => setDetailModal(null)}
|
||
footer={null}
|
||
width={640}
|
||
>
|
||
{detailModal && (
|
||
<div>
|
||
<Card size="small" style={{ marginBottom: 16 }}>
|
||
<p>
|
||
<strong>当前可用押金:</strong> ¥{Number(detailModal.amount).toFixed(2)}
|
||
</p>
|
||
<p>
|
||
<strong>最近收取日期:</strong> {detailModal.paidDate}
|
||
</p>
|
||
<p>
|
||
<strong>状态:</strong>{' '}
|
||
<Tag color={statusMap[detailModal.status]?.color}>
|
||
{statusMap[detailModal.status]?.text || detailModal.status}
|
||
</Tag>
|
||
</p>
|
||
{detailModal.notes && (
|
||
<p>
|
||
<strong>备注:</strong> {detailModal.notes}
|
||
</p>
|
||
)}
|
||
</Card>
|
||
|
||
{/* Installments Section */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
marginBottom: 8,
|
||
}}
|
||
>
|
||
<h4 style={{ margin: 0 }}>分期记录</h4>
|
||
<PermissionButton
|
||
permission="deposit:edit"
|
||
size="small"
|
||
type="primary"
|
||
icon={<PlusOutlined />}
|
||
onClick={() => {
|
||
setInstallmentModal(detailModal.id);
|
||
installmentForm.resetFields();
|
||
}}
|
||
>
|
||
添加分期
|
||
</PermissionButton>
|
||
</div>
|
||
{detailModal.installments && detailModal.installments.length > 0 ? (
|
||
<Table
|
||
size="small"
|
||
pagination={false}
|
||
rowKey="id"
|
||
dataSource={detailModal.installments}
|
||
columns={[
|
||
{
|
||
title: '金额',
|
||
dataIndex: 'amount',
|
||
render: (value: number) => `¥${Number(value).toFixed(2)}`,
|
||
},
|
||
{ title: '到期日', dataIndex: 'dueDate' },
|
||
{
|
||
title: '实付日',
|
||
dataIndex: 'paidDate',
|
||
render: (value: string, item: any) => (
|
||
<EditableCell
|
||
value={value}
|
||
editor="date"
|
||
permission="deposit:edit"
|
||
onSave={(next) => saveInstallmentCell(item.id, 'paidDate', next)}
|
||
>
|
||
{value || '-'}
|
||
</EditableCell>
|
||
),
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
render: (value: string, item: any) => (
|
||
<EditableCell
|
||
value={value}
|
||
editor="select"
|
||
options={[
|
||
{ value: 'pending', label: '待缴' },
|
||
{ value: 'paid', label: '已缴' },
|
||
{ value: 'overdue', label: '逾期' },
|
||
]}
|
||
permission="deposit:edit"
|
||
onSave={(next) => saveInstallmentCell(item.id, 'status', next)}
|
||
>
|
||
<Tag color={installmentStatusMap[value]?.color}>
|
||
{installmentStatusMap[value]?.text || value}
|
||
</Tag>
|
||
</EditableCell>
|
||
),
|
||
},
|
||
{
|
||
title: '操作',
|
||
render: (_: unknown, item: any) => (
|
||
<Space>
|
||
{item.status === 'pending' && (
|
||
<PermissionButton
|
||
permission="deposit:edit"
|
||
size="small"
|
||
type="primary"
|
||
icon={<DollarOutlined />}
|
||
onClick={() => handlePayInstallment(item.id)}
|
||
>
|
||
标记已缴
|
||
</PermissionButton>
|
||
)}
|
||
<Popconfirm
|
||
title="确定归档?"
|
||
onConfirm={() => handleDeleteInstallment(item.id)}
|
||
>
|
||
<PermissionButton
|
||
permission="deposit:delete"
|
||
size="small"
|
||
danger
|
||
icon={<InboxOutlined />}
|
||
>
|
||
归档
|
||
</PermissionButton>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
) : (
|
||
<p style={{ color: '#999' }}>暂无分期记录</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
</Modal>
|
||
|
||
{/* Add Installment Modal */}
|
||
<Modal
|
||
title="添加分期"
|
||
open={installmentModal != null}
|
||
onOk={handleAddInstallment}
|
||
onCancel={() => setInstallmentModal(null)}
|
||
okText="确认"
|
||
>
|
||
<Form form={installmentForm} layout="vertical">
|
||
<Form.Item name="amount" label="分期金额(元)" rules={[{ required: true }]}>
|
||
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="dueDate" label="到期日期" rules={[{ required: true }]}>
|
||
<DatePicker style={{ width: '100%' }} placeholder="选择到期日期" format="YYYY-MM-DD" />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default DepositsPage;
|