From 26302151c358f4017c6f33386a66f492d2c1817e Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Sat, 31 Jan 2026 22:43:11 +0800 Subject: [PATCH] update qc putaway --- src/components/Jodetail/JobPickExecution.tsx | 1159 ++++++++++++++--- .../ProductionProcessJobOrderDetail.tsx | 2 +- src/components/PutAwayScan/PutAwayModal.tsx | 78 +- 3 files changed, 1022 insertions(+), 217 deletions(-) diff --git a/src/components/Jodetail/JobPickExecution.tsx b/src/components/Jodetail/JobPickExecution.tsx index ebec2ef..f3e74e9 100644 --- a/src/components/Jodetail/JobPickExecution.tsx +++ b/src/components/Jodetail/JobPickExecution.tsx @@ -20,7 +20,7 @@ import { Modal, } from "@mui/material"; import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider'; -import { useCallback, useEffect, useState, useRef, useMemo } from "react"; +import { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/navigation"; import { @@ -68,6 +68,129 @@ interface Props { filterArgs: Record; } +// Manual Lot Confirmation Modal Component +const ManualLotConfirmationModal: React.FC<{ + open: boolean; + onClose: () => void; + onConfirm: (expectedLotNo: string, scannedLotNo: string) => void; + expectedLot: { + lotNo: string; + itemCode: string; + itemName: string; + } | null; + scannedLot: { + lotNo: string; + itemCode: string; + itemName: string; + } | null; + isLoading?: boolean; +}> = ({ open, onClose, onConfirm, expectedLot, scannedLot, isLoading = false }) => { + const { t } = useTranslation("jo"); + const [expectedLotInput, setExpectedLotInput] = useState(''); + const [scannedLotInput, setScannedLotInput] = useState(''); + const [error, setError] = useState(''); + + // 当模态框打开时,预填充输入框 + useEffect(() => { + if (open) { + setExpectedLotInput(expectedLot?.lotNo || ''); + setScannedLotInput(scannedLot?.lotNo || ''); + setError(''); + } + }, [open, expectedLot, scannedLot]); + + const handleConfirm = () => { + if (!expectedLotInput.trim() || !scannedLotInput.trim()) { + setError(t("Please enter both expected and scanned lot numbers.")); + return; + } + + if (expectedLotInput.trim() === scannedLotInput.trim()) { + setError(t("Expected and scanned lot numbers cannot be the same.")); + return; + } + + onConfirm(expectedLotInput.trim(), scannedLotInput.trim()); + }; + + return ( + + + + {t("Manual Lot Confirmation")} + + + + + {t("Expected Lot Number")}: + + { + setExpectedLotInput(e.target.value); + setError(''); + }} + placeholder={expectedLot?.lotNo || t("Enter expected lot number")} + sx={{ mb: 2 }} + error={!!error && !expectedLotInput.trim()} + /> + + + + + {t("Scanned Lot Number")}: + + { + setScannedLotInput(e.target.value); + setError(''); + }} + placeholder={scannedLot?.lotNo || t("Enter scanned lot number")} + sx={{ mb: 2 }} + error={!!error && !scannedLotInput.trim()} + /> + + + {error && ( + + + {error} + + + )} + + + + + + + + ); +}; + // QR Code Modal Component (from GoodPickExecution) const QrCodeModal: React.FC<{ open: boolean; @@ -376,9 +499,28 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { // Add these missing state variables 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); + + // 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); + + // Add Manual Lot Confirmation Modal state + const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false); @@ -434,6 +576,121 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { const originalCombinedData = useMemo(() => { return allLotsFromData; }, [allLotsFromData]); + + // 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(); + // 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) { + if (!byItemCode.has(lot.itemCode)) { + byItemCode.set(lot.itemCode, []); + } + byItemCode.get(lot.itemCode)!.push(lot); + } + + if (lot.lotId) { + byLotId.set(lot.lotId, lot); + } + + if (lot.lotNo) { + if (!byLotNo.has(lot.lotNo)) { + byLotNo.set(lot.lotNo, []); + } + byLotNo.get(lot.lotNo)!.push(lot); + } + + if (lot.stockInLineId) { + if (!byStockInLineId.has(lot.stockInLineId)) { + byStockInLineId.set(lot.stockInLineId, []); + } + byStockInLineId.get(lot.stockInLineId)!.push(lot); + } + } + + 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]); + + // 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; + } + }, []); // 修改:加载未分配的 Job Order 订单 const loadUnassignedOrders = useCallback(async () => { setIsLoadingUnassigned(true); @@ -595,6 +852,20 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { loadUnassignedOrders(); } }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]); + + // Auto-start scanner only once on mount + const scannerInitializedRef = useRef(false); + + // 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(() => { @@ -739,105 +1010,274 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { } }, [combinedLotData, fetchJobOrderData]); 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 }); - setExpectedLotData(expectedLot); - setScannedLotData(scannedLot); - setLotConfirmationOpen(true); - }, []); + + // ✅ 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) { + 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(); + + fetchStockInLineInfoCached(stockInLineId) + .then((stockInLineInfo) => { + const fetchTime = performance.now() - fetchStartTime; + console.log(`⏱️ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached time: ${fetchTime.toFixed(2)}ms (${(fetchTime / 1000).toFixed(3)}s)`); + + 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 (${(totalTime / 1000).toFixed(3)}s)`); + }) + .catch((error) => { + 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 (${(totalTime / 1000).toFixed(3)}s)`); + } + }, [fetchStockInLineInfoCached]); // Add handleLotConfirmation function const handleLotConfirmation = useCallback(async () => { if (!expectedLotData || !scannedLotData || !selectedLotForQr) return; setIsConfirmingLot(true); try { - let newLotLineId = scannedLotData?.inventoryLotLineId; - if (!newLotLineId && scannedLotData?.stockInLineId) { - const ld = await fetchLotDetail(scannedLotData.stockInLineId); - newLotLineId = ld.inventoryLotLineId; - } - if (!newLotLineId) { - console.error("No inventory lot line id for scanned lot"); - return; - } - - console.log("=== Lot Confirmation Debug ==="); - console.log("Selected Lot:", selectedLotForQr); - console.log("Pick Order Line ID:", selectedLotForQr.pickOrderLineId); - console.log("Stock Out Line ID:", selectedLotForQr.stockOutLineId); - console.log("Suggested Pick Lot ID:", selectedLotForQr.suggestedPickLotId); - console.log("Lot ID (fallback):", selectedLotForQr.lotId); - console.log("New Inventory Lot Line ID:", newLotLineId); - - // Call confirmLotSubstitution to update the suggested lot - const substitutionResult = await confirmLotSubstitution({ + const newLotNo = scannedLotData?.lotNo; + const newStockInLineId = scannedLotData?.stockInLineId; + + await confirmLotSubstitution({ pickOrderLineId: selectedLotForQr.pickOrderLineId, stockOutLineId: selectedLotForQr.stockOutLineId, - originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId, - newInventoryLotNo: scannedLotData.lotNo + originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId, + newInventoryLotNo: "", + newStockInLineId: newStockInLineId }); - - console.log(" Lot substitution result:", substitutionResult); - // Update stock out line status to 'checked' after substitution + setQrScanError(false); + setQrScanSuccess(false); + setQrScanInput(''); + + // ✅ 修复:在确认后重置扫描状态,避免重复处理 + resetScan(); + + setPickExecutionFormOpen(false); if(selectedLotForQr?.stockOutLineId){ - await updateStockOutLineStatus({ + const stockOutLineUpdate = await updateStockOutLineStatus({ id: selectedLotForQr.stockOutLineId, status: 'checked', qty: 0 }); - console.log(" Stock out line status updated to 'checked'"); } - // Close modal and clean up state BEFORE refreshing + // ✅ 修复:先关闭 modal 和清空状态,再刷新数据 setLotConfirmationOpen(false); setExpectedLotData(null); setScannedLotData(null); setSelectedLotForQr(null); - // Clear QR processing state but DON'T clear processedQrCodes yet - setQrScanError(false); - setQrScanSuccess(true); - setQrScanInput(''); - - // Set refreshing flag to prevent QR processing during refresh + // ✅ 修复:刷新数据前设置刷新标志,避免在刷新期间处理新的 QR code setIsRefreshingData(true); - - // Refresh data to show updated lot - console.log("🔄 Refreshing job order data..."); await fetchJobOrderData(); - console.log(" Lot substitution confirmed and data refreshed"); + setIsRefreshingData(false); + } catch (error) { + console.error("Error confirming lot substitution:", error); + } finally { + setIsConfirmingLot(false); + } + }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData, resetScan]); + + const handleManualLotConfirmation = useCallback(async (currentLotNo: string, newLotNo: string) => { + console.log(` Manual lot confirmation: Current=${currentLotNo}, New=${newLotNo}`); + + // 使用第一个输入框的 lot number 查找当前数据 + const currentLot = combinedLotData.find(lot => + lot.lotNo && lot.lotNo === currentLotNo + ); + + if (!currentLot) { + console.error(`❌ Current lot not found: ${currentLotNo}`); + alert(t("Current lot number not found. Please verify and try again.")); + return; + } + + if (!currentLot.stockOutLineId) { + console.error("❌ No stockOutLineId found for current lot"); + alert(t("No stock out line found for current lot. Please contact administrator.")); + return; + } + + setIsConfirmingLot(true); + + try { + // 调用 updateStockOutLineStatusByQRCodeAndLotNo API + // 第一个 lot 用于获取 pickOrderLineId, stockOutLineId, itemId + // 第二个 lot 作为 inventoryLotNo + const res = await updateStockOutLineStatusByQRCodeAndLotNo({ + pickOrderLineId: currentLot.pickOrderLineId, + inventoryLotNo: newLotNo, // 第二个输入框的值 + stockOutLineId: currentLot.stockOutLineId, + itemId: currentLot.itemId, + status: "checked", + }); - // Clear processed QR codes and flags immediately after refresh - // This allows new QR codes to be processed right away - setTimeout(() => { - console.log(" Clearing processed QR codes and resuming scan"); - setProcessedQrCodes(new Set()); - setLastProcessedQr(''); + console.log("📥 updateStockOutLineStatusByQRCodeAndLotNo result:", res); + + if (res.code === "checked" || res.code === "SUCCESS") { + // ✅ 更新本地状态 + const entity = res.entity as any; + + if (filteredLotData.length > 0) { + setFilteredLotData(prev => prev.map(lot => { + if (lot.stockOutLineId === currentLot.stockOutLineId && + lot.pickOrderLineId === currentLot.pickOrderLineId) { + return { + ...lot, + stockOutLineStatus: 'checked', + stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty, + }; + } + return lot; + })); + } + + console.log("✅ Lot substitution completed successfully"); + setQrScanSuccess(true); + setQrScanError(false); + + // 关闭手动输入模态框 + setManualLotConfirmationOpen(false); + + // 刷新数据 + await fetchJobOrderData(); + } else if (res.code === "LOT_NUMBER_MISMATCH") { + console.warn("⚠️ Backend reported LOT_NUMBER_MISMATCH:", res.message); + + // ✅ 打开 lot confirmation modal 而不是显示 alert + const expectedLotNo = currentLotNo; // 当前 lot 是期望的 + + // 查找新 lot 的信息(如果存在于 combinedLotData 中) + const newLot = combinedLotData.find(lot => + lot.lotNo && lot.lotNo === newLotNo + ); + + // 设置 expected lot data + setExpectedLotData({ + lotNo: expectedLotNo, + itemCode: currentLot.itemCode || '', + itemName: currentLot.itemName || '' + }); + + // 设置 scanned lot data + setScannedLotData({ + lotNo: newLotNo, + itemCode: newLot?.itemCode || currentLot.itemCode || '', + itemName: newLot?.itemName || currentLot.itemName || '', + inventoryLotLineId: newLot?.lotId || null, + stockInLineId: null // 手动输入时可能没有 stockInLineId + }); + + // 设置 selectedLotForQr 为当前 lot + setSelectedLotForQr(currentLot); + + // 关闭手动输入模态框 + setManualLotConfirmationOpen(false); + + // 打开 lot confirmation modal + setLotConfirmationOpen(true); + + setQrScanError(false); // 不显示错误,因为会打开确认模态框 setQrScanSuccess(false); - setIsRefreshingData(false); - }, 500); // Reduced from 3000ms to 500ms - just enough for UI update + } else if (res.code === "ITEM_MISMATCH") { + console.warn("⚠️ Backend reported ITEM_MISMATCH:", res.message); + alert(t("Item mismatch: {message}", { message: res.message || "" })); + setQrScanError(true); + setQrScanSuccess(false); + + // 关闭手动输入模态框 + setManualLotConfirmationOpen(false); + } else { + console.warn("⚠️ Unexpected response code:", res.code); + alert(t("Failed to update lot status. Response: {code}", { code: res.code })); + setQrScanError(true); + setQrScanSuccess(false); + + // 关闭手动输入模态框 + setManualLotConfirmationOpen(false); + } } catch (error) { - console.error("Error confirming lot substitution:", error); + console.error("❌ Error in manual lot confirmation:", error); + alert(t("Failed to confirm lot substitution. Please try again.")); setQrScanError(true); setQrScanSuccess(false); - // Clear refresh flag on error - setIsRefreshingData(false); + + // 关闭手动输入模态框 + setManualLotConfirmationOpen(false); } finally { setIsConfirmingLot(false); } - }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData]); + }, [combinedLotData, filteredLotData, fetchJobOrderData, t]); + const handleFastQrScan = useCallback(async (lotNo: string) => { + const startTime = performance.now(); + console.log(`⏱️ [FAST SCAN START] Lot: ${lotNo}`); + console.log(`⏰ Start time: ${new Date().toISOString()}`); + + // 从 combinedLotData 中找到对应的 lot + const findStartTime = performance.now(); const matchingLot = combinedLotData.find(lot => lot.lotNo && lot.lotNo === lotNo ); - + const findTime = performance.now() - findStartTime; + console.log(`⏱️ Find lot time: ${findTime.toFixed(2)}ms`); + if (!matchingLot || !matchingLot.stockOutLineId) { + const totalTime = performance.now() - startTime; console.warn(`⚠️ Fast scan: Lot ${lotNo} not found or no stockOutLineId`); + console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`); return; } - + try { + // ✅ 使用快速 API + const apiStartTime = performance.now(); const res = await updateStockOutLineStatusByQRCodeAndLotNo({ pickOrderLineId: matchingLot.pickOrderLineId, inventoryLotNo: lotNo, @@ -845,11 +1285,14 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { itemId: matchingLot.itemId, status: "checked", }); - + const apiTime = performance.now() - apiStartTime; + console.log(`⏱️ API call time: ${apiTime.toFixed(2)}ms`); + if (res.code === "checked" || res.code === "SUCCESS") { + // ✅ 只更新本地状态,不调用 fetchJobOrderData + const updateStartTime = performance.now(); const entity = res.entity as any; - // ✅ 更新 filteredLotData(如果存在)或刷新数据 if (filteredLotData.length > 0) { setFilteredLotData(prev => prev.map((lot: any) => { if (lot.stockOutLineId === matchingLot.stockOutLineId && @@ -863,184 +1306,281 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { return lot; })); } - - // ✅ 刷新 jobOrderData 以更新所有计算值 - await fetchJobOrderData(); - - console.log("✅ Fast scan completed successfully"); + const updateTime = performance.now() - updateStartTime; + console.log(`⏱️ State update time: ${updateTime.toFixed(2)}ms`); + + const totalTime = performance.now() - startTime; + console.log(`✅ [FAST SCAN END] Lot: ${lotNo}`); + console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); + console.log(`⏰ End time: ${new Date().toISOString()}`); + } else { + const totalTime = performance.now() - startTime; + console.warn(`⚠️ Fast scan failed for ${lotNo}:`, res.code); + console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`); } } catch (error) { + const totalTime = performance.now() - startTime; console.error(`❌ Fast scan error for ${lotNo}:`, error); + console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`); } - }, [combinedLotData, filteredLotData, fetchJobOrderData]); + }, [combinedLotData, filteredLotData]); const processOutsideQrCode = useCallback(async (latestQr: string) => { - // Don't process if confirmation modal is open - if (lotConfirmationOpen) { - console.log("⏸️ Confirmation modal is open, skipping QR processing"); - return; - } - - // 1) Parse JSON safely + const totalStartTime = performance.now(); + console.log(`⏱️ [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`); + console.log(`⏰ Start time: ${new Date().toISOString()}`); + + // ✅ 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; try { qrData = JSON.parse(latestQr); + 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 { - // Only use the new API when we have JSON with stockInLineId + itemId + const validationStartTime = performance.now(); if (!(qrData?.stockInLineId && qrData?.itemId)) { console.log("QR JSON missing required fields (itemId, stockInLineId)."); - setQrScanError(true); - setQrScanSuccess(false); - return; - } - - // Call new analyze-qr-code API - const analysis = await analyzeQrCode({ - itemId: qrData.itemId, - stockInLineId: qrData.stockInLineId - }); - - if (!analysis) { - console.error("analyzeQrCode returned no data"); - 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 { - itemId: analyzedItemId, - itemCode: analyzedItemCode, - itemName: analyzedItemName, - scanned, - } = analysis || {}; - - // 1) Find all lots for the same item from current expected list - const sameItemLotsInExpected = combinedLotData.filter((l: any) => - (l.itemId && analyzedItemId && l.itemId === analyzedItemId) || - (l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode) - ); - - if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) { - console.error("No item match in expected lots for scanned code"); - setQrScanError(true); - setQrScanSuccess(false); + const scannedItemId = qrData.itemId; + const scannedStockInLineId = qrData.stockInLineId; + + // ✅ 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`); - // Find the ACTIVE suggested lot (not rejected lots) - const activeSuggestedLots = sameItemLotsInExpected.filter((lot: any) => - lot.lotAvailability !== 'rejected' && - lot.stockOutLineStatus !== 'rejected' && - lot.stockOutLineStatus !== 'completed' - ); + // ✅ 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.warn("All lots for this item are rejected or completed"); - setQrScanError(true); - setQrScanSuccess(false); + console.error("No active suggested lots found for this item"); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + }); return; } - // 2) Check if scanned lot is exactly in active suggested lots - const exactLotMatch = activeSuggestedLots.find((l: any) => - (scanned?.inventoryLotLineId && l.lotId === scanned.inventoryLotLineId) || - (scanned?.lotNo && l.lotNo === scanned.lotNo) - ); - - if (exactLotMatch && scanned?.lotNo) { - // ✅ Case 1: 使用 updateStockOutLineStatusByQRCodeAndLotNo API(更快) - console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`); + // ✅ OPTIMIZATION: Direct Map lookup for stockInLineId match (O(1)) + const matchStartTime = performance.now(); + 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(`⏱️ [PERF] Find exact match time: ${matchTime.toFixed(2)}ms, found: ${exactMatch ? 'yes' : 'no'}`); + + if (exactMatch) { + // ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认 + console.log(`✅ Exact stockInLineId match found for lot: ${exactMatch.lotNo}`); - if (!exactLotMatch.stockOutLineId) { - console.warn("No stockOutLineId on exactLotMatch, cannot update status by QR."); - setQrScanError(true); - setQrScanSuccess(false); + if (!exactMatch.stockOutLineId) { + console.warn("No stockOutLineId on exactMatch, cannot update status by QR."); + 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: exactLotMatch.pickOrderLineId, - inventoryLotNo: scanned.lotNo, - stockOutLineId: exactLotMatch.stockOutLineId, - itemId: exactLotMatch.itemId, + pickOrderLineId: exactMatch.pickOrderLineId, + inventoryLotNo: exactMatch.lotNo, + stockOutLineId: exactMatch.stockOutLineId, + itemId: exactMatch.itemId, status: "checked", }); + 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") { - setQrScanError(false); - setQrScanSuccess(true); + const entity = res.entity as any; - // ✅ 刷新数据而不是直接更新 state + // ✅ Batch state updates using startTransition + const stateUpdateStartTime = performance.now(); + startTransition(() => { + setQrScanError(false); + setQrScanSuccess(true); + + // Update filteredLotData if it exists, otherwise update allLotsFromData via refresh + if (filteredLotData.length > 0) { + setFilteredLotData(prev => prev.map((lot: any) => { + 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(`⏱️ [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`); + + // Refresh data to ensure consistency await fetchJobOrderData(); + + 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, 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, data refreshed"); - } else if (res.code === "LOT_NUMBER_MISMATCH") { - console.warn("Backend reported LOT_NUMBER_MISMATCH:", res.message); - setQrScanError(true); - setQrScanSuccess(false); - } else if (res.code === "ITEM_MISMATCH") { - console.warn("Backend reported ITEM_MISMATCH:", res.message); - setQrScanError(true); - setQrScanSuccess(false); } 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; // ✅ 直接返回,不再调用 handleQrCodeSubmit + return; // ✅ 直接返回,不需要确认表单 } - - // Case 2: Item matches but lot number differs -> open confirmation modal + + // ✅ 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); - return; - } - - // Check if the expected lot is already the scanned lot (after substitution) - if (expectedLot.lotNo === scanned?.lotNo) { - console.log(`Lot already substituted, proceeding with ${scanned.lotNo}`); - await handleQrCodeSubmit(scanned.lotNo); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + }); return; } - - console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); + 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, - itemCode: analyzedItemCode || expectedLot.itemCode, - itemName: analyzedItemName || expectedLot.itemName + itemCode: expectedLot.itemCode, + itemName: expectedLot.itemName }, { - lotNo: scanned?.lotNo || '', - itemCode: analyzedItemCode || expectedLot.itemCode, - itemName: analyzedItemName || expectedLot.itemName, - inventoryLotLineId: scanned?.inventoryLotLineId, - stockInLineId: qrData.stockInLineId + lotNo: null, // 扫描的 lotNo 未知,需要从后端获取或显示为未知 + itemCode: expectedLot.itemCode, + itemName: expectedLot.itemName, + inventoryLotLineId: null, + 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) { - console.error("Error during analyzeQrCode flow:", error); - setQrScanError(true); - setQrScanSuccess(false); + 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); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + }); return; } - }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen, fetchJobOrderData]); + }, [lotDataIndexes, handleLotMismatch, processedQrCombinations, filteredLotData, fetchJobOrderData]); + + // Store processOutsideQrCode in ref for immediate access (update on every render) + processOutsideQrCodeRef.current = processOutsideQrCode; + + // Store resetScan in ref for immediate access (update on every render) + resetScanRef.current = resetScan; const handleManualInputSubmit = useCallback(() => { @@ -1093,26 +1633,218 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { useEffect(() => { - // Add isManualScanning check - if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData || lotConfirmationOpen) { + // 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()}`); - if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { - console.log("QR code already processed, skipping..."); + // ✅ 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`); - if (latestQr && latestQr !== lastProcessedQr) { - console.log(`🔍 Processing new QR code with enhanced validation: ${latestQr}`); + // 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 => new Set(prev).add(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`); + }); + } - processOutsideQrCode(latestQr); + // 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`); } - }, [qrValues, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode, combinedLotData, isManualScanning, lotConfirmationOpen]); + + return () => { + if (qrProcessingTimeoutRef.current) { + clearTimeout(qrProcessingTimeoutRef.current); + qrProcessingTimeoutRef.current = null; + } + }; + }, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]); const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { if (value === '' || value === null || value === undefined) { @@ -1869,25 +2601,36 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { + @@ -1947,6 +2690,18 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { isLoading={isConfirmingLot} /> )} + + {/* Manual Lot Confirmation Modal */} + { + setManualLotConfirmationOpen(false); + }} + onConfirm={handleManualLotConfirmation} + expectedLot={expectedLotData} + scannedLot={scannedLotData} + isLoading={isConfirmingLot} + /> {/* Pick Execution Form Modal */} {pickExecutionFormOpen && selectedLotForExecutionForm && ( (false); // PickTable 组件内容 const getStockAvailable = (line: JobOrderLineInfo) => { if (line.type?.toLowerCase() === "consumables" || line.type?.toLowerCase() === "nm") { - return null; + return line.stockQty || 0; } const inventory = inventoryData.find(inv => inv.itemCode === line.itemCode || inv.itemName === line.itemName diff --git a/src/components/PutAwayScan/PutAwayModal.tsx b/src/components/PutAwayScan/PutAwayModal.tsx index c242ca2..17e2f75 100644 --- a/src/components/PutAwayScan/PutAwayModal.tsx +++ b/src/components/PutAwayScan/PutAwayModal.tsx @@ -82,6 +82,10 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId const params = useSearchParams(); const [isOpenScanner, setIsOpenScanner] = useState(false); + const [firstWarehouseId, setFirstWarehouseId] = useState(null); + const [warehouseMismatchError, setWarehouseMismatchError] = useState(""); + + const [firstWarehouseInfo, setFirstWarehouseInfo] = useState<{name: string, code: string} | null>(null); const [itemDetail, setItemDetail] = useState(); const [totalPutAwayQty, setTotalPutAwayQty] = useState(0); @@ -89,7 +93,7 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId undefined, ); - const [putQty, setPutQty] = useState(itemDetail?.demandQty ?? 0); + const [putQty, setPutQty] = useState(itemDetail?.acceptedQty ?? 0); const [verified, setVerified] = useState(false); const [qtyError, setQtyError] = useState(""); @@ -108,7 +112,7 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId productionDate: itemDetail?.productionDate ? arrayToDateString(itemDetail?.productionDate, "input") : undefined, expiryDate: itemDetail?.expiryDate ? arrayToDateString(itemDetail?.expiryDate, "input") : undefined, receiptDate: itemDetail?.receiptDate ? arrayToDateString(itemDetail?.receiptDate, "input") : undefined, - // acceptQty: itemDetail.demandQty?? itemDetail.acceptedQty, + acceptQty: itemDetail?.acceptedQty ?? 0, defaultWarehouseId: itemDetail?.defaultWarehouseId ?? 1, } as ModalFormInput ) @@ -159,21 +163,37 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId console.log("%c Scanning started ", "color:cyan"); }; useEffect(() => { - if (warehouseId > 0) { // Scanned Warehouse + if (warehouseId > 0 && firstWarehouseId !== null) { + if (warehouseId !== firstWarehouseId) { + const firstWh = warehouse.find((w) => w.id == firstWarehouseId); + const scannedWh = warehouse.find((w) => w.id == warehouseId); + setWarehouseMismatchError("倉庫不匹配!必須使用首次上架的倉庫"); + setVerified(false); + } else { + setWarehouseMismatchError(""); + if (scanner.isScanning) { + setIsOpenScanner(false); + setVerified(true); + msg("貨倉掃瞄成功!"); + scanner.resetScan(); + } + } + } else if (warehouseId > 0 && firstWarehouseId === null) { + // First put away - no validation needed if (scanner.isScanning) { setIsOpenScanner(false); setVerified(true); msg("貨倉掃瞄成功!"); scanner.resetScan(); - console.log("%c Scanner reset", "color:cyan"); } } - }, [warehouseId]) + }, [warehouseId, firstWarehouseId]) const warehouseDisplay = useMemo(() => { + const targetWarehouseId = firstWarehouseId || warehouseId || 1; const wh = warehouse.find((w) => w.id == warehouseId) ?? warehouse.find((w) => w.id == 1); return <>{wh?.name}
[{wh?.code}]; - }, [warehouse, warehouseId, verified]); + }, [warehouse, warehouseId, firstWarehouseId,verified]); // useEffect(() => { // Restart scanner for changing warehouse // if (warehouseId > 0) { @@ -189,7 +209,25 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId ...defaultNewValue }) const total = itemDetail.putAwayLines?.reduce((sum, p) => sum + p.qty, 0) ?? 0; - setPutQty(itemDetail?.demandQty - total); + setPutQty(itemDetail?.acceptedQty - total); + + // ✅ Get first warehouse from existing put away lines + const firstPutAwayLine = itemDetail.putAwayLines?.[0]; + if (firstPutAwayLine?.warehouseId) { + setFirstWarehouseId(firstPutAwayLine.warehouseId); + // ✅ Store first warehouse info for display + const firstWh = warehouse.find((w) => w.id == firstPutAwayLine.warehouseId); + if (firstWh) { + setFirstWarehouseInfo({ + name: firstWh.name || "", + code: firstWh.code || "" + }); + } + } else { + setFirstWarehouseId(null); + setFirstWarehouseInfo(null); + } + console.log("%c Loaded data:", "color:lime", defaultNewValue); } else { switch (itemDetail.status) { @@ -236,10 +274,15 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId if (!Number.isInteger(qty)) { setQtyError(t("value must be integer")); } - if (qty > itemDetail?.demandQty!! - totalPutAwayQty) { + //if (qty > itemDetail?.demandQty!! - totalPutAwayQty) { + //setQtyError(`${t("putQty must not greater than")} ${ + // itemDetail?.demandQty!! - totalPutAwayQty}` ); + //} + if (qty > itemDetail?.acceptedQty!! - totalPutAwayQty) { setQtyError(`${t("putQty must not greater than")} ${ - itemDetail?.demandQty!! - totalPutAwayQty}` ); - } else + itemDetail?.acceptedQty!! - totalPutAwayQty}` ); + } + else // if (qty > itemDetail?.acceptedQty!!) { // setQtyError(`${t("putQty must not greater than")} ${ // itemDetail?.acceptedQty}` ); @@ -260,6 +303,10 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId // qty: acceptQty; // } try { + if (firstWarehouseId !== null && warehouseId !== firstWarehouseId) { + setWarehouseMismatchError("倉庫不匹配!必須使用首次上架的倉庫"); + return; + } const args = { // ...itemDetail, id: itemDetail?.id, @@ -267,7 +314,7 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId purchaseOrderLineId: itemDetail?.purchaseOrderLineId, itemId: itemDetail?.itemId, acceptedQty: itemDetail?.acceptedQty, - acceptQty: itemDetail?.demandQty, + acceptQty: itemDetail?.acceptedQty, status: "received", // purchaseOrderId: parseInt(params.get("id")!), // purchaseOrderLineId: itemDetail?.purchaseOrderLineId, @@ -329,7 +376,7 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId console.log(e); } }, - [t, itemDetail, putQty, warehouseId], + [t, itemDetail, putQty, warehouseId, firstWarehouseId], ); return ( @@ -419,7 +466,9 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId }} noWrap > - 請掃瞄倉庫二維碼 + {warehouseMismatchError || (firstWarehouseId !== null && warehouseId > 0 && warehouseId !== firstWarehouseId) + ? "倉庫不匹配!請掃瞄首次上架的倉庫" + : "請掃瞄倉庫二維碼"} ) @@ -480,8 +529,9 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId lineHeight: "1.1", }, }} - defaultValue={itemDetail?.demandQty!! - totalPutAwayQty} + // defaultValue={itemDetail?.demandQty!! - totalPutAwayQty} // defaultValue={itemDetail.demandQty} + defaultValue={itemDetail?.acceptedQty!! - totalPutAwayQty} onChange={(e) => { const value = e.target.value; validateQty(Number(value));