feat: link deposits to bill payment
This commit is contained in:
@@ -26,11 +26,8 @@ import PermissionButton from '../../components/PermissionButton';
|
||||
import { downloadBlob } from '../../utils/download';
|
||||
import { message } from '../../ui/app-message';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
draft: { text: '草稿', color: 'default' },
|
||||
confirmed: { text: '已确认', color: 'blue' },
|
||||
paid: { text: '已支付', color: 'green' },
|
||||
};
|
||||
|
||||
@@ -61,6 +58,7 @@ const buildBillPrintHtml = (bill: any) => {
|
||||
? dayjs(bill.generatedAt).format('YYYY-MM-DD HH:mm')
|
||||
: dayjs().format('YYYY-MM-DD HH:mm');
|
||||
const hasDeposit = Number(bill.availableDeposit || 0) > 0;
|
||||
const depositDeducted = Number(bill.depositDeductedAmount || 0);
|
||||
const items = bill.items || [];
|
||||
|
||||
return `<!doctype html>
|
||||
@@ -93,8 +91,7 @@ const buildBillPrintHtml = (bill: any) => {
|
||||
.amount-summary { font-size: 12px; line-height: 1.75; }
|
||||
.total { color: #007aff; font-size: 14px; font-weight: 700; }
|
||||
.deposit { color: #52c41a; font-size: 11px; }
|
||||
.deposit-applied { color: #fa8c16; font-size: 11px; }
|
||||
.after-deposit { color: #ff3b30; font-size: 14px; font-weight: 700; }
|
||||
.deposit-deducted { color: #fa8c16; font-size: 11px; }
|
||||
table { width: 100%; border-collapse: collapse; table-layout: fixed; margin-top: 8px; }
|
||||
th, td { padding: 5px 6px; border-bottom: 1px solid #ccc; font-size: 9px; line-height: 1.45; text-align: left; vertical-align: top; word-break: break-word; }
|
||||
th { color: #333; font-weight: 700; }
|
||||
@@ -144,10 +141,9 @@ const buildBillPrintHtml = (bill: any) => {
|
||||
<div>个人费用: ${escapeHtml(money(bill.personalAmount))}</div>
|
||||
<div class="total">应付总额: ${escapeHtml(money(bill.totalAmount))}</div>
|
||||
${
|
||||
hasDeposit
|
||||
hasDeposit || depositDeducted > 0
|
||||
? `<div class="deposit">可用押金: ${escapeHtml(money(bill.availableDeposit))}</div>
|
||||
<div class="deposit-applied">押金抵扣: -${escapeHtml(money(bill.depositApplied))}</div>
|
||||
<div class="after-deposit">抵扣后应付: ${escapeHtml(money(bill.amountAfterDeposit ?? bill.totalAmount))}</div>`
|
||||
${depositDeducted > 0 ? `<div class="deposit-deducted">已扣押金: -${escapeHtml(money(depositDeducted))}</div>` : ''}`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
@@ -243,6 +239,16 @@ const BillsPage: React.FC = () => {
|
||||
});
|
||||
}, [bills, searchText, filterStatus]);
|
||||
|
||||
const selectedBillRows = useMemo(
|
||||
() => bills.filter((bill: any) => selectedRows.includes(bill.id)),
|
||||
[bills, selectedRows],
|
||||
);
|
||||
const canBatchConfirm =
|
||||
selectedBillRows.length > 0
|
||||
&& selectedBillRows.every((bill: any) => bill.status === 'draft' && bill.depositSufficient);
|
||||
const canBatchDelete =
|
||||
selectedBillRows.length > 0 && selectedBillRows.every((bill: any) => bill.status !== 'paid');
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (saving) return;
|
||||
|
||||
@@ -250,8 +256,7 @@ const BillsPage: React.FC = () => {
|
||||
const values = await generateForm.validateFields();
|
||||
setSaving(true);
|
||||
const res: any = await api.post('/bills/generate', {
|
||||
periodStart: values.period[0].format('YYYY-MM-DD'),
|
||||
periodEnd: values.period[1].format('YYYY-MM-DD'),
|
||||
billingMonth: values.billingMonth.format('YYYY-MM'),
|
||||
});
|
||||
message.success(res.message || '生成成功');
|
||||
setGenerateModal(false);
|
||||
@@ -282,10 +287,10 @@ const BillsPage: React.FC = () => {
|
||||
const updateStatus = async (id: number, status: string) => {
|
||||
try {
|
||||
await api.put(`/bills/${id}/status`, { status });
|
||||
message.success('状态更新成功');
|
||||
message.success('账单已确认支付,押金已自动扣除');
|
||||
fetchData();
|
||||
if (detailModal?.id === id) {
|
||||
setDetailModal({ ...detailModal, status });
|
||||
void showDetail(id);
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败');
|
||||
@@ -298,7 +303,7 @@ const BillsPage: React.FC = () => {
|
||||
setBatchLoading(true);
|
||||
try {
|
||||
await api.put('/bills/batch/status', { ids: selectedRows, status });
|
||||
message.success(`已批量更新 ${selectedRows.length} 条账单`);
|
||||
message.success(`已确认支付 ${selectedRows.length} 条账单,并自动扣除押金`);
|
||||
setSelectedRows([]);
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
@@ -396,20 +401,15 @@ const BillsPage: React.FC = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '抵扣后应付',
|
||||
dataIndex: 'amountAfterDeposit',
|
||||
width: 130,
|
||||
render: (v: number, r: any) => {
|
||||
const has = Number(r.availableDeposit || 0) > 0;
|
||||
if (!has) return <span style={{ color: '#999' }}>-</span>;
|
||||
const after = Number(v ?? r.totalAmount).toFixed(2);
|
||||
const applied = Number(r.depositApplied || 0).toFixed(2);
|
||||
return (
|
||||
<Tooltip title={`已抵扣押金 ¥${applied}`}>
|
||||
<strong style={{ color: '#fa541c' }}>¥{after}</strong>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
title: '已扣押金',
|
||||
dataIndex: 'depositDeductedAmount',
|
||||
width: 120,
|
||||
render: (v: number) =>
|
||||
Number(v || 0) > 0 ? (
|
||||
<span style={{ color: '#fa8c16' }}>¥{Number(v).toFixed(2)}</span>
|
||||
) : (
|
||||
<span style={{ color: '#999' }}>-</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
@@ -437,23 +437,23 @@ const BillsPage: React.FC = () => {
|
||||
详情
|
||||
</PermissionButton>
|
||||
{record.status === 'draft' && (
|
||||
<PermissionButton
|
||||
permission="bill:confirm"
|
||||
size="small"
|
||||
onClick={() => updateStatus(record.id, 'confirmed')}
|
||||
<Tooltip
|
||||
title={
|
||||
record.depositSufficient
|
||||
? '确认后将自动从该学生押金余额中扣除账单金额'
|
||||
: '押金不足,请先到押金管理收取押金'
|
||||
}
|
||||
>
|
||||
确认
|
||||
</PermissionButton>
|
||||
)}
|
||||
{record.status === 'confirmed' && (
|
||||
<PermissionButton
|
||||
permission="bill:confirm"
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => updateStatus(record.id, 'paid')}
|
||||
>
|
||||
标记已付
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
permission="bill:confirm"
|
||||
size="small"
|
||||
type="primary"
|
||||
disabled={!record.depositSufficient}
|
||||
onClick={() => updateStatus(record.id, 'paid')}
|
||||
>
|
||||
确认支付
|
||||
</PermissionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<PermissionButton
|
||||
permission="bill:export-pdf"
|
||||
@@ -469,7 +469,13 @@ const BillsPage: React.FC = () => {
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
>
|
||||
<PermissionButton permission="bill:delete" size="small" danger icon={<DeleteOutlined />}>
|
||||
<PermissionButton
|
||||
permission="bill:delete"
|
||||
size="small"
|
||||
danger
|
||||
disabled={record.status === 'paid'}
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
删除
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
@@ -507,38 +513,30 @@ const BillsPage: React.FC = () => {
|
||||
onChange={(v) => setFilterStatus(v)}
|
||||
options={[
|
||||
{ value: 'draft', label: '草稿' },
|
||||
{ value: 'confirmed', label: '已确认' },
|
||||
{ value: 'paid', label: '已支付' },
|
||||
]}
|
||||
/>
|
||||
<Select placeholder="费用类型" allowClear style={{ width: 120 }} value={filterExpenseType} onChange={setFilterExpenseType}
|
||||
options={[{value:'water',label:'水费'},{value:'electricity',label:'电费'},{value:'cleaning',label:'保洁费'},{value:'rent',label:'租金'},{value:'other',label:'其他'}]} />
|
||||
<PermissionButton
|
||||
permission="bill:confirm"
|
||||
onClick={() => batchUpdateStatus('confirmed')}
|
||||
disabled={selectedRows.length === 0}
|
||||
>
|
||||
批量确认
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
permission="bill:confirm"
|
||||
type="primary"
|
||||
onClick={() => batchUpdateStatus('paid')}
|
||||
disabled={selectedRows.length === 0}
|
||||
disabled={!canBatchConfirm}
|
||||
>
|
||||
批量标记已付
|
||||
批量确认支付
|
||||
</PermissionButton>
|
||||
<Popconfirm
|
||||
title={`确定删除选中的 ${selectedRows.length} 条账单?`}
|
||||
onConfirm={batchDelete}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
disabled={selectedRows.length === 0}
|
||||
disabled={!canBatchDelete}
|
||||
>
|
||||
<PermissionButton
|
||||
permission="bill:delete"
|
||||
danger
|
||||
disabled={selectedRows.length === 0}
|
||||
disabled={!canBatchDelete}
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
批量删除
|
||||
@@ -595,15 +593,19 @@ const BillsPage: React.FC = () => {
|
||||
>
|
||||
<Form form={generateForm} layout="vertical">
|
||||
<Form.Item
|
||||
name="period"
|
||||
label="账单周期"
|
||||
rules={[{ required: true, message: '请选择账单周期' }]}
|
||||
extra="选择费用对应的时间段,系统将自动计算每个学生的分摊费用"
|
||||
name="billingMonth"
|
||||
label="账单月份"
|
||||
rules={[{ required: true, message: '请选择账单月份' }]}
|
||||
extra="只能选择已结束月份,每个月只能生成一次账单"
|
||||
>
|
||||
<RangePicker
|
||||
<DatePicker
|
||||
style={{ width: '100%' }}
|
||||
placeholder={['开始日期', '结束日期']}
|
||||
format="YYYY-MM-DD"
|
||||
picker="month"
|
||||
placeholder="选择月份"
|
||||
format="YYYY-MM"
|
||||
disabledDate={(current) =>
|
||||
!!current && !current.endOf('month').isBefore(dayjs(), 'day')
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -643,18 +645,20 @@ const BillsPage: React.FC = () => {
|
||||
</strong>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{Number(detailModal.availableDeposit || 0) > 0 && (
|
||||
{(Number(detailModal.availableDeposit || 0) > 0
|
||||
|| Number(detailModal.depositDeductedAmount || 0) > 0
|
||||
|| detailModal.status === 'draft') && (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
padding: 12,
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
background: detailModal.depositSufficient || detailModal.status === 'paid' ? '#f6ffed' : '#fff2f0',
|
||||
border: `1px solid ${detailModal.depositSufficient || detailModal.status === 'paid' ? '#b7eb8f' : '#ffccc7'}`,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 13, color: '#666', marginBottom: 6 }}>
|
||||
押金联动(不影响实际押金状态,仅作收款参考)
|
||||
押金联动(确认支付时实时扣除,不在生成账单时冻结)
|
||||
</div>
|
||||
<Space size={24} wrap>
|
||||
<span>
|
||||
@@ -664,16 +668,9 @@ const BillsPage: React.FC = () => {
|
||||
</strong>
|
||||
</span>
|
||||
<span>
|
||||
本账单可抵扣:
|
||||
已扣押金:
|
||||
<strong style={{ color: '#fa8c16' }}>
|
||||
-¥{Number(detailModal.depositApplied || 0).toFixed(2)}
|
||||
</strong>
|
||||
</span>
|
||||
<span>
|
||||
抵扣后实付:
|
||||
<strong style={{ color: '#fa541c', fontSize: 16 }}>
|
||||
¥
|
||||
{Number(detailModal.amountAfterDeposit ?? detailModal.totalAmount).toFixed(2)}
|
||||
-¥{Number(detailModal.depositDeductedAmount || 0).toFixed(2)}
|
||||
</strong>
|
||||
</span>
|
||||
</Space>
|
||||
|
||||
Reference in New Issue
Block a user