- CEO 대시보드: 섹션별 API 연동 강화 (매출/매입/생산 실데이터 표시) - DashboardSettingsDialog 드래그 정렬 및 설정 UX 개선 - dashboard transformers 모듈 분리 (파일 분할) - DocumentTable/DocumentWrapper 공통 문서 컴포넌트 추출 - LineItemsTable organisms 컴포넌트 추가 - PurchaseOrderDocument/InspectionRequestDocument 문서 컴포넌트 리팩토링 - PermissionContext → permissionStore(Zustand) 전환 - useUIStore, stores/utils/userStorage 추가 - favoritesStore/useTableColumnStore 사용자별 저장 지원 - DepositDetail/WithdrawalDetail 삭제 (통합) - PurchaseDetail/SalesDetail 간소화 - amount.ts/formatters.ts 유틸 확장 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
309 lines
15 KiB
TypeScript
309 lines
15 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import {
|
|
Factory,
|
|
AlertTriangle,
|
|
Star,
|
|
Layers,
|
|
Users,
|
|
Truck,
|
|
Package,
|
|
ClipboardList,
|
|
ListTodo,
|
|
Play,
|
|
CheckCircle2,
|
|
} from 'lucide-react';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { formatKoreanAmount } from '@/lib/utils/amount';
|
|
import type { DailyProductionData } from '../types';
|
|
|
|
// 출고 현황 독립 섹션
|
|
interface ShipmentSectionProps {
|
|
data: DailyProductionData;
|
|
}
|
|
|
|
export function ShipmentSection({ data }: ShipmentSectionProps) {
|
|
return (
|
|
<div className="rounded-xl border overflow-hidden">
|
|
<div style={{ backgroundColor: '#1e293b' }} className="px-6 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div style={{ backgroundColor: 'rgba(255,255,255,0.1)' }} className="p-2 rounded-lg">
|
|
<Truck style={{ color: '#ffffff' }} className="h-5 w-5" />
|
|
</div>
|
|
<div>
|
|
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">출고 현황</h3>
|
|
<p style={{ color: '#cbd5e1' }} className="text-sm">예상 출고 정보</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style={{ backgroundColor: '#ffffff' }} className="p-6">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div className="rounded-xl border p-4" style={{ backgroundColor: '#eff6ff', borderColor: '#bfdbfe' }}>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Package className="h-4 w-4 text-blue-500" />
|
|
<span className="text-sm font-medium text-gray-700">예상 출고 (7일 이내)</span>
|
|
</div>
|
|
<p className="text-xl font-bold text-gray-900 mb-1">{formatKoreanAmount(data.shipment.expectedAmount)}</p>
|
|
<p className="text-xs text-gray-500">{data.shipment.expectedCount}건</p>
|
|
</div>
|
|
<div className="rounded-xl border p-4" style={{ backgroundColor: '#f0fdf4', borderColor: '#bbf7d0' }}>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Truck className="h-4 w-4 text-green-500" />
|
|
<span className="text-sm font-medium text-gray-700">예상 출고 (30일 이내)</span>
|
|
</div>
|
|
<p className="text-xl font-bold text-gray-900 mb-1">{formatKoreanAmount(data.shipment.actualAmount)}</p>
|
|
<p className="text-xs text-gray-500">{data.shipment.actualCount}건</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface DailyProductionSectionProps {
|
|
data: DailyProductionData;
|
|
showShipment?: boolean;
|
|
}
|
|
|
|
const PROCESS_TAB_NAMES: Record<string, string> = {
|
|
'스크린': '스크린 공정',
|
|
'슬랫': '슬랫 공정',
|
|
'절곡': '절곡 공정',
|
|
};
|
|
|
|
export function DailyProductionSection({ data, showShipment = true }: DailyProductionSectionProps) {
|
|
const [activeTab, setActiveTab] = useState(data.processes[0]?.processName ?? '');
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="rounded-xl border overflow-hidden">
|
|
{/* 다크 헤더 */}
|
|
<div style={{ backgroundColor: '#1e293b' }} className="px-6 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div style={{ backgroundColor: 'rgba(255,255,255,0.1)' }} className="p-2 rounded-lg">
|
|
<Factory style={{ color: '#ffffff' }} className="h-5 w-5" />
|
|
</div>
|
|
<div>
|
|
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">당일 생산 현황</h3>
|
|
<p style={{ color: '#cbd5e1' }} className="text-sm">{data.date}</p>
|
|
</div>
|
|
</div>
|
|
<Badge
|
|
style={{ backgroundColor: '#8b5cf6', color: '#ffffff', border: 'none' }}
|
|
className="hover:opacity-90"
|
|
>
|
|
실시간
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ backgroundColor: '#ffffff' }} className="p-6">
|
|
{/* 공정 탭 */}
|
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
<TabsList className="mb-4">
|
|
{data.processes.map((process) => (
|
|
<TabsTrigger key={process.processName} value={process.processName}>
|
|
{PROCESS_TAB_NAMES[process.processName] ?? `${process.processName} 공정`}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
|
|
{data.processes.map((process) => (
|
|
<TabsContent key={process.processName} value={process.processName}>
|
|
{/* 요약 카드: 전체 작업 / 할일 / 작업중 / 완료 */}
|
|
<div className="grid grid-cols-4 gap-3 mb-4">
|
|
<div className="flex items-center gap-2 rounded-lg border p-3">
|
|
<ClipboardList className="h-4 w-4 text-gray-500" />
|
|
<div>
|
|
<p className="text-[11px] text-gray-500">전체 작업</p>
|
|
<p className="text-sm font-bold text-gray-900">{process.totalWork}건</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 rounded-lg border p-3">
|
|
<ListTodo className="h-4 w-4 text-orange-500" />
|
|
<div>
|
|
<p className="text-[11px] text-gray-500">할일</p>
|
|
<p className="text-sm font-bold text-gray-900">{process.todo}건</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 rounded-lg border p-3">
|
|
<Play className="h-4 w-4 text-blue-500" />
|
|
<div>
|
|
<p className="text-[11px] text-gray-500">작업중</p>
|
|
<p className="text-sm font-bold text-gray-900">{process.inProgress}건</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 rounded-lg border p-3">
|
|
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
|
<div>
|
|
<p className="text-[11px] text-gray-500">완료</p>
|
|
<p className="text-sm font-bold text-gray-900">{process.completed}건</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 4열 카드 레이아웃: 긴급 / 우선 / 일반 / 작업자 현황 */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{/* 긴급 */}
|
|
<div className="border rounded-lg overflow-hidden">
|
|
<div className="p-3 border-b" style={{ backgroundColor: '#fef2f2' }}>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-1.5">
|
|
<AlertTriangle className="h-3.5 w-3.5 text-red-500" />
|
|
<span className="text-xs font-semibold text-red-600">긴급</span>
|
|
</div>
|
|
<span className="text-sm font-bold text-gray-900">{process.urgent}건</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-2 space-y-2">
|
|
{process.workItems
|
|
.filter((item) => item.status === '진행중')
|
|
.slice(0, process.urgent)
|
|
.map((item) => (
|
|
<div key={item.id} className="border rounded p-2 hover:bg-gray-50 cursor-pointer transition-colors">
|
|
<p className="text-xs font-medium text-gray-900 truncate">{item.client}</p>
|
|
<p className="text-[11px] text-gray-500">{item.product}</p>
|
|
<p className="text-[11px] text-gray-400">{item.quantity}건</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 우선 */}
|
|
<div className="border rounded-lg overflow-hidden">
|
|
<div className="p-3 border-b" style={{ backgroundColor: '#fff7ed' }}>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-1.5">
|
|
<Star className="h-3.5 w-3.5 text-orange-500" />
|
|
<span className="text-xs font-semibold text-orange-600">우선</span>
|
|
</div>
|
|
<span className="text-sm font-bold text-gray-900">{process.subLine}건</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-2 space-y-2">
|
|
{process.workItems
|
|
.filter((item) => item.status === '대기')
|
|
.slice(0, process.subLine)
|
|
.map((item) => (
|
|
<div key={item.id} className="border rounded p-2 hover:bg-gray-50 cursor-pointer transition-colors">
|
|
<p className="text-xs font-medium text-gray-900 truncate">{item.client}</p>
|
|
<p className="text-[11px] text-gray-500">{item.product}</p>
|
|
<p className="text-[11px] text-gray-400">{item.quantity}건</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 일반 */}
|
|
<div className="border rounded-lg overflow-hidden">
|
|
<div className="p-3 border-b" style={{ backgroundColor: '#eff6ff' }}>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-1.5">
|
|
<Layers className="h-3.5 w-3.5 text-blue-500" />
|
|
<span className="text-xs font-semibold text-blue-600">일반</span>
|
|
</div>
|
|
<span className="text-sm font-bold text-gray-900">{process.regular}건</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-2 space-y-2">
|
|
{process.workItems
|
|
.slice(0, process.regular)
|
|
.map((item) => (
|
|
<div key={item.id} className="border rounded p-2 hover:bg-gray-50 cursor-pointer transition-colors">
|
|
<p className="text-xs font-medium text-gray-900 truncate">{item.client}</p>
|
|
<p className="text-[11px] text-gray-500">{item.product}</p>
|
|
<p className="text-[11px] text-gray-400">{item.quantity}건</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 작업자 현황 */}
|
|
<div className="border rounded-lg overflow-hidden">
|
|
<div className="p-3 border-b" style={{ backgroundColor: '#f0fdf4' }}>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-1.5">
|
|
<Users className="h-3.5 w-3.5 text-green-500" />
|
|
<span className="text-xs font-semibold text-green-600">작업자 현황</span>
|
|
</div>
|
|
<span className="text-sm font-bold text-gray-900">{process.workerCount}명</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-2 space-y-2">
|
|
{process.workers.map((worker, idx) => (
|
|
<div key={idx} className="border rounded p-2">
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className="text-xs font-medium text-gray-900">{worker.name}</span>
|
|
<span className="text-[11px] text-gray-500">{worker.completed}/{worker.assigned}건</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<div className="flex-1 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full rounded-full"
|
|
style={{
|
|
width: `${worker.rate}%`,
|
|
backgroundColor: worker.rate >= 80 ? '#22c55e' : worker.rate >= 50 ? '#f59e0b' : '#ef4444',
|
|
}}
|
|
/>
|
|
</div>
|
|
<span className="text-[10px] text-gray-500 min-w-[28px] text-right">{worker.rate}%</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
))}
|
|
</Tabs>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{/* 출고 현황 (별도 카드) */}
|
|
{showShipment && (
|
|
<div className="rounded-xl border overflow-hidden">
|
|
<div style={{ backgroundColor: '#1e293b' }} className="px-6 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div style={{ backgroundColor: 'rgba(255,255,255,0.1)' }} className="p-2 rounded-lg">
|
|
<Truck style={{ color: '#ffffff' }} className="h-5 w-5" />
|
|
</div>
|
|
<div>
|
|
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">출고 현황</h3>
|
|
<p style={{ color: '#cbd5e1' }} className="text-sm">예상 출고 정보</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style={{ backgroundColor: '#ffffff' }} className="p-6">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div className="rounded-xl border p-4" style={{ backgroundColor: '#eff6ff', borderColor: '#bfdbfe' }}>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Package className="h-4 w-4 text-blue-500" />
|
|
<span className="text-sm font-medium text-gray-700">예상 출고 (7일 이내)</span>
|
|
</div>
|
|
<p className="text-xl font-bold text-gray-900 mb-1">{formatKoreanAmount(data.shipment.expectedAmount)}</p>
|
|
<p className="text-xs text-gray-500">{data.shipment.expectedCount}건</p>
|
|
</div>
|
|
<div className="rounded-xl border p-4" style={{ backgroundColor: '#f0fdf4', borderColor: '#bbf7d0' }}>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Truck className="h-4 w-4 text-green-500" />
|
|
<span className="text-sm font-medium text-gray-700">예상 출고 (30일 이내)</span>
|
|
</div>
|
|
<p className="text-xl font-bold text-gray-900 mb-1">{formatKoreanAmount(data.shipment.actualAmount)}</p>
|
|
<p className="text-xs text-gray-500">{data.shipment.actualCount}건</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|