|
|
|
@@ -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<string, any>; |
|
|
|
} |
|
|
|
@@ -332,7 +333,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { |
|
|
|
const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); |
|
|
|
|
|
|
|
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); |
|
|
|
|
|
|
|
const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); |
|
|
|
const [expectedLotData, setExpectedLotData] = useState<any>(null); |
|
|
|
const [scannedLotData, setScannedLotData] = useState<any>(null); |
|
|
|
const [isConfirmingLot, setIsConfirmingLot] = useState(false); |
|
|
|
const [qrScanInput, setQrScanInput] = useState<string>(''); |
|
|
|
const [qrScanError, setQrScanError] = useState<boolean>(false); |
|
|
|
const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false); |
|
|
|
@@ -733,6 +737,182 @@ const JobPickExecution: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ filterArgs }) => { |
|
|
|
combinedLotData={combinedLotData} |
|
|
|
onQrCodeSubmit={handleQrCodeSubmitFromModal} |
|
|
|
/> |
|
|
|
|
|
|
|
{/* ✅ Add Lot Confirmation Modal */} |
|
|
|
{lotConfirmationOpen && expectedLotData && scannedLotData && ( |
|
|
|
<LotConfirmationModal |
|
|
|
open={lotConfirmationOpen} |
|
|
|
onClose={() => { |
|
|
|
setLotConfirmationOpen(false); |
|
|
|
setExpectedLotData(null); |
|
|
|
setScannedLotData(null); |
|
|
|
}} |
|
|
|
onConfirm={handleLotConfirmation} |
|
|
|
expectedLot={expectedLotData} |
|
|
|
scannedLot={scannedLotData} |
|
|
|
isLoading={isConfirmingLot} |
|
|
|
/> |
|
|
|
)} |
|
|
|
{/* ✅ Pick Execution Form Modal */} |
|
|
|
{pickExecutionFormOpen && selectedLotForExecutionForm && ( |
|
|
|
<GoodPickExecutionForm |
|
|
|
|