From 3c4e3bf16217b64a9d04e1f706b42df1f2f7dbcc Mon Sep 17 00:00:00 2001 From: xiong Date: Tue, 14 Jul 2026 18:05:04 +0800 Subject: [PATCH] feat: implement utility recharge feature with CRUD operations and balance management - Added UtilityRecharge entity to manage utility recharge records. - Created DTO for utility recharge with validation. - Developed UtilityBalancesService to handle business logic for utility recharges and balances. - Implemented UtilityBalancesController for API endpoints related to utility recharges. - Introduced utility balance management in BillsService to reflect utility charges in bills. - Updated DepositsService to include personal expense amounts in deposit records. - Created frontend page for managing utility balances and recharges with Ant Design components. - Refactored existing bill generation logic to exclude personal expenses and integrate utility charges. - Updated tests to cover new utility recharge functionality and ensure existing features remain intact. --- apps/admin/src/App.tsx | 9 + apps/admin/src/auth/menu-policy.ts | 1 + apps/admin/src/layouts/MainLayout.tsx | 1 + apps/admin/src/pages/Bills/index.tsx | 144 ++++++------- apps/admin/src/pages/Deposits/index.tsx | 25 ++- .../admin/src/pages/UtilityBalances/index.tsx | 190 ++++++++++++++++++ apps/server/src/app.module.ts | 4 + apps/server/src/bills/bills-export.service.ts | 116 ++++++----- apps/server/src/bills/bills.module.ts | 6 +- apps/server/src/bills/bills.service.spec.ts | 62 +----- apps/server/src/bills/bills.service.ts | 131 +++++------- .../src/deposits/deposits.lookups.spec.ts | 2 +- apps/server/src/deposits/deposits.module.ts | 7 +- .../src/deposits/deposits.refund.spec.ts | 2 +- apps/server/src/deposits/deposits.service.ts | 34 +++- apps/server/src/entities/index.ts | 1 + .../src/entities/utility-recharge.entity.ts | 37 ++++ .../expense-types/expense-types.service.ts | 3 +- apps/server/src/expenses/expenses.service.ts | 59 ++---- .../dto/utility-recharge.dto.ts | 17 ++ .../utility-balances.controller.ts | 71 +++++++ .../utility-balances.module.ts | 16 ++ .../utility-balances.service.ts | 102 ++++++++++ 23 files changed, 719 insertions(+), 321 deletions(-) create mode 100644 apps/admin/src/pages/UtilityBalances/index.tsx create mode 100644 apps/server/src/entities/utility-recharge.entity.ts create mode 100644 apps/server/src/utility-balances/dto/utility-recharge.dto.ts create mode 100644 apps/server/src/utility-balances/utility-balances.controller.ts create mode 100644 apps/server/src/utility-balances/utility-balances.module.ts create mode 100644 apps/server/src/utility-balances/utility-balances.service.ts diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 70a01af1..867a7634 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -13,6 +13,7 @@ const StudentsPage = lazy(() => import('./pages/Students')); const RoomsPage = lazy(() => import('./pages/Rooms')); const OccupanciesPage = lazy(() => import('./pages/Occupancies')); const ExpensesPage = lazy(() => import('./pages/Expenses')); +const UtilityBalancesPage = lazy(() => import('./pages/UtilityBalances')); const BillsPage = lazy(() => import('./pages/Bills')); const RoomVisualPage = lazy(() => import('./pages/RoomVisual')); const OperationLogsPage = lazy(() => import('./pages/OperationLogs')); @@ -126,6 +127,14 @@ const App: React.FC = () => { } /> + + + + } + /> = { overview: , occupancy: , expense: , + utility: , bill: , deposit: , classroom: , diff --git a/apps/admin/src/pages/Bills/index.tsx b/apps/admin/src/pages/Bills/index.tsx index bccffead..36f7cb64 100644 --- a/apps/admin/src/pages/Bills/index.tsx +++ b/apps/admin/src/pages/Bills/index.tsx @@ -10,7 +10,6 @@ import { Popconfirm, Input, Select, - Tooltip, Spin, Empty, } from 'antd'; @@ -35,8 +34,7 @@ const statusMap: Record = { }; const typeMap: Record = { - water: '水费', - electricity: '电费', + utility: '水电费', cleaning: '保洁费', rent: '租金', damage: '损坏赔偿', @@ -60,7 +58,6 @@ const buildBillPrintHtml = (bill: any) => { const generatedAt = bill.generatedAt ? dayjs(bill.generatedAt).format('YYYY-MM-DD HH:mm') : dayjs().format('YYYY-MM-DD HH:mm'); - const hasDeposit = Number(bill.availableDeposit || 0) > 0; const items = bill.items || []; return ` @@ -92,9 +89,9 @@ const buildBillPrintHtml = (bill: any) => { .section-title { margin: 16px 0 6px; font-size: 14px; font-weight: 700; text-decoration: underline; } .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; } + .balance { color: #52c41a; font-size: 11px; } + .balance-after { color: #fa541c; font-size: 12px; font-weight: 700; } + .shortage { color: #ff4d4f; font-size: 12px; font-weight: 700; } 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; } @@ -141,13 +138,12 @@ const buildBillPrintHtml = (bill: any) => {
费用汇总
分摊费用: ${escapeHtml(money(bill.sharedAmount))}
-
个人费用: ${escapeHtml(money(bill.personalAmount))}
应付总额: ${escapeHtml(money(bill.totalAmount))}
+
当前水电余额: ${escapeHtml(money(bill.utilityBalance))}
+
扣本账单后余额: ${escapeHtml(money(bill.utilityBalanceAfterBill))}
${ - hasDeposit - ? `
可用押金: ${escapeHtml(money(bill.availableDeposit))}
-
押金抵扣: -${escapeHtml(money(bill.depositApplied))}
-
抵扣后应付: ${escapeHtml(money(bill.amountAfterDeposit ?? bill.totalAmount))}
` + Number(bill.utilityShortageAmount || 0) > 0 + ? `
需补缴: ${escapeHtml(money(bill.utilityShortageAmount))}
` : '' }
@@ -370,13 +366,6 @@ const BillsPage: React.FC = () => { align: 'right' as const, render: (v: number) => `¥${Number(v).toFixed(2)}`, }, - { - title: '个人费用', - dataIndex: 'personalAmount', - width: 120, - align: 'right' as const, - render: (v: number) => `¥${Number(v).toFixed(2)}`, - }, { title: '总计', dataIndex: 'totalAmount', @@ -385,32 +374,39 @@ const BillsPage: React.FC = () => { render: (v: number) => ¥{Number(v).toFixed(2)}, }, { - title: '可用押金', - dataIndex: 'availableDeposit', - width: 120, + title: '当前水电余额', + dataIndex: 'utilityBalance', + width: 130, + align: 'right' as const, + render: (v: number) => ( + + ¥{Number(v || 0).toFixed(2)} + + ), + }, + { + title: '扣本账单后余额', + dataIndex: 'utilityBalanceAfterBill', + width: 150, + align: 'right' as const, + render: (v: number) => ( + + ¥{Number(v || 0).toFixed(2)} + + ), + }, + { + title: '需补缴', + dataIndex: 'utilityShortageAmount', + width: 110, + align: 'right' as const, render: (v: number) => - v > 0 ? ( - ¥{Number(v).toFixed(2)} + Number(v || 0) > 0 ? ( + ¥{Number(v).toFixed(2)} ) : ( - ), }, - { - title: '抵扣后应付', - dataIndex: 'amountAfterDeposit', - width: 130, - render: (v: number, r: any) => { - const has = Number(r.availableDeposit || 0) > 0; - if (!has) return -; - const after = Number(v ?? r.totalAmount).toFixed(2); - const applied = Number(r.depositApplied || 0).toFixed(2); - return ( - - ¥{after} - - ); - }, - }, { title: '状态', dataIndex: 'status', @@ -512,7 +508,7 @@ const BillsPage: React.FC = () => { ]} /> + + } + onClick={() => { + form.setFieldsValue({ rechargeDate: dayjs() }); + setModalOpen(true); + }} + > + 水电余额充值 + + + + '水电余额汇总'} + loading={loading} + dataSource={balances} + rowKey="studentId" + pagination={{ defaultPageSize: 10, showSizeChanger: true }} + locale={{ emptyText: }} + columns={[ + { title: '学生', render: (_: any, r: any) => r.student?.name || '-' }, + { title: '学号', render: (_: any, r: any) => r.student?.studentNo || '-' }, + { title: '累计充值', dataIndex: 'totalRecharged', align: 'right' as const, render: money }, + { title: '账单扣款', dataIndex: 'usedAmount', align: 'right' as const, render: money }, + { + title: '剩余水电余额', + dataIndex: 'balance', + align: 'right' as const, + render: (v: number) => ( + {money(v)} + ), + }, + ]} + /> + +
'充值记录'} + loading={loading} + dataSource={recharges} + rowKey="id" + pagination={{ defaultPageSize: 15, showSizeChanger: true }} + locale={{ emptyText: }} + columns={[ + { title: '学生', render: (_: any, r: any) => r.student?.name || '-' }, + { title: '充值金额', dataIndex: 'amount', align: 'right' as const, render: money }, + { title: '充值日期', dataIndex: 'rechargeDate' }, + { title: '类型', render: () => 充值 }, + { title: '备注', dataIndex: 'notes' }, + { + title: '录入时间', + dataIndex: 'createdAt', + render: (v: string) => (v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-'), + }, + { + title: '操作', + width: 100, + render: (_: any, record: any) => ( + { + await api.delete(`/utility-balances/recharges/${record.id}`); + message.success('删除成功'); + fetchData(); + }} + > + }> + 删除 + + + ), + }, + ]} + /> + + setModalOpen(false)} + confirmLoading={saving} + okText="确认充值" + > +
+ +