|
|
@@ -81,20 +81,115 @@ interface Props { |
|
|
onWorkbenchHierarchyEmpty?: () => void; |
|
|
onWorkbenchHierarchyEmpty?: () => void; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** 同物料多行时,优先对「有建议批次号」的行做替换,避免误选「无批次/不足」行 */ |
|
|
|
|
|
function pickExpectedLotForSubstitution(activeSuggestedLots: any[]): any | null { |
|
|
|
|
|
if (!activeSuggestedLots?.length) return null; |
|
|
|
|
|
const withLotNo = activeSuggestedLots.filter( |
|
|
|
|
|
(l) => l.lotNo != null && String(l.lotNo).trim() !== "" |
|
|
|
|
|
|
|
|
type ProcessedStockOutLinesByItemId = Map<number, Set<number>>; |
|
|
|
|
|
|
|
|
|
|
|
function isLotRowPending(lot: any): boolean { |
|
|
|
|
|
const st = String(lot?.stockOutLineStatus ?? "").toLowerCase(); |
|
|
|
|
|
return ( |
|
|
|
|
|
st === "pending" || |
|
|
|
|
|
st === "partially_completed" || |
|
|
|
|
|
st === "partially_complete" || |
|
|
|
|
|
st === "" |
|
|
); |
|
|
); |
|
|
if (withLotNo.length === 1) return withLotNo[0]; |
|
|
|
|
|
if (withLotNo.length > 1) { |
|
|
|
|
|
const pending = withLotNo.find( |
|
|
|
|
|
(l) => (l.stockOutLineStatus || "").toLowerCase() === "pending" |
|
|
|
|
|
); |
|
|
|
|
|
return pending || withLotNo[0]; |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function isStockOutLineAlreadyProcessed( |
|
|
|
|
|
processedByItemId: ProcessedStockOutLinesByItemId, |
|
|
|
|
|
itemId: number, |
|
|
|
|
|
stockOutLineId: number | null | undefined, |
|
|
|
|
|
): boolean { |
|
|
|
|
|
const solId = Number(stockOutLineId); |
|
|
|
|
|
if (!Number.isFinite(solId) || solId <= 0) return false; |
|
|
|
|
|
return processedByItemId.get(itemId)?.has(solId) ?? false; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function markProcessedStockOutLine( |
|
|
|
|
|
prev: ProcessedStockOutLinesByItemId, |
|
|
|
|
|
itemId: number, |
|
|
|
|
|
stockOutLineId: number | null | undefined, |
|
|
|
|
|
): ProcessedStockOutLinesByItemId { |
|
|
|
|
|
const solId = Number(stockOutLineId); |
|
|
|
|
|
if (!Number.isFinite(solId) || solId <= 0) return prev; |
|
|
|
|
|
const newMap = new Map(prev); |
|
|
|
|
|
if (!newMap.has(itemId)) newMap.set(itemId, new Set()); |
|
|
|
|
|
newMap.get(itemId)!.add(solId); |
|
|
|
|
|
return newMap; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function parseWorkbenchQrPayload( |
|
|
|
|
|
latestQr: string, |
|
|
|
|
|
): { itemId: number; stockInLineId: number } | null { |
|
|
|
|
|
try { |
|
|
|
|
|
const qrData = JSON.parse(latestQr); |
|
|
|
|
|
const itemId = Number(qrData?.itemId); |
|
|
|
|
|
const stockInLineId = Number(qrData?.stockInLineId); |
|
|
|
|
|
if (!Number.isFinite(itemId) || !Number.isFinite(stockInLineId)) return null; |
|
|
|
|
|
return { itemId, stockInLineId }; |
|
|
|
|
|
} catch { |
|
|
|
|
|
return null; |
|
|
} |
|
|
} |
|
|
return activeSuggestedLots[0]; |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function hasPendingActiveRowForStockInLine( |
|
|
|
|
|
indexes: { |
|
|
|
|
|
byStockInLineId: Map<number, any[]>; |
|
|
|
|
|
activeLotsByItemId: Map<number, any[]>; |
|
|
|
|
|
}, |
|
|
|
|
|
itemId: number, |
|
|
|
|
|
stockInLineId: number, |
|
|
|
|
|
processedByItemId: ProcessedStockOutLinesByItemId, |
|
|
|
|
|
): boolean { |
|
|
|
|
|
const rows = indexes.byStockInLineId.get(stockInLineId) ?? []; |
|
|
|
|
|
const activeSet = new Set(indexes.activeLotsByItemId.get(itemId) ?? []); |
|
|
|
|
|
return rows.some( |
|
|
|
|
|
(lot) => |
|
|
|
|
|
lot.itemId === itemId && |
|
|
|
|
|
activeSet.has(lot) && |
|
|
|
|
|
isLotRowPending(lot) && |
|
|
|
|
|
!isStockOutLineAlreadyProcessed(processedByItemId, itemId, lot.stockOutLineId), |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function findExactActiveMatchForStockInLine( |
|
|
|
|
|
stockInLineLots: any[], |
|
|
|
|
|
scannedItemId: number, |
|
|
|
|
|
activeSuggestedLots: any[], |
|
|
|
|
|
processedByItemId: ProcessedStockOutLinesByItemId, |
|
|
|
|
|
): any | null { |
|
|
|
|
|
const activeSet = new Set(activeSuggestedLots); |
|
|
|
|
|
const candidates = stockInLineLots.filter( |
|
|
|
|
|
(lot) => |
|
|
|
|
|
lot.itemId === scannedItemId && |
|
|
|
|
|
activeSet.has(lot) && |
|
|
|
|
|
!isStockOutLineAlreadyProcessed( |
|
|
|
|
|
processedByItemId, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
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() !== ""); |
|
|
|
|
|
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<{ |
|
|
@@ -580,8 +675,9 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); |
|
|
|
|
|
|
|
|
// Add these missing state variables after line 352 |
|
|
// Add these missing state variables after line 352 |
|
|
const [isManualScanning, setIsManualScanning] = useState<boolean>(false); |
|
|
const [isManualScanning, setIsManualScanning] = 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()); |
|
|
|
|
|
|
|
|
// Track processed stock-out lines per item (allow same physical lot QR for next SOL) |
|
|
|
|
|
const [processedQrCombinations, setProcessedQrCombinations] = |
|
|
|
|
|
useState<ProcessedStockOutLinesByItemId>(new Map()); |
|
|
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); |
|
|
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); |
|
|
const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); |
|
|
const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); |
|
|
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); |
|
|
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); |
|
|
@@ -596,6 +692,7 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); |
|
|
// 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 |
|
|
const processedQrCodesRef = useRef<Set<string>>(new Set()); |
|
|
const processedQrCodesRef = useRef<Set<string>>(new Set()); |
|
|
const lastProcessedQrRef = useRef<string>(''); |
|
|
const lastProcessedQrRef = useRef<string>(''); |
|
|
|
|
|
const qrPickInFlightRef = useRef(false); |
|
|
|
|
|
|
|
|
// Store callbacks in refs to avoid useEffect dependency issues |
|
|
// Store callbacks in refs to avoid useEffect dependency issues |
|
|
const processOutsideQrCodeRef = useRef< |
|
|
const processOutsideQrCodeRef = useRef< |
|
|
@@ -847,21 +944,25 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
: mergedPickOrder.pickOrderLines || []; |
|
|
: mergedPickOrder.pickOrderLines || []; |
|
|
|
|
|
|
|
|
pickOrderLinesForDisplay.forEach((line: any) => { |
|
|
pickOrderLinesForDisplay.forEach((line: any) => { |
|
|
// 用来记录这一行已经通过 lots 出现过的 lotId |
|
|
|
|
|
|
|
|
// 用来记录这一行已经通过 lots 出现过的 lotId / stockOutLineId |
|
|
const lotIdSet = new Set<number>(); |
|
|
const lotIdSet = new Set<number>(); |
|
|
|
|
|
const stockOutLineIdSet = new Set<number>(); |
|
|
|
|
|
|
|
|
// ✅ lots:按 lotId 去重并合并 requiredQty |
|
|
|
|
|
|
|
|
// ✅ lots:按 SOL 优先去重(其次 lotId),保持 requiredQty 原值,不在前端累计 |
|
|
if (line.lots && line.lots.length > 0) { |
|
|
if (line.lots && line.lots.length > 0) { |
|
|
const lotMap = new Map<number, any>(); |
|
|
|
|
|
|
|
|
const lotMap = new Map<string, any>(); |
|
|
|
|
|
|
|
|
line.lots.forEach((lot: any) => { |
|
|
|
|
|
|
|
|
line.lots.forEach((lot: any, lotIdx: number) => { |
|
|
const lotId = lot.id; |
|
|
const lotId = lot.id; |
|
|
if (lotMap.has(lotId)) { |
|
|
|
|
|
const existingLot = lotMap.get(lotId); |
|
|
|
|
|
existingLot.requiredQty = |
|
|
|
|
|
(existingLot.requiredQty || 0) + (lot.requiredQty || 0); |
|
|
|
|
|
} else { |
|
|
|
|
|
lotMap.set(lotId, { ...lot }); |
|
|
|
|
|
|
|
|
const solId = Number(lot?.stockOutLineId); |
|
|
|
|
|
const lotKey = |
|
|
|
|
|
Number.isFinite(solId) && solId > 0 |
|
|
|
|
|
? `sol:${solId}` |
|
|
|
|
|
: lotId != null |
|
|
|
|
|
? `lot:${lotId}` |
|
|
|
|
|
: `idx:${lotIdx}`; |
|
|
|
|
|
if (!lotMap.has(lotKey)) { |
|
|
|
|
|
lotMap.set(lotKey, { ...lot }); |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
@@ -869,6 +970,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
if (lot.id != null) { |
|
|
if (lot.id != null) { |
|
|
lotIdSet.add(lot.id); |
|
|
lotIdSet.add(lot.id); |
|
|
} |
|
|
} |
|
|
|
|
|
const solId = Number(lot?.stockOutLineId); |
|
|
|
|
|
if (Number.isFinite(solId) && solId > 0) { |
|
|
|
|
|
stockOutLineIdSet.add(solId); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
flatLotData.push({ |
|
|
flatLotData.push({ |
|
|
pickOrderConsoCode: Array.isArray(mergedPickOrder.consoCodes) |
|
|
pickOrderConsoCode: Array.isArray(mergedPickOrder.consoCodes) |
|
|
@@ -921,20 +1026,19 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
if (line.stockouts && line.stockouts.length > 0) { |
|
|
if (line.stockouts && line.stockouts.length > 0) { |
|
|
line.stockouts.forEach((stockout: any) => { |
|
|
line.stockouts.forEach((stockout: any) => { |
|
|
const hasLot = stockout.lotId != null; |
|
|
const hasLot = stockout.lotId != null; |
|
|
|
|
|
const hasSolId = stockout.id != null; |
|
|
const lotAlreadyInLots = |
|
|
const lotAlreadyInLots = |
|
|
hasLot && lotIdSet.has(stockout.lotId as number); |
|
|
hasLot && lotIdSet.has(stockout.lotId as number); |
|
|
|
|
|
const solAlreadyInLots = |
|
|
|
|
|
hasSolId && stockOutLineIdSet.has(Number(stockout.id)); |
|
|
|
|
|
|
|
|
// 有批次 & 已经通过 lots 渲染过 → 跳过,避免一条变两行 |
|
|
// 有批次 & 已经通过 lots 渲染过 → 跳过,避免一条变两行 |
|
|
if (!stockout.noLot && lotAlreadyInLots) { |
|
|
|
|
|
|
|
|
if (!stockout.noLot && (solAlreadyInLots || lotAlreadyInLots)) { |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const stockoutRequiredQty = Number( |
|
|
|
|
|
stockout?.requiredQty ?? stockout?.suggestedPickLotQty, |
|
|
|
|
|
); |
|
|
|
|
|
const effectiveStockoutRequiredQty = Number.isFinite(stockoutRequiredQty) |
|
|
|
|
|
? stockoutRequiredQty |
|
|
|
|
|
: Number(line.requiredQty) || 0; |
|
|
|
|
|
|
|
|
const effectiveStockoutRequiredQty = |
|
|
|
|
|
stockout?.requiredQty ?? stockout?.suggestedPickLotQty ?? null; |
|
|
const fallbackRouteFromLine = |
|
|
const fallbackRouteFromLine = |
|
|
line?.lots?.[0]?.router?.route ?? line?.lots?.[0]?.location ?? null; |
|
|
line?.lots?.[0]?.router?.route ?? line?.lots?.[0]?.location ?? null; |
|
|
|
|
|
|
|
|
@@ -1399,16 +1503,33 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
resetScanRef.current = resetScan; |
|
|
resetScanRef.current = resetScan; |
|
|
|
|
|
|
|
|
const processOutsideQrCode = useCallback(async (latestQr: string, qrScanCountAtInvoke?: number) => { |
|
|
const processOutsideQrCode = useCallback(async (latestQr: string, qrScanCountAtInvoke?: number) => { |
|
|
|
|
|
if (qrPickInFlightRef.current) { |
|
|
|
|
|
console.log(" [SKIP] QR pick already in flight"); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
qrPickInFlightRef.current = true; |
|
|
|
|
|
|
|
|
const totalStartTime = performance.now(); |
|
|
const totalStartTime = performance.now(); |
|
|
console.log(` [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`); |
|
|
console.log(` [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`); |
|
|
console.log(` Start time: ${new Date().toISOString()}`); |
|
|
console.log(` Start time: ${new Date().toISOString()}`); |
|
|
|
|
|
|
|
|
// ✅ Measure index access time |
|
|
|
|
|
|
|
|
|
|
|
const indexAccessStart = performance.now(); |
|
|
const indexAccessStart = performance.now(); |
|
|
const indexes = lotDataIndexes; // Access the memoized indexes |
|
|
|
|
|
|
|
|
const indexes = lotDataIndexes; |
|
|
const indexAccessTime = performance.now() - indexAccessStart; |
|
|
const indexAccessTime = performance.now() - indexAccessStart; |
|
|
console.log(` [PERF] Index access time: ${indexAccessTime.toFixed(2)}ms`); |
|
|
console.log(` [PERF] Index access time: ${indexAccessTime.toFixed(2)}ms`); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const maybeReleaseQrForNextSol = ( |
|
|
|
|
|
itemId: number, |
|
|
|
|
|
stockInLineId: number, |
|
|
|
|
|
processedAfterMark: ProcessedStockOutLinesByItemId, |
|
|
|
|
|
) => { |
|
|
|
|
|
if (hasPendingActiveRowForStockInLine(indexes, itemId, stockInLineId, processedAfterMark)) { |
|
|
|
|
|
lastProcessedQrRef.current = ""; |
|
|
|
|
|
processedQrCodesRef.current.delete(latestQr); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
// 1) Parse JSON safely (parse once, reuse) |
|
|
// 1) Parse JSON safely (parse once, reuse) |
|
|
const parseStartTime = performance.now(); |
|
|
const parseStartTime = performance.now(); |
|
|
let qrData: any = null; |
|
|
let qrData: any = null; |
|
|
@@ -1441,17 +1562,20 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
|
|
|
|
|
|
const scannedItemId = qrData.itemId; |
|
|
const scannedItemId = qrData.itemId; |
|
|
const scannedStockInLineId = qrData.stockInLineId; |
|
|
const scannedStockInLineId = qrData.stockInLineId; |
|
|
|
|
|
|
|
|
// ✅ Check if this combination was already processed |
|
|
|
|
|
const duplicateCheckStartTime = performance.now(); |
|
|
|
|
|
const itemProcessedSet = processedQrCombinations.get(scannedItemId); |
|
|
|
|
|
if (itemProcessedSet?.has(scannedStockInLineId)) { |
|
|
|
|
|
const duplicateCheckTime = performance.now() - duplicateCheckStartTime; |
|
|
|
|
|
console.log(` [SKIP] Already processed combination: itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId} (check time: ${duplicateCheckTime.toFixed(2)}ms)`); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
!hasPendingActiveRowForStockInLine( |
|
|
|
|
|
indexes, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
scannedStockInLineId, |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
) |
|
|
|
|
|
) { |
|
|
|
|
|
console.log( |
|
|
|
|
|
` [SKIP] No pending stock-out line left for itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId}`, |
|
|
|
|
|
); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
const duplicateCheckTime = performance.now() - duplicateCheckStartTime; |
|
|
|
|
|
console.log(` [PERF] Duplicate check time: ${duplicateCheckTime.toFixed(2)}ms`); |
|
|
|
|
|
|
|
|
|
|
|
// ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed) |
|
|
// ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed) |
|
|
const lookupStartTime = performance.now(); |
|
|
const lookupStartTime = performance.now(); |
|
|
@@ -1463,9 +1587,15 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
|
|
|
|
|
|
// ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots |
|
|
// ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots |
|
|
// This allows users to scan other lots even when all suggested lots are rejected |
|
|
// This allows users to scan other lots even when all suggested lots are rejected |
|
|
const scannedLot = allLotsForItem.find( |
|
|
|
|
|
(lot: any) => lot.stockInLineId === scannedStockInLineId |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
const stockInLineRows = indexes.byStockInLineId.get(scannedStockInLineId) ?? []; |
|
|
|
|
|
const scannedLot = |
|
|
|
|
|
findExactActiveMatchForStockInLine( |
|
|
|
|
|
stockInLineRows, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
activeSuggestedLots, |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
) ?? |
|
|
|
|
|
allLotsForItem.find((lot: any) => lot.stockInLineId === scannedStockInLineId); |
|
|
|
|
|
|
|
|
if (scannedLot) { |
|
|
if (scannedLot) { |
|
|
const isRejected = |
|
|
const isRejected = |
|
|
@@ -1482,13 +1612,14 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
`此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。` |
|
|
`此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。` |
|
|
); |
|
|
); |
|
|
}); |
|
|
}); |
|
|
// 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; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
if (scannedLot?.stockOutLineId != null) { |
|
|
|
|
|
const nextProcessed = markProcessedStockOutLine( |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
scannedLot.stockOutLineId, |
|
|
|
|
|
); |
|
|
|
|
|
setProcessedQrCombinations(nextProcessed); |
|
|
|
|
|
} |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -1675,12 +1806,13 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
); |
|
|
); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
setProcessedQrCombinations((prev) => { |
|
|
|
|
|
const newMap = new Map(prev); |
|
|
|
|
|
if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); |
|
|
|
|
|
newMap.get(scannedItemId)!.add(scannedStockInLineId); |
|
|
|
|
|
return newMap; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
const nextProcessedNoActive = markProcessedStockOutLine( |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
expectedLot.stockOutLineId, |
|
|
|
|
|
); |
|
|
|
|
|
setProcessedQrCombinations(nextProcessedNoActive); |
|
|
|
|
|
maybeReleaseQrForNextSol(scannedItemId, scannedStockInLineId, nextProcessedNoActive); |
|
|
|
|
|
|
|
|
if (workbenchMode) { |
|
|
if (workbenchMode) { |
|
|
await refreshWorkbenchAfterScanPick(); |
|
|
await refreshWorkbenchAfterScanPick(); |
|
|
@@ -1691,16 +1823,13 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
|
|
|
|
|
|
// ✅ OPTIMIZATION: Direct Map lookup for stockInLineId match (O(1)) |
|
|
// ✅ OPTIMIZATION: Direct Map lookup for stockInLineId match (O(1)) |
|
|
const matchStartTime = performance.now(); |
|
|
const matchStartTime = performance.now(); |
|
|
let exactMatch: any = null; |
|
|
|
|
|
const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || []; |
|
|
const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || []; |
|
|
// Find exact match from stockInLineId index, then verify it's in active lots |
|
|
|
|
|
for (let i = 0; i < stockInLineLots.length; i++) { |
|
|
|
|
|
const lot = stockInLineLots[i]; |
|
|
|
|
|
if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) { |
|
|
|
|
|
exactMatch = lot; |
|
|
|
|
|
break; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const exactMatch = findExactActiveMatchForStockInLine( |
|
|
|
|
|
stockInLineLots, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
activeSuggestedLots, |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
); |
|
|
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'}`); |
|
|
|
|
|
|
|
|
@@ -1709,7 +1838,8 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
// Also handle case where scanned lot is not in allLotsForItem (scannedLot is undefined) |
|
|
// Also handle case where scanned lot is not in allLotsForItem (scannedLot is undefined) |
|
|
if (!exactMatch) { |
|
|
if (!exactMatch) { |
|
|
const expectedLot = |
|
|
const expectedLot = |
|
|
pickExpectedLotForSubstitution(activeSuggestedLots) || allLotsForItem[0]; |
|
|
|
|
|
|
|
|
pickExpectedLotForSubstitution(activeSuggestedLots, processedQrCombinations) || |
|
|
|
|
|
allLotsForItem[0]; |
|
|
if (expectedLot) { |
|
|
if (expectedLot) { |
|
|
const shouldAutoSwitch = |
|
|
const shouldAutoSwitch = |
|
|
!scannedLot || (scannedLot.stockInLineId !== expectedLot.stockInLineId); |
|
|
!scannedLot || (scannedLot.stockInLineId !== expectedLot.stockInLineId); |
|
|
@@ -1842,12 +1972,13 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
); |
|
|
); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
setProcessedQrCombinations((prev) => { |
|
|
|
|
|
const newMap = new Map(prev); |
|
|
|
|
|
if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); |
|
|
|
|
|
newMap.get(scannedItemId)!.add(scannedStockInLineId); |
|
|
|
|
|
return newMap; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
const nextProcessedAuto = markProcessedStockOutLine( |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
expectedLot.stockOutLineId, |
|
|
|
|
|
); |
|
|
|
|
|
setProcessedQrCombinations(nextProcessedAuto); |
|
|
|
|
|
maybeReleaseQrForNextSol(scannedItemId, scannedStockInLineId, nextProcessedAuto); |
|
|
if (workbenchMode) { |
|
|
if (workbenchMode) { |
|
|
await refreshWorkbenchAfterScanPick(); |
|
|
await refreshWorkbenchAfterScanPick(); |
|
|
} |
|
|
} |
|
|
@@ -1940,14 +2071,13 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
|
|
|
|
|
|
// Mark this combination as processed |
|
|
// Mark this combination as processed |
|
|
const markProcessedStartTime = performance.now(); |
|
|
const markProcessedStartTime = performance.now(); |
|
|
setProcessedQrCombinations(prev => { |
|
|
|
|
|
const newMap = new Map(prev); |
|
|
|
|
|
if (!newMap.has(scannedItemId)) { |
|
|
|
|
|
newMap.set(scannedItemId, new Set()); |
|
|
|
|
|
} |
|
|
|
|
|
newMap.get(scannedItemId)!.add(scannedStockInLineId); |
|
|
|
|
|
return newMap; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
const nextProcessedExact = markProcessedStockOutLine( |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
exactMatch.stockOutLineId, |
|
|
|
|
|
); |
|
|
|
|
|
setProcessedQrCombinations(nextProcessedExact); |
|
|
|
|
|
maybeReleaseQrForNextSol(scannedItemId, scannedStockInLineId, nextProcessedExact); |
|
|
const markProcessedTime = performance.now() - markProcessedStartTime; |
|
|
const markProcessedTime = performance.now() - markProcessedStartTime; |
|
|
console.log(` [PERF] Mark processed time: ${markProcessedTime.toFixed(2)}ms`); |
|
|
console.log(` [PERF] Mark processed time: ${markProcessedTime.toFixed(2)}ms`); |
|
|
|
|
|
|
|
|
@@ -1958,7 +2088,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
const totalTime = performance.now() - totalStartTime; |
|
|
const totalTime = performance.now() - totalStartTime; |
|
|
console.log(`✅ [PROCESS OUTSIDE QR END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); |
|
|
console.log(`✅ [PROCESS OUTSIDE QR END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); |
|
|
console.log(` End time: ${new Date().toISOString()}`); |
|
|
console.log(` End time: ${new Date().toISOString()}`); |
|
|
console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, api=${apiTime.toFixed(2)}ms, stateUpdate=${stateUpdateTime.toFixed(2)}ms, markProcessed=${markProcessedTime.toFixed(2)}ms`); |
|
|
|
|
|
|
|
|
console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, api=${apiTime.toFixed(2)}ms, stateUpdate=${stateUpdateTime.toFixed(2)}ms, markProcessed=${markProcessedTime.toFixed(2)}ms`); |
|
|
console.log( |
|
|
console.log( |
|
|
workbenchMode |
|
|
workbenchMode |
|
|
? "✅ Workbench scan-pick: list refreshed from server" |
|
|
? "✅ Workbench scan-pick: list refreshed from server" |
|
|
@@ -2000,20 +2130,11 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
|
|
|
|
|
|
// ✅ Case 2: itemId 匹配但 stockInLineId 不匹配 |
|
|
// ✅ Case 2: itemId 匹配但 stockInLineId 不匹配 |
|
|
// Workbench 策略:不彈窗,直接切換到掃到的批次並提交一次掃描 |
|
|
// Workbench 策略:不彈窗,直接切換到掃到的批次並提交一次掃描 |
|
|
const mismatchCheckStartTime = performance.now(); |
|
|
|
|
|
const itemProcessedSet2 = processedQrCombinations.get(scannedItemId); |
|
|
|
|
|
if (itemProcessedSet2?.has(scannedStockInLineId)) { |
|
|
|
|
|
const mismatchCheckTime = performance.now() - mismatchCheckStartTime; |
|
|
|
|
|
console.log( |
|
|
|
|
|
` [SKIP] Already processed this exact combination (check time: ${mismatchCheckTime.toFixed(2)}ms)`, |
|
|
|
|
|
); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
const mismatchCheckTime = performance.now() - mismatchCheckStartTime; |
|
|
|
|
|
console.log(` [PERF] Mismatch check time: ${mismatchCheckTime.toFixed(2)}ms`); |
|
|
|
|
|
|
|
|
|
|
|
const expectedLotStartTime = performance.now(); |
|
|
const expectedLotStartTime = performance.now(); |
|
|
const expectedLot = pickExpectedLotForSubstitution(activeSuggestedLots); |
|
|
|
|
|
|
|
|
const expectedLot = pickExpectedLotForSubstitution( |
|
|
|
|
|
activeSuggestedLots, |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
); |
|
|
if (!expectedLot) { |
|
|
if (!expectedLot) { |
|
|
console.error("Could not determine expected lot for auto-switch"); |
|
|
console.error("Could not determine expected lot for auto-switch"); |
|
|
startTransition(() => { |
|
|
startTransition(() => { |
|
|
@@ -2152,12 +2273,13 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
); |
|
|
); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
setProcessedQrCombinations((prev) => { |
|
|
|
|
|
const newMap = new Map(prev); |
|
|
|
|
|
if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); |
|
|
|
|
|
newMap.get(scannedItemId)!.add(scannedStockInLineId); |
|
|
|
|
|
return newMap; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
const nextProcessedMismatch = markProcessedStockOutLine( |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
scannedItemId, |
|
|
|
|
|
expectedLot.stockOutLineId, |
|
|
|
|
|
); |
|
|
|
|
|
setProcessedQrCombinations(nextProcessedMismatch); |
|
|
|
|
|
maybeReleaseQrForNextSol(scannedItemId, scannedStockInLineId, nextProcessedMismatch); |
|
|
|
|
|
|
|
|
if (workbenchMode) { |
|
|
if (workbenchMode) { |
|
|
await refreshWorkbenchAfterScanPick(); |
|
|
await refreshWorkbenchAfterScanPick(); |
|
|
@@ -2175,7 +2297,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
); |
|
|
); |
|
|
console.log(` End time: ${new Date().toISOString()}`); |
|
|
console.log(` End time: ${new Date().toISOString()}`); |
|
|
console.log( |
|
|
console.log( |
|
|
`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, mismatchCheck=${mismatchCheckTime.toFixed(2)}ms, expectedLot=${expectedLotTime.toFixed(2)}ms`, |
|
|
|
|
|
|
|
|
`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, expectedLot=${expectedLotTime.toFixed(2)}ms`, |
|
|
); |
|
|
); |
|
|
} catch (error) { |
|
|
} catch (error) { |
|
|
const totalTime = performance.now() - totalStartTime; |
|
|
const totalTime = performance.now() - totalStartTime; |
|
|
@@ -2187,6 +2309,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
}); |
|
|
}); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
} finally { |
|
|
|
|
|
qrPickInFlightRef.current = false; |
|
|
|
|
|
} |
|
|
}, [ |
|
|
}, [ |
|
|
lotDataIndexes, |
|
|
lotDataIndexes, |
|
|
processedQrCombinations, |
|
|
processedQrCombinations, |
|
|
@@ -2310,13 +2435,32 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
console.log(` [QR DETECTION] Detection time: ${new Date().toISOString()}`); |
|
|
console.log(` [QR DETECTION] Detection time: ${new Date().toISOString()}`); |
|
|
console.log(` [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`); |
|
|
console.log(` [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`); |
|
|
|
|
|
|
|
|
// Skip if already processed (use refs to avoid dependency issues and delays) |
|
|
|
|
|
|
|
|
const qrPayload = parseWorkbenchQrPayload(latestQr); |
|
|
|
|
|
const canRetrySamePhysicalLot = |
|
|
|
|
|
qrPayload != null && |
|
|
|
|
|
hasPendingActiveRowForStockInLine( |
|
|
|
|
|
lotDataIndexes, |
|
|
|
|
|
qrPayload.itemId, |
|
|
|
|
|
qrPayload.stockInLineId, |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// Skip if already processed (allow same QR when another pending SOL needs this lot) |
|
|
const checkProcessedStartTime = performance.now(); |
|
|
const checkProcessedStartTime = performance.now(); |
|
|
if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) { |
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
(processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) && |
|
|
|
|
|
!canRetrySamePhysicalLot |
|
|
|
|
|
) { |
|
|
const checkTime = performance.now() - checkProcessedStartTime; |
|
|
const checkTime = performance.now() - checkProcessedStartTime; |
|
|
console.log(` [QR PROCESS] Already processed check time: ${checkTime.toFixed(2)}ms`); |
|
|
console.log(` [QR PROCESS] Already processed check time: ${checkTime.toFixed(2)}ms`); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
if (canRetrySamePhysicalLot) { |
|
|
|
|
|
processedQrCodesRef.current.delete(latestQr); |
|
|
|
|
|
if (lastProcessedQrRef.current === latestQr) { |
|
|
|
|
|
lastProcessedQrRef.current = ""; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
const checkTime = performance.now() - checkProcessedStartTime; |
|
|
const checkTime = performance.now() - checkProcessedStartTime; |
|
|
console.log(` [QR PROCESS] Not processed check time: ${checkTime.toFixed(2)}ms`); |
|
|
console.log(` [QR PROCESS] Not processed check time: ${checkTime.toFixed(2)}ms`); |
|
|
|
|
|
|
|
|
@@ -2351,8 +2495,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Process new QR code immediately (background mode - no modal) |
|
|
// Process new QR code immediately (background mode - no modal) |
|
|
// Check against refs to avoid state update delays |
|
|
|
|
|
if (latestQr && latestQr !== lastProcessedQrRef.current) { |
|
|
|
|
|
|
|
|
if (latestQr && (latestQr !== lastProcessedQrRef.current || canRetrySamePhysicalLot)) { |
|
|
const processingStartTime = performance.now(); |
|
|
const processingStartTime = performance.now(); |
|
|
console.log(` [QR PROCESS] Starting processing at: ${new Date().toISOString()}`); |
|
|
console.log(` [QR PROCESS] Starting processing at: ${new Date().toISOString()}`); |
|
|
console.log(` [QR PROCESS] Time since detection: ${(processingStartTime - qrDetectionStartTime).toFixed(2)}ms`); |
|
|
console.log(` [QR PROCESS] Time since detection: ${(processingStartTime - qrDetectionStartTime).toFixed(2)}ms`); |
|
|
@@ -2416,7 +2559,15 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO |
|
|
qrProcessingTimeoutRef.current = null; |
|
|
qrProcessingTimeoutRef.current = null; |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
}, [qrValues, isManualScanning, isRefreshingData, combinedLotData.length, manualLotConfirmationOpen]); |
|
|
|
|
|
|
|
|
}, [ |
|
|
|
|
|
qrValues, |
|
|
|
|
|
isManualScanning, |
|
|
|
|
|
isRefreshingData, |
|
|
|
|
|
combinedLotData.length, |
|
|
|
|
|
manualLotConfirmationOpen, |
|
|
|
|
|
lotDataIndexes, |
|
|
|
|
|
processedQrCombinations, |
|
|
|
|
|
]); |
|
|
const renderCountRef = useRef(0); |
|
|
const renderCountRef = useRef(0); |
|
|
const renderStartTimeRef = useRef<number | null>(null); |
|
|
const renderStartTimeRef = useRef<number | null>(null); |
|
|
|
|
|
|
|
|
@@ -2642,7 +2793,9 @@ useEffect(() => { |
|
|
type RowMeta = { |
|
|
type RowMeta = { |
|
|
lot: any; |
|
|
lot: any; |
|
|
isGroupFirst: boolean; |
|
|
isGroupFirst: boolean; |
|
|
|
|
|
isGroupLast: boolean; |
|
|
groupDisplayIndex: number; |
|
|
groupDisplayIndex: number; |
|
|
|
|
|
isMultiLotGroup: boolean; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const isCompletedStatus = (lot: any) => { |
|
|
const isCompletedStatus = (lot: any) => { |
|
|
@@ -2676,15 +2829,14 @@ useEffect(() => { |
|
|
{ firstIndex: number; items: { lot: any; originalIndex: number }[] } |
|
|
{ firstIndex: number; items: { lot: any; originalIndex: number }[] } |
|
|
>(); |
|
|
>(); |
|
|
combinedLotData.forEach((lot: any, originalIndex: number) => { |
|
|
combinedLotData.forEach((lot: any, originalIndex: number) => { |
|
|
const routeKey = String(lot?.routerRoute ?? "").trim(); |
|
|
|
|
|
const pickOrderLineKey = |
|
|
const pickOrderLineKey = |
|
|
lot?.pickOrderLineId != null ? `pol:${String(lot.pickOrderLineId)}` : "pol:unknown"; |
|
|
lot?.pickOrderLineId != null ? `pol:${String(lot.pickOrderLineId)}` : "pol:unknown"; |
|
|
const itemKey = |
|
|
const itemKey = |
|
|
lot?.itemId != null |
|
|
lot?.itemId != null |
|
|
? `itemId:${String(lot.itemId)}` |
|
|
? `itemId:${String(lot.itemId)}` |
|
|
: `itemCode:${String(lot?.itemCode ?? "").trim()}`; |
|
|
: `itemCode:${String(lot?.itemCode ?? "").trim()}`; |
|
|
// Group by pickOrderLine first so no-lot row stays with its lot rows even when route is empty. |
|
|
|
|
|
const key = `${pickOrderLineKey}__${itemKey}__${routeKey}`; |
|
|
|
|
|
|
|
|
// Group by pick order line + item so split lots (different routes) share one display index. |
|
|
|
|
|
const key = `${pickOrderLineKey}__${itemKey}`; |
|
|
const g = groups.get(key); |
|
|
const g = groups.get(key); |
|
|
if (!g) { |
|
|
if (!g) { |
|
|
groups.set(key, { firstIndex: originalIndex, items: [{ lot, originalIndex }] }); |
|
|
groups.set(key, { firstIndex: originalIndex, items: [{ lot, originalIndex }] }); |
|
|
@@ -2712,7 +2864,9 @@ useEffect(() => { |
|
|
flattened.push({ |
|
|
flattened.push({ |
|
|
lot: it.lot, |
|
|
lot: it.lot, |
|
|
isGroupFirst: idx === 0, |
|
|
isGroupFirst: idx === 0, |
|
|
|
|
|
isGroupLast: idx === sortedWithin.length - 1, |
|
|
groupDisplayIndex, |
|
|
groupDisplayIndex, |
|
|
|
|
|
isMultiLotGroup: sortedWithin.length > 1, |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
@@ -3331,12 +3485,12 @@ const handleSubmitAllScanned = useCallback(async () => { |
|
|
// 添加调试日志 |
|
|
// 添加调试日志 |
|
|
const noLotCount = filtered.filter(l => l.noLot === true).length; |
|
|
const noLotCount = filtered.filter(l => l.noLot === true).length; |
|
|
const normalCount = filtered.filter(l => l.noLot !== true).length; |
|
|
const normalCount = filtered.filter(l => l.noLot !== true).length; |
|
|
console.log(`📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`); |
|
|
|
|
|
console.log(`📊 All items breakdown:`, { |
|
|
|
|
|
total: combinedLotData.length, |
|
|
|
|
|
noLot: combinedLotData.filter(l => l.noLot === true).length, |
|
|
|
|
|
normal: combinedLotData.filter(l => l.noLot !== true).length |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
//console.log(`📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`); |
|
|
|
|
|
//console.log(`📊 All items breakdown:`, { |
|
|
|
|
|
//total: combinedLotData.length, |
|
|
|
|
|
//noLot: combinedLotData.filter(l => l.noLot === true).length, |
|
|
|
|
|
//normal: combinedLotData.filter(l => l.noLot !== true).length |
|
|
|
|
|
//}); |
|
|
|
|
|
|
|
|
return filtered.length; |
|
|
return filtered.length; |
|
|
}, [combinedLotData]); |
|
|
}, [combinedLotData]); |
|
|
@@ -3647,16 +3801,42 @@ paginatedData.map((row, index) => { |
|
|
const solSt = String(lot.stockOutLineStatus || "").toLowerCase(); |
|
|
const solSt = String(lot.stockOutLineStatus || "").toLowerCase(); |
|
|
const isSolRejected = |
|
|
const isSolRejected = |
|
|
solSt === "rejected" || String(lot.lotAvailability || "").toLowerCase() === "rejected"; |
|
|
solSt === "rejected" || String(lot.lotAvailability || "").toLowerCase() === "rejected"; |
|
|
|
|
|
|
|
|
|
|
|
const isFirstInGroup = row.isGroupFirst; |
|
|
|
|
|
const isLastInGroup = row.isGroupLast; |
|
|
|
|
|
const shouldOutline = row.isMultiLotGroup; |
|
|
|
|
|
const outlineColor = "#008000"; |
|
|
|
|
|
const groupUsesAltBg = row.groupDisplayIndex % 2 === 0; |
|
|
|
|
|
const groupRowBg = groupUsesAltBg ? "neutral.50" : "background.paper"; |
|
|
return ( |
|
|
return ( |
|
|
<TableRow |
|
|
<TableRow |
|
|
key={`${lot.pickOrderLineId}-${lot.lotId || 'null'}`} |
|
|
|
|
|
|
|
|
key={`${lot.pickOrderLineId}-${lot.stockOutLineId ?? "null"}-${lot.lotId ?? "nolot"}`} |
|
|
sx={{ |
|
|
sx={{ |
|
|
//backgroundColor: isIssueLot ? '#fff3e0' : 'inherit', |
|
|
|
|
|
// opacity: isIssueLot ? 0.6 : 1, |
|
|
|
|
|
'& .MuiTableCell-root': { |
|
|
|
|
|
//color: isIssueLot ? 'warning.main' : 'inherit' |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
backgroundColor: groupRowBg, |
|
|
|
|
|
"&:nth-of-type(even)": { backgroundColor: groupRowBg }, |
|
|
|
|
|
"& .MuiTableCell-root": { |
|
|
|
|
|
backgroundColor: groupRowBg, |
|
|
|
|
|
...(shouldOutline |
|
|
|
|
|
? { |
|
|
|
|
|
borderTop: isFirstInGroup ? `2px solid ${outlineColor}` : "none", |
|
|
|
|
|
borderBottom: isLastInGroup ? `2px solid ${outlineColor}` : "none", |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
: {}), |
|
|
|
|
|
}, |
|
|
|
|
|
"& .MuiTableCell-root:first-of-type": shouldOutline |
|
|
|
|
|
? { |
|
|
|
|
|
borderLeft: `2px solid ${outlineColor}`, |
|
|
|
|
|
borderTopLeftRadius: isFirstInGroup ? 6 : 0, |
|
|
|
|
|
borderBottomLeftRadius: isLastInGroup ? 6 : 0, |
|
|
|
|
|
} |
|
|
|
|
|
: {}, |
|
|
|
|
|
"& .MuiTableCell-root:last-of-type": shouldOutline |
|
|
|
|
|
? { |
|
|
|
|
|
borderRight: `2px solid ${outlineColor}`, |
|
|
|
|
|
borderTopRightRadius: isFirstInGroup ? 6 : 0, |
|
|
|
|
|
borderBottomRightRadius: isLastInGroup ? 6 : 0, |
|
|
|
|
|
} |
|
|
|
|
|
: {}, |
|
|
}} |
|
|
}} |
|
|
> |
|
|
> |
|
|
<TableCell> |
|
|
<TableCell> |
|
|
@@ -3671,7 +3851,7 @@ paginatedData.map((row, index) => { |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
<TableCell> |
|
|
<TableCell> |
|
|
<Typography variant="body2"> |
|
|
<Typography variant="body2"> |
|
|
{lot.routerRoute || '-'} |
|
|
|
|
|
|
|
|
{lot.noLot ? "-" : lot.routerRoute || "-"} |
|
|
</Typography> |
|
|
</Typography> |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
<TableCell> |
|
|
<TableCell> |
|
|
@@ -3746,8 +3926,13 @@ paginatedData.map((row, index) => { |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
<TableCell align="right"> |
|
|
<TableCell align="right"> |
|
|
{(() => { |
|
|
{(() => { |
|
|
const requiredQty = lot.requiredQty || 0; |
|
|
|
|
|
return requiredQty.toLocaleString() + '(' + lot.uomShortDesc + ')'; |
|
|
|
|
|
|
|
|
if (!row.isGroupFirst) return null; |
|
|
|
|
|
const requiredQty = lot.pickOrderLineRequiredQty; |
|
|
|
|
|
if (requiredQty == null) return null; |
|
|
|
|
|
const requiredQtyNum = Number(requiredQty); |
|
|
|
|
|
if (!Number.isFinite(requiredQtyNum)) return String(requiredQty); |
|
|
|
|
|
const uom = lot.uomShortDesc ? `(${lot.uomShortDesc})` : ""; |
|
|
|
|
|
return requiredQtyNum.toLocaleString() + uom; |
|
|
})()} |
|
|
})()} |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
|
|
|
|
|
|
@@ -3848,7 +4033,6 @@ paginatedData.map((row, index) => { |
|
|
const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected'; |
|
|
const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected'; |
|
|
const isNoLot = !lot.lotNo; |
|
|
const isNoLot = !lot.lotNo; |
|
|
const isUnavailableRow = isInventoryLotLineUnavailable(lot); |
|
|
const isUnavailableRow = isInventoryLotLineUnavailable(lot); |
|
|
|
|
|
|
|
|
// ✅ rejected lot:显示提示文本(换行显示) |
|
|
// ✅ rejected lot:显示提示文本(换行显示) |
|
|
if (isRejected && !isNoLot) { |
|
|
if (isRejected && !isNoLot) { |
|
|
const rejectHint = buildLotRejectDisplayMessage(lot, scanRejectMessageBySolId, t); |
|
|
const rejectHint = buildLotRejectDisplayMessage(lot, scanRejectMessageBySolId, t); |
|
|
@@ -3908,103 +4092,103 @@ paginatedData.map((row, index) => { |
|
|
? String(pickQtyData[lotKey]) |
|
|
? String(pickQtyData[lotKey]) |
|
|
: String(displayedSubmitQty) |
|
|
: String(displayedSubmitQty) |
|
|
: String(displayedSubmitQty); |
|
|
: String(displayedSubmitQty); |
|
|
|
|
|
const isRowPicked = |
|
|
|
|
|
status === "completed" || |
|
|
|
|
|
status === "checked" || |
|
|
|
|
|
status === "partially_completed" || |
|
|
|
|
|
status === "partially_complete"; |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<Stack direction="row" spacing={1} alignItems="center"> |
|
|
|
|
|
{/* |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="contained" |
|
|
|
|
|
onClick={() => { |
|
|
|
|
|
const submitQty = displayedSubmitQty; |
|
|
|
|
|
handlePickQtyChange(lotKey, submitQty); |
|
|
|
|
|
handleSubmitPickQtyWithQty(lot, submitQty, 'singleSubmit'); |
|
|
|
|
|
}} |
|
|
|
|
|
disabled={ |
|
|
|
|
|
lot.lotAvailability === 'expired' || |
|
|
|
|
|
isInventoryLotLineUnavailable(lot) || |
|
|
|
|
|
lot.lotAvailability === 'rejected' || |
|
|
|
|
|
lot.stockOutLineStatus === 'completed' || |
|
|
|
|
|
lot.stockOutLineStatus === 'pending' || |
|
|
|
|
|
(Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) |
|
|
|
|
|
} |
|
|
|
|
|
sx={{ fontSize: '0.75rem', py: 0.5, minHeight: '28px', minWidth: '70px' }} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Submit")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
*/} |
|
|
|
|
|
|
|
|
|
|
|
<TextField |
|
|
|
|
|
type="number" |
|
|
|
|
|
size="small" |
|
|
|
|
|
disabled={!qtyFieldEnabled} |
|
|
|
|
|
value={textFieldValue} |
|
|
|
|
|
onKeyDown={(e) => { |
|
|
|
|
|
if (!qtyFieldEnabled) return; |
|
|
|
|
|
if (e.key !== "{") return; |
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
setWorkbenchSubmitQtyFieldEnabledByLotKey((prev) => ({ |
|
|
|
|
|
...prev, |
|
|
|
|
|
[lotKey]: false, |
|
|
|
|
|
})); |
|
|
|
|
|
(e.currentTarget as HTMLInputElement).blur(); |
|
|
|
|
|
}} |
|
|
|
|
|
onChange={(e) => { |
|
|
|
|
|
if (!qtyFieldEnabled) return; |
|
|
|
|
|
const n = Number(e.target.value); |
|
|
|
|
|
if (Number.isFinite(n) && n < 0) return; |
|
|
|
|
|
handlePickQtyChange(lotKey, e.target.value); |
|
|
|
|
|
}} |
|
|
|
|
|
inputProps={{ min: 0, step: 1 }} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
width: 96, |
|
|
|
|
|
'& .MuiInputBase-input': { fontSize: '0.75rem', py: 0.5, textAlign: 'center' }, |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
|
|
|
|
|
|
<Button |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
size="small" |
|
|
|
|
|
onClick={() => { |
|
|
|
|
|
setWorkbenchSubmitQtyFieldEnabledByLotKey((prev) => ({ |
|
|
|
|
|
...prev, |
|
|
|
|
|
[lotKey]: !(prev[lotKey] === true), |
|
|
|
|
|
})); |
|
|
|
|
|
}} |
|
|
|
|
|
disabled={ |
|
|
|
|
|
lot.stockOutLineStatus === 'completed' || |
|
|
|
|
|
(Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) |
|
|
|
|
|
} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
fontSize: '0.7rem', |
|
|
|
|
|
py: 0.5, |
|
|
|
|
|
minHeight: '28px', |
|
|
|
|
|
minWidth: '60px', |
|
|
|
|
|
borderColor: 'warning.main', |
|
|
|
|
|
color: 'warning.main', |
|
|
|
|
|
}} |
|
|
|
|
|
title={qtyFieldEnabled ? t('Lock quantity') : t('Edit quantity')} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Edit")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
size="small" |
|
|
|
|
|
onClick={() => handleSkip(lot)} |
|
|
|
|
|
disabled={ |
|
|
|
|
|
lot.stockOutLineStatus === 'completed' || |
|
|
|
|
|
lot.stockOutLineStatus === 'checked' || |
|
|
|
|
|
lot.stockOutLineStatus === 'partially_completed' || |
|
|
|
|
|
|
|
|
<Stack direction="row" spacing={1} alignItems="center" justifyContent="center"> |
|
|
|
|
|
{isRowPicked ? ( |
|
|
|
|
|
<Typography |
|
|
|
|
|
variant="body2" |
|
|
|
|
|
sx={{ |
|
|
|
|
|
width: 96, |
|
|
|
|
|
textAlign: "center", |
|
|
|
|
|
fontSize: "0.75rem", |
|
|
|
|
|
fontWeight: 500, |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
{textFieldValue} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
) : ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
type="number" |
|
|
|
|
|
size="small" |
|
|
|
|
|
disabled={!qtyFieldEnabled} |
|
|
|
|
|
value={textFieldValue} |
|
|
|
|
|
onKeyDown={(e) => { |
|
|
|
|
|
if (!qtyFieldEnabled) return; |
|
|
|
|
|
if (e.key !== "{") return; |
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
setWorkbenchSubmitQtyFieldEnabledByLotKey((prev) => ({ |
|
|
|
|
|
...prev, |
|
|
|
|
|
[lotKey]: false, |
|
|
|
|
|
})); |
|
|
|
|
|
(e.currentTarget as HTMLInputElement).blur(); |
|
|
|
|
|
}} |
|
|
|
|
|
onChange={(e) => { |
|
|
|
|
|
if (!qtyFieldEnabled) return; |
|
|
|
|
|
const n = Number(e.target.value); |
|
|
|
|
|
if (Number.isFinite(n) && n < 0) return; |
|
|
|
|
|
handlePickQtyChange(lotKey, e.target.value); |
|
|
|
|
|
}} |
|
|
|
|
|
inputProps={{ min: 0, step: 1 }} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
width: 96, |
|
|
|
|
|
"& .MuiInputBase-input": { fontSize: "0.75rem", py: 0.5, textAlign: "center" }, |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
|
|
|
// 使用 issue form 後,禁用「Just Completed」(避免再次点击造成重复提交) |
|
|
|
|
|
(Number(lot.stockOutLineId) > 0 && issuePickedQtyBySolId[Number(lot.stockOutLineId)] !== undefined) || |
|
|
|
|
|
(Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Just Completed")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
|
|
|
{isRowPicked ? ( |
|
|
|
|
|
<Typography variant="body2" color="success.main" sx={{ fontSize: "0.75rem", fontWeight: 500 }}> |
|
|
|
|
|
{t("")} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
) : ( |
|
|
|
|
|
<> |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
size="small" |
|
|
|
|
|
onClick={() => { |
|
|
|
|
|
setWorkbenchSubmitQtyFieldEnabledByLotKey((prev) => ({ |
|
|
|
|
|
...prev, |
|
|
|
|
|
[lotKey]: !(prev[lotKey] === true), |
|
|
|
|
|
})); |
|
|
|
|
|
}} |
|
|
|
|
|
disabled={ |
|
|
|
|
|
Number(lot.stockOutLineId) > 0 && |
|
|
|
|
|
actionBusyBySolId[Number(lot.stockOutLineId)] === true |
|
|
|
|
|
} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
fontSize: "0.7rem", |
|
|
|
|
|
py: 0.5, |
|
|
|
|
|
minHeight: "28px", |
|
|
|
|
|
minWidth: "60px", |
|
|
|
|
|
borderColor: "warning.main", |
|
|
|
|
|
color: "warning.main", |
|
|
|
|
|
}} |
|
|
|
|
|
title={qtyFieldEnabled ? t("Lock quantity") : t("Edit quantity")} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Edit")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
size="small" |
|
|
|
|
|
onClick={() => handleSkip(lot)} |
|
|
|
|
|
disabled={ |
|
|
|
|
|
(Number(lot.stockOutLineId) > 0 && |
|
|
|
|
|
issuePickedQtyBySolId[Number(lot.stockOutLineId)] !== undefined) || |
|
|
|
|
|
(Number(lot.stockOutLineId) > 0 && |
|
|
|
|
|
actionBusyBySolId[Number(lot.stockOutLineId)] === true) |
|
|
|
|
|
} |
|
|
|
|
|
sx={{ fontSize: "0.7rem", py: 0.5, minHeight: "28px", minWidth: "60px" }} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Just Completed")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</> |
|
|
|
|
|
)} |
|
|
</Stack> |
|
|
</Stack> |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|