- DashboardSettingsSections, DetailModalSections 분리 - 모달 설정(카드/접대비/복리후생/부가세/월비용) 개선 - 섹션 컴포넌트 최적화 (매출/매입/카드/미출고 등) - mockData, types 정리 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
227 lines
9.7 KiB
TypeScript
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>
|
|
);
|
|
}
|