feat: 공정실행 단일/다중품목 뱃지 + 품목타입 표시
- 단일품목: 회색 뱃지 [단일 · 제품] - 다중품목: 파랑 뱃지 [다중 1/2 · 반제품] - 리워크: 주황 뱃지 유지 (기존) - item_info.type으로 품목타입(제품/반제품/원재료/부재료) 표시 - workInstructionController: getList에 item_type 추가 - WorkOrderList: multiBatchInfo useMemo로 단일/다중 판단 - ProcessWork: batchBadge로 헤더에 뱃지 표시
This commit is contained in:
@@ -404,6 +404,14 @@ export function ProcessWork({ processId }: ProcessWorkProps) {
|
||||
const [packageUnit, setPackageUnit] = useState<string>("");
|
||||
const [inboundDone, setInboundDone] = useState(false);
|
||||
|
||||
/* ---- Batch Badge (단일/다중품목) ---- */
|
||||
const [batchBadge, setBatchBadge] = useState<{
|
||||
isMulti: boolean;
|
||||
index: number;
|
||||
total: number;
|
||||
itemType: string;
|
||||
} | null>(null);
|
||||
|
||||
/* ---- Batch History ---- */
|
||||
const [history, setHistory] = useState<BatchHistoryItem[]>([]);
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
@@ -453,6 +461,7 @@ export function ProcessWork({ processId }: ProcessWorkProps) {
|
||||
}
|
||||
|
||||
// batch_id가 있으면 해당 품목의 이름을 조회 (다중 품목 지원)
|
||||
let batchItemType = "";
|
||||
if (procData.batch_id) {
|
||||
try {
|
||||
const batchItemRes = await dataApi.getTableData("item_info", {
|
||||
@@ -463,6 +472,7 @@ export function ProcessWork({ processId }: ProcessWorkProps) {
|
||||
if (batchItem) {
|
||||
itemName = String(batchItem.item_name || procData.batch_id);
|
||||
itemCode = String(batchItem.item_number || procData.batch_id);
|
||||
batchItemType = String(batchItem.type || "");
|
||||
} else {
|
||||
itemName = procData.batch_id;
|
||||
itemCode = procData.batch_id;
|
||||
@@ -472,6 +482,23 @@ export function ProcessWork({ processId }: ProcessWorkProps) {
|
||||
itemCode = procData.batch_id;
|
||||
}
|
||||
}
|
||||
// item_type이 없으면 WI의 item_number로 조회
|
||||
if (!batchItemType && wi.item_number) {
|
||||
try {
|
||||
const wiItemRes = await dataApi.getTableData("item_info", {
|
||||
size: 1,
|
||||
filters: { item_number: String(wi.item_number) },
|
||||
});
|
||||
const wiItem = wiItemRes.data?.[0] as Record<string, unknown> | undefined;
|
||||
if (wiItem) {
|
||||
batchItemType = String(wiItem.type || "");
|
||||
}
|
||||
} catch {
|
||||
/* non-critical */
|
||||
}
|
||||
}
|
||||
// batchItemType을 임시 저장 (step 6에서 사용)
|
||||
(procData as unknown as Record<string, unknown>)._itemType = batchItemType;
|
||||
|
||||
setWiInfo({
|
||||
work_instruction_no: String(wi.work_instruction_no || ""),
|
||||
@@ -524,22 +551,40 @@ export function ProcessWork({ processId }: ProcessWorkProps) {
|
||||
size: 100,
|
||||
filters: { wo_id: procData.wo_id },
|
||||
});
|
||||
const masters = ((plRes.data ?? []) as ProcessData[])
|
||||
const allSiblings = (plRes.data ?? []) as ProcessData[];
|
||||
const masters = allSiblings
|
||||
.filter((p) => !p.parent_process_id)
|
||||
.sort((a, b) => parseInt(a.seq_no, 10) - parseInt(b.seq_no, 10))
|
||||
.map((p) => ({
|
||||
process_code: p.process_code,
|
||||
process_name: p.process_name,
|
||||
}));
|
||||
.sort((a, b) => parseInt(a.seq_no, 10) - parseInt(b.seq_no, 10));
|
||||
// 중복 제거
|
||||
const seen = new Set<string>();
|
||||
setProcessList(
|
||||
masters.filter((m) => {
|
||||
masters.map((p) => ({
|
||||
process_code: p.process_code,
|
||||
process_name: p.process_name,
|
||||
})).filter((m) => {
|
||||
if (seen.has(m.process_code)) return false;
|
||||
seen.add(m.process_code);
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
// 다중품목 판단: 마스터 공정의 DISTINCT batch_id
|
||||
const uniqueBatches: string[] = [];
|
||||
for (const p of masters) {
|
||||
const bid = p.batch_id || "";
|
||||
if (bid && !uniqueBatches.includes(bid)) {
|
||||
uniqueBatches.push(bid);
|
||||
}
|
||||
}
|
||||
const currentBid = procData?.batch_id || "";
|
||||
const isMultiBatch = uniqueBatches.length > 1;
|
||||
const bIdx = currentBid ? uniqueBatches.indexOf(currentBid) + 1 : 1;
|
||||
const fetchedItemType = String((procData as unknown as Record<string, unknown>)?._itemType || "");
|
||||
setBatchBadge({
|
||||
isMulti: isMultiBatch,
|
||||
index: Math.max(bIdx, 1),
|
||||
total: Math.max(uniqueBatches.length, 1),
|
||||
itemType: fetchedItemType,
|
||||
});
|
||||
} catch {
|
||||
setProcessList([]);
|
||||
}
|
||||
@@ -1113,6 +1158,19 @@ export function ProcessWork({ processId }: ProcessWorkProps) {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{batchBadge && (
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
{batchBadge.isMulti ? (
|
||||
<span className="bg-blue-100 text-blue-700 text-xs font-bold px-2 py-0.5 rounded-full">
|
||||
다중 {batchBadge.index}/{batchBadge.total}{batchBadge.itemType ? ` · ${batchBadge.itemType}` : ""}
|
||||
</span>
|
||||
) : (
|
||||
<span className="bg-gray-100 text-gray-600 text-xs font-bold px-2 py-0.5 rounded-full">
|
||||
단일{batchBadge.itemType ? ` · ${batchBadge.itemType}` : ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<span className="text-white/40 text-sm">공정</span>
|
||||
<span className="text-white font-medium text-base">
|
||||
|
||||
@@ -225,6 +225,7 @@ function FullscreenWorkModal({
|
||||
myProcesses,
|
||||
instructionMap,
|
||||
itemNameMap,
|
||||
multiBatchInfo,
|
||||
onSwitch,
|
||||
onClose,
|
||||
}: {
|
||||
@@ -232,6 +233,7 @@ function FullscreenWorkModal({
|
||||
myProcesses: WorkOrderProcess[];
|
||||
instructionMap: Record<string, WorkInstruction>;
|
||||
itemNameMap: Record<string, string>;
|
||||
multiBatchInfo: Record<string, { isMulti: boolean; index: number; total: number; itemType: string }>;
|
||||
onSwitch: (id: string) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
@@ -305,6 +307,20 @@ function FullscreenWorkModal({
|
||||
<div className="text-base font-bold text-gray-900 mb-1">
|
||||
{wi?.work_instruction_no || "작업지시"}
|
||||
</div>
|
||||
{(() => {
|
||||
const bInfo = multiBatchInfo[proc.id];
|
||||
if (!bInfo) return null;
|
||||
const typeLabel = bInfo.itemType || "";
|
||||
return bInfo.isMulti ? (
|
||||
<span className="bg-blue-100 text-blue-700 text-xs font-bold px-2 py-0.5 rounded-full self-start mb-0.5">
|
||||
다중 {bInfo.index}/{bInfo.total}{typeLabel ? ` · ${typeLabel}` : ""}
|
||||
</span>
|
||||
) : (
|
||||
<span className="bg-gray-100 text-gray-600 text-xs font-bold px-2 py-0.5 rounded-full self-start mb-0.5">
|
||||
단일{typeLabel ? ` · ${typeLabel}` : ""}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
<AutoScrollText className="text-sm text-gray-500 mb-0.5">
|
||||
📦 {proc.batch_id
|
||||
? `${itemNameMap[proc.batch_id] || proc.batch_id}(${proc.batch_id})`
|
||||
@@ -971,6 +987,7 @@ export function WorkOrderList() {
|
||||
const [processList, setProcessList] = useState<ProcessMng[]>([]);
|
||||
const [equipmentList, setEquipmentList] = useState<EquipmentMng[]>([]);
|
||||
const [itemNameMap, setItemNameMap] = useState<Record<string, string>>({});
|
||||
const [itemTypeMap, setItemTypeMap] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<TabFilter>("acceptable");
|
||||
@@ -1056,14 +1073,19 @@ export function WorkOrderList() {
|
||||
const seen = new Set<string>();
|
||||
const wiData: WorkInstruction[] = [];
|
||||
const newItemNameMap: Record<string, string> = {};
|
||||
const newItemTypeMap: Record<string, string> = {};
|
||||
for (const raw of wiRaw) {
|
||||
const wiId = String(raw.wi_id || raw.id || "");
|
||||
// item_number → item_name 매핑 (모든 행에서 수집)
|
||||
// item_number → item_name / item_type 매핑 (모든 행에서 수집)
|
||||
const rawItemNumber = String(raw.item_number || "");
|
||||
const rawItemName = String(raw.item_name || "");
|
||||
const rawItemType = String(raw.item_type || "");
|
||||
if (rawItemNumber && rawItemName) {
|
||||
newItemNameMap[rawItemNumber] = rawItemName;
|
||||
}
|
||||
if (rawItemNumber && rawItemType) {
|
||||
newItemTypeMap[rawItemNumber] = rawItemType;
|
||||
}
|
||||
if (!wiId || seen.has(wiId)) continue;
|
||||
seen.add(wiId);
|
||||
wiData.push({
|
||||
@@ -1077,6 +1099,7 @@ export function WorkOrderList() {
|
||||
}
|
||||
setInstructions(wiData);
|
||||
setItemNameMap(newItemNameMap);
|
||||
setItemTypeMap(newItemTypeMap);
|
||||
|
||||
const procRes = await dataApi.getTableData("work_order_process", {
|
||||
size: 1000,
|
||||
@@ -1141,6 +1164,44 @@ export function WorkOrderList() {
|
||||
return map;
|
||||
}, [allProcesses]);
|
||||
|
||||
/** 다중품목 판단: wo_id별 DISTINCT batch_id 집합 + 순번 매핑 */
|
||||
const multiBatchInfo = useMemo(() => {
|
||||
// wo_id → 고유 batch_id 목록 (마스터 행 기준)
|
||||
const woBatches: Record<string, string[]> = {};
|
||||
for (const proc of allProcesses) {
|
||||
if (proc.parent_process_id) continue; // 마스터만
|
||||
if (!proc.wo_id) continue;
|
||||
if (!woBatches[proc.wo_id]) woBatches[proc.wo_id] = [];
|
||||
const bid = proc.batch_id || "";
|
||||
if (bid && !woBatches[proc.wo_id].includes(bid)) {
|
||||
woBatches[proc.wo_id].push(bid);
|
||||
}
|
||||
}
|
||||
// proc.id → { isMulti, index, total, itemType }
|
||||
const info: Record<string, { isMulti: boolean; index: number; total: number; itemType: string }> = {};
|
||||
for (const proc of allProcesses) {
|
||||
if (!proc.wo_id) continue;
|
||||
const batches = woBatches[proc.wo_id] || [];
|
||||
const bid = proc.batch_id || "";
|
||||
const isMulti = batches.length > 1;
|
||||
const index = bid ? batches.indexOf(bid) + 1 : 1;
|
||||
const total = Math.max(batches.length, 1);
|
||||
// item_type: batch_id가 있으면 itemTypeMap에서, 없으면 WI의 item_number로
|
||||
let itemType = "";
|
||||
if (bid) {
|
||||
itemType = itemTypeMap[bid] || "";
|
||||
}
|
||||
if (!itemType) {
|
||||
const wi = instructionMap[proc.wo_id];
|
||||
if (wi?.item_number) {
|
||||
itemType = itemTypeMap[wi.item_number] || "";
|
||||
}
|
||||
}
|
||||
info[proc.id] = { isMulti, index, total, itemType };
|
||||
}
|
||||
return info;
|
||||
}, [allProcesses, itemTypeMap, instructionMap]);
|
||||
|
||||
const masterProcesses = useMemo(() => {
|
||||
// 마스터 행 + 분할 행(진행중/완료/리워크) — 중복 제거
|
||||
const seen = new Set<string>();
|
||||
@@ -1910,6 +1971,26 @@ export function WorkOrderList() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 단일/다중품목 뱃지 */}
|
||||
{(() => {
|
||||
const bInfo = multiBatchInfo[proc.id];
|
||||
if (!bInfo) return null;
|
||||
const typeLabel = bInfo.itemType || "";
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
{bInfo.isMulti ? (
|
||||
<span className="bg-blue-100 text-blue-700 text-xs font-bold px-2 py-0.5 rounded-full">
|
||||
다중 {bInfo.index}/{bInfo.total}{typeLabel ? ` · ${typeLabel}` : ""}
|
||||
</span>
|
||||
) : (
|
||||
<span className="bg-gray-100 text-gray-600 text-xs font-bold px-2 py-0.5 rounded-full">
|
||||
단일{typeLabel ? ` · ${typeLabel}` : ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Sub-info: item name + equipment */}
|
||||
<AutoScrollText className="text-sm text-gray-500 mb-3">
|
||||
📦 {proc.batch_id
|
||||
@@ -2107,6 +2188,7 @@ export function WorkOrderList() {
|
||||
)}
|
||||
instructionMap={instructionMap}
|
||||
itemNameMap={itemNameMap}
|
||||
multiBatchInfo={multiBatchInfo}
|
||||
onSwitch={(id) => setWorkModalProcessId(id)}
|
||||
onClose={() => {
|
||||
setWorkModalProcessId(null);
|
||||
|
||||
Reference in New Issue
Block a user