feat(WEB): 절곡 자재투입 LOT 매핑 프론트엔드 연동
- actions.ts: MaterialForInput에 workOrderItemId/lotPrefix/partType/category 필드 추가 - MaterialInputModal: dynamic_bom 세부품목 단위 그룹핑 + category 배지 표시 - 작업일지 4개 섹션 lotNoMap prop 추가 (GuideRail/BottomBar/ShutterBox/SmokeBarrier) - WorkLogModal: materialLots에서 BD-* 필터링 → lotNoMap 빌드 후 전달 - utils.ts: lengthToCode() 래퍼 함수 추가
This commit is contained in:
@@ -47,6 +47,7 @@ interface MaterialInputModalProps {
|
||||
|
||||
interface MaterialGroup {
|
||||
itemId: number;
|
||||
groupKey: string; // 그룹 식별 키 (itemId 또는 itemId_woItemId)
|
||||
materialName: string;
|
||||
materialCode: string;
|
||||
requiredQty: number;
|
||||
@@ -54,6 +55,11 @@ interface MaterialGroup {
|
||||
alreadyInputted: number; // 이미 투입된 수량
|
||||
unit: string;
|
||||
lots: MaterialForInput[];
|
||||
// dynamic_bom 추가 정보
|
||||
workOrderItemId?: number;
|
||||
lotPrefix?: string;
|
||||
partType?: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
const fmtQty = (v: number) => formatNumber(parseFloat(String(v)));
|
||||
@@ -93,19 +99,22 @@ export function MaterialInputModal({
|
||||
|
||||
// 품목별 그룹핑
|
||||
const materialGroups: MaterialGroup[] = useMemo(() => {
|
||||
const groups = new Map<number, MaterialForInput[]>();
|
||||
// dynamic_bom 항목은 (itemId, workOrderItemId) 쌍으로 그룹핑
|
||||
const groups = new Map<string, MaterialForInput[]>();
|
||||
for (const m of materials) {
|
||||
const existing = groups.get(m.itemId) || [];
|
||||
const groupKey = m.workOrderItemId ? `${m.itemId}_${m.workOrderItemId}` : String(m.itemId);
|
||||
const existing = groups.get(groupKey) || [];
|
||||
existing.push(m);
|
||||
groups.set(m.itemId, existing);
|
||||
groups.set(groupKey, existing);
|
||||
}
|
||||
return Array.from(groups.entries()).map(([itemId, lots]) => {
|
||||
return Array.from(groups.entries()).map(([groupKey, lots]) => {
|
||||
const first = lots[0];
|
||||
const itemInput = first as unknown as MaterialForItemInput;
|
||||
const alreadyInputted = itemInput.alreadyInputted ?? 0;
|
||||
const effectiveRequiredQty = Math.max(0, itemInput.remainingRequiredQty ?? first.requiredQty);
|
||||
return {
|
||||
itemId,
|
||||
itemId: first.itemId,
|
||||
groupKey,
|
||||
materialName: first.materialName,
|
||||
materialCode: first.materialCode,
|
||||
requiredQty: first.requiredQty,
|
||||
@@ -113,6 +122,10 @@ export function MaterialInputModal({
|
||||
alreadyInputted,
|
||||
unit: first.unit,
|
||||
lots: lots.sort((a, b) => a.fifoRank - b.fifoRank),
|
||||
workOrderItemId: first.workOrderItemId,
|
||||
lotPrefix: first.lotPrefix,
|
||||
partType: first.partType,
|
||||
category: first.category,
|
||||
};
|
||||
});
|
||||
}, [materials]);
|
||||
@@ -208,13 +221,20 @@ export function MaterialInputModal({
|
||||
const handleSubmit = async () => {
|
||||
if (!order) return;
|
||||
|
||||
// 배분된 로트만 추출
|
||||
const inputs: { stock_lot_id: number; qty: number }[] = [];
|
||||
// 배분된 로트만 추출 (dynamic_bom이면 work_order_item_id 포함)
|
||||
const inputs: { stock_lot_id: number; qty: number; work_order_item_id?: number }[] = [];
|
||||
for (const [lotKey, allocQty] of allocations) {
|
||||
if (allocQty > 0) {
|
||||
const material = materials.find((m) => getLotKey(m) === lotKey);
|
||||
if (material?.stockLotId) {
|
||||
inputs.push({ stock_lot_id: material.stockLotId, qty: allocQty });
|
||||
const input: { stock_lot_id: number; qty: number; work_order_item_id?: number } = {
|
||||
stock_lot_id: material.stockLotId,
|
||||
qty: allocQty,
|
||||
};
|
||||
if (material.workOrderItemId) {
|
||||
input.work_order_item_id = material.workOrderItemId;
|
||||
}
|
||||
inputs.push(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,10 +330,25 @@ export function MaterialInputModal({
|
||||
const isFulfilled = isAlreadyComplete || groupAllocated >= group.effectiveRequiredQty;
|
||||
|
||||
return (
|
||||
<div key={group.itemId} className="border rounded-lg overflow-hidden">
|
||||
<div key={group.groupKey} className="border rounded-lg overflow-hidden">
|
||||
{/* 품목 그룹 헤더 */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 bg-gray-50 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
{group.category && (
|
||||
<span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
|
||||
group.category === 'guideRail' ? 'bg-blue-100 text-blue-700' :
|
||||
group.category === 'bottomBar' ? 'bg-green-100 text-green-700' :
|
||||
group.category === 'shutterBox' ? 'bg-orange-100 text-orange-700' :
|
||||
group.category === 'smokeBarrier' ? 'bg-red-100 text-red-700' :
|
||||
'bg-gray-100 text-gray-600'
|
||||
}`}>
|
||||
{group.category === 'guideRail' ? '가이드레일' :
|
||||
group.category === 'bottomBar' ? '하단마감재' :
|
||||
group.category === 'shutterBox' ? '셔터박스' :
|
||||
group.category === 'smokeBarrier' ? '연기차단재' :
|
||||
group.category}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
{group.materialName}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user