/** * [환수감면신청] 화면 * - 업무: 설계사에게 부과된 "환수액"의 일부/전부를 감면해 달라는 신청을 등록하고, 관리자가 승인/반려하는 워크플로 화면. * - 호출 API: commissionApi.chargebackWaiverList(조회), chargebackWaiverCreate(신청), * chargebackWaiverApprove(승인), chargebackWaiverReject(반려). * - 주요 상태: searchParams(검색조건), createOpen(신청 모달), rejectTarget/rejectReason(반려 모달). * - 도메인 용어: * - 환수: 계약 해지 등으로 이미 지급한 수수료를 회수하는 것. * - 감면(waiver): 환수액 중 일부를 사정 참작해 면제해 주는 것. * - 승인상태: 신청(REQUESTED) -> 승인(APPROVED) 또는 반려(REJECTED). */ import { useState } from 'react'; import { Alert, Form, Input, InputNumber, Modal, Select, Space, message } from 'antd'; import { ProCard } from '@ant-design/pro-components'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import type { ColDef } from '@ag-grid-community/core'; import dayjs from 'dayjs'; import PageContainer from '@/components/common/PageContainer'; import SearchForm from '@/components/common/SearchForm'; import DataGrid from '@/components/common/DataGrid'; import PermissionButton from '@/components/common/PermissionButton'; import { commissionApi, ChargebackWaiverRow, ChargebackWaiverSaveReq, CommissionSearchParam, } from '@/api/commission'; import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; const STATUS_OPTIONS = [ { value: 'REQUESTED', label: '신청' }, { value: 'APPROVED', label: '승인' }, { value: 'REJECTED', label: '반려' }, ]; // 그리드 컬럼: 감면 신청 목록(원환수액 대비 감면액, 사유, 승인상태) const COLS: ColDef[] = [ { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, { field: 'agentName', headerName: '설계사', width: 130 }, { field: 'contractNo', headerName: '증권번호', width: 140 }, { field: 'originalChargeback', headerName: '원환수액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, { field: 'waiverAmount', headerName: '감면액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, { field: 'reason', headerName: '사유', flex: 1 }, { field: 'approvalStatus', headerName: '승인상태', width: 110 }, { field: 'approvedAt', headerName: '승인일시', width: 160 }, ]; export default function ChargebackWaiver() { const qc = useQueryClient(); const [searchParams, setSearchParams] = useState({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200, }); const [createOpen, setCreateOpen] = useState(false); const [rejectTarget, setRejectTarget] = useState(null); const [rejectReason, setRejectReason] = useState(''); const [form] = Form.useForm(); // 감면 신청 목록 조회(읽기). 검색조건 변경 시 자동 재조회 const { data: listData, isLoading, isError } = useQuery({ queryKey: ['chargebackWaiver', 'list', searchParams], queryFn: () => commissionApi.chargebackWaiverList(searchParams), retry: false, }); const rows = listData?.list ?? []; // 감면 신청 등록 mutation: 성공 시 목록 새로고침 + 모달 닫기 const createMutation = useMutation({ mutationFn: (body: ChargebackWaiverSaveReq) => commissionApi.chargebackWaiverCreate(body), onSuccess: () => { message.success('신청 완료'); qc.invalidateQueries({ queryKey: ['chargebackWaiver', 'list'] }); setCreateOpen(false); form.resetFields(); }, }); // 신청 승인 mutation const approveMutation = useMutation({ mutationFn: (requestId: number) => commissionApi.chargebackWaiverApprove(requestId), onSuccess: () => { message.success('승인 완료'); qc.invalidateQueries({ queryKey: ['chargebackWaiver', 'list'] }); }, }); // 신청 반려 mutation(사유 필요) const rejectMutation = useMutation({ mutationFn: ({ requestId, reason }: { requestId: number; reason: string }) => commissionApi.chargebackWaiverReject(requestId, reason), onSuccess: () => { message.success('반려 완료'); qc.invalidateQueries({ queryKey: ['chargebackWaiver', 'list'] }); setRejectTarget(null); setRejectReason(''); }, }); // 신청 모달 "신청" 버튼: 폼 검증 후 등록 const handleCreate = async () => { const values = await form.validateFields(); createMutation.mutate(values); }; // 승인 버튼: 확인창 후 승인 처리 const handleApprove = (row: ChargebackWaiverRow) => Modal.confirm({ title: '승인', content: `${row.agentName} 감면 신청(감면액: ${fmt(row.waiverAmount)})을 승인하시겠습니까?`, onOk: () => approveMutation.mutateAsync(row.requestId), }); // 반려 모달 열기(대상 행 지정) const openReject = (row: ChargebackWaiverRow) => { setRejectTarget(row); setRejectReason(''); }; // 반려 확정: 사유 필수 검증 후 반려 처리 const handleReject = () => { if (!rejectReason.trim()) { message.warning('반려 사유를 입력하세요'); return; } rejectMutation.mutate({ requestId: rejectTarget!.requestId, reason: rejectReason }); }; const pinnedRow = rows.length > 0 ? { agentName: '합계', originalChargeback: rows.reduce((s, r) => s + (r.originalChargeback ?? 0), 0), waiverAmount: rows.reduce((s, r) => s + (r.waiverAmount ?? 0), 0), } as Partial : undefined; const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; return ( {isError && ( )} setSearchParams({ ...v as typeof searchParams, pageSize: 200 })} onReset={() => setSearchParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} />
setCreateOpen(true)}> + 감면 신청
rows={rows} columns={[ ...COLS, { headerName: '액션', width: 180, pinned: 'right', cellRenderer: (p: { data: ChargebackWaiverRow }) => ( handleApprove(p.data)} > 승인 openReject(p.data)} > 반려 ), }, ]} loading={isLoading} height={520} rowKey="requestId" pinnedBottomRow={pinnedRow} />
{/* 신청 모달 */} { setCreateOpen(false); form.resetFields(); }} okText="신청" cancelText="취소" confirmLoading={createMutation.isPending} >
{/* 반려 사유 모달 */} { setRejectTarget(null); setRejectReason(''); }} okText="반려" okButtonProps={{ danger: true }} cancelText="취소" confirmLoading={rejectMutation.isPending} > setRejectReason(e.target.value)} />
); }