'use client'; /** * 자재투입 모달 (로트 기반) * * 입고관리에서 생성된 실제 로트번호 기준으로 자재를 표시합니다. * 컬럼: 로트번호 | 품목명 | 가용수량 | 단위 | 투입 수량 (input, 숫자만) * 하단: 취소 / 투입 */ import { useState, useEffect, useCallback } from 'react'; import { Loader2 } from 'lucide-react'; import { ContentSkeleton } from '@/components/ui/skeleton'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { toast } from 'sonner'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { getMaterialsForWorkOrder, registerMaterialInput, type MaterialForInput } from './actions'; import type { WorkOrder } from '../ProductionDashboard/types'; import type { MaterialInput } from './types'; 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, }: MaterialInputModalProps) { const [materials, setMaterials] = useState([]); const [inputQuantities, setInputQuantities] = useState>({}); const [isLoading, setIsLoading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); // 목업 자재 데이터 (개발용) const MOCK_MATERIALS: MaterialForInput[] = Array.from({ length: 5 }, (_, i) => ({ stockLotId: 100 + i, itemId: 200 + i, lotNo: `LOT-2026-${String(i + 1).padStart(3, '0')}`, materialCode: `MAT-${String(i + 1).padStart(3, '0')}`, materialName: `자재 ${i + 1}`, specification: '', unit: 'EA', requiredQty: 100, lotAvailableQty: 500 - i * 50, fifoRank: i + 1, })); // API로 자재 목록 로드 const loadMaterials = useCallback(async () => { if (!order) return; setIsLoading(true); try { // 목업 아이템인 경우 목업 자재 데이터 사용 if (order.id.startsWith('mock-')) { setMaterials(MOCK_MATERIALS); const initialQuantities: Record = {}; MOCK_MATERIALS.forEach((m) => { initialQuantities[String(m.stockLotId)] = ''; }); setInputQuantities(initialQuantities); setIsLoading(false); return; } const result = await getMaterialsForWorkOrder(order.id); if (result.success) { setMaterials(result.data); const initialQuantities: Record = {}; result.data.forEach((m) => { initialQuantities[String(m.stockLotId ?? `item-${m.itemId}`)] = ''; }); setInputQuantities(initialQuantities); } else { toast.error(result.error || '자재 목록 조회에 실패했습니다.'); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[MaterialInputModal] loadMaterials error:', error); toast.error('자재 목록 로드 중 오류가 발생했습니다.'); } finally { setIsLoading(false); } }, [order]); // 모달이 열릴 때 데이터 로드 useEffect(() => { if (open && order) { loadMaterials(); } }, [open, order, loadMaterials]); // 투입 수량 변경 핸들러 (숫자만 허용) const handleQuantityChange = (key: string, value: string) => { const numericValue = value.replace(/[^0-9]/g, ''); setInputQuantities((prev) => ({ ...prev, [key]: numericValue, })); }; // 로트 키 생성 const getLotKey = (material: MaterialForInput) => String(material.stockLotId ?? `item-${material.itemId}`); // 투입 등록 const handleSubmit = async () => { if (!order) return; // 투입 수량이 입력된 항목 필터 (재고 있는 로트만) const materialsWithQuantity = materials.filter((m) => { if (!m.stockLotId) return false; const qty = inputQuantities[getLotKey(m)]; return qty && parseInt(qty) > 0; }); if (materialsWithQuantity.length === 0) { toast.error('투입 수량을 입력해주세요.'); return; } // 가용수량 초과 검증 for (const m of materialsWithQuantity) { const inputQty = parseInt(inputQuantities[getLotKey(m)] || '0'); if (inputQty > m.lotAvailableQty) { toast.error(`${m.lotNo}: 가용수량(${m.lotAvailableQty})을 초과할 수 없습니다.`); return; } } setIsSubmitting(true); try { const inputs = materialsWithQuantity.map((m) => ({ stock_lot_id: m.stockLotId!, qty: parseInt(inputQuantities[getLotKey(m)] || '0'), })); const result = await registerMaterialInput(order.id, inputs); if (result.success) { toast.success('자재 투입이 등록되었습니다.'); if (onSaveMaterials) { const savedList: MaterialInput[] = materialsWithQuantity.map((m) => ({ id: String(m.stockLotId), lotNo: m.lotNo || '', materialName: m.materialName, quantity: m.lotAvailableQty, unit: m.unit, inputQuantity: parseInt(inputQuantities[getLotKey(m)] || '0'), })); onSaveMaterials(order.id, savedList); } resetAndClose(); if (isCompletionFlow && onComplete) { onComplete(); } } else { toast.error(result.error || '자재 투입 등록에 실패했습니다.'); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[MaterialInputModal] handleSubmit error:', error); toast.error('자재 투입 등록 중 오류가 발생했습니다.'); } finally { setIsSubmitting(false); } }; const handleCancel = () => { resetAndClose(); }; const resetAndClose = () => { setInputQuantities({}); onOpenChange(false); }; if (!order) return null; return ( {/* 헤더 */} 자재 투입
{/* 자재 목록 테이블 */} {isLoading ? ( ) : materials.length === 0 ? (
로트번호 품목명 필요수량 가용수량 단위 투입 수량 이 공정에 배정된 자재가 없습니다.
) : (
로트번호 품목명 필요수량 가용수량 단위 투입 수량 {materials.map((material, index) => { const lotKey = getLotKey(material); const hasStock = material.stockLotId !== null; return ( {material.lotNo || ( 재고 없음 )} {material.materialName} {material.requiredQty.toLocaleString()} {hasStock ? material.lotAvailableQty.toLocaleString() : ( 0 )} {material.unit} {hasStock ? ( handleQuantityChange(lotKey, e.target.value) } className="w-20 mx-auto text-center h-8 text-sm" /> ) : ( - )} ); })}
)} {/* 버튼 영역 */}
); }