|
|
|
@@ -20,7 +20,7 @@ import { |
|
|
|
Modal, |
|
|
|
} from "@mui/material"; |
|
|
|
import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider'; |
|
|
|
import { useCallback, useEffect, useState, useRef, useMemo } from "react"; |
|
|
|
import { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react"; |
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
import { useRouter } from "next/navigation"; |
|
|
|
import { |
|
|
|
@@ -69,6 +69,110 @@ interface Props { |
|
|
|
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<string>(''); |
|
|
|
const [scannedLotInput, setScannedLotInput] = useState<string>(''); |
|
|
|
const [error, setError] = useState<string>(''); |
|
|
|
|
|
|
|
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 ( |
|
|
|
<Modal open={open} onClose={onClose}> |
|
|
|
<Box sx={{ |
|
|
|
position: 'absolute', |
|
|
|
top: '50%', |
|
|
|
left: '50%', |
|
|
|
transform: 'translate(-50%, -50%)', |
|
|
|
bgcolor: 'background.paper', |
|
|
|
p: 3, |
|
|
|
borderRadius: 2, |
|
|
|
minWidth: 500, |
|
|
|
}}> |
|
|
|
<Typography variant="h6" gutterBottom color="warning.main"> |
|
|
|
{t("Manual Lot Confirmation")} |
|
|
|
</Typography> |
|
|
|
|
|
|
|
<Box sx={{ mb: 2 }}> |
|
|
|
<Typography variant="body2" gutterBottom> |
|
|
|
<strong>{t("Expected Lot Number")}:</strong> |
|
|
|
</Typography> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
size="small" |
|
|
|
value={expectedLotInput} |
|
|
|
onChange={(e) => { setExpectedLotInput(e.target.value); setError(''); }} |
|
|
|
sx={{ mb: 2 }} |
|
|
|
error={!!error && !expectedLotInput.trim()} |
|
|
|
/> |
|
|
|
</Box> |
|
|
|
|
|
|
|
<Box sx={{ mb: 2 }}> |
|
|
|
<Typography variant="body2" gutterBottom> |
|
|
|
<strong>{t("Scanned Lot Number")}:</strong> |
|
|
|
</Typography> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
size="small" |
|
|
|
value={scannedLotInput} |
|
|
|
onChange={(e) => { setScannedLotInput(e.target.value); setError(''); }} |
|
|
|
sx={{ mb: 2 }} |
|
|
|
error={!!error && !scannedLotInput.trim()} |
|
|
|
/> |
|
|
|
</Box> |
|
|
|
|
|
|
|
{error && ( |
|
|
|
<Box sx={{ mb: 2, p: 1, backgroundColor: '#ffebee', borderRadius: 1 }}> |
|
|
|
<Typography variant="body2" color="error"> |
|
|
|
{error} |
|
|
|
</Typography> |
|
|
|
</Box> |
|
|
|
)} |
|
|
|
|
|
|
|
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end', gap: 2 }}> |
|
|
|
<Button onClick={onClose} variant="outlined" disabled={isLoading}> |
|
|
|
{t("Cancel")} |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
onClick={handleConfirm} |
|
|
|
variant="contained" |
|
|
|
color="warning" |
|
|
|
disabled={isLoading || !expectedLotInput.trim() || !scannedLotInput.trim()} |
|
|
|
> |
|
|
|
{isLoading ? t("Processing...") : t("Confirm")} |
|
|
|
</Button> |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
</Modal> |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
// QR Code Modal Component (from GoodPickExecution) |
|
|
|
const QrCodeModal: React.FC<{ |
|
|
|
open: boolean; |
|
|
|
@@ -345,6 +449,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
|
const [isConfirmingLot, setIsConfirmingLot] = useState(false); |
|
|
|
const [qrScanInput, setQrScanInput] = useState<string>(''); |
|
|
|
const [qrScanError, setQrScanError] = useState<boolean>(false); |
|
|
|
const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>(''); |
|
|
|
const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false); |
|
|
|
const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null); |
|
|
|
const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({}); |
|
|
|
@@ -379,6 +484,25 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
|
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); |
|
|
|
const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); |
|
|
|
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); |
|
|
|
// Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling |
|
|
|
const [processedQrCombinations, setProcessedQrCombinations] = useState<Map<number, Set<number>>>(new Map()); |
|
|
|
|
|
|
|
// Cache for fetchStockInLineInfo API calls to avoid redundant requests |
|
|
|
const stockInLineInfoCache = useRef<Map<number, { lotNo: string | null; timestamp: number }>>(new Map()); |
|
|
|
const CACHE_TTL = 60000; // 60 seconds cache TTL |
|
|
|
const abortControllerRef = useRef<AbortController | null>(null); |
|
|
|
const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null); |
|
|
|
|
|
|
|
// Use refs for processed QR tracking to avoid useEffect dependency issues and delays |
|
|
|
const processedQrCodesRef = useRef<Set<string>>(new Set()); |
|
|
|
const lastProcessedQrRef = useRef<string>(''); |
|
|
|
|
|
|
|
// Store callbacks in refs to avoid useEffect dependency issues |
|
|
|
const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | 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[] => { |
|
|
|
@@ -426,6 +550,74 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
|
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<number, any[]>(); |
|
|
|
const byItemCode = new Map<string, any[]>(); |
|
|
|
const byLotId = new Map<number, any>(); |
|
|
|
const byLotNo = new Map<string, any[]>(); |
|
|
|
const byStockInLineId = new Map<number, any[]>(); |
|
|
|
const activeLotsByItemId = new Map<number, any[]>(); |
|
|
|
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); |
|
|
|
@@ -719,53 +911,119 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
|
} |
|
|
|
}, [combinedLotData, fetchJobOrderData]); |
|
|
|
const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => { |
|
|
|
console.log("Lot mismatch detected:", { expectedLot, scannedLot }); |
|
|
|
setExpectedLotData(expectedLot); |
|
|
|
setScannedLotData(scannedLot); |
|
|
|
setLotConfirmationOpen(true); |
|
|
|
}, []); |
|
|
|
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) return; |
|
|
|
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) { |
|
|
|
const ld = await fetchLotDetail(scannedLotData.stockInLineId); |
|
|
|
newLotLineId = ld.inventoryLotLineId; |
|
|
|
try { |
|
|
|
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.error("No inventory lot line id for scanned lot"); |
|
|
|
return; |
|
|
|
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 Confirmation Debug ==="); |
|
|
|
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); |
|
|
|
|
|
|
|
// Call confirmLotSubstitution to update the suggested lot |
|
|
|
console.log("🔄 [LOT CONFIRM] Calling confirmLotSubstitution..."); |
|
|
|
const substitutionResult = await confirmLotSubstitution({ |
|
|
|
pickOrderLineId: selectedLotForQr.pickOrderLineId, |
|
|
|
stockOutLineId: selectedLotForQr.stockOutLineId, |
|
|
|
originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId, |
|
|
|
newInventoryLotNo: scannedLotData.lotNo |
|
|
|
newInventoryLotNo: scannedLotData.lotNo || '', |
|
|
|
// ✅ required by LotSubstitutionConfirmRequest |
|
|
|
newStockInLineId: scannedLotData?.stockInLineId ?? null, |
|
|
|
}); |
|
|
|
|
|
|
|
console.log(" Lot substitution result:", substitutionResult); |
|
|
|
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(" Stock out line status updated to 'checked'"); |
|
|
|
console.log(`✅ [LOT CONFIRM] Stock out line ${selectedLotForQr.stockOutLineId} status updated to 'checked'`); |
|
|
|
} |
|
|
|
|
|
|
|
// Close modal and clean up state BEFORE refreshing |
|
|
|
@@ -777,6 +1035,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
|
// 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 |
|
|
|
@@ -796,12 +1055,27 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
|
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 { |
|
|
|
@@ -810,182 +1084,178 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
|
}, [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; |
|
|
|
} |
|
|
|
|
|
|
|
// ✅ Only JSON QR supported for outside scanner (avoid false positive with lotNo) |
|
|
|
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); |
|
|
|
} |
|
|
|
startTransition(() => { |
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
// 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); |
|
|
|
if (!(qrData?.stockInLineId && qrData?.itemId)) { |
|
|
|
startTransition(() => { |
|
|
|
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"); |
|
|
|
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) || []; |
|
|
|
if (activeSuggestedLots.length === 0) { |
|
|
|
startTransition(() => { |
|
|
|
setQrScanError(true); |
|
|
|
setQrScanSuccess(false); |
|
|
|
return; |
|
|
|
}); |
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
// Use the first active suggested lot as the "expected" lot |
|
|
|
const expectedLot = activeSuggestedLots[0]; |
|
|
|
|
|
|
|
if (scanned?.lotNo === expectedLot.lotNo) { |
|
|
|
console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`); |
|
|
|
|
|
|
|
if (!expectedLot.stockOutLineId) { |
|
|
|
console.warn("No stockOutLineId on expectedLot, cannot update status by QR."); |
|
|
|
} |
|
|
|
|
|
|
|
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'}`); |
|
|
|
|
|
|
|
if (exactMatch) { |
|
|
|
if (!exactMatch.stockOutLineId) { |
|
|
|
console.error(`❌ [QR PROCESS] Exact match found but no stockOutLineId`); |
|
|
|
startTransition(() => { |
|
|
|
setQrScanError(true); |
|
|
|
setQrScanSuccess(false); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
const res = await updateStockOutLineStatusByQRCodeAndLotNo({ |
|
|
|
pickOrderLineId: expectedLot.pickOrderLineId, |
|
|
|
inventoryLotNo: scanned.lotNo, |
|
|
|
stockOutLineId: expectedLot.stockOutLineId, |
|
|
|
itemId: expectedLot.itemId, |
|
|
|
status: "checked", |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
console.log(`✅ [QR PROCESS] Processing exact match: lotNo=${exactMatch.lotNo}, stockOutLineId=${exactMatch.stockOutLineId}`); |
|
|
|
try { |
|
|
|
const res = await updateStockOutLineStatusByQRCodeAndLotNo({ |
|
|
|
pickOrderLineId: exactMatch.pickOrderLineId, |
|
|
|
inventoryLotNo: exactMatch.lotNo, |
|
|
|
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); |
|
|
|
}); |
|
|
|
|
|
|
|
const updateOk = |
|
|
|
res?.type === "checked" || |
|
|
|
typeof res?.id === "number" || |
|
|
|
(res?.message && res.message.includes("success")); |
|
|
|
|
|
|
|
if (updateOk) { |
|
|
|
setQrScanError(false); |
|
|
|
setQrScanSuccess(true); |
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
expectedLot.pickOrderId && |
|
|
|
expectedLot.itemId && |
|
|
|
(expectedLot.stockOutLineStatus?.toLowerCase?.() === "pending" || |
|
|
|
!expectedLot.stockOutLineStatus) && |
|
|
|
!expectedLot.handler |
|
|
|
) { |
|
|
|
await updateHandledBy(expectedLot.pickOrderId, expectedLot.itemId); |
|
|
|
} |
|
|
|
|
|
|
|
const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; |
|
|
|
await fetchJobOrderData(pickOrderId); |
|
|
|
} else if (res?.code === "LOT_NUMBER_MISMATCH" || res?.code === "ITEM_MISMATCH") { |
|
|
|
setQrScanError(true); |
|
|
|
setQrScanSuccess(false); |
|
|
|
} else { |
|
|
|
console.warn("Unexpected response from backend:", res); |
|
|
|
// 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 (e) { |
|
|
|
console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e); |
|
|
|
}); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error(`❌ [QR PROCESS] Error updating stockOutLine:`, error); |
|
|
|
startTransition(() => { |
|
|
|
setQrScanError(true); |
|
|
|
setQrScanSuccess(false); |
|
|
|
} |
|
|
|
|
|
|
|
return; // ✅ 直接返回,不再调用后面的分支 |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// Case 2: Same item, different lot - show confirmation modal |
|
|
|
console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); |
|
|
|
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}`); |
|
|
|
|
|
|
|
// DON'T stop scanning - just pause QR processing by showing modal |
|
|
|
// ✅ stockInLineId exists, 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: analyzedItemCode || expectedLot.itemCode, |
|
|
|
itemName: analyzedItemName || expectedLot.itemName |
|
|
|
itemCode: expectedLot.itemCode, |
|
|
|
itemName: expectedLot.itemName |
|
|
|
}, |
|
|
|
{ |
|
|
|
lotNo: scanned?.lotNo || '', |
|
|
|
itemCode: analyzedItemCode || expectedLot.itemCode, |
|
|
|
itemName: analyzedItemName || expectedLot.itemName, |
|
|
|
inventoryLotLineId: scanned?.inventoryLotLineId, |
|
|
|
stockInLineId: qrData.stockInLineId |
|
|
|
lotNo: stockInLineInfo.lotNo || null, // Use fetched lotNo for display |
|
|
|
itemCode: expectedLot.itemCode, |
|
|
|
itemName: expectedLot.itemName, |
|
|
|
inventoryLotLineId: null, |
|
|
|
stockInLineId: scannedStockInLineId |
|
|
|
} |
|
|
|
); |
|
|
|
} catch (error) { |
|
|
|
console.error("Error during analyzeQrCode flow:", error); |
|
|
|
setQrScanError(true); |
|
|
|
setQrScanSuccess(false); |
|
|
|
return; |
|
|
|
// ✅ 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; |
|
|
|
}); |
|
|
|
} |
|
|
|
}, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen, updateHandledBy]); |
|
|
|
}, [filterArgs?.pickOrderId, fetchJobOrderData, handleLotMismatch, lotDataIndexes, processedQrCombinations]); |
|
|
|
|
|
|
|
// Store in refs for immediate access in qrValues effect |
|
|
|
processOutsideQrCodeRef.current = processOutsideQrCode; |
|
|
|
resetScanRef.current = resetScan; |
|
|
|
|
|
|
|
|
|
|
|
const handleManualInputSubmit = useCallback(() => { |
|
|
|
@@ -1039,26 +1309,84 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
// Add isManualScanning check |
|
|
|
if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData || lotConfirmationOpen) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 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]; |
|
|
|
|
|
|
|
if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { |
|
|
|
console.log("QR code already processed, skipping..."); |
|
|
|
return; |
|
|
|
|
|
|
|
// ✅ 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; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (latestQr && latestQr !== lastProcessedQr) { |
|
|
|
console.log(`🔍 Processing new QR code with enhanced validation: ${latestQr}`); |
|
|
|
|
|
|
|
// ✅ Shortcut: {2fic} open manual lot confirmation modal |
|
|
|
if (latestQr === "{2fic}") { |
|
|
|
setManualLotConfirmationOpen(true); |
|
|
|
resetScanRef.current?.(); |
|
|
|
lastProcessedQrRef.current = latestQr; |
|
|
|
processedQrCodesRef.current.add(latestQr); |
|
|
|
setLastProcessedQr(latestQr); |
|
|
|
setProcessedQrCodes(prev => new Set(prev).add(latestQr)); |
|
|
|
|
|
|
|
processOutsideQrCode(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); |
|
|
|
} |
|
|
|
}, [qrValues, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode, combinedLotData, isManualScanning, lotConfirmationOpen]); |
|
|
|
|
|
|
|
// 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) { |
|
|
|
@@ -1715,7 +2043,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
|
backgroundColor: "error.light", |
|
|
|
}} |
|
|
|
> |
|
|
|
{t("QR code does not match any item in current orders.")} |
|
|
|
{qrScanErrorMsg || t("QR code does not match any item in current orders.")} |
|
|
|
</Alert> |
|
|
|
)} |
|
|
|
{qrScanSuccess && ( |
|
|
|
@@ -1878,34 +2206,32 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
|
<Button |
|
|
|
variant="outlined" |
|
|
|
size="small" |
|
|
|
onClick={() => handlePickExecutionForm(lot)} |
|
|
|
disabled={ |
|
|
|
(lot.lotAvailability === 'expired' || |
|
|
|
lot.lotAvailability === 'status_unavailable' || |
|
|
|
lot.lotAvailability === 'rejected') || |
|
|
|
lot.stockOutLineStatus === 'completed' || // Disable when finished |
|
|
|
lot.stockOutLineStatus === 'pending' // Disable when QR scan not passed |
|
|
|
} |
|
|
|
sx={{ |
|
|
|
fontSize: '0.7rem', |
|
|
|
py: 0.5, |
|
|
|
minHeight: '28px', |
|
|
|
minWidth: '60px', |
|
|
|
borderColor: 'warning.main', |
|
|
|
color: 'warning.main' |
|
|
|
}} |
|
|
|
title="Report missing or bad items" |
|
|
|
> |
|
|
|
{t("Issue")} |
|
|
|
onClick={() => handlePickExecutionForm(lot)} |
|
|
|
disabled={ |
|
|
|
// ✅ align with GoodPickExecutiondetail: Edit only disabled when completed |
|
|
|
lot.stockOutLineStatus === 'completed' |
|
|
|
} |
|
|
|
sx={{ |
|
|
|
fontSize: '0.7rem', |
|
|
|
py: 0.5, |
|
|
|
minHeight: '28px', |
|
|
|
minWidth: '60px', |
|
|
|
borderColor: 'warning.main', |
|
|
|
color: 'warning.main' |
|
|
|
}} |
|
|
|
title="Report missing or bad items" |
|
|
|
> |
|
|
|
{t("Edit")} |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant="outlined" |
|
|
|
size="small" |
|
|
|
onClick={() => handleSkip(lot)} |
|
|
|
// ✅ align with GoodPickExecutiondetail: Just Complete submits requiredQty (not 0) |
|
|
|
onClick={() => handleSubmitPickQtyWithQty(lot, lot.requiredQty || lot.pickOrderLineRequiredQty || 0)} |
|
|
|
disabled={lot.stockOutLineStatus === 'completed'} |
|
|
|
sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }} |
|
|
|
sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }} |
|
|
|
> |
|
|
|
{t("Skip")} |
|
|
|
{t("Just Complete")} |
|
|
|
</Button> |
|
|
|
</Stack> |
|
|
|
</Box> |
|
|
|
@@ -1953,11 +2279,35 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
|
<LotConfirmationModal |
|
|
|
open={lotConfirmationOpen} |
|
|
|
onClose={() => { |
|
|
|
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} |
|
|
|
@@ -1965,6 +2315,25 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
|
isLoading={isConfirmingLot} |
|
|
|
/> |
|
|
|
)} |
|
|
|
|
|
|
|
{/* Manual Lot Confirmation Modal (test shortcut {2fic}) */} |
|
|
|
<ManualLotConfirmationModal |
|
|
|
open={manualLotConfirmationOpen} |
|
|
|
onClose={() => 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 && ( |
|
|
|
<GoodPickExecutionForm |
|
|
|
|