|
|
|
@@ -573,6 +573,10 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); |
|
|
|
// Store callbacks in refs to avoid useEffect dependency issues |
|
|
|
const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | null>(null); |
|
|
|
const resetScanRef = useRef<(() => void) | null>(null); |
|
|
|
const lotConfirmOpenedQrCountRef = useRef<number>(0); |
|
|
|
const lotConfirmOpenedQrValueRef = useRef<string>(''); |
|
|
|
const lotConfirmInitialSameQrSkippedRef = useRef<boolean>(false); |
|
|
|
const autoConfirmInProgressRef = useRef<boolean>(false); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1137,6 +1141,55 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
|
}); |
|
|
|
window.dispatchEvent(event); |
|
|
|
}, [allLotsCompleted]); |
|
|
|
|
|
|
|
const clearLotConfirmationState = useCallback((clearProcessedRefs: boolean = false) => { |
|
|
|
setLotConfirmationOpen(false); |
|
|
|
setExpectedLotData(null); |
|
|
|
setScannedLotData(null); |
|
|
|
setSelectedLotForQr(null); |
|
|
|
|
|
|
|
if (clearProcessedRefs) { |
|
|
|
setTimeout(() => { |
|
|
|
lastProcessedQrRef.current = ''; |
|
|
|
processedQrCodesRef.current.clear(); |
|
|
|
console.log(`⏱️ [LOT CONFIRM MODAL] Cleared refs to allow reprocessing`); |
|
|
|
}, 100); |
|
|
|
} |
|
|
|
}, []); |
|
|
|
|
|
|
|
const parseQrPayload = useCallback((rawQr: string): { itemId: number; stockInLineId: number } | null => { |
|
|
|
if (!rawQr) return null; |
|
|
|
|
|
|
|
if ((rawQr.startsWith("{2fitest") || rawQr.startsWith("{2fittest")) && rawQr.endsWith("}")) { |
|
|
|
let content = ''; |
|
|
|
if (rawQr.startsWith("{2fittest")) { |
|
|
|
content = rawQr.substring(9, rawQr.length - 1); |
|
|
|
} else { |
|
|
|
content = rawQr.substring(8, rawQr.length - 1); |
|
|
|
} |
|
|
|
|
|
|
|
const parts = content.split(','); |
|
|
|
if (parts.length === 2) { |
|
|
|
const itemId = parseInt(parts[0].trim(), 10); |
|
|
|
const stockInLineId = parseInt(parts[1].trim(), 10); |
|
|
|
if (!isNaN(itemId) && !isNaN(stockInLineId)) { |
|
|
|
return { itemId, stockInLineId }; |
|
|
|
} |
|
|
|
} |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
const parsed = JSON.parse(rawQr); |
|
|
|
if (parsed?.itemId && parsed?.stockInLineId) { |
|
|
|
return { itemId: parsed.itemId, stockInLineId: parsed.stockInLineId }; |
|
|
|
} |
|
|
|
return null; |
|
|
|
} catch { |
|
|
|
return null; |
|
|
|
} |
|
|
|
}, []); |
|
|
|
|
|
|
|
const handleLotConfirmation = useCallback(async () => { |
|
|
|
if (!expectedLotData || !scannedLotData || !selectedLotForQr) return; |
|
|
|
setIsConfirmingLot(true); |
|
|
|
@@ -1175,10 +1228,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
|
} |
|
|
|
|
|
|
|
// ✅ 修复:先关闭 modal 和清空状态,再刷新数据 |
|
|
|
setLotConfirmationOpen(false); |
|
|
|
setExpectedLotData(null); |
|
|
|
setScannedLotData(null); |
|
|
|
setSelectedLotForQr(null); |
|
|
|
clearLotConfirmationState(false); |
|
|
|
|
|
|
|
// ✅ 修复:刷新数据前设置刷新标志,避免在刷新期间处理新的 QR code |
|
|
|
setIsRefreshingData(true); |
|
|
|
@@ -1189,7 +1239,94 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
|
} finally { |
|
|
|
setIsConfirmingLot(false); |
|
|
|
} |
|
|
|
}, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData, resetScan]); |
|
|
|
}, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData, resetScan, clearLotConfirmationState]); |
|
|
|
|
|
|
|
const handleLotConfirmationByRescan = useCallback(async (rawQr: string): Promise<boolean> => { |
|
|
|
if (!lotConfirmationOpen || !selectedLotForQr || !expectedLotData || !scannedLotData) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
const payload = parseQrPayload(rawQr); |
|
|
|
const expectedStockInLineId = Number(selectedLotForQr.stockInLineId); |
|
|
|
const mismatchedStockInLineId = Number(scannedLotData?.stockInLineId); |
|
|
|
|
|
|
|
if (payload) { |
|
|
|
const rescannedStockInLineId = Number(payload.stockInLineId); |
|
|
|
|
|
|
|
// 再扫“差异 lot” => 直接执行切换 |
|
|
|
if ( |
|
|
|
Number.isFinite(mismatchedStockInLineId) && |
|
|
|
rescannedStockInLineId === mismatchedStockInLineId |
|
|
|
) { |
|
|
|
await handleLotConfirmation(); |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
// 再扫“原建议 lot” => 关闭弹窗并按原 lot 正常记一次扫描 |
|
|
|
if ( |
|
|
|
Number.isFinite(expectedStockInLineId) && |
|
|
|
rescannedStockInLineId === expectedStockInLineId |
|
|
|
) { |
|
|
|
clearLotConfirmationState(false); |
|
|
|
if (processOutsideQrCodeRef.current) { |
|
|
|
await processOutsideQrCodeRef.current(JSON.stringify(payload)); |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
} else { |
|
|
|
// 兼容纯 lotNo 文本扫码 |
|
|
|
const scannedText = rawQr?.trim(); |
|
|
|
const expectedLotNo = expectedLotData?.lotNo?.trim(); |
|
|
|
const mismatchedLotNo = scannedLotData?.lotNo?.trim(); |
|
|
|
|
|
|
|
if (mismatchedLotNo && scannedText === mismatchedLotNo) { |
|
|
|
await handleLotConfirmation(); |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
if (expectedLotNo && scannedText === expectedLotNo) { |
|
|
|
clearLotConfirmationState(false); |
|
|
|
if (processOutsideQrCodeRef.current) { |
|
|
|
await processOutsideQrCodeRef.current(JSON.stringify({ |
|
|
|
itemId: selectedLotForQr.itemId, |
|
|
|
stockInLineId: selectedLotForQr.stockInLineId, |
|
|
|
})); |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return false; |
|
|
|
}, [lotConfirmationOpen, selectedLotForQr, expectedLotData, scannedLotData, parseQrPayload, handleLotConfirmation, clearLotConfirmationState]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (!lotConfirmationOpen || !expectedLotData || !scannedLotData || !selectedLotForQr) { |
|
|
|
autoConfirmInProgressRef.current = false; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (autoConfirmInProgressRef.current || isConfirmingLot) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
autoConfirmInProgressRef.current = true; |
|
|
|
handleLotConfirmation() |
|
|
|
.catch((error) => { |
|
|
|
console.error("Auto confirm lot substitution failed:", error); |
|
|
|
}) |
|
|
|
.finally(() => { |
|
|
|
autoConfirmInProgressRef.current = false; |
|
|
|
}); |
|
|
|
}, [lotConfirmationOpen, expectedLotData, scannedLotData, selectedLotForQr, isConfirmingLot, handleLotConfirmation]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (lotConfirmationOpen) { |
|
|
|
// 记录弹窗打开时的扫码数量,避免把“触发弹窗的同一次扫码”当作二次确认 |
|
|
|
lotConfirmOpenedQrCountRef.current = qrValues.length; |
|
|
|
lotConfirmOpenedQrValueRef.current = qrValues[qrValues.length - 1] || ''; |
|
|
|
lotConfirmInitialSameQrSkippedRef.current = true; |
|
|
|
} |
|
|
|
}, [lotConfirmationOpen, qrValues.length]); |
|
|
|
const handleQrCodeSubmit = useCallback(async (lotNo: string) => { |
|
|
|
console.log(` Processing QR Code for lot: ${lotNo}`); |
|
|
|
|
|
|
|
@@ -1937,17 +2074,22 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Skip processing if confirmation modals are open |
|
|
|
// BUT: Allow processing if modal was just closed (to allow reopening for different stockInLineId) |
|
|
|
if (lotConfirmationOpen || manualLotConfirmationOpen) { |
|
|
|
// lot confirm 弹窗打开时,允许通过“再次扫码”决定走向(切换或继续原 lot) |
|
|
|
if (lotConfirmationOpen) { |
|
|
|
// 已改回自动确认:弹窗打开时不再等待二次扫码 |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Skip processing if manual confirmation modal is open |
|
|
|
if (manualLotConfirmationOpen) { |
|
|
|
// Check if this is a different QR code than what triggered the modal |
|
|
|
const modalTriggerQr = lastProcessedQrRef.current; |
|
|
|
if (latestQr === modalTriggerQr) { |
|
|
|
console.log(`⏱️ [QR PROCESS] Skipping - modal open for same QR: lotConfirmation=${lotConfirmationOpen}, manual=${manualLotConfirmationOpen}`); |
|
|
|
console.log(`⏱️ [QR PROCESS] Skipping - manual modal open for same QR`); |
|
|
|
return; |
|
|
|
} |
|
|
|
// If it's a different QR, allow processing (user might have canceled and scanned different lot) |
|
|
|
console.log(`⏱️ [QR PROCESS] Different QR detected while modal open, allowing processing`); |
|
|
|
// If it's a different QR, allow processing |
|
|
|
console.log(`⏱️ [QR PROCESS] Different QR detected while manual modal open, allowing processing`); |
|
|
|
} |
|
|
|
|
|
|
|
const qrDetectionStartTime = performance.now(); |
|
|
|
@@ -2061,7 +2203,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
|
qrProcessingTimeoutRef.current = null; |
|
|
|
} |
|
|
|
}; |
|
|
|
}, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]); |
|
|
|
}, [qrValues, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen, handleLotConfirmationByRescan]); |
|
|
|
const renderCountRef = useRef(0); |
|
|
|
const renderStartTimeRef = useRef<number | null>(null); |
|
|
|
|
|
|
|
@@ -3440,21 +3582,7 @@ paginatedData.map((lot, index) => { |
|
|
|
open={lotConfirmationOpen} |
|
|
|
onClose={() => { |
|
|
|
console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`); |
|
|
|
setLotConfirmationOpen(false); |
|
|
|
setExpectedLotData(null); |
|
|
|
setScannedLotData(null); |
|
|
|
setSelectedLotForQr(null); |
|
|
|
|
|
|
|
// ✅ IMPORTANT: Clear refs to allow reprocessing the same QR code if user cancels and scans again |
|
|
|
// This allows the modal to reopen for the same itemId with a different stockInLineId |
|
|
|
setTimeout(() => { |
|
|
|
lastProcessedQrRef.current = ''; |
|
|
|
processedQrCodesRef.current.clear(); |
|
|
|
console.log(`⏱️ [LOT CONFIRM MODAL] Cleared refs to allow reprocessing`); |
|
|
|
}, 100); |
|
|
|
|
|
|
|
// ✅ Don't clear processedQrCombinations - it tracks by itemId+stockInLineId, |
|
|
|
// so reopening for same itemId but different stockInLineId is allowed |
|
|
|
clearLotConfirmationState(true); |
|
|
|
}} |
|
|
|
onConfirm={handleLotConfirmation} |
|
|
|
expectedLot={expectedLotData} |
|
|
|
|