동기화
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { Alert } from 'antd';
|
||||
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import MoneyText from '@/components/common/MoneyText';
|
||||
import ExcelExportButton from '@/components/common/ExcelExportButton';
|
||||
import api, { unwrap } from '@/api/request';
|
||||
|
||||
interface RiskRow {
|
||||
agentId: number;
|
||||
agentName: string;
|
||||
orgName: string;
|
||||
policyNo: string;
|
||||
companyName: string;
|
||||
recruitMonth: string;
|
||||
monthsElapsed: number;
|
||||
recruitAmount: number;
|
||||
chargebackRate: number;
|
||||
estimatedChargeback: number;
|
||||
riskLevel: 'HIGH' | 'MEDIUM' | 'LOW';
|
||||
}
|
||||
|
||||
function fetchRisk() {
|
||||
return unwrap<RiskRow[]>(api.get('/api/report/chargeback-risk'));
|
||||
}
|
||||
|
||||
const riskColor: Record<string, string> = {
|
||||
HIGH: '#f5222d',
|
||||
MEDIUM: '#fa8c16',
|
||||
LOW: '#52c41a',
|
||||
};
|
||||
|
||||
const riskLabel: Record<string, string> = {
|
||||
HIGH: '고위험',
|
||||
MEDIUM: '중위험',
|
||||
LOW: '저위험',
|
||||
};
|
||||
|
||||
const columns: ProColumns<RiskRow>[] = [
|
||||
{ title: '설계사', dataIndex: 'agentName', width: 100, fixed: 'left' },
|
||||
{ title: '소속', dataIndex: 'orgName', width: 160 },
|
||||
{ title: '증권번호', dataIndex: 'policyNo', width: 130 },
|
||||
{ title: '보험사', dataIndex: 'companyName', width: 110 },
|
||||
{ title: '모집월', dataIndex: 'recruitMonth', width: 90 },
|
||||
{ title: '경과월', dataIndex: 'monthsElapsed', width: 80, align: 'right', render: (_, r) => `${r.monthsElapsed}개월` },
|
||||
{
|
||||
title: '모집수수료', dataIndex: 'recruitAmount', width: 120, align: 'right',
|
||||
render: (_, r) => <MoneyText value={r.recruitAmount} />,
|
||||
},
|
||||
{
|
||||
title: '환수율(%)', dataIndex: 'chargebackRate', width: 90, align: 'right',
|
||||
render: (_, r) => `${r.chargebackRate}%`,
|
||||
},
|
||||
{
|
||||
title: '예상환수액', dataIndex: 'estimatedChargeback', width: 130, align: 'right',
|
||||
render: (_, r) => <MoneyText value={r.estimatedChargeback} />,
|
||||
},
|
||||
{
|
||||
title: '위험등급', dataIndex: 'riskLevel', width: 90, align: 'center', fixed: 'right',
|
||||
render: (_, r) => (
|
||||
<span style={{ color: riskColor[r.riskLevel], fontWeight: 600 }}>
|
||||
{riskLabel[r.riskLevel] ?? r.riskLevel}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export default function ChargebackRiskReport() {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['report', 'chargeback-risk'],
|
||||
queryFn: fetchRisk,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="환수위험 리포트"
|
||||
description="경과월별 환수 가능성 분석"
|
||||
extra={<ExcelExportButton url="/api/report/chargeback-risk/export" fileName="환수위험리포트" />}
|
||||
>
|
||||
{isError && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="백엔드 미구현 — 곧 추가됩니다"
|
||||
description="/api/report/chargeback-risk API가 아직 준비되지 않았습니다."
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ProTable<RiskRow>
|
||||
rowKey={(r) => `${r.agentId}-${r.policyNo}`}
|
||||
columns={columns}
|
||||
dataSource={data ?? []}
|
||||
loading={isLoading}
|
||||
search={false}
|
||||
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}건` }}
|
||||
options={{ density: false, reload: false }}
|
||||
scroll={{ x: 1100 }}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useState } from 'react';
|
||||
import { Alert, DatePicker } from 'antd';
|
||||
import { ProCard, ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dayjs, { type Dayjs } from 'dayjs';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import MoneyText from '@/components/common/MoneyText';
|
||||
import ExcelExportButton from '@/components/common/ExcelExportButton';
|
||||
import api, { unwrap } from '@/api/request';
|
||||
|
||||
interface OrgRow {
|
||||
orgId: number;
|
||||
orgName: string;
|
||||
orgType: string;
|
||||
agentCount: number;
|
||||
recruitTotal: number;
|
||||
maintainTotal: number;
|
||||
netAmount: number;
|
||||
}
|
||||
|
||||
function fetchOrgReport(settleMonth: string) {
|
||||
return unwrap<OrgRow[]>(api.get('/api/report/org', { params: { settleMonth } }));
|
||||
}
|
||||
|
||||
const columns: ProColumns<OrgRow>[] = [
|
||||
{ title: '조직명', dataIndex: 'orgName', width: 180, fixed: 'left' },
|
||||
{ title: '유형', dataIndex: 'orgType', width: 80 },
|
||||
{ title: '설계사 수', dataIndex: 'agentCount', width: 90, align: 'right' },
|
||||
{
|
||||
title: '모집수수료', dataIndex: 'recruitTotal', width: 130, align: 'right',
|
||||
render: (_, r) => <MoneyText value={r.recruitTotal} />,
|
||||
},
|
||||
{
|
||||
title: '유지수수료', dataIndex: 'maintainTotal', width: 130, align: 'right',
|
||||
render: (_, r) => <MoneyText value={r.maintainTotal} />,
|
||||
},
|
||||
{
|
||||
title: '실지급 합계', dataIndex: 'netAmount', width: 140, align: 'right',
|
||||
render: (_, r) => <strong><MoneyText value={r.netAmount} /></strong>,
|
||||
},
|
||||
];
|
||||
|
||||
export default function OrgReport() {
|
||||
const [settleMonth, setSettleMonth] = useState<Dayjs>(dayjs());
|
||||
|
||||
const monthStr = settleMonth.format('YYYYMM');
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['report', 'org', monthStr],
|
||||
queryFn: () => fetchOrgReport(monthStr),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="조직별 리포트"
|
||||
description="조직 단위 정산 집계"
|
||||
extra={
|
||||
<ExcelExportButton
|
||||
url="/api/report/org/export"
|
||||
fileName="조직별리포트"
|
||||
params={{ settleMonth: monthStr }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isError && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="백엔드 미구현 — 곧 추가됩니다"
|
||||
description="/api/report/org API가 아직 준비되지 않았습니다."
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ProCard bordered style={{ marginBottom: 16 }}>
|
||||
<DatePicker
|
||||
picker="month"
|
||||
value={settleMonth}
|
||||
onChange={(d) => d && setSettleMonth(d)}
|
||||
allowClear={false}
|
||||
format="YYYY-MM"
|
||||
/>
|
||||
</ProCard>
|
||||
|
||||
<ProTable<OrgRow>
|
||||
rowKey="orgId"
|
||||
columns={columns}
|
||||
dataSource={data ?? []}
|
||||
loading={isLoading}
|
||||
search={false}
|
||||
pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}건` }}
|
||||
options={{ density: false, reload: false }}
|
||||
scroll={{ x: 800 }}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useState } from 'react';
|
||||
import { Alert, Col, Empty, Row, Spin } from 'antd';
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import dayjs from 'dayjs';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import ExcelExportButton from '@/components/common/ExcelExportButton';
|
||||
import api, { unwrap } from '@/api/request';
|
||||
|
||||
interface PerfRow {
|
||||
settleMonth: string;
|
||||
agentCount: number;
|
||||
recruitTotal: number;
|
||||
maintainTotal: number;
|
||||
netAmount: number;
|
||||
}
|
||||
|
||||
function fetchPerf(year: string) {
|
||||
return unwrap<PerfRow[]>(api.get('/api/report/performance', { params: { year } }));
|
||||
}
|
||||
|
||||
export default function PerformanceReport() {
|
||||
const [year] = useState(() => dayjs().format('YYYY'));
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['report', 'performance', year],
|
||||
queryFn: () => fetchPerf(year),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="실적 리포트"
|
||||
description="월별 수수료 집계 — 모집/유지/실지급"
|
||||
extra={<ExcelExportButton url="/api/report/performance/export" fileName="실적리포트" params={{ year }} />}
|
||||
>
|
||||
{isError && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="백엔드 미구현 — 곧 추가됩니다"
|
||||
description="/api/report/performance API가 아직 준비되지 않았습니다."
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ProCard bordered style={{ marginBottom: 16 }}>
|
||||
<Spin spinning={isLoading}>
|
||||
{data && data.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={340}>
|
||||
<BarChart data={data} margin={{ top: 16, right: 24, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="settleMonth" />
|
||||
<YAxis tickFormatter={(v: number | string) => `${(Number(v) / 1_000_000).toFixed(0)}백만`} />
|
||||
<Tooltip formatter={(v: number | string) => Number(v).toLocaleString()} />
|
||||
<Legend />
|
||||
<Bar dataKey="recruitTotal" name="모집수수료" fill="#1677ff" />
|
||||
<Bar dataKey="maintainTotal" name="유지수수료" fill="#52c41a" />
|
||||
<Bar dataKey="netAmount" name="실지급" fill="#faad14" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<Empty description={isLoading ? '로딩 중...' : '데이터 없음'} style={{ padding: 40 }} />
|
||||
)}
|
||||
</Spin>
|
||||
</ProCard>
|
||||
|
||||
{data && data.length > 0 && (
|
||||
<ProCard bordered>
|
||||
<Row gutter={16}>
|
||||
{data.map((row) => (
|
||||
<Col key={row.settleMonth} xs={24} sm={12} md={8} lg={6} xl={4}>
|
||||
<ProCard size="small" bordered style={{ marginBottom: 8 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 4 }}>{row.settleMonth}</div>
|
||||
<div style={{ fontSize: 12, color: '#666' }}>설계사: {row.agentCount}명</div>
|
||||
<div style={{ fontSize: 12, color: '#1677ff' }}>
|
||||
모집: {(row.recruitTotal ?? 0).toLocaleString()}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#52c41a' }}>
|
||||
유지: {(row.maintainTotal ?? 0).toLocaleString()}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 600 }}>
|
||||
실지급: {(row.netAmount ?? 0).toLocaleString()}
|
||||
</div>
|
||||
</ProCard>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</ProCard>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user