From 270763a2ae6c8890e78ce05346a787e217477188 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Sat, 11 Apr 2026 17:22:29 +0800 Subject: [PATCH] do switch lot update V1 --- .../GoodPickExecutiondetail.tsx | 228 +++++++++++------- .../LotConfirmationModal.tsx | 114 +++++---- src/i18n/zh/pickOrder.json | 12 + 3 files changed, 218 insertions(+), 136 deletions(-) diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx index f1363cb..1cdd5f5 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx @@ -75,11 +75,27 @@ import GoodPickExecutionForm from "./GoodPickExecutionForm"; import FGPickOrderCard from "./FGPickOrderCard"; import LinearProgressWithLabel from "../common/LinearProgressWithLabel"; import ScanStatusAlert from "../common/ScanStatusAlert"; +import { translateLotSubstitutionFailure } from "./lotSubstitutionMessage"; interface Props { filterArgs: Record; onSwitchToRecordTab?: () => void; onRefreshReleasedOrderCount?: () => void; } +type LotConfirmRunContext = { + expectedLotData: { + lotNo: string | null; + itemCode?: string; + itemName?: string; + }; + scannedLotData: { + lotNo: string | null; + itemCode?: string; + itemName?: string; + stockInLineId: number; // 必須有,API 用 + inventoryLotLineId?: number | null; + }; + selectedLotForQr: any; // 與現在一樣:含 pickOrderLineId, stockOutLineId, suggestedPickLotId, itemId… +}; /** 同物料多行时,优先对「有建议批次号」的行做替换,避免误选「无批次/不足」行 */ function pickExpectedLotForSubstitution(activeSuggestedLots: any[]): any | null { @@ -115,12 +131,12 @@ const QrCodeModal: React.FC<{ const [isProcessingQr, setIsProcessingQr] = useState(false); const [qrScanFailed, setQrScanFailed] = useState(false); const [qrScanSuccess, setQrScanSuccess] = useState(false); - + const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [scannedQrResult, setScannedQrResult] = useState(''); const [fgPickOrder, setFgPickOrder] = useState(null); const fetchingRef = useRef>(new Set()); - // Process scanned QR codes + useEffect(() => { // ✅ Don't process if modal is not open if (!open) { @@ -619,6 +635,10 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); const [selectedLotForQr, setSelectedLotForQr] = useState(null); const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); const [lotConfirmationError, setLotConfirmationError] = useState(null); + /** QR 静默换批失败时显示在对应行的 Lot# 列,key = stockOutLineId */ + const [lotSwitchFailByStockOutLineId, setLotSwitchFailByStockOutLineId] = useState< + Record + >({}); const [expectedLotData, setExpectedLotData] = useState(null); const [scannedLotData, setScannedLotData] = useState(null); const [isConfirmingLot, setIsConfirmingLot] = useState(false); @@ -626,7 +646,6 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState(null); const [fgPickOrders, setFgPickOrders] = useState([]); - const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false); // Add these missing state variables after line 352 const [isManualScanning, setIsManualScanning] = useState(false); @@ -656,9 +675,10 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); const lotConfirmLastQrRef = useRef(''); const lotConfirmSkipNextScanRef = useRef(false); const lotConfirmOpenedAtRef = useRef(0); - - - + const handleLotConfirmationRef = useRef< + ((overrideScannedLot?: any, runContext?: LotConfirmRunContext) => Promise) | null + >(null); + // Handle QR code button click const handleQrCodeClick = (pickOrderId: number) => { console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); @@ -733,33 +753,69 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); } }, []); - const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any, qrScanCountAtOpen?: number) => { + const handleLotMismatch = useCallback((fullExpectedLotRow: any, scannedLot: any, qrScanCountAtOpen?: number) => { const mismatchStartTime = performance.now(); console.log(` [HANDLE LOT MISMATCH START]`); console.log(` Start time: ${new Date().toISOString()}`); - console.log("Lot mismatch detected:", { expectedLot, scannedLot }); + console.log("Lot mismatch detected:", { fullExpectedLotRow, scannedLot }); lotConfirmOpenedQrCountRef.current = typeof qrScanCountAtOpen === "number" ? qrScanCountAtOpen : 1; - // ✅ Use setTimeout to avoid flushSync warning - schedule modal update in next tick + // ✅ Use setTimeout to avoid flushSync warning - schedule state + silent substitution in next tick const setTimeoutStartTime = performance.now(); - console.time('setLotConfirmationOpen'); + console.time('setLotMismatchStateAndSubstitute'); setTimeout(() => { const setStateStartTime = performance.now(); - setExpectedLotData(expectedLot); - setScannedLotData({ + const expectedForDisplay = { + lotNo: fullExpectedLotRow.lotNo, + itemCode: fullExpectedLotRow.itemCode, + itemName: fullExpectedLotRow.itemName, + }; + const scannedMerged = { ...scannedLot, lotNo: scannedLot.lotNo || null, - }); - // The QR that opened modal must NOT be treated as confirmation rescan. + }; + setExpectedLotData(expectedForDisplay); + setScannedLotData(scannedMerged); + setSelectedLotForQr(fullExpectedLotRow); + // The QR that triggered mismatch must NOT be treated as confirmation rescan. lotConfirmSkipNextScanRef.current = true; lotConfirmOpenedAtRef.current = Date.now(); - setLotConfirmationOpen(true); + + const sid = Number(scannedLot.stockInLineId); + if (!Number.isFinite(sid)) { + console.error(` [HANDLE LOT MISMATCH] Invalid stockInLineId for substitution: ${scannedLot.stockInLineId}`); + const errMsg = t("Lot switch failed; pick line was not marked as checked."); + const rowSol = Number(fullExpectedLotRow.stockOutLineId); + if (Number.isFinite(rowSol)) { + setLotSwitchFailByStockOutLineId((prev) => ({ ...prev, [rowSol]: errMsg })); + } + setQrScanError(true); + setQrScanSuccess(false); + setQrScanErrorMsg(errMsg); + const setStateTime = performance.now() - setStateStartTime; + console.timeEnd('setLotMismatchStateAndSubstitute'); + console.log(` [HANDLE LOT MISMATCH] Lot switch failed (invalid stockInLineId), setState time: ${setStateTime.toFixed(2)}ms`); + return; + } + + const runContext: LotConfirmRunContext = { + expectedLotData: expectedForDisplay, + scannedLotData: { + ...scannedMerged, + stockInLineId: sid, + itemCode: scannedMerged.itemCode ?? fullExpectedLotRow.itemCode, + itemName: scannedMerged.itemName ?? fullExpectedLotRow.itemName, + inventoryLotLineId: scannedLot.inventoryLotLineId ?? scannedLot.lotId ?? null, + }, + selectedLotForQr: fullExpectedLotRow, + }; + void handleLotConfirmationRef.current?.(undefined, runContext); + const setStateTime = performance.now() - setStateStartTime; - console.timeEnd('setLotConfirmationOpen'); - console.log(` [HANDLE LOT MISMATCH] Modal state set to open (setState time: ${setStateTime.toFixed(2)}ms)`); - console.log(`✅ [HANDLE LOT MISMATCH] Modal state set to open`); + console.timeEnd('setLotMismatchStateAndSubstitute'); + console.log(` [HANDLE LOT MISMATCH] Silent lot substitution scheduled (setState time: ${setStateTime.toFixed(2)}ms)`); }, 0); const setTimeoutTime = performance.now() - setTimeoutStartTime; console.log(` [PERF] setTimeout scheduling time: ${setTimeoutTime.toFixed(2)}ms`); @@ -802,7 +858,7 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); const totalTime = performance.now() - mismatchStartTime; console.log(` [HANDLE LOT MISMATCH END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); } - }, [fetchStockInLineInfoCached]); + }, [fetchStockInLineInfoCached, t]); const checkAllLotsCompleted = useCallback((lotData: any[]) => { if (lotData.length === 0) { setAllLotsCompleted(false); @@ -1314,74 +1370,75 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO } }, []); - const handleLotConfirmation = useCallback(async (overrideScannedLot?: any) => { - const effectiveScannedLot = overrideScannedLot ?? scannedLotData; - if (!expectedLotData || !effectiveScannedLot || !selectedLotForQr) return; + const handleLotConfirmation = useCallback(async (overrideScannedLot?: any, runContext?: LotConfirmRunContext) => { + const exp = runContext?.expectedLotData ?? expectedLotData; + const scan = overrideScannedLot ?? runContext?.scannedLotData ?? scannedLotData; + const sel = runContext?.selectedLotForQr ?? selectedLotForQr; + if (!exp || !scan || !sel) return; + + const newStockInLineId = scan?.stockInLineId; + if (newStockInLineId == null || Number.isNaN(Number(newStockInLineId))) return; + + const rowSolKey = Number(sel.stockOutLineId); + if (Number.isFinite(rowSolKey)) { + setLotSwitchFailByStockOutLineId((prev) => { + const next = { ...prev }; + delete next[rowSolKey]; + return next; + }); + } + setIsConfirmingLot(true); setLotConfirmationError(null); try { - const newLotNo = effectiveScannedLot?.lotNo; - - const newStockInLineId = effectiveScannedLot?.stockInLineId; - const substitutionResult = await confirmLotSubstitution({ - pickOrderLineId: selectedLotForQr.pickOrderLineId, - stockOutLineId: selectedLotForQr.stockOutLineId, - originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId, - newInventoryLotNo: "", - newStockInLineId: newStockInLineId + pickOrderLineId: sel.pickOrderLineId, + stockOutLineId: sel.stockOutLineId, + originalSuggestedPickLotId: sel.suggestedPickLotId, + newInventoryLotNo: "", + newStockInLineId: newStockInLineId, }); const substitutionCode = substitutionResult?.code; const switchedToUnavailable = substitutionCode === "SUCCESS_UNAVAILABLE" || substitutionCode === "BOUND_UNAVAILABLE"; if (!substitutionResult || (substitutionCode !== "SUCCESS" && !switchedToUnavailable)) { - const errMsg = - substitutionResult?.code === "LOT_UNAVAILABLE" - ? t( - "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated." - ) - : substitutionResult?.message || - t("Lot switch failed; pick line was not marked as checked."); - setLotConfirmationError(errMsg); + const errMsg = translateLotSubstitutionFailure(t, substitutionResult); + if (Number.isFinite(rowSolKey)) { + setLotSwitchFailByStockOutLineId((prev) => ({ ...prev, [rowSolKey]: errMsg })); + } setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg(errMsg); return; } - + setQrScanError(false); setQrScanSuccess(false); - setQrScanInput(''); - - // ✅ 修复:在确认后重置扫描状态,避免重复处理 + setQrScanInput(""); + resetScan(); - - // ✅ 修复:不要清空 processedQrCodes,而是保留当前 QR code 的标记 - // 或者如果确实需要清空,应该在重置扫描后再清空 - // setProcessedQrCodes(new Set()); - // setLastProcessedQr(''); - + setPickExecutionFormOpen(false); - if (selectedLotForQr?.stockOutLineId && !switchedToUnavailable) { - const stockOutLineUpdate = await updateStockOutLineStatus({ - id: selectedLotForQr.stockOutLineId, - status: 'checked', - qty: 0 + if (sel?.stockOutLineId && !switchedToUnavailable) { + await updateStockOutLineStatus({ + id: sel.stockOutLineId, + status: "checked", + qty: 0, }); } - - // ✅ 修复:先关闭 modal 和清空状态,再刷新数据 + clearLotConfirmationState(false); - - // ✅ 修复:刷新数据前设置刷新标志,避免在刷新期间处理新的 QR code + setIsRefreshingData(true); await fetchAllCombinedLotData(); setIsRefreshingData(false); } catch (error) { console.error("Error confirming lot substitution:", error); const errMsg = t("Lot confirmation failed. Please try again."); - setLotConfirmationError(errMsg); + if (Number.isFinite(rowSolKey)) { + setLotSwitchFailByStockOutLineId((prev) => ({ ...prev, [rowSolKey]: errMsg })); + } setQrScanError(true); setQrScanErrorMsg(errMsg); } finally { @@ -1389,6 +1446,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO } }, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData, resetScan, clearLotConfirmationState, t]); + useEffect(() => { + handleLotConfirmationRef.current = handleLotConfirmation; + }, [handleLotConfirmation]); + const handleLotConfirmationByRescan = useCallback(async (rawQr: string): Promise => { if (!lotConfirmationOpen || !selectedLotForQr || !expectedLotData || !scannedLotData) { return false; @@ -1923,22 +1984,17 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO ) || allLotsForItem[0]; - // ✅ Always open confirmation modal when no active lots (user needs to confirm switching) - // handleLotMismatch will fetch lotNo from backend using stockInLineId if needed - console.log(`⚠️ [QR PROCESS] Opening confirmation modal for lot switch (no active lots)`); + // Silent lot substitution; modal only if switch fails + console.log(`⚠️ [QR PROCESS] Lot switch (no active lots), attempting substitution`); setSelectedLotForQr(expectedLot); handleLotMismatch( + expectedLot, { - lotNo: expectedLot.lotNo, - itemCode: expectedLot.itemCode, - itemName: expectedLot.itemName - }, - { - lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null + lotNo: scannedLot?.lotNo || null, itemCode: expectedLot.itemCode, itemName: expectedLot.itemName, inventoryLotLineId: scannedLot?.lotId || null, - stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo + stockInLineId: scannedStockInLineId, }, qrScanCountAtInvoke ); @@ -1971,20 +2027,16 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO // Check if scanned lot is different from expected, or if scannedLot is undefined (not in allLotsForItem) const shouldOpenModal = !scannedLot || (scannedLot.stockInLineId !== expectedLot.stockInLineId); if (shouldOpenModal) { - console.log(`⚠️ [QR PROCESS] Opening confirmation modal (scanned lot ${scannedLot?.lotNo || 'not in data'} is not in active suggested lots)`); + console.log(`⚠️ [QR PROCESS] Lot switch (scanned lot ${scannedLot?.lotNo || 'not in data'} not in active suggested lots)`); setSelectedLotForQr(expectedLot); handleLotMismatch( + expectedLot, { - lotNo: expectedLot.lotNo, - itemCode: expectedLot.itemCode, - itemName: expectedLot.itemName - }, - { - lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null + lotNo: scannedLot?.lotNo || null, itemCode: expectedLot.itemCode, itemName: expectedLot.itemName, inventoryLotLineId: scannedLot?.lotId || null, - stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo + stockInLineId: scannedStockInLineId, }, qrScanCountAtInvoke ); @@ -2130,21 +2182,15 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO const setSelectedLotTime = performance.now() - setSelectedLotStartTime; console.log(` [PERF] Set selected lot time: ${setSelectedLotTime.toFixed(2)}ms`); - // ✅ 获取扫描的 lot 信息(从 QR 数据中提取,或使用默认值) - // Call handleLotMismatch immediately - it will open the modal const handleMismatchStartTime = performance.now(); handleLotMismatch( + expectedLot, { - lotNo: expectedLot.lotNo, - itemCode: expectedLot.itemCode, - itemName: expectedLot.itemName - }, - { - lotNo: null, // 扫描的 lotNo 未知,需要从后端获取或显示为未知 + lotNo: null, itemCode: expectedLot.itemCode, itemName: expectedLot.itemName, inventoryLotLineId: null, - stockInLineId: scannedStockInLineId // ✅ 传递 stockInLineId + stockInLineId: scannedStockInLineId, }, qrScanCountAtInvoke ); @@ -3608,7 +3654,10 @@ const handleSubmitAllScanned = useCallback(async () => { paginatedData.map((lot, index) => { // 检查是否是 issue lot const isIssueLot = lot.stockOutLineStatus === 'rejected' || !lot.lotNo; - + const rowSolId = Number(lot.stockOutLineId); + const lotSwitchErr = + Number.isFinite(rowSolId) ? lotSwitchFailByStockOutLineId[rowSolId] : undefined; + return ( { ) )} + {lotSwitchErr ? ( + + {lotSwitchErr} + + ) : null} diff --git a/src/components/FinishedGoodSearch/LotConfirmationModal.tsx b/src/components/FinishedGoodSearch/LotConfirmationModal.tsx index fee9bc3..f023fc3 100644 --- a/src/components/FinishedGoodSearch/LotConfirmationModal.tsx +++ b/src/components/FinishedGoodSearch/LotConfirmationModal.tsx @@ -19,14 +19,14 @@ interface LotConfirmationModalProps { onClose: () => void; onConfirm: () => void; expectedLot: { - lotNo: string; - itemCode: string; - itemName: string; + lotNo: string | null; + itemCode?: string; + itemName?: string; }; scannedLot: { - lotNo: string; - itemCode: string; - itemName: string; + lotNo: string | null; + itemCode?: string; + itemName?: string; }; isLoading?: boolean; /** Shown inside the dialog when confirm/switch API fails (e.g. LOT_UNAVAILABLE). */ @@ -43,24 +43,25 @@ const LotConfirmationModal: React.FC = ({ errorMessage = null, }) => { const { t } = useTranslation("pickOrder"); + const isFailure = Boolean(errorMessage); return ( + open={open} + onClose={onClose} + maxWidth="md" + fullWidth + disableScrollLock + disableAutoFocus + disableEnforceFocus + disableRestoreFocus + > - - {t("Lot Number Mismatch")} - - - + + {isFailure ? t("Lot switch failed") : t("Lot Number Mismatch")} + + + {errorMessage ? ( @@ -68,23 +69,33 @@ const LotConfirmationModal: React.FC = ({ {t(errorMessage)} ) : null} - - {t("The scanned item matches the expected item, but the lot number is different. Scan again to confirm: scan the expected lot QR to keep the suggested lot, or scan the other lot QR again to switch.")} - + {isFailure ? ( + + {t( + "The system could not switch to the scanned lot. Review the lots below, then tap Confirm to retry." + )} + + ) : ( + + {t( + "The scanned item matches the expected item, but the lot number is different. Scan again to confirm: scan the expected lot QR to keep the suggested lot, or scan the other lot QR again to switch." + )} + + )} {t("Expected Lot:")} - + - {t("Item Code")}: {expectedLot.itemCode} + {t("Item Code")}: {expectedLot.itemCode ?? "—"} - {t("Item Name")}: {expectedLot.itemName} + {t("Item Name")}: {expectedLot.itemName ?? "—"} - {t("Lot No")}: {expectedLot.lotNo} + {t("Lot No")}: {expectedLot.lotNo ?? "—"} @@ -95,42 +106,47 @@ const LotConfirmationModal: React.FC = ({ {t("Scanned Lot:")} - + - {t("Item Code")}: {scannedLot.itemCode} + {t("Item Code")}: {scannedLot.itemCode ?? "—"} - {t("Item Name")}: {scannedLot.itemName} + {t("Item Name")}: {scannedLot.itemName ?? "—"} - {t("Lot No")}: {scannedLot.lotNo} + {t("Lot No")}: {scannedLot.lotNo ?? "—"} - - {t("After you scan to choose, the system will update the pick line to the lot you confirmed.")} - - - {t("Or use the Confirm button below if you cannot scan again (same as scanning the other lot again).")} - + {isFailure ? ( + + {t( + "You can also scan again: expected lot QR keeps the suggested line; scanned lot QR retries the switch." + )} + + ) : ( + <> + + {t( + "After you scan to choose, the system will update the pick line to the lot you confirmed." + )} + + + {t( + "Or use the Confirm button below if you cannot scan again (same as scanning the other lot again)." + )} + + + )} - - @@ -138,4 +154,4 @@ const LotConfirmationModal: React.FC = ({ ); }; -export default LotConfirmationModal; \ No newline at end of file +export default LotConfirmationModal; diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index 4952eda..f3a12f2 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -329,6 +329,9 @@ "If you confirm, the system will:":"如果您確認,系統將:", "After you scan to choose, the system will update the pick line to the lot you confirmed.":"確認後,系統會將您選擇的批次套用到對應提料行。", "Or use the Confirm button below if you cannot scan again (same as scanning the other lot again).":"若無法再掃描,可按下「確認」以切換為剛才掃描到的批次(與再掃一次該批次 QR 相同)。", + "Lot switch failed":"批次切換失敗", + "The system could not switch to the scanned lot. Review the lots below, then tap Confirm to retry.":"系統無法切換至掃描的批次。請核對下方批次後按「確認」重試。", + "You can also scan again: expected lot QR keeps the suggested line; scanned lot QR retries the switch.":"您也可以再掃描:掃描建議批次 QR 可保留該行;掃描欲切換批次 QR 可再次嘗試切換。", "QR code verified.":"QR 碼驗證成功。", "Order Finished":"訂單完成", "Submitted Status":"提交狀態", @@ -465,5 +468,14 @@ "Lot switch failed; pick line was not marked as checked.": "換批失敗;揀貨行未標為已核對。", "Lot confirmation failed. Please try again.": "確認批號失敗,請重試。", "Powder Mixture": "箱料粉", + "This lot is not yet putaway": "此批次尚未上架", + "Cannot resolve new inventory lot line": "無法解析新批號庫存行(請確認已上架且資料正確)。", + "Pick order line item is null": "提料單行未關聯物料。", + "New lot line item does not match pick order line item": "新批號行的物料與提料單行不一致。", + "Pick order line {{id}} not found": "找不到有關提料單行的資料。", + "SuggestedPickLot not found for pickOrderLineId {{polId}}": "找不到該提料單行的建議揀貨批", + "SuggestedPickLot qty is invalid: {{qty}}": "建議揀貨數量無效:{{qty}}。", + "Reject switch lot: available {{available}} less than required {{required}}": "此批次貨品已被其他送貨單留起,請掃描其他批次。", + "Reject switch lot: picked {{picked}} already greater or equal required {{required}}": "換批被拒:已揀數量({{picked}})已達或超過建議量({{required}}),無法再拆分換批。", "Lot status is unavailable. Cannot switch or bind; pick line was not updated.": "批號狀態為「不可用」,無法換批或綁定;揀貨行未更新。" } \ No newline at end of file