From 771f74c4c686dee48b6f0e2de065df774ed64252 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 30 Sep 2025 11:42:43 +0800 Subject: [PATCH] update --- src/components/JoSave/PickTable.tsx | 4 +- src/components/Jodetail/JobPickExecution.tsx | 256 +++++++++++++++--- .../Jodetail/LotConfirmationModal.tsx | 2 +- src/i18n/zh/jo.json | 16 +- 4 files changed, 235 insertions(+), 43 deletions(-) diff --git a/src/components/JoSave/PickTable.tsx b/src/components/JoSave/PickTable.tsx index 6ab6f3f..12f34ca 100644 --- a/src/components/JoSave/PickTable.tsx +++ b/src/components/JoSave/PickTable.tsx @@ -181,7 +181,7 @@ const PickTable: React.FC = ({ headerAlign: "right", renderCell: (params: GridRenderCellParams) => { const uomShortDesc = getUomShortDesc(params.row); - return `${decimalFormatter.format(params.value)} ${params.row.shortUom}`; + return `${decimalFormatter.format(params.value)} ${uomShortDesc}`; }, }, { @@ -193,7 +193,7 @@ const PickTable: React.FC = ({ type: "number", renderCell: (params: GridRenderCellParams) => { const uomShortDesc = getUomShortDesc(params.row); - return `${decimalFormatter.format(params.value)} ${params.row.shortUom}`; + return `${decimalFormatter.format(params.value)} ${uomShortDesc}`; }, }, { diff --git a/src/components/Jodetail/JobPickExecution.tsx b/src/components/Jodetail/JobPickExecution.tsx index fc89e7a..dc9cdc5 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); @@ -733,6 +737,182 @@ 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; + } + + await confirmLotSubstitution({ + pickOrderLineId: selectedLotForQr.pickOrderLineId, + stockOutLineId: selectedLotForQr.stockOutLineId, + originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId, + newInventoryLotLineId: newLotLineId + }); + + setQrScanError(false); + setQrScanSuccess(false); + setQrScanInput(''); + setProcessedQrCodes(new Set()); + setLastProcessedQr(''); + + if(selectedLotForQr?.stockOutLineId){ + await updateStockOutLineStatus({ + id: selectedLotForQr.stockOutLineId, + status: 'checked', + qty: 0 + }); + } + + setLotConfirmationOpen(false); + setExpectedLotData(null); + setScannedLotData(null); + setSelectedLotForQr(null); + + await fetchJobOrderData(); + console.log("✅ Lot substitution confirmed and data refreshed"); + } catch (error) { + console.error("Error confirming lot substitution:", error); + } finally { + setIsConfirmingLot(false); + } + }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData]); + + const processOutsideQrCode = useCallback(async (latestQr: string) => { + 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}`); + 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]); const handleManualInputSubmit = useCallback(() => { if (qrScanInput.trim() !== '') { @@ -782,43 +962,27 @@ 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, ''); - } + if (qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { + return; + } + + const latestQr = qrValues[qrValues.length - 1]; + + if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { + console.log("QR code already processed, skipping..."); + return; + } + + if (latestQr && 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]); const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { if (value === '' || value === null || value === undefined) { @@ -1487,7 +1651,21 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { combinedLotData={combinedLotData} onQrCodeSubmit={handleQrCodeSubmitFromModal} /> - + {/* ✅ Add Lot Confirmation Modal */} + {lotConfirmationOpen && expectedLotData && scannedLotData && ( + { + setLotConfirmationOpen(false); + setExpectedLotData(null); + setScannedLotData(null); + }} + onConfirm={handleLotConfirmation} + expectedLot={expectedLotData} + scannedLot={scannedLotData} + isLoading={isConfirmingLot} + /> + )} {/* ✅ Pick Execution Form Modal */} {pickExecutionFormOpen && selectedLotForExecutionForm && ( = ({ 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..a43ccb6 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,19 @@ "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": "確認批號替換" + }