Files
sam-react-prod/src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx
유병철 4e179d2eca refactor: [CEO대시보드] 컴포넌트 분리 및 모달/섹션 리팩토링
- DashboardSettingsSections, DetailModalSections 분리
- 모달 설정(카드/접대비/복리후생/부가세/월비용) 개선
- 섹션 컴포넌트 최적화 (매출/매입/카드/미출고 등)
- mockData, types 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:20:05 +09:00

227 lines
9.7 KiB
TypeScript

'use client';
import { useState } from 'react';
import {
TrendingDown,
ArrowDownRight,
ArrowUpRight,
ShoppingCart,
DollarSign,
AlertCircle,
} from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { formatCompactAmount } from '@/lib/utils/amount';
import { MultiSelectCombobox } from '@/components/ui/multi-select-combobox';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell,
Legend,
} from 'recharts';
import { formatKoreanAmount } from '@/lib/utils/amount';
import { CollapsibleDashboardCard } from '../components';
import type { PurchaseStatusData } from '../types';
interface PurchaseStatusSectionProps {
data: PurchaseStatusData;
}
export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
const [supplierFilter, setSupplierFilter] = useState<string[]>([]);
const filteredItems = data.dailyItems
.filter((item) => supplierFilter.length === 0 || supplierFilter.includes(item.supplier));
const suppliers = [...new Set(data.dailyItems.map((item) => item.supplier))];
return (
<div className="space-y-6">
<CollapsibleDashboardCard
icon={<ShoppingCart className="h-5 w-5 text-white" />}
title="매입 현황"
subtitle="당월 매입 실적"
rightElement={
<Badge className="bg-amber-500 text-white border-none hover:opacity-90">
</Badge>
}
>
{/* 통계카드 3개 - 가로 배치 */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
{/* 누적 매입 */}
<div className="rounded-xl p-4 border bg-orange-50 border-orange-200 dark:bg-orange-900/30 dark:border-orange-800">
<div className="flex items-center gap-2 mb-2">
<div style={{ backgroundColor: '#f59e0b' }} className="p-1.5 rounded-lg">
<DollarSign className="h-4 w-4 text-white" />
</div>
<span className="text-sm font-medium text-amber-700 dark:text-amber-300"> </span>
</div>
<span className="text-xl font-bold text-foreground">
{formatKoreanAmount(data.cumulativePurchase)}
</span>
</div>
{/* 미결제 금액 */}
<div className="rounded-xl p-4 border bg-red-50 border-red-200 dark:bg-red-900/30 dark:border-red-800">
<div className="flex items-center gap-2 mb-2">
<div style={{ backgroundColor: '#ef4444' }} className="p-1.5 rounded-lg">
<AlertCircle className="h-4 w-4 text-white" />
</div>
<span className="text-sm font-medium text-red-700 dark:text-red-300"> </span>
</div>
<span className="text-xl font-bold text-foreground">
{formatKoreanAmount(data.unpaidAmount)}
</span>
</div>
{/* 전년 동기 대비 */}
<div
className={`rounded-xl p-4 border ${data.yoyChange >= 0 ? 'bg-red-50 border-red-200 dark:bg-red-900/30 dark:border-red-800' : 'bg-blue-50 border-blue-200 dark:bg-blue-900/30 dark:border-blue-800'}`}
>
<div className="flex items-center gap-2 mb-2">
<div style={{ backgroundColor: data.yoyChange >= 0 ? '#ef4444' : '#3b82f6' }} className="p-1.5 rounded-lg">
<TrendingDown className="h-4 w-4 text-white" />
</div>
<span className={`text-sm font-medium ${data.yoyChange >= 0 ? 'text-red-700 dark:text-red-300' : 'text-blue-700 dark:text-blue-300'}`}> </span>
</div>
<div className="flex items-center gap-1">
<span className="text-xl font-bold text-foreground">
{data.yoyChange >= 0 ? '+' : ''}{data.yoyChange}%
</span>
{data.yoyChange >= 0
? <ArrowUpRight className="h-4 w-4 text-red-500" />
: <ArrowDownRight className="h-4 w-4 text-blue-500" />}
</div>
</div>
</div>
{/* 차트 2열 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* 월별 매입 추이 */}
<div className="border border-border rounded-lg p-4">
<h4 className="text-sm font-semibold text-foreground mb-3"> </h4>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={data.monthlyTrend}>
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
<XAxis dataKey="month" tick={{ fontSize: 12 }} />
<YAxis tickFormatter={formatCompactAmount} tick={{ fontSize: 11 }} />
<Tooltip
formatter={(value) => [formatKoreanAmount(Number(value) || 0), '매입']}
/>
<Bar dataKey="amount" fill="#f59e0b" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
{/* 자재 유형별 비율 (Donut) */}
<div className="border border-border rounded-lg p-4">
<h4 className="text-sm font-semibold text-foreground mb-3"> </h4>
<ResponsiveContainer width="100%" height={220}>
<PieChart>
<Pie
data={data.materialRatio.map((r) => ({ name: r.name, value: r.value, percentage: r.percentage, color: r.color }) as Record<string, unknown>)}
cx="50%"
cy="40%"
innerRadius={40}
outerRadius={65}
dataKey="value"
nameKey="name"
>
{data.materialRatio.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip formatter={(value: number | undefined) => [formatKoreanAmount(value ?? 0), '금액']} />
<Legend
verticalAlign="bottom"
wrapperStyle={{ fontSize: '12px', paddingTop: '8px' }}
formatter={(value: string) => {
const item = data.materialRatio.find((r) => r.name === value);
return `${value} ${item?.percentage ?? 0}%`;
}}
/>
</PieChart>
</ResponsiveContainer>
</div>
</div>
</CollapsibleDashboardCard>
{/* 당월 매입 내역 (별도 카드) */}
<CollapsibleDashboardCard
icon={<ShoppingCart className="h-5 w-5 text-white" />}
title="당월 매입 내역"
subtitle="당월 매입 거래 상세"
bodyClassName="p-0"
>
<div className="p-3 bg-muted/50 border-b border-border space-y-2">
<div className="text-sm text-muted-foreground"> {filteredItems.length}</div>
<MultiSelectCombobox
options={suppliers.map((s) => ({ value: s, label: s }))}
value={supplierFilter}
onChange={setSupplierFilter}
placeholder="전체 공급처"
className="w-full h-8 text-xs"
/>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm min-w-[500px]">
<thead>
<tr className="bg-muted/50 border-b border-border">
<th className="px-4 py-2 text-left text-muted-foreground font-medium"></th>
<th className="px-4 py-2 text-left text-muted-foreground font-medium"></th>
<th className="px-4 py-2 text-left text-muted-foreground font-medium"></th>
<th className="px-4 py-2 text-right text-muted-foreground font-medium"></th>
<th className="px-4 py-2 text-center text-muted-foreground font-medium"></th>
</tr>
</thead>
<tbody>
{filteredItems.map((item, idx) => (
<tr key={idx} className="border-b border-border last:border-b-0 hover:bg-muted/30">
<td className="px-4 py-2 text-muted-foreground">{item.date}</td>
<td className="px-4 py-2 text-muted-foreground">{item.supplier}</td>
<td className="px-4 py-2 text-muted-foreground">{item.item}</td>
<td className="px-4 py-2 text-right text-foreground font-medium">
{item.amount.toLocaleString()}
</td>
<td className="px-4 py-2 text-center">
<Badge
variant="outline"
className={
item.status === '결제완료'
? 'text-green-600 border-green-200 bg-green-50 dark:text-green-400 dark:border-green-800 dark:bg-green-900/30'
: item.status === '미결제'
? 'text-red-600 border-red-200 bg-red-50 dark:text-red-400 dark:border-red-800 dark:bg-red-900/30'
: 'text-orange-600 border-orange-200 bg-orange-50 dark:text-orange-400 dark:border-orange-800 dark:bg-orange-900/30'
}
>
{item.status}
</Badge>
</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="bg-muted font-semibold">
<td className="px-4 py-2 text-muted-foreground" colSpan={3}></td>
<td className="px-4 py-2 text-right text-foreground">
{filteredItems.reduce((sum, item) => sum + item.amount, 0).toLocaleString()}
</td>
<td />
</tr>
</tfoot>
</table>
</div>
</CollapsibleDashboardCard>
</div>
);
}