"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 { useCallback, useEffect, useState, useRef, useMemo } 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 } 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"; interface Props { filterArgs: Record; onSwitchToRecordTab?: () => void; onRefreshReleasedOrderCount?: () => void; } // 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 }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { 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); // 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 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} )} ); }; 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([]); const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const [qrScanInput, setQrScanInput] = useState(''); const [qrScanError, setQrScanError] = useState(false); const [qrScanSuccess, setQrScanSuccess] = useState(false); const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = 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; // Add QR modal states const [qrModalOpen, setQrModalOpen] = useState(false); const [selectedLotForQr, setSelectedLotForQr] = useState(null); const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); 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); // Add these missing state variables after line 352 const [isManualScanning, setIsManualScanning] = useState(false); const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [lastProcessedQr, setLastProcessedQr] = useState(''); const [isRefreshingData, setIsRefreshingData] = useState(false); const [isSubmittingAll, setIsSubmittingAll] = useState(false); // 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 handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => { console.log("Lot mismatch detected:", { expectedLot, scannedLot }); setExpectedLotData(expectedLot); setScannedLotData(scannedLot); setLotConfirmationOpen(true); }, []); 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); 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); 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] || "", // 兼容字段 pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0, pickOrderConsoCode: mergedPickOrder.consoCode || "", 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 信息 - 保持数组格式,不要 join 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); // 移除:不需要 doPickOrderDetail 和 switcher 逻辑 // if (hierarchicalData.pickOrders.length > 1) { ... } // 直接使用合并后的 pickOrderLines console.log("🎯 Displaying merged pick order lines"); // 将层级数据转换为平铺格式(用于表格显示) const flatLotData: any[] = []; mergedPickOrder.pickOrderLines.forEach((line: any) => { // ✅ FIXED: 处理 lots(如果有) if (line.lots && line.lots.length > 0) { // 修复:先对 lots 按 lotId 去重并合并 requiredQty const lotMap = new Map(); line.lots.forEach((lot: any) => { const lotId = lot.id; if (lotMap.has(lotId)) { // 如果已存在,合并 requiredQty const existingLot = lotMap.get(lotId); existingLot.requiredQty = (existingLot.requiredQty || 0) + (lot.requiredQty || 0); // 保留其他字段(使用第一个遇到的 lot 的字段) } else { // 首次遇到,添加到 map lotMap.set(lotId, { ...lot }); } }); // 遍历去重后的 lots lotMap.forEach((lot: any) => { flatLotData.push({ // 使用合并后的数据 pickOrderConsoCode: mergedPickOrder.consoCode, pickOrderTargetDate: mergedPickOrder.targetDate, pickOrderStatus: mergedPickOrder.status, pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0, // 使用第一个 pickOrderId 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, // 使用合并后的 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, routerId: lot.router?.id, routerIndex: lot.router?.index, routerRoute: lot.router?.route, routerArea: lot.router?.area, noLot: false, }); }); } // ✅ FIXED: 同时处理 stockouts(无论是否有 lots) if (line.stockouts && line.stockouts.length > 0) { // ✅ FIXED: 处理所有 stockouts,而不仅仅是第一个 line.stockouts.forEach((stockout: any) => { flatLotData.push({ pickOrderConsoCode: 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, // Null stock 字段 - 从 stockouts 数组中获取 lotId: stockout.lotId || null, lotNo: stockout.lotNo || null, expiryDate: null, location: stockout.location || null, stockUnit: line.item.uomDesc, availableQty: stockout.availableQty || 0, requiredQty: line.requiredQty, actualPickQty: stockout.qty || 0, inQty: 0, outQty: 0, holdQty: 0, lotStatus: 'unavailable', lotAvailability: 'insufficient_stock', processingStatus: stockout.status || 'pending', suggestedPickLotId: null, stockOutLineId: stockout.id || null, // 使用 stockouts 数组中的 id stockOutLineStatus: stockout.status || null, stockOutLineQty: stockout.qty || 0, routerId: null, routerIndex: 999999, routerRoute: null, routerArea: null, noLot: true, }); }); } }); console.log(" Transformed flat lot data:", flatLotData); console.log("🔍 Total items (including null stock):", flatLotData.length); setCombinedLotData(flatLotData); setOriginalCombinedData(flatLotData); checkAllLotsCompleted(flatLotData); } catch (error) { console.error(" Error fetching combined lot data:", error); setCombinedLotData([]); setOriginalCombinedData([]); setAllLotsCompleted(false); } 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 handleLotConfirmation = useCallback(async () => { if (!expectedLotData || !scannedLotData || !selectedLotForQr) return; setIsConfirmingLot(true); try { const newLotNo = scannedLotData?.lotNo; if (!newLotNo) { console.error("No lot number for scanned lot"); alert(t("Cannot find lot number for scanned lot. Please verify the lot number is correct.")); setIsConfirmingLot(false); return; } await confirmLotSubstitution({ pickOrderLineId: selectedLotForQr.pickOrderLineId, stockOutLineId: selectedLotForQr.stockOutLineId, originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId, newInventoryLotNo: newLotNo }); setQrScanError(false); setQrScanSuccess(false); setQrScanInput(''); // ✅ 修复:在确认后重置扫描状态,避免重复处理 resetScan(); // ✅ 修复:不要清空 processedQrCodes,而是保留当前 QR code 的标记 // 或者如果确实需要清空,应该在重置扫描后再清空 // setProcessedQrCodes(new Set()); // setLastProcessedQr(''); setQrModalOpen(false); 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 fetchAllCombinedLotData(); setIsRefreshingData(false); } catch (error) { console.error("Error confirming lot substitution:", error); } finally { setIsConfirmingLot(false); } }, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData, resetScan]); 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; } 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]); const processOutsideQrCode = useCallback(async (latestQr: string) => { // 1) Parse JSON safely let qrData: any = null; try { qrData = JSON.parse(latestQr); } catch { console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches."); setQrScanError(true); setQrScanSuccess(false); return; } try { // Only use the new API when we have JSON with stockInLineId + itemId if (!(qrData?.stockInLineId && qrData?.itemId)) { console.log("QR JSON missing required fields (itemId, stockInLineId)."); setQrScanError(true); setQrScanSuccess(false); return; } // Call new analyze-qr-code API const analysis = await analyzeQrCode({ itemId: qrData.itemId, stockInLineId: qrData.stockInLineId }); if (!analysis) { console.error("analyzeQrCode returned no data"); setQrScanError(true); setQrScanSuccess(false); return; } const { itemId: analyzedItemId, itemCode: analyzedItemCode, itemName: analyzedItemName, scanned, } = analysis || {}; // 1) Find all lots for the same item from current expected list const sameItemLotsInExpected = combinedLotData.filter(l => (l.itemId && analyzedItemId && l.itemId === analyzedItemId) || (l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode) ); if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) { // Case 3: No item code match console.error("No item match in expected lots for scanned code"); setQrScanError(true); setQrScanSuccess(false); return; } // FIXED: Find the ACTIVE suggested lot (not rejected lots) const activeSuggestedLots = sameItemLotsInExpected.filter(lot => lot.lotAvailability !== 'rejected' && lot.stockOutLineStatus !== 'rejected' && lot.processingStatus !== 'rejected' ); if (activeSuggestedLots.length === 0) { console.error("No active suggested lots found for this item"); setQrScanError(true); setQrScanSuccess(false); return; } // 2) Check if scanned lot is exactly in active suggested lots const exactLotMatch = activeSuggestedLots.find(l => (scanned?.inventoryLotLineId && l.lotId === scanned.inventoryLotLineId) || (scanned?.lotNo && l.lotNo === scanned.lotNo) ); if (exactLotMatch && scanned?.lotNo) { // ✅ Case 1: 使用 updateStockOutLineStatusByQRCodeAndLotNo API(更快) console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`); if (!exactLotMatch.stockOutLineId) { console.warn("No stockOutLineId on exactLotMatch, cannot update status by QR."); setQrScanError(true); setQrScanSuccess(false); return; } try { // ✅ 直接调用后端 API,后端会处理所有匹配逻辑 const res = await updateStockOutLineStatusByQRCodeAndLotNo({ pickOrderLineId: exactLotMatch.pickOrderLineId, inventoryLotNo: scanned.lotNo, stockOutLineId: exactLotMatch.stockOutLineId, itemId: exactLotMatch.itemId, status: "checked", }); console.log("updateStockOutLineStatusByQRCodeAndLotNo result:", res); // 后端返回三种 code:checked / LOT_NUMBER_MISMATCH / ITEM_MISMATCH if (res.code === "checked" || res.code === "SUCCESS") { // ✅ 完全匹配 - 只更新本地状态,不调用 fetchAllCombinedLotData setQrScanError(false); setQrScanSuccess(true); // ✅ 更新本地状态 const entity = res.entity as any; setCombinedLotData(prev => prev.map(lot => { if (lot.stockOutLineId === exactLotMatch.stockOutLineId && lot.pickOrderLineId === exactLotMatch.pickOrderLineId) { return { ...lot, stockOutLineStatus: 'checked', stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty, }; } return lot; })); setOriginalCombinedData(prev => prev.map(lot => { if (lot.stockOutLineId === exactLotMatch.stockOutLineId && lot.pickOrderLineId === exactLotMatch.pickOrderLineId) { return { ...lot, stockOutLineStatus: 'checked', stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty, }; } return lot; })); console.log("✅ Status updated locally, no full data refresh needed"); } else if (res.code === "LOT_NUMBER_MISMATCH") { console.warn("Backend reported LOT_NUMBER_MISMATCH:", res.message); setQrScanError(true); setQrScanSuccess(false); } else if (res.code === "ITEM_MISMATCH") { console.warn("Backend reported ITEM_MISMATCH:", res.message); setQrScanError(true); setQrScanSuccess(false); } else { console.warn("Unexpected response code from backend:", res.code); setQrScanError(true); setQrScanSuccess(false); } } catch (e) { console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e); setQrScanError(true); setQrScanSuccess(false); } return; // ✅ 直接返回,不再调用 handleQrCodeSubmit } // Case 2: Item matches but lot number differs -> open confirmation modal const expectedLot = activeSuggestedLots[0]; if (!expectedLot) { console.error("Could not determine expected lot for confirmation"); setQrScanError(true); setQrScanSuccess(false); return; } // Check if the expected lot is already the scanned lot (after substitution) if (expectedLot.lotNo === scanned?.lotNo) { console.log(`Lot already substituted, proceeding with ${scanned.lotNo}`); handleQrCodeSubmit(scanned.lotNo); return; } console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); setSelectedLotForQr(expectedLot); handleLotMismatch( { lotNo: expectedLot.lotNo, itemCode: analyzedItemCode || expectedLot.itemCode, itemName: analyzedItemName || expectedLot.itemName }, { lotNo: scanned?.lotNo || '', itemCode: analyzedItemCode || expectedLot.itemCode, itemName: analyzedItemName || expectedLot.itemName, inventoryLotLineId: scanned?.inventoryLotLineId, stockInLineId: qrData.stockInLineId } ); } catch (error) { console.error("Error during analyzeQrCode flow:", error); setQrScanError(true); setQrScanSuccess(false); return; } }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch]); // Update the outside QR scanning effect to use enhanced processing // Update the outside QR scanning effect to use enhanced processing useEffect(() => { if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { return; } const latestQr = qrValues[qrValues.length - 1]; if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { console.log("QR code already processed, skipping..."); return; } if (latestQr === "{2fic}") { console.log("🔍 Detected {2fic} shortcut - opening manual lot confirmation form"); setManualLotConfirmationOpen(true); resetScan(); setLastProcessedQr(latestQr); setProcessedQrCodes(prev => new Set(prev).add(latestQr)); return; // 直接返回,不继续处理其他逻辑 } if (latestQr && latestQr !== lastProcessedQr) { console.log(`🔍 Processing new QR code with enhanced validation: ${latestQr}`); setLastProcessedQr(latestQr); setProcessedQrCodes(prev => new Set(prev).add(latestQr)); processOutsideQrCode(latestQr); } }, [qrValues, isManualScanning, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode, combinedLotData]); // Only fetch existing data when session is ready, no auto-assignment 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]); // 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); // 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); } 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]); // 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 { // FIXED: Calculate cumulative quantity correctly const currentActualPickQty = lot.actualPickQty || 0; const cumulativeQty = currentActualPickQty + newQty; // 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: ${newQty}`); console.log(`Cumulative Qty: ${cumulativeQty}`); console.log(`New Status: ${newStatus}`); console.log(`=====================================`); await updateStockOutLineStatus({ id: lot.stockOutLineId, status: newStatus, qty: cumulativeQty // Use cumulative quantity }); if (newQty > 0) { await updateInventoryLotLineQuantities({ inventoryLotLineId: lot.lotId, qty: newQty, 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 fetchAllCombinedLotData(); console.log("Pick quantity submitted successfully!"); setTimeout(() => { checkAndAutoAssignNext(); }, 1000); } catch (error) { console.error("Error submitting pick quantity:", error); } }, [pickQtyData, fetchAllCombinedLotData, checkAndAutoAssignNext]); // 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 fetchAllCombinedLotData(); console.log("Lot rejected successfully!"); setTimeout(() => { checkAndAutoAssignNext(); }, 1000); } catch (error) { console.error("Error rejecting lot:", error); } }, [fetchAllCombinedLotData, 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: "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"); } else { console.error(" Failed to record pick execution issue:", result); } setPickExecutionFormOpen(false); setSelectedLotForExecutionForm(null); setQrScanError(false); setQrScanSuccess(false); setQrScanInput(''); setIsManualScanning(false); stopScan(); resetScan(); setProcessedQrCodes(new Set()); setLastProcessedQr(''); await fetchAllCombinedLotData(); } catch (error) { console.error("Error submitting pick execution form:", error); } }, [fetchAllCombinedLotData]); // 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, }); }, []); // Pagination data with sorting by routerIndex // Remove the sorting logic and just do pagination const paginatedData = useMemo(() => { const startIndex = paginationController.pageNum * paginationController.pageSize; const endIndex = startIndex + paginationController.pageSize; return combinedLotData.slice(startIndex, endIndex); // No sorting needed }, [combinedLotData, paginationController]); 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(可以提交) if (lot.noLot === true) { return isChecked || isCompleted || isRejected || isPending; } // 正常 lot:必须已扫描/提交或者被拒收 return isChecked || isCompleted || isRejected; }); }, [combinedLotData]); const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => { if (!lot.stockOutLineId) { console.error("No stock out line found for this lot"); return; } try { // FIXED: Calculate cumulative quantity correctly const currentActualPickQty = lot.actualPickQty || 0; const cumulativeQty = currentActualPickQty + submitQty; // 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: ${submitQty}`); console.log(`Cumulative Qty: ${cumulativeQty}`); console.log(`New Status: ${newStatus}`); console.log(`=====================================`); await updateStockOutLineStatus({ id: lot.stockOutLineId, status: newStatus, qty: cumulativeQty // Use cumulative quantity }); 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 fetchAllCombinedLotData(); console.log("Pick quantity submitted successfully!"); setTimeout(() => { checkAndAutoAssignNext(); }, 1000); } catch (error) { console.error("Error submitting pick quantity:", error); } }, [fetchAllCombinedLotData, checkAndAutoAssignNext]); // Add these functions after line 395 const handleStartScan = useCallback(() => { console.log(" Starting manual QR scan..."); setIsManualScanning(true); setProcessedQrCodes(new Set()); setLastProcessedQr(''); setQrScanError(false); setQrScanSuccess(false); startScan(); }, [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("⏹️ Stopping manual QR scan..."); 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; } try { // 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); } }, [fetchAllCombinedLotData, session, currentUserId, fgPickOrders]); // ... existing code ... 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 => { // 如果是 noLot 情况,检查状态是否为 pending 或 partially_complete if (lot.noLot === true) { return lot.stockOutLineStatus === 'checked' || lot.stockOutLineStatus === 'pending' || lot.stockOutLineStatus === 'partially_completed' || lot.stockOutLineStatus === 'PARTIALLY_COMPLETE'; } // 正常情况:只包含 checked 状态 return 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 所需的格式(与后端 QrPickBatchSubmitRequest 匹配) 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(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]); // Calculate scanned items count // Calculate scanned items count (should match handleSubmitAllScanned filter logic) const scannedItemsCount = useMemo(() => { const filtered = combinedLotData.filter(lot => { // ✅ FIXED: 使用与 handleSubmitAllScanned 相同的过滤逻辑 if (lot.noLot === true) { // ✅ 只包含可以提交的状态(与 handleSubmitAllScanned 保持一致) return lot.stockOutLineStatus === 'checked' || lot.stockOutLineStatus === 'pending' || lot.stockOutLineStatus === 'partially_completed' || lot.stockOutLineStatus === 'PARTIALLY_COMPLETE'; } // 正常情况:只包含 checked 状态 return lot.stockOutLineStatus === 'checked'; }); // 添加调试日志 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")} {!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("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; return ( {paginationController.pageNum * paginationController.pageSize + index + 1} {lot.routerRoute || '-'} {lot.itemCode} {lot.itemName + '(' + lot.stockUnit + ')'} {lot.lotNo || t('⚠️ No Stock Available')} {(() => { 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 ( ); } // 正常 lot:已扫描(checked/partially_completed/completed) if (!isNoLot && status !== 'pending' && status !== 'rejected') { return ( ); } // noLot 且已完成/部分完成:显示红色勾选 if (isNoLot && (status === 'partially_completed' || status === 'completed')) { return ( ); } return null; })()} {(() => { const status = lot.stockOutLineStatus?.toLowerCase(); const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected'; const isNoLot = !lot.lotNo; // rejected lot:不显示任何按钮 if (isRejected && !isNoLot) { return null; } // noLot 情况:只显示 Issue 按钮 if (isNoLot) { return ( ); } // 正常 lot:显示 Submit 和 Issue 按钮 return ( ); })()} ); }) )}
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` } />
{/* 保留:QR Code Modal */} { setQrModalOpen(false); setSelectedLotForQr(null); stopScan(); resetScan(); }} lot={selectedLotForQr} combinedLotData={combinedLotData} onQrCodeSubmit={handleQrCodeSubmitFromModal} /> { setManualLotConfirmationOpen(false); }} onConfirm={handleManualLotConfirmation} expectedLot={expectedLotData} scannedLot={scannedLotData} isLoading={isConfirmingLot} /> {/* 保留:Lot Confirmation Modal */} {lotConfirmationOpen && expectedLotData && scannedLotData && ( { setLotConfirmationOpen(false); setExpectedLotData(null); setScannedLotData(null); if (lastProcessedQr) { setProcessedQrCodes(prev => { const newSet = new Set(prev); newSet.delete(lastProcessedQr); return newSet; }); setLastProcessedQr(''); } }} onConfirm={handleLotConfirmation} expectedLot={expectedLotData} scannedLot={scannedLotData} isLoading={isConfirmingLot} /> )} {/* 保留: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()} /> )}
); }; export default PickExecution;