diff --git a/src/components/Jodetail/JobPickExecution.tsx b/src/components/Jodetail/JobPickExecution.tsx index fc89e7a..c3cd28e 100644 --- a/src/components/Jodetail/JobPickExecution.tsx +++ b/src/components/Jodetail/JobPickExecution.tsx @@ -32,7 +32,8 @@ import { AutoAssignReleaseResponse, checkPickOrderCompletion, PickOrderCompletionResponse, - checkAndCompletePickOrderByConsoCode + checkAndCompletePickOrderByConsoCode, + confirmLotSubstitution } from "@/app/api/pickOrder/actions"; // ✅ 修改:使用 Job Order API import { @@ -47,7 +48,7 @@ import { } from "react-hook-form"; import SearchBox, { Criterion } from "../SearchBox"; import { CreateStockOutLine } from "@/app/api/pickOrder/actions"; -import { updateInventoryLotLineQuantities } from "@/app/api/inventory/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"; @@ -55,7 +56,7 @@ import { SessionWithTokens } from "@/config/authConfig"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; import GoodPickExecutionForm from "./JobPickExecutionForm"; import FGPickOrderCard from "./FGPickOrderCard"; - +import LotConfirmationModal from "./LotConfirmationModal"; interface Props { filterArgs: Record; } @@ -332,7 +333,10 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); - + const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); + const [expectedLotData, setExpectedLotData] = useState(null); + const [scannedLotData, setScannedLotData] = useState(null); + const [isConfirmingLot, setIsConfirmingLot] = useState(false); const [qrScanInput, setQrScanInput] = useState(''); const [qrScanError, setQrScanError] = useState(false); const [qrScanSuccess, setQrScanSuccess] = useState(false); @@ -522,7 +526,7 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { stockOutLineId: lot.stockOutLineId, stockOutLineStatus: lot.stockOutLineStatus, stockOutLineQty: lot.stockOutLineQty, - + suggestedPickLotId: lot.suggestedPickLotId, // Router info routerIndex: lot.routerIndex, secondQrScanStatus: lot.secondQrScanStatus, @@ -710,9 +714,7 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { setQrScanSuccess(true); setQrScanError(false); setQrScanInput(''); // Clear input after successful processing - setIsManualScanning(false); - stopScan(); - resetScan(); + } else { console.error(`❌ QR Code processing failed: ${errorCount} errors`); setQrScanError(true); @@ -733,6 +735,224 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { }, 1000); } }, [combinedLotData, fetchJobOrderData]); + const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => { + console.log("Lot mismatch detected:", { expectedLot, scannedLot }); + setExpectedLotData(expectedLot); + setScannedLotData(scannedLot); + setLotConfirmationOpen(true); + }, []); + + // ✅ Add handleLotConfirmation function + const handleLotConfirmation = useCallback(async () => { + if (!expectedLotData || !scannedLotData || !selectedLotForQr) return; + setIsConfirmingLot(true); + try { + let newLotLineId = scannedLotData?.inventoryLotLineId; + if (!newLotLineId && scannedLotData?.stockInLineId) { + const ld = await fetchLotDetail(scannedLotData.stockInLineId); + newLotLineId = ld.inventoryLotLineId; + } + if (!newLotLineId) { + console.error("No inventory lot line id for scanned lot"); + return; + } + + console.log("=== Lot Confirmation Debug ==="); + console.log("Selected Lot:", selectedLotForQr); + console.log("Pick Order Line ID:", selectedLotForQr.pickOrderLineId); + console.log("Stock Out Line ID:", selectedLotForQr.stockOutLineId); + console.log("Suggested Pick Lot ID:", selectedLotForQr.suggestedPickLotId); + console.log("Lot ID (fallback):", selectedLotForQr.lotId); + console.log("New Inventory Lot Line ID:", newLotLineId); + + // ✅ Call confirmLotSubstitution to update the suggested lot + const substitutionResult = await confirmLotSubstitution({ + pickOrderLineId: selectedLotForQr.pickOrderLineId, + stockOutLineId: selectedLotForQr.stockOutLineId, + originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId, + newInventoryLotLineId: newLotLineId + }); + + console.log("✅ Lot substitution result:", substitutionResult); + + // ✅ Update stock out line status to 'checked' after substitution + if(selectedLotForQr?.stockOutLineId){ + await updateStockOutLineStatus({ + id: selectedLotForQr.stockOutLineId, + status: 'checked', + qty: 0 + }); + console.log("✅ Stock out line status updated to 'checked'"); + } + + // ✅ Close modal and clean up state BEFORE refreshing + setLotConfirmationOpen(false); + setExpectedLotData(null); + setScannedLotData(null); + setSelectedLotForQr(null); + + // ✅ Clear QR processing state but DON'T clear processedQrCodes yet + setQrScanError(false); + setQrScanSuccess(true); + setQrScanInput(''); + + // ✅ Set refreshing flag to prevent QR processing during refresh + setIsRefreshingData(true); + + // ✅ Refresh data to show updated lot + console.log("🔄 Refreshing job order data..."); + await fetchJobOrderData(); + 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); + }, 500); // ✅ Reduced from 3000ms to 500ms - just enough for UI update + + } catch (error) { + console.error("Error confirming lot substitution:", error); + setQrScanError(true); + setQrScanSuccess(false); + // ✅ Clear refresh flag on error + setIsRefreshingData(false); + } finally { + setIsConfirmingLot(false); + } + }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData]); + + const processOutsideQrCode = useCallback(async (latestQr: string) => { + // ✅ Don't process if confirmation modal is open + if (lotConfirmationOpen) { + console.log("⏸️ Confirmation modal is open, skipping QR processing"); + return; + } + + let qrData: any = null; + try { + qrData = JSON.parse(latestQr); + } catch { + console.log("QR is not JSON format"); + // Handle non-JSON QR codes as direct lot numbers + const directLotNo = latestQr.replace(/[{}]/g, ''); + if (directLotNo) { + console.log(`Processing direct lot number: ${directLotNo}`); + await handleQrCodeSubmit(directLotNo); + } + 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; + } + + // ✅ First, fetch stock in line info to get the lot number + let stockInLineInfo: any; + try { + stockInLineInfo = await fetchStockInLineInfo(qrData.stockInLineId); + console.log("Stock in line info:", stockInLineInfo); + } catch (error) { + console.error("Error fetching stock in line info:", error); + 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; + } + + // Find the ACTIVE suggested lot (not rejected lots) + const activeSuggestedLots = sameItemLotsInExpected.filter(lot => + lot.lotAvailability !== 'rejected' && + lot.stockOutLineStatus !== 'rejected' && + lot.stockOutLineStatus !== 'completed' + ); + + if (activeSuggestedLots.length === 0) { + console.warn("All lots for this item are rejected or completed"); + setQrScanError(true); + setQrScanSuccess(false); + return; + } + + // Use the first active suggested lot as the "expected" lot + const expectedLot = activeSuggestedLots[0]; + + // 2) Check if the scanned lot matches exactly + if (scanned?.lotNo === expectedLot.lotNo) { + // Case 1: Exact match - process normally + console.log(`✅ Exact lot match: ${scanned.lotNo}`); + await handleQrCodeSubmit(scanned.lotNo); + return; + } + + // Case 2: Same item, different lot - show confirmation modal + console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); + + // ✅ DON'T stop scanning - just pause QR processing by showing modal + 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, lotConfirmationOpen]); + const handleManualInputSubmit = useCallback(() => { if (qrScanInput.trim() !== '') { @@ -782,43 +1002,28 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { } }, [selectedLotForQr, fetchJobOrderData]); - // ✅ Outside QR scanning - process QR codes from outside the page automatically + useEffect(() => { - if (qrValues.length > 0 && combinedLotData.length > 0) { - const latestQr = qrValues[qrValues.length - 1]; - - // Extract lot number from QR code - let lotNo = ''; - try { - const qrData = JSON.parse(latestQr); - if (qrData.stockInLineId && qrData.itemId) { - // For JSON QR codes, we need to fetch the lot number - fetchStockInLineInfo(qrData.stockInLineId) - .then((stockInLineInfo) => { - console.log("Outside QR scan - Stock in line info:", stockInLineInfo); - const extractedLotNo = stockInLineInfo.lotNo; - if (extractedLotNo) { - console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`); - handleQrCodeSubmit(extractedLotNo); - } - }) - .catch((error) => { - console.error("Outside QR scan - Error fetching stock in line info:", error); - }); - return; // Exit early for JSON QR codes - } - } catch (error) { - // Not JSON format, treat as direct lot number - lotNo = latestQr.replace(/[{}]/g, ''); - } + // ✅ Add isManualScanning check + if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData || lotConfirmationOpen) { + return; + } + + const latestQr = qrValues[qrValues.length - 1]; + + if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { + console.log("QR code already processed, skipping..."); + return; + } + + if (latestQr && latestQr !== lastProcessedQr) { + console.log(`🔍 Processing new QR code with enhanced validation: ${latestQr}`); + setLastProcessedQr(latestQr); + setProcessedQrCodes(prev => new Set(prev).add(latestQr)); - // For direct lot number QR codes - if (lotNo) { - console.log(`Outside QR scan detected (direct): ${lotNo}`); - handleQrCodeSubmit(lotNo); - } + processOutsideQrCode(latestQr); } - }, [qrValues, combinedLotData, handleQrCodeSubmit]); + }, [qrValues, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode, combinedLotData, isManualScanning, lotConfirmationOpen]); const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { if (value === '' || value === null || value === undefined) { @@ -1208,6 +1413,16 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { 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]); const getStatusMessage = useCallback((lot: any) => { switch (lot.stockOutLineStatus?.toLowerCase()) { @@ -1475,6 +1690,7 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { {/* ✅ QR Code Modal */} + {!lotConfirmationOpen && ( { @@ -1487,7 +1703,24 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { combinedLotData={combinedLotData} onQrCodeSubmit={handleQrCodeSubmitFromModal} /> - + )} + {/* ✅ Add Lot Confirmation Modal */} + {lotConfirmationOpen && expectedLotData && scannedLotData && ( + { + setLotConfirmationOpen(false); + setExpectedLotData(null); + setScannedLotData(null); + setSelectedLotForQr(null); + + }} + onConfirm={handleLotConfirmation} + expectedLot={expectedLotData} + scannedLot={scannedLotData} + isLoading={isConfirmingLot} + /> + )} {/* ✅ Pick Execution Form Modal */} {pickExecutionFormOpen && selectedLotForExecutionForm && ( = ({ filterArgs }) => { if (successCount > 0) { setQrScanSuccess(true); setQrScanError(false); + + // ✅ Set refreshing flag briefly to prevent duplicate processing + setIsRefreshingData(true); await fetchJobOrderData(); // Refresh data + + // ✅ Clear refresh flag and success message after a short delay + setTimeout(() => { + setQrScanSuccess(false); + setIsRefreshingData(false); + }, 500); } else { setQrScanError(true); setQrScanSuccess(false); @@ -626,52 +635,68 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { } }, [combinedLotData, fetchJobOrderData, processedQrCodes]); + useEffect(() => { - if (qrValues.length > 0 && combinedLotData.length > 0) { - const latestQr = qrValues[qrValues.length - 1]; - - // ✅ Check if this QR was already processed recently - if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { - console.log("⏭️ QR code already processed, skipping..."); - return; - } - - // ✅ Mark as processed - setProcessedQrCodes(prev => new Set(prev).add(latestQr)); - setLastProcessedQr(latestQr); - - // Extract lot number from QR code - let lotNo = ''; - try { - const qrData = JSON.parse(latestQr); - if (qrData.stockInLineId && qrData.itemId) { - // For JSON QR codes, we need to fetch the lot number - fetchStockInLineInfo(qrData.stockInLineId) - .then((stockInLineInfo) => { - console.log("Outside QR scan - Stock in line info:", stockInLineInfo); - const extractedLotNo = stockInLineInfo.lotNo; - if (extractedLotNo) { - console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`); - handleQrCodeSubmit(extractedLotNo); - } - }) - .catch((error) => { - console.error("Outside QR scan - Error fetching stock in line info:", error); - }); - return; // Exit early for JSON QR codes - } - } catch (error) { - // Not JSON format, treat as direct lot number - lotNo = latestQr.replace(/[{}]/g, ''); - } - - // For direct lot number QR codes - if (lotNo) { - console.log(`Outside QR scan detected (direct): ${lotNo}`); - handleQrCodeSubmit(lotNo); + // ✅ Add isManualScanning and isRefreshingData checks + if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { + return; + } + + const latestQr = qrValues[qrValues.length - 1]; + + // ✅ Check if this QR was already processed recently + if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { + console.log("⏭️ QR code already processed, skipping..."); + return; + } + + // ✅ Mark as processed + setProcessedQrCodes(prev => new Set(prev).add(latestQr)); + setLastProcessedQr(latestQr); + + // Extract lot number from QR code + let lotNo = ''; + try { + const qrData = JSON.parse(latestQr); + if (qrData.stockInLineId && qrData.itemId) { + // For JSON QR codes, we need to fetch the lot number + fetchStockInLineInfo(qrData.stockInLineId) + .then((stockInLineInfo) => { + console.log("Outside QR scan - Stock in line info:", stockInLineInfo); + const extractedLotNo = stockInLineInfo.lotNo; + if (extractedLotNo) { + console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`); + handleQrCodeSubmit(extractedLotNo); + } + }) + .catch((error) => { + console.error("Outside QR scan - Error fetching stock in line info:", error); + }); + return; // Exit early for JSON QR codes } + } catch (error) { + // Not JSON format, treat as direct lot number + lotNo = latestQr.replace(/[{}]/g, ''); + } + + // For direct lot number QR codes + if (lotNo) { + console.log(`Outside QR scan detected (direct): ${lotNo}`); + handleQrCodeSubmit(lotNo); } - }, [qrValues, combinedLotData, handleQrCodeSubmit, processedQrCodes, lastProcessedQr]); + }, [qrValues, combinedLotData, handleQrCodeSubmit, processedQrCodes, lastProcessedQr, isManualScanning, isRefreshingData]); + + // ✅ ADD THIS: Cleanup effect + useEffect(() => { + return () => { + // Cleanup when component unmounts (e.g., when switching tabs) + if (isManualScanning) { + console.log("🧹 Second scan component unmounting, stopping QR scanner..."); + stopScan(); + resetScan(); + } + }; + }, [isManualScanning, stopScan, resetScan]); const handleManualInputSubmit = useCallback(() => { if (qrScanInput.trim() !== '') { handleQrCodeSubmit(qrScanInput.trim()); @@ -721,42 +746,7 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { }, [selectedLotForQr, fetchJobOrderData]); // ✅ Outside QR scanning - process QR codes from outside the page automatically - useEffect(() => { - if (qrValues.length > 0 && combinedLotData.length > 0) { - const latestQr = qrValues[qrValues.length - 1]; - - // Extract lot number from QR code - let lotNo = ''; - try { - const qrData = JSON.parse(latestQr); - if (qrData.stockInLineId && qrData.itemId) { - // For JSON QR codes, we need to fetch the lot number - fetchStockInLineInfo(qrData.stockInLineId) - .then((stockInLineInfo) => { - console.log("Outside QR scan - Stock in line info:", stockInLineInfo); - const extractedLotNo = stockInLineInfo.lotNo; - if (extractedLotNo) { - console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`); - handleQrCodeSubmit(extractedLotNo); - } - }) - .catch((error) => { - console.error("Outside QR scan - Error fetching stock in line info:", error); - }); - return; // Exit early for JSON QR codes - } - } catch (error) { - // Not JSON format, treat as direct lot number - lotNo = latestQr.replace(/[{}]/g, ''); - } - - // For direct lot number QR codes - if (lotNo) { - console.log(`Outside QR scan detected (direct): ${lotNo}`); - handleQrCodeSubmit(lotNo); - } - } - }, [qrValues, combinedLotData, handleQrCodeSubmit]); + const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { if (value === '' || value === null || value === undefined) { @@ -993,7 +983,16 @@ const paginatedData = useMemo(() => { stopScan(); resetScan(); }, [stopScan, resetScan]); - + useEffect(() => { + return () => { + // Cleanup when component unmounts (e.g., when switching tabs) + if (isManualScanning) { + console.log("🧹 Second scan component unmounting, stopping QR scanner..."); + stopScan(); + resetScan(); + } + }; + }, [isManualScanning, stopScan, resetScan]); const getStatusMessage = useCallback((lot: any) => { switch (lot.stockOutLineStatus?.toLowerCase()) { case 'pending': @@ -1152,8 +1151,8 @@ const paginatedData = useMemo(() => { })()} - - {lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? ( + + {lot.secondQrScanStatus?.toLowerCase() !== 'pending' ? ( { height: '100%' }}> = ({ scannedLot, isLoading = false, }) => { - const { t } = useTranslation("jo"); + const { t } = useTranslation("pickOrder"); return ( diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index 9276778..dd318dc 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -40,6 +40,7 @@ "Route": "路線", "Qty": "數量", "Unit": "單位", + "Issue": "問題", "Location": "位置", "Scan Result": "掃碼結果", "Expiry Date": "有效期", @@ -88,6 +89,20 @@ "qty is required": "數量是必需的", "qty is not allowed to be greater than remaining available qty": "數量不能大於剩餘可用數量", "qty is not allowed to be greater than required qty": "數量不能大於需求數量", - "qty is not allowed to be greater than picked qty": "數量不能大於已提料數量" + "qty is not allowed to be greater than picked qty": "數量不能大於已提料數量", + "QR code verified.": "QR碼驗證成功。", + "QR code does not match any item in current orders.": "QR碼不匹配當前工單的物料。", + "This form is for reporting issues only. You must report either missing items or bad items.": "此表單僅用於報告問題。您必須報告缺失的物料或不良的物料。", + "Pick Execution Issue Form": "提料執行問題表單", + "Verified Qty": "驗證數量", + "Missing item Qty": "缺失的物料數量", + "Bad Item Qty": "不良的物料數量", + "submit": "提交", + "Issue Remark": "問題描述", + "Received Qty": "接收數量", + "Create": "創建", + "Confirm Lot Substitution": "確認批號替換", + "Processing...": "處理中", +"Processing...": "處理中" } diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index c80f181..aa990a1 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -224,6 +224,8 @@ "Selected items will join above created group": "已選擇的貨品將加入以上建立的分組", "Issue":"問題", "issue":"問題", + "Processing...":"處理中", + "Assigning pick order...":"分派提料單中...", "Pick Execution Issue Form":"提料問題表單", "This form is for reporting issues only. You must report either missing items or bad items.":"此表單僅用於報告問題。您必須報告缺少的貨品或不良貨品。", "Bad item Qty":"不良貨品數量",