244 lines
7.1 KiB
TypeScript
244 lines
7.1 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 자재투입 모달
|
||
|
|
*
|
||
|
|
* - FIFO 순위 표시
|
||
|
|
* - 자재 테이블 (BOM 기준)
|
||
|
|
* - 투입 등록 기능
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { useState } from 'react';
|
||
|
|
import { Package } from 'lucide-react';
|
||
|
|
import {
|
||
|
|
Dialog,
|
||
|
|
DialogContent,
|
||
|
|
DialogDescription,
|
||
|
|
DialogFooter,
|
||
|
|
DialogHeader,
|
||
|
|
DialogTitle,
|
||
|
|
} from '@/components/ui/dialog';
|
||
|
|
import { Button } from '@/components/ui/button';
|
||
|
|
import { Badge } from '@/components/ui/badge';
|
||
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
||
|
|
import {
|
||
|
|
Table,
|
||
|
|
TableBody,
|
||
|
|
TableCell,
|
||
|
|
TableHead,
|
||
|
|
TableHeader,
|
||
|
|
TableRow,
|
||
|
|
} from '@/components/ui/table';
|
||
|
|
import type { WorkOrder } from '../ProductionDashboard/types';
|
||
|
|
import type { MaterialInput } from './types';
|
||
|
|
|
||
|
|
// Mock 자재 데이터
|
||
|
|
const MOCK_MATERIALS: MaterialInput[] = [
|
||
|
|
{
|
||
|
|
id: '1',
|
||
|
|
materialCode: 'KD-RM-001',
|
||
|
|
materialName: 'SPHC-SD 1.6T',
|
||
|
|
unit: 'KG',
|
||
|
|
currentStock: 500,
|
||
|
|
fifoRank: 1,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '2',
|
||
|
|
materialCode: 'KD-RM-002',
|
||
|
|
materialName: 'EGI 1.55T',
|
||
|
|
unit: 'KG',
|
||
|
|
currentStock: 350,
|
||
|
|
fifoRank: 2,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '3',
|
||
|
|
materialCode: 'KD-SM-001',
|
||
|
|
materialName: '볼트 M6x20',
|
||
|
|
unit: 'EA',
|
||
|
|
currentStock: 1200,
|
||
|
|
fifoRank: 3,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
interface MaterialInputModalProps {
|
||
|
|
open: boolean;
|
||
|
|
onOpenChange: (open: boolean) => void;
|
||
|
|
order: WorkOrder | null;
|
||
|
|
/** 전량완료 흐름에서 사용 - 투입 등록/취소 후 완료 처리 */
|
||
|
|
onComplete?: () => void;
|
||
|
|
/** 전량완료 흐름 여부 (취소 시에도 완료 처리) */
|
||
|
|
isCompletionFlow?: boolean;
|
||
|
|
/** 자재 투입 저장 콜백 */
|
||
|
|
onSaveMaterials?: (orderId: string, materials: MaterialInput[]) => void;
|
||
|
|
/** 이미 투입된 자재 목록 */
|
||
|
|
savedMaterials?: MaterialInput[];
|
||
|
|
}
|
||
|
|
|
||
|
|
export function MaterialInputModal({
|
||
|
|
open,
|
||
|
|
onOpenChange,
|
||
|
|
order,
|
||
|
|
onComplete,
|
||
|
|
isCompletionFlow = false,
|
||
|
|
onSaveMaterials,
|
||
|
|
savedMaterials = [],
|
||
|
|
}: MaterialInputModalProps) {
|
||
|
|
const [selectedMaterials, setSelectedMaterials] = useState<Set<string>>(new Set());
|
||
|
|
const [materials] = useState<MaterialInput[]>(MOCK_MATERIALS);
|
||
|
|
|
||
|
|
// 이미 투입된 자재가 있으면 선택 상태로 초기화
|
||
|
|
const hasSavedMaterials = savedMaterials.length > 0;
|
||
|
|
|
||
|
|
const handleToggleMaterial = (materialId: string) => {
|
||
|
|
setSelectedMaterials((prev) => {
|
||
|
|
const next = new Set(prev);
|
||
|
|
if (next.has(materialId)) {
|
||
|
|
next.delete(materialId);
|
||
|
|
} else {
|
||
|
|
next.add(materialId);
|
||
|
|
}
|
||
|
|
return next;
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
// 자재 투입 등록
|
||
|
|
const handleSubmit = () => {
|
||
|
|
if (!order) return;
|
||
|
|
|
||
|
|
// 선택된 자재 정보 추출
|
||
|
|
const selectedMaterialList = materials.filter((m) => selectedMaterials.has(m.id));
|
||
|
|
console.log('[자재투입] 저장:', order.id, selectedMaterialList);
|
||
|
|
|
||
|
|
// 자재 저장 콜백
|
||
|
|
if (onSaveMaterials) {
|
||
|
|
onSaveMaterials(order.id, selectedMaterialList);
|
||
|
|
}
|
||
|
|
|
||
|
|
setSelectedMaterials(new Set());
|
||
|
|
onOpenChange(false);
|
||
|
|
|
||
|
|
// 전량완료 흐름이면 완료 처리
|
||
|
|
if (isCompletionFlow && onComplete) {
|
||
|
|
onComplete();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 건너뛰기 (자재 없이 완료) - 전량완료 흐름에서만 사용
|
||
|
|
const handleSkip = () => {
|
||
|
|
setSelectedMaterials(new Set());
|
||
|
|
onOpenChange(false);
|
||
|
|
// 전량완료 흐름이면 완료 처리
|
||
|
|
if (onComplete) {
|
||
|
|
onComplete();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 취소 (모달만 닫기)
|
||
|
|
const handleCancel = () => {
|
||
|
|
setSelectedMaterials(new Set());
|
||
|
|
onOpenChange(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
const getFifoRankBadge = (rank: number) => {
|
||
|
|
const colors = {
|
||
|
|
1: 'bg-red-100 text-red-800',
|
||
|
|
2: 'bg-orange-100 text-orange-800',
|
||
|
|
3: 'bg-gray-100 text-gray-800',
|
||
|
|
};
|
||
|
|
const labels = {
|
||
|
|
1: '최우선',
|
||
|
|
2: '차선',
|
||
|
|
3: '대기',
|
||
|
|
};
|
||
|
|
return (
|
||
|
|
<Badge className={colors[rank as 1 | 2 | 3] || colors[3]}>
|
||
|
|
{rank}위 ({labels[rank as 1 | 2 | 3] || labels[3]})
|
||
|
|
</Badge>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!order) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
|
|
<DialogContent className="max-w-2xl">
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle className="flex items-center gap-2">
|
||
|
|
<Package className="h-5 w-5" />
|
||
|
|
투입자재 등록
|
||
|
|
</DialogTitle>
|
||
|
|
<DialogDescription>
|
||
|
|
작업지시 {order.orderNo}에 투입할 자재를 선택하세요.
|
||
|
|
</DialogDescription>
|
||
|
|
</DialogHeader>
|
||
|
|
|
||
|
|
<div className="space-y-4">
|
||
|
|
{/* FIFO 순위 안내 */}
|
||
|
|
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||
|
|
<span>FIFO 순위:</span>
|
||
|
|
<span className="flex items-center gap-1">
|
||
|
|
<Badge className="bg-red-100 text-red-800">1</Badge> 최우선
|
||
|
|
</span>
|
||
|
|
<span className="flex items-center gap-1">
|
||
|
|
<Badge className="bg-orange-100 text-orange-800">2</Badge> 차선
|
||
|
|
</span>
|
||
|
|
<span className="flex items-center gap-1">
|
||
|
|
<Badge className="bg-gray-100 text-gray-800">3+</Badge> 대기
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 자재 테이블 */}
|
||
|
|
{materials.length === 0 ? (
|
||
|
|
<div className="py-8 text-center text-muted-foreground border rounded-lg">
|
||
|
|
이 공정에 배정된 자재가 없습니다.
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<Table>
|
||
|
|
<TableHeader>
|
||
|
|
<TableRow>
|
||
|
|
<TableHead className="w-12">선택</TableHead>
|
||
|
|
<TableHead>자재코드</TableHead>
|
||
|
|
<TableHead>자재명</TableHead>
|
||
|
|
<TableHead>단위</TableHead>
|
||
|
|
<TableHead className="text-right">현재고</TableHead>
|
||
|
|
<TableHead>FIFO</TableHead>
|
||
|
|
</TableRow>
|
||
|
|
</TableHeader>
|
||
|
|
<TableBody>
|
||
|
|
{materials.map((material) => (
|
||
|
|
<TableRow key={material.id}>
|
||
|
|
<TableCell>
|
||
|
|
<Checkbox
|
||
|
|
checked={selectedMaterials.has(material.id)}
|
||
|
|
onCheckedChange={() => handleToggleMaterial(material.id)}
|
||
|
|
/>
|
||
|
|
</TableCell>
|
||
|
|
<TableCell className="font-medium">{material.materialCode}</TableCell>
|
||
|
|
<TableCell>{material.materialName}</TableCell>
|
||
|
|
<TableCell>{material.unit}</TableCell>
|
||
|
|
<TableCell className="text-right">{material.currentStock.toLocaleString()}</TableCell>
|
||
|
|
<TableCell>{getFifoRankBadge(material.fifoRank)}</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
))}
|
||
|
|
</TableBody>
|
||
|
|
</Table>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<DialogFooter className="flex-col sm:flex-row gap-2">
|
||
|
|
<Button variant="outline" onClick={handleCancel}>
|
||
|
|
취소
|
||
|
|
</Button>
|
||
|
|
{isCompletionFlow && (
|
||
|
|
<Button variant="secondary" onClick={handleSkip}>
|
||
|
|
건너뛰기
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
<Button onClick={handleSubmit} disabled={selectedMaterials.size === 0}>
|
||
|
|
투입 등록
|
||
|
|
</Button>
|
||
|
|
</DialogFooter>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
);
|
||
|
|
}
|