diff --git a/src/app/api/inventory/actions.ts b/src/app/api/inventory/actions.ts index a19c390..5f09c65 100644 --- a/src/app/api/inventory/actions.ts +++ b/src/app/api/inventory/actions.ts @@ -52,8 +52,34 @@ export interface PostInventoryLotLineResponse { entity?: T | T[]; consoCode?: string; } - - +export interface QrCodeAnalysisResponse { + itemId: number; + itemCode: string; + itemName: string; + scanned: ScannedLotInfo; + sameItemLots: SameItemLotInfo[]; +} +export interface ScannedLotInfo { + stockInLineId: number; + lotNo: string; + inventoryLotLineId: number; +} +export interface SameItemLotInfo { + lotNo: string; + inventoryLotLineId: number; + availableQty: number; + uom: string; +} +export const analyzeQrCode = async (data: { + itemId: number; + stockInLineId: number; +}) => { + return serverFetchJson(`${BASE_API_URL}/inventoryLotLine/analyze-qr-code`, { + method: 'POST', + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); +}; export const updateInventoryStatus = async (data: { itemId: number; lotId: number; diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index 5fa527b..70986c3 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -260,6 +260,7 @@ export interface FGPickOrderResponse { numberOfCartons: number; DepartureTime: string; truckNo: string; + storeId: string; qrCodeData: number; } export interface AutoAssignReleaseByStoreRequest { @@ -526,7 +527,24 @@ export interface CheckCompleteResponse { message: string | null; errorPosition: string; } - +export interface LotSubstitutionConfirmRequest { + pickOrderLineId: number; + stockOutLineId: number; + originalSuggestedPickLotId: number; + newInventoryLotLineId: number; +} +export const confirmLotSubstitution = async (data: LotSubstitutionConfirmRequest) => { + const response = await serverFetchJson( + `${BASE_API_URL}/pickOrder/lot-substitution/confirm`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + revalidateTag("pickorder"); + return response; +}; export const checkAndCompletePickOrderByConsoCode = async (consoCode: string): Promise => { const response = await serverFetchJson( `${BASE_API_URL}/pickOrder/check-complete/${consoCode}`, diff --git a/src/components/FinishedGoodSearch/FGPickOrderCard.tsx b/src/components/FinishedGoodSearch/FGPickOrderCard.tsx index eae5d66..c694447 100644 --- a/src/components/FinishedGoodSearch/FGPickOrderCard.tsx +++ b/src/components/FinishedGoodSearch/FGPickOrderCard.tsx @@ -43,6 +43,14 @@ const FGPickOrderCard: React.FC = ({ fgOrder, onQrCodeClick }) => { value={fgOrder.shopPoNo} /> + + + = ({ filterArgs }) => { // ✅ Add QR modal states const [qrModalOpen, setQrModalOpen] = useState(false); const [selectedLotForQr, setSelectedLotForQr] = useState(null); - + const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); +const [expectedLotData, setExpectedLotData] = useState(null); +const [scannedLotData, setScannedLotData] = useState(null); +const [isConfirmingLot, setIsConfirmingLot] = useState(false); // ✅ Add GoodPickExecutionForm states const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState(null); @@ -399,6 +410,12 @@ const PickExecution: React.FC = ({ filterArgs }) => { // TODO: Implement QR code functionality }; + const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => { + console.log("Lot mismatch detected:", { expectedLot, scannedLot }); + setExpectedLotData(expectedLot); + setScannedLotData(scannedLot); + setLotConfirmationOpen(true); + }, []); const fetchAllCombinedLotData = useCallback(async (userId?: number) => { setCombinedDataLoading(true); @@ -427,32 +444,38 @@ const PickExecution: React.FC = ({ filterArgs }) => { setCombinedDataLoading(false); } }, [currentUserId]); - - // ✅ Only fetch existing data when session is ready, no auto-assignment - useEffect(() => { - if (session && currentUserId && !initializationRef.current) { - console.log("✅ Session loaded, initializing pick order..."); - initializationRef.current = true; - - // ✅ Only fetch existing data, no auto-assignment - fetchAllCombinedLotData(); + 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 + }); + + setLotConfirmationOpen(false); + setExpectedLotData(null); + setScannedLotData(null); + setSelectedLotForQr(null); + await fetchAllCombinedLotData(); + } catch (error) { + console.error("Error confirming lot substitution:", error); + } finally { + setIsConfirmingLot(false); } - }, [session, currentUserId, fetchAllCombinedLotData]); - - // ✅ Add event listener for manual assignment - useEffect(() => { - const handlePickOrderAssigned = () => { - console.log("🔄 Pick order assigned event received, refreshing data..."); - fetchAllCombinedLotData(); - }; - - window.addEventListener('pickOrderAssigned', handlePickOrderAssigned); - - return () => { - window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned); - }; - }, [fetchAllCombinedLotData]); - + }, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData]); const handleQrCodeSubmit = useCallback(async (lotNo: string) => { console.log(`✅ Processing QR Code for lot: ${lotNo}`); @@ -469,6 +492,8 @@ const PickExecution: React.FC = ({ filterArgs }) => { 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; } @@ -483,38 +508,55 @@ const PickExecution: React.FC = ({ filterArgs }) => { console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`); if (matchingLot.stockOutLineId) { - // ✅ FIXED: Only update status to 'checked', keep qty at 0 const stockOutLineUpdate = await updateStockOutLineStatus({ id: matchingLot.stockOutLineId, status: 'checked', - qty: 0 // ✅ Keep qty at 0 until user actually submits + qty: 0 }); console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate); if (stockOutLineUpdate && stockOutLineUpdate.code === "SUCCESS") { - console.log(`✅ Stock out line updated successfully for line ${matchingLot.pickOrderLineId}`); successCount++; } else { - console.error(`❌ Failed to update stock out line for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate); errorCount++; } } else { - // ✅ If no stock out line exists, create one with qty = 0 const createStockOutLineData = { consoCode: matchingLot.pickOrderConsoCode, pickOrderLineId: matchingLot.pickOrderLineId, inventoryLotLineId: matchingLot.lotId, - qty: 0 // ✅ Create with qty = 0 + qty: 0 }; const createResult = await createStockOutLine(createStockOutLineData); console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult); if (createResult && createResult.code === "SUCCESS") { - console.log(`✅ Stock out line created successfully for line ${matchingLot.pickOrderLineId}`); - successCount++; + // 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 { - console.error(`❌ Failed to create stock out line for line ${matchingLot.pickOrderLineId}:`, createResult); errorCount++; } } @@ -531,18 +573,19 @@ const PickExecution: React.FC = ({ filterArgs }) => { setQrScanInput(''); // Clear input after successful processing // ✅ Clear success state after a delay - setTimeout(() => { - setQrScanSuccess(false); - }, 2000); + + //setTimeout(() => { + //setQrScanSuccess(false); + //}, 2000); } else { console.error(`❌ QR Code processing failed: ${errorCount} errors`); setQrScanError(true); setQrScanSuccess(false); // ✅ Clear error state after a delay - setTimeout(() => { - setQrScanError(false); - }, 3000); + // setTimeout(() => { + // setQrScanError(false); + //}, 3000); } } catch (error) { console.error("❌ Error processing QR code:", error); @@ -564,6 +607,152 @@ const PickExecution: React.FC = ({ filterArgs }) => { }, 1000); } }, [combinedLotData, fetchAllCombinedLotData]); + const processOutsideQrCode = useCallback(async (latestQr: string) => { + // 1) Parse JSON safely + let qrData: any = null; + try { + qrData = JSON.parse(latestQr); + } catch { + console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches."); + setQrScanError(true); + setQrScanSuccess(false); + return; + } + + try { + // Only use the new API when we have JSON with stockInLineId + itemId + if (!(qrData?.stockInLineId && qrData?.itemId)) { + console.log("QR JSON missing required fields (itemId, stockInLineId)."); + setQrScanError(true); + setQrScanSuccess(false); + return; + } + + // Call new analyze-qr-code API + const analysis = await analyzeQrCode({ + itemId: qrData.itemId, + stockInLineId: qrData.stockInLineId + }); + + if (!analysis) { + console.error("analyzeQrCode returned no data"); + setQrScanError(true); + setQrScanSuccess(false); + return; + } + + const { + itemId: analyzedItemId, + itemCode: analyzedItemCode, + itemName: analyzedItemName, + scanned, + } = analysis || {}; + + // 1) Find all lots for the same item from current expected list + const sameItemLotsInExpected = combinedLotData.filter(l => + (l.itemId && analyzedItemId && l.itemId === analyzedItemId) || + (l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode) + ); + + if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) { + // Case 3: No item code match + console.error("No item match in expected lots for scanned code"); + setQrScanError(true); + setQrScanSuccess(false); + + return; + } + // 2) Check if scanned lot is exactly in expected lots + const exactLotMatch = sameItemLotsInExpected.find(l => + (scanned?.inventoryLotLineId && l.lotId === scanned.inventoryLotLineId) || + (scanned?.lotNo && l.lotNo === scanned.lotNo) + ); + + if (exactLotMatch && scanned?.lotNo) { + // Case 1: Normal case - item matches AND lot matches -> proceed + console.log(`Exact lot match found for ${scanned.lotNo}, submitting QR`); + handleQrCodeSubmit(scanned.lotNo); + return; + } + + // Case 2: Item matches but lot number differs -> open confirmation modal + const expectedLot = sameItemLotsInExpected[0]; + if (!expectedLot) { + console.error("Could not determine expected lot for confirmation"); + setQrScanError(true); + setQrScanSuccess(false); + return; + } + + setSelectedLotForQr(expectedLot); + handleLotMismatch( + { + lotNo: expectedLot.lotNo, + itemCode: analyzedItemCode || expectedLot.itemCode, + itemName: analyzedItemName || expectedLot.itemName + }, + { + lotNo: scanned?.lotNo || '', + itemCode: analyzedItemCode || expectedLot.itemCode, + itemName: analyzedItemName || expectedLot.itemName, + inventoryLotLineId: scanned?.inventoryLotLineId, + stockInLineId: qrData.stockInLineId + } + ); + } catch (error) { + console.error("Error during analyzeQrCode flow:", error); + setQrScanError(true); + setQrScanSuccess(false); + return; + } + }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch]); + // ✅ Update the outside QR scanning effect to use enhanced processing + useEffect(() => { + if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { + return; + } + + const latestQr = qrValues[qrValues.length - 1]; + + if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { + console.log("QR code already processed, skipping..."); + return; + } + + if (latestQr && latestQr !== lastProcessedQr) { + console.log(`🔍 Processing new QR code with enhanced validation: ${latestQr}`); + setLastProcessedQr(latestQr); + setProcessedQrCodes(prev => new Set(prev).add(latestQr)); + + processOutsideQrCode(latestQr); + } + }, [qrValues, isManualScanning, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode]); + // ✅ Only fetch existing data when session is ready, no auto-assignment + useEffect(() => { + if (session && currentUserId && !initializationRef.current) { + console.log("✅ Session loaded, initializing pick order..."); + initializationRef.current = true; + + // ✅ Only fetch existing data, no auto-assignment + fetchAllCombinedLotData(); + } + }, [session, currentUserId, fetchAllCombinedLotData]); + + // ✅ Add event listener for manual assignment + useEffect(() => { + const handlePickOrderAssigned = () => { + console.log("🔄 Pick order assigned event received, refreshing data..."); + fetchAllCombinedLotData(); + }; + + window.addEventListener('pickOrderAssigned', handlePickOrderAssigned); + + return () => { + window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned); + }; + }, [fetchAllCombinedLotData]); + + const handleManualInputSubmit = useCallback(() => { if (qrScanInput.trim() !== '') { @@ -612,55 +801,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { } }, [selectedLotForQr, fetchAllCombinedLotData]); - // ✅ Outside QR scanning - process QR codes from outside the page automatically - useEffect(() => { - // ✅ Don't process QR codes when refreshing data or if not manually scanning - if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { - return; - } - - const latestQr = qrValues[qrValues.length - 1]; - - // ✅ Prevent processing the same QR code multiple times - if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { - console.log(" QR code already processed, skipping..."); - return; - } - - if (latestQr && latestQr !== lastProcessedQr) { - console.log(` Processing new QR code: ${latestQr}`); - setLastProcessedQr(latestQr); - setProcessedQrCodes(prev => new Set(prev).add(latestQr)); - - let lotNo = ''; - try { - const qrData = JSON.parse(latestQr); - if (qrData.stockInLineId && qrData.itemId) { - 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; - } - } catch (error) { - lotNo = latestQr.replace(/[{}]/g, ''); - } - - if (lotNo) { - console.log(`Outside QR scan detected (direct): ${lotNo}`); - handleQrCodeSubmit(lotNo); - } - } - }, [qrValues, isManualScanning, processedQrCodes, lastProcessedQr, isRefreshingData, handleQrCodeSubmit]); - const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { if (value === '' || value === null || value === undefined) { @@ -1026,12 +1166,16 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe 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]); @@ -1074,6 +1218,9 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe {t("Pick Order Code")}:{fgPickOrders[0].pickOrderCode || '-'} + + {t("Store ID")}: {fgPickOrders[0].storeId || '-'} + {t("Ticket No.")}: {fgPickOrders[0].ticketNo || '-'} @@ -1125,8 +1272,20 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe )} + + + {qrScanError && ( + + {t("QR code does not match any item in current orders.")} + + )} + {qrScanSuccess && ( + + {t("QR code verified.")} + + )} @@ -1134,6 +1293,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe {t("Index")} {t("Route")} + {t("Item Code")} {t("Item Name")} {t("Lot#")} {/* {t("Target Date")} */} @@ -1178,6 +1338,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe {lot.routerRoute || '-'} + {lot.itemCode} {lot.itemName+'('+lot.stockUnit+')'} @@ -1323,7 +1484,21 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe combinedLotData={combinedLotData} // ✅ Add this prop onQrCodeSubmit={handleQrCodeSubmitFromModal} /> - + {/* ✅ Lot Confirmation Modal */} + {lotConfirmationOpen && expectedLotData && scannedLotData && ( + { + setLotConfirmationOpen(false); + setExpectedLotData(null); + setScannedLotData(null); + }} + onConfirm={handleLotConfirmation} + expectedLot={expectedLotData} + scannedLot={scannedLotData} + isLoading={isConfirmingLot} + /> + )} {/* ✅ Good Pick Execution Form Modal */} {pickExecutionFormOpen && selectedLotForExecutionForm && ( void; + onConfirm: () => void; + expectedLot: { + lotNo: string; + itemCode: string; + itemName: string; + }; + scannedLot: { + lotNo: string; + itemCode: string; + itemName: string; + }; + isLoading?: boolean; +} + +const LotConfirmationModal: React.FC = ({ + open, + onClose, + onConfirm, + expectedLot, + scannedLot, + isLoading = false, +}) => { + const { t } = useTranslation("pickOrder"); + + return ( + + + + {t("Lot Number Mismatch")} + + + + + + + {t("The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?")} + + + + + {t("Expected Lot:")} + + + + {t("Item Code")}: {expectedLot.itemCode} + + + {t("Item Name")}: {expectedLot.itemName} + + + {t("Lot No")}: {expectedLot.lotNo} + + + + + + + + + {t("Scanned Lot:")} + + + + {t("Item Code")}: {scannedLot.itemCode} + + + {t("Item Name")}: {scannedLot.itemName} + + + {t("Lot No")}: {scannedLot.lotNo} + + + + + + {t("If you proceed, the system will:")} +
    +
  • {t("Update the stock out line to use the scanned lot")}
  • +
  • {t("Put the original suggested lot on hold")}
  • +
  • {t("Update inventory lot line for the new lot")}
  • +
+
+
+
+ + + + + +
+ ); +}; + +export default LotConfirmationModal; \ No newline at end of file diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index ba56adf..ee124f9 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -261,5 +261,8 @@ "Start QR Scan":"開始QR掃描", "Stop QR Scan":"停止QR掃描", "Scanning...":"掃描中...", - "Print DN/Label":"列印送貨單/標籤" + "Print DN/Label":"列印送貨單/標籤", + "Store ID":"店鋪編號", + "QR code does not match any item in current orders.":"QR 碼不符合當前訂單中的任何貨品。" + } \ No newline at end of file