"use client"; import { Box, Button, Stack, TextField, Typography, Alert, CircularProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Checkbox, TablePagination, Modal, } from "@mui/material"; import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider'; import { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/navigation"; import { updateStockOutLineStatus, createStockOutLine, recordPickExecutionIssue, fetchFGPickOrders, FGPickOrderResponse, autoAssignAndReleasePickOrder, AutoAssignReleaseResponse, checkPickOrderCompletion, PickOrderCompletionResponse, checkAndCompletePickOrderByConsoCode, confirmLotSubstitution, updateStockOutLineStatusByQRCodeAndLotNo, batchSubmitList, batchSubmitListRequest, batchSubmitListLineRequest, } from "@/app/api/pickOrder/actions"; // 修改:使用 Job Order API import { fetchJobOrderLotsHierarchical, fetchUnassignedJobOrderPickOrders, assignJobOrderPickOrder, updateJo, JobOrderLotsHierarchicalResponse, } from "@/app/api/jo/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, analyzeQrCode, fetchLotDetail } 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 "./JobPickExecutionForm"; import FGPickOrderCard from "./FGPickOrderCard"; import LotConfirmationModal from "./LotConfirmationModal"; interface Props { filterArgs: Record; } // Manual Lot Confirmation Modal Component const ManualLotConfirmationModal: React.FC<{ open: boolean; onClose: () => void; onConfirm: (expectedLotNo: string, scannedLotNo: string) => void; expectedLot: { lotNo: string; itemCode: string; itemName: string; } | null; scannedLot: { lotNo: string; itemCode: string; itemName: string; } | null; isLoading?: boolean; }> = ({ open, onClose, onConfirm, expectedLot, scannedLot, isLoading = false }) => { const { t } = useTranslation("jo"); const [expectedLotInput, setExpectedLotInput] = useState(''); const [scannedLotInput, setScannedLotInput] = useState(''); const [error, setError] = useState(''); // 当模态框打开时,预填充输入框 useEffect(() => { if (open) { setExpectedLotInput(expectedLot?.lotNo || ''); setScannedLotInput(scannedLot?.lotNo || ''); setError(''); } }, [open, expectedLot, scannedLot]); const handleConfirm = () => { if (!expectedLotInput.trim() || !scannedLotInput.trim()) { setError(t("Please enter both expected and scanned lot numbers.")); return; } if (expectedLotInput.trim() === scannedLotInput.trim()) { setError(t("Expected and scanned lot numbers cannot be the same.")); return; } onConfirm(expectedLotInput.trim(), scannedLotInput.trim()); }; return ( {t("Manual Lot Confirmation")} {t("Expected Lot Number")}: { setExpectedLotInput(e.target.value); setError(''); }} placeholder={expectedLot?.lotNo || t("Enter expected lot number")} sx={{ mb: 2 }} error={!!error && !expectedLotInput.trim()} /> {t("Scanned Lot Number")}: { setScannedLotInput(e.target.value); setError(''); }} placeholder={scannedLot?.lotNo || t("Enter scanned lot number")} sx={{ mb: 2 }} error={!!error && !scannedLotInput.trim()} /> {error && ( {error} )} ); }; // QR Code Modal Component (from GoodPickExecution) const QrCodeModal: React.FC<{ open: boolean; onClose: () => void; lot: any | null; onQrCodeSubmit: (lotNo: string) => void; combinedLotData: any[]; }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { const { t } = useTranslation("jo"); 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(''); // Process scanned QR codes useEffect(() => { 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; } setProcessedQrCodes(prev => new Set(prev).add(latestQr)); try { const qrData = JSON.parse(latestQr); if (qrData.stockInLineId && qrData.itemId) { setIsProcessingQr(true); setQrScanFailed(false); fetchStockInLineInfo(qrData.stockInLineId) .then((stockInLineInfo) => { 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) => { console.error("Error fetching stock in line info:", 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]); // 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 JobPickExecution: React.FC = ({ filterArgs }) => { const { t } = useTranslation("jo"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; const [jobOrderData, setJobOrderData] = useState(null); // 修改:使用 Job Order 数据结构 const [filteredLotData, setFilteredLotData] = useState([]); const [combinedDataLoading, setCombinedDataLoading] = useState(false); const [unassignedOrders, setUnassignedOrders] = useState([]); const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); const [expectedLotData, setExpectedLotData] = useState(null); const [scannedLotData, setScannedLotData] = useState(null); const [isConfirmingLot, setIsConfirmingLot] = useState(false); const [qrScanInput, setQrScanInput] = useState(''); const [qrScanError, setQrScanError] = useState(false); const [qrScanSuccess, setQrScanSuccess] = useState(false); const [pickQtyData, setPickQtyData] = useState>({}); const [searchQuery, setSearchQuery] = useState>({}); const [paginationController, setPaginationController] = useState({ pageNum: 0, pageSize: 10, }); const [usernameList, setUsernameList] = useState([]); const initializationRef = useRef(false); const autoAssignRef = useRef(false); const formProps = useForm(); const errors = formProps.formState.errors; const [isSubmittingAll, setIsSubmittingAll] = useState(false); // Add QR modal states const [qrModalOpen, setQrModalOpen] = useState(false); const [selectedLotForQr, setSelectedLotForQr] = useState(null); // Add GoodPickExecutionForm states const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState(null); const [fgPickOrders, setFgPickOrders] = useState([]); const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false); // Add these missing state variables const [isManualScanning, setIsManualScanning] = useState(false); // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling const [processedQrCombinations, setProcessedQrCombinations] = useState>>(new Map()); const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [lastProcessedQr, setLastProcessedQr] = useState(''); const [isRefreshingData, setIsRefreshingData] = useState(false); // Cache for fetchStockInLineInfo API calls to avoid redundant requests const stockInLineInfoCache = useRef>(new Map()); const CACHE_TTL = 60000; // 60 seconds cache TTL const abortControllerRef = useRef(null); const qrProcessingTimeoutRef = useRef(null); // Use refs for processed QR tracking to avoid useEffect dependency issues and delays const processedQrCodesRef = useRef>(new Set()); const lastProcessedQrRef = useRef(''); // Store callbacks in refs to avoid useEffect dependency issues const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise) | null>(null); const resetScanRef = useRef<(() => void) | null>(null); // Add Manual Lot Confirmation Modal state const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false); const getAllLotsFromHierarchical = useCallback(( data: JobOrderLotsHierarchicalResponse | null ): any[] => { if (!data || !data.pickOrder || !data.pickOrderLines) { return []; } const allLots: any[] = []; data.pickOrderLines.forEach((line) => { if (line.lots && line.lots.length > 0) { line.lots.forEach((lot) => { allLots.push({ ...lot, pickOrderLineId: line.id, itemId: line.itemId, itemCode: line.itemCode, itemName: line.itemName, uomCode: line.uomCode, uomDesc: line.uomDesc, pickOrderLineRequiredQty: line.requiredQty, pickOrderLineStatus: line.status, jobOrderId: data.pickOrder.jobOrder.id, jobOrderCode: data.pickOrder.jobOrder.code, // 添加 pickOrder 信息(如果需要) pickOrderId: data.pickOrder.id, pickOrderCode: data.pickOrder.code, pickOrderConsoCode: data.pickOrder.consoCode, pickOrderTargetDate: data.pickOrder.targetDate, pickOrderType: data.pickOrder.type, pickOrderStatus: data.pickOrder.status, pickOrderAssignTo: data.pickOrder.assignTo, }); }); } }); return allLots; }, []); const allLotsFromData = useMemo(() => { return getAllLotsFromHierarchical(jobOrderData); }, [jobOrderData, getAllLotsFromHierarchical]); // 用于显示的 combinedLotData(支持搜索过滤) const combinedLotData = useMemo(() => { return filteredLotData.length > 0 ? filteredLotData : allLotsFromData; }, [filteredLotData, allLotsFromData]); // 用于搜索的原始数据 const originalCombinedData = useMemo(() => { return allLotsFromData; }, [allLotsFromData]); // Enhanced lotDataIndexes with cached active lots for better performance const lotDataIndexes = useMemo(() => { const indexStartTime = performance.now(); console.log(`⏱️ [PERF] lotDataIndexes calculation START, data length: ${combinedLotData.length}`); const byItemId = new Map(); const byItemCode = new Map(); const byLotId = new Map(); const byLotNo = new Map(); const byStockInLineId = new Map(); // Cache active lots separately to avoid filtering on every scan const activeLotsByItemId = new Map(); const rejectedStatuses = new Set(['rejected']); // ✅ Use for loop instead of forEach for better performance on tablets for (let i = 0; i < combinedLotData.length; i++) { const lot = combinedLotData[i]; const isActive = !rejectedStatuses.has(lot.lotAvailability) && !rejectedStatuses.has(lot.stockOutLineStatus) && !rejectedStatuses.has(lot.processingStatus); if (lot.itemId) { if (!byItemId.has(lot.itemId)) { byItemId.set(lot.itemId, []); activeLotsByItemId.set(lot.itemId, []); } byItemId.get(lot.itemId)!.push(lot); if (isActive) { activeLotsByItemId.get(lot.itemId)!.push(lot); } } if (lot.itemCode) { if (!byItemCode.has(lot.itemCode)) { byItemCode.set(lot.itemCode, []); } byItemCode.get(lot.itemCode)!.push(lot); } if (lot.lotId) { byLotId.set(lot.lotId, lot); } if (lot.lotNo) { if (!byLotNo.has(lot.lotNo)) { byLotNo.set(lot.lotNo, []); } byLotNo.get(lot.lotNo)!.push(lot); } if (lot.stockInLineId) { if (!byStockInLineId.has(lot.stockInLineId)) { byStockInLineId.set(lot.stockInLineId, []); } byStockInLineId.get(lot.stockInLineId)!.push(lot); } } const indexTime = performance.now() - indexStartTime; if (indexTime > 10) { console.log(`⏱️ [PERF] lotDataIndexes calculation END: ${indexTime.toFixed(2)}ms (${(indexTime / 1000).toFixed(3)}s)`); } return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId }; }, [combinedLotData.length, combinedLotData]); // Cached version of fetchStockInLineInfo to avoid redundant API calls const fetchStockInLineInfoCached = useCallback(async (stockInLineId: number): Promise<{ lotNo: string | null }> => { const now = Date.now(); const cached = stockInLineInfoCache.current.get(stockInLineId); // Return cached value if still valid if (cached && (now - cached.timestamp) < CACHE_TTL) { console.log(`✅ [CACHE HIT] Using cached stockInLineInfo for ${stockInLineId}`); return { lotNo: cached.lotNo }; } // Cancel previous request if exists if (abortControllerRef.current) { abortControllerRef.current.abort(); } // Create new abort controller for this request const abortController = new AbortController(); abortControllerRef.current = abortController; try { console.log(`⏱️ [CACHE MISS] Fetching stockInLineInfo for ${stockInLineId}`); const stockInLineInfo = await fetchStockInLineInfo(stockInLineId); // Store in cache stockInLineInfoCache.current.set(stockInLineId, { lotNo: stockInLineInfo.lotNo || null, timestamp: now }); // Limit cache size to prevent memory leaks if (stockInLineInfoCache.current.size > 100) { const firstKey = stockInLineInfoCache.current.keys().next().value; if (firstKey !== undefined) { stockInLineInfoCache.current.delete(firstKey); } } return { lotNo: stockInLineInfo.lotNo || null }; } catch (error: any) { if (error.name === 'AbortError') { console.log(`⏱️ [CACHE] Request aborted for ${stockInLineId}`); throw error; } console.error(`❌ [CACHE] Error fetching stockInLineInfo for ${stockInLineId}:`, error); throw error; } }, []); // 修改:加载未分配的 Job Order 订单 const loadUnassignedOrders = useCallback(async () => { setIsLoadingUnassigned(true); try { const orders = await fetchUnassignedJobOrderPickOrders(); setUnassignedOrders(orders); } catch (error) { console.error("Error loading unassigned orders:", error); } finally { setIsLoadingUnassigned(false); } }, []); // 修改:分配订单给当前用户 const handleAssignOrder = useCallback(async (pickOrderId: number) => { if (!currentUserId) { console.error("Missing user id in session"); return; } try { const result = await assignJobOrderPickOrder(pickOrderId, currentUserId); if (result.message === "Successfully assigned") { console.log(" Successfully assigned pick order"); // 刷新数据 window.dispatchEvent(new CustomEvent('pickOrderAssigned')); // 重新加载未分配订单列表 loadUnassignedOrders(); } else { console.warn("⚠️ Assignment failed:", result.message); alert(`Assignment failed: ${result.message}`); } } catch (error) { console.error("❌ Error assigning order:", error); alert("Error occurred during assignment"); } }, [currentUserId, loadUnassignedOrders]); const fetchFgPickOrdersData = useCallback(async () => { if (!currentUserId) return; setFgPickOrdersLoading(true); try { // Get all pick order IDs from combinedLotData const pickOrderIds = Array.from(new Set(combinedLotData.map(lot => lot.pickOrderId))); if (pickOrderIds.length === 0) { setFgPickOrders([]); return; } // Fetch FG pick orders for each pick order ID const fgPickOrdersPromises = pickOrderIds.map(pickOrderId => fetchFGPickOrders(pickOrderId) ); const fgPickOrdersResults = await Promise.all(fgPickOrdersPromises); // Flatten the results (each fetchFGPickOrders returns an array) const allFgPickOrders = fgPickOrdersResults.flat(); setFgPickOrders(allFgPickOrders); console.log(" Fetched FG pick orders:", allFgPickOrders); } catch (error) { console.error("❌ Error fetching FG pick orders:", error); setFgPickOrders([]); } finally { setFgPickOrdersLoading(false); } }, [currentUserId, combinedLotData]); useEffect(() => { if (combinedLotData.length > 0) { fetchFgPickOrdersData(); } }, [combinedLotData, fetchFgPickOrdersData]); // 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 getPickOrderId = useCallback(() => { return filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; }, [filterArgs?.pickOrderId]); // 修改:使用 Job Order API 获取数据 const fetchJobOrderData = useCallback(async (userId?: number) => { setCombinedDataLoading(true); try { const userIdToUse = userId || currentUserId; if (!userIdToUse) { console.warn("⚠️ No userId available, skipping API call"); setJobOrderData(null); return; } window.dispatchEvent(new CustomEvent('jobOrderDataStatus', { detail: { hasData: false, tabIndex: 0 } })); // 直接使用类型化的响应 const jobOrderData = await fetchJobOrderLotsHierarchical(userIdToUse); console.log(" Job Order data (hierarchical):", jobOrderData); setJobOrderData(jobOrderData); // 使用辅助函数获取所有 lots(用于计算完成状态等) const allLots = getAllLotsFromHierarchical(jobOrderData); setFilteredLotData(allLots); const hasData = allLots.length > 0; window.dispatchEvent(new CustomEvent('jobOrderDataStatus', { detail: { hasData: hasData, tabIndex: 0 } })); // 计算完成状态 const allCompleted = allLots.length > 0 && allLots.every((lot) => lot.processingStatus === 'completed' ); window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { detail: { allLotsCompleted: allCompleted, tabIndex: 0 } })); } catch (error) { console.error("❌ Error fetching job order data:", error); setJobOrderData(null); window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { detail: { allLotsCompleted: false, tabIndex: 0 } })); } finally { setCombinedDataLoading(false); } }, [currentUserId, getAllLotsFromHierarchical]); // 修改:初始化时加载数据 useEffect(() => { if (session && currentUserId && !initializationRef.current) { console.log(" Session loaded, initializing job order..."); initializationRef.current = true; // 加载 Job Order 数据 fetchJobOrderData(); // 加载未分配订单 loadUnassignedOrders(); } }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]); // Auto-start scanner only once on mount const scannerInitializedRef = useRef(false); // Separate effect for auto-starting scanner (only once, prevents multiple resets) useEffect(() => { if (session && currentUserId && !scannerInitializedRef.current) { scannerInitializedRef.current = true; // ✅ Auto-start scanner on mount for tablet use (background mode - no modal) console.log("✅ Auto-starting QR scanner in background mode"); setIsManualScanning(true); startScan(); } }, [session, currentUserId, startScan]); // Add event listener for manual assignment useEffect(() => { const handlePickOrderAssigned = () => { console.log("🔄 Pick order assigned event received, refreshing data..."); fetchJobOrderData(); }; window.addEventListener('pickOrderAssigned', handlePickOrderAssigned); return () => { window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned); }; }, [fetchJobOrderData]); // Handle QR code submission for matched lot (external scanning) const handleQrCodeSubmit = useCallback(async (lotNo: string) => { console.log(` Processing QR Code for lot: ${lotNo}`); // Use current data without refreshing to avoid infinite loop const currentLotData = combinedLotData; console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo)); const matchingLots = currentLotData.filter(lot => lot.lotNo === lotNo || lot.lotNo?.toLowerCase() === lotNo.toLowerCase() ); 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; } 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 fetchJobOrderData(); if (successCount > 0) { console.log(` QR Code processing completed: ${successCount} updated/created`); setQrScanSuccess(true); setQrScanError(false); setQrScanInput(''); // Clear input after successful processing } else { console.error(`❌ QR Code processing failed: ${errorCount} errors`); setQrScanError(true); setQrScanSuccess(false); } } catch (error) { console.error("❌ Error processing QR code:", error); setQrScanError(true); setQrScanSuccess(false); // Still refresh data even on error setIsRefreshingData(true); await fetchJobOrderData(); } finally { // Clear refresh flag after a short delay setTimeout(() => { setIsRefreshingData(false); }, 1000); } }, [combinedLotData, fetchJobOrderData]); const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => { const mismatchStartTime = performance.now(); console.log(`⏱️ [HANDLE LOT MISMATCH START]`); console.log(`⏰ Start time: ${new Date().toISOString()}`); console.log("Lot mismatch detected:", { expectedLot, scannedLot }); // ✅ Use setTimeout to avoid flushSync warning - schedule modal update in next tick const setTimeoutStartTime = performance.now(); console.time('setLotConfirmationOpen'); setTimeout(() => { const setStateStartTime = performance.now(); setExpectedLotData(expectedLot); setScannedLotData({ ...scannedLot, lotNo: scannedLot.lotNo || null, }); setLotConfirmationOpen(true); const setStateTime = performance.now() - setStateStartTime; console.timeEnd('setLotConfirmationOpen'); console.log(`⏱️ [HANDLE LOT MISMATCH] Modal state set to open (setState time: ${setStateTime.toFixed(2)}ms)`); console.log(`✅ [HANDLE LOT MISMATCH] Modal state set to open`); }, 0); const setTimeoutTime = performance.now() - setTimeoutStartTime; console.log(`⏱️ [PERF] setTimeout scheduling time: ${setTimeoutTime.toFixed(2)}ms`); // ✅ Fetch lotNo in background ONLY for display purposes (using cached version) if (!scannedLot.lotNo && scannedLot.stockInLineId) { const stockInLineId = scannedLot.stockInLineId; if (typeof stockInLineId !== 'number') { console.warn(`⏱️ [HANDLE LOT MISMATCH] Invalid stockInLineId: ${stockInLineId}`); return; } console.log(`⏱️ [HANDLE LOT MISMATCH] Fetching lotNo in background (stockInLineId: ${stockInLineId})`); const fetchStartTime = performance.now(); fetchStockInLineInfoCached(stockInLineId) .then((stockInLineInfo) => { const fetchTime = performance.now() - fetchStartTime; console.log(`⏱️ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached time: ${fetchTime.toFixed(2)}ms (${(fetchTime / 1000).toFixed(3)}s)`); const updateStateStartTime = performance.now(); startTransition(() => { setScannedLotData((prev: any) => ({ ...prev, lotNo: stockInLineInfo.lotNo || null, })); }); const updateStateTime = performance.now() - updateStateStartTime; console.log(`⏱️ [PERF] Update scanned lot data time: ${updateStateTime.toFixed(2)}ms`); const totalTime = performance.now() - mismatchStartTime; console.log(`⏱️ [HANDLE LOT MISMATCH] Background fetch completed: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); }) .catch((error) => { if (error.name !== 'AbortError') { const fetchTime = performance.now() - fetchStartTime; console.error(`❌ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached failed after ${fetchTime.toFixed(2)}ms:`, error); } }); } else { const totalTime = performance.now() - mismatchStartTime; console.log(`⏱️ [HANDLE LOT MISMATCH END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); } }, [fetchStockInLineInfoCached]); // Add handleLotConfirmation function const handleLotConfirmation = useCallback(async () => { if (!expectedLotData || !scannedLotData || !selectedLotForQr) return; setIsConfirmingLot(true); try { const newLotNo = scannedLotData?.lotNo; const newStockInLineId = scannedLotData?.stockInLineId; await confirmLotSubstitution({ pickOrderLineId: selectedLotForQr.pickOrderLineId, stockOutLineId: selectedLotForQr.stockOutLineId, originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId, newInventoryLotNo: "", newStockInLineId: newStockInLineId }); setQrScanError(false); setQrScanSuccess(false); setQrScanInput(''); // ✅ 修复:在确认后重置扫描状态,避免重复处理 resetScan(); setPickExecutionFormOpen(false); if(selectedLotForQr?.stockOutLineId){ const stockOutLineUpdate = await updateStockOutLineStatus({ id: selectedLotForQr.stockOutLineId, status: 'checked', qty: 0 }); } // ✅ 修复:先关闭 modal 和清空状态,再刷新数据 setLotConfirmationOpen(false); setExpectedLotData(null); setScannedLotData(null); setSelectedLotForQr(null); // ✅ 修复:刷新数据前设置刷新标志,避免在刷新期间处理新的 QR code setIsRefreshingData(true); await fetchJobOrderData(); setIsRefreshingData(false); } catch (error) { console.error("Error confirming lot substitution:", error); } finally { setIsConfirmingLot(false); } }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData, resetScan]); const handleManualLotConfirmation = useCallback(async (currentLotNo: string, newLotNo: string) => { console.log(` Manual lot confirmation: Current=${currentLotNo}, New=${newLotNo}`); // 使用第一个输入框的 lot number 查找当前数据 const currentLot = combinedLotData.find(lot => lot.lotNo && lot.lotNo === currentLotNo ); if (!currentLot) { console.error(`❌ Current lot not found: ${currentLotNo}`); alert(t("Current lot number not found. Please verify and try again.")); return; } if (!currentLot.stockOutLineId) { console.error("❌ No stockOutLineId found for current lot"); alert(t("No stock out line found for current lot. Please contact administrator.")); return; } setIsConfirmingLot(true); try { // 调用 updateStockOutLineStatusByQRCodeAndLotNo API // 第一个 lot 用于获取 pickOrderLineId, stockOutLineId, itemId // 第二个 lot 作为 inventoryLotNo const res = await updateStockOutLineStatusByQRCodeAndLotNo({ pickOrderLineId: currentLot.pickOrderLineId, inventoryLotNo: newLotNo, // 第二个输入框的值 stockOutLineId: currentLot.stockOutLineId, itemId: currentLot.itemId, status: "checked", }); console.log("📥 updateStockOutLineStatusByQRCodeAndLotNo result:", res); if (res.code === "checked" || res.code === "SUCCESS") { // ✅ 更新本地状态 const entity = res.entity as any; if (filteredLotData.length > 0) { setFilteredLotData(prev => prev.map(lot => { if (lot.stockOutLineId === currentLot.stockOutLineId && lot.pickOrderLineId === currentLot.pickOrderLineId) { return { ...lot, stockOutLineStatus: 'checked', stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty, }; } return lot; })); } console.log("✅ Lot substitution completed successfully"); setQrScanSuccess(true); setQrScanError(false); // 关闭手动输入模态框 setManualLotConfirmationOpen(false); // 刷新数据 await fetchJobOrderData(); } else if (res.code === "LOT_NUMBER_MISMATCH") { console.warn("⚠️ Backend reported LOT_NUMBER_MISMATCH:", res.message); // ✅ 打开 lot confirmation modal 而不是显示 alert const expectedLotNo = currentLotNo; // 当前 lot 是期望的 // 查找新 lot 的信息(如果存在于 combinedLotData 中) const newLot = combinedLotData.find(lot => lot.lotNo && lot.lotNo === newLotNo ); // 设置 expected lot data setExpectedLotData({ lotNo: expectedLotNo, itemCode: currentLot.itemCode || '', itemName: currentLot.itemName || '' }); // 设置 scanned lot data setScannedLotData({ lotNo: newLotNo, itemCode: newLot?.itemCode || currentLot.itemCode || '', itemName: newLot?.itemName || currentLot.itemName || '', inventoryLotLineId: newLot?.lotId || null, stockInLineId: null // 手动输入时可能没有 stockInLineId }); // 设置 selectedLotForQr 为当前 lot setSelectedLotForQr(currentLot); // 关闭手动输入模态框 setManualLotConfirmationOpen(false); // 打开 lot confirmation modal setLotConfirmationOpen(true); setQrScanError(false); // 不显示错误,因为会打开确认模态框 setQrScanSuccess(false); } 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, filteredLotData, fetchJobOrderData, t]); const handleFastQrScan = useCallback(async (lotNo: string) => { const startTime = performance.now(); console.log(`⏱️ [FAST SCAN START] Lot: ${lotNo}`); console.log(`⏰ Start time: ${new Date().toISOString()}`); // 从 combinedLotData 中找到对应的 lot const findStartTime = performance.now(); const matchingLot = combinedLotData.find(lot => lot.lotNo && lot.lotNo === lotNo ); const findTime = performance.now() - findStartTime; console.log(`⏱️ Find lot time: ${findTime.toFixed(2)}ms`); if (!matchingLot || !matchingLot.stockOutLineId) { const totalTime = performance.now() - startTime; console.warn(`⚠️ Fast scan: Lot ${lotNo} not found or no stockOutLineId`); console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`); return; } try { // ✅ 使用快速 API const apiStartTime = performance.now(); const res = await updateStockOutLineStatusByQRCodeAndLotNo({ pickOrderLineId: matchingLot.pickOrderLineId, inventoryLotNo: lotNo, 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") { // ✅ 只更新本地状态,不调用 fetchJobOrderData const updateStartTime = performance.now(); const entity = res.entity as any; if (filteredLotData.length > 0) { setFilteredLotData(prev => prev.map((lot: any) => { 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, filteredLotData]); const processOutsideQrCode = useCallback(async (latestQr: string) => { const totalStartTime = performance.now(); console.log(`⏱️ [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`); console.log(`⏰ Start time: ${new Date().toISOString()}`); // ✅ 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) || []; const lookupTime = performance.now() - lookupStartTime; console.log(`⏱️ [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${activeSuggestedLots.length} active lots`); if (activeSuggestedLots.length === 0) { console.error("No active suggested lots found for this item"); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); }); 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'}`); 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); // Update filteredLotData if it exists, otherwise update allLotsFromData via refresh if (filteredLotData.length > 0) { setFilteredLotData(prev => prev.map((lot: any) => { if (lot.stockOutLineId === exactMatch.stockOutLineId && lot.pickOrderLineId === exactMatch.pickOrderLineId) { return { ...lot, stockOutLineStatus: 'checked', stockOutLineQty: entity?.qty ?? lot.stockOutLineQty, }; } return lot; })); } }); const stateUpdateTime = performance.now() - stateUpdateStartTime; console.log(`⏱️ [PERF] State update time: ${stateUpdateTime.toFixed(2)}ms`); // Mark this combination as processed const markProcessedStartTime = performance.now(); setProcessedQrCombinations(prev => { const newMap = new Map(prev); if (!newMap.has(scannedItemId)) { newMap.set(scannedItemId, new Set()); } newMap.get(scannedItemId)!.add(scannedStockInLineId); return newMap; }); const markProcessedTime = performance.now() - markProcessedStartTime; console.log(`⏱️ [PERF] Mark processed time: ${markProcessedTime.toFixed(2)}ms`); // Refresh data to ensure consistency await fetchJobOrderData(); const totalTime = performance.now() - totalStartTime; console.log(`✅ [PROCESS OUTSIDE QR END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); console.log(`⏰ End time: ${new Date().toISOString()}`); console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, api=${apiTime.toFixed(2)}ms, stateUpdate=${stateUpdateTime.toFixed(2)}ms, markProcessed=${markProcessedTime.toFixed(2)}ms`); console.log("✅ Status updated, data refreshed"); } else { 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`); // 取第一个活跃的 lot 作为期望的 lot const expectedLotStartTime = performance.now(); const expectedLot = activeSuggestedLots[0]; 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`); // ✅ 获取扫描的 lot 信息(从 QR 数据中提取,或使用默认值) // Call handleLotMismatch immediately - it will open the modal const handleMismatchStartTime = performance.now(); handleLotMismatch( { lotNo: expectedLot.lotNo, itemCode: expectedLot.itemCode, itemName: expectedLot.itemName }, { lotNo: null, // 扫描的 lotNo 未知,需要从后端获取或显示为未知 itemCode: expectedLot.itemCode, itemName: expectedLot.itemName, inventoryLotLineId: null, stockInLineId: scannedStockInLineId // ✅ 传递 stockInLineId } ); const handleMismatchTime = performance.now() - handleMismatchStartTime; console.log(`⏱️ [PERF] Handle mismatch call time: ${handleMismatchTime.toFixed(2)}ms`); const totalTime = performance.now() - totalStartTime; console.log(`⚠️ [PROCESS OUTSIDE QR MISMATCH] Total time before modal: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); console.log(`⏰ End time: ${new Date().toISOString()}`); console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, mismatchCheck=${mismatchCheckTime.toFixed(2)}ms, expectedLot=${expectedLotTime.toFixed(2)}ms, setSelectedLot=${setSelectedLotTime.toFixed(2)}ms, handleMismatch=${handleMismatchTime.toFixed(2)}ms`); } catch (error) { 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, filteredLotData, fetchJobOrderData]); // Store processOutsideQrCode in ref for immediate access (update on every render) processOutsideQrCodeRef.current = processOutsideQrCode; // Store resetScan in ref for immediate access (update on every render) resetScanRef.current = resetScan; const handleManualInputSubmit = useCallback(() => { 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 const stockOutLineData: CreateStockOutLine = { consoCode: selectedLotForQr.pickOrderConsoCode, pickOrderLineId: selectedLotForQr.pickOrderLineId, inventoryLotLineId: selectedLotForQr.lotId, qty: 0.0 }; try { await createStockOutLine(stockOutLineData); console.log("Stock out line created successfully!"); // Close modal setQrModalOpen(false); 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); // Refresh data await fetchJobOrderData(); } catch (error) { console.error("Error creating stock out line:", error); } } }, [selectedLotForQr, fetchJobOrderData]); useEffect(() => { // Skip if scanner is not active or no data available if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { return; } const qrValuesChangeStartTime = performance.now(); console.log(`⏱️ [QR VALUES EFFECT] Triggered at: ${new Date().toISOString()}`); console.log(`⏱️ [QR VALUES EFFECT] qrValues.length: ${qrValues.length}`); console.log(`⏱️ [QR VALUES EFFECT] qrValues:`, qrValues); const latestQr = qrValues[qrValues.length - 1]; console.log(`⏱️ [QR VALUES EFFECT] Latest QR: ${latestQr}`); console.log(`⏰ [QR VALUES EFFECT] Latest QR detected at: ${new Date().toISOString()}`); // ✅ FIXED: Handle test shortcut {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId // Support both formats: {2fitest (2 t's) and {2fittest (3 t's) if ((latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) && latestQr.endsWith("}")) { // Extract content: remove "{2fitest" or "{2fittest" and "}" let content = ''; if (latestQr.startsWith("{2fittest")) { content = latestQr.substring(9, latestQr.length - 1); // Remove "{2fittest" and "}" } else if (latestQr.startsWith("{2fitest")) { content = latestQr.substring(8, latestQr.length - 1); // Remove "{2fitest" and "}" } const parts = content.split(','); if (parts.length === 2) { const itemId = parseInt(parts[0].trim(), 10); const stockInLineId = parseInt(parts[1].trim(), 10); if (!isNaN(itemId) && !isNaN(stockInLineId)) { console.log( `%c TEST QR: Detected ${latestQr.substring(0, 9)}... - Simulating QR input (itemId=${itemId}, stockInLineId=${stockInLineId})`, "color: purple; font-weight: bold" ); // ✅ Simulate QR code JSON format const simulatedQr = JSON.stringify({ itemId: itemId, stockInLineId: stockInLineId }); console.log(`⏱️ [TEST QR] Simulated QR content: ${simulatedQr}`); console.log(`⏱️ [TEST QR] Start time: ${new Date().toISOString()}`); const testStartTime = performance.now(); // ✅ Mark as processed FIRST to avoid duplicate processing lastProcessedQrRef.current = latestQr; processedQrCodesRef.current.add(latestQr); if (processedQrCodesRef.current.size > 100) { const firstValue = processedQrCodesRef.current.values().next().value; if (firstValue !== undefined) { processedQrCodesRef.current.delete(firstValue); } } setLastProcessedQr(latestQr); setProcessedQrCodes(new Set(processedQrCodesRef.current)); // ✅ Process immediately (bypass QR scanner delay) if (processOutsideQrCodeRef.current) { processOutsideQrCodeRef.current(simulatedQr).then(() => { const testTime = performance.now() - testStartTime; console.log(`⏱️ [TEST QR] Total processing time: ${testTime.toFixed(2)}ms (${(testTime / 1000).toFixed(3)}s)`); console.log(`⏱️ [TEST QR] End time: ${new Date().toISOString()}`); }).catch((error) => { const testTime = performance.now() - testStartTime; console.error(`❌ [TEST QR] Error after ${testTime.toFixed(2)}ms:`, error); }); } // Reset scan if (resetScanRef.current) { resetScanRef.current(); } const qrValuesChangeTime = performance.now() - qrValuesChangeStartTime; console.log(`⏱️ [QR VALUES EFFECT] Test QR handling time: ${qrValuesChangeTime.toFixed(2)}ms`); return; // ✅ IMPORTANT: Return early to prevent normal processing } else { console.warn(`⏱️ [TEST QR] Invalid itemId or stockInLineId: itemId=${parts[0]}, stockInLineId=${parts[1]}`); } } else { console.warn(`⏱️ [TEST QR] Invalid format. Expected {2fitestx,y} or {2fittestx,y}, got: ${latestQr}`); } } // Skip processing if confirmation modals are open // BUT: Allow processing if modal was just closed (to allow reopening for different stockInLineId) if (lotConfirmationOpen || manualLotConfirmationOpen) { // Check if this is a different QR code than what triggered the modal const modalTriggerQr = lastProcessedQrRef.current; if (latestQr === modalTriggerQr) { console.log(`⏱️ [QR PROCESS] Skipping - modal open for same QR: lotConfirmation=${lotConfirmationOpen}, manual=${manualLotConfirmationOpen}`); return; } // If it's a different QR, allow processing (user might have canceled and scanned different lot) console.log(`⏱️ [QR PROCESS] Different QR detected while modal open, allowing processing`); } const qrDetectionStartTime = performance.now(); console.log(`⏱️ [QR DETECTION] Latest QR detected: ${latestQr?.substring(0, 50)}...`); console.log(`⏰ [QR DETECTION] Detection time: ${new Date().toISOString()}`); console.log(`⏱️ [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`); // Skip if already processed (use refs to avoid dependency issues and delays) const checkProcessedStartTime = performance.now(); if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) { const checkTime = performance.now() - checkProcessedStartTime; console.log(`⏱️ [QR PROCESS] Already processed check time: ${checkTime.toFixed(2)}ms`); return; } const checkTime = performance.now() - checkProcessedStartTime; console.log(`⏱️ [QR PROCESS] Not processed check time: ${checkTime.toFixed(2)}ms`); // Handle special shortcut if (latestQr === "{2fic}") { console.log(" Detected {2fic} shortcut - opening manual lot confirmation form"); setManualLotConfirmationOpen(true); if (resetScanRef.current) { resetScanRef.current(); } lastProcessedQrRef.current = latestQr; processedQrCodesRef.current.add(latestQr); if (processedQrCodesRef.current.size > 100) { const firstValue = processedQrCodesRef.current.values().next().value; if (firstValue !== undefined) { processedQrCodesRef.current.delete(firstValue); } } setLastProcessedQr(latestQr); setProcessedQrCodes(prev => { const newSet = new Set(prev); newSet.add(latestQr); if (newSet.size > 100) { const firstValue = newSet.values().next().value; if (firstValue !== undefined) { newSet.delete(firstValue); } } return newSet; }); return; } // Process new QR code immediately (background mode - no modal) // Check against refs to avoid state update delays if (latestQr && latestQr !== lastProcessedQrRef.current) { const processingStartTime = performance.now(); console.log(`⏱️ [QR PROCESS] Starting processing at: ${new Date().toISOString()}`); console.log(`⏱️ [QR PROCESS] Time since detection: ${(processingStartTime - qrDetectionStartTime).toFixed(2)}ms`); // ✅ Process immediately for better responsiveness // Clear any pending debounced processing if (qrProcessingTimeoutRef.current) { clearTimeout(qrProcessingTimeoutRef.current); qrProcessingTimeoutRef.current = null; } // Log immediately (console.log is synchronous) console.log(`⏱️ [QR PROCESS] Processing new QR code with enhanced validation: ${latestQr}`); // Update refs immediately (no state update delay) - do this FIRST const refUpdateStartTime = performance.now(); lastProcessedQrRef.current = latestQr; processedQrCodesRef.current.add(latestQr); if (processedQrCodesRef.current.size > 100) { const firstValue = processedQrCodesRef.current.values().next().value; if (firstValue !== undefined) { processedQrCodesRef.current.delete(firstValue); } } const refUpdateTime = performance.now() - refUpdateStartTime; console.log(`⏱️ [QR PROCESS] Ref update time: ${refUpdateTime.toFixed(2)}ms`); // Process immediately in background - no modal/form needed, no delays // Use ref to avoid dependency issues const processCallStartTime = performance.now(); if (processOutsideQrCodeRef.current) { processOutsideQrCodeRef.current(latestQr).then(() => { const processCallTime = performance.now() - processCallStartTime; const totalProcessingTime = performance.now() - processingStartTime; console.log(`⏱️ [QR PROCESS] processOutsideQrCode call time: ${processCallTime.toFixed(2)}ms`); console.log(`⏱️ [QR PROCESS] Total processing time: ${totalProcessingTime.toFixed(2)}ms (${(totalProcessingTime / 1000).toFixed(3)}s)`); }).catch((error) => { const processCallTime = performance.now() - processCallStartTime; const totalProcessingTime = performance.now() - processingStartTime; console.error(`❌ [QR PROCESS] processOutsideQrCode error after ${processCallTime.toFixed(2)}ms:`, error); console.error(`❌ [QR PROCESS] Total processing time before error: ${totalProcessingTime.toFixed(2)}ms`); }); } // Update state for UI (but don't block on it) const stateUpdateStartTime = performance.now(); setLastProcessedQr(latestQr); setProcessedQrCodes(new Set(processedQrCodesRef.current)); const stateUpdateTime = performance.now() - stateUpdateStartTime; console.log(`⏱️ [QR PROCESS] State update time: ${stateUpdateTime.toFixed(2)}ms`); const detectionTime = performance.now() - qrDetectionStartTime; const totalEffectTime = performance.now() - qrValuesChangeStartTime; console.log(`⏱️ [QR DETECTION] Total detection time: ${detectionTime.toFixed(2)}ms`); console.log(`⏱️ [QR VALUES EFFECT] Total effect time: ${totalEffectTime.toFixed(2)}ms`); } return () => { if (qrProcessingTimeoutRef.current) { clearTimeout(qrProcessingTimeoutRef.current); qrProcessingTimeoutRef.current = null; } }; }, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]); const 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]); // Handle submit pick quantity const handleSubmitPickQty = useCallback(async (lot: any) => { const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; const newQty = pickQtyData[lotKey] || 0; if (!lot.stockOutLineId) { console.error("No stock out line found for this lot"); return; } try { const currentActualPickQty = lot.actualPickQty || 0; const cumulativeQty = currentActualPickQty + newQty; let newStatus = 'partially_completed'; if (cumulativeQty >= lot.requiredQty) { newStatus = 'completed'; } 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: ${newQty}`); console.log(`Cumulative Qty: ${cumulativeQty}`); console.log(`New Status: ${newStatus}`); console.log(`=====================================`); await updateStockOutLineStatus({ id: lot.stockOutLineId, status: newStatus, qty: cumulativeQty }); if (newQty > 0) { await updateInventoryLotLineQuantities({ inventoryLotLineId: lot.lotId, qty: newQty, status: 'available', operation: 'pick' }); } // FIXED: Use the proper API function instead of direct fetch if (newStatus === 'completed' && lot.pickOrderConsoCode) { console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`); try { // Use the imported API function instead of direct fetch 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); } } await fetchJobOrderData(); console.log("Pick quantity submitted successfully!"); setTimeout(() => { checkAndAutoAssignNext(); }, 1000); } catch (error) { console.error("Error submitting pick quantity:", error); } }, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]); const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => { console.log('=== handleSubmitPickQtyWithQty called ==='); console.log('Lot:', lot); console.log('submitQty:', submitQty); console.log('stockOutLineId:', lot.stockOutLineId); if (!lot.stockOutLineId) { console.error("No stock out line found for this lot:", lot); alert(`Error: No stock out line ID found for lot ${lot.lotNo}. Cannot update status.`); return; } try { // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0 if (submitQty === 0) { console.log(`=== SUBMITTING ALL ZEROS CASE ===`); console.log(`Lot: ${lot.lotNo}`); console.log(`Stock Out Line ID: ${lot.stockOutLineId}`); console.log(`Setting status to 'completed' with qty: 0`); const updateResult = await updateStockOutLineStatus({ id: lot.stockOutLineId, status: 'completed', qty: 0 }); console.log('Update result:', updateResult); if (!updateResult || (updateResult as any).code !== 'SUCCESS') { console.error('Failed to update stock out line status:', updateResult); throw new Error('Failed to update stock out line status'); } // Check if pick order is completed if (lot.pickOrderConsoCode) { console.log(` Lot ${lot.lotNo} completed (all zeros), 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); } } await fetchJobOrderData(); console.log("All zeros submission completed successfully!"); setTimeout(() => { checkAndAutoAssignNext(); }, 1000); return; } // Normal case: Calculate cumulative quantity correctly const currentActualPickQty = lot.actualPickQty || 0; const cumulativeQty = currentActualPickQty + submitQty; // 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: ${submitQty}`); console.log(`Cumulative Qty: ${cumulativeQty}`); console.log(`New Status: ${newStatus}`); console.log(`=====================================`); await updateStockOutLineStatus({ id: lot.stockOutLineId, status: newStatus, qty: cumulativeQty }); if (submitQty > 0) { await updateInventoryLotLineQuantities({ inventoryLotLineId: lot.lotId, qty: submitQty, status: 'available', operation: 'pick' }); } // 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); } } await fetchJobOrderData(); console.log("Pick quantity submitted successfully!"); setTimeout(() => { checkAndAutoAssignNext(); }, 1000); } catch (error) { console.error("Error submitting pick quantity:", error); } }, [fetchJobOrderData, checkAndAutoAssignNext]); const handleSubmitAllScanned = useCallback(async () => { const scannedLots = combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked' ); 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 所需的格式 const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => { const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty || 0; const currentActualPickQty = lot.actualPickQty || 0; const cumulativeQty = currentActualPickQty + submitQty; let newStatus = 'partially_completed'; if (cumulativeQty >= (lot.requiredQty || 0)) { newStatus = 'completed'; } return { stockOutLineId: Number(lot.stockOutLineId) || 0, pickOrderLineId: Number(lot.pickOrderLineId), inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null, requiredQty: Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0), actualPickQty: Number(cumulativeQty), stockOutLineStatus: newStatus, pickOrderConsoCode: String(lot.pickOrderConsoCode || ''), noLot: Boolean(false) // Job Order 通常都有 lot }; }); const request: batchSubmitListRequest = { userId: currentUserId || 0, lines: lines }; // 使用 batchSubmitList API const result = await batchSubmitList(request); console.log(`📥 Batch submit result:`, result); // 刷新数据 await fetchJobOrderData(); // 或 pickOrderId,根据页面 if (result && result.code === "SUCCESS") { setQrScanSuccess(true); setTimeout(() => { setQrScanSuccess(false); checkAndAutoAssignNext(); }, 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, fetchJobOrderData, checkAndAutoAssignNext, currentUserId]); // Calculate scanned items count const scannedItemsCount = useMemo(() => { return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length; }, [combinedLotData]); // Handle reject lot const handleRejectLot = useCallback(async (lot: any) => { if (!lot.stockOutLineId) { console.error("No stock out line found for this lot"); return; } try { await updateStockOutLineStatus({ id: lot.stockOutLineId, status: 'rejected', qty: 0 }); await fetchJobOrderData(); console.log("Lot rejected successfully!"); setTimeout(() => { checkAndAutoAssignNext(); }, 1000); } catch (error) { console.error("Error rejecting lot:", error); } }, [fetchJobOrderData, checkAndAutoAssignNext]); // 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: "Jo", // Delivery Order Record 类型 }; const result = await recordPickExecutionIssue(issueData); console.log("Pick execution issue recorded:", result); if (result && result.code === "SUCCESS") { console.log(" Pick execution issue recorded successfully"); } else { console.error("❌ Failed to record pick execution issue:", result); } setPickExecutionFormOpen(false); setSelectedLotForExecutionForm(null); await fetchJobOrderData(); } catch (error) { console.error("Error submitting pick execution form:", error); } }, [fetchJobOrderData]); // 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 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, }); }, []); // Pagination data with sorting by routerIndex const paginatedData = useMemo(() => { // Sort by routerIndex first, then by other criteria const sortedData = [...combinedLotData].sort((a, b) => { const aIndex = a.routerIndex || 0; const bIndex = b.routerIndex || 0; // Primary sort: by routerIndex if (aIndex !== bIndex) { return aIndex - bIndex; } // Secondary sort: by pickOrderCode if routerIndex is the same if (a.pickOrderCode !== b.pickOrderCode) { return a.pickOrderCode.localeCompare(b.pickOrderCode); } // Tertiary sort: by lotNo if everything else is the same return (a.lotNo || '').localeCompare(b.lotNo || ''); }); const startIndex = paginationController.pageNum * paginationController.pageSize; const endIndex = startIndex + paginationController.pageSize; return sortedData.slice(startIndex, endIndex); }, [combinedLotData, paginationController]); // Add these functions for manual scanning const handleStartScan = useCallback(() => { console.log(" Starting manual QR scan..."); setIsManualScanning(true); setProcessedQrCodes(new Set()); setLastProcessedQr(''); setQrScanError(false); setQrScanSuccess(false); startScan(); }, [startScan]); const handleStopScan = useCallback(() => { console.log("⏹️ Stopping manual QR scan..."); setIsManualScanning(false); setQrScanError(false); setQrScanSuccess(false); stopScan(); resetScan(); }, [stopScan, resetScan]); useEffect(() => { return () => { // Cleanup when component unmounts (e.g., when switching tabs) if (isManualScanning) { console.log("🧹 Component unmounting, stopping QR scanner..."); stopScan(); resetScan(); } }; }, [isManualScanning, stopScan, resetScan]); 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("🧹 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' )} > {/* Job Order Header */} {jobOrderData && ( {t("Job Order")}: {jobOrderData.pickOrder?.jobOrder?.code || '-'} {t("Pick Order Code")}: {jobOrderData.pickOrder?.code || '-'} {t("Target Date")}: {jobOrderData.pickOrder?.targetDate || '-'} )} {/* Combined Lot Table */} {!isManualScanning ? ( ) : ( )} {/* ADD THIS: Submit All Scanned Button */} {qrScanError && !qrScanSuccess && ( {t("QR code does not match any item in current orders.")} )} {qrScanSuccess && ( {t("QR code verified.")} )} {t("Index")} {t("Route")} {t("Item Code")} {t("Item Name")} {t("Lot No")} {t("Lot Required Pick Qty")} {t("Scan Result")} {t("Submit Required Pick Qty")} {paginatedData.length === 0 ? ( {t("No data available")} ) : ( paginatedData.map((lot, index) => ( {index + 1} {lot.routerRoute || '-'} {lot.itemCode} {lot.itemName+'('+lot.uomDesc+')'} {lot.lotNo} {(() => { const requiredQty = lot.requiredQty || 0; return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; })()} {lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? ( ) : null} )) )}
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` } />
{/* QR Code Modal */} {!lotConfirmationOpen && ( { setQrModalOpen(false); setSelectedLotForQr(null); stopScan(); resetScan(); }} lot={selectedLotForQr} combinedLotData={combinedLotData} onQrCodeSubmit={handleQrCodeSubmitFromModal} /> )} {/* Add Lot Confirmation Modal */} {lotConfirmationOpen && expectedLotData && scannedLotData && ( { setLotConfirmationOpen(false); setExpectedLotData(null); setScannedLotData(null); setSelectedLotForQr(null); }} onConfirm={handleLotConfirmation} expectedLot={expectedLotData} scannedLot={scannedLotData} isLoading={isConfirmingLot} /> )} {/* Manual Lot Confirmation Modal */} { setManualLotConfirmationOpen(false); }} onConfirm={handleManualLotConfirmation} expectedLot={expectedLotData} scannedLot={scannedLotData} isLoading={isConfirmingLot} /> {/* 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, // Add missing required properties from GetPickOrderLineInfo interface availableQty: selectedLotForExecutionForm.availableQty || 0, requiredQty: selectedLotForExecutionForm.requiredQty || 0, uomDesc: selectedLotForExecutionForm.uomDesc || '', uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '', pickedQty: selectedLotForExecutionForm.actualPickQty || 0, suggestedList: [], noLotLines: [] }} pickOrderId={selectedLotForExecutionForm.pickOrderId} pickOrderCreateDate={new Date()} onNormalPickSubmit={async (lot, submitQty) => { console.log('onNormalPickSubmit called in newJobPickExecution:', { lot, submitQty }); if (!lot) { console.error('Lot is null or undefined'); return; } const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; handlePickQtyChange(lotKey, submitQty); await handleSubmitPickQtyWithQty(lot, submitQty); }} /> )}
); }; export default JobPickExecution