import { useState } from 'react'; import { Drawer, Form, Input, Modal, Select, Space, Tag, Timeline, Typography, 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 { clawbackReceivableApi, ClawbackReceivableRow, ClawbackReceivableDetail, ClawbackSearchParam, } from '@/api/clawbackReceivable'; import { GRAY, RADIUS, SHADOW, COLOR_SUCCESS, COLOR_ERROR, COLOR_WARNING, COLOR_PRIMARY } from '@/theme/tokens'; const { Text } = Typography; const fmt = (v: unknown) => typeof v === 'number' ? v.toLocaleString() : (v as string | undefined) ?? '-'; const STATUS_COLOR: Record = { OUTSTANDING: COLOR_ERROR, RECOVERING: COLOR_WARNING, CLEARED: COLOR_SUCCESS, WRITTEN_OFF: GRAY[400], }; const STATUS_LABEL: Record = { OUTSTANDING: '미회수', RECOVERING: '회수중', CLEARED: '상환완료', WRITTEN_OFF: '대손', }; function StatusChip({ status, statusName }: { status: string; statusName?: string }) { const color = STATUS_COLOR[status] ?? GRAY[400]; const label = statusName ?? STATUS_LABEL[status] ?? status; return ( {label} ); } const GRID_COLS: ColDef[] = [ { field: 'agentName', headerName: '설계사', width: 130, pinned: 'left' }, { field: 'originSettleMonth', headerName: '발생월', width: 110 }, { field: 'originalAmount', headerName: '발생액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value), }, { field: 'recoveredAmount', headerName: '회수액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value), }, { field: 'balance', headerName: '잔액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value), }, { field: 'status', headerName: '상태', width: 120, cellRenderer: (p: { data: ClawbackReceivableRow }) => p.data ? : null, }, { field: 'lastRecoveryMonth', headerName: '최근회수월', width: 120 }, ]; const MENU_CODE = 'CLAWBACK_RECEIVABLE'; export default function ClawbackReceivable() { const qc = useQueryClient(); const [params, setParams] = useState({ pageSize: 200 }); const [selectedRow, setSelectedRow] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [writeOffOpen, setWriteOffOpen] = useState(false); const [writeOffForm] = Form.useForm(); // ── 목록 조회 ────────────────────────────────────── const { data: listData, isLoading } = useQuery({ queryKey: ['clawback-receivables', params], queryFn: () => clawbackReceivableApi.list(params), retry: false, }); // ── 상세 조회 (행 클릭 시) ───────────────────────── const { data: detail, isLoading: detailLoading } = useQuery({ queryKey: ['clawback-receivable', selectedRow?.receivableId], queryFn: () => clawbackReceivableApi.detail(selectedRow!.receivableId), enabled: !!selectedRow, retry: false, }); // ── 대손/상환 처리 뮤테이션 ────────────────────────── const writeOffMutation = useMutation({ mutationFn: ({ id, body }: { id: number; body: { status: 'CLEARED' | 'WRITTEN_OFF'; writeoffReason: string } }) => clawbackReceivableApi.writeOff(id, body), onSuccess: () => { message.success('처리 완료'); qc.invalidateQueries({ queryKey: ['clawback-receivables'] }); qc.invalidateQueries({ queryKey: ['clawback-receivable', selectedRow?.receivableId] }); setWriteOffOpen(false); writeOffForm.resetFields(); }, }); const rows = listData?.list ?? []; // ── 합계행 ───────────────────────────────────────── const pinnedBottom = rows.length > 0 ? ({ agentName: '합계', originalAmount: rows.reduce((s, r) => s + (r.originalAmount ?? 0), 0), recoveredAmount: rows.reduce((s, r) => s + (r.recoveredAmount ?? 0), 0), balance: rows.reduce((s, r) => s + (r.balance ?? 0), 0), } as Partial) : undefined; const handleRowClick = (row: ClawbackReceivableRow) => { setSelectedRow(row); setDrawerOpen(true); }; const handleExport = async () => { try { const res = await clawbackReceivableApi.export(params); const url = URL.createObjectURL(new Blob([res.data as BlobPart])); const a = document.createElement('a'); a.href = url; a.download = `환수채권_${dayjs().format('YYYYMMDD')}.xlsx`; a.click(); URL.revokeObjectURL(url); } catch { /* request.ts 처리 */ } }; const handleWriteOffSubmit = async () => { const values = await writeOffForm.validateFields(); if (!selectedRow) return; writeOffMutation.mutate({ id: selectedRow.receivableId, body: { status: values.status, writeoffReason: values.writeoffReason }, }); }; const canWriteOff = selectedRow?.status === 'OUTSTANDING' || selectedRow?.status === 'RECOVERING'; const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm, }; return ( 엑셀 다운로드 } > setParams({ ...(v as ClawbackSearchParam), pageSize: 200 })} onReset={() => setParams({ pageSize: 200 })} /> rows={rows} columns={GRID_COLS} loading={isLoading} height={560} rowKey="receivableId" pinnedBottomRow={pinnedBottom} onSelectionChanged={(e) => { const sel = e.api.getSelectedRows(); if (sel.length > 0) handleRowClick(sel[0]); }} /> {/* ── 상세 Drawer ─────────────────────────────── */} setDrawerOpen(false)} extra={ setWriteOffOpen(true)} > 대손/상환 처리 } > {detailLoading || !detail ? ( 불러오는 중... ) : ( )} {/* ── 대손/상환 처리 모달 ──────────────────────── */} { setWriteOffOpen(false); writeOffForm.resetFields(); }} okText="처리" cancelText="취소" confirmLoading={writeOffMutation.isPending} >
); } // ── 상세 내용 컴포넌트 ───────────────────────────────── function DrawerContent({ detail }: { detail: ClawbackReceivableDetail }) { const fields: Array<{ label: string; value: React.ReactNode }> = [ { label: '설계사', value: detail.agentName }, { label: '발생월', value: detail.originSettleMonth }, { label: '발생액', value: fmt(detail.originalAmount) }, { label: '회수액', value: fmt(detail.recoveredAmount) }, { label: '잔액', value: fmt(detail.balance) }, { label: '상태', value: }, { label: '최근회수월', value: detail.lastRecoveryMonth ?? '-' }, { label: '등록일', value: detail.createdAt ? dayjs(detail.createdAt).format('YYYY-MM-DD') : '-' }, ]; if (detail.writeoffReason) { fields.push({ label: '처리사유', value: detail.writeoffReason }); } return ( <>
{fields.map(({ label, value }) => ( <> {label} {value} ))}
{detail.recoveries.length > 0 && ( <> 회수 이력 ({ color: COLOR_PRIMARY, children: ( {r.settleMonth} 정산월 회수액: {fmt(r.recoveredAmount)}원 {dayjs(r.createdAt).format('YYYY-MM-DD')} ), }))} /> )} {detail.recoveries.length === 0 && ( 회수 이력이 없습니다. )} ); }