feat(deposits): add refund rejection path with reason
This commit is contained in:
@@ -58,6 +58,8 @@ const DepositsPage: React.FC = () => {
|
|||||||
const [installmentModal, setInstallmentModal] = useState<number | null>(null);
|
const [installmentModal, setInstallmentModal] = useState<number | null>(null);
|
||||||
const [pendingRefunds, setPendingRefunds] = useState<PendingRefund[]>([]);
|
const [pendingRefunds, setPendingRefunds] = useState<PendingRefund[]>([]);
|
||||||
const [pendingLoading, setPendingLoading] = useState(false);
|
const [pendingLoading, setPendingLoading] = useState(false);
|
||||||
|
const [rejectModal, setRejectModal] = useState<any>(null);
|
||||||
|
const [rejectReason, setRejectReason] = useState('');
|
||||||
const [createForm] = Form.useForm();
|
const [createForm] = Form.useForm();
|
||||||
const [refundForm] = Form.useForm();
|
const [refundForm] = Form.useForm();
|
||||||
const [installmentForm] = Form.useForm();
|
const [installmentForm] = Form.useForm();
|
||||||
@@ -160,6 +162,20 @@ const DepositsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleRejectRefund = async () => {
|
||||||
|
if (!rejectModal) return;
|
||||||
|
try {
|
||||||
|
await api.put(`/deposits/${rejectModal.id}/reject-refund`, { reason: rejectReason || '未说明原因' });
|
||||||
|
message.success('已驳回退款申请');
|
||||||
|
setRejectModal(null);
|
||||||
|
setRejectReason('');
|
||||||
|
fetchPendingRefunds();
|
||||||
|
fetchData();
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '驳回失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
const handleAddInstallment = async () => {
|
const handleAddInstallment = async () => {
|
||||||
if (installmentModal == null) return;
|
if (installmentModal == null) return;
|
||||||
const values = await installmentForm.validateFields();
|
const values = await installmentForm.validateFields();
|
||||||
@@ -306,16 +322,29 @@ const DepositsPage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
render: (_: any, record: any) => (
|
render: (_: unknown, record: PendingRefund) => (
|
||||||
<PermissionButton
|
<Space>
|
||||||
permission="deposit:approve"
|
<PermissionButton
|
||||||
size="small"
|
permission="deposit:approve"
|
||||||
type="primary"
|
size="small"
|
||||||
icon={<CheckOutlined />}
|
type="primary"
|
||||||
onClick={() => handleApproveRefund(record)}
|
icon={<CheckOutlined />}
|
||||||
>
|
onClick={() => handleApproveRefund(record)}
|
||||||
审批通过
|
>
|
||||||
</PermissionButton>
|
审批通过
|
||||||
|
</PermissionButton>
|
||||||
|
<PermissionButton
|
||||||
|
permission="deposit:approve"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
onClick={() => {
|
||||||
|
setRejectModal(record);
|
||||||
|
setRejectReason('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
驳回
|
||||||
|
</PermissionButton>
|
||||||
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -586,6 +615,23 @@ const DepositsPage: React.FC = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Reject Refund Modal */}
|
||||||
|
<Modal
|
||||||
|
title={`驳回退款申请 - ${rejectModal?.student?.name || ''}`}
|
||||||
|
open={!!rejectModal}
|
||||||
|
onOk={handleRejectRefund}
|
||||||
|
onCancel={() => { setRejectModal(null); setRejectReason(''); }}
|
||||||
|
okText="确认驳回"
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="请输入驳回原因"
|
||||||
|
value={rejectReason}
|
||||||
|
onChange={(e) => setRejectReason(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,8 +95,22 @@ export class DepositsController {
|
|||||||
async addInstallment(
|
async addInstallment(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() body: { amount: number; dueDate: string },
|
@Body() body: { amount: number; dueDate: string },
|
||||||
|
@Request() req: { user?: { id: number; username: string }; headers?: Record<string, string> },
|
||||||
) {
|
) {
|
||||||
return this.service.addInstallment(+id, body.amount, body.dueDate);
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||||
|
const result = await this.service.addInstallment(+id, body.amount, body.dueDate);
|
||||||
|
await this.logService.log({
|
||||||
|
userId: req.user?.id,
|
||||||
|
username: req.user?.username,
|
||||||
|
module: '押金管理',
|
||||||
|
action: '新增分期',
|
||||||
|
targetId: result.id,
|
||||||
|
targetType: 'deposit-installment',
|
||||||
|
detail: `押金${id} 新增分期 ¥${result.amount}`,
|
||||||
|
ipAddress,
|
||||||
|
userAgent,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('installments/:installmentId')
|
@Put('installments/:installmentId')
|
||||||
@@ -104,14 +118,44 @@ export class DepositsController {
|
|||||||
async updateInstallment(
|
async updateInstallment(
|
||||||
@Param('installmentId') installmentId: string,
|
@Param('installmentId') installmentId: string,
|
||||||
@Body() body: { paidDate?: string; status?: string },
|
@Body() body: { paidDate?: string; status?: string },
|
||||||
|
@Request() req: { user?: { id: number; username: string }; headers?: Record<string, string> },
|
||||||
) {
|
) {
|
||||||
return this.service.updateInstallment(+installmentId, body);
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||||
|
const result = await this.service.updateInstallment(+installmentId, body);
|
||||||
|
await this.logService.log({
|
||||||
|
userId: req.user?.id,
|
||||||
|
username: req.user?.username,
|
||||||
|
module: '押金管理',
|
||||||
|
action: '更新分期',
|
||||||
|
targetId: +installmentId,
|
||||||
|
targetType: 'deposit-installment',
|
||||||
|
detail: `更新分期${installmentId}, 状态:${result.status ?? '-'}, 实付日:${result.paidDate ?? '-'}`,
|
||||||
|
ipAddress,
|
||||||
|
userAgent,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('installments/:installmentId')
|
@Delete('installments/:installmentId')
|
||||||
@RequirePermission('deposit:delete')
|
@RequirePermission('deposit:delete')
|
||||||
async deleteInstallment(@Param('installmentId') installmentId: string) {
|
async deleteInstallment(
|
||||||
return this.service.deleteInstallment(+installmentId);
|
@Param('installmentId') installmentId: string,
|
||||||
|
@Request() req: { user?: { id: number; username: string }; headers?: Record<string, string> },
|
||||||
|
) {
|
||||||
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||||
|
const result = await this.service.deleteInstallment(+installmentId);
|
||||||
|
await this.logService.log({
|
||||||
|
userId: req.user?.id,
|
||||||
|
username: req.user?.username,
|
||||||
|
module: '押金管理',
|
||||||
|
action: '删除分期',
|
||||||
|
targetId: +installmentId,
|
||||||
|
targetType: 'deposit-installment',
|
||||||
|
detail: `删除分期${installmentId}`,
|
||||||
|
ipAddress,
|
||||||
|
userAgent,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id/refund')
|
@Put(':id/refund')
|
||||||
@@ -183,6 +227,29 @@ export class DepositsController {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Put(':id/reject-refund')
|
||||||
|
@RequirePermission('deposit:approve')
|
||||||
|
async rejectRefund(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: { reason: string },
|
||||||
|
@Request() req: any,
|
||||||
|
) {
|
||||||
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||||
|
const result = await this.service.rejectRefund(+id, body.reason, req.user?.id);
|
||||||
|
await this.logService.log({
|
||||||
|
userId: req.user?.id,
|
||||||
|
username: req.user?.username,
|
||||||
|
module: '押金管理',
|
||||||
|
action: '驳回退款',
|
||||||
|
targetId: +id,
|
||||||
|
targetType: 'deposit',
|
||||||
|
detail: `驳回原因:${body.reason}`,
|
||||||
|
ipAddress,
|
||||||
|
userAgent,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@RequirePermission('deposit:delete')
|
@RequirePermission('deposit:delete')
|
||||||
async remove(@Param('id') id: string, @Request() req: any) {
|
async remove(@Param('id') id: string, @Request() req: any) {
|
||||||
|
|||||||
@@ -158,6 +158,21 @@ export class DepositsService {
|
|||||||
|
|
||||||
return this.repo.save(deposit);
|
return this.repo.save(deposit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async rejectRefund(id: number, reason: string, userId: number) {
|
||||||
|
const deposit = await this.repo.findOne({ where: { id } });
|
||||||
|
if (!deposit) throw new NotFoundException('押金记录不存在');
|
||||||
|
|
||||||
|
if (!deposit.refundStatus || deposit.refundStatus === 'refunded') {
|
||||||
|
throw new BadRequestException('未找到待审批的退款申请');
|
||||||
|
}
|
||||||
|
|
||||||
|
deposit.refundStatus = null;
|
||||||
|
deposit.refundApprovedBy = userId;
|
||||||
|
deposit.refundApprovedAt = new Date();
|
||||||
|
deposit.refundRejectedReason = reason;
|
||||||
|
return this.repo.save(deposit);
|
||||||
|
}
|
||||||
async findPendingRefunds() {
|
async findPendingRefunds() {
|
||||||
const baseWhere = await this.scope.filter({});
|
const baseWhere = await this.scope.filter({});
|
||||||
return this.repo.find({
|
return this.repo.find({
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export class Deposit {
|
|||||||
recordedBy: number;
|
recordedBy: number;
|
||||||
|
|
||||||
@Column({ name: 'refund_status', length: 30, nullable: true })
|
@Column({ name: 'refund_status', length: 30, nullable: true })
|
||||||
refundStatus: string; // pending | head_teacher_approved | finance_approved | refunded
|
refundStatus: string | null; // pending | head_teacher_approved | finance_approved | refunded
|
||||||
|
|
||||||
@Column({ name: 'refund_requested_at', type: 'datetime', nullable: true })
|
@Column({ name: 'refund_requested_at', type: 'datetime', nullable: true })
|
||||||
refundRequestedAt: Date;
|
refundRequestedAt: Date;
|
||||||
@@ -59,6 +59,9 @@ export class Deposit {
|
|||||||
@Column({ name: 'refund_approved_at', type: 'datetime', nullable: true })
|
@Column({ name: 'refund_approved_at', type: 'datetime', nullable: true })
|
||||||
refundApprovedAt: Date;
|
refundApprovedAt: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'refund_rejected_reason', length: 500, nullable: true })
|
||||||
|
refundRejectedReason: string;
|
||||||
|
|
||||||
@OneToMany(() => DepositInstallment, (i) => i.deposit, { cascade: true, eager: true })
|
@OneToMany(() => DepositInstallment, (i) => i.deposit, { cascade: true, eager: true })
|
||||||
installments: DepositInstallment[];
|
installments: DepositInstallment[];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user