Files
ga-commission-system/ga-frontend/src/pages/Dashboard.tsx
T

189 lines
8.3 KiB
TypeScript
Raw Normal View History

import { Card, Col, Row, Typography } from 'antd';
import {
IconCash, IconCheck, IconClock, IconAlertTriangle,
IconTrendingUp, IconTrendingDown,
} from '@tabler/icons-react';
import {
ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, Legend,
} from 'recharts';
import PageContainer from '@/components/common/PageContainer';
import MoneyText from '@/components/common/MoneyText';
const { Text } = Typography;
const KPI_CARDS = [
{ key: 'recruit', label: '모집수수료', value: 124_500_000, delta: +8.2,
icon: <IconCash size={20} />, color: '#1677FF', bg: '#E6F1FB' },
{ key: 'maintain', label: '유지수수료', value: 82_300_000, delta: +3.1,
icon: <IconCheck size={20} />, color: '#0F6E56', bg: '#E1F5EE' },
{ key: 'pending', label: '확정 대기', value: 12, delta: -2.0, suffix: '건',
icon: <IconClock size={20} />, color: '#BA7517', bg: '#FAEEDA' },
{ key: 'alert', label: '대사 차이', value: 3, delta: +50.0, suffix: '건',
icon: <IconAlertTriangle size={20} />, color: '#E24B4A', bg: '#FCEBEB' },
];
const MONTHLY_TREND = [
{ month: '12월', recruit: 95, maintain: 70, chargeback: 5 },
{ month: '1월', recruit: 105, maintain: 72, chargeback: 8 },
{ month: '2월', recruit: 110, maintain: 75, chargeback: 6 },
{ month: '3월', recruit: 118, maintain: 78, chargeback: 7 },
{ month: '4월', recruit: 122, maintain: 80, chargeback: 9 },
{ month: '5월', recruit: 125, maintain: 82, chargeback: 4 },
];
const BATCH_STEPS = [
{ name: '데이터 수신', status: 'done', time: '02:14' },
{ name: '데이터 변환', status: 'done', time: '00:52' },
{ name: '대사 검증', status: 'done', time: '00:21' },
{ name: '모집수수료', status: 'done', time: '01:08' },
{ name: '유지수수료', status: 'done', time: '00:46' },
{ name: '환수/예외', status: 'progress', time: '00:33' },
{ name: '오버라이드', status: 'pending' },
{ name: '집계', status: 'pending' },
];
export default function Dashboard() {
return (
<PageContainer
title="대시보드"
description="이번 달 (2026-05) 정산 요약 / 실시간 배치 진행 / 알림"
>
{/* KPI 카드 4개 */}
<Row gutter={16} style={{ marginBottom: 16 }}>
{KPI_CARDS.map((c) => {
const isUp = c.delta >= 0;
return (
<Col span={6} key={c.key}>
<Card
bordered={false}
style={{
boxShadow: 'var(--ga-shadow-card)',
borderTop: `3px solid ${c.color}`,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div
style={{
width: 44, height: 44, borderRadius: 10,
background: c.bg, color: c.color,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
{c.icon}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 12, color: '#888' }}>{c.label}</div>
<div style={{ fontSize: 20, fontWeight: 600, color: '#222', marginTop: 2 }}>
{c.suffix === '건' ? c.value.toLocaleString() : (
<MoneyText value={c.value} />
)}
<span style={{ fontSize: 12, color: '#888', marginLeft: 4, fontWeight: 400 }}>
{c.suffix ?? '원'}
</span>
</div>
<div
style={{
fontSize: 11, marginTop: 4,
color: isUp ? '#0F6E56' : '#E24B4A',
display: 'flex', alignItems: 'center', gap: 2,
}}
>
{isUp ? <IconTrendingUp size={12} /> : <IconTrendingDown size={12} />}
{Math.abs(c.delta).toFixed(1)}%
</div>
</div>
</div>
</Card>
</Col>
);
})}
</Row>
{/* 차트 + 배치 현황 */}
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={16}>
<Card
title={<span style={{ fontWeight: 600 }}> ( 6)</span>}
bordered={false}
style={{ boxShadow: 'var(--ga-shadow-card)' }}
styles={{ body: { padding: 16, height: 280 } }}
>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={MONTHLY_TREND}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" vertical={false} />
<XAxis dataKey="month" tick={{ fontSize: 12, fill: '#666' }} />
<YAxis tick={{ fontSize: 12, fill: '#666' }}
label={{ value: '백만원', angle: 0, position: 'top', fontSize: 11, fill: '#999' }} />
<Tooltip />
<Legend wrapperStyle={{ fontSize: 12 }} />
<Bar dataKey="recruit" name="모집" fill="#1677FF" radius={[4, 4, 0, 0]} />
<Bar dataKey="maintain" name="유지" fill="#0F6E56" radius={[4, 4, 0, 0]} />
<Bar dataKey="chargeback" name="환수" fill="#E24B4A" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</Card>
</Col>
<Col span={8}>
<Card
title={<span style={{ fontWeight: 600 }}> (5)</span>}
bordered={false}
style={{ boxShadow: 'var(--ga-shadow-card)' }}
styles={{ body: { padding: 16 } }}
>
{BATCH_STEPS.map((s, i) => {
const dotColor = s.status === 'done' ? '#0F6E56' :
s.status === 'progress' ? '#1677FF' : '#ddd';
return (
<div key={i} style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '6px 0', borderBottom: i < BATCH_STEPS.length - 1 ? '1px dashed #f5f5f5' : 'none',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 8, height: 8, borderRadius: '50%', background: dotColor,
boxShadow: s.status === 'progress' ? '0 0 0 4px rgba(22,119,255,0.15)' : 'none',
}} />
<span style={{ fontSize: 13, color: s.status === 'pending' ? '#bbb' : '#333' }}>
Step {i + 1}. {s.name}
</span>
</div>
<span style={{ fontSize: 11, color: '#999' }}>{s.time ?? '-'}</span>
</div>
);
})}
</Card>
</Col>
</Row>
{/* 알림 */}
<Card
title={<span style={{ fontWeight: 600 }}> </span>}
bordered={false}
style={{ boxShadow: 'var(--ga-shadow-card)' }}
styles={{ body: { padding: 0 } }}
>
{[
{ tag: '대사불일치', tagColor: '#E24B4A', tagBg: '#FCEBEB',
text: '삼성생명 5월 통보액 vs 시스템 계산액 3건 차이', time: '10분 전' },
{ tag: '파싱에러', tagColor: '#BA7517', tagBg: '#FAEEDA',
text: '한화생명 raw 파일 12행 날짜 형식 오류 (parse_error_log #4521)', time: '1시간 전' },
{ tag: '승인대기', tagColor: '#1677FF', tagBg: '#E6F1FB',
text: '예외금액 원장 승인 대기 5건 (정산월 202605)', time: '3시간 전' },
].map((n, i) => (
<div key={i} style={{
display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px',
borderBottom: i < 2 ? '1px solid #f5f5f5' : 'none',
}}>
<span style={{
fontSize: 11, fontWeight: 500, padding: '2px 8px', borderRadius: 4,
color: n.tagColor, background: n.tagBg,
}}>{n.tag}</span>
<Text style={{ flex: 1, fontSize: 13 }}>{n.text}</Text>
<span style={{ fontSize: 11, color: '#999' }}>{n.time}</span>
</div>
))}
</Card>
</PageContainer>
);
}