fix: [자재투입] 입고 로트번호 표시 및 로트별 수량 투입으로 변경
- MaterialForInput 타입: stockLotId, lotNo, lotAvailableQty 추가 - 로트번호 컬럼에 실제 입고 로트번호(lot_no) 표시 - 수량 컬럼을 가용수량(lotAvailableQty)으로 변경 - 가용수량 초과 검증 추가 - registerMaterialInput: stock_lot_id+qty 로트별 투입 방식으로 변경
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 자재투입 모달 (기획서 기반)
|
||||
* 자재투입 모달 (로트 기반)
|
||||
*
|
||||
* 기획서 변경: BOM 체크박스 → 투입수량 입력 테이블
|
||||
* 컬럼: 로트번호 | 품목명 | 수량 | 단위 | 투입 수량 (input, 숫자만)
|
||||
* 입고관리에서 생성된 실제 로트번호 기준으로 자재를 표시합니다.
|
||||
* 컬럼: 로트번호 | 품목명 | 가용수량 | 단위 | 투입 수량 (input, 숫자만)
|
||||
* 하단: 취소 / 투입
|
||||
*/
|
||||
|
||||
@@ -56,13 +56,17 @@ export function MaterialInputModal({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// 목업 자재 데이터 (기획서 기반 10행)
|
||||
const MOCK_MATERIALS: MaterialForInput[] = Array.from({ length: 10 }, (_, i) => ({
|
||||
id: 100 + i,
|
||||
materialCode: '123123',
|
||||
materialName: '품목명',
|
||||
unit: 'm',
|
||||
currentStock: 500,
|
||||
// 목업 자재 데이터 (개발용)
|
||||
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,
|
||||
}));
|
||||
|
||||
@@ -77,7 +81,7 @@ export function MaterialInputModal({
|
||||
setMaterials(MOCK_MATERIALS);
|
||||
const initialQuantities: Record<string, string> = {};
|
||||
MOCK_MATERIALS.forEach((m) => {
|
||||
initialQuantities[String(m.id)] = '';
|
||||
initialQuantities[String(m.stockLotId)] = '';
|
||||
});
|
||||
setInputQuantities(initialQuantities);
|
||||
setIsLoading(false);
|
||||
@@ -87,10 +91,9 @@ export function MaterialInputModal({
|
||||
const result = await getMaterialsForWorkOrder(order.id);
|
||||
if (result.success) {
|
||||
setMaterials(result.data);
|
||||
// 초기 투입 수량 비우기
|
||||
const initialQuantities: Record<string, string> = {};
|
||||
result.data.forEach((m) => {
|
||||
initialQuantities[String(m.id)] = '';
|
||||
initialQuantities[String(m.stockLotId ?? `item-${m.itemId}`)] = '';
|
||||
});
|
||||
setInputQuantities(initialQuantities);
|
||||
} else {
|
||||
@@ -113,22 +116,26 @@ export function MaterialInputModal({
|
||||
}, [open, order, loadMaterials]);
|
||||
|
||||
// 투입 수량 변경 핸들러 (숫자만 허용)
|
||||
const handleQuantityChange = (materialId: string, value: string) => {
|
||||
// 숫자만 허용
|
||||
const handleQuantityChange = (key: string, value: string) => {
|
||||
const numericValue = value.replace(/[^0-9]/g, '');
|
||||
setInputQuantities((prev) => ({
|
||||
...prev,
|
||||
[materialId]: numericValue,
|
||||
[key]: numericValue,
|
||||
}));
|
||||
};
|
||||
|
||||
// 로트 키 생성
|
||||
const getLotKey = (material: MaterialForInput) =>
|
||||
String(material.stockLotId ?? `item-${material.itemId}`);
|
||||
|
||||
// 투입 등록
|
||||
const handleSubmit = async () => {
|
||||
if (!order) return;
|
||||
|
||||
// 투입 수량이 입력된 항목 필터
|
||||
// 투입 수량이 입력된 항목 필터 (재고 있는 로트만)
|
||||
const materialsWithQuantity = materials.filter((m) => {
|
||||
const qty = inputQuantities[String(m.id)];
|
||||
if (!m.stockLotId) return false;
|
||||
const qty = inputQuantities[getLotKey(m)];
|
||||
return qty && parseInt(qty) > 0;
|
||||
});
|
||||
|
||||
@@ -137,23 +144,35 @@ export function MaterialInputModal({
|
||||
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 materialIds = materialsWithQuantity.map((m) => m.id);
|
||||
const result = await registerMaterialInput(order.id, materialIds);
|
||||
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('자재 투입이 등록되었습니다.');
|
||||
|
||||
// onSaveMaterials 콜백 호출
|
||||
if (onSaveMaterials) {
|
||||
const savedList: MaterialInput[] = materialsWithQuantity.map((m) => ({
|
||||
id: String(m.id),
|
||||
lotNo: '', // API에서 가져올 필드
|
||||
id: String(m.stockLotId),
|
||||
lotNo: m.lotNo || '',
|
||||
materialName: m.materialName,
|
||||
quantity: m.currentStock,
|
||||
quantity: m.lotAvailableQty,
|
||||
unit: m.unit,
|
||||
inputQuantity: parseInt(inputQuantities[String(m.id)] || '0'),
|
||||
inputQuantity: parseInt(inputQuantities[getLotKey(m)] || '0'),
|
||||
}));
|
||||
onSaveMaterials(order.id, savedList);
|
||||
}
|
||||
@@ -205,7 +224,7 @@ export function MaterialInputModal({
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="text-center">로트번호</TableHead>
|
||||
<TableHead className="text-center">품목명</TableHead>
|
||||
<TableHead className="text-center">수량</TableHead>
|
||||
<TableHead className="text-center">가용수량</TableHead>
|
||||
<TableHead className="text-center">단위</TableHead>
|
||||
<TableHead className="text-center">투입 수량</TableHead>
|
||||
</TableRow>
|
||||
@@ -226,40 +245,52 @@ export function MaterialInputModal({
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="text-center font-medium">로트번호</TableHead>
|
||||
<TableHead className="text-center font-medium">품목명</TableHead>
|
||||
<TableHead className="text-center font-medium">수량</TableHead>
|
||||
<TableHead className="text-center font-medium">가용수량</TableHead>
|
||||
<TableHead className="text-center font-medium">단위</TableHead>
|
||||
<TableHead className="text-center font-medium">투입 수량</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{materials.map((material, index) => (
|
||||
<TableRow key={`mat-${material.id}-${index}`}>
|
||||
<TableCell className="text-center text-sm">
|
||||
{material.materialCode}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{material.materialName}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{material.currentStock.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{material.unit}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
placeholder="0"
|
||||
value={inputQuantities[String(material.id)] || ''}
|
||||
onChange={(e) =>
|
||||
handleQuantityChange(String(material.id), e.target.value)
|
||||
}
|
||||
className="w-20 mx-auto text-center h-8 text-sm"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{materials.map((material, index) => {
|
||||
const lotKey = getLotKey(material);
|
||||
const hasStock = material.stockLotId !== null;
|
||||
return (
|
||||
<TableRow key={`mat-${lotKey}-${index}`}>
|
||||
<TableCell className="text-center text-sm">
|
||||
{material.lotNo || (
|
||||
<span className="text-gray-400">재고 없음</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{material.materialName}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{hasStock ? material.lotAvailableQty.toLocaleString() : (
|
||||
<span className="text-red-500">0</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{material.unit}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{hasStock ? (
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
placeholder="0"
|
||||
value={inputQuantities[lotKey] || ''}
|
||||
onChange={(e) =>
|
||||
handleQuantityChange(lotKey, e.target.value)
|
||||
}
|
||||
className="w-20 mx-auto text-center h-8 text-sm"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-400 text-sm">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
@@ -294,4 +325,4 @@ export function MaterialInputModal({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -277,13 +277,17 @@ export async function completeWorkOrder(
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 자재 목록 조회 (BOM 기준) =====
|
||||
// ===== 자재 목록 조회 (로트 기준) =====
|
||||
export interface MaterialForInput {
|
||||
id: number;
|
||||
stockLotId: number | null; // StockLot ID (null이면 재고 없음)
|
||||
itemId: number;
|
||||
lotNo: string | null; // 실제 입고 로트번호
|
||||
materialCode: string;
|
||||
materialName: string;
|
||||
specification: string;
|
||||
unit: string;
|
||||
currentStock: number;
|
||||
requiredQty: number; // 필요 수량
|
||||
lotAvailableQty: number; // 로트별 가용 수량
|
||||
fifoRank: number;
|
||||
}
|
||||
|
||||
@@ -330,20 +334,28 @@ export async function getMaterialsForWorkOrder(
|
||||
};
|
||||
}
|
||||
|
||||
// API 응답을 MaterialForInput 형식으로 변환
|
||||
// API 응답을 MaterialForInput 형식으로 변환 (로트 단위)
|
||||
const materials: MaterialForInput[] = (result.data || []).map((item: {
|
||||
id: number;
|
||||
stock_lot_id: number | null;
|
||||
item_id: number;
|
||||
lot_no: string | null;
|
||||
material_code: string;
|
||||
material_name: string;
|
||||
specification: string;
|
||||
unit: string;
|
||||
current_stock: number;
|
||||
required_qty: number;
|
||||
lot_available_qty: number;
|
||||
fifo_rank: number;
|
||||
}) => ({
|
||||
id: item.id,
|
||||
stockLotId: item.stock_lot_id,
|
||||
itemId: item.item_id,
|
||||
lotNo: item.lot_no,
|
||||
materialCode: item.material_code,
|
||||
materialName: item.material_name,
|
||||
specification: item.specification ?? '',
|
||||
unit: item.unit,
|
||||
currentStock: item.current_stock,
|
||||
requiredQty: item.required_qty,
|
||||
lotAvailableQty: item.lot_available_qty,
|
||||
fifoRank: item.fifo_rank,
|
||||
}));
|
||||
|
||||
@@ -362,17 +374,17 @@ export async function getMaterialsForWorkOrder(
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 자재 투입 등록 =====
|
||||
// ===== 자재 투입 등록 (로트별 수량) =====
|
||||
export async function registerMaterialInput(
|
||||
workOrderId: string,
|
||||
materialIds: number[]
|
||||
inputs: { stock_lot_id: number; qty: number }[]
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/material-inputs`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ material_ids: materialIds }),
|
||||
body: JSON.stringify({ inputs }),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user