fix: [production] 자재투입 모달 — bomGroupKey 그룹핑, 카테고리 순서 정렬, 번호 표시
- bomGroupKey 기반 그룹핑 (같은 item_id라도 category+partType별 분리) - 카테고리 순서 정렬 (가이드레일→하단마감재→셔터박스→연기차단재) - 카테고리 내 원형번호(①②③) 표시 - partType 배지 추가 - MaterialForItemInput에 bomGroupKey 필드 추가
This commit is contained in:
@@ -99,16 +99,22 @@ export function MaterialInputModal({
|
|||||||
const getLotKey = (material: MaterialForInput) =>
|
const getLotKey = (material: MaterialForInput) =>
|
||||||
String(material.stockLotId ?? `item-${material.itemId}`);
|
String(material.stockLotId ?? `item-${material.itemId}`);
|
||||||
|
|
||||||
// 품목별 그룹핑
|
// 품목별 그룹핑 (BOM 엔트리별 고유키 사용 — 같은 item_id라도 category+partType 다르면 별도 그룹)
|
||||||
const materialGroups: MaterialGroup[] = useMemo(() => {
|
const materialGroups: MaterialGroup[] = useMemo(() => {
|
||||||
// dynamic_bom 항목은 (itemId, workOrderItemId) 쌍으로 그룹핑
|
|
||||||
const groups = new Map<string, MaterialForInput[]>();
|
const groups = new Map<string, MaterialForInput[]>();
|
||||||
for (const m of materials) {
|
for (const m of materials) {
|
||||||
const groupKey = m.workOrderItemId ? `${m.itemId}_${m.workOrderItemId}` : String(m.itemId);
|
const itemInput = m as unknown as MaterialForItemInput;
|
||||||
|
const groupKey = itemInput.bomGroupKey
|
||||||
|
?? (m.workOrderItemId ? `${m.itemId}_${m.workOrderItemId}` : String(m.itemId));
|
||||||
const existing = groups.get(groupKey) || [];
|
const existing = groups.get(groupKey) || [];
|
||||||
existing.push(m);
|
existing.push(m);
|
||||||
groups.set(groupKey, existing);
|
groups.set(groupKey, existing);
|
||||||
}
|
}
|
||||||
|
// 작업일지와 동일한 카테고리 순서
|
||||||
|
const categoryOrder: Record<string, number> = {
|
||||||
|
guideRail: 0, bottomBar: 1, shutterBox: 2, smokeBarrier: 3,
|
||||||
|
};
|
||||||
|
|
||||||
return Array.from(groups.entries()).map(([groupKey, lots]) => {
|
return Array.from(groups.entries()).map(([groupKey, lots]) => {
|
||||||
const first = lots[0];
|
const first = lots[0];
|
||||||
const itemInput = first as unknown as MaterialForItemInput;
|
const itemInput = first as unknown as MaterialForItemInput;
|
||||||
@@ -129,6 +135,10 @@ export function MaterialInputModal({
|
|||||||
partType: first.partType,
|
partType: first.partType,
|
||||||
category: first.category,
|
category: first.category,
|
||||||
};
|
};
|
||||||
|
}).sort((a, b) => {
|
||||||
|
const catA = categoryOrder[a.category ?? ''] ?? 99;
|
||||||
|
const catB = categoryOrder[b.category ?? ''] ?? 99;
|
||||||
|
return catA - catB;
|
||||||
});
|
});
|
||||||
}, [materials]);
|
}, [materials]);
|
||||||
|
|
||||||
@@ -344,7 +354,15 @@ export function MaterialInputModal({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4 flex-1 overflow-y-auto min-h-0">
|
<div className="space-y-4 flex-1 overflow-y-auto min-h-0">
|
||||||
{materialGroups.map((group) => {
|
{materialGroups.map((group, groupIdx) => {
|
||||||
|
// 같은 카테고리 내 순번 계산 (①②③...)
|
||||||
|
const categoryIndex = group.category
|
||||||
|
? materialGroups.slice(0, groupIdx).filter(g => g.category === group.category).length
|
||||||
|
: -1;
|
||||||
|
const circledNumbers = ['①','②','③','④','⑤','⑥','⑦','⑧','⑨','⑩'];
|
||||||
|
const circledNum = categoryIndex >= 0 && categoryIndex < circledNumbers.length
|
||||||
|
? circledNumbers[categoryIndex] : '';
|
||||||
|
|
||||||
const groupAllocated = group.lots.reduce(
|
const groupAllocated = group.lots.reduce(
|
||||||
(sum, lot) => sum + (allocations.get(getLotKey(lot)) || 0),
|
(sum, lot) => sum + (allocations.get(getLotKey(lot)) || 0),
|
||||||
0
|
0
|
||||||
@@ -372,6 +390,11 @@ export function MaterialInputModal({
|
|||||||
group.category}
|
group.category}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{group.partType && (
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-gray-200 text-gray-600 font-medium">
|
||||||
|
{circledNum}{group.partType}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="text-sm font-semibold text-gray-900">
|
<span className="text-sm font-semibold text-gray-900">
|
||||||
{group.materialName}
|
{group.materialName}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ export async function registerMaterialInput(
|
|||||||
export interface MaterialForItemInput extends MaterialForInput {
|
export interface MaterialForItemInput extends MaterialForInput {
|
||||||
alreadyInputted: number; // 이미 투입된 수량
|
alreadyInputted: number; // 이미 투입된 수량
|
||||||
remainingRequiredQty: number; // 남은 필요 수량
|
remainingRequiredQty: number; // 남은 필요 수량
|
||||||
|
bomGroupKey?: string; // BOM 엔트리별 고유 그룹키 (category+partType 기반)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMaterialsForItem(
|
export async function getMaterialsForItem(
|
||||||
@@ -336,6 +337,7 @@ export async function getMaterialsForItem(
|
|||||||
receipt_date: string | null; supplier: string | null;
|
receipt_date: string | null; supplier: string | null;
|
||||||
// dynamic_bom 추가 필드
|
// dynamic_bom 추가 필드
|
||||||
work_order_item_id?: number; lot_prefix?: string; part_type?: string; category?: string;
|
work_order_item_id?: number; lot_prefix?: string; part_type?: string; category?: string;
|
||||||
|
bom_group_key?: string;
|
||||||
}
|
}
|
||||||
const result = await executeServerAction<MaterialItemApiItem[]>({
|
const result = await executeServerAction<MaterialItemApiItem[]>({
|
||||||
url: `${API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/materials`,
|
url: `${API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/materials`,
|
||||||
@@ -354,6 +356,7 @@ export async function getMaterialsForItem(
|
|||||||
remainingRequiredQty: item.remaining_required_qty,
|
remainingRequiredQty: item.remaining_required_qty,
|
||||||
workOrderItemId: item.work_order_item_id, lotPrefix: item.lot_prefix,
|
workOrderItemId: item.work_order_item_id, lotPrefix: item.lot_prefix,
|
||||||
partType: item.part_type, category: item.category,
|
partType: item.part_type, category: item.category,
|
||||||
|
bomGroupKey: item.bom_group_key,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user