Files
gongxue-base/apps/admin/src/pages/Deposits/DepositModals.tsx

430 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import {
Card,
DatePicker,
Empty,
Form,
Input,
InputNumber,
Modal,
Popconfirm,
Select,
Space,
Table,
Tag,
} from 'antd';
import { DollarOutlined, InboxOutlined, PlusOutlined } from '@ant-design/icons';
import PermissionButton from '../../components/PermissionButton';
import EditableCell from '../../components/EditableCell';
import type { DepositStudentLookup } from './deposit-student-option';
import { useSubmitShortcut } from '../../hooks/useSubmitShortcut';
export 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;
}
export interface EligibleStudent {
studentId: number;
studentName: string;
studentNo?: string | null;
roomId: number;
roomNumber: string;
building?: string | null;
roomType?: string | null;
capacity: number;
depositAmount: number;
}
export const statusMap: Record<string, { text: string; color: string }> = {
paid: { text: '有余额', color: 'green' },
refunded: { text: '已全退', color: 'blue' },
depleted: { text: '已扣完', color: 'red' },
};
export const installmentStatusMap: Record<string, { text: string; color: string }> = {
pending: { text: '待缴', color: 'orange' },
paid: { text: '已缴', color: 'green' },
};
export const roomTypeOptions = [
{ value: '单人间', label: '单人间' },
{ value: '四人间', label: '四人间' },
];
export const suggestedDepositByRoomType: Record<string, number> = {
单人间: 200,
四人间: 100,
};
export interface DepositModalsProps {
batchModal: boolean;
createModal: boolean;
refundModal: DepositRecord | null;
detailModal: DepositRecord | null;
installmentModal: number | null;
batchForm: ReturnType<typeof Form.useForm>[0];
createForm: ReturnType<typeof Form.useForm>[0];
refundForm: ReturnType<typeof Form.useForm>[0];
installmentForm: ReturnType<typeof Form.useForm>[0];
saving: boolean;
batchRoomType: string;
effectiveSelectedEligibleIds: number[];
eligibleStudents: EligibleStudent[];
eligibleLoading: boolean;
eligibleColumns: Array<{ title: string; render?: unknown; dataIndex?: string }>;
studentOptions: Array<{ value: number; label: string }>;
onBatchRoomTypeChange: (roomType: string) => void;
onBatchCreate: () => void;
onCreate: () => void;
onRefund: () => void;
onAddInstallment: () => void;
onPayInstallment: (installmentId: number) => void;
onSaveInstallmentCell: (
installmentId: number,
field: 'status' | 'paidDate',
value: unknown,
) => void;
onDeleteInstallment: (installmentId: number) => void;
onCloseBatch: () => void;
onCloseCreate: () => void;
onCloseRefund: () => void;
onCloseDetail: () => void;
onCloseInstallment: () => void;
onOpenInstallment: (id: number) => void;
onSelectEligible: (ids: number[]) => void;
}
export const DepositModals: React.FC<DepositModalsProps> = ({
batchModal,
createModal,
refundModal,
detailModal,
installmentModal,
batchForm,
createForm,
refundForm,
installmentForm,
saving,
batchRoomType,
effectiveSelectedEligibleIds,
eligibleStudents,
eligibleLoading,
eligibleColumns,
studentOptions,
onBatchRoomTypeChange,
onBatchCreate,
onCreate,
onRefund,
onAddInstallment,
onPayInstallment,
onSaveInstallmentCell,
onDeleteInstallment,
onCloseBatch,
onCloseCreate,
onCloseRefund,
onCloseDetail,
onCloseInstallment,
onOpenInstallment,
onSelectEligible,
}) => {
useSubmitShortcut(batchModal && !saving, onBatchCreate);
useSubmitShortcut(createModal && !saving, onCreate);
useSubmitShortcut(!!refundModal && !saving, onRefund);
useSubmitShortcut(!!installmentModal && !saving, onAddInstallment);
return (
<>
<Modal
title="按房型批量收取押金"
open={batchModal}
onOk={onBatchCreate}
onCancel={onCloseBatch}
okText="确认批量收取"
confirmLoading={saving}
okButtonProps={{ disabled: effectiveSelectedEligibleIds.length === 0 }}
width={760}
>
<Form form={batchForm} layout="vertical" scrollToFirstError>
<Space style={{ width: '100%' }} align="start" wrap>
<Form.Item
name="roomType"
label="房型"
rules={[{ required: true, message: '请选择房型' }]}
>
<Select
style={{ width: 140 }}
options={roomTypeOptions}
onChange={onBatchRoomTypeChange}
/>
</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>{effectiveSelectedEligibleIds.length}</strong> / {eligibleStudents.length}{' '}
{suggestedDepositByRoomType[batchRoomType] && (
<span style={{ color: '#999', marginLeft: 8 }}>
¥{suggestedDepositByRoomType[batchRoomType]}
</span>
)}
</div>
<Table scroll={{ x: 'max-content' }}
size="small"
columns={eligibleColumns as never}
dataSource={eligibleStudents}
rowKey="studentId"
loading={eligibleLoading}
locale={{ emptyText: <Empty description="暂无符合条件的在住人员" /> }}
pagination={{ pageSize: 6, showSizeChanger: false }}
rowSelection={{
selectedRowKeys: effectiveSelectedEligibleIds,
onChange: (keys) => onSelectEligible(keys as number[]),
}}
/>
</Modal>
<Modal
title="收取押金"
open={createModal}
onOk={onCreate}
onCancel={onCloseCreate}
okText="确认"
confirmLoading={saving}
>
<Form form={createForm} layout="vertical" scrollToFirstError>
<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>
<Modal
title={`退还押金 - ${refundModal?.student?.name}`}
open={!!refundModal}
onOk={onRefund}
onCancel={onCloseRefund}
okText="确认退还"
confirmLoading={saving}
>
<Form form={refundForm} layout="vertical" scrollToFirstError>
<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>
<Modal
title={`押金详情 - ${detailModal?.student?.name}`}
open={!!detailModal}
onCancel={onCloseDetail}
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>
<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={() => onOpenInstallment(detailModal.id)}
>
</PermissionButton>
</div>
{detailModal.installments && detailModal.installments.length > 0 ? (
<Table scroll={{ x: 'max-content' }}
size="small"
pagination={false}
rowKey="id"
dataSource={detailModal.installments}
columns={[
{
title: '金额',
dataIndex: 'amount',
render: (value: number) => `¥${value.toFixed(2)}`,
},
{ title: '到期日', dataIndex: 'dueDate' },
{
title: '实付日',
dataIndex: 'paidDate',
render: (value: string, item: any) => (
<EditableCell
value={value}
editor="date"
permission="deposit:edit"
onSave={async (next) =>
onSaveInstallmentCell(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={async (next) =>
onSaveInstallmentCell(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={() => onPayInstallment(item.id)}
>
</PermissionButton>
)}
<Popconfirm
title="确定归档?"
onConfirm={() => onDeleteInstallment(item.id)}
>
<PermissionButton
permission="deposit:delete"
size="small"
danger
icon={<InboxOutlined />}
>
</PermissionButton>
</Popconfirm>
</Space>
),
},
]}
/>
) : (
<p style={{ color: '#999' }}></p>
)}
</div>
)}
</Modal>
<Modal
title="添加分期"
open={installmentModal != null}
onOk={onAddInstallment}
onCancel={onCloseInstallment}
okText="确认"
confirmLoading={saving}
>
<Form form={installmentForm} layout="vertical" scrollToFirstError>
<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>
</>
);
};