diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx index 5403780..137ef52 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx @@ -24,7 +24,7 @@ import { import dayjs from 'dayjs'; import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider'; import { fetchLotDetail } from "@/app/api/inventory/actions"; -import { useCallback, useEffect, useState, useRef, useMemo } from "react"; +import React, { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/navigation"; import { @@ -560,8 +560,7 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); const formProps = useForm(); const errors = formProps.formState.errors; - // Add QR modal states - const [qrModalOpen, setQrModalOpen] = useState(false); + // QR scanner states (always-on, no modal) const [selectedLotForQr, setSelectedLotForQr] = useState(null); const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); const [expectedLotData, setExpectedLotData] = useState(null); @@ -575,11 +574,27 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false); // Add these missing state variables after line 352 const [isManualScanning, setIsManualScanning] = useState(false); + // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling + const [processedQrCombinations, setProcessedQrCombinations] = useState>>(new Map()); const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [lastProcessedQr, setLastProcessedQr] = useState(''); const [isRefreshingData, setIsRefreshingData] = useState(false); const [isSubmittingAll, setIsSubmittingAll] = useState(false); + // Cache for fetchStockInLineInfo API calls to avoid redundant requests + const stockInLineInfoCache = useRef>(new Map()); + const CACHE_TTL = 60000; // 60 seconds cache TTL + const abortControllerRef = useRef(null); + const qrProcessingTimeoutRef = useRef(null); + + // Use refs for processed QR tracking to avoid useEffect dependency issues and delays + const processedQrCodesRef = useRef>(new Set()); + const lastProcessedQrRef = useRef(''); + + // Store callbacks in refs to avoid useEffect dependency issues + const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise) | null>(null); + const resetScanRef = useRef<(() => void) | null>(null); + // Handle QR code button click @@ -604,51 +619,119 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); }, [combinedLotData]); + // Cached version of fetchStockInLineInfo to avoid redundant API calls + const fetchStockInLineInfoCached = useCallback(async (stockInLineId: number): Promise<{ lotNo: string | null }> => { + const now = Date.now(); + const cached = stockInLineInfoCache.current.get(stockInLineId); + + // Return cached value if still valid + if (cached && (now - cached.timestamp) < CACHE_TTL) { + console.log(`✅ [CACHE HIT] Using cached stockInLineInfo for ${stockInLineId}`); + return { lotNo: cached.lotNo }; + } + + // Cancel previous request if exists + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + // Create new abort controller for this request + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + try { + console.log(`⏱️ [CACHE MISS] Fetching stockInLineInfo for ${stockInLineId}`); + const stockInLineInfo = await fetchStockInLineInfo(stockInLineId); + + // Store in cache + stockInLineInfoCache.current.set(stockInLineId, { + lotNo: stockInLineInfo.lotNo || null, + timestamp: now + }); + + // Limit cache size to prevent memory leaks + if (stockInLineInfoCache.current.size > 100) { + const firstKey = stockInLineInfoCache.current.keys().next().value; + if (firstKey !== undefined) { + stockInLineInfoCache.current.delete(firstKey); + } + } + + return { lotNo: stockInLineInfo.lotNo || null }; + } catch (error: any) { + if (error.name === 'AbortError') { + console.log(`⏱️ [CACHE] Request aborted for ${stockInLineId}`); + throw error; + } + console.error(`❌ [CACHE] Error fetching stockInLineInfo for ${stockInLineId}:`, error); + throw error; + } + }, []); + const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => { const mismatchStartTime = performance.now(); console.log(`⏱️ [HANDLE LOT MISMATCH START]`); + console.log(`⏰ Start time: ${new Date().toISOString()}`); console.log("Lot mismatch detected:", { expectedLot, scannedLot }); - // ✅ Open modal immediately - we already have stockInLineId and itemId for matching - const updateStartTime = performance.now(); - setExpectedLotData(expectedLot); - setScannedLotData({ - ...scannedLot, - lotNo: scannedLot.lotNo || null, // Will be fetched in background if null - }); - setLotConfirmationOpen(true); // ✅ Open modal immediately - const updateTime = performance.now() - updateStartTime; - console.log(`⏱️ [HANDLE LOT MISMATCH] Modal opened immediately: ${updateTime.toFixed(2)}ms`); - - // ✅ Fetch lotNo in background ONLY for display purposes (matching is already done by stockInLineId + itemId) + // ✅ Use setTimeout to avoid flushSync warning - schedule modal update in next tick + const setTimeoutStartTime = performance.now(); + console.time('setLotConfirmationOpen'); + setTimeout(() => { + const setStateStartTime = performance.now(); + setExpectedLotData(expectedLot); + setScannedLotData({ + ...scannedLot, + lotNo: scannedLot.lotNo || null, + }); + setLotConfirmationOpen(true); + 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`); + }, 0); + const setTimeoutTime = performance.now() - setTimeoutStartTime; + console.log(`⏱️ [PERF] setTimeout scheduling time: ${setTimeoutTime.toFixed(2)}ms`); + + // ✅ Fetch lotNo in background ONLY for display purposes (using cached version) if (!scannedLot.lotNo && scannedLot.stockInLineId) { - console.log(`⏱️ [HANDLE LOT MISMATCH] Fetching lotNo in background for display (stockInLineId: ${scannedLot.stockInLineId})`); + const stockInLineId = scannedLot.stockInLineId; + if (typeof stockInLineId !== 'number') { + console.warn(`⏱️ [HANDLE LOT MISMATCH] Invalid stockInLineId: ${stockInLineId}`); + return; + } + console.log(`⏱️ [HANDLE LOT MISMATCH] Fetching lotNo in background (stockInLineId: ${stockInLineId})`); const fetchStartTime = performance.now(); - fetchStockInLineInfo(scannedLot.stockInLineId) + fetchStockInLineInfoCached(stockInLineId) .then((stockInLineInfo) => { const fetchTime = performance.now() - fetchStartTime; - console.log(`⏱️ [HANDLE LOT MISMATCH] fetchStockInLineInfo time: ${fetchTime.toFixed(2)}ms (${(fetchTime / 1000).toFixed(3)}s)`); + console.log(`⏱️ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached time: ${fetchTime.toFixed(2)}ms (${(fetchTime / 1000).toFixed(3)}s)`); - // ✅ Update the modal with fetched lotNo (modal is already open) - setScannedLotData((prev: any) => ({ - ...prev, - lotNo: stockInLineInfo.lotNo || null, - })); + const updateStateStartTime = performance.now(); + startTransition(() => { + setScannedLotData((prev: any) => ({ + ...prev, + lotNo: stockInLineInfo.lotNo || null, + })); + }); + const updateStateTime = performance.now() - updateStateStartTime; + console.log(`⏱️ [PERF] Update scanned lot data time: ${updateStateTime.toFixed(2)}ms`); const totalTime = performance.now() - mismatchStartTime; - console.log(`⏱️ [HANDLE LOT MISMATCH] Background fetch completed: ${totalTime.toFixed(2)}ms`); + console.log(`⏱️ [HANDLE LOT MISMATCH] Background fetch completed: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); }) .catch((error) => { - const fetchTime = performance.now() - fetchStartTime; - console.error(`❌ [HANDLE LOT MISMATCH] fetchStockInLineInfo failed after ${fetchTime.toFixed(2)}ms:`, error); - // Modal stays open, lotNo will show as "Loading..." or "Unknown" + if (error.name !== 'AbortError') { + const fetchTime = performance.now() - fetchStartTime; + console.error(`❌ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached failed after ${fetchTime.toFixed(2)}ms:`, error); + } }); } else { const totalTime = performance.now() - mismatchStartTime; - console.log(`⏱️ [HANDLE LOT MISMATCH END] Total time: ${totalTime.toFixed(2)}ms`); + console.log(`⏱️ [HANDLE LOT MISMATCH END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); } - }, []); + }, [fetchStockInLineInfoCached]); const checkAllLotsCompleted = useCallback((lotData: any[]) => { if (lotData.length === 0) { setAllLotsCompleted(false); @@ -1087,7 +1170,6 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); // setProcessedQrCodes(new Set()); // setLastProcessedQr(''); - setQrModalOpen(false); setPickExecutionFormOpen(false); if(selectedLotForQr?.stockOutLineId){ const stockOutLineUpdate = await updateStockOutLineStatus({ @@ -1341,19 +1423,36 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`); } }, [combinedLotData, updateStockOutLineStatusByQRCodeAndLotNo]); + // Enhanced lotDataIndexes with cached active lots for better performance const lotDataIndexes = useMemo(() => { + const indexStartTime = performance.now(); + console.log(`⏱️ [PERF] lotDataIndexes calculation START, data length: ${combinedLotData.length}`); + const byItemId = new Map(); const byItemCode = new Map(); const byLotId = new Map(); const byLotNo = new Map(); - const byStockInLineId = new Map(); // ✅ 新增:按 stockInLineId 索引 - - combinedLotData.forEach(lot => { + const byStockInLineId = new Map(); + // Cache active lots separately to avoid filtering on every scan + const activeLotsByItemId = new Map(); + const rejectedStatuses = new Set(['rejected']); + + // ✅ Use for loop instead of forEach for better performance on tablets + for (let i = 0; i < combinedLotData.length; i++) { + const lot = combinedLotData[i]; + const isActive = !rejectedStatuses.has(lot.lotAvailability) && + !rejectedStatuses.has(lot.stockOutLineStatus) && + !rejectedStatuses.has(lot.processingStatus); + if (lot.itemId) { if (!byItemId.has(lot.itemId)) { byItemId.set(lot.itemId, []); + activeLotsByItemId.set(lot.itemId, []); } byItemId.get(lot.itemId)!.push(lot); + if (isActive) { + activeLotsByItemId.get(lot.itemId)!.push(lot); + } } if (lot.itemCode) { @@ -1374,93 +1473,109 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); byLotNo.get(lot.lotNo)!.push(lot); } - // ✅ 新增:按 stockInLineId 索引 if (lot.stockInLineId) { if (!byStockInLineId.has(lot.stockInLineId)) { byStockInLineId.set(lot.stockInLineId, []); } byStockInLineId.get(lot.stockInLineId)!.push(lot); } - }); + } - return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId }; // ✅ 添加 byStockInLineId - }, [combinedLotData]); + const indexTime = performance.now() - indexStartTime; + if (indexTime > 10) { + console.log(`⏱️ [PERF] lotDataIndexes calculation END: ${indexTime.toFixed(2)}ms (${(indexTime / 1000).toFixed(3)}s)`); + } + + return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId }; + }, [combinedLotData.length, combinedLotData]); + + // Store resetScan in ref for immediate access (update on every render) + resetScanRef.current = resetScan; + const processOutsideQrCode = useCallback(async (latestQr: string) => { const totalStartTime = performance.now(); console.log(`⏱️ [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`); console.log(`⏰ Start time: ${new Date().toISOString()}`); - // 1) Parse JSON safely + // ✅ Measure index access time + const indexAccessStart = performance.now(); + const indexes = lotDataIndexes; // Access the memoized indexes + const indexAccessTime = performance.now() - indexAccessStart; + console.log(`⏱️ [PERF] Index access time: ${indexAccessTime.toFixed(2)}ms`); + + // 1) Parse JSON safely (parse once, reuse) const parseStartTime = performance.now(); let qrData: any = null; - let parseTime = 0; // ✅ Declare parseTime in outer scope + let parseTime = 0; try { qrData = JSON.parse(latestQr); - parseTime = performance.now() - parseStartTime; // ✅ Assign value - console.log(`⏱️ JSON parse time: ${parseTime.toFixed(2)}ms`); + parseTime = performance.now() - parseStartTime; + console.log(`⏱️ [PERF] JSON parse time: ${parseTime.toFixed(2)}ms`); } catch { console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches."); - setQrScanError(true); - setQrScanSuccess(false); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + }); return; } try { - // ✅ OPTIMIZATION: 直接使用 QR 数据,不需要调用 analyzeQrCode API + const validationStartTime = performance.now(); if (!(qrData?.stockInLineId && qrData?.itemId)) { console.log("QR JSON missing required fields (itemId, stockInLineId)."); - setQrScanError(true); - setQrScanSuccess(false); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + }); return; } + const validationTime = performance.now() - validationStartTime; + console.log(`⏱️ [PERF] Validation time: ${validationTime.toFixed(2)}ms`); const scannedItemId = qrData.itemId; const scannedStockInLineId = qrData.stockInLineId; - - // ✅ OPTIMIZATION: 使用索引快速查找相同 item 的 lots - const lookupStartTime = performance.now(); - const sameItemLots: any[] = []; - // 使用索引快速查找 - if (lotDataIndexes.byItemId.has(scannedItemId)) { - sameItemLots.push(...lotDataIndexes.byItemId.get(scannedItemId)!); - } - const lookupTime = performance.now() - lookupStartTime; - console.log(`⏱️ Index lookup time: ${lookupTime.toFixed(2)}ms, found ${sameItemLots.length} lots`); - - if (sameItemLots.length === 0) { - console.error("No item match in expected lots for scanned code"); - setQrScanError(true); - setQrScanSuccess(false); + // ✅ Check if this combination was already processed + const duplicateCheckStartTime = performance.now(); + const itemProcessedSet = processedQrCombinations.get(scannedItemId); + if (itemProcessedSet?.has(scannedStockInLineId)) { + const duplicateCheckTime = performance.now() - duplicateCheckStartTime; + console.log(`⏱️ [SKIP] Already processed combination: itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId} (check time: ${duplicateCheckTime.toFixed(2)}ms)`); return; } + const duplicateCheckTime = performance.now() - duplicateCheckStartTime; + console.log(`⏱️ [PERF] Duplicate check time: ${duplicateCheckTime.toFixed(2)}ms`); - // ✅ OPTIMIZATION: 过滤出活跃的 lots(非 rejected) - const filterStartTime = performance.now(); - const rejectedStatuses = new Set(['rejected']); - const activeSuggestedLots = sameItemLots.filter(lot => - !rejectedStatuses.has(lot.lotAvailability) && - !rejectedStatuses.has(lot.stockOutLineStatus) && - !rejectedStatuses.has(lot.processingStatus) - ); - const filterTime = performance.now() - filterStartTime; - console.log(`⏱️ Filter active lots time: ${filterTime.toFixed(2)}ms, active: ${activeSuggestedLots.length}`); + // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed) + const lookupStartTime = performance.now(); + const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || []; + const lookupTime = performance.now() - lookupStartTime; + console.log(`⏱️ [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${activeSuggestedLots.length} active lots`); if (activeSuggestedLots.length === 0) { console.error("No active suggested lots found for this item"); - setQrScanError(true); - setQrScanSuccess(false); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + }); return; } - // ✅ OPTIMIZATION: 按优先级查找匹配的 lot + // ✅ OPTIMIZATION: Direct Map lookup for stockInLineId match (O(1)) const matchStartTime = performance.now(); - // 1. 首先查找 stockInLineId 完全匹配的(正确的 lot) - let exactMatch = activeSuggestedLots.find(lot => - lot.stockInLineId === scannedStockInLineId - ); + let exactMatch: any = null; + const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || []; + // Find exact match from stockInLineId index, then verify it's in active lots + for (let i = 0; i < stockInLineLots.length; i++) { + const lot = stockInLineLots[i]; + if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) { + exactMatch = lot; + break; + } + } const matchTime = performance.now() - matchStartTime; - console.log(`⏱️ Find exact match time: ${matchTime.toFixed(2)}ms, found: ${exactMatch ? 'yes' : 'no'}`); + console.log(`⏱️ [PERF] Find exact match time: ${matchTime.toFixed(2)}ms, found: ${exactMatch ? 'yes' : 'no'}`); if (exactMatch) { // ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认 @@ -1468,14 +1583,17 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); if (!exactMatch.stockOutLineId) { console.warn("No stockOutLineId on exactMatch, cannot update status by QR."); - setQrScanError(true); - setQrScanSuccess(false); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + }); return; } try { const apiStartTime = performance.now(); console.log(`⏱️ [API CALL START] Calling updateStockOutLineStatusByQRCodeAndLotNo`); + console.log(`⏰ [API CALL] API start time: ${new Date().toISOString()}`); const res = await updateStockOutLineStatusByQRCodeAndLotNo({ pickOrderLineId: exactMatch.pickOrderLineId, inventoryLotNo: exactMatch.lotNo, @@ -1485,76 +1603,120 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); }); const apiTime = performance.now() - apiStartTime; console.log(`⏱️ [API CALL END] Total API time: ${apiTime.toFixed(2)}ms (${(apiTime / 1000).toFixed(3)}s)`); + console.log(`⏰ [API CALL] API end time: ${new Date().toISOString()}`); if (res.code === "checked" || res.code === "SUCCESS") { - const stateUpdateStartTime = performance.now(); - setQrScanError(false); - setQrScanSuccess(true); - const entity = res.entity as any; - setCombinedLotData(prev => prev.map(lot => { - if (lot.stockOutLineId === exactMatch.stockOutLineId && - lot.pickOrderLineId === exactMatch.pickOrderLineId) { - return { - ...lot, - stockOutLineStatus: 'checked', - stockOutLineQty: entity?.qty ?? lot.stockOutLineQty, - }; - } - return lot; - })); + // ✅ Batch state updates using startTransition + const stateUpdateStartTime = performance.now(); + startTransition(() => { + setQrScanError(false); + setQrScanSuccess(true); + + setCombinedLotData(prev => prev.map(lot => { + if (lot.stockOutLineId === exactMatch.stockOutLineId && + lot.pickOrderLineId === exactMatch.pickOrderLineId) { + return { + ...lot, + stockOutLineStatus: 'checked', + stockOutLineQty: entity?.qty ?? lot.stockOutLineQty, + }; + } + return lot; + })); - setOriginalCombinedData(prev => prev.map(lot => { - if (lot.stockOutLineId === exactMatch.stockOutLineId && - lot.pickOrderLineId === exactMatch.pickOrderLineId) { - return { - ...lot, - stockOutLineStatus: 'checked', - stockOutLineQty: entity?.qty ?? lot.stockOutLineQty, - }; - } - return lot; - })); + setOriginalCombinedData(prev => prev.map(lot => { + if (lot.stockOutLineId === exactMatch.stockOutLineId && + lot.pickOrderLineId === exactMatch.pickOrderLineId) { + return { + ...lot, + stockOutLineStatus: 'checked', + stockOutLineQty: entity?.qty ?? lot.stockOutLineQty, + }; + } + return lot; + })); + }); const stateUpdateTime = performance.now() - stateUpdateStartTime; - console.log(`⏱️ State update time: ${stateUpdateTime.toFixed(2)}ms`); + console.log(`⏱️ [PERF] State update time: ${stateUpdateTime.toFixed(2)}ms`); + + // Mark this combination as processed + const markProcessedStartTime = performance.now(); + setProcessedQrCombinations(prev => { + const newMap = new Map(prev); + if (!newMap.has(scannedItemId)) { + newMap.set(scannedItemId, new Set()); + } + newMap.get(scannedItemId)!.add(scannedStockInLineId); + return newMap; + }); + const markProcessedTime = performance.now() - markProcessedStartTime; + console.log(`⏱️ [PERF] Mark processed time: ${markProcessedTime.toFixed(2)}ms`); const totalTime = performance.now() - totalStartTime; console.log(`✅ [PROCESS OUTSIDE QR END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); console.log(`⏰ End time: ${new Date().toISOString()}`); - console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, filter=${filterTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, api=${apiTime.toFixed(2)}ms, state=${stateUpdateTime.toFixed(2)}ms`); + console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, api=${apiTime.toFixed(2)}ms, stateUpdate=${stateUpdateTime.toFixed(2)}ms, markProcessed=${markProcessedTime.toFixed(2)}ms`); console.log("✅ Status updated locally, no full data refresh needed"); } else { console.warn("Unexpected response code from backend:", res.code); - setQrScanError(true); - setQrScanSuccess(false); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + }); } } catch (e) { const totalTime = performance.now() - totalStartTime; console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`); console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e); - setQrScanError(true); - setQrScanSuccess(false); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + }); } return; // ✅ 直接返回,不需要确认表单 } // ✅ Case 2: itemId 匹配但 stockInLineId 不匹配 - 显示确认表单 + // Check if we should allow reopening (different stockInLineId) + const mismatchCheckStartTime = performance.now(); + const itemProcessedSet2 = processedQrCombinations.get(scannedItemId); + if (itemProcessedSet2?.has(scannedStockInLineId)) { + const mismatchCheckTime = performance.now() - mismatchCheckStartTime; + console.log(`⏱️ [SKIP] Already processed this exact combination (check time: ${mismatchCheckTime.toFixed(2)}ms)`); + return; + } + const mismatchCheckTime = performance.now() - mismatchCheckStartTime; + console.log(`⏱️ [PERF] Mismatch check time: ${mismatchCheckTime.toFixed(2)}ms`); + // 取第一个活跃的 lot 作为期望的 lot + const expectedLotStartTime = performance.now(); const expectedLot = activeSuggestedLots[0]; if (!expectedLot) { console.error("Could not determine expected lot for confirmation"); - setQrScanError(true); - setQrScanSuccess(false); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + }); return; } - + const expectedLotTime = performance.now() - expectedLotStartTime; + console.log(`⏱️ [PERF] Get expected lot time: ${expectedLotTime.toFixed(2)}ms`); + // ✅ 立即打开确认模态框,不等待其他操作 console.log(`⚠️ Lot mismatch: Expected stockInLineId=${expectedLot.stockInLineId}, Scanned stockInLineId=${scannedStockInLineId}`); + + // Set selected lot immediately (no transition delay) + const setSelectedLotStartTime = performance.now(); setSelectedLotForQr(expectedLot); + 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( { lotNo: expectedLot.lotNo, @@ -1569,48 +1731,265 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); stockInLineId: scannedStockInLineId // ✅ 传递 stockInLineId } ); + const handleMismatchTime = performance.now() - handleMismatchStartTime; + console.log(`⏱️ [PERF] Handle mismatch call time: ${handleMismatchTime.toFixed(2)}ms`); + + const totalTime = performance.now() - totalStartTime; + console.log(`⚠️ [PROCESS OUTSIDE QR MISMATCH] Total time before modal: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); + console.log(`⏰ End time: ${new Date().toISOString()}`); + console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, mismatchCheck=${mismatchCheckTime.toFixed(2)}ms, expectedLot=${expectedLotTime.toFixed(2)}ms, setSelectedLot=${setSelectedLotTime.toFixed(2)}ms, handleMismatch=${handleMismatchTime.toFixed(2)}ms`); } catch (error) { const totalTime = performance.now() - totalStartTime; console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`); console.error("Error during QR code processing:", error); - setQrScanError(true); - setQrScanSuccess(false); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + }); return; } - }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotDataIndexes, updateStockOutLineStatusByQRCodeAndLotNo]); -useEffect(() => { - if (lotConfirmationOpen || manualLotConfirmationOpen) { - console.log("Confirmation modal is open, skipping QR processing..."); - return; - } + }, [lotDataIndexes, handleLotMismatch, processedQrCombinations]); + // Store processOutsideQrCode in ref for immediate access (update on every render) + processOutsideQrCodeRef.current = processOutsideQrCode; - if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { - return; - } + useEffect(() => { + // Skip if scanner is not active or no data available + if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { + return; + } + + const qrValuesChangeStartTime = performance.now(); + console.log(`⏱️ [QR VALUES EFFECT] Triggered at: ${new Date().toISOString()}`); + console.log(`⏱️ [QR VALUES EFFECT] qrValues.length: ${qrValues.length}`); + console.log(`⏱️ [QR VALUES EFFECT] qrValues:`, qrValues); + + const latestQr = qrValues[qrValues.length - 1]; + console.log(`⏱️ [QR VALUES EFFECT] Latest QR: ${latestQr}`); + console.log(`⏰ [QR VALUES EFFECT] Latest QR detected at: ${new Date().toISOString()}`); + + // ✅ FIXED: Handle test shortcut {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId + // Support both formats: {2fitest (2 t's) and {2fittest (3 t's) + if ((latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) && latestQr.endsWith("}")) { + // Extract content: remove "{2fitest" or "{2fittest" and "}" + let content = ''; + if (latestQr.startsWith("{2fittest")) { + content = latestQr.substring(9, latestQr.length - 1); // Remove "{2fittest" and "}" + } else if (latestQr.startsWith("{2fitest")) { + content = latestQr.substring(8, latestQr.length - 1); // Remove "{2fitest" and "}" + } + + 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)) { + console.log( + `%c TEST QR: Detected ${latestQr.substring(0, 9)}... - Simulating QR input (itemId=${itemId}, stockInLineId=${stockInLineId})`, + "color: purple; font-weight: bold" + ); + + // ✅ Simulate QR code JSON format + const simulatedQr = JSON.stringify({ + itemId: itemId, + stockInLineId: stockInLineId + }); + + console.log(`⏱️ [TEST QR] Simulated QR content: ${simulatedQr}`); + console.log(`⏱️ [TEST QR] Start time: ${new Date().toISOString()}`); + const testStartTime = performance.now(); + + // ✅ Mark as processed FIRST to avoid duplicate processing + lastProcessedQrRef.current = latestQr; + processedQrCodesRef.current.add(latestQr); + if (processedQrCodesRef.current.size > 100) { + const firstValue = processedQrCodesRef.current.values().next().value; + if (firstValue !== undefined) { + processedQrCodesRef.current.delete(firstValue); + } + } + setLastProcessedQr(latestQr); + setProcessedQrCodes(new Set(processedQrCodesRef.current)); + + // ✅ Process immediately (bypass QR scanner delay) + if (processOutsideQrCodeRef.current) { + processOutsideQrCodeRef.current(simulatedQr).then(() => { + const testTime = performance.now() - testStartTime; + console.log(`⏱️ [TEST QR] Total processing time: ${testTime.toFixed(2)}ms (${(testTime / 1000).toFixed(3)}s)`); + console.log(`⏱️ [TEST QR] End time: ${new Date().toISOString()}`); + }).catch((error) => { + const testTime = performance.now() - testStartTime; + console.error(`❌ [TEST QR] Error after ${testTime.toFixed(2)}ms:`, error); + }); + } + + // Reset scan + if (resetScanRef.current) { + resetScanRef.current(); + } + + const qrValuesChangeTime = performance.now() - qrValuesChangeStartTime; + console.log(`⏱️ [QR VALUES EFFECT] Test QR handling time: ${qrValuesChangeTime.toFixed(2)}ms`); + return; // ✅ IMPORTANT: Return early to prevent normal processing + } else { + console.warn(`⏱️ [TEST QR] Invalid itemId or stockInLineId: itemId=${parts[0]}, stockInLineId=${parts[1]}`); + } + } else { + console.warn(`⏱️ [TEST QR] Invalid format. Expected {2fitestx,y} or {2fittestx,y}, got: ${latestQr}`); + } + } + + // Skip processing if confirmation modals are open + // BUT: Allow processing if modal was just closed (to allow reopening for different stockInLineId) + if (lotConfirmationOpen || 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}`); + 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`); + } + + const qrDetectionStartTime = performance.now(); + console.log(`⏱️ [QR DETECTION] Latest QR detected: ${latestQr?.substring(0, 50)}...`); + console.log(`⏰ [QR DETECTION] Detection time: ${new Date().toISOString()}`); + console.log(`⏱️ [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`); + + // Skip if already processed (use refs to avoid dependency issues and delays) + const checkProcessedStartTime = performance.now(); + if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) { + const checkTime = performance.now() - checkProcessedStartTime; + console.log(`⏱️ [QR PROCESS] Already processed check time: ${checkTime.toFixed(2)}ms`); + return; + } + const checkTime = performance.now() - checkProcessedStartTime; + console.log(`⏱️ [QR PROCESS] Not processed check time: ${checkTime.toFixed(2)}ms`); + + // Handle special shortcut + if (latestQr === "{2fic}") { + console.log(" Detected {2fic} shortcut - opening manual lot confirmation form"); + setManualLotConfirmationOpen(true); + if (resetScanRef.current) { + resetScanRef.current(); + } + lastProcessedQrRef.current = latestQr; + processedQrCodesRef.current.add(latestQr); + if (processedQrCodesRef.current.size > 100) { + const firstValue = processedQrCodesRef.current.values().next().value; + if (firstValue !== undefined) { + processedQrCodesRef.current.delete(firstValue); + } + } + setLastProcessedQr(latestQr); + setProcessedQrCodes(prev => { + const newSet = new Set(prev); + newSet.add(latestQr); + if (newSet.size > 100) { + const firstValue = newSet.values().next().value; + if (firstValue !== undefined) { + newSet.delete(firstValue); + } + } + return newSet; + }); + return; + } + + // Process new QR code immediately (background mode - no modal) + // Check against refs to avoid state update delays + if (latestQr && latestQr !== lastProcessedQrRef.current) { + const processingStartTime = performance.now(); + console.log(`⏱️ [QR PROCESS] Starting processing at: ${new Date().toISOString()}`); + console.log(`⏱️ [QR PROCESS] Time since detection: ${(processingStartTime - qrDetectionStartTime).toFixed(2)}ms`); + + // ✅ Process immediately for better responsiveness + // Clear any pending debounced processing + if (qrProcessingTimeoutRef.current) { + clearTimeout(qrProcessingTimeoutRef.current); + qrProcessingTimeoutRef.current = null; + } + + // Log immediately (console.log is synchronous) + console.log(`⏱️ [QR PROCESS] Processing new QR code with enhanced validation: ${latestQr}`); + + // Update refs immediately (no state update delay) - do this FIRST + const refUpdateStartTime = performance.now(); + lastProcessedQrRef.current = latestQr; + processedQrCodesRef.current.add(latestQr); + if (processedQrCodesRef.current.size > 100) { + const firstValue = processedQrCodesRef.current.values().next().value; + if (firstValue !== undefined) { + processedQrCodesRef.current.delete(firstValue); + } + } + const refUpdateTime = performance.now() - refUpdateStartTime; + console.log(`⏱️ [QR PROCESS] Ref update time: ${refUpdateTime.toFixed(2)}ms`); + + // Process immediately in background - no modal/form needed, no delays + // Use ref to avoid dependency issues + const processCallStartTime = performance.now(); + if (processOutsideQrCodeRef.current) { + processOutsideQrCodeRef.current(latestQr).then(() => { + const processCallTime = performance.now() - processCallStartTime; + const totalProcessingTime = performance.now() - processingStartTime; + console.log(`⏱️ [QR PROCESS] processOutsideQrCode call time: ${processCallTime.toFixed(2)}ms`); + console.log(`⏱️ [QR PROCESS] Total processing time: ${totalProcessingTime.toFixed(2)}ms (${(totalProcessingTime / 1000).toFixed(3)}s)`); + }).catch((error) => { + const processCallTime = performance.now() - processCallStartTime; + const totalProcessingTime = performance.now() - processingStartTime; + console.error(`❌ [QR PROCESS] processOutsideQrCode error after ${processCallTime.toFixed(2)}ms:`, error); + console.error(`❌ [QR PROCESS] Total processing time before error: ${totalProcessingTime.toFixed(2)}ms`); + }); + } + + // Update state for UI (but don't block on it) + const stateUpdateStartTime = performance.now(); + setLastProcessedQr(latestQr); + setProcessedQrCodes(new Set(processedQrCodesRef.current)); + const stateUpdateTime = performance.now() - stateUpdateStartTime; + console.log(`⏱️ [QR PROCESS] State update time: ${stateUpdateTime.toFixed(2)}ms`); + + const detectionTime = performance.now() - qrDetectionStartTime; + const totalEffectTime = performance.now() - qrValuesChangeStartTime; + console.log(`⏱️ [QR DETECTION] Total detection time: ${detectionTime.toFixed(2)}ms`); + console.log(`⏱️ [QR VALUES EFFECT] Total effect time: ${totalEffectTime.toFixed(2)}ms`); + } + + return () => { + if (qrProcessingTimeoutRef.current) { + clearTimeout(qrProcessingTimeoutRef.current); + qrProcessingTimeoutRef.current = null; + } + }; + }, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]); +const renderCountRef = useRef(0); +const renderStartTimeRef = useRef(null); - const latestQr = qrValues[qrValues.length - 1]; +// Track render performance +useEffect(() => { + renderCountRef.current++; + const now = performance.now(); - if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { - console.log("QR code already processed, skipping..."); - return; - } - if (latestQr === "{2fic}") { - console.log(" Detected {2fic} shortcut - opening manual lot confirmation form"); - setManualLotConfirmationOpen(true); - resetScan(); - setLastProcessedQr(latestQr); - setProcessedQrCodes(prev => new Set(prev).add(latestQr)); - return; // 直接返回,不继续处理其他逻辑 + if (renderStartTimeRef.current !== null) { + const renderTime = now - renderStartTimeRef.current; + if (renderTime > 100) { // Only log slow renders (>100ms) + console.log(`⏱️ [PERF] Render #${renderCountRef.current} took ${renderTime.toFixed(2)}ms, combinedLotData length: ${combinedLotData.length}`); + } + renderStartTimeRef.current = null; } - if (latestQr && latestQr !== lastProcessedQr) { - console.log(` Processing new QR code with enhanced validation: ${latestQr}`); - setLastProcessedQr(latestQr); - setProcessedQrCodes(prev => new Set(prev).add(latestQr)); - - processOutsideQrCode(latestQr); + + // Track when lotConfirmationOpen changes + if (lotConfirmationOpen) { + renderStartTimeRef.current = performance.now(); + console.log(`⏱️ [PERF] Render triggered by lotConfirmationOpen=true`); } -}, [qrValues, isManualScanning, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode, combinedLotData]); - // Only fetch existing data when session is ready, no auto-assignment +}, [combinedLotData.length, lotConfirmationOpen]); + // Auto-start scanner only once on mount + const scannerInitializedRef = useRef(false); + useEffect(() => { if (session && currentUserId && !initializationRef.current) { console.log(" Session loaded, initializing pick order..."); @@ -1620,6 +1999,17 @@ useEffect(() => { fetchAllCombinedLotData(); } }, [session, currentUserId, fetchAllCombinedLotData]); + + // Separate effect for auto-starting scanner (only once, prevents multiple resets) + useEffect(() => { + if (session && currentUserId && !scannerInitializedRef.current) { + scannerInitializedRef.current = true; + // ✅ Auto-start scanner on mount for tablet use (background mode - no modal) + console.log("✅ Auto-starting QR scanner in background mode"); + setIsManualScanning(true); + startScan(); + } + }, [session, currentUserId, startScan]); // Add event listener for manual assignment useEffect(() => { @@ -1664,8 +2054,7 @@ useEffect(() => { setQrScanSuccess(true); setQrScanError(false); - // Close modal - setQrModalOpen(false); + // Clear selected lot (scanner stays active) setSelectedLotForQr(null); // Set pick quantity @@ -1880,11 +2269,12 @@ useEffect(() => { setQrScanError(false); setQrScanSuccess(false); setQrScanInput(''); - setIsManualScanning(false); - stopScan(); - resetScan(); - setProcessedQrCodes(new Set()); - setLastProcessedQr(''); + // ✅ Keep scanner active after form submission - don't stop scanning + // Only clear processed QR codes for the specific lot, not all + // setIsManualScanning(false); // Removed - keep scanner active + // stopScan(); // Removed - keep scanner active + // resetScan(); // Removed - keep scanner active + // Don't clear all processed codes - only clear for this specific lot if needed await fetchAllCombinedLotData(); } catch (error) { console.error("Error submitting pick execution form:", error); @@ -1972,6 +2362,7 @@ useEffect(() => { // Pagination data with sorting by routerIndex // Remove the sorting logic and just do pagination +// ✅ Memoize paginatedData to prevent re-renders when modal opens const paginatedData = useMemo(() => { if (paginationController.pageSize === -1) { return combinedLotData; // Show all items @@ -1979,7 +2370,7 @@ const paginatedData = useMemo(() => { const startIndex = paginationController.pageNum * paginationController.pageSize; const endIndex = startIndex + paginationController.pageSize; return combinedLotData.slice(startIndex, endIndex); // No sorting needed -}, [combinedLotData, paginationController]); +}, [combinedLotData, paginationController.pageNum, paginationController.pageSize]); const allItemsReady = useMemo(() => { if (combinedLotData.length === 0) return false; @@ -2143,15 +2534,29 @@ const handleSkip = useCallback(async (lot: any) => { console.error("Error in Skip:", err); } }, [handleSubmitPickQtyWithQty]); - const handleStartScan = useCallback(() => { - console.log(" Starting manual QR scan..."); - setIsManualScanning(true); - setProcessedQrCodes(new Set()); - setLastProcessedQr(''); - setQrScanError(false); - setQrScanSuccess(false); - startScan(); - }, [startScan]); +const handleStartScan = useCallback(() => { + const startTime = performance.now(); + console.log(`⏱️ [START SCAN] Called at: ${new Date().toISOString()}`); + console.log(`⏱️ [START SCAN] Starting manual QR scan...`); + + setIsManualScanning(true); + const setManualScanningTime = performance.now() - startTime; + console.log(`⏱️ [START SCAN] setManualScanning time: ${setManualScanningTime.toFixed(2)}ms`); + + setProcessedQrCodes(new Set()); + setLastProcessedQr(''); + setQrScanError(false); + setQrScanSuccess(false); + + const beforeStartScanTime = performance.now(); + startScan(); + const startScanTime = performance.now() - beforeStartScanTime; + console.log(`⏱️ [START SCAN] startScan() call time: ${startScanTime.toFixed(2)}ms`); + + const totalTime = performance.now() - startTime; + console.log(`⏱️ [START SCAN] Total start scan time: ${totalTime.toFixed(2)}ms`); + console.log(`⏰ [START SCAN] Start scan completed at: ${new Date().toISOString()}`); +}, [startScan]); const handlePickOrderSwitch = useCallback(async (pickOrderId: number) => { if (pickOrderSwitching) return; @@ -2170,7 +2575,7 @@ const handleSkip = useCallback(async (lot: any) => { }, [pickOrderSwitching, currentUserId, fetchAllCombinedLotData]); const handleStopScan = useCallback(() => { - console.log("⏹️ Stopping manual QR scan..."); + console.log("⏸️ Pausing QR scanner..."); setIsManualScanning(false); setQrScanError(false); setQrScanSuccess(false); @@ -2529,25 +2934,45 @@ const handleSubmitAllScanned = useCallback(async () => { - {!isManualScanning ? ( + {/* Scanner status indicator (always visible) */} + {/* + + + + {isManualScanning ? t("Scanner Active") : t("Scanner Inactive")} + + + */} + + {/* Pause/Resume button instead of Start/Stop */} + {isManualScanning ? ( ) : ( )} @@ -2914,20 +3339,7 @@ paginatedData.map((lot, index) => { - {/* 保留:QR Code Modal */} - { - setQrModalOpen(false); - setSelectedLotForQr(null); - stopScan(); - resetScan(); - }} - lot={selectedLotForQr} - combinedLotData={combinedLotData} - onQrCodeSubmit={handleQrCodeSubmitFromModal} - lotConfirmationOpen={lotConfirmationOpen} // ✅ Add this prop - /> + {/* QR Code Scanner works in background - no modal needed */} { @@ -2943,17 +3355,22 @@ paginatedData.map((lot, index) => { { + console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`); setLotConfirmationOpen(false); setExpectedLotData(null); setScannedLotData(null); - setSelectedLotForQr(null); // ✅ 新增:清除选中的 lot + setSelectedLotForQr(null); - // ✅ 修复:不要清除 processedQrCodes,而是保留它,避免重复处理 - // 或者,如果确实需要清除,应该在清除后立即重新标记为已处理 - if (lastProcessedQr) { - - setLastProcessedQr(''); - } + // ✅ 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 }} onConfirm={handleLotConfirmation} expectedLot={expectedLotData}