|
|
@@ -43,7 +43,6 @@ import { |
|
|
PickOrderCompletionResponse, |
|
|
PickOrderCompletionResponse, |
|
|
updateSuggestedLotLineId, |
|
|
updateSuggestedLotLineId, |
|
|
updateStockOutLineStatusByQRCodeAndLotNo, |
|
|
updateStockOutLineStatusByQRCodeAndLotNo, |
|
|
confirmLotSubstitution, |
|
|
|
|
|
fetchDoPickOrderDetail, // 必须添加 |
|
|
fetchDoPickOrderDetail, // 必须添加 |
|
|
DoPickOrderDetail, // 必须添加 |
|
|
DoPickOrderDetail, // 必须添加 |
|
|
batchScan, |
|
|
batchScan, |
|
|
@@ -68,7 +67,6 @@ import QrCodeIcon from '@mui/icons-material/QrCode'; |
|
|
import { useQrCodeScannerContext } from "@/components/QrCodeScannerProvider/QrCodeScannerProvider"; |
|
|
import { useQrCodeScannerContext } from "@/components/QrCodeScannerProvider/QrCodeScannerProvider"; |
|
|
import { useSession } from "next-auth/react"; |
|
|
import { useSession } from "next-auth/react"; |
|
|
import { SessionWithTokens } from "@/config/authConfig"; |
|
|
import { SessionWithTokens } from "@/config/authConfig"; |
|
|
import { fetchStockInLineInfo } from "@/app/api/po/actions"; |
|
|
|
|
|
import GoodPickExecutionForm from "@/components/FinishedGoodSearch/GoodPickExecutionForm"; |
|
|
import GoodPickExecutionForm from "@/components/FinishedGoodSearch/GoodPickExecutionForm"; |
|
|
import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; |
|
|
import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; |
|
|
import FGPickOrderCard from "@/components/FinishedGoodSearch/FGPickOrderCard"; |
|
|
import FGPickOrderCard from "@/components/FinishedGoodSearch/FGPickOrderCard"; |
|
|
@@ -211,7 +209,7 @@ function hasPendingSwitchableRowForItem( |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** Allow scan when scanned SIL still needed, or item has any pending/noLot row for auto-switch. */ |
|
|
|
|
|
|
|
|
/** Allow scan when the scanned lot is still needed, or the item still has a pending line to reject against. */ |
|
|
function canProcessWorkbenchScanPick( |
|
|
function canProcessWorkbenchScanPick( |
|
|
indexes: { |
|
|
indexes: { |
|
|
byStockInLineId: Map<number, any[]>; |
|
|
byStockInLineId: Map<number, any[]>; |
|
|
@@ -237,42 +235,43 @@ function findExactActiveMatchForStockInLine( |
|
|
scannedItemId: number, |
|
|
scannedItemId: number, |
|
|
activeSuggestedLots: any[], |
|
|
activeSuggestedLots: any[], |
|
|
processedByItemId: ProcessedStockOutLinesByItemId, |
|
|
processedByItemId: ProcessedStockOutLinesByItemId, |
|
|
|
|
|
scannedStockInLineId?: number, |
|
|
): any | null { |
|
|
): any | null { |
|
|
const activeSet = new Set(activeSuggestedLots); |
|
|
const activeSet = new Set(activeSuggestedLots); |
|
|
|
|
|
const activeIdSet = new Set( |
|
|
|
|
|
activeSuggestedLots |
|
|
|
|
|
.map((lot) => Number(lot?.stockInLineId)) |
|
|
|
|
|
.filter((id) => Number.isFinite(id) && id > 0), |
|
|
|
|
|
); |
|
|
|
|
|
const isSuggested = (lot: any) => |
|
|
|
|
|
activeSet.has(lot) || activeIdSet.has(Number(lot?.stockInLineId)); |
|
|
const candidates = stockInLineLots.filter( |
|
|
const candidates = stockInLineLots.filter( |
|
|
(lot) => |
|
|
(lot) => |
|
|
lot.itemId === scannedItemId && |
|
|
|
|
|
activeSet.has(lot) && |
|
|
|
|
|
|
|
|
Number(lot.itemId) === Number(scannedItemId) && |
|
|
|
|
|
isSuggested(lot) && |
|
|
!isStockOutLineAlreadyProcessed( |
|
|
!isStockOutLineAlreadyProcessed( |
|
|
processedByItemId, |
|
|
processedByItemId, |
|
|
scannedItemId, |
|
|
scannedItemId, |
|
|
lot.stockOutLineId, |
|
|
lot.stockOutLineId, |
|
|
), |
|
|
), |
|
|
); |
|
|
); |
|
|
if (candidates.length === 0) return null; |
|
|
|
|
|
return candidates.find((lot) => isLotRowPending(lot)) ?? candidates[0]; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function pickExpectedLotForSubstitution( |
|
|
|
|
|
activeSuggestedLots: any[], |
|
|
|
|
|
processedByItemId?: ProcessedStockOutLinesByItemId, |
|
|
|
|
|
): any | null { |
|
|
|
|
|
if (!activeSuggestedLots?.length) return null; |
|
|
|
|
|
const itemId = activeSuggestedLots[0]?.itemId; |
|
|
|
|
|
const processed = processedByItemId ?? new Map(); |
|
|
|
|
|
const unprocessed = activeSuggestedLots.filter( |
|
|
|
|
|
(l) => |
|
|
|
|
|
!itemId || |
|
|
|
|
|
!isStockOutLineAlreadyProcessed(processed, itemId, l.stockOutLineId), |
|
|
|
|
|
); |
|
|
|
|
|
const pool = unprocessed.length > 0 ? unprocessed : activeSuggestedLots; |
|
|
|
|
|
const withLotNo = pool.filter( |
|
|
|
|
|
(l) => l.lotNo != null && String(l.lotNo).trim() !== "", |
|
|
|
|
|
|
|
|
if (candidates.length > 0) { |
|
|
|
|
|
return candidates.find((lot) => isLotRowPending(lot)) ?? candidates[0]; |
|
|
|
|
|
} |
|
|
|
|
|
const scannedId = Number(scannedStockInLineId); |
|
|
|
|
|
if (!(scannedId > 0)) return null; |
|
|
|
|
|
return ( |
|
|
|
|
|
activeSuggestedLots.find( |
|
|
|
|
|
(lot) => |
|
|
|
|
|
Number(lot?.itemId) === Number(scannedItemId) && |
|
|
|
|
|
Number(lot?.stockInLineId) === scannedId && |
|
|
|
|
|
!isStockOutLineAlreadyProcessed( |
|
|
|
|
|
processedByItemId, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
lot.stockOutLineId, |
|
|
|
|
|
), |
|
|
|
|
|
) ?? null |
|
|
); |
|
|
); |
|
|
const searchPool = withLotNo.length > 0 ? withLotNo : pool; |
|
|
|
|
|
if (searchPool.length === 1) return searchPool[0]; |
|
|
|
|
|
const pending = searchPool.find((l) => isLotRowPending(l)); |
|
|
|
|
|
return pending ?? searchPool[0]; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const ManualLotConfirmationModal: React.FC<{ |
|
|
const ManualLotConfirmationModal: React.FC<{ |
|
|
@@ -777,10 +776,6 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); |
|
|
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); |
|
|
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); |
|
|
const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false); |
|
|
const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false); |
|
|
|
|
|
|
|
|
// 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); |
|
|
const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null); |
|
|
|
|
|
|
|
|
// Use refs for processed QR tracking to avoid useEffect dependency issues and delays |
|
|
// Use refs for processed QR tracking to avoid useEffect dependency issues and delays |
|
|
@@ -834,56 +829,6 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); |
|
|
}; |
|
|
}; |
|
|
}, [combinedLotData]); |
|
|
}, [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); |
|
|
|
|
|
|
|
|
|
|
|
// Return cached value if still valid |
|
|
|
|
|
if (cached && (now - cached.timestamp) < CACHE_TTL) { |
|
|
|
|
|
console.log(`✅ [CACHE HIT] Using cached stockInLineInfo for ${stockInLineId}`); |
|
|
|
|
|
return { lotNo: cached.lotNo }; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Cancel previous request if exists |
|
|
|
|
|
if (abortControllerRef.current) { |
|
|
|
|
|
abortControllerRef.current.abort(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Create new abort controller for this request |
|
|
|
|
|
const abortController = new AbortController(); |
|
|
|
|
|
abortControllerRef.current = abortController; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
console.log(` [CACHE MISS] Fetching stockInLineInfo for ${stockInLineId}`); |
|
|
|
|
|
const stockInLineInfo = await fetchStockInLineInfo(stockInLineId); |
|
|
|
|
|
|
|
|
|
|
|
// Store in cache |
|
|
|
|
|
stockInLineInfoCache.current.set(stockInLineId, { |
|
|
|
|
|
lotNo: stockInLineInfo.lotNo || null, |
|
|
|
|
|
timestamp: now |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Limit cache size to prevent memory leaks |
|
|
|
|
|
if (stockInLineInfoCache.current.size > 100) { |
|
|
|
|
|
const firstKey = stockInLineInfoCache.current.keys().next().value; |
|
|
|
|
|
if (firstKey !== undefined) { |
|
|
|
|
|
stockInLineInfoCache.current.delete(firstKey); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return { lotNo: stockInLineInfo.lotNo || null }; |
|
|
|
|
|
} catch (error: any) { |
|
|
|
|
|
if (error.name === 'AbortError') { |
|
|
|
|
|
console.log(` [CACHE] Request aborted for ${stockInLineId}`); |
|
|
|
|
|
throw error; |
|
|
|
|
|
} |
|
|
|
|
|
console.error(`❌ [CACHE] Error fetching stockInLineInfo for ${stockInLineId}:`, error); |
|
|
|
|
|
throw error; |
|
|
|
|
|
} |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const checkAllLotsCompleted = useCallback((lotData: any[]) => { |
|
|
const checkAllLotsCompleted = useCallback((lotData: any[]) => { |
|
|
if (lotData.length === 0) { |
|
|
if (lotData.length === 0) { |
|
|
setAllLotsCompleted(false); |
|
|
setAllLotsCompleted(false); |
|
|
@@ -1805,6 +1750,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
scannedItemId, |
|
|
scannedItemId, |
|
|
activeSuggestedLots, |
|
|
activeSuggestedLots, |
|
|
processedQrCombinations, |
|
|
processedQrCombinations, |
|
|
|
|
|
scannedStockInLineId, |
|
|
) ?? |
|
|
) ?? |
|
|
allLotsForItem.find( |
|
|
allLotsForItem.find( |
|
|
(lot: any) => lot.stockInLineId === scannedStockInLineId, |
|
|
(lot: any) => lot.stockInLineId === scannedStockInLineId, |
|
|
@@ -1869,7 +1815,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// ✅ If no active suggested lots, auto-switch to scanned lot (no modal) |
|
|
|
|
|
|
|
|
// No active suggested lot: reject the scan. Do not bind another lot. |
|
|
if (activeSuggestedLots.length === 0) { |
|
|
if (activeSuggestedLots.length === 0) { |
|
|
// Check if there are any lots for this item (even if all are rejected) |
|
|
// Check if there are any lots for this item (even if all are rejected) |
|
|
if (allLotsForItem.length === 0) { |
|
|
if (allLotsForItem.length === 0) { |
|
|
@@ -1882,184 +1828,17 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
console.log(`⚠️ [QR PROCESS] No active suggested lots, auto-switching to scanned lot.`); |
|
|
|
|
|
|
|
|
|
|
|
// Prefer pending noLot / rejected / unavailable rows (not completed ones). |
|
|
|
|
|
const switchableLot = allLotsForItem.find( |
|
|
|
|
|
(lot: any) => |
|
|
|
|
|
isLotRowPending(lot) && |
|
|
|
|
|
!isStockOutLineAlreadyProcessed( |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
lot.stockOutLineId, |
|
|
|
|
|
) && |
|
|
|
|
|
(lot.noLot === true || |
|
|
|
|
|
lot.stockOutLineStatus?.toLowerCase() === "rejected" || |
|
|
|
|
|
lot.lotAvailability === "rejected" || |
|
|
|
|
|
isInventoryLotLineUnavailable(lot) || |
|
|
|
|
|
isLotAvailabilityExpired(lot)), |
|
|
|
|
|
|
|
|
const notSuggestedMsg = t( |
|
|
|
|
|
"Scanned lot is not the suggested lot. Please scan the suggested lot.", |
|
|
|
|
|
); |
|
|
|
|
|
console.warn( |
|
|
|
|
|
`⚠️ [QR PROCESS] No active suggested lot for scanned stockInLineId=${scannedStockInLineId}`, |
|
|
); |
|
|
); |
|
|
const expectedLot = |
|
|
|
|
|
switchableLot || |
|
|
|
|
|
pickExpectedLotForSubstitution( |
|
|
|
|
|
allLotsForItem.filter( |
|
|
|
|
|
(l: any) => l.lotNo != null && String(l.lotNo).trim() !== "" |
|
|
|
|
|
), |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
) || |
|
|
|
|
|
allLotsForItem.find( |
|
|
|
|
|
(lot: any) => |
|
|
|
|
|
isLotRowPending(lot) && |
|
|
|
|
|
!isStockOutLineAlreadyProcessed( |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
lot.stockOutLineId, |
|
|
|
|
|
), |
|
|
|
|
|
) || |
|
|
|
|
|
allLotsForItem[0]; |
|
|
|
|
|
|
|
|
|
|
|
let scannedLotNo: string | null = scannedLot?.lotNo || null; |
|
|
|
|
|
if (!scannedLotNo) { |
|
|
|
|
|
try { |
|
|
|
|
|
const info = await fetchStockInLineInfoCached(scannedStockInLineId); |
|
|
|
|
|
scannedLotNo = info?.lotNo || null; |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.warn("Failed to fetch lotNo for stockInLineId:", scannedStockInLineId, e); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
if (!scannedLotNo) { |
|
|
|
|
|
startTransition(() => { |
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setQrScanErrorMsg( |
|
|
|
|
|
t("Cannot resolve lot number from QR. Please rescan or use manual confirmation.") |
|
|
|
|
|
); |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!workbenchMode) { |
|
|
|
|
|
const substitutionResult = await confirmLotSubstitution({ |
|
|
|
|
|
pickOrderLineId: expectedLot.pickOrderLineId, |
|
|
|
|
|
stockOutLineId: expectedLot.stockOutLineId, |
|
|
|
|
|
originalSuggestedPickLotId: expectedLot.suggestedPickLotId, |
|
|
|
|
|
newInventoryLotNo: "", |
|
|
|
|
|
newStockInLineId: scannedStockInLineId, |
|
|
|
|
|
}); |
|
|
|
|
|
const substitutionCode = (substitutionResult as any)?.code; |
|
|
|
|
|
const switchedToUnavailable = |
|
|
|
|
|
substitutionCode === "SUCCESS_UNAVAILABLE" || substitutionCode === "BOUND_UNAVAILABLE"; |
|
|
|
|
|
if (!substitutionResult || (substitutionCode !== "SUCCESS" && !switchedToUnavailable)) { |
|
|
|
|
|
const errMsg = |
|
|
|
|
|
substitutionResult?.message || |
|
|
|
|
|
t("Lot switch failed; pick line was not updated."); |
|
|
|
|
|
startTransition(() => { |
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setQrScanErrorMsg(errMsg); |
|
|
|
|
|
}); |
|
|
|
|
|
if (expectedLot.stockOutLineId != null) { |
|
|
|
|
|
rememberWorkbenchScanReject(Number(expectedLot.stockOutLineId), errMsg); |
|
|
|
|
|
} |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const res = await workbenchScanPick({ |
|
|
|
|
|
stockOutLineId: expectedLot.stockOutLineId, |
|
|
|
|
|
lotNo: scannedLotNo, |
|
|
|
|
|
...(typeof scannedStockInLineId === "number" && |
|
|
|
|
|
Number.isFinite(scannedStockInLineId) && |
|
|
|
|
|
scannedStockInLineId > 0 |
|
|
|
|
|
? { stockInLineId: scannedStockInLineId } |
|
|
|
|
|
: {}), |
|
|
|
|
|
...workbenchScanPickQtyFromLot(expectedLot), |
|
|
|
|
|
storeId: fgPickOrders?.[0]?.storeId ?? null, |
|
|
|
|
|
userId: currentUserId ?? 1, |
|
|
|
|
|
}); |
|
|
|
|
|
const ok = res.code === "SUCCESS"; |
|
|
|
|
|
if (!ok) { |
|
|
|
|
|
const failMsg = |
|
|
|
|
|
(res as { message?: string })?.message || |
|
|
|
|
|
t("Workbench scan-pick failed."); |
|
|
|
|
|
if ( |
|
|
|
|
|
shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && |
|
|
|
|
|
expectedLot |
|
|
|
|
|
) { |
|
|
|
|
|
openWorkbenchLotLabelModalForLot(expectedLot, failMsg, { |
|
|
|
|
|
markAsScanIssue: true, |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
if (workbenchMode && expectedLot.stockOutLineId != null) { |
|
|
|
|
|
rememberWorkbenchScanReject(Number(expectedLot.stockOutLineId), failMsg); |
|
|
|
|
|
} |
|
|
|
|
|
startTransition(() => { |
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setQrScanErrorMsg(failMsg); |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
clearWorkbenchScanReject(Number(expectedLot.stockOutLineId)); |
|
|
|
|
|
const entity = res.entity as any; |
|
|
|
|
|
const nextStatus = String(entity?.status ?? "completed").toLowerCase(); |
|
|
|
|
|
const nextQty = entity?.qty != null ? Number(entity.qty) : undefined; |
|
|
|
|
|
|
|
|
|
|
|
startTransition(() => { |
|
|
startTransition(() => { |
|
|
setQrScanError(false); |
|
|
|
|
|
setQrScanSuccess(true); |
|
|
|
|
|
setCombinedLotData((prev) => |
|
|
|
|
|
prev.map((lot) => { |
|
|
|
|
|
if ( |
|
|
|
|
|
lot.stockOutLineId === expectedLot.stockOutLineId && |
|
|
|
|
|
lot.pickOrderLineId === expectedLot.pickOrderLineId |
|
|
|
|
|
) { |
|
|
|
|
|
return { |
|
|
|
|
|
...lot, |
|
|
|
|
|
lotNo: scannedLotNo, |
|
|
|
|
|
stockOutLineStatus: nextStatus, |
|
|
|
|
|
stockOutLineQty: nextQty ?? lot.stockOutLineQty, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
return lot; |
|
|
|
|
|
}), |
|
|
|
|
|
); |
|
|
|
|
|
setOriginalCombinedData((prev) => |
|
|
|
|
|
prev.map((lot) => { |
|
|
|
|
|
if ( |
|
|
|
|
|
lot.stockOutLineId === expectedLot.stockOutLineId && |
|
|
|
|
|
lot.pickOrderLineId === expectedLot.pickOrderLineId |
|
|
|
|
|
) { |
|
|
|
|
|
return { |
|
|
|
|
|
...lot, |
|
|
|
|
|
lotNo: scannedLotNo, |
|
|
|
|
|
stockOutLineStatus: nextStatus, |
|
|
|
|
|
stockOutLineQty: nextQty ?? lot.stockOutLineQty, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
return lot; |
|
|
|
|
|
}), |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setQrScanErrorMsg(notSuggestedMsg); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const nextProcessedNoActive = markProcessedStockOutLine( |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
expectedLot.stockOutLineId, |
|
|
|
|
|
); |
|
|
|
|
|
setProcessedQrCombinations(nextProcessedNoActive); |
|
|
|
|
|
maybeReleaseQrForNextSol( |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
scannedStockInLineId, |
|
|
|
|
|
nextProcessedNoActive, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
if (workbenchMode) { |
|
|
|
|
|
await refreshWorkbenchAfterScanPick(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -2071,170 +1850,24 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
scannedItemId, |
|
|
scannedItemId, |
|
|
activeSuggestedLots, |
|
|
activeSuggestedLots, |
|
|
processedQrCombinations, |
|
|
processedQrCombinations, |
|
|
|
|
|
scannedStockInLineId, |
|
|
); |
|
|
); |
|
|
const matchTime = performance.now() - matchStartTime; |
|
|
const matchTime = performance.now() - matchStartTime; |
|
|
console.log(` [PERF] Find exact match time: ${matchTime.toFixed(2)}ms, found: ${exactMatch ? 'yes' : 'no'}`); |
|
|
console.log(` [PERF] Find exact match time: ${matchTime.toFixed(2)}ms, found: ${exactMatch ? 'yes' : 'no'}`); |
|
|
|
|
|
|
|
|
// ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots |
|
|
|
|
|
// This handles the case where Lot A is rejected and user scans Lot B |
|
|
|
|
|
// Also handle case where scanned lot is not in allLotsForItem (scannedLot is undefined) |
|
|
|
|
|
if (!exactMatch) { |
|
|
if (!exactMatch) { |
|
|
const expectedLot = |
|
|
|
|
|
pickExpectedLotForSubstitution( |
|
|
|
|
|
activeSuggestedLots, |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
) || allLotsForItem[0]; |
|
|
|
|
|
if (expectedLot) { |
|
|
|
|
|
const shouldAutoSwitch = |
|
|
|
|
|
!scannedLot || (scannedLot.stockInLineId !== expectedLot.stockInLineId); |
|
|
|
|
|
if (shouldAutoSwitch) { |
|
|
|
|
|
console.log( |
|
|
|
|
|
`⚠️ [QR PROCESS] Auto-switching (scanned lot ${scannedLot?.lotNo || 'not in data'} is not in active suggested lots)`, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
let scannedLotNo: string | null = scannedLot?.lotNo || null; |
|
|
|
|
|
if (!scannedLotNo) { |
|
|
|
|
|
try { |
|
|
|
|
|
const info = await fetchStockInLineInfoCached(scannedStockInLineId); |
|
|
|
|
|
scannedLotNo = info?.lotNo || null; |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.warn("Failed to fetch lotNo for stockInLineId:", scannedStockInLineId, e); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
if (!scannedLotNo) { |
|
|
|
|
|
startTransition(() => { |
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setQrScanErrorMsg( |
|
|
|
|
|
t("Cannot resolve lot number from QR. Please rescan or use manual confirmation.") |
|
|
|
|
|
); |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!workbenchMode) { |
|
|
|
|
|
const substitutionResult = await confirmLotSubstitution({ |
|
|
|
|
|
pickOrderLineId: expectedLot.pickOrderLineId, |
|
|
|
|
|
stockOutLineId: expectedLot.stockOutLineId, |
|
|
|
|
|
originalSuggestedPickLotId: expectedLot.suggestedPickLotId, |
|
|
|
|
|
newInventoryLotNo: "", |
|
|
|
|
|
newStockInLineId: scannedStockInLineId, |
|
|
|
|
|
}); |
|
|
|
|
|
const substitutionCode = (substitutionResult as any)?.code; |
|
|
|
|
|
const switchedToUnavailable = |
|
|
|
|
|
substitutionCode === "SUCCESS_UNAVAILABLE" || substitutionCode === "BOUND_UNAVAILABLE"; |
|
|
|
|
|
if (!substitutionResult || (substitutionCode !== "SUCCESS" && !switchedToUnavailable)) { |
|
|
|
|
|
const errMsg = |
|
|
|
|
|
substitutionResult?.message || |
|
|
|
|
|
t("Lot switch failed; pick line was not updated."); |
|
|
|
|
|
startTransition(() => { |
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setQrScanErrorMsg(errMsg); |
|
|
|
|
|
}); |
|
|
|
|
|
if (expectedLot.stockOutLineId != null) { |
|
|
|
|
|
rememberWorkbenchScanReject(Number(expectedLot.stockOutLineId), errMsg); |
|
|
|
|
|
} |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const res = await workbenchScanPick({ |
|
|
|
|
|
stockOutLineId: expectedLot.stockOutLineId, |
|
|
|
|
|
lotNo: scannedLotNo, |
|
|
|
|
|
...(typeof scannedStockInLineId === "number" && |
|
|
|
|
|
Number.isFinite(scannedStockInLineId) && |
|
|
|
|
|
scannedStockInLineId > 0 |
|
|
|
|
|
? { stockInLineId: scannedStockInLineId } |
|
|
|
|
|
: {}), |
|
|
|
|
|
...workbenchScanPickQtyFromLot(expectedLot), |
|
|
|
|
|
storeId: fgPickOrders?.[0]?.storeId ?? null, |
|
|
|
|
|
userId: currentUserId ?? 1, |
|
|
|
|
|
}); |
|
|
|
|
|
const ok = res.code === "SUCCESS"; |
|
|
|
|
|
if (!ok) { |
|
|
|
|
|
const failMsg = |
|
|
|
|
|
(res as { message?: string })?.message || |
|
|
|
|
|
t("Workbench scan-pick failed."); |
|
|
|
|
|
if ( |
|
|
|
|
|
shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && |
|
|
|
|
|
expectedLot |
|
|
|
|
|
) { |
|
|
|
|
|
openWorkbenchLotLabelModalForLot(expectedLot, failMsg, { |
|
|
|
|
|
markAsScanIssue: true, |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
if (workbenchMode && expectedLot.stockOutLineId != null) { |
|
|
|
|
|
rememberWorkbenchScanReject(Number(expectedLot.stockOutLineId), failMsg); |
|
|
|
|
|
} |
|
|
|
|
|
startTransition(() => { |
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setQrScanErrorMsg(failMsg); |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
clearWorkbenchScanReject(Number(expectedLot.stockOutLineId)); |
|
|
|
|
|
const entity = res.entity as any; |
|
|
|
|
|
const nextStatus = String(entity?.status ?? "completed").toLowerCase(); |
|
|
|
|
|
const nextQty = entity?.qty != null ? Number(entity.qty) : undefined; |
|
|
|
|
|
|
|
|
|
|
|
startTransition(() => { |
|
|
|
|
|
setQrScanError(false); |
|
|
|
|
|
setQrScanSuccess(true); |
|
|
|
|
|
setCombinedLotData((prev) => |
|
|
|
|
|
prev.map((lot) => { |
|
|
|
|
|
if ( |
|
|
|
|
|
lot.stockOutLineId === expectedLot.stockOutLineId && |
|
|
|
|
|
lot.pickOrderLineId === expectedLot.pickOrderLineId |
|
|
|
|
|
) { |
|
|
|
|
|
return { |
|
|
|
|
|
...lot, |
|
|
|
|
|
lotNo: scannedLotNo, |
|
|
|
|
|
stockOutLineStatus: nextStatus, |
|
|
|
|
|
stockOutLineQty: nextQty ?? lot.stockOutLineQty, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
return lot; |
|
|
|
|
|
}), |
|
|
|
|
|
); |
|
|
|
|
|
setOriginalCombinedData((prev) => |
|
|
|
|
|
prev.map((lot) => { |
|
|
|
|
|
if ( |
|
|
|
|
|
lot.stockOutLineId === expectedLot.stockOutLineId && |
|
|
|
|
|
lot.pickOrderLineId === expectedLot.pickOrderLineId |
|
|
|
|
|
) { |
|
|
|
|
|
return { |
|
|
|
|
|
...lot, |
|
|
|
|
|
lotNo: scannedLotNo, |
|
|
|
|
|
stockOutLineStatus: nextStatus, |
|
|
|
|
|
stockOutLineQty: nextQty ?? lot.stockOutLineQty, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
return lot; |
|
|
|
|
|
}), |
|
|
|
|
|
); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const nextProcessedAuto = markProcessedStockOutLine( |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
expectedLot.stockOutLineId, |
|
|
|
|
|
); |
|
|
|
|
|
setProcessedQrCombinations(nextProcessedAuto); |
|
|
|
|
|
maybeReleaseQrForNextSol( |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
scannedStockInLineId, |
|
|
|
|
|
nextProcessedAuto, |
|
|
|
|
|
); |
|
|
|
|
|
if (workbenchMode) { |
|
|
|
|
|
await refreshWorkbenchAfterScanPick(); |
|
|
|
|
|
} |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const notSuggestedMsg = t( |
|
|
|
|
|
"Scanned lot is not the suggested lot. Please scan the suggested lot.", |
|
|
|
|
|
); |
|
|
|
|
|
console.warn( |
|
|
|
|
|
`⚠️ [QR PROCESS] Reject scanned lot ${scannedLot?.lotNo || scannedStockInLineId}: not a suggested lot`, |
|
|
|
|
|
); |
|
|
|
|
|
startTransition(() => { |
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setQrScanErrorMsg(notSuggestedMsg); |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (exactMatch) { |
|
|
if (exactMatch) { |
|
|
@@ -2384,213 +2017,6 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
return; // ✅ 直接返回,不需要确认表单 |
|
|
return; // ✅ 直接返回,不需要确认表单 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// ✅ Case 2: itemId 匹配但 stockInLineId 不匹配 |
|
|
|
|
|
// Workbench 策略:不彈窗,直接切換到掃到的批次並提交一次掃描 |
|
|
|
|
|
const mismatchCheckStartTime = performance.now(); |
|
|
|
|
|
if ( |
|
|
|
|
|
!canProcessWorkbenchScanPick( |
|
|
|
|
|
indexes, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
scannedStockInLineId, |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
) |
|
|
|
|
|
) { |
|
|
|
|
|
const mismatchCheckTime = performance.now() - mismatchCheckStartTime; |
|
|
|
|
|
const itemRows = indexes.byItemId.get(scannedItemId) ?? []; |
|
|
|
|
|
const errMsg = |
|
|
|
|
|
itemRows.length === 0 |
|
|
|
|
|
? t("QR code does not match any item in current orders.") |
|
|
|
|
|
: t( |
|
|
|
|
|
"No pending pick line left for this item. It may already be completed or fully processed.", |
|
|
|
|
|
); |
|
|
|
|
|
console.log( |
|
|
|
|
|
` [SKIP] No pending stock-out line left for mismatch path (check time: ${mismatchCheckTime.toFixed(2)}ms)`, |
|
|
|
|
|
); |
|
|
|
|
|
startTransition(() => { |
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setQrScanErrorMsg(errMsg); |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
const mismatchCheckTime = performance.now() - mismatchCheckStartTime; |
|
|
|
|
|
console.log(` [PERF] Mismatch check time: ${mismatchCheckTime.toFixed(2)}ms`); |
|
|
|
|
|
|
|
|
|
|
|
const expectedLotStartTime = performance.now(); |
|
|
|
|
|
const expectedLot = pickExpectedLotForSubstitution( |
|
|
|
|
|
activeSuggestedLots, |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
); |
|
|
|
|
|
if (!expectedLot) { |
|
|
|
|
|
console.error("Could not determine expected lot for auto-switch"); |
|
|
|
|
|
startTransition(() => { |
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
const expectedLotTime = performance.now() - expectedLotStartTime; |
|
|
|
|
|
console.log(` [PERF] Get expected lot time: ${expectedLotTime.toFixed(2)}ms`); |
|
|
|
|
|
|
|
|
|
|
|
console.log( |
|
|
|
|
|
`⚠️ Lot mismatch (auto): Expected stockInLineId=${expectedLot.stockInLineId}, Scanned stockInLineId=${scannedStockInLineId}`, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// 1) 先把掃到的 stockInLineId 轉成 lotNo(workbenchScanPick 需要 lotNo) |
|
|
|
|
|
let scannedLotNo: string | null = null; |
|
|
|
|
|
try { |
|
|
|
|
|
const info = await fetchStockInLineInfoCached(scannedStockInLineId); |
|
|
|
|
|
scannedLotNo = info?.lotNo || null; |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.warn("Failed to fetch lotNo for stockInLineId:", scannedStockInLineId, e); |
|
|
|
|
|
} |
|
|
|
|
|
if (!scannedLotNo) { |
|
|
|
|
|
const msg = t("Cannot resolve lot number from QR. Please rescan or use manual confirmation."); |
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setQrScanErrorMsg(msg); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 2) 非 workbench:先 confirmLotSubstitution;workbench 僅依 scan-pick 規則與錯誤訊息 |
|
|
|
|
|
if (!workbenchMode) { |
|
|
|
|
|
const substitutionResult = await confirmLotSubstitution({ |
|
|
|
|
|
pickOrderLineId: expectedLot.pickOrderLineId, |
|
|
|
|
|
stockOutLineId: expectedLot.stockOutLineId, |
|
|
|
|
|
originalSuggestedPickLotId: expectedLot.suggestedPickLotId, |
|
|
|
|
|
newInventoryLotNo: "", |
|
|
|
|
|
newStockInLineId: scannedStockInLineId, |
|
|
|
|
|
}); |
|
|
|
|
|
const substitutionCode = (substitutionResult as any)?.code; |
|
|
|
|
|
const switchedToUnavailable = |
|
|
|
|
|
substitutionCode === "SUCCESS_UNAVAILABLE" || substitutionCode === "BOUND_UNAVAILABLE"; |
|
|
|
|
|
if (!substitutionResult || (substitutionCode !== "SUCCESS" && !switchedToUnavailable)) { |
|
|
|
|
|
const errMsg = |
|
|
|
|
|
substitutionResult?.message || |
|
|
|
|
|
t("Lot switch failed; pick line was not updated."); |
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setQrScanErrorMsg(errMsg); |
|
|
|
|
|
if (expectedLot.stockOutLineId != null) { |
|
|
|
|
|
rememberWorkbenchScanReject(Number(expectedLot.stockOutLineId), errMsg); |
|
|
|
|
|
} |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 3) 提交掃描(workbench:直接 workbenchScanPick) |
|
|
|
|
|
try { |
|
|
|
|
|
const res = await workbenchScanPick({ |
|
|
|
|
|
stockOutLineId: expectedLot.stockOutLineId, |
|
|
|
|
|
lotNo: scannedLotNo, |
|
|
|
|
|
...(typeof scannedStockInLineId === "number" && |
|
|
|
|
|
Number.isFinite(scannedStockInLineId) && |
|
|
|
|
|
scannedStockInLineId > 0 |
|
|
|
|
|
? { stockInLineId: scannedStockInLineId } |
|
|
|
|
|
: {}), |
|
|
|
|
|
...workbenchScanPickQtyFromLot(expectedLot), |
|
|
|
|
|
storeId: fgPickOrders?.[0]?.storeId ?? null, |
|
|
|
|
|
userId: currentUserId ?? 1, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const ok = res.code === "SUCCESS"; |
|
|
|
|
|
|
|
|
|
|
|
if (!ok) { |
|
|
|
|
|
const failMsg = |
|
|
|
|
|
(res as { message?: string })?.message || |
|
|
|
|
|
t("Workbench scan-pick failed."); |
|
|
|
|
|
if ( |
|
|
|
|
|
shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && |
|
|
|
|
|
expectedLot |
|
|
|
|
|
) { |
|
|
|
|
|
openWorkbenchLotLabelModalForLot(expectedLot, failMsg, { |
|
|
|
|
|
markAsScanIssue: true, |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
if (workbenchMode && expectedLot.stockOutLineId != null) { |
|
|
|
|
|
rememberWorkbenchScanReject(Number(expectedLot.stockOutLineId), failMsg); |
|
|
|
|
|
} |
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setQrScanErrorMsg(failMsg); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
clearWorkbenchScanReject(Number(expectedLot.stockOutLineId)); |
|
|
|
|
|
const entity = res.entity as any; |
|
|
|
|
|
const nextStatus = String(entity?.status ?? "completed").toLowerCase(); |
|
|
|
|
|
const nextQty = entity?.qty != null ? Number(entity.qty) : undefined; |
|
|
|
|
|
|
|
|
|
|
|
startTransition(() => { |
|
|
|
|
|
setQrScanError(false); |
|
|
|
|
|
setQrScanSuccess(true); |
|
|
|
|
|
|
|
|
|
|
|
setCombinedLotData((prev) => |
|
|
|
|
|
prev.map((lot) => { |
|
|
|
|
|
if ( |
|
|
|
|
|
lot.stockOutLineId === expectedLot.stockOutLineId && |
|
|
|
|
|
lot.pickOrderLineId === expectedLot.pickOrderLineId |
|
|
|
|
|
) { |
|
|
|
|
|
return { |
|
|
|
|
|
...lot, |
|
|
|
|
|
lotNo: scannedLotNo, |
|
|
|
|
|
stockOutLineStatus: nextStatus, |
|
|
|
|
|
stockOutLineQty: nextQty ?? lot.stockOutLineQty, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
return lot; |
|
|
|
|
|
}), |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
setOriginalCombinedData((prev) => |
|
|
|
|
|
prev.map((lot) => { |
|
|
|
|
|
if ( |
|
|
|
|
|
lot.stockOutLineId === expectedLot.stockOutLineId && |
|
|
|
|
|
lot.pickOrderLineId === expectedLot.pickOrderLineId |
|
|
|
|
|
) { |
|
|
|
|
|
return { |
|
|
|
|
|
...lot, |
|
|
|
|
|
lotNo: scannedLotNo, |
|
|
|
|
|
stockOutLineStatus: nextStatus, |
|
|
|
|
|
stockOutLineQty: nextQty ?? lot.stockOutLineQty, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
return lot; |
|
|
|
|
|
}), |
|
|
|
|
|
); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const nextProcessedMismatch = markProcessedStockOutLine( |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
expectedLot.stockOutLineId, |
|
|
|
|
|
); |
|
|
|
|
|
setProcessedQrCombinations(nextProcessedMismatch); |
|
|
|
|
|
maybeReleaseQrForNextSol( |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
scannedStockInLineId, |
|
|
|
|
|
nextProcessedMismatch, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
if (workbenchMode) { |
|
|
|
|
|
await refreshWorkbenchAfterScanPick(); |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.error("Auto-switch scanPick failed:", e); |
|
|
|
|
|
setQrScanError(true); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const totalTime = performance.now() - totalStartTime; |
|
|
|
|
|
console.log( |
|
|
|
|
|
`✅ [PROCESS OUTSIDE QR AUTO-SWITCH] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`, |
|
|
|
|
|
); |
|
|
|
|
|
console.log(` End time: ${new Date().toISOString()}`); |
|
|
|
|
|
console.log( |
|
|
|
|
|
`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, mismatchCheck=${mismatchCheckTime.toFixed(2)}ms, expectedLot=${expectedLotTime.toFixed(2)}ms`, |
|
|
|
|
|
); |
|
|
|
|
|
} catch (error) { |
|
|
} catch (error) { |
|
|
const totalTime = performance.now() - totalStartTime; |
|
|
const totalTime = performance.now() - totalStartTime; |
|
|
console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`); |
|
|
console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`); |
|
|
@@ -2608,7 +2034,6 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
lotDataIndexes, |
|
|
lotDataIndexes, |
|
|
processedQrCombinations, |
|
|
processedQrCombinations, |
|
|
combinedLotData, |
|
|
combinedLotData, |
|
|
fetchStockInLineInfoCached, |
|
|
|
|
|
workbenchMode, |
|
|
workbenchMode, |
|
|
currentUserId, |
|
|
currentUserId, |
|
|
clearWorkbenchScanReject, |
|
|
clearWorkbenchScanReject, |
|
|
@@ -4346,23 +3771,6 @@ paginatedData.map((row, index) => { |
|
|
})()} |
|
|
})()} |
|
|
</Box> |
|
|
</Box> |
|
|
|
|
|
|
|
|
{/* Lot QR Code Button */} |
|
|
|
|
|
{Number(lot.stockOutLineId) > 0 && Number(lot.itemId) > 0 && ( |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
size="small" |
|
|
|
|
|
onClick={() => openWorkbenchLotLabelModalForLot(lot)} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
flexShrink: 0, |
|
|
|
|
|
fontSize: "0.75rem", |
|
|
|
|
|
minWidth: "auto", |
|
|
|
|
|
px: 1, |
|
|
|
|
|
whiteSpace: "nowrap", |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
{t("lot QR code")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
)} |
|
|
|
|
|
</Stack> |
|
|
</Stack> |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
|
|
|
|
|
|
|