From 7dc1fbf323af351ab5524e455bae11b083b74777 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 25 Mar 2026 11:35:19 +0800 Subject: [PATCH] update --- src/app/api/bom/client.ts | 42 +- .../ImportBom/ImportBomDetailTab.tsx | 387 +++++++++++++----- .../PickOrderSearch/AssignAndRelease.tsx | 3 +- .../ApproverStockTakeAll.tsx | 27 +- 4 files changed, 329 insertions(+), 130 deletions(-) diff --git a/src/app/api/bom/client.ts b/src/app/api/bom/client.ts index 2d17ffc..722030b 100644 --- a/src/app/api/bom/client.ts +++ b/src/app/api/bom/client.ts @@ -115,4 +115,44 @@ export async function fetchBomComboClient(): Promise { { params: { batchId } } ); return response.data; - } \ No newline at end of file + } + +/** Master `equipment` rows for BOM process editor (description/name → code). */ +export type EquipmentMasterRow = { + code: string; + name: string; + description: string; +}; + +/** Master `process` rows for BOM process editor (dropdown by code). */ +export type ProcessMasterRow = { + code: string; + name: string; +}; + +export async function fetchAllEquipmentsMasterClient(): Promise< + EquipmentMasterRow[] +> { + const response = await axiosInstance.get( + `${NEXT_PUBLIC_API_URL}/Equipment`, + ); + const rows = Array.isArray(response.data) ? response.data : []; + return rows.map((r: any) => ({ + code: String(r?.code ?? "").trim(), + name: String(r?.name ?? "").trim(), + description: String(r?.description ?? "").trim(), + })); +} + +export async function fetchAllProcessesMasterClient(): Promise< + ProcessMasterRow[] +> { + const response = await axiosInstance.get( + `${NEXT_PUBLIC_API_URL}/Process`, + ); + const rows = Array.isArray(response.data) ? response.data : []; + return rows.map((r: any) => ({ + code: String(r?.code ?? "").trim(), + name: String(r?.name ?? "").trim(), + })); +} \ No newline at end of file diff --git a/src/components/ImportBom/ImportBomDetailTab.tsx b/src/components/ImportBom/ImportBomDetailTab.tsx index f4754ac..227a980 100644 --- a/src/components/ImportBom/ImportBomDetailTab.tsx +++ b/src/components/ImportBom/ImportBomDetailTab.tsx @@ -27,6 +27,10 @@ import { editBomClient, fetchBomComboClient, fetchBomDetailClient, + fetchAllEquipmentsMasterClient, + fetchAllProcessesMasterClient, + type EquipmentMasterRow, + type ProcessMasterRow, } from "@/app/api/bom/client"; import type { SelectChangeEvent } from "@mui/material/Select"; import { useTranslation } from "react-i18next"; @@ -37,6 +41,26 @@ import SaveIcon from "@mui/icons-material/Save"; import CancelIcon from "@mui/icons-material/Cancel"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; + +/** 以 description + "-" + name 對應 code,或同一筆設備的 description+name。 */ +function resolveEquipmentCode( + list: EquipmentMasterRow[], + description: string, + name: string, +): string | null { + const d = description.trim(); + const n = name.trim(); + if (!d && !n) return null; + if (!d || !n) return null; + const composite = `${d}-${n}`; + const byCode = list.find((e) => e.code === composite); + if (byCode) return byCode.code; + const byPair = list.find( + (e) => e.description === d && e.name === n, + ); + return byPair?.code ?? null; +} + const ImportBomDetailTab: React.FC = () => { const { t } = useTranslation( "common" ); const [bomList, setBomList] = useState([]); @@ -69,7 +93,9 @@ const ImportBomDetailTab: React.FC = () => { processCode?: string; processName?: string; description: string; - equipmentCode?: string; + /** 設備主檔 description(下拉),與 equipmentName 一併解析為 equipment.code */ + equipmentDescription: string; + equipmentName: string; durationInMinute: number; prepTimeInMinute: number; postProdTimeInMinute: number; @@ -96,17 +122,27 @@ const ImportBomDetailTab: React.FC = () => { const [editMaterials, setEditMaterials] = useState([]); const [editProcesses, setEditProcesses] = useState([]); - // Process add form (uses dropdown selections). + const [equipmentMasterList, setEquipmentMasterList] = useState< + EquipmentMasterRow[] + >([]); + const [processMasterList, setProcessMasterList] = useState< + ProcessMasterRow[] + >([]); + const [editMasterLoading, setEditMasterLoading] = useState(false); + + // Process add form (uses dropdown selections from master tables). const [processAddForm, setProcessAddForm] = useState<{ processCode: string; - equipmentCode: string; + equipmentDescription: string; + equipmentName: string; description: string; durationInMinute: number; prepTimeInMinute: number; postProdTimeInMinute: number; }>({ processCode: "", - equipmentCode: "", + equipmentDescription: "", + equipmentName: "", description: "", durationInMinute: 0, prepTimeInMinute: 0, @@ -115,19 +151,27 @@ const ImportBomDetailTab: React.FC = () => { const processCodeOptions = useMemo(() => { const codes = new Set(); - (detail?.processes ?? []).forEach((p) => { - if (p.processCode) codes.add(p.processCode); + processMasterList.forEach((p) => { + if (p.code) codes.add(p.code); }); - return Array.from(codes); - }, [detail]); + return Array.from(codes).sort(); + }, [processMasterList]); - const equipmentCodeOptions = useMemo(() => { - const codes = new Set(); - (detail?.processes ?? []).forEach((p) => { - if (p.equipmentCode) codes.add(p.equipmentCode); + const equipmentDescriptionOptions = useMemo(() => { + const s = new Set(); + equipmentMasterList.forEach((e) => { + if (e.description) s.add(e.description); }); - return Array.from(codes); - }, [detail]); + return Array.from(s).sort(); + }, [equipmentMasterList]); + + const equipmentNameOptions = useMemo(() => { + const s = new Set(); + equipmentMasterList.forEach((e) => { + if (e.name) s.add(e.name); + }); + return Array.from(s).sort(); + }, [equipmentMasterList]); useEffect(() => { const loadList = async () => { @@ -242,57 +286,82 @@ const ImportBomDetailTab: React.FC = () => { const genKey = () => Math.random().toString(36).slice(2); - const startEdit = useCallback(() => { + const startEdit = useCallback(async () => { if (!detail) return; setEditError(null); - setEditBasic({ - description: detail.description ?? "", - outputQty: detail.outputQty ?? 0, - outputQtyUom: detail.outputQtyUom ?? "", + setEditMasterLoading(true); + try { + const [equipments, processes] = await Promise.all([ + fetchAllEquipmentsMasterClient(), + fetchAllProcessesMasterClient(), + ]); + setEquipmentMasterList(equipments); + setProcessMasterList(processes); - isDark: detail.isDark ?? 0, - isFloat: detail.isFloat ?? 0, - isDense: detail.isDense ?? 0, - scrapRate: detail.scrapRate ?? 0, - allergicSubstances: detail.allergicSubstances ?? 0, - timeSequence: detail.timeSequence ?? 0, - complexity: detail.complexity ?? 0, - isDrink: detail.isDrink ?? false, - }); + setEditBasic({ + description: detail.description ?? "", + outputQty: detail.outputQty ?? 0, + outputQtyUom: detail.outputQtyUom ?? "", - setEditMaterials( - (detail.materials ?? []).map((m) => ({ - key: genKey(), - id: undefined, - itemCode: m.itemCode ?? "", - itemName: m.itemName ?? "", - qty: m.baseQty ?? 0, - isConsumable: m.isConsumable ?? false, - baseUom: m.baseUom, - stockQty: m.stockQty, - stockUom: m.stockUom, - salesQty: m.salesQty, - salesUom: m.salesUom, - })), - ); + isDark: detail.isDark ?? 0, + isFloat: detail.isFloat ?? 0, + isDense: detail.isDense ?? 0, + scrapRate: detail.scrapRate ?? 0, + allergicSubstances: detail.allergicSubstances ?? 0, + timeSequence: detail.timeSequence ?? 0, + complexity: detail.complexity ?? 0, + isDrink: detail.isDrink ?? false, + }); - setEditProcesses( - (detail.processes ?? []).map((p) => ({ - key: genKey(), - id: undefined, - seqNo: p.seqNo, - processCode: p.processCode ?? "", - processName: p.processName, - description: p.processDescription ?? "", - equipmentCode: p.equipmentCode ?? p.equipmentName ?? "", - durationInMinute: p.durationInMinute ?? 0, - prepTimeInMinute: p.prepTimeInMinute ?? 0, - postProdTimeInMinute: p.postProdTimeInMinute ?? 0, - })), - ); + setEditMaterials( + (detail.materials ?? []).map((m) => ({ + key: genKey(), + id: undefined, + itemCode: m.itemCode ?? "", + itemName: m.itemName ?? "", + qty: m.baseQty ?? 0, + isConsumable: m.isConsumable ?? false, + baseUom: m.baseUom, + stockQty: m.stockQty, + stockUom: m.stockUom, + salesQty: m.salesQty, + salesUom: m.salesUom, + })), + ); + + setEditProcesses( + (detail.processes ?? []).map((p) => { + const code = (p.equipmentCode ?? "").trim(); + const eq = code + ? equipments.find((e) => e.code === code) + : undefined; + return { + key: genKey(), + id: undefined, + seqNo: p.seqNo, + processCode: p.processCode ?? "", + processName: p.processName, + description: p.processDescription ?? "", + equipmentDescription: eq?.description ?? "", + equipmentName: eq?.name ?? "", + durationInMinute: p.durationInMinute ?? 0, + prepTimeInMinute: p.prepTimeInMinute ?? 0, + postProdTimeInMinute: p.postProdTimeInMinute ?? 0, + }; + }), + ); - setIsEditing(true); + setIsEditing(true); + } catch (e: unknown) { + const msg = + e && typeof e === "object" && "message" in e + ? String((e as { message?: string }).message) + : "載入製程/設備主檔失敗"; + setEditError(msg); + } finally { + setEditMasterLoading(false); + } }, [detail]); const cancelEdit = useCallback(() => { @@ -304,12 +373,15 @@ const ImportBomDetailTab: React.FC = () => { setEditProcesses([]); setProcessAddForm({ processCode: "", - equipmentCode: "", + equipmentDescription: "", + equipmentName: "", description: "", durationInMinute: 0, prepTimeInMinute: 0, postProdTimeInMinute: 0, }); + setEquipmentMasterList([]); + setProcessMasterList([]); }, []); const addMaterialRow = useCallback(() => { @@ -339,7 +411,8 @@ const ImportBomDetailTab: React.FC = () => { processCode: "", processName: "", description: "", - equipmentCode: "", + equipmentDescription: "", + equipmentName: "", durationInMinute: 0, prepTimeInMinute: 0, postProdTimeInMinute: 0, @@ -354,6 +427,22 @@ const ImportBomDetailTab: React.FC = () => { return; } + const ed = processAddForm.equipmentDescription.trim(); + const en = processAddForm.equipmentName.trim(); + if ((ed && !en) || (!ed && en)) { + setEditError("設備描述與名稱需同時選取,或同時留空(不適用)"); + return; + } + if (ed && en) { + const resolved = resolveEquipmentCode(equipmentMasterList, ed, en); + if (!resolved) { + setEditError( + `設備組合「${ed}-${en}」在主檔中找不到對應設備代碼,請確認後再試`, + ); + return; + } + } + setEditProcesses((prev) => [ ...prev, { @@ -362,7 +451,8 @@ const ImportBomDetailTab: React.FC = () => { processCode: pCode, processName: "", description: processAddForm.description ?? "", - equipmentCode: processAddForm.equipmentCode.trim(), + equipmentDescription: ed, + equipmentName: en, durationInMinute: processAddForm.durationInMinute ?? 0, prepTimeInMinute: processAddForm.prepTimeInMinute ?? 0, postProdTimeInMinute: processAddForm.postProdTimeInMinute ?? 0, @@ -371,14 +461,15 @@ const ImportBomDetailTab: React.FC = () => { setProcessAddForm({ processCode: "", - equipmentCode: "", + equipmentDescription: "", + equipmentName: "", description: "", durationInMinute: 0, prepTimeInMinute: 0, postProdTimeInMinute: 0, }); setEditError(null); - }, [processAddForm]); + }, [processAddForm, equipmentMasterList]); const deleteMaterialRow = useCallback((key: string) => { setEditMaterials((prev) => prev.filter((r) => r.key !== key)); @@ -398,6 +489,19 @@ const ImportBomDetailTab: React.FC = () => { if (!p.processCode?.trim()) { throw new Error("工序行 Process Code 不能为空"); } + const ed = p.equipmentDescription.trim(); + const en = p.equipmentName.trim(); + if ((ed && !en) || (!ed && en)) { + throw new Error("各製程行的設備描述與名稱需同時填寫或同時留空"); + } + if (ed && en) { + const resolved = resolveEquipmentCode(equipmentMasterList, ed, en); + if (!resolved) { + throw new Error( + `設備「${ed}-${en}」在主檔中無對應設備代碼,請修正後再儲存`, + ); + } + } } const payload: any = { @@ -413,16 +517,24 @@ const ImportBomDetailTab: React.FC = () => { timeSequence: editBasic.timeSequence, complexity: editBasic.complexity, isDrink: editBasic.isDrink, - processes: editProcesses.map((p) => ({ - id: p.id, - seqNo: p.seqNo, - processCode: p.processCode?.trim() || undefined, - equipmentCode: p.equipmentCode?.trim() || undefined, - description: p.description || undefined, - durationInMinute: p.durationInMinute, - prepTimeInMinute: p.prepTimeInMinute, - postProdTimeInMinute: p.postProdTimeInMinute, - })), + processes: editProcesses.map((p) => { + const ed = p.equipmentDescription.trim(); + const en = p.equipmentName.trim(); + const equipmentCode = + ed && en + ? resolveEquipmentCode(equipmentMasterList, ed, en) ?? undefined + : undefined; + return { + id: p.id, + seqNo: p.seqNo, + processCode: p.processCode?.trim() || undefined, + equipmentCode, + description: p.description || undefined, + durationInMinute: p.durationInMinute, + prepTimeInMinute: p.prepTimeInMinute, + postProdTimeInMinute: p.postProdTimeInMinute, + }; + }), }; const updated = await editBomClient(detail.id, payload); @@ -433,7 +545,7 @@ const ImportBomDetailTab: React.FC = () => { } finally { setEditLoading(false); } - }, [detail, editBasic, editProcesses]); + }, [detail, editBasic, editProcesses, equipmentMasterList]); return ( @@ -480,11 +592,18 @@ const ImportBomDetailTab: React.FC = () => { {!isEditing ? ( ) : ( @@ -770,6 +889,9 @@ const ImportBomDetailTab: React.FC = () => { })) } > + + 請選擇 + {processCodeOptions.map((c) => ( {c} @@ -779,19 +901,40 @@ const ImportBomDetailTab: React.FC = () => { - {t("Equipment Code")} + 設備說明 + + + + + 設備名稱 - setEditProcesses((prev) => - prev.map((x) => - x.key === p.key - ? { - ...x, - equipmentCode: String(e.target.value), - } - : x, - ), - ) - } - > - 不適用 - {equipmentCodeOptions.map((c) => ( - - {c} - - ))} - - + + + + + + + + = ({ filterArgs }) => { {/* Target Date - 只在第一个项目显示 */} {index === 0 ? ( - arrayToDayjs(item.targetDate) - .add(-1, "month") + arrayToDayjs(item.targetDate) .format(OUTPUT_DATE_FORMAT) ) : null} diff --git a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx index e7e7588..8b59227 100644 --- a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx +++ b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx @@ -181,6 +181,10 @@ const ApproverStockTakeAll: React.FC = ({ (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0 ? "second" : "first"); + // 避免 Approver 手动输入过程中被 variance 过滤掉,导致“输入后行消失无法提交” + if (selection === "approver") { + return true; + } const difference = calculateDifference(detail, selection); const bookQty = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0); @@ -230,26 +234,9 @@ const ApproverStockTakeAll: React.FC = ({ finalQty = detail.secondStockTakeQty; finalBadQty = detail.secondBadQty || 0; } else { - const approverQtyValue = approverQty[detail.id]; - const approverBadQtyValue = approverBadQty[detail.id]; - - if ( - approverQtyValue === undefined || - approverQtyValue === null || - approverQtyValue === "" - ) { - onSnackbar(t("Please enter Approver QTY"), "error"); - return; - } - if ( - approverBadQtyValue === undefined || - approverBadQtyValue === null || - approverBadQtyValue === "" - ) { - onSnackbar(t("Please enter Approver Bad QTY"), "error"); - return; - } - + // 与 Picker 逻辑一致:Approver 输入为空时按 0 处理 + const approverQtyValue = approverQty[detail.id] || "0"; + const approverBadQtyValue = approverBadQty[detail.id] || "0"; finalQty = parseFloat(approverQtyValue) || 0; finalBadQty = parseFloat(approverBadQtyValue) || 0; }