"use client"; import { Box, Button, Stack, TextField, Typography, Alert, CircularProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Checkbox, TablePagination, Modal, Chip, } from "@mui/material"; import dayjs from "dayjs"; import TestQrCodeProvider from "../QrCodeScannerProvider/TestQrCodeProvider"; import { fetchLotDetail } from "@/app/api/inventory/actions"; import React, { useCallback, useEffect, useState, useRef, useMemo, startTransition, } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/navigation"; import { updateStockOutLineStatus, createStockOutLine, updateStockOutLine, recordPickExecutionIssue, fetchFGPickOrders, // Add this import FGPickOrderResponse, stockReponse, PickExecutionIssueData, checkPickOrderCompletion, fetchAllPickOrderLotsHierarchical, PickOrderCompletionResponse, checkAndCompletePickOrderByConsoCode, updateSuggestedLotLineId, updateStockOutLineStatusByQRCodeAndLotNo, confirmLotSubstitution, fetchDoPickOrderDetail, // 必须添加 DoPickOrderDetail, // 必须添加 fetchFGPickOrdersByUserId, batchQrSubmit, batchSubmitList, // 添加:导入 batchSubmitList batchSubmitListRequest, // 添加:导入类型 batchSubmitListLineRequest, batchScan, BatchScanRequest, BatchScanLineRequest, } from "@/app/api/pickOrder/actions"; import FGPickOrderInfoCard from "./FGPickOrderInfoCard"; import LotConfirmationModal from "./LotConfirmationModal"; //import { fetchItem } from "@/app/api/settings/item"; import { updateInventoryLotLineStatus, analyzeQrCode, } from "@/app/api/inventory/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; import { FormProvider, useForm } from "react-hook-form"; import SearchBox, { Criterion } from "../SearchBox"; import { CreateStockOutLine } from "@/app/api/pickOrder/actions"; import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions"; import QrCodeIcon from "@mui/icons-material/QrCode"; import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; import GoodPickExecutionForm from "./GoodPickExecutionForm"; import FGPickOrderCard from "./FGPickOrderCard"; import LinearProgressWithLabel from "../common/LinearProgressWithLabel"; import ScanStatusAlert from "../common/ScanStatusAlert"; import { translateLotSubstitutionFailure } from "./lotSubstitutionMessage"; import LotLabelPrintModal from "@/components/InventorySearch/LotLabelPrintModal"; interface Props { filterArgs: Record; onSwitchToRecordTab?: () => void; onRefreshReleasedOrderCount?: () => void; } type LotConfirmRunContext = { expectedLotData: { lotNo: string | null; itemCode?: string; itemName?: string; }; scannedLotData: { lotNo: string | null; itemCode?: string; itemName?: string; stockInLineId: number; // 必須有,API 用 inventoryLotLineId?: number | null; }; selectedLotForQr: any; // 與現在一樣:含 pickOrderLineId, stockOutLineId, suggestedPickLotId, itemId… }; /** 同物料多行时,优先对「有建议批次号」的行做替换,避免误选「无批次/不足」行 */ function pickExpectedLotForSubstitution( activeSuggestedLots: any[], ): any | null { if (!activeSuggestedLots?.length) return null; const withLotNo = activeSuggestedLots.filter( (l) => l.lotNo != null && String(l.lotNo).trim() !== "", ); if (withLotNo.length === 1) return withLotNo[0]; if (withLotNo.length > 1) { const pending = withLotNo.find( (l) => (l.stockOutLineStatus || "").toLowerCase() === "pending", ); return pending || withLotNo[0]; } return activeSuggestedLots[0]; } // QR Code Modal Component (from LotTable) const QrCodeModal: React.FC<{ open: boolean; onClose: () => void; lot: any | null; onQrCodeSubmit: (lotNo: string) => void; combinedLotData: any[]; // Add this prop lotConfirmationOpen: boolean; }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData, lotConfirmationOpen = false, }) => { const { t } = useTranslation("pickOrder"); const { values: qrValues, isScanning, startScan, stopScan, resetScan, } = useQrCodeScannerContext(); const [manualInput, setManualInput] = useState(""); const [manualInputSubmitted, setManualInputSubmitted] = useState(false); const [manualInputError, setManualInputError] = useState(false); const [isProcessingQr, setIsProcessingQr] = useState(false); const [qrScanFailed, setQrScanFailed] = useState(false); const [qrScanSuccess, setQrScanSuccess] = useState(false); const [processedQrCodes, setProcessedQrCodes] = useState>( new Set(), ); const [scannedQrResult, setScannedQrResult] = useState(""); const [fgPickOrder, setFgPickOrder] = useState( null, ); const fetchingRef = useRef>(new Set()); useEffect(() => { // ✅ Don't process if modal is not open if (!open) { return; } // ✅ Don't process if lot confirmation modal is open if (lotConfirmationOpen) { console.log( "Lot confirmation modal is open, skipping QrCodeModal processing...", ); return; } if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) { const latestQr = qrValues[qrValues.length - 1]; if (processedQrCodes.has(latestQr)) { console.log("QR code already processed, skipping..."); return; } try { const qrData = JSON.parse(latestQr); if (qrData.stockInLineId && qrData.itemId) { // ✅ Check if we're already fetching this stockInLineId if (fetchingRef.current.has(qrData.stockInLineId)) { console.log( ` [QR MODAL] Already fetching stockInLineId: ${qrData.stockInLineId}, skipping duplicate call`, ); return; } setProcessedQrCodes((prev) => new Set(prev).add(latestQr)); setIsProcessingQr(true); setQrScanFailed(false); // ✅ Mark as fetching fetchingRef.current.add(qrData.stockInLineId); const fetchStartTime = performance.now(); console.log( ` [QR MODAL] Starting fetchStockInLineInfo for stockInLineId: ${qrData.stockInLineId}`, ); fetchStockInLineInfo(qrData.stockInLineId) .then((stockInLineInfo) => { // ✅ Remove from fetching set fetchingRef.current.delete(qrData.stockInLineId); // ✅ Check again if modal is still open and lot confirmation is not open if (!open || lotConfirmationOpen) { console.log("Modal state changed, skipping result processing"); return; } const fetchTime = performance.now() - fetchStartTime; console.log( ` [QR MODAL] fetchStockInLineInfo time: ${fetchTime.toFixed( 2, )}ms (${(fetchTime / 1000).toFixed(3)}s)`, ); console.log("Stock in line info:", stockInLineInfo); setScannedQrResult(stockInLineInfo.lotNo || "Unknown lot number"); if (stockInLineInfo.lotNo === lot.lotNo) { console.log(` QR Code verified for lot: ${lot.lotNo}`); setQrScanSuccess(true); onQrCodeSubmit(lot.lotNo); // onClose(); //resetScan(); } else { console.log( ` QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`, ); setQrScanFailed(true); setManualInputError(true); setManualInputSubmitted(true); } }) .catch((error) => { // ✅ Remove from fetching set fetchingRef.current.delete(qrData.stockInLineId); // ✅ Check again if modal is still open if (!open || lotConfirmationOpen) { console.log("Modal state changed, skipping error handling"); return; } const fetchTime = performance.now() - fetchStartTime; console.error( `❌ [QR MODAL] fetchStockInLineInfo failed after ${fetchTime.toFixed( 2, )}ms:`, error, ); setScannedQrResult("Error fetching data"); setQrScanFailed(true); setManualInputError(true); setManualInputSubmitted(true); }) .finally(() => { setIsProcessingQr(false); }); } else { const qrContent = latestQr.replace(/[{}]/g, ""); setScannedQrResult(qrContent); if (qrContent === lot.lotNo) { setQrScanSuccess(true); onQrCodeSubmit(lot.lotNo); onClose(); resetScan(); } else { setQrScanFailed(true); setManualInputError(true); setManualInputSubmitted(true); } } } catch (error) { console.log("QR code is not JSON format, trying direct comparison"); const qrContent = latestQr.replace(/[{}]/g, ""); setScannedQrResult(qrContent); if (qrContent === lot.lotNo) { setQrScanSuccess(true); onQrCodeSubmit(lot.lotNo); onClose(); resetScan(); } else { setQrScanFailed(true); setManualInputError(true); setManualInputSubmitted(true); } } } }, [ qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes, lotConfirmationOpen, open, ]); // Clear states when modal opens useEffect(() => { if (open) { setManualInput(""); setManualInputSubmitted(false); setManualInputError(false); setIsProcessingQr(false); setQrScanFailed(false); setQrScanSuccess(false); setScannedQrResult(""); setProcessedQrCodes(new Set()); } }, [open]); useEffect(() => { if (lot) { setManualInput(""); setManualInputSubmitted(false); setManualInputError(false); setIsProcessingQr(false); setQrScanFailed(false); setQrScanSuccess(false); setScannedQrResult(""); setProcessedQrCodes(new Set()); } }, [lot]); // Auto-submit manual input when it matches useEffect(() => { if ( manualInput.trim() === lot?.lotNo && manualInput.trim() !== "" && !qrScanFailed && !qrScanSuccess ) { console.log(" Auto-submitting manual input:", manualInput.trim()); const timer = setTimeout(() => { setQrScanSuccess(true); onQrCodeSubmit(lot.lotNo); onClose(); setManualInput(""); setManualInputError(false); setManualInputSubmitted(false); }, 200); return () => clearTimeout(timer); } }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]); const handleManualSubmit = () => { if (manualInput.trim() === lot?.lotNo) { setQrScanSuccess(true); onQrCodeSubmit(lot.lotNo); onClose(); setManualInput(""); } else { setQrScanFailed(true); setManualInputError(true); setManualInputSubmitted(true); } }; useEffect(() => { if (open) { startScan(); } }, [open, startScan]); return ( {t("QR Code Scan for Lot")}: {lot?.lotNo} {isProcessingQr && ( {t("Processing QR code...")} )} {t("Manual Input")}: { setManualInput(e.target.value); if (qrScanFailed || manualInputError) { setQrScanFailed(false); setManualInputError(false); setManualInputSubmitted(false); } }} sx={{ mb: 1 }} error={manualInputSubmitted && manualInputError} helperText={ manualInputSubmitted && manualInputError ? `${t( "The input is not the same as the expected lot number.", )}` : "" } /> {qrValues.length > 0 && ( {t("QR Scan Result:")} {scannedQrResult} {qrScanSuccess && ( {t("Verified successfully!")} )} )} ); }; 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("pickOrder"); 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} )} ); }; /** 過期批號(未換有效批前):與 noLot 類似——單筆/批量預設提交量為 0,除非 Issue 改數 */ function isLotAvailabilityExpired(lot: any): boolean { return String(lot?.lotAvailability || "").toLowerCase() === "expired"; } /** inventory_lot_line.status = unavailable(API 可能用 lotAvailability 或 lotStatus) */ function isInventoryLotLineUnavailable(lot: any): boolean { if (!lot) return false; if (lot.lotAvailability === "status_unavailable") return true; return String(lot.lotStatus || "").toLowerCase() === "unavailable"; } /** Issue「改數」未寫入 SOL,刷新/換頁後需靠 session 還原,否則 Qty will submit 會回到 req */ const FG_ISSUE_PICKED_KEY = (doPickOrderId: number) => `fpsms-fg-issuePickedQty:${doPickOrderId}`; function loadIssuePickedMap(doPickOrderId: number): Record { if (typeof window === "undefined" || !doPickOrderId) return {}; try { const raw = sessionStorage.getItem(FG_ISSUE_PICKED_KEY(doPickOrderId)); if (!raw) return {}; const parsed = JSON.parse(raw) as Record; const out: Record = {}; Object.entries(parsed).forEach(([k, v]) => { const n = Number(v); if (!Number.isNaN(n)) out[Number(k)] = n; }); return out; } catch { return {}; } } function saveIssuePickedMap( doPickOrderId: number, map: Record, ) { if (typeof window === "undefined" || !doPickOrderId) return; try { sessionStorage.setItem( FG_ISSUE_PICKED_KEY(doPickOrderId), JSON.stringify(map), ); } catch { // quota / private mode } } const PickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab, onRefreshReleasedOrderCount, }) => { const { t } = useTranslation("pickOrder"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const [doPickOrderDetail, setDoPickOrderDetail] = useState(null); const [selectedPickOrderId, setSelectedPickOrderId] = useState( null, ); const [pickOrderSwitching, setPickOrderSwitching] = useState(false); const currentUserId = session?.id ? parseInt(session.id) : undefined; const [allLotsCompleted, setAllLotsCompleted] = useState(false); const [combinedLotData, setCombinedLotData] = useState([]); const [combinedDataLoading, setCombinedDataLoading] = useState(false); const [originalCombinedData, setOriginalCombinedData] = useState([]); // issue form 里填的 actualPickQty(用于 batch submit 只提交实际拣到数量,而不是补拣到 required) const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState< Record >({}); const applyLocalStockOutLineUpdate = useCallback( (stockOutLineId: number, status: string, actualPickQty?: number) => { setCombinedLotData((prev) => prev.map((lot) => { if (Number(lot.stockOutLineId) !== Number(stockOutLineId)) return lot; return { ...lot, stockOutLineStatus: status, ...(typeof actualPickQty === "number" ? { actualPickQty, stockOutLineQty: actualPickQty } : {}), }; }), ); }, [], ); // 防止重复点击(Submit / Just Completed / Issue) const [actionBusyBySolId, setActionBusyBySolId] = useState< Record >({}); const { values: qrValues, isScanning, startScan, stopScan, resetScan, } = useQrCodeScannerContext(); const [qrScanInput, setQrScanInput] = useState(""); const [qrScanError, setQrScanError] = useState(false); const [qrScanErrorMsg, setQrScanErrorMsg] = useState(""); const [qrScanSuccess, setQrScanSuccess] = useState(false); const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false); const [lotLabelPrintModalOpen, setLotLabelPrintModalOpen] = useState(false); const [lotLabelPrintInitialPayload, setLotLabelPrintInitialPayload] = useState<{ itemId: number; stockInLineId: number; } | null>(null); const [lotLabelPrintReminderText, setLotLabelPrintReminderText] = useState< string | null >(null); const [pickQtyData, setPickQtyData] = useState>({}); const [searchQuery, setSearchQuery] = useState>({}); const [paginationController, setPaginationController] = useState({ pageNum: 0, pageSize: -1, }); const [usernameList, setUsernameList] = useState([]); const initializationRef = useRef(false); const autoAssignRef = useRef(false); const formProps = useForm(); const errors = formProps.formState.errors; // QR scanner states (always-on, no modal) const [selectedLotForQr, setSelectedLotForQr] = useState(null); const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); const [lotConfirmationError, setLotConfirmationError] = useState< string | null >(null); /** QR 静默换批失败时显示在对应行的 Lot# 列,key = stockOutLineId */ const [lotSwitchFailByStockOutLineId, setLotSwitchFailByStockOutLineId] = useState>({}); const [expectedLotData, setExpectedLotData] = useState(null); const [scannedLotData, setScannedLotData] = useState(null); const [isConfirmingLot, setIsConfirmingLot] = useState(false); // Add GoodPickExecutionForm states const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState(null); const [fgPickOrders, setFgPickOrders] = useState([]); const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false); const lotFloorPrefixFilter = useMemo(() => { const storeId = String(fgPickOrders?.[0]?.storeId ?? "") .trim() .toUpperCase() .replace(/\s/g, ""); // e.g. "2/F" -> "2F-", "4/F" -> "4F-" const floorKey = storeId.replace(/\//g, ""); return floorKey ? `${floorKey}-` : ""; }, [fgPickOrders]); const defaultLabelPrinterName = useMemo(() => { const storeId = String(fgPickOrders?.[0]?.storeId ?? "") .trim() .toUpperCase() .replace(/\s/g, ""); const floorKey = storeId.replace(/\//g, ""); if (floorKey === "2F") return "Label機 2F A+B"; if (floorKey === "4F") return "Label機 4F 乾貨 C, D"; return undefined; }, [fgPickOrders]); // 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< Map> >(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< Map >(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, qrScanCountAtInvoke?: number) => Promise) | null >(null); const resetScanRef = useRef<(() => void) | null>(null); const lotConfirmOpenedQrCountRef = useRef(0); const lotConfirmLastQrRef = useRef(""); const lotConfirmSkipNextScanRef = useRef(false); const lotConfirmOpenedAtRef = useRef(0); const handleLotConfirmationRef = useRef< | (( overrideScannedLot?: any, runContext?: LotConfirmRunContext, ) => Promise) | null >(null); // Handle QR code button click const handleQrCodeClick = (pickOrderId: number) => { console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); // TODO: Implement QR code functionality }; const progress = useMemo(() => { if (combinedLotData.length === 0) { return { completed: 0, total: 0 }; } // 與 allItemsReady 一致:noLot / 過期 / unavailable 的 pending 也算「已面對該行」可收尾 const nonPendingCount = combinedLotData.filter((lot) => { const status = lot.stockOutLineStatus?.toLowerCase(); if (status !== "pending") return true; if ( lot.noLot === true || isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot) ) return true; return false; }).length; return { completed: nonPendingCount, total: 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; } }, [], ); const handleLotMismatch = useCallback( (fullExpectedLotRow: any, scannedLot: any, qrScanCountAtOpen?: number) => { const mismatchStartTime = performance.now(); console.log(` [HANDLE LOT MISMATCH START]`); console.log(` Start time: ${new Date().toISOString()}`); console.log("Lot mismatch detected:", { fullExpectedLotRow, scannedLot }); lotConfirmOpenedQrCountRef.current = typeof qrScanCountAtOpen === "number" ? qrScanCountAtOpen : 1; // ✅ Use setTimeout to avoid flushSync warning - schedule state + silent substitution in next tick const setTimeoutStartTime = performance.now(); console.time("setLotMismatchStateAndSubstitute"); setTimeout(() => { const setStateStartTime = performance.now(); const expectedForDisplay = { lotNo: fullExpectedLotRow.lotNo, itemCode: fullExpectedLotRow.itemCode, itemName: fullExpectedLotRow.itemName, }; const scannedMerged = { ...scannedLot, lotNo: scannedLot.lotNo || null, }; setExpectedLotData(expectedForDisplay); setScannedLotData(scannedMerged); setSelectedLotForQr(fullExpectedLotRow); // The QR that triggered mismatch must NOT be treated as confirmation rescan. lotConfirmSkipNextScanRef.current = true; lotConfirmOpenedAtRef.current = Date.now(); const sid = Number(scannedLot.stockInLineId); if (!Number.isFinite(sid)) { console.error( ` [HANDLE LOT MISMATCH] Invalid stockInLineId for substitution: ${scannedLot.stockInLineId}`, ); const errMsg = t( "Lot switch failed; pick line was not marked as checked.", ); const rowSol = Number(fullExpectedLotRow.stockOutLineId); if (Number.isFinite(rowSol)) { setLotSwitchFailByStockOutLineId((prev) => ({ ...prev, [rowSol]: errMsg, })); } setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg(errMsg); const setStateTime = performance.now() - setStateStartTime; console.timeEnd("setLotMismatchStateAndSubstitute"); console.log( ` [HANDLE LOT MISMATCH] Lot switch failed (invalid stockInLineId), setState time: ${setStateTime.toFixed( 2, )}ms`, ); return; } const runContext: LotConfirmRunContext = { expectedLotData: expectedForDisplay, scannedLotData: { ...scannedMerged, stockInLineId: sid, itemCode: scannedMerged.itemCode ?? fullExpectedLotRow.itemCode, itemName: scannedMerged.itemName ?? fullExpectedLotRow.itemName, inventoryLotLineId: scannedLot.inventoryLotLineId ?? scannedLot.lotId ?? null, }, selectedLotForQr: fullExpectedLotRow, }; void handleLotConfirmationRef.current?.(undefined, runContext); const setStateTime = performance.now() - setStateStartTime; console.timeEnd("setLotMismatchStateAndSubstitute"); console.log( ` [HANDLE LOT MISMATCH] Silent lot substitution scheduled (setState time: ${setStateTime.toFixed( 2, )}ms)`, ); }, 0); const setTimeoutTime = performance.now() - setTimeoutStartTime; console.log( ` [PERF] setTimeout scheduling time: ${setTimeoutTime.toFixed(2)}ms`, ); // ✅ 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, t], ); const checkAllLotsCompleted = useCallback((lotData: any[]) => { if (lotData.length === 0) { setAllLotsCompleted(false); return false; } // Filter out rejected lots const nonRejectedLots = lotData.filter( (lot) => lot.lotAvailability !== "rejected" && lot.stockOutLineStatus !== "rejected", ); if (nonRejectedLots.length === 0) { setAllLotsCompleted(false); return false; } // Check if all non-rejected lots are completed const allCompleted = nonRejectedLots.every( (lot) => lot.stockOutLineStatus === "completed", ); setAllLotsCompleted(allCompleted); return allCompleted; }, []); // 在 fetchAllCombinedLotData 函数中(约 446-684 行) const fetchAllCombinedLotData = useCallback( async (userId?: number, pickOrderIdOverride?: number) => { setCombinedDataLoading(true); try { const userIdToUse = userId || currentUserId; console.log( " fetchAllCombinedLotData called with userId:", userIdToUse, ); if (!userIdToUse) { console.warn("⚠️ No userId available, skipping API call"); setCombinedLotData([]); setOriginalCombinedData([]); setAllLotsCompleted(false); setIssuePickedQtyBySolId({}); return; } // 获取新结构的层级数据 const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse); console.log(" Hierarchical data (new structure):", hierarchicalData); // 检查数据结构 if ( !hierarchicalData.fgInfo || !hierarchicalData.pickOrders || hierarchicalData.pickOrders.length === 0 ) { console.warn("⚠️ No FG info or pick orders found"); setCombinedLotData([]); setOriginalCombinedData([]); setAllLotsCompleted(false); setIssuePickedQtyBySolId({}); return; } // 使用合并后的 pick order 对象(现在只有一个对象,包含所有数据) const mergedPickOrder = hierarchicalData.pickOrders[0]; // 设置 FG info 到 fgPickOrders(用于显示 FG 信息卡片) // 修改第 478-509 行的 fgOrder 构建逻辑: const fgOrder: FGPickOrderResponse = { doPickOrderId: hierarchicalData.fgInfo.doPickOrderId, ticketNo: hierarchicalData.fgInfo.ticketNo, storeId: hierarchicalData.fgInfo.storeId, shopCode: hierarchicalData.fgInfo.shopCode, shopName: hierarchicalData.fgInfo.shopName, truckLanceCode: hierarchicalData.fgInfo.truckLanceCode, DepartureTime: hierarchicalData.fgInfo.departureTime, shopAddress: "", pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "", // 兼容字段(注意 consoCodes 是数组) pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0, pickOrderConsoCode: Array.isArray(mergedPickOrder.consoCodes) ? mergedPickOrder.consoCodes[0] || "" : "", pickOrderTargetDate: mergedPickOrder.targetDate || "", pickOrderStatus: mergedPickOrder.status || "", deliveryOrderId: mergedPickOrder.doOrderIds?.[0] || 0, deliveryNo: mergedPickOrder.deliveryOrderCodes?.[0] || "", deliveryDate: "", shopId: 0, shopPoNo: "", numberOfCartons: mergedPickOrder.pickOrderLines?.length || 0, qrCodeData: hierarchicalData.fgInfo.doPickOrderId, // 多个 pick orders 信息:全部保留为数组 numberOfPickOrders: mergedPickOrder.pickOrderIds?.length || 0, pickOrderIds: mergedPickOrder.pickOrderIds || [], pickOrderCodes: Array.isArray(mergedPickOrder.pickOrderCodes) ? mergedPickOrder.pickOrderCodes : [], deliveryOrderIds: mergedPickOrder.doOrderIds || [], deliveryNos: Array.isArray(mergedPickOrder.deliveryOrderCodes) ? mergedPickOrder.deliveryOrderCodes : [], lineCountsPerPickOrder: Array.isArray( mergedPickOrder.lineCountsPerPickOrder, ) ? mergedPickOrder.lineCountsPerPickOrder : [], }; setFgPickOrders([fgOrder]); console.log( " DEBUG fgOrder.lineCountsPerPickOrder:", fgOrder.lineCountsPerPickOrder, ); console.log(" DEBUG fgOrder.pickOrderCodes:", fgOrder.pickOrderCodes); console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); // 直接使用合并后的 pickOrderLines console.log("🎯 Displaying merged pick order lines"); // 将层级数据转换为平铺格式(用于表格显示) const flatLotData: any[] = []; // 2/F 與後端 store_id 一致時需按 itemOrder;避免 API 未走 2F 分支時畫面仍亂序 const doFloorKey = String(hierarchicalData.fgInfo.storeId ?? "") .trim() .toUpperCase() .replace(/\//g, "") .replace(/\s/g, ""); const pickOrderLinesForDisplay = doFloorKey === "2F" ? [...(mergedPickOrder.pickOrderLines || [])].sort( (a: any, b: any) => { const ao = a.itemOrder != null ? Number(a.itemOrder) : 999999; const bo = b.itemOrder != null ? Number(b.itemOrder) : 999999; if (ao !== bo) return ao - bo; return (Number(a.id) || 0) - (Number(b.id) || 0); }, ) : mergedPickOrder.pickOrderLines || []; pickOrderLinesForDisplay.forEach((line: any) => { // 用来记录这一行已经通过 lots 出现过的 lotId const lotIdSet = new Set(); /** 已由有批次建議分配的量(加總後與 pick_order_line.requiredQty 的差額 = 無批次列應顯示的數) */ let lotsAllocatedSumForLine = 0; // ✅ lots:按 lotId 去重并合并 requiredQty if (line.lots && line.lots.length > 0) { const lotMap = new Map(); line.lots.forEach((lot: any) => { const lotId = lot.id; if (lotMap.has(lotId)) { const existingLot = lotMap.get(lotId); existingLot.requiredQty = (existingLot.requiredQty || 0) + (lot.requiredQty || 0); } else { lotMap.set(lotId, { ...lot }); } }); lotMap.forEach((lot: any) => { lotsAllocatedSumForLine += Number(lot.requiredQty) || 0; if (lot.id != null) { lotIdSet.add(lot.id); } flatLotData.push({ pickOrderConsoCode: Array.isArray(mergedPickOrder.consoCodes) ? mergedPickOrder.consoCodes[0] || "" : "", pickOrderTargetDate: mergedPickOrder.targetDate, pickOrderStatus: mergedPickOrder.status, pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0, pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "", pickOrderLineId: line.id, pickOrderLineRequiredQty: line.requiredQty, pickOrderLineStatus: line.status, itemId: line.item.id, itemCode: line.item.code, itemName: line.item.name, uomDesc: line.item.uomDesc, uomShortDesc: line.item.uomShortDesc, lotId: lot.id, lotNo: lot.lotNo, expiryDate: lot.expiryDate, location: lot.location, stockUnit: lot.stockUnit, availableQty: lot.availableQty, requiredQty: lot.requiredQty, actualPickQty: lot.actualPickQty, inQty: lot.inQty, outQty: lot.outQty, holdQty: lot.holdQty, lotStatus: lot.lotStatus, lotAvailability: lot.lotAvailability, processingStatus: lot.processingStatus, suggestedPickLotId: lot.suggestedPickLotId, stockOutLineId: lot.stockOutLineId, stockOutLineStatus: lot.stockOutLineStatus, stockOutLineQty: lot.stockOutLineQty, stockInLineId: lot.stockInLineId, routerId: lot.router?.id, routerIndex: lot.router?.index, routerRoute: lot.router?.route, routerArea: lot.router?.area, noLot: false, }); }); } // ✅ stockouts:只保留“真正无批次 / 未在 lots 出现过”的行 if (line.stockouts && line.stockouts.length > 0) { line.stockouts.forEach((stockout: any) => { const hasLot = stockout.lotId != null; const lotAlreadyInLots = hasLot && lotIdSet.has(stockout.lotId as number); // 有批次 & 已经通过 lots 渲染过 → 跳过,避免一条变两行 if (!stockout.noLot && lotAlreadyInLots) { return; } // 只渲染: // - noLot === true 的 Null stock 行 // - 或者 lotId 在 lots 中不存在的特殊情况 flatLotData.push({ pickOrderConsoCode: Array.isArray(mergedPickOrder.consoCodes) ? mergedPickOrder.consoCodes[0] || "" : "", pickOrderTargetDate: mergedPickOrder.targetDate, pickOrderStatus: mergedPickOrder.status, pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0, pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "", pickOrderLineId: line.id, pickOrderLineRequiredQty: line.requiredQty, pickOrderLineStatus: line.status, itemId: line.item.id, itemCode: line.item.code, itemName: line.item.name, uomDesc: line.item.uomDesc, uomShortDesc: line.item.uomShortDesc, lotId: stockout.lotId || null, lotNo: stockout.lotNo || null, expiryDate: null, location: stockout.location || null, stockUnit: line.item.uomDesc, availableQty: stockout.availableQty || 0, // 無批次列對應 suggested_pick_lot 的缺口量(如 11),勿用整行 POL 需求(100)以免顯示成 89 / 100 requiredQty: stockout.noLot ? Math.max( 0, (Number(line.requiredQty) || 0) - lotsAllocatedSumForLine, ) : Number(line.requiredQty) || 0, actualPickQty: stockout.qty || 0, inQty: 0, outQty: 0, holdQty: 0, lotStatus: stockout.noLot ? "unavailable" : "available", lotAvailability: stockout.noLot ? "insufficient_stock" : "available", processingStatus: stockout.status || "pending", suggestedPickLotId: null, stockOutLineId: stockout.id || null, stockOutLineStatus: stockout.status || null, stockOutLineQty: stockout.qty || 0, routerId: null, routerIndex: stockout.noLot ? 999999 : null, routerRoute: null, routerArea: null, noLot: !!stockout.noLot, }); }); } }); console.log(" Transformed flat lot data:", flatLotData); console.log( " Total items (including null stock):", flatLotData.length, ); setCombinedLotData(flatLotData); setOriginalCombinedData(flatLotData); const doPid = hierarchicalData.fgInfo?.doPickOrderId; if (doPid) { setIssuePickedQtyBySolId(loadIssuePickedMap(doPid)); } checkAllLotsCompleted(flatLotData); } catch (error) { console.error(" Error fetching combined lot data:", error); setCombinedLotData([]); setOriginalCombinedData([]); setAllLotsCompleted(false); setIssuePickedQtyBySolId({}); } finally { setCombinedDataLoading(false); } }, [currentUserId, checkAllLotsCompleted], ); // 移除 selectedPickOrderId 依赖 // Add effect to check completion when lot data changes 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", }); console.log("📥 updateStockOutLineStatusByQRCodeAndLotNo result:", res); if (res.code === "checked" || res.code === "SUCCESS") { // ✅ 更新本地状态 const entity = res.entity as any; setCombinedLotData((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; }), ); setOriginalCombinedData((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 fetchAllCombinedLotData(); } else if (res.code === "LOT_NUMBER_MISMATCH") { console.warn("⚠️ Backend reported LOT_NUMBER_MISMATCH:", res.message); // ✅ 打开 lot confirmation modal 而不是显示 alert // 从响应消息中提取 expected lot number(如果可能) // 或者使用 currentLotNo 作为 expected lot 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); } 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 in manual lot confirmation:", error); alert(t("Failed to confirm lot substitution. Please try again.")); setQrScanError(true); setQrScanSuccess(false); // 关闭手动输入模态框 setManualLotConfirmationOpen(false); } finally { setIsConfirmingLot(false); } }, [combinedLotData, fetchAllCombinedLotData, t], ); useEffect(() => { if (combinedLotData.length > 0) { checkAllLotsCompleted(combinedLotData); } }, [combinedLotData, checkAllLotsCompleted]); // Add function to expose completion status to parent const getCompletionStatus = useCallback(() => { return allLotsCompleted; }, [allLotsCompleted]); // Expose completion status to parent component useEffect(() => { // Dispatch custom event with completion status const event = new CustomEvent("pickOrderCompletionStatus", { detail: { allLotsCompleted, tabIndex: 1, // 明确指定这是来自标签页 1 的事件 }, }); window.dispatchEvent(event); }, [allLotsCompleted]); const clearLotConfirmationState = useCallback( (clearProcessedRefs: boolean = false) => { setLotConfirmationOpen(false); setLotConfirmationError(null); setExpectedLotData(null); setScannedLotData(null); setSelectedLotForQr(null); lotConfirmLastQrRef.current = ""; lotConfirmSkipNextScanRef.current = false; lotConfirmOpenedAtRef.current = 0; if (clearProcessedRefs) { setTimeout(() => { lastProcessedQrRef.current = ""; processedQrCodesRef.current.clear(); console.log( ` [LOT CONFIRM MODAL] Cleared refs to allow reprocessing`, ); }, 100); } }, [], ); const parseQrPayload = useCallback( (rawQr: string): { itemId: number; stockInLineId: number } | null => { if (!rawQr) return null; if ( (rawQr.startsWith("{2fitest") || rawQr.startsWith("{2fittest")) && rawQr.endsWith("}") ) { let content = ""; if (rawQr.startsWith("{2fittest")) { content = rawQr.substring(9, rawQr.length - 1); } else { content = rawQr.substring(8, rawQr.length - 1); } const parts = content.split(","); if (parts.length === 2) { const itemId = parseInt(parts[0].trim(), 10); const stockInLineId = parseInt(parts[1].trim(), 10); if (!isNaN(itemId) && !isNaN(stockInLineId)) { return { itemId, stockInLineId }; } } return null; } try { const parsed = JSON.parse(rawQr); if (parsed?.itemId && parsed?.stockInLineId) { return { itemId: parsed.itemId, stockInLineId: parsed.stockInLineId }; } return null; } catch { return null; } }, [], ); const handleLotConfirmation = useCallback( async (overrideScannedLot?: any, runContext?: LotConfirmRunContext) => { const exp = runContext?.expectedLotData ?? expectedLotData; const scan = overrideScannedLot ?? runContext?.scannedLotData ?? scannedLotData; const sel = runContext?.selectedLotForQr ?? selectedLotForQr; if (!exp || !scan || !sel) return; const newStockInLineId = scan?.stockInLineId; if (newStockInLineId == null || Number.isNaN(Number(newStockInLineId))) return; const rowSolKey = Number(sel.stockOutLineId); if (Number.isFinite(rowSolKey)) { setLotSwitchFailByStockOutLineId((prev) => { const next = { ...prev }; delete next[rowSolKey]; return next; }); } setIsConfirmingLot(true); setLotConfirmationError(null); try { const substitutionResult = await confirmLotSubstitution({ pickOrderLineId: sel.pickOrderLineId, stockOutLineId: sel.stockOutLineId, originalSuggestedPickLotId: sel.suggestedPickLotId, newInventoryLotNo: "", newStockInLineId: newStockInLineId, }); const substitutionCode = substitutionResult?.code; const switchedToUnavailable = substitutionCode === "SUCCESS_UNAVAILABLE" || substitutionCode === "BOUND_UNAVAILABLE"; if ( !substitutionResult || (substitutionCode !== "SUCCESS" && !switchedToUnavailable) ) { const errMsg = translateLotSubstitutionFailure(t, substitutionResult); if (Number.isFinite(rowSolKey)) { setLotSwitchFailByStockOutLineId((prev) => ({ ...prev, [rowSolKey]: errMsg, })); } setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg(errMsg); return; } if (switchedToUnavailable) { const itemId = Number(sel?.itemId ?? exp?.itemId); const stockInLineId = Number(newStockInLineId); if (Number.isFinite(itemId) && Number.isFinite(stockInLineId)) { setLotLabelPrintInitialPayload({ itemId, stockInLineId }); setLotLabelPrintReminderText( "該批次不可用,請移除該Label並列印新Label。", ); setLotLabelPrintModalOpen(true); } } setQrScanError(false); setQrScanSuccess(false); setQrScanInput(""); resetScan(); setPickExecutionFormOpen(false); if (sel?.stockOutLineId && !switchedToUnavailable) { await updateStockOutLineStatus({ id: sel.stockOutLineId, status: "checked", qty: 0, }); } clearLotConfirmationState(false); setIsRefreshingData(true); await fetchAllCombinedLotData(); setIsRefreshingData(false); } catch (error) { console.error("Error confirming lot substitution:", error); const errMsg = t("Lot confirmation failed. Please try again."); if (Number.isFinite(rowSolKey)) { setLotSwitchFailByStockOutLineId((prev) => ({ ...prev, [rowSolKey]: errMsg, })); } setQrScanError(true); setQrScanErrorMsg(errMsg); } finally { setIsConfirmingLot(false); } }, [ expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData, resetScan, clearLotConfirmationState, t, ], ); useEffect(() => { handleLotConfirmationRef.current = handleLotConfirmation; }, [handleLotConfirmation]); const handleLotConfirmationByRescan = useCallback( async (rawQr: string): Promise => { if ( !lotConfirmationOpen || !selectedLotForQr || !expectedLotData || !scannedLotData ) { return false; } const payload = parseQrPayload(rawQr); const expectedStockInLineId = Number(selectedLotForQr.stockInLineId); const mismatchedStockInLineId = Number(scannedLotData?.stockInLineId); if (payload) { const rescannedStockInLineId = Number(payload.stockInLineId); // 再扫“差异 lot” => 直接执行切换 if ( Number.isFinite(mismatchedStockInLineId) && rescannedStockInLineId === mismatchedStockInLineId ) { await handleLotConfirmation(); return true; } // 再扫“原建议 lot” => 关闭弹窗并按原 lot 正常记一次扫描 if ( Number.isFinite(expectedStockInLineId) && rescannedStockInLineId === expectedStockInLineId ) { clearLotConfirmationState(false); if (processOutsideQrCodeRef.current) { await processOutsideQrCodeRef.current(JSON.stringify(payload)); } return true; } // 扫到第三个 lot(既不是当前差异 lot,也不是原建议 lot): // 直接按“扫描到的这一批”执行切换。 await handleLotConfirmation({ lotNo: null, itemCode: expectedLotData?.itemCode, itemName: expectedLotData?.itemName, inventoryLotLineId: null, stockInLineId: rescannedStockInLineId, }); return true; } else { // 兼容纯 lotNo 文本扫码 const scannedText = rawQr?.trim(); const expectedLotNo = expectedLotData?.lotNo?.trim(); const mismatchedLotNo = scannedLotData?.lotNo?.trim(); if (mismatchedLotNo && scannedText === mismatchedLotNo) { await handleLotConfirmation(); return true; } if (expectedLotNo && scannedText === expectedLotNo) { clearLotConfirmationState(false); if (processOutsideQrCodeRef.current) { await processOutsideQrCodeRef.current( JSON.stringify({ itemId: selectedLotForQr.itemId, stockInLineId: selectedLotForQr.stockInLineId, }), ); } return true; } } return false; }, [ lotConfirmationOpen, selectedLotForQr, expectedLotData, scannedLotData, parseQrPayload, handleLotConfirmation, clearLotConfirmationState, handleLotMismatch, ], ); const handleQrCodeSubmit = useCallback( async (lotNo: string) => { console.log(` Processing QR Code for lot: ${lotNo}`); // 检查 lotNo 是否为 null 或 undefined(包括字符串 "null") if (!lotNo || lotNo === "null" || lotNo.trim() === "") { console.error(" Invalid lotNo: null, undefined, or empty"); return; } // Use current data without refreshing to avoid infinite loop const currentLotData = combinedLotData; console.log( ` Available lots:`, currentLotData.map((lot) => lot.lotNo), ); // 修复:在比较前确保 lotNo 不为 null const lotNoLower = lotNo.toLowerCase(); const matchingLots = currentLotData.filter((lot) => { if (!lot.lotNo) return false; // 跳过 null lotNo return lot.lotNo === lotNo || lot.lotNo.toLowerCase() === lotNoLower; }); if (matchingLots.length === 0) { console.error(` Lot not found: ${lotNo}`); setQrScanError(true); setQrScanSuccess(false); const availableLotNos = currentLotData .map((lot) => lot.lotNo) .join(", "); console.log( ` QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`, ); return; } const hasExpiredLot = matchingLots.some( (lot: any) => String(lot.lotAvailability || "").toLowerCase() === "expired", ); if (hasExpiredLot) { console.warn(`⚠️ [QR PROCESS] Scanned lot ${lotNo} is expired`); setQrScanError(true); setQrScanSuccess(false); return; } console.log(` Found ${matchingLots.length} matching lots:`, matchingLots); setQrScanError(false); try { let successCount = 0; let errorCount = 0; for (const matchingLot of matchingLots) { console.log( `🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`, ); if (matchingLot.stockOutLineId) { const stockOutLineUpdate = await updateStockOutLineStatus({ id: matchingLot.stockOutLineId, status: "checked", qty: 0, }); console.log( `Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate, ); // Treat multiple backend shapes as success (type-safe via any) const r: any = stockOutLineUpdate as any; const updateOk = r?.code === "SUCCESS" || typeof r?.id === "number" || r?.type === "checked" || r?.status === "checked" || typeof r?.entity?.id === "number" || r?.entity?.status === "checked"; if (updateOk) { successCount++; } else { errorCount++; } } else { const createStockOutLineData = { consoCode: matchingLot.pickOrderConsoCode, pickOrderLineId: matchingLot.pickOrderLineId, inventoryLotLineId: matchingLot.lotId, qty: 0, }; const createResult = await createStockOutLine( createStockOutLineData, ); console.log( `Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult, ); if (createResult && createResult.code === "SUCCESS") { // Immediately set status to checked for new line let newSolId: number | undefined; const anyRes: any = createResult as any; if (typeof anyRes?.id === "number") { newSolId = anyRes.id; } else if (anyRes?.entity) { newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id; } if (newSolId) { const setChecked = await updateStockOutLineStatus({ id: newSolId, status: "checked", qty: 0, }); if (setChecked && setChecked.code === "SUCCESS") { successCount++; } else { errorCount++; } } else { console.warn( "Created stock out line but no ID returned; cannot set to checked", ); errorCount++; } } else { errorCount++; } } } // FIXED: Set refresh flag before refreshing data setIsRefreshingData(true); console.log("🔄 Refreshing data after QR code processing..."); await fetchAllCombinedLotData(); if (successCount > 0) { console.log( ` QR Code processing completed: ${successCount} updated/created`, ); setQrScanSuccess(true); setQrScanError(false); setQrScanInput(""); // Clear input after successful processing //setIsManualScanning(false); // stopScan(); // resetScan(); // Clear success state after a delay //setTimeout(() => { //setQrScanSuccess(false); //}, 2000); } else { console.error(` QR Code processing failed: ${errorCount} errors`); setQrScanError(true); setQrScanSuccess(false); // Clear error state after a delay // setTimeout(() => { // setQrScanError(false); //}, 3000); } } catch (error) { console.error(" Error processing QR code:", error); setQrScanError(true); setQrScanSuccess(false); // Clear error state after a delay setTimeout(() => { setQrScanError(false); }, 3000); } finally { // Clear refresh flag after a short delay setTimeout(() => { setIsRefreshingData(false); }, 1000); } }, [combinedLotData], ); 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, stockOutLineId: matchingLot.stockOutLineId, 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") { // ✅ 只更新本地状态,不调用 fetchAllCombinedLotData const updateStartTime = performance.now(); const entity = res.entity as any; setCombinedLotData((prev) => prev.map((lot) => { if ( lot.stockOutLineId === matchingLot.stockOutLineId && lot.pickOrderLineId === matchingLot.pickOrderLineId ) { return { ...lot, stockOutLineStatus: "checked", stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty, }; } return lot; }), ); setOriginalCombinedData((prev) => prev.map((lot) => { if ( lot.stockOutLineId === matchingLot.stockOutLineId && lot.pickOrderLineId === matchingLot.pickOrderLineId ) { return { ...lot, stockOutLineStatus: "checked", stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty, }; } return lot; }), ); 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, 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(); // 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 solStatus = String(lot.stockOutLineStatus || "").toLowerCase(); const lotAvailability = String(lot.lotAvailability || "").toLowerCase(); const processingStatus = String(lot.processingStatus || "").toLowerCase(); const isUnavailable = isInventoryLotLineUnavailable(lot); const isExpired = isLotAvailabilityExpired(lot); const isRejected = rejectedStatuses.has(lotAvailability) || rejectedStatuses.has(solStatus) || rejectedStatuses.has(processingStatus); const isEnded = solStatus === "checked" || solStatus === "completed"; const isPartially = solStatus === "partially_completed" || solStatus === "partially_complete"; const isPending = solStatus === "pending" || solStatus === ""; const isActive = !isRejected && !isUnavailable && !isExpired && !isEnded && (isPending || isPartially); 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]); // Store resetScan in ref for immediate access (update on every render) resetScanRef.current = resetScan; const processOutsideQrCode = useCallback( async (latestQr: string, qrScanCountAtInvoke?: number) => { 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.", ); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); }); return; } try { const validationStartTime = performance.now(); if (!(qrData?.stockInLineId && qrData?.itemId)) { console.log( "QR JSON missing required fields (itemId, stockInLineId).", ); 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; // ✅ 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: Use cached active lots directly (no filtering needed) const lookupStartTime = performance.now(); const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || []; // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected const allLotsForItem = indexes.byItemId.get(scannedItemId) || []; const lookupTime = performance.now() - lookupStartTime; console.log( ` [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${ activeSuggestedLots.length } active lots, ${allLotsForItem.length} total lots`, ); // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots // This allows users to scan other lots even when all suggested lots are rejected const scannedLot = allLotsForItem.find( (lot: any) => lot.stockInLineId === scannedStockInLineId, ); if (scannedLot) { const isRejected = scannedLot.stockOutLineStatus?.toLowerCase() === "rejected" || scannedLot.lotAvailability === "rejected" || isInventoryLotLineUnavailable(scannedLot); if (isRejected) { console.warn( `⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`, ); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg( `此批次(${ scannedLot.lotNo || scannedStockInLineId })已被拒绝,无法使用。请扫描其他批次。`, ); }); // Mark as processed to prevent re-processing setProcessedQrCombinations((prev) => { const newMap = new Map(prev); if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); newMap.get(scannedItemId)!.add(scannedStockInLineId); return newMap; }); return; } const isExpired = String(scannedLot.lotAvailability || "").toLowerCase() === "expired"; if (isExpired) { console.warn( `⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is expired`, ); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg( `此批次(${ scannedLot.lotNo || scannedStockInLineId })已过期,无法使用。请扫描其他批次。`, ); }); // Mark as processed to prevent re-processing the same expired QR repeatedly setProcessedQrCombinations((prev) => { const newMap = new Map(prev); if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); newMap.get(scannedItemId)!.add(scannedStockInLineId); return newMap; }); return; } } // ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching if (activeSuggestedLots.length === 0) { // Check if there are any lots for this item (even if all are rejected) if (allLotsForItem.length === 0) { console.error("No lots found for this item"); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg("当前订单中没有此物品的批次信息"); }); return; } // ✅ Allow lot switching: find a rejected lot as expected lot, or use first lot // This allows users to switch to a new lot even when all suggested lots are rejected console.log( `⚠️ [QR PROCESS] No active suggested lots, but allowing lot switching.`, ); // Find a rejected lot as expected lot (the one that was rejected) const rejectedLot = allLotsForItem.find( (lot: any) => lot.stockOutLineStatus?.toLowerCase() === "rejected" || lot.lotAvailability === "rejected" || isInventoryLotLineUnavailable(lot), ); const expectedLot = rejectedLot || pickExpectedLotForSubstitution( allLotsForItem.filter( (l: any) => l.lotNo != null && String(l.lotNo).trim() !== "", ), ) || allLotsForItem[0]; // Silent lot substitution; modal only if switch fails console.log( `⚠️ [QR PROCESS] Lot switch (no active lots), attempting substitution`, ); setSelectedLotForQr(expectedLot); handleLotMismatch( expectedLot, { lotNo: scannedLot?.lotNo || null, itemCode: expectedLot.itemCode, itemName: expectedLot.itemName, inventoryLotLineId: scannedLot?.lotId || null, stockInLineId: scannedStockInLineId, }, qrScanCountAtInvoke, ); return; } // ✅ 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" }`, ); // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots // This handles the case where Lot A is rejected and user scans Lot B // Also handle case where scanned lot is not in allLotsForItem (scannedLot is undefined) if (!exactMatch) { // Scanned lot is not in active suggested lots, open confirmation modal const expectedLot = pickExpectedLotForSubstitution(activeSuggestedLots) || allLotsForItem[0]; if (expectedLot) { // Check if scanned lot is different from expected, or if scannedLot is undefined (not in allLotsForItem) const shouldOpenModal = !scannedLot || scannedLot.stockInLineId !== expectedLot.stockInLineId; if (shouldOpenModal) { console.log( `⚠️ [QR PROCESS] Lot switch (scanned lot ${ scannedLot?.lotNo || "not in data" } not in active suggested lots)`, ); setSelectedLotForQr(expectedLot); handleLotMismatch( expectedLot, { lotNo: scannedLot?.lotNo || null, itemCode: expectedLot.itemCode, itemName: expectedLot.itemName, inventoryLotLineId: scannedLot?.lotId || null, stockInLineId: scannedStockInLineId, }, qrScanCountAtInvoke, ); return; } } } if (exactMatch) { // ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认 console.log( `✅ Exact stockInLineId match found for lot: ${exactMatch.lotNo}`, ); 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: 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") { const entity = res.entity as any; // ✅ 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; }), ); }); 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`, ); 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 locally, no full data refresh needed", ); } else { console.warn("Unexpected response code from backend:", res.code); 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, ); 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`, ); // 取应被替换的活跃行(同物料多行时优先有建议批次的行) const expectedLotStartTime = performance.now(); const expectedLot = pickExpectedLotForSubstitution(activeSuggestedLots); if (!expectedLot) { console.error("Could not determine expected lot for confirmation"); 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`, ); const handleMismatchStartTime = performance.now(); handleLotMismatch( expectedLot, { lotNo: null, itemCode: expectedLot.itemCode, itemName: expectedLot.itemName, inventoryLotLineId: null, stockInLineId: scannedStockInLineId, }, qrScanCountAtInvoke, ); 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); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); }); return; } }, [ lotDataIndexes, handleLotMismatch, processedQrCombinations, combinedLotData, fetchStockInLineInfoCached, ], ); // Store processOutsideQrCode in ref for immediate access (update on every render) processOutsideQrCodeRef.current = processOutsideQrCode; 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, qrValues.length) .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}`, ); } } // 批次确认弹窗:须第二次扫码选择沿用建议批次或切换(不再自动确认) if (lotConfirmationOpen) { if (isConfirmingLot) { return; } if (lotConfirmSkipNextScanRef.current) { lotConfirmSkipNextScanRef.current = false; lotConfirmLastQrRef.current = latestQr || ""; return; } if (!latestQr) { return; } // Prevent auto-accept from buffered duplicate right after modal opens, // but allow intentional second scan of the same QR after debounce window. const sameQr = latestQr === lotConfirmLastQrRef.current; const justOpened = lotConfirmOpenedAtRef.current > 0 && Date.now() - lotConfirmOpenedAtRef.current < 800; if (sameQr && justOpened) { return; } lotConfirmLastQrRef.current = latestQr; void (async () => { try { const handled = await handleLotConfirmationByRescan(latestQr); if (handled && resetScanRef.current) { resetScanRef.current(); } } catch (e) { console.error("Lot confirmation rescan failed:", e); } })(); return; } // Skip processing if manual confirmation modal is open if (manualLotConfirmationOpen) { // Check if this is a different QR code than what triggered the modal const modalTriggerQr = lastProcessedQrRef.current; if (latestQr === modalTriggerQr) { console.log(` [QR PROCESS] Skipping - manual modal open for same QR`); return; } // If it's a different QR, allow processing console.log( ` [QR PROCESS] Different QR detected while manual modal open, allowing processing`, ); } const qrDetectionStartTime = performance.now(); 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, qrValues.length) .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, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen, handleLotConfirmationByRescan, isConfirmingLot, ]); const renderCountRef = useRef(0); const renderStartTimeRef = useRef(null); // Track render performance useEffect(() => { renderCountRef.current++; const now = performance.now(); 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; } // Track when lotConfirmationOpen changes if (lotConfirmationOpen) { renderStartTimeRef.current = performance.now(); console.log(` [PERF] Render triggered by lotConfirmationOpen=true`); } }, [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..."); initializationRef.current = true; // Only fetch existing data, no auto-assignment 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(() => { const handlePickOrderAssigned = () => { console.log("🔄 Pick order assigned event received, refreshing data..."); fetchAllCombinedLotData(); }; window.addEventListener("pickOrderAssigned", handlePickOrderAssigned); return () => { window.removeEventListener("pickOrderAssigned", handlePickOrderAssigned); }; }, [fetchAllCombinedLotData]); const handleManualInputSubmit = useCallback(() => { if (qrScanInput.trim() !== "") { handleQrCodeSubmit(qrScanInput.trim()); } }, [qrScanInput, handleQrCodeSubmit]); // Handle QR code submission from modal (internal scanning) const handleQrCodeSubmitFromModal = useCallback( async (lotNo: string) => { if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { console.log(` QR Code verified for lot: ${lotNo}`); const requiredQty = selectedLotForQr.requiredQty; const lotId = selectedLotForQr.lotId; // Create stock out line try { const stockOutLineUpdate = await updateStockOutLineStatus({ id: selectedLotForQr.stockOutLineId, status: "checked", qty: selectedLotForQr.stockOutLineQty || 0, }); console.log("Stock out line updated successfully!"); setQrScanSuccess(true); setQrScanError(false); // Clear selected lot (scanner stays active) setSelectedLotForQr(null); // Set pick quantity const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`; setTimeout(() => { setPickQtyData((prev) => ({ ...prev, [lotKey]: requiredQty, })); console.log( ` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`, ); }, 500); } catch (error) { console.error("Error creating stock out line:", error); } } }, [selectedLotForQr], ); const handlePickQtyChange = useCallback( (lotKey: string, value: number | string) => { if (value === "" || value === null || value === undefined) { setPickQtyData((prev) => ({ ...prev, [lotKey]: 0, })); return; } const numericValue = typeof value === "string" ? parseFloat(value) : value; if (isNaN(numericValue)) { setPickQtyData((prev) => ({ ...prev, [lotKey]: 0, })); return; } setPickQtyData((prev) => ({ ...prev, [lotKey]: numericValue, })); }, [], ); const [autoAssignStatus, setAutoAssignStatus] = useState< "idle" | "checking" | "assigned" | "no_orders" >("idle"); const [autoAssignMessage, setAutoAssignMessage] = useState(""); const [completionStatus, setCompletionStatus] = useState(null); const checkAndAutoAssignNext = useCallback(async () => { if (!currentUserId) return; try { const completionResponse = await checkPickOrderCompletion(currentUserId); if ( completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders ) { console.log("Found completed pick orders, auto-assigning next..."); // 移除前端的自动分配逻辑,因为后端已经处理了 // await handleAutoAssignAndRelease(); // 删除这个函数 } } catch (error) { console.error("Error checking pick order completion:", error); } }, [currentUserId]); const resolveSingleSubmitQty = useCallback( (lot: any) => { const required = Number( lot.requiredQty || lot.pickOrderLineRequiredQty || 0, ); const solId = Number(lot.stockOutLineId) || 0; const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined; if (issuePicked !== undefined && !Number.isNaN(Number(issuePicked))) { return Number(issuePicked); } const fromPick = pickQtyData[lotKey]; if ( fromPick !== undefined && fromPick !== null && !Number.isNaN(Number(fromPick)) ) { return Number(fromPick); } if (lot.noLot === true) { return 0; } if (isInventoryLotLineUnavailable(lot)) { return 0; } if (isLotAvailabilityExpired(lot)) { return 0; } return required; }, [issuePickedQtyBySolId, pickQtyData], ); // Handle reject lot // Handle pick execution form const handlePickExecutionForm = useCallback((lot: any) => { console.log("=== Pick Execution Form ==="); console.log("Lot data:", lot); if (!lot) { console.warn("No lot data provided for pick execution form"); return; } console.log("Opening pick execution form for lot:", lot.lotNo); setSelectedLotForExecutionForm(lot); setPickExecutionFormOpen(true); console.log("Pick execution form opened for lot ID:", lot.lotId); }, []); const handlePickExecutionFormSubmit = useCallback( async (data: any) => { try { console.log("Pick execution form submitted:", data); const issueData = { ...data, type: "Do", // Delivery Order Record 类型 pickerName: session?.user?.name || "", }; const result = await recordPickExecutionIssue(issueData); console.log("Pick execution issue recorded:", result); if (result && result.code === "SUCCESS") { console.log(" Pick execution issue recorded successfully"); // 关键:issue form 只记录问题,不会更新 SOL.qty // 但 batch submit 需要知道“实际拣到多少”,否则会按 requiredQty 补拣到满 const solId = Number( issueData.stockOutLineId || issueData.stockOutLineId === 0 ? issueData.stockOutLineId : data?.stockOutLineId, ); if (solId > 0) { const picked = Number(issueData.actualPickQty || 0); setIssuePickedQtyBySolId((prev) => { const next = { ...prev, [solId]: picked }; const doId = fgPickOrders[0]?.doPickOrderId; if (doId) saveIssuePickedMap(doId, next); return next; }); setCombinedLotData((prev) => prev.map((lot) => { if (Number(lot.stockOutLineId) === solId) { return { ...lot, actualPickQty: picked, stockOutLineQty: picked, }; } return lot; }), ); } } else { console.error(" Failed to record pick execution issue:", result); } setPickExecutionFormOpen(false); setSelectedLotForExecutionForm(null); setQrScanError(false); setQrScanSuccess(false); setQrScanInput(""); // ✅ 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); } }, [fetchAllCombinedLotData, session, fgPickOrders], ); // Calculate remaining required quantity const calculateRemainingRequiredQty = useCallback((lot: any) => { const requiredQty = lot.requiredQty || 0; const stockOutLineQty = lot.stockOutLineQty || 0; return Math.max(0, requiredQty - stockOutLineQty); }, []); // Search criteria const searchCriteria: Criterion[] = [ { label: t("Pick Order Code"), paramName: "pickOrderCode", type: "text", }, { label: t("Item Code"), paramName: "itemCode", type: "text", }, { label: t("Item Name"), paramName: "itemName", type: "text", }, { label: t("Lot No"), paramName: "lotNo", type: "text", }, ]; const handleSearch = useCallback( (query: Record) => { setSearchQuery({ ...query }); console.log("Search query:", query); if (!originalCombinedData) return; const filtered = originalCombinedData.filter((lot: any) => { const pickOrderCodeMatch = !query.pickOrderCode || lot.pickOrderCode ?.toLowerCase() .includes((query.pickOrderCode || "").toLowerCase()); const itemCodeMatch = !query.itemCode || lot.itemCode ?.toLowerCase() .includes((query.itemCode || "").toLowerCase()); const itemNameMatch = !query.itemName || lot.itemName ?.toLowerCase() .includes((query.itemName || "").toLowerCase()); const lotNoMatch = !query.lotNo || lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase()); return ( pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch ); }); setCombinedLotData(filtered); console.log("Filtered lots count:", filtered.length); }, [originalCombinedData], ); const handleReset = useCallback(() => { setSearchQuery({}); if (originalCombinedData) { setCombinedLotData(originalCombinedData); } }, [originalCombinedData]); const handlePageChange = useCallback((event: unknown, newPage: number) => { setPaginationController((prev) => ({ ...prev, pageNum: newPage, })); }, []); const handlePageSizeChange = useCallback( (event: React.ChangeEvent) => { const newPageSize = parseInt(event.target.value, 10); setPaginationController({ pageNum: 0, pageSize: newPageSize === -1 ? -1 : newPageSize, }); }, [], ); // 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 } const startIndex = paginationController.pageNum * paginationController.pageSize; const endIndex = startIndex + paginationController.pageSize; return combinedLotData.slice(startIndex, endIndex); // No sorting needed }, [ combinedLotData, paginationController.pageNum, paginationController.pageSize, ]); const allItemsReady = useMemo(() => { if (combinedLotData.length === 0) return false; return combinedLotData.every((lot: any) => { const status = lot.stockOutLineStatus?.toLowerCase(); const isRejected = status === "rejected" || lot.lotAvailability === "rejected"; const isCompleted = status === "completed" || status === "partially_completed" || status === "partially_complete"; const isChecked = status === "checked"; const isPending = status === "pending"; // ✅ FIXED: 无库存(noLot)行:pending 状态也应该被视为 ready(可以提交) // ✅ 過期批號(未換批):與 noLot 相同,視為可收尾 if ( lot.noLot === true || isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot) ) { return isChecked || isCompleted || isRejected || isPending; } // 正常 lot:必须已扫描/提交或者被拒收 return isChecked || isCompleted || isRejected; }); }, [combinedLotData]); const handleSubmitPickQtyWithQty = useCallback( async ( lot: any, submitQty: number, source: "justComplete" | "singleSubmit", ) => { if (!lot.stockOutLineId) { console.error("No stock out line found for this lot"); return; } const solId = Number(lot.stockOutLineId); if (solId > 0 && actionBusyBySolId[solId]) { console.warn("Action already in progress for stockOutLineId:", solId); return; } try { if (solId > 0) setActionBusyBySolId((prev) => ({ ...prev, [solId]: true })); const targetUnavailable = isInventoryLotLineUnavailable(lot); const effectiveSubmitQty = targetUnavailable && submitQty > 0 ? 0 : submitQty; // Just Complete: mark checked only, real posting happens in batch submit if (effectiveSubmitQty === 0 && source === "justComplete") { console.log(`=== SUBMITTING ALL ZEROS CASE ===`); console.log(`Lot: ${lot.lotNo}`); console.log(`Stock Out Line ID: ${lot.stockOutLineId}`); console.log(`Setting status to 'checked' with qty: 0`); const updateResult = await updateStockOutLineStatus({ id: lot.stockOutLineId, status: "checked", qty: 0, }); console.log("Update result:", updateResult); const r: any = updateResult as any; const updateOk = r?.code === "SUCCESS" || r?.type === "completed" || typeof r?.id === "number" || typeof r?.entity?.id === "number" || (r?.message && r.message.includes("successfully")); if (!updateResult || !updateOk) { console.error( "Failed to update stock out line status:", updateResult, ); throw new Error("Failed to update stock out line status"); } applyLocalStockOutLineUpdate( Number(lot.stockOutLineId), "checked", Number(lot.actualPickQty || 0), ); void fetchAllCombinedLotData(); console.log( "Just Complete marked as checked successfully (waiting for batch submit).", ); setTimeout(() => { checkAndAutoAssignNext(); }, 1000); return; } if (effectiveSubmitQty === 0 && source === "singleSubmit") { console.log(`=== SUBMITTING ALL ZEROS CASE ===`); console.log(`Lot: ${lot.lotNo}`); console.log(`Stock Out Line ID: ${lot.stockOutLineId}`); console.log(`Setting status to 'checked' with qty: 0`); const updateResult = await updateStockOutLineStatus({ id: lot.stockOutLineId, status: "checked", qty: 0, }); console.log("Update result:", updateResult); const r: any = updateResult as any; const updateOk = r?.code === "SUCCESS" || r?.type === "completed" || typeof r?.id === "number" || typeof r?.entity?.id === "number" || (r?.message && r.message.includes("successfully")); if (!updateResult || !updateOk) { console.error( "Failed to update stock out line status:", updateResult, ); throw new Error("Failed to update stock out line status"); } applyLocalStockOutLineUpdate( Number(lot.stockOutLineId), "checked", Number(lot.actualPickQty || 0), ); void fetchAllCombinedLotData(); console.log( "Just Complete marked as checked successfully (waiting for batch submit).", ); setTimeout(() => { checkAndAutoAssignNext(); }, 1000); return; } // FIXED: Calculate cumulative quantity correctly const currentActualPickQty = lot.actualPickQty || 0; const cumulativeQty = currentActualPickQty + effectiveSubmitQty; // FIXED: Determine status based on cumulative quantity vs required quantity let newStatus = "partially_completed"; if (cumulativeQty >= lot.requiredQty) { newStatus = "completed"; } else if (cumulativeQty > 0) { newStatus = "partially_completed"; } else { newStatus = "checked"; // QR scanned but no quantity submitted yet } console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`); console.log(`Lot: ${lot.lotNo}`); console.log(`Required Qty: ${lot.requiredQty}`); console.log(`Current Actual Pick Qty: ${currentActualPickQty}`); console.log(`New Submitted Qty: ${effectiveSubmitQty}`); console.log(`Cumulative Qty: ${cumulativeQty}`); console.log(`New Status: ${newStatus}`); console.log(`=====================================`); await updateStockOutLineStatus({ id: lot.stockOutLineId, status: newStatus, // 后端 updateStatus 的 qty 是“增量 delta”,不能传 cumulativeQty(否则会重复累加导致 out/hold 大幅偏移) qty: effectiveSubmitQty, }); applyLocalStockOutLineUpdate( Number(lot.stockOutLineId), newStatus, cumulativeQty, ); // 注意:库存过账(hold->out)与 ledger 由后端 updateStatus 内部统一处理; // 前端不再额外调用 updateInventoryLotLineQuantities(operation='pick'),避免 double posting。 // Check if pick order is completed when lot status becomes 'completed' if (newStatus === "completed" && lot.pickOrderConsoCode) { console.log( ` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`, ); try { const completionResponse = await checkAndCompletePickOrderByConsoCode( lot.pickOrderConsoCode, ); console.log( ` Pick order completion check result:`, completionResponse, ); if (completionResponse.code === "SUCCESS") { console.log( ` Pick order ${lot.pickOrderConsoCode} completed successfully!`, ); } else if (completionResponse.message === "not completed") { console.log( `Pick order not completed yet, more lines remaining`, ); } else { console.error( ` Error checking completion: ${completionResponse.message}`, ); } } catch (error) { console.error("Error checking pick order completion:", error); } } void fetchAllCombinedLotData(); console.log("Pick quantity submitted successfully!"); setTimeout(() => { checkAndAutoAssignNext(); }, 1000); } catch (error) { console.error("Error submitting pick quantity:", error); } finally { if (solId > 0) setActionBusyBySolId((prev) => ({ ...prev, [solId]: false })); } }, [ fetchAllCombinedLotData, checkAndAutoAssignNext, actionBusyBySolId, applyLocalStockOutLineUpdate, ], ); const handleSkip = useCallback( async (lot: any) => { try { console.log( "Just Complete clicked, mark checked with 0 qty for lot:", lot.lotNo, ); await handleSubmitPickQtyWithQty(lot, 0, "justComplete"); } catch (err) { console.error("Error in Skip:", err); } }, [handleSubmitPickQtyWithQty], ); const hasPendingBatchSubmit = useMemo(() => { return combinedLotData.some((lot) => { const status = String(lot.stockOutLineStatus || "").toLowerCase(); return ( status === "checked" || status === "pending" || status === "partially_completed" || status === "partially_complete" ); }); }, [combinedLotData]); useEffect(() => { if (!hasPendingBatchSubmit) return; const handler = (event: BeforeUnloadEvent) => { event.preventDefault(); event.returnValue = ""; }; window.addEventListener("beforeunload", handler); return () => window.removeEventListener("beforeunload", handler); }, [hasPendingBatchSubmit]); 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; setPickOrderSwitching(true); try { console.log(" Switching to pick order:", pickOrderId); setSelectedPickOrderId(pickOrderId); // 强制刷新数据,确保显示正确的 pick order 数据 await fetchAllCombinedLotData(currentUserId, pickOrderId); } catch (error) { console.error("Error switching pick order:", error); } finally { setPickOrderSwitching(false); } }, [pickOrderSwitching, currentUserId, fetchAllCombinedLotData], ); const handleStopScan = useCallback(() => { console.log("⏸️ Pausing QR scanner..."); setIsManualScanning(false); setQrScanError(false); setQrScanSuccess(false); stopScan(); resetScan(); }, [stopScan, resetScan]); // ... existing code around line 1469 ... const handlelotnull = useCallback( async (lot: any) => { // 优先使用 stockouts 中的 id,如果没有则使用 stockOutLineId const stockOutLineId = lot.stockOutLineId; if (!stockOutLineId) { console.error(" No stockOutLineId found for lot:", lot); return; } const solId = Number(stockOutLineId); if (solId > 0 && actionBusyBySolId[solId]) { console.warn("Action already in progress for stockOutLineId:", solId); return; } try { if (solId > 0) setActionBusyBySolId((prev) => ({ ...prev, [solId]: true })); // Step 1: Update stock out line status await updateStockOutLineStatus({ id: stockOutLineId, status: "completed", qty: 0, }); // Step 2: Create pick execution issue for no-lot case // Get pick order ID from fgPickOrders or use 0 if not available const pickOrderId = lot.pickOrderId || fgPickOrders[0]?.pickOrderId || 0; const pickOrderCode = lot.pickOrderCode || fgPickOrders[0]?.pickOrderCode || lot.pickOrderConsoCode || ""; const issueData: PickExecutionIssueData = { type: "Do", // Delivery Order type pickOrderId: pickOrderId, pickOrderCode: pickOrderCode, pickOrderCreateDate: dayjs().format("YYYY-MM-DD"), // Use dayjs format pickExecutionDate: dayjs().format("YYYY-MM-DD"), pickOrderLineId: lot.pickOrderLineId, itemId: lot.itemId, itemCode: lot.itemCode || "", itemDescription: lot.itemName || "", lotId: null, // No lot available lotNo: null, // No lot number storeLocation: lot.location || "", requiredQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0, actualPickQty: 0, // No items picked (no lot available) missQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0, // All quantity is missing badItemQty: 0, issueRemark: `No lot available for this item. Handled via handlelotnull.`, pickerName: session?.user?.name || "", }; const result = await recordPickExecutionIssue(issueData); console.log(" Pick execution issue created for no-lot item:", result); if (result && result.code === "SUCCESS") { console.log(" No-lot item handled and issue recorded successfully"); } else { console.error(" Failed to record pick execution issue:", result); } // Step 3: Refresh data await fetchAllCombinedLotData(); } catch (error) { console.error(" Error in handlelotnull:", error); } finally { if (solId > 0) setActionBusyBySolId((prev) => ({ ...prev, [solId]: false })); } }, [ fetchAllCombinedLotData, session, currentUserId, fgPickOrders, actionBusyBySolId, ], ); const handleBatchScan = useCallback(async () => { const startTime = performance.now(); console.log(` [BATCH SCAN START]`); console.log(` Start time: ${new Date().toISOString()}`); // 获取所有活跃批次(未扫描的) const activeLots = combinedLotData.filter((lot) => { return ( lot.lotAvailability !== "rejected" && lot.stockOutLineStatus !== "rejected" && lot.stockOutLineStatus !== "completed" && lot.stockOutLineStatus !== "checked" && // ✅ 只处理未扫描的 lot.processingStatus !== "completed" && lot.noLot !== true && lot.lotNo // ✅ 必须有 lotNo ); }); if (activeLots.length === 0) { console.log("No active lots to scan"); return; } console.log( `📦 Batch scanning ${activeLots.length} active lots using batch API...`, ); try { // ✅ 转换为批量扫描 API 所需的格式 const lines: BatchScanLineRequest[] = activeLots.map((lot) => ({ pickOrderLineId: Number(lot.pickOrderLineId), inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null, pickOrderConsoCode: String(lot.pickOrderConsoCode || ""), lotNo: lot.lotNo || null, itemId: Number(lot.itemId), itemCode: String(lot.itemCode || ""), stockOutLineId: lot.stockOutLineId ? Number(lot.stockOutLineId) : null, // ✅ 新增 })); const request: BatchScanRequest = { userId: currentUserId || 0, lines: lines, }; console.log(`📤 Sending batch scan request with ${lines.length} lines`); console.log(`📋 Request data:`, JSON.stringify(request, null, 2)); const scanStartTime = performance.now(); // ✅ 使用新的批量扫描 API(一次性处理所有请求) const result = await batchScan(request); const scanTime = performance.now() - scanStartTime; console.log( ` Batch scan API call completed in ${scanTime.toFixed(2)}ms (${( scanTime / 1000 ).toFixed(3)}s)`, ); console.log(`📥 Batch scan result:`, result); // ✅ 刷新数据以获取最新的状态 const refreshStartTime = performance.now(); await fetchAllCombinedLotData(); const refreshTime = performance.now() - refreshStartTime; console.log( ` Data refresh time: ${refreshTime.toFixed(2)}ms (${( refreshTime / 1000 ).toFixed(3)}s)`, ); const totalTime = performance.now() - startTime; console.log(` [BATCH SCAN END]`); console.log( ` Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed( 3, )}s)`, ); console.log(` End time: ${new Date().toISOString()}`); if (result && result.code === "SUCCESS") { setQrScanSuccess(true); setQrScanError(false); } else { console.error("❌ Batch scan failed:", result); setQrScanError(true); setQrScanSuccess(false); } } catch (error) { console.error("❌ Error in batch scan:", error); setQrScanError(true); setQrScanSuccess(false); } }, [combinedLotData, fetchAllCombinedLotData, currentUserId]); const handleSubmitAllScanned = useCallback(async () => { const startTime = performance.now(); console.log(` [BATCH SUBMIT START]`); console.log(` Start time: ${new Date().toISOString()}`); const scannedLots = combinedLotData.filter((lot) => { const status = lot.stockOutLineStatus; const statusLower = String(status || "").toLowerCase(); if (statusLower === "completed" || statusLower === "complete") { return false; } // ✅ noLot / 過期批號:與 noLot 相同,允許 pending(未換批也可批量收尾) if ( lot.noLot === true || isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot) ) { return ( status === "checked" || status === "pending" || status === "partially_completed" || status === "PARTIALLY_COMPLETE" ); } return ( status === "checked" || status === "partially_completed" || status === "PARTIALLY_COMPLETE" ); }); if (scannedLots.length === 0) { console.log("No scanned items to submit"); return; } setIsSubmittingAll(true); console.log( `📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`, ); try { // 转换为 batchSubmitList 所需的格式(与后端 QrPickBatchSubmitRequest 匹配) const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => { // 1. 需求数量 const requiredQty = Number( lot.requiredQty || lot.pickOrderLineRequiredQty || 0, ); // 2. 当前已经拣到的数量 // issue form 不会写回 SOL.qty,所以如果这条 SOL 有 issue,就用 issue form 的 actualPickQty 作为“已拣到数量” const solId = Number(lot.stockOutLineId) || 0; const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined; const currentActualPickQty = Number( issuePicked ?? lot.actualPickQty ?? 0, ); // 🔹 判断是否走“只改状态模式” // 这里先给一个简单条件示例:如果你不想再补拣,只想把当前数量标记完成, // 就让这个条件为 true(后面你可以根据业务加 UI 开关或别的 flag)。 const onlyComplete = lot.stockOutLineStatus === "partially_completed" || issuePicked !== undefined; const expired = isLotAvailabilityExpired(lot); const unavailable = isInventoryLotLineUnavailable(lot); let targetActual: number; let newStatus: string; // ✅ 過期且未在 Issue 填實際量:與 noLot 一樣按 0 完成 if (unavailable) { targetActual = currentActualPickQty; newStatus = "completed"; } else if (expired && issuePicked === undefined) { targetActual = 0; newStatus = "completed"; } else if (onlyComplete) { targetActual = currentActualPickQty; newStatus = "completed"; } else { const remainingQty = Math.max(0, requiredQty - currentActualPickQty); const cumulativeQty = currentActualPickQty + remainingQty; targetActual = cumulativeQty; newStatus = "partially_completed"; if (requiredQty > 0 && cumulativeQty >= requiredQty) { newStatus = "completed"; } } return { stockOutLineId: Number(lot.stockOutLineId) || 0, pickOrderLineId: Number(lot.pickOrderLineId), inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null, requiredQty, // 后端用 targetActual - 当前 qty 算增量,onlyComplete 时就是 0 actualPickQty: targetActual, stockOutLineStatus: newStatus, pickOrderConsoCode: String(lot.pickOrderConsoCode || ""), noLot: Boolean(lot.noLot === true), }; }); const request: batchSubmitListRequest = { userId: currentUserId || 0, lines: lines, }; console.log(`📤 Sending batch submit request with ${lines.length} lines`); console.log(`📋 Request data:`, JSON.stringify(request, null, 2)); const submitStartTime = performance.now(); // 使用 batchSubmitList API const result = await batchSubmitList(request); const submitTime = performance.now() - submitStartTime; console.log( ` Batch submit API call completed in ${submitTime.toFixed(2)}ms (${( submitTime / 1000 ).toFixed(3)}s)`, ); console.log(`📥 Batch submit result:`, result); // Refresh data once after batch submission const refreshStartTime = performance.now(); await fetchAllCombinedLotData(); const refreshTime = performance.now() - refreshStartTime; console.log( ` Data refresh time: ${refreshTime.toFixed(2)}ms (${( refreshTime / 1000 ).toFixed(3)}s)`, ); const totalTime = performance.now() - startTime; console.log(` [BATCH SUBMIT END]`); console.log( ` Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed( 3, )}s)`, ); console.log(` End time: ${new Date().toISOString()}`); if (result && result.code === "SUCCESS") { setQrScanSuccess(true); setTimeout(() => { setQrScanSuccess(false); checkAndAutoAssignNext(); if (onSwitchToRecordTab) { onSwitchToRecordTab(); } if (onRefreshReleasedOrderCount) { onRefreshReleasedOrderCount(); } }, 2000); } else { console.error("Batch submit failed:", result); setQrScanError(true); } } catch (error) { console.error("Error submitting all scanned items:", error); setQrScanError(true); } finally { setIsSubmittingAll(false); } }, [ combinedLotData, fetchAllCombinedLotData, checkAndAutoAssignNext, currentUserId, onSwitchToRecordTab, onRefreshReleasedOrderCount, issuePickedQtyBySolId, ]); // Calculate scanned items count // Calculate scanned items count (should match handleSubmitAllScanned filter logic) const scannedItemsCount = useMemo(() => { const filtered = combinedLotData.filter((lot) => { const status = lot.stockOutLineStatus; const statusLower = String(status || "").toLowerCase(); if (statusLower === "completed" || statusLower === "complete") { return false; } // ✅ 与 handleSubmitAllScanned 完全保持一致 if ( lot.noLot === true || isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot) ) { return ( status === "checked" || status === "pending" || status === "partially_completed" || status === "PARTIALLY_COMPLETE" ); } return ( status === "checked" || status === "partially_completed" || status === "PARTIALLY_COMPLETE" ); }); // 添加调试日志 const noLotCount = filtered.filter((l) => l.noLot === true).length; const normalCount = filtered.filter((l) => l.noLot !== true).length; console.log( `📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`, ); console.log(`📊 All items breakdown:`, { total: combinedLotData.length, noLot: combinedLotData.filter((l) => l.noLot === true).length, normal: combinedLotData.filter((l) => l.noLot !== true).length, }); return filtered.length; }, [combinedLotData]); /* // ADD THIS: Auto-stop scan when no data available useEffect(() => { if (isManualScanning && combinedLotData.length === 0) { console.log("⏹️ No data available, auto-stopping QR scan..."); handleStopScan(); } }, [combinedLotData.length, isManualScanning, handleStopScan]); */ // Cleanup effect useEffect(() => { return () => { // Cleanup when component unmounts (e.g., when switching tabs) if (isManualScanning) { console.log( "🧹 Pick execution component unmounting, stopping QR scanner...", ); stopScan(); resetScan(); } }; }, [isManualScanning, stopScan, resetScan]); const getStatusMessage = useCallback( (lot: any) => { switch (lot.stockOutLineStatus?.toLowerCase()) { case "pending": return t("Please finish QR code scan and pick order."); case "checked": return t("Please submit the pick order."); case "partially_completed": return t( "Partial quantity submitted. Please submit more or complete the order.", ); case "completed": return t("Pick order completed successfully!"); case "rejected": return t("Lot has been rejected and marked as unavailable."); case "unavailable": return t("This order is insufficient, please pick another lot."); default: return t("Please finish QR code scan and pick order."); } }, [t], ); return ( lot.lotAvailability !== "rejected" && lot.stockOutLineStatus !== "rejected" && lot.stockOutLineStatus !== "completed" } > {/* DO Header */} {/* 保留:Combined Lot Table - 包含所有 QR 扫描功能 */} {t("All Pick Order Lots")} {/* Scanner status indicator (always visible) */} {/* {isManualScanning ? t("Scanner Active") : t("Scanner Inactive")} */} {/* Pause/Resume button instead of Start/Stop */} {isManualScanning ? ( ) : ( )} {/* 保留:Submit All Scanned Button */} {fgPickOrders.length > 0 && ( {/* 基本信息 */} {t("Shop Name")}:{" "} {fgPickOrders[0].shopName || "-"} {t("Store ID")}:{" "} {fgPickOrders[0].storeId || "-"} {t("Ticket No.")}:{" "} {fgPickOrders[0].ticketNo || "-"} {t("Departure Time")}:{" "} {fgPickOrders[0].DepartureTime || "-"} {/* 改进:三个字段显示在一起,使用表格式布局 */} {/* 改进:三个字段合并显示 */} {/* 改进:表格式显示每个 pick order */} {t("Pick Orders Details")}: {(() => { const pickOrderCodes = fgPickOrders[0].pickOrderCodes as | string[] | string | undefined; const deliveryNos = fgPickOrders[0].deliveryNos as | string[] | string | undefined; const lineCounts = fgPickOrders[0].lineCountsPerPickOrder; const pickOrderCodesArray = Array.isArray(pickOrderCodes) ? pickOrderCodes : typeof pickOrderCodes === "string" ? pickOrderCodes.split(", ") : []; const deliveryNosArray = Array.isArray(deliveryNos) ? deliveryNos : typeof deliveryNos === "string" ? deliveryNos.split(", ") : []; const lineCountsArray = Array.isArray(lineCounts) ? lineCounts : []; const maxLength = Math.max( pickOrderCodesArray.length, deliveryNosArray.length, lineCountsArray.length, ); if (maxLength === 0) { return ( - ); } // 使用与外部基本信息相同的样式 return Array.from({ length: maxLength }, (_, idx) => ( {t("Delivery Order")}:{" "} {deliveryNosArray[idx] || "-"} {t("Pick Order")}:{" "} {pickOrderCodesArray[idx] || "-"} {t("Finsihed good items")}:{" "} {lineCountsArray[idx] || "-"} {t("kinds")} )); })()} )} {t("Index")} {t("Route")} {t("Item Code")} {t("Item Name")} {t("Lot#")} {t("Lot Required Pick Qty")} {t("Scan Result")} {t("Qty will submit")} {t("Submit Required Pick Qty")} {paginatedData.length === 0 ? ( {t("No data available")} ) : ( // 在第 1797-1938 行之间,将整个 map 函数修改为: paginatedData.map((lot, index) => { // 检查是否是 issue lot const isIssueLot = lot.stockOutLineStatus === "rejected" || !lot.lotNo; const rowSolId = Number(lot.stockOutLineId); const lotSwitchErr = Number.isFinite(rowSolId) ? lotSwitchFailByStockOutLineId[rowSolId] : undefined; return ( {paginationController.pageNum * paginationController.pageSize + index + 1} {lot.routerRoute || "-"} {lot.itemCode} {lot.itemName + "(" + lot.stockUnit + ")"} {lot.lotNo ? ( isInventoryLotLineUnavailable(lot) ? ( <> {lot.lotNo}{" "} {t( "is unavable. Please check around have available QR code or not.", )} ) : lot.lotAvailability === "expired" ? ( <> {lot.lotNo}{" "} {t( "is expired. Please check around have available QR code or not.", )} ) : ( lot.lotNo ) ) : ( t( "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.", ) )} {lotSwitchErr ? ( {lotSwitchErr} ) : null} {(() => { const requiredQty = lot.requiredQty || 0; return ( requiredQty.toLocaleString() + "(" + lot.uomShortDesc + ")" ); })()} {(() => { const status = lot.stockOutLineStatus?.toLowerCase(); const isRejected = status === "rejected" || lot.lotAvailability === "rejected"; const isNoLot = !lot.lotNo; // rejected lot:显示红色勾选(已扫描但被拒绝) if (isRejected && !isNoLot) { return ( ); } // 過期批號:與 noLot 同類——視為已掃到/可處理(含 pending),顯示警示色勾選 if ( isLotAvailabilityExpired(lot) && status !== "rejected" ) { return ( ); } // 正常 lot:已扫描(checked/partially_completed/completed) if ( !isNoLot && status !== "pending" && status !== "rejected" ) { return ( ); } // noLot 且已完成/部分完成:显示红色勾选 if ( isNoLot && (status === "partially_completed" || status === "completed") ) { return ( ); } return null; })()} {isInventoryLotLineUnavailable(lot) ? 0 : resolveSingleSubmitQty(lot)} {(() => { const status = lot.stockOutLineStatus?.toLowerCase(); const isRejected = status === "rejected" || lot.lotAvailability === "rejected"; const isNoLot = !lot.lotNo; const isUnavailableLot = isInventoryLotLineUnavailable(lot); // ✅ rejected lot:显示提示文本(换行显示) if (isRejected && !isNoLot) { return ( {t( "This lot is rejected, please scan another lot.", )} ); } // noLot 情况:只显示 Issue 按钮 if (isNoLot) { return ( ); } // 正常 lot:显示 Submit 和 Issue 按钮 return ( ); })()} ); }) )}
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` } />
{/* QR Code Scanner works in background - no modal needed */} { setManualLotConfirmationOpen(false); }} onConfirm={handleManualLotConfirmation} expectedLot={expectedLotData} scannedLot={scannedLotData} isLoading={isConfirmingLot} /> {/* 保留:Lot Confirmation Modal */} {lotConfirmationOpen && expectedLotData && scannedLotData && ( { console.log( ` [LOT CONFIRM MODAL] Closing modal, reset scanner and release raw-QR dedupe`, ); // 1) Reset scanner buffer first to avoid immediate reopen from buffered same QR. if (resetScanRef.current) { resetScanRef.current(); } // 2) Close modal state. clearLotConfirmationState(false); // 3) Release raw-QR dedupe after a short delay so user can re-scan B/C again. setTimeout(() => { lastProcessedQrRef.current = ""; processedQrCodesRef.current.clear(); }, 250); }} onConfirm={handleLotConfirmation} expectedLot={expectedLotData} scannedLot={scannedLotData} isLoading={isConfirmingLot} errorMessage={lotConfirmationError} /> )} {/* 保留:Good Pick Execution Form Modal */} {pickExecutionFormOpen && selectedLotForExecutionForm && ( { setPickExecutionFormOpen(false); setSelectedLotForExecutionForm(null); }} onSubmit={handlePickExecutionFormSubmit} selectedLot={selectedLotForExecutionForm} selectedPickOrderLine={{ id: selectedLotForExecutionForm.pickOrderLineId, itemId: selectedLotForExecutionForm.itemId, itemCode: selectedLotForExecutionForm.itemCode, itemName: selectedLotForExecutionForm.itemName, pickOrderCode: selectedLotForExecutionForm.pickOrderCode, availableQty: selectedLotForExecutionForm.availableQty || 0, requiredQty: selectedLotForExecutionForm.requiredQty || 0, // uomCode: selectedLotForExecutionForm.uomCode || '', uomDesc: selectedLotForExecutionForm.uomDesc || "", pickedQty: selectedLotForExecutionForm.actualPickQty || 0, uomShortDesc: selectedLotForExecutionForm.uomShortDesc || "", suggestedList: [], noLotLines: [], }} pickOrderId={selectedLotForExecutionForm.pickOrderId} pickOrderCreateDate={new Date()} /> )} { setLotLabelPrintModalOpen(false); setLotLabelPrintReminderText(null); }} initialPayload={lotLabelPrintInitialPayload} defaultPrinterName={defaultLabelPrinterName} hideScanSection reminderText={lotLabelPrintReminderText ?? undefined} statusTitleText="此批號的已用完/已過期" warehouseCodePrefixFilter={lotFloorPrefixFilter} hideTriggeredLot />
); }; export default PickExecution;