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 = { paid: { text: '有余额', color: 'green' }, refunded: { text: '已全退', color: 'blue' }, depleted: { text: '已扣完', color: 'red' }, }; const installmentStatusMap: Record = { pending: { text: '待缴', color: 'orange' }, paid: { text: '已缴', color: 'green' }, }; const roomTypeOptions = [ { value: '单人间', label: '单人间' }, { value: '四人间', label: '四人间' }, ]; const suggestedDepositByRoomType: Record = { 单人间: 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([]); const [students, setStudents] = useState([]); const [eligibleStudents, setEligibleStudents] = useState([]); const [selectedEligibleStudentIds, setSelectedEligibleStudentIds] = useState([]); const [loading, setLoading] = useState(false); const [eligibleLoading, setEligibleLoading] = useState(false); const [createModal, setCreateModal] = useState(false); const [batchModal, setBatchModal] = useState(false); const [refundModal, setRefundModal] = useState(null); const [detailModal, setDetailModal] = useState(null); const [installmentModal, setInstallmentModal] = useState(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(undefined); const [filterRoomType, setFilterRoomType] = useState(undefined); const [batchRoomType, setBatchRoomType] = useState('四人间'); const [saving, setSaving] = useState(false); const fetchData = useCallback(async () => { setLoading(true); try { const [d, s] = await Promise.all([ api.get('/deposits'), api.get('/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(`/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(); 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(`/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' ? ( 未缴 ) : ( {statusMap[s]?.text || s} ), }, { 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 ( {hasDeposit && ( { setDetailModal(record); }} > 详情 )} {record.status === 'paid' && hasDeposit && ( { setRefundModal(record); refundForm.setFieldsValue({ refundDate: dayjs() }); }} > 退还 )} {hasDeposit && ( { try { await api.delete(`/deposits/${record.id}`); message.success('归档成功'); fetchData(); fetchEligibleStudents(filterRoomType); } catch (e: any) { message.error(e?.message || '归档失败'); } }} > } > 归档 )} ); }, }, ], [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 (
setSearchText(v)} onChange={(e) => { setSearchText(e.target.value); }} /> setFilterStatus(v)} options={[ { value: 'paid', label: '有余额' }, { value: 'refunded', label: '已全退' }, { value: 'depleted', label: '已扣完' }, ]} /> } onClick={() => openBatchModal()} > 按房型批量收取 } onClick={() => { createForm.resetFields(); createForm.setFieldsValue({ amount: 500, paidDate: dayjs() }); setCreateModal(true); }} > 收取押金
`共 ${total} 条`, }} locale={{ emptyText: }} /> {/* Batch Create Modal */} setBatchModal(false)} okText="确认批量收取" confirmLoading={saving} okButtonProps={{ disabled: selectedEligibleStudentIds.length === 0 }} width={760} >
}} pagination={{ pageSize: 6, showSizeChanger: false }} rowSelection={{ selectedRowKeys: selectedEligibleStudentIds, onChange: (keys) => setSelectedEligibleStudentIds(keys as number[]), }} /> {/* Create Modal */} setCreateModal(false)} okText="确认" confirmLoading={saving} >
`¥${Number(value).toFixed(2)}`, }, { title: '到期日', dataIndex: 'dueDate' }, { title: '实付日', dataIndex: 'paidDate', render: (value: string, item: any) => ( saveInstallmentCell(item.id, 'paidDate', next)} > {value || '-'} ), }, { title: '状态', dataIndex: 'status', render: (value: string, item: any) => ( saveInstallmentCell(item.id, 'status', next)} > {installmentStatusMap[value]?.text || value} ), }, { title: '操作', render: (_: unknown, item: any) => ( {item.status === 'pending' && ( } onClick={() => handlePayInstallment(item.id)} > 标记已缴 )} handleDeleteInstallment(item.id)} > } > 归档 ), }, ]} /> ) : (

暂无分期记录

)} )} {/* Add Installment Modal */} setInstallmentModal(null)} okText="确认" > ); }; export default DepositsPage;