@@ -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<string, any>;
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<boolean>(false);
const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
const [scannedQrResult, setScannedQrResult] = useState<string>('');
const [fgPickOrder, setFgPickOrder] = useState<FGPickOrderResponse | null>(null);
const fetchingRef = useRef<Set<number>>(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<any | null>(null);
const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
const [lotConfirmationError, setLotConfirmationError] = useState<string | null>(null);
/** QR 静默换批失败时显示在对应行的 Lot# 列,key = stockOutLineId */
const [lotSwitchFailByStockOutLineId, setLotSwitchFailByStockOutLineId] = useState<
Record<number, string>
>({});
const [expectedLotData, setExpectedLotData] = useState<any>(null);
const [scannedLotData, setScannedLotData] = useState<any>(null);
const [isConfirmingLot, setIsConfirmingLot] = useState(false);
@@ -626,7 +646,6 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
// Add these missing state variables after line 352
const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
@@ -656,9 +675,10 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
const lotConfirmLastQrRef = useRef<string>('');
const lotConfirmSkipNextScanRef = useRef<boolean>(false);
const lotConfirmOpenedAtRef = useRef<number>(0);
const handleLotConfirmationRef = useRef<
((overrideScannedLot?: any, runContext?: LotConfirmRunContext) => Promise<void>) | 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<boolean> => {
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 (
<TableRow
key={`${lot.pickOrderLineId}-${lot.lotId || 'null'}`}
@@ -3664,6 +3713,11 @@ paginatedData.map((lot, index) => {
)
)}
</Typography>
{lotSwitchErr ? (
<Typography variant="body2" color="error" sx={{ mt: 0.5, display: 'block', fontWeight: 500 }}>
{lotSwitchErr}
</Typography>
) : null}
</Box>
</TableCell>
<TableCell align="right">