import { useMemo, useState } from 'react';
import { Alert, Button, Col, Row, Space, Typography } from 'antd';
import { ProCard } from '@ant-design/pro-components';
import { useQuery } from '@tanstack/react-query';
import {
AreaChart, Area, BarChart, Bar, XAxis, YAxis,
CartesianGrid, Tooltip, ResponsiveContainer, Legend,
} from 'recharts';
import {
IconTrendingUp, IconTrendingDown, IconMinus, IconDownload,
} from '@tabler/icons-react';
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 { kpiApi, MonthlySummaryRow, KpiSearchParam } from '@/api/kpi';
import {
COLOR_PRIMARY, COLOR_ERROR, COLOR_SUCCESS,
GRAY, SHADOW, RADIUS,
} from '@/theme/tokens';
const { Text } = Typography;
// ────────────────────────────────────────────────────────
// 유틸
// ────────────────────────────────────────────────────────
const fmt = (v: number) => v.toLocaleString();
const fmtM = (v: number) => `${(v / 1_000_000).toFixed(1)}M`;
function calcPct(curr: number, prev: number): number | null {
if (prev === 0) return null;
return Math.round(((curr - prev) / Math.abs(prev)) * 1000) / 10;
}
// ────────────────────────────────────────────────────────
// 서브 컴포넌트 — KPI 카드
// ────────────────────────────────────────────────────────
interface KpiCardProps {
label: string;
value: number;
unit?: string;
delta?: number | null;
deltaLabel?: string;
color?: string;
bg?: string;
}
function KpiCard({ label, value, unit = '원', delta, deltaLabel, color = COLOR_PRIMARY, bg = '#EBF3FF' }: KpiCardProps) {
const renderDelta = () => {
if (delta === null || delta === undefined) return null;
if (delta > 0) return (
+{delta.toFixed(1)}%
);
if (delta < 0) return (
{delta.toFixed(1)}%
);
return (
변동없음
);
};
return (
{fmtM(value)}
{unit}
{(delta !== null && delta !== undefined) && (
{renderDelta()}
{deltaLabel && {deltaLabel}}
)}
);
}
// ────────────────────────────────────────────────────────
// AG Grid 컬럼 정의
// ────────────────────────────────────────────────────────
const COLUMNS: ColDef[] = [
{ field: 'yyyymm', headerName: '정산월', width: 110, pinned: 'left' },
{ field: 'agentCount', headerName: '설계사수', width: 100, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) },
{ field: 'totalGross', headerName: '총수수료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) },
{ field: 'totalTax', headerName: '총공제(세)', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) },
{ field: 'totalNet', headerName: '총지급', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) },
{ field: 'totalChargeback', headerName: '환수', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) },
{ field: 'totalIncentive', headerName: '시책', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value ?? 0) },
];
// ────────────────────────────────────────────────────────
// 클라이언트 사이드 엑셀 내보내기 (xlsx 없음 — CSV 대체)
// ────────────────────────────────────────────────────────
function exportCsv(rows: MonthlySummaryRow[]) {
const headers = ['정산월', '설계사수', '총수수료', '총공제', '총지급', '환수', '시책'];
const lines = [
headers.join(','),
...rows.map((r) =>
[r.yyyymm, r.agentCount, r.totalGross, r.totalTax, r.totalNet, r.totalChargeback, r.totalIncentive].join(','),
),
];
const blob = new Blob(['' + lines.join('\n')], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `월별정산요약_${dayjs().format('YYYYMMDD')}.csv`;
link.click();
URL.revokeObjectURL(link.href);
}
// ────────────────────────────────────────────────────────
// 메인 컴포넌트
// ────────────────────────────────────────────────────────
export default function KpiMonthlySummary() {
const [params, setParams] = useState({
fromMonth: dayjs().subtract(11, 'month').format('YYYYMM'),
toMonth: dayjs().format('YYYYMM'),
pageSize: 100,
});
const { data, isLoading, isError } = useQuery({
queryKey: ['kpi', 'monthly', params],
queryFn: () => kpiApi.monthlySummary(params),
retry: false,
});
const rows = data?.list ?? [];
// KPI 카드: 최신월 / 전월 / YTD 합산
const thisMonth = rows[rows.length - 1];
const prevMonth = rows.length >= 2 ? rows[rows.length - 2] : undefined;
const ytdGross = rows.reduce((s, r) => s + (r.totalGross ?? 0), 0);
const grossDelta = thisMonth && prevMonth
? calcPct(thisMonth.totalGross, prevMonth.totalGross)
: null;
// 합계행
const pinnedRow = rows.length > 0 ? {
yyyymm: '합계',
agentCount: rows.reduce((s, r) => s + r.agentCount, 0),
totalGross: rows.reduce((s, r) => s + r.totalGross, 0),
totalTax: rows.reduce((s, r) => s + r.totalTax, 0),
totalNet: rows.reduce((s, r) => s + r.totalNet, 0),
totalChargeback: rows.reduce((s, r) => s + r.totalChargeback, 0),
totalIncentive: rows.reduce((s, r) => s + r.totalIncentive, 0),
} as Partial : undefined;
// 차트 데이터 — 최근 12개월
const chartData = useMemo(() =>
rows.slice(-12).map((r) => ({
month: r.yyyymm.slice(4, 6) + '월',
총수수료: Math.round(r.totalGross / 1_000_000),
총지급: Math.round(r.totalNet / 1_000_000),
환수: Math.round(r.totalChargeback / 1_000_000),
})),
[rows]);
const handleSearch = (values: Record) => {
setParams({
fromMonth: values.fromMonth as string | undefined,
toMonth: values.toMonth as string | undefined,
pageSize: 100,
});
};
return (
}
onClick={() => rows.length && exportCsv(rows)}
disabled={!rows.length}
style={{ display: 'flex', alignItems: 'center', gap: 4 }}
>
CSV 다운로드
}
>
{/* 검색 */}
setParams({ fromMonth: dayjs().subtract(11, 'month').format('YYYYMM'), toMonth: dayjs().format('YYYYMM'), pageSize: 100 })}
/>
{isError && (
)}
{/* KPI 카드 3개 */}
{/* 차트 */}
최근 12개월 수수료 추이}
subTitle={단위: 백만원}
headerBordered
style={{
background: '#ffffff', borderRadius: RADIUS.lg,
border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm,
marginBottom: 20,
}}
>
{chartData.length > 0 ? (
[`${v}백만`, '']}
/>
) : (
)}
{/* AG Grid */}
rows={rows}
columns={COLUMNS}
loading={isLoading}
height={480}
pinnedBottomRow={pinnedRow}
/>
);
}