"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 { assignJobOrderPickOrder, fetchJobOrderLotsHierarchicalByPickOrderId, updateJoPickOrderHandledBy, 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"; import LinearProgressWithLabel from "../common/LinearProgressWithLabel"; import ScanStatusAlert from "../common/ScanStatusAlert"; interface Props { filterArgs: Record; //onSwitchToRecordTab: () => void; onBackToList?: () => void; } // Manual Lot Confirmation Modal (align with GoodPickExecutiondetail, opened by {2fic}) 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(''); }} sx={{ mb: 2 }} error={!!error && !expectedLotInput.trim()} /> {t("Scanned Lot Number")}: { setScannedLotInput(e.target.value); setError(''); }} 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 [selectedFloor, setSelectedFloor] = useState(null); 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, onBackToList }) => { const { t } = useTranslation("jo"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; // 修改:使用 Job Order 数据结构 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 [qrScanErrorMsg, setQrScanErrorMsg] = useState(''); const [qrScanSuccess, setQrScanSuccess] = useState(false); const [jobOrderData, setJobOrderData] = useState(null); const [pickQtyData, setPickQtyData] = useState>({}); const [searchQuery, setSearchQuery] = useState>({}); // issue form 里填的 actualPickQty(用于 submit/batch submit 不补拣到 required) const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState>({}); const [localSolStatusById, setLocalSolStatusById] = useState>({}); // 防止同一行(以 stockOutLineId/solId 识别)被重复点击提交/完成 const [actionBusyBySolId, setActionBusyBySolId] = 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); const [selectedFloor, setSelectedFloor] = 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); const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [lastProcessedQr, setLastProcessedQr] = useState(''); const [isRefreshingData, setIsRefreshingData] = useState(false); // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling const [processedQrCombinations, setProcessedQrCombinations] = useState>>(new Map()); // 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); // Manual lot confirmation modal state (test shortcut {2fic}) 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) => { // 用来记录这一行已经通过 lots 出现过的 lotId(避免 stockouts 再渲染一次) const lotIdSet = new Set(); // lots:按 lotId 去重并合并 requiredQty(对齐 GoodPickExecutiondetail) if (line.lots && line.lots.length > 0) { const lotMap = new Map(); line.lots.forEach((lot: any) => { const lotId = lot.lotId; if (lotId == null) return; if (lotMap.has(lotId)) { const existingLot = lotMap.get(lotId); existingLot.requiredQty = (existingLot.requiredQty || 0) + (lot.requiredQty || 0); } else { lotMap.set(lotId, { ...lot }); } }); lotMap.forEach((lot: any) => { if (lot.lotId != null) lotIdSet.add(lot.lotId); allLots.push({ ...lot, pickOrderLineId: line.id, itemId: line.itemId, itemCode: line.itemCode, itemName: line.itemName, uomCode: line.uomCode, uomDesc: line.uomDesc, itemTotalAvailableQty: line.totalAvailableQty ?? null, pickOrderLineRequiredQty: line.requiredQty, pickOrderLineStatus: line.status, jobOrderId: data.pickOrder.jobOrder.id, jobOrderCode: data.pickOrder.jobOrder.code, 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, handler: line.handler, noLot: false, }); }); } // stockouts:用于“无 suggested lot / noLot”场景也显示并可 submit 0 闭环 if (line.stockouts && line.stockouts.length > 0) { line.stockouts.forEach((stockout: any) => { const hasLot = stockout.lotId != null; const lotAlreadyInLots = hasLot && lotIdSet.has(stockout.lotId as number); // 有批次 & 已经通过 lots 渲染过 → 跳过,避免一条变两行 if (!stockout.noLot && lotAlreadyInLots) { return; } allLots.push({ pickOrderLineId: line.id, itemId: line.itemId, itemCode: line.itemCode, itemName: line.itemName, uomCode: line.uomCode, uomDesc: line.uomDesc, itemTotalAvailableQty: line.totalAvailableQty ?? null, pickOrderLineRequiredQty: line.requiredQty, pickOrderLineStatus: line.status, jobOrderId: data.pickOrder.jobOrder.id, jobOrderCode: data.pickOrder.jobOrder.code, 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, handler: line.handler, lotId: stockout.lotId || null, lotNo: stockout.lotNo || null, expiryDate: null, location: stockout.location || null, availableQty: stockout.availableQty ?? 0, requiredQty: line.requiredQty ?? 0, actualPickQty: stockout.qty ?? 0, processingStatus: stockout.status || "pending", lotAvailability: stockout.noLot ? "insufficient_stock" : "available", suggestedPickLotId: null, stockOutLineId: stockout.id || null, stockOutLineQty: stockout.qty ?? 0, stockOutLineStatus: stockout.status || null, stockInLineId: null, routerIndex: stockout.noLot ? 999999 : null, routerArea: null, routerRoute: null, noLot: !!stockout.noLot, }); }); } }); return allLots; }, []); const extractFloor = (lot: any): string => { const raw = lot.routerRoute || lot.routerArea || lot.location || ''; const match = raw.match(/^(\d+F?)/i) || raw.split('-')[0]; return (match?.[1] || match || raw || '').toUpperCase().replace(/(\d)F?/i, '$1F'); }; // 楼层排序权重:4F > 3F > 2F(数字越大越靠前) const floorSortOrder = (floor: string): number => { const n = parseInt(floor.replace(/\D/g, ''), 10); return isNaN(n) ? 0 : n; }; const combinedLotData = useMemo(() => { const lots = getAllLotsFromHierarchical(jobOrderData); // 前端覆盖:issue form/submit0 不会立刻改写后端 qty 时,用本地缓存让 UI 与 batch submit 计算一致 return lots.map((lot: any) => { const solId = Number(lot.stockOutLineId) || 0; if (solId > 0) { const hasPickedOverride = Object.prototype.hasOwnProperty.call(issuePickedQtyBySolId, solId); const picked = Number(issuePickedQtyBySolId[solId] ?? lot.actualPickQty ?? 0); const statusRaw = localSolStatusById[solId] ?? lot.stockOutLineStatus ?? ""; const status = String(statusRaw).toLowerCase(); const isEnded = status === 'completed' || status === 'rejected'; return { ...lot, actualPickQty: hasPickedOverride ? picked : lot.actualPickQty, stockOutLineQty: hasPickedOverride ? picked : lot.stockOutLineQty, stockOutLineStatus: isEnded ? statusRaw : (statusRaw || "checked"), }; } return lot; }); }, [jobOrderData, getAllLotsFromHierarchical, issuePickedQtyBySolId, localSolStatusById]); const originalCombinedData = useMemo(() => { return getAllLotsFromHierarchical(jobOrderData); }, [jobOrderData, getAllLotsFromHierarchical]); // Enhanced lotDataIndexes with cached active lots for better performance (align with GoodPickExecutiondetail) const lotDataIndexes = useMemo(() => { const byItemId = new Map(); const byItemCode = new Map(); const byLotId = new Map(); const byLotNo = new Map(); const byStockInLineId = new Map(); const activeLotsByItemId = new Map(); const rejectedStatuses = new Set(['rejected']); 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) && lot.stockOutLineStatus !== 'completed'; 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); } } return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId }; }, [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); if (cached && (now - cached.timestamp) < CACHE_TTL) { return { lotNo: cached.lotNo }; } if (abortControllerRef.current) abortControllerRef.current.abort(); const abortController = new AbortController(); abortControllerRef.current = abortController; const stockInLineInfo = await fetchStockInLineInfo(stockInLineId); stockInLineInfoCache.current.set(stockInLineId, { lotNo: stockInLineInfo.lotNo || null, timestamp: now }); if (stockInLineInfoCache.current.size > 100) { const firstKey = stockInLineInfoCache.current.keys().next().value; if (firstKey !== undefined) stockInLineInfoCache.current.delete(firstKey); } return { lotNo: stockInLineInfo.lotNo || null }; }, []); // 修改:加载未分配的 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 }; // 修改:使用 Job Order API 获取数据 const fetchJobOrderData = useCallback(async (pickOrderId?: number) => { setCombinedDataLoading(true); try { if (!pickOrderId) { console.warn("⚠️ No pickOrderId provided, skipping API call"); setJobOrderData(null); return; } // 直接使用类型化的响应 const jobOrderData = await fetchJobOrderLotsHierarchicalByPickOrderId(pickOrderId); console.log("✅ Job Order data (hierarchical):", jobOrderData); setJobOrderData(jobOrderData); // 使用辅助函数获取所有 lots(不再扁平化) const allLots = getAllLotsFromHierarchical(jobOrderData); } catch (error) { console.error("❌ Error fetching job order data:", error); setJobOrderData(null); } finally { setCombinedDataLoading(false); } }, [getAllLotsFromHierarchical]); const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => { if (!currentUserId || !pickOrderId || !itemId) { return; } try { console.log(`Updating JoPickOrder.handledBy for pickOrderId: ${pickOrderId}, itemId: ${itemId}, userId: ${currentUserId}`); await updateJoPickOrderHandledBy({ pickOrderId: pickOrderId, itemId: itemId, userId: currentUserId }); console.log("✅ JoPickOrder.handledBy updated successfully"); } catch (error) { console.error("❌ Error updating JoPickOrder.handledBy:", error); // Don't throw - this is not critical for the main flow } }, [currentUserId]); // 修改:初始化时加载数据 useEffect(() => { if (session && currentUserId && !initializationRef.current) { console.log("✅ Session loaded, initializing job order..."); initializationRef.current = true; // Get pickOrderId from filterArgs if available (when viewing from list) const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; if (pickOrderId) { fetchJobOrderData(pickOrderId); } loadUnassignedOrders(); } }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders, filterArgs?.pickOrderId]); // Add event listener for manual assignment useEffect(() => { const handlePickOrderAssigned = () => { console.log("🔄 Pick order assigned event received, refreshing data..."); const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; if (pickOrderId) { fetchJobOrderData(pickOrderId); } }; window.addEventListener('pickOrderAssigned', handlePickOrderAssigned); return () => { window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned); }; }, [fetchJobOrderData, filterArgs?.pickOrderId]); // 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..."); const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; await fetchJobOrderData(pickOrderId); 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); const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; await fetchJobOrderData( pickOrderId); } finally { // Clear refresh flag after a short delay setTimeout(() => { setIsRefreshingData(false); }, 1000); } }, [combinedLotData, fetchJobOrderData]); const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => { console.log("⚠️ [LOT MISMATCH] Lot mismatch detected:", { expectedLot, scannedLot }); console.log("⚠️ [LOT MISMATCH] Opening confirmation modal - NO lot will be marked as scanned until user confirms"); // ✅ schedule modal open in next tick (avoid flushSync warnings on some builds) // ✅ IMPORTANT: This function ONLY opens the modal. It does NOT process any lot. setTimeout(() => { setExpectedLotData(expectedLot); setScannedLotData({ ...scannedLot, lotNo: scannedLot.lotNo || null, }); setLotConfirmationOpen(true); console.log("⚠️ [LOT MISMATCH] Modal opened - waiting for user confirmation"); }, 0); // ✅ Fetch lotNo in background for display purposes (cached) // ✅ This is ONLY for display - it does NOT process any lot if (!scannedLot.lotNo && scannedLot.stockInLineId) { console.log(`⚠️ [LOT MISMATCH] Fetching lotNo for display (stockInLineId: ${scannedLot.stockInLineId})`); fetchStockInLineInfoCached(scannedLot.stockInLineId) .then((info) => { console.log(`⚠️ [LOT MISMATCH] Fetched lotNo for display: ${info.lotNo}`); startTransition(() => { setScannedLotData((prev: any) => ({ ...prev, lotNo: info.lotNo || null, })); }); }) .catch((error) => { console.error(`❌ [LOT MISMATCH] Error fetching lotNo for display (stockInLineId may not exist):`, error); // ignore display fetch errors - this does NOT affect processing }); } }, [fetchStockInLineInfoCached]); // Add handleLotConfirmation function const handleLotConfirmation = useCallback(async () => { if (!expectedLotData || !scannedLotData || !selectedLotForQr) { console.error("❌ [LOT CONFIRM] Missing required data for lot confirmation"); return; } console.log("✅ [LOT CONFIRM] User confirmed lot substitution - processing now"); console.log("✅ [LOT CONFIRM] Expected lot:", expectedLotData); console.log("✅ [LOT CONFIRM] Scanned lot:", scannedLotData); console.log("✅ [LOT CONFIRM] Selected lot for QR:", selectedLotForQr); setIsConfirmingLot(true); try { let newLotLineId = scannedLotData?.inventoryLotLineId; if (!newLotLineId && scannedLotData?.stockInLineId) { try { if (currentUserId && selectedLotForQr.pickOrderId && selectedLotForQr.itemId) { try { await updateHandledBy(selectedLotForQr.pickOrderId, selectedLotForQr.itemId); console.log(`✅ [LOT CONFIRM] Handler updated for itemId ${selectedLotForQr.itemId}`); } catch (error) { console.error(`❌ [LOT CONFIRM] Error updating handler (non-critical):`, error); } } console.log(`🔍 [LOT CONFIRM] Fetching lot detail for stockInLineId: ${scannedLotData.stockInLineId}`); const ld = await fetchLotDetail(scannedLotData.stockInLineId); newLotLineId = ld.inventoryLotLineId; console.log(`✅ [LOT CONFIRM] Fetched lot detail: inventoryLotLineId=${newLotLineId}`); } catch (error) { console.error("❌ [LOT CONFIRM] Error fetching lot detail (stockInLineId may not exist):", error); // If stockInLineId doesn't exist, we can still proceed with lotNo substitution // The backend confirmLotSubstitution should handle this case } } if (!newLotLineId) { console.warn("⚠️ [LOT CONFIRM] No inventory lot line id for scanned lot, proceeding with lotNo only"); // Continue anyway - backend may handle lotNo substitution without inventoryLotLineId } console.log("=== [LOT CONFIRM] Lot Confirmation Debug ==="); console.log("Selected Lot:", selectedLotForQr); console.log("Pick Order Line ID:", selectedLotForQr.pickOrderLineId); console.log("Stock Out Line ID:", selectedLotForQr.stockOutLineId); console.log("Suggested Pick Lot ID:", selectedLotForQr.suggestedPickLotId); console.log("Lot ID (fallback):", selectedLotForQr.lotId); console.log("New Inventory Lot Line ID:", newLotLineId); console.log("Scanned Lot No:", scannedLotData.lotNo); console.log("Scanned StockInLineId:", scannedLotData.stockInLineId); const originalSuggestedPickLotId = selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId; // noLot / missing suggestedPickLotId 场景:没有 originalSuggestedPickLotId,改用 updateStockOutLineStatusByQRCodeAndLotNo if (!originalSuggestedPickLotId) { if (!selectedLotForQr?.stockOutLineId) { throw new Error("Missing stockOutLineId for noLot line"); } console.log("🔄 [LOT CONFIRM] No originalSuggestedPickLotId, using updateStockOutLineStatusByQRCodeAndLotNo..."); const res = await updateStockOutLineStatusByQRCodeAndLotNo({ pickOrderLineId: selectedLotForQr.pickOrderLineId, inventoryLotNo: scannedLotData.lotNo || '', stockInLineId: scannedLotData?.stockInLineId ?? null, stockOutLineId: selectedLotForQr.stockOutLineId, itemId: selectedLotForQr.itemId, status: "checked", }); console.log("✅ [LOT CONFIRM] updateStockOutLineStatusByQRCodeAndLotNo result:", res); const ok = res?.code === "checked" || res?.code === "SUCCESS"; if (!ok) { setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg(res?.message || "换批失败:无法更新 stock out line"); return; } } else { // Call confirmLotSubstitution to update the suggested lot console.log("🔄 [LOT CONFIRM] Calling confirmLotSubstitution..."); const substitutionResult = await confirmLotSubstitution({ pickOrderLineId: selectedLotForQr.pickOrderLineId, stockOutLineId: selectedLotForQr.stockOutLineId, originalSuggestedPickLotId, newInventoryLotNo: scannedLotData.lotNo || '', // ✅ required by LotSubstitutionConfirmRequest newStockInLineId: scannedLotData?.stockInLineId ?? null, }); console.log("✅ [LOT CONFIRM] Lot substitution result:", substitutionResult); // ✅ CRITICAL: substitution failed => DO NOT mark original stockOutLine as checked. // Keep modal open so user can cancel/rescan. if (!substitutionResult || substitutionResult.code !== "SUCCESS") { console.error("❌ [LOT CONFIRM] Lot substitution failed. Will NOT update stockOutLine status."); setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg( substitutionResult?.message || `换批失败:stockInLineId ${scannedLotData?.stockInLineId ?? ""} 不存在或无法匹配` ); return; } } // Update stock out line status to 'checked' after substitution if(selectedLotForQr?.stockOutLineId){ console.log(`🔄 [LOT CONFIRM] Updating stockOutLine ${selectedLotForQr.stockOutLineId} to 'checked'`); await updateStockOutLineStatus({ id: selectedLotForQr.stockOutLineId, status: 'checked', qty: 0 }); console.log(`✅ [LOT CONFIRM] Stock out line ${selectedLotForQr.stockOutLineId} status updated to 'checked'`); } // Close modal and clean up state BEFORE refreshing setLotConfirmationOpen(false); setExpectedLotData(null); setScannedLotData(null); setSelectedLotForQr(null); // Clear QR processing state but DON'T clear processedQrCodes yet setQrScanError(false); setQrScanSuccess(true); setQrScanErrorMsg(''); setQrScanInput(''); // Set refreshing flag to prevent QR processing during refresh setIsRefreshingData(true); // Refresh data to show updated lot console.log("🔄 Refreshing job order data..."); const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; await fetchJobOrderData(pickOrderId); console.log(" Lot substitution confirmed and data refreshed"); // Clear processed QR codes and flags immediately after refresh // This allows new QR codes to be processed right away setTimeout(() => { console.log(" Clearing processed QR codes and resuming scan"); setProcessedQrCodes(new Set()); setLastProcessedQr(''); setQrScanSuccess(false); setIsRefreshingData(false); // ✅ Clear processedQrCombinations to allow reprocessing the same QR if needed if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) { setProcessedQrCombinations(prev => { const newMap = new Map(prev); const itemId = selectedLotForQr.itemId; if (itemId && newMap.has(itemId)) { newMap.get(itemId)!.delete(scannedLotData.stockInLineId); if (newMap.get(itemId)!.size === 0) { newMap.delete(itemId); } } return newMap; }); } }, 500); // Reduced from 3000ms to 500ms - just enough for UI update } catch (error) { console.error("Error confirming lot substitution:", error); setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg('换批发生异常,请重试或联系管理员'); // Clear refresh flag on error setIsRefreshingData(false); } finally { setIsConfirmingLot(false); } }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData,currentUserId, updateHandledBy ]); const processOutsideQrCode = useCallback(async (latestQr: string) => { // ✅ Only JSON QR supported for outside scanner (avoid false positive with lotNo) let qrData: any = null; try { qrData = JSON.parse(latestQr); } catch { startTransition(() => { setQrScanError(true); setQrScanSuccess(false); }); return; } if (!(qrData?.stockInLineId && qrData?.itemId)) { startTransition(() => { setQrScanError(true); setQrScanSuccess(false); }); return; } const scannedItemId = Number(qrData.itemId); const scannedStockInLineId = Number(qrData.stockInLineId); // ✅ avoid duplicate processing by itemId+stockInLineId const itemProcessedSet = processedQrCombinations.get(scannedItemId); if (itemProcessedSet?.has(scannedStockInLineId)) return; const indexes = lotDataIndexes; const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || []; // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected const allLotsForItem = indexes.byItemId.get(scannedItemId) || []; // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots // This allows users to scan other lots even when all suggested lots are rejected const scannedLot = allLotsForItem.find( (lot: any) => lot.stockInLineId === scannedStockInLineId ); if (scannedLot) { const isRejected = scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' || scannedLot.lotAvailability === 'rejected' || scannedLot.lotAvailability === 'status_unavailable'; if (isRejected) { console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg( `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。` ); }); // Mark as processed to prevent re-processing setProcessedQrCombinations(prev => { const newMap = new Map(prev); if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); newMap.get(scannedItemId)!.add(scannedStockInLineId); return newMap; }); return; } } // ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching if (activeSuggestedLots.length === 0) { // Check if there are any lots for this item (even if all are rejected) if (allLotsForItem.length === 0) { console.error("No lots found for this item"); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg("当前订单中没有此物品的批次信息"); }); return; } // ✅ Allow lot switching: find a rejected lot as expected lot, or use first lot // This allows users to switch to a new lot even when all suggested lots are rejected console.log(`⚠️ [QR PROCESS] No active suggested lots, but allowing lot switching. Scanned lot is not rejected.`); // Find a rejected lot as expected lot (the one that was rejected) const rejectedLot = allLotsForItem.find((lot: any) => lot.stockOutLineStatus?.toLowerCase() === 'rejected' || lot.lotAvailability === 'rejected' || lot.lotAvailability === 'status_unavailable' ); const expectedLot = rejectedLot || allLotsForItem[0]; // Use rejected lot if exists, otherwise first lot // ✅ Always open confirmation modal when no active lots (user needs to confirm switching) // handleLotMismatch will fetch lotNo from backend using stockInLineId if needed console.log(`⚠️ [QR PROCESS] Opening confirmation modal for lot switch (no active lots)`); setSelectedLotForQr(expectedLot); handleLotMismatch( { lotNo: expectedLot.lotNo, itemCode: expectedLot.itemCode, itemName: expectedLot.itemName }, { lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null itemCode: expectedLot.itemCode, itemName: expectedLot.itemName, inventoryLotLineId: scannedLot?.lotId || null, stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo } ); return; } // ✅ direct stockInLineId match (O(1)) const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || []; let exactMatch: any = null; for (let i = 0; i < stockInLineLots.length; i++) { const lot = stockInLineLots[i]; if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) { exactMatch = lot; break; } } console.log(`🔍 [QR PROCESS] Scanned stockInLineId: ${scannedStockInLineId}, itemId: ${scannedItemId}`); console.log(`🔍 [QR PROCESS] Found ${stockInLineLots.length} lots with stockInLineId ${scannedStockInLineId}`); console.log(`🔍 [QR PROCESS] Exact match found: ${exactMatch ? `YES (lotNo: ${exactMatch.lotNo}, stockOutLineId: ${exactMatch.stockOutLineId})` : 'NO'}`); // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots // This handles the case where Lot A is rejected and user scans Lot B if (!exactMatch && scannedLot && !activeSuggestedLots.includes(scannedLot)) { // Scanned lot is not in active suggested lots, open confirmation modal const expectedLot = activeSuggestedLots[0] || allLotsForItem[0]; // Use first active lot or first lot as expected if (expectedLot && scannedLot.stockInLineId !== expectedLot.stockInLineId) { console.log(`⚠️ [QR PROCESS] Scanned lot ${scannedLot.lotNo} is not in active suggested lots, opening confirmation modal`); setSelectedLotForQr(expectedLot); handleLotMismatch( { lotNo: expectedLot.lotNo, itemCode: expectedLot.itemCode, itemName: expectedLot.itemName }, { lotNo: scannedLot.lotNo || null, itemCode: expectedLot.itemCode, itemName: expectedLot.itemName, inventoryLotLineId: scannedLot.lotId || null, stockInLineId: scannedStockInLineId } ); return; } } if (exactMatch) { if (!exactMatch.stockOutLineId) { console.error(`❌ [QR PROCESS] Exact match found but no stockOutLineId`); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); }); return; } console.log(`✅ [QR PROCESS] Processing exact match: lotNo=${exactMatch.lotNo}, stockOutLineId=${exactMatch.stockOutLineId}`); try { if (currentUserId && exactMatch.pickOrderId && exactMatch.itemId) { try { await updateHandledBy(exactMatch.pickOrderId, exactMatch.itemId); console.log(`✅ [QR PROCESS] Handler updated for itemId ${exactMatch.itemId}`); } catch (error) { console.error(`❌ [QR PROCESS] Error updating handler (non-critical):`, error); } } const res = await updateStockOutLineStatusByQRCodeAndLotNo({ pickOrderLineId: exactMatch.pickOrderLineId, inventoryLotNo: exactMatch.lotNo, stockInLineId: exactMatch.stockInLineId ?? null, stockOutLineId: exactMatch.stockOutLineId, itemId: exactMatch.itemId, status: "checked", }); if (res.code === "checked" || res.code === "SUCCESS") { console.log(`✅ [QR PROCESS] Successfully updated stockOutLine ${exactMatch.stockOutLineId} to checked`); const entity = res.entity as any; startTransition(() => { setQrScanError(false); setQrScanSuccess(true); }); // mark combination processed setProcessedQrCombinations(prev => { const newMap = new Map(prev); if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); newMap.get(scannedItemId)!.add(scannedStockInLineId); return newMap; }); // refresh to keep consistency with server & handler updates const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; await fetchJobOrderData(pickOrderId); } else { console.error(`❌ [QR PROCESS] Update failed: ${res.code}`); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); }); } } catch (error) { console.error(`❌ [QR PROCESS] Error updating stockOutLine:`, error); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); }); } return; } // ✅ mismatch: validate scanned stockInLineId exists before opening confirmation modal console.log(`⚠️ [QR PROCESS] No exact match found. Validating scanned stockInLineId ${scannedStockInLineId} for itemId ${scannedItemId}`); console.log(`⚠️ [QR PROCESS] Active suggested lots for itemId ${scannedItemId}:`, activeSuggestedLots.map(l => ({ lotNo: l.lotNo, stockInLineId: l.stockInLineId }))); if (activeSuggestedLots.length === 0) { console.error(`❌ [QR PROCESS] No active suggested lots found for itemId ${scannedItemId}`); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg(`当前订单中没有 itemId ${scannedItemId} 的可用批次`); }); return; } const expectedLot = activeSuggestedLots[0]; console.log(`⚠️ [QR PROCESS] Expected lot: ${expectedLot.lotNo} (stockInLineId: ${expectedLot.stockInLineId}), Scanned stockInLineId: ${scannedStockInLineId}`); // ✅ Validate scanned stockInLineId exists before opening modal // This ensures the backend can find the lot when user confirms try { console.log(`🔍 [QR PROCESS] Validating scanned stockInLineId ${scannedStockInLineId} exists...`); const stockInLineInfo = await fetchStockInLineInfoCached(scannedStockInLineId); console.log(`✅ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} exists, lotNo: ${stockInLineInfo.lotNo}`); // ✅ 检查扫描的批次是否已被拒绝 const scannedLot = combinedLotData.find( (lot: any) => lot.stockInLineId === scannedStockInLineId && lot.itemId === scannedItemId ); if (scannedLot) { const isRejected = scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' || scannedLot.lotAvailability === 'rejected' || scannedLot.lotAvailability === 'status_unavailable'; if (isRejected) { console.warn(`⚠️ [QR PROCESS] Scanned lot ${stockInLineInfo.lotNo} (stockInLineId: ${scannedStockInLineId}) is rejected or unavailable`); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg( `此批次(${stockInLineInfo.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。` ); }); // Mark as processed to prevent re-processing setProcessedQrCombinations(prev => { const newMap = new Map(prev); if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); newMap.get(scannedItemId)!.add(scannedStockInLineId); return newMap; }); return; } } // ✅ stockInLineId exists and is not rejected, open confirmation modal console.log(`⚠️ [QR PROCESS] Opening confirmation modal - user must confirm before any lot is marked as scanned`); setSelectedLotForQr(expectedLot); handleLotMismatch( { lotNo: expectedLot.lotNo, itemCode: expectedLot.itemCode, itemName: expectedLot.itemName }, { lotNo: stockInLineInfo.lotNo || null, // Use fetched lotNo for display itemCode: expectedLot.itemCode, itemName: expectedLot.itemName, inventoryLotLineId: null, stockInLineId: scannedStockInLineId } ); } catch (error) { // ✅ stockInLineId does NOT exist, show error immediately (don't open modal) console.error(`❌ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} does NOT exist:`, error); startTransition(() => { setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg( `扫描的 stockInLineId ${scannedStockInLineId} 不存在。请检查 QR 码是否正确,或联系管理员。` ); }); // Mark as processed to prevent re-processing setProcessedQrCombinations(prev => { const newMap = new Map(prev); if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); newMap.get(scannedItemId)!.add(scannedStockInLineId); return newMap; }); } }, [filterArgs?.pickOrderId, fetchJobOrderData, handleLotMismatch, lotDataIndexes, processedQrCombinations, combinedLotData, fetchStockInLineInfoCached,currentUserId, updateHandledBy ]); // Store in refs for immediate access in qrValues effect processOutsideQrCodeRef.current = processOutsideQrCode; 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 const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; await fetchJobOrderData(pickOrderId); } catch (error) { console.error("Error creating stock out line:", error); } } }, [selectedLotForQr, fetchJobOrderData]); useEffect(() => { // Skip if scanner not active or no data or currently refreshing if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) return; const latestQr = qrValues[qrValues.length - 1]; // ✅ Test shortcut: {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId if ((latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) && latestQr.endsWith("}")) { let content = ''; if (latestQr.startsWith("{2fittest")) content = latestQr.substring(9, latestQr.length - 1); else content = latestQr.substring(8, latestQr.length - 1); const parts = content.split(','); if (parts.length === 2) { const itemId = parseInt(parts[0].trim(), 10); const stockInLineId = parseInt(parts[1].trim(), 10); if (!isNaN(itemId) && !isNaN(stockInLineId)) { const simulatedQr = JSON.stringify({ itemId, stockInLineId }); lastProcessedQrRef.current = latestQr; processedQrCodesRef.current.add(latestQr); setLastProcessedQr(latestQr); setProcessedQrCodes(new Set(processedQrCodesRef.current)); processOutsideQrCodeRef.current?.(simulatedQr); resetScanRef.current?.(); return; } } } // ✅ Shortcut: {2fic} open manual lot confirmation modal if (latestQr === "{2fic}") { setManualLotConfirmationOpen(true); resetScanRef.current?.(); lastProcessedQrRef.current = latestQr; processedQrCodesRef.current.add(latestQr); setLastProcessedQr(latestQr); setProcessedQrCodes(new Set(processedQrCodesRef.current)); return; } // Skip processing if modal open for same QR if (lotConfirmationOpen || manualLotConfirmationOpen) { if (latestQr === lastProcessedQrRef.current) return; } // Skip if already processed (refs) if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) return; // Mark processed immediately 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); } // Process immediately if (qrProcessingTimeoutRef.current) { clearTimeout(qrProcessingTimeoutRef.current); qrProcessingTimeoutRef.current = null; } processOutsideQrCodeRef.current?.(latestQr); // UI state updates (non-blocking) startTransition(() => { setLastProcessedQr(latestQr); setProcessedQrCodes(new Set(processedQrCodesRef.current)); }); 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); } } const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; await fetchJobOrderData(pickOrderId); 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) => { if (!lot.stockOutLineId) { console.error("No stock out line found for this lot"); return; } const solId = Number(lot.stockOutLineId) || 0; try { if (currentUserId && lot.pickOrderId && lot.itemId) { try { await updateHandledBy(lot.pickOrderId, lot.itemId); } catch (error) { console.error("❌ Error updating handler (non-critical):", error); // Continue even if handler update fails } } // ✅ 两步完成(与 DO 对齐): // 1) Skip/Submit0 只把 SOL 标记为 checked(不直接 completed) // 2) 之后由 batch submit 把 SOL 推到 completed(允许 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 'checked' with qty: 0 (will complete in batch submit)`); const updateResult = await updateStockOutLineStatus({ id: lot.stockOutLineId, status: 'checked', qty: 0 }); console.log('Update result:', updateResult); const r: any = updateResult as any; const updateOk = r?.code === 'SUCCESS' || r?.type === 'completed' || typeof r?.id === 'number' || typeof r?.entity?.id === 'number' || (r?.message && r.message.includes('successfully')); if (!updateResult || !updateOk) { console.error('Failed to update stock out line status:', updateResult); throw new Error('Failed to update stock out line status'); } // 记录该 SOL 的“目标实际拣货量=0”,让 batch submit 走 onlyComplete(不补拣到 required) if (solId > 0) { setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: 0 })); setLocalSolStatusById(prev => ({ ...prev, [solId]: 'checked' })); } const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; void fetchJobOrderData(pickOrderId); console.log("All zeros submission marked as checked successfully (waiting for batch submit)."); 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 (solId > 0) { setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: cumulativeQty })); setLocalSolStatusById(prev => ({ ...prev, [solId]: newStatus })); } 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!`); setTimeout(() => { if (onBackToList) { onBackToList(); } }, 1500); } 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); } } const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; void fetchJobOrderData(pickOrderId); console.log("Pick quantity submitted successfully!"); setTimeout(() => { checkAndAutoAssignNext(); }, 1000); } catch (error) { console.error("Error submitting pick quantity:", error); } }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]); const handleSkip = useCallback(async (lot: any) => { try { console.log("Just Complete clicked, mark checked with 0 qty for lot:", lot.lotNo); await handleSubmitPickQtyWithQty(lot, 0); } catch (err) { console.error("Error in Skip:", err); } }, [handleSubmitPickQtyWithQty]); const hasPendingBatchSubmit = useMemo(() => { return combinedLotData.some((lot) => { const status = String(lot.stockOutLineStatus || "").toLowerCase(); return status === "checked" || status === "pending" || status === "partially_completed" || status === "partially_complete"; }); }, [combinedLotData]); useEffect(() => { if (!hasPendingBatchSubmit) return; const handler = (event: BeforeUnloadEvent) => { event.preventDefault(); event.returnValue = ""; }; window.addEventListener("beforeunload", handler); return () => window.removeEventListener("beforeunload", handler); }, [hasPendingBatchSubmit]); const handleSubmitAllScanned = useCallback(async () => { const scannedLots = combinedLotData.filter(lot => { const status = lot.stockOutLineStatus; const statusLower = String(status || "").toLowerCase(); if (statusLower === "completed" || statusLower === "complete") { return false; } console.log("lot.noLot:", lot.noLot); console.log("lot.status:", lot.stockOutLineStatus); // ✅ no-lot:允許 pending / checked / partially_completed / PARTIALLY_COMPLETE if (lot.noLot === true || !lot.lotId) { return ( status === 'checked' || status === 'pending' || status === 'partially_completed' || status === 'PARTIALLY_COMPLETE' ); } // ✅ 有 lot:維持原本規則 return ( status === 'checked' || status === 'partially_completed' || status === 'PARTIALLY_COMPLETE' ); }); if (scannedLots.length === 0) { console.log("No scanned items to submit"); return; } setIsSubmittingAll(true); console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`); try { // ✅ 批量更新所有相关行的 handler(在提交前) if (currentUserId) { const uniqueItemIds = new Set(scannedLots.map(lot => lot.itemId)); const updatePromises = Array.from(uniqueItemIds).map(itemId => { const lot = scannedLots.find(l => l.itemId === itemId); if (lot && lot.pickOrderId) { return updateHandledBy(lot.pickOrderId, itemId).catch(err => { console.error(`❌ Error updating handler for itemId ${itemId}:`, err); }); } return Promise.resolve(); }); await Promise.all(updatePromises); console.log(`✅ Updated handlers for ${uniqueItemIds.size} unique items`); } // ✅ 转换为 batchSubmitList 所需的格式 const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => { const requiredQty = Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0); const solId = Number(lot.stockOutLineId) || 0; const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined; const currentActualPickQty = Number(issuePicked ?? lot.actualPickQty ?? 0); const isNoLot = lot.noLot === true || !lot.lotId; // ✅ 只改狀態模式:有 issuePicked 或 noLot const onlyComplete = lot.stockOutLineStatus === 'partially_completed' || issuePicked !== undefined || isNoLot; let targetActual: number; let newStatus: string; if (onlyComplete) { targetActual = currentActualPickQty; // no‑lot = 0,一律只改狀態 newStatus = 'completed'; } else { const remainingQty = Math.max(0, requiredQty - currentActualPickQty); targetActual = currentActualPickQty + remainingQty; newStatus = requiredQty > 0 && targetActual >= requiredQty ? 'completed' : 'partially_completed'; } return { stockOutLineId: Number(lot.stockOutLineId) || 0, pickOrderLineId: Number(lot.pickOrderLineId), inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null, requiredQty, actualPickQty: Number(targetActual), stockOutLineStatus: newStatus, pickOrderConsoCode: String(lot.pickOrderConsoCode || ''), noLot: Boolean(lot.noLot === true) }; }); const request: batchSubmitListRequest = { userId: currentUserId || 0, lines: lines }; // ✅ 使用 batchSubmitList API const result = await batchSubmitList(request); console.log(`📥 Batch submit result:`, result); // ✅ After batch submit, explicitly trigger completion check per consoCode. // Otherwise pick_order/job_order may stay RELEASED even when all lines are completed. try { const consoCodes = Array.from( new Set( lines .map((l) => (l.pickOrderConsoCode || "").trim()) .filter((c) => c.length > 0), ), ); if (consoCodes.length > 0) { await Promise.all( consoCodes.map(async (code) => { try { const completionResponse = await checkAndCompletePickOrderByConsoCode(code); console.log(`✅ Pick order completion check (${code}):`, completionResponse); } catch (e) { console.error(`❌ Error checking completion for ${code}:`, e); } }), ); } } catch (e) { console.error("❌ Error triggering completion checks after batch submit:", e); } // 刷新数据 const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; await fetchJobOrderData(pickOrderId); if (result && result.code === "SUCCESS") { setQrScanSuccess(true); setTimeout(() => { setQrScanSuccess(false); checkAndAutoAssignNext(); if (onBackToList) { onBackToList(); } }, 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, filterArgs?.pickOrderId, onBackToList, updateHandledBy, issuePickedQtyBySolId]) const scannedItemsCount = useMemo(() => { return combinedLotData.filter(lot => { const status = lot.stockOutLineStatus; const statusLower = String(status || "").toLowerCase(); if (statusLower === "completed" || statusLower === "complete") { return false; } const isNoLot = lot.noLot === true || !lot.lotId; if (isNoLot) { // no-lot:pending / checked / partially_completed 都算「已掃描」 return ( status === 'pending' || status === 'checked' || status === 'partially_completed' || status === 'PARTIALLY_COMPLETE' ); } // 有 lot:維持原規則 return ( status === 'checked' || status === 'partially_completed' || status === 'PARTIALLY_COMPLETE' ); }).length; }, [combinedLotData]); // 先定义 filteredByFloor 和 availableFloors const availableFloors = useMemo(() => { const floors = new Set(); combinedLotData.forEach(lot => { const f = extractFloor(lot); if (f) floors.add(f); }); return Array.from(floors).sort((a, b) => floorSortOrder(b) - floorSortOrder(a)); }, [combinedLotData]); const filteredByFloor = useMemo(() => { if (!selectedFloor) return combinedLotData; return combinedLotData.filter(lot => extractFloor(lot) === selectedFloor); }, [combinedLotData, selectedFloor]); // Progress bar data - 现在可以正确引用 filteredByFloor const progress = useMemo(() => { const data = selectedFloor ? filteredByFloor : combinedLotData; if (data.length === 0) return { completed: 0, total: 0 }; const nonPendingCount = data.filter(lot => lot.stockOutLineStatus?.toLowerCase() !== 'pending' ).length; return { completed: nonPendingCount, total: data.length }; }, [selectedFloor, filteredByFloor, 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 }); const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; await fetchJobOrderData(pickOrderId); 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 { if (currentUserId && selectedLotForExecutionForm?.pickOrderId && selectedLotForExecutionForm?.itemId) { try { await updateHandledBy(selectedLotForExecutionForm.pickOrderId, selectedLotForExecutionForm.itemId); console.log(`✅ [ISSUE FORM] Handler updated for itemId ${selectedLotForExecutionForm.itemId}`); } catch (error) { console.error(`❌ [ISSUE FORM] Error updating handler (non-critical):`, error); } } console.log("Pick execution form submitted:", data); const issueData = { ...data, type: "Jo", // Delivery Order Record 类型 pickerName: session?.user?.name || undefined, handledBy: currentUserId || undefined, }; const result = await recordPickExecutionIssue(issueData); console.log("Pick execution issue recorded:", result); if (result && result.code === "SUCCESS") { console.log(" Pick execution issue recorded successfully"); const solId = Number(issueData.stockOutLineId || data?.stockOutLineId); if (solId > 0) { const picked = Number(issueData.actualPickQty || 0); setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: picked })); } } else { console.error("❌ Failed to record pick execution issue:", result); } setPickExecutionFormOpen(false); setSelectedLotForExecutionForm(null); const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; await fetchJobOrderData(pickOrderId); } catch (error) { console.error("Error submitting pick execution form:", error); } }, [fetchJobOrderData, currentUserId, selectedLotForExecutionForm, updateHandledBy, filterArgs?.pickOrderId]); // 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(() => { const sourceData = selectedFloor ? filteredByFloor : combinedLotData; const sortedData = [...sourceData].sort((a, b) => { const floorA = extractFloor(a); const floorB = extractFloor(b); const orderA = floorSortOrder(floorA); const orderB = floorSortOrder(floorB); if (orderA !== orderB) return orderB - orderA; // 4F, 3F, 2F // 同楼层再按 routerIndex、pickOrderCode、lotNo const aIndex = a.routerIndex ?? 0; const bIndex = b.routerIndex ?? 0; 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); }, [selectedFloor, filteredByFloor, 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) => { if (lot?.noLot === true || lot?.lotAvailability === 'insufficient_stock') { return t("This order is insufficient, please pick another lot."); } 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' )} > {/* Progress bar + scan status fixed at top */} {availableFloors.map(floor => ( ))} {/* 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 */} {t("Index")} {t("Route")} {t("Handler")} {t("Item Code")} {t("Item Name")} {t("Lot No")} {t("Lot Required Pick Qty")} {t("Available 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.handler || '-'} {lot.itemCode} {lot.itemName+'('+lot.uomDesc+')'} {lot.noLot === true || !lot.lotId ? t("Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.") // i18n key,下一步加文案 : (lot.lotNo || '-')} {(() => { const requiredQty = lot.requiredQty || 0; const unit = (lot.noLot === true || !lot.lotId) ? (lot.uomDesc || "") : ( lot.uomDesc || ""); return `${requiredQty.toLocaleString()}(${unit})`; })()} {(() => { const avail = lot.itemTotalAvailableQty; if (avail == null) return "-"; const unit = lot.uomDesc || ""; return `${Number(avail).toLocaleString()}(${unit})`; })()} {(() => { 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 ( ); } return null; })()} {(() => { const status = lot.stockOutLineStatus?.toLowerCase(); const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected'; const isNoLot = !lot.lotNo; // ✅ rejected lot:显示提示文本(换行显示) if (isRejected && !isNoLot) { return ( {t("This lot is rejected, please scan another lot.")} ); } // 正常 lot:显示按钮 return ( ); })()} )) )}
`${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 && ( { console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`); setLotConfirmationOpen(false); setExpectedLotData(null); setScannedLotData(null); setSelectedLotForQr(null); // ✅ IMPORTANT: Clear refs and processedQrCombinations to allow reprocessing the same QR code // This allows the modal to reopen if user cancels and scans the same QR again setTimeout(() => { lastProcessedQrRef.current = ''; processedQrCodesRef.current.clear(); // Clear processedQrCombinations for this itemId+stockInLineId combination if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) { setProcessedQrCombinations(prev => { const newMap = new Map(prev); const itemId = selectedLotForQr.itemId; if (itemId && newMap.has(itemId)) { newMap.get(itemId)!.delete(scannedLotData.stockInLineId); if (newMap.get(itemId)!.size === 0) { newMap.delete(itemId); } } return newMap; }); } console.log(`⏱️ [LOT CONFIRM MODAL] Cleared refs and processedQrCombinations to allow reprocessing`); }, 100); }} onConfirm={handleLotConfirmation} expectedLot={expectedLotData} scannedLot={scannedLotData} isLoading={isConfirmingLot} /> )} {/* Manual Lot Confirmation Modal (test shortcut {2fic}) */} setManualLotConfirmationOpen(false)} // Reuse existing handler: expectedLotInput=current lot, scannedLotInput=new lot onConfirm={(currentLotNo, newLotNo) => { // Use existing manual flow from handleManualLotConfirmation in other screens: // Here we route through updateStockOutLineStatusByQRCodeAndLotNo via handleManualLotConfirmation-like inline logic. // For now: open LotConfirmationModal path by setting expected/scanned and letting user confirm substitution. setExpectedLotData({ lotNo: currentLotNo, itemCode: '', itemName: '' }); setScannedLotData({ lotNo: newLotNo, itemCode: '', itemName: '', inventoryLotLineId: null, stockInLineId: null }); setManualLotConfirmationOpen(false); setLotConfirmationOpen(true); }} 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