| @@ -191,6 +191,45 @@ function hasPendingActiveRowForStockInLine( | |||||
| ); | ); | ||||
| } | } | ||||
| /** | |||||
| * True when this item still has a pending SOL that scan can bind to — including | |||||
| * noLot / expired / unavailable rows (those are excluded from activeLotsByItemId). | |||||
| * Used so a brand-new stockInLineId can still reach auto-switch. | |||||
| */ | |||||
| function hasPendingSwitchableRowForItem( | |||||
| indexes: { byItemId: Map<number, any[]> }, | |||||
| itemId: number, | |||||
| processedByItemId: ProcessedStockOutLinesByItemId, | |||||
| ): boolean { | |||||
| const rows = indexes.byItemId.get(itemId) ?? []; | |||||
| return rows.some( | |||||
| (lot) => | |||||
| isLotRowPending(lot) && | |||||
| !isStockOutLineAlreadyProcessed(processedByItemId, itemId, lot.stockOutLineId), | |||||
| ); | |||||
| } | |||||
| /** Allow scan when scanned SIL still needed, or item has any pending/noLot row for auto-switch. */ | |||||
| function canProcessWorkbenchScanPick( | |||||
| indexes: { | |||||
| byStockInLineId: Map<number, any[]>; | |||||
| activeLotsByItemId: Map<number, any[]>; | |||||
| byItemId: Map<number, any[]>; | |||||
| }, | |||||
| itemId: number, | |||||
| stockInLineId: number, | |||||
| processedByItemId: ProcessedStockOutLinesByItemId, | |||||
| ): boolean { | |||||
| return ( | |||||
| hasPendingActiveRowForStockInLine( | |||||
| indexes, | |||||
| itemId, | |||||
| stockInLineId, | |||||
| processedByItemId, | |||||
| ) || hasPendingSwitchableRowForItem(indexes, itemId, processedByItemId) | |||||
| ); | |||||
| } | |||||
| function findExactActiveMatchForStockInLine( | function findExactActiveMatchForStockInLine( | ||||
| stockInLineLots: any[], | stockInLineLots: any[], | ||||
| scannedItemId: number, | scannedItemId: number, | ||||
| @@ -1625,7 +1664,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| const scannedStockInLineId = qrData.stockInLineId; | const scannedStockInLineId = qrData.stockInLineId; | ||||
| if ( | if ( | ||||
| !hasPendingActiveRowForStockInLine( | |||||
| !canProcessWorkbenchScanPick( | |||||
| indexes, | indexes, | ||||
| scannedItemId, | scannedItemId, | ||||
| scannedStockInLineId, | scannedStockInLineId, | ||||
| @@ -1730,20 +1769,38 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| console.log(`⚠️ [QR PROCESS] No active suggested lots, auto-switching to scanned lot.`); | console.log(`⚠️ [QR PROCESS] No active suggested lots, auto-switching to scanned lot.`); | ||||
| // Find a rejected lot as expected lot (the one that was rejected) | |||||
| const rejectedLot = allLotsForItem.find((lot: any) => | |||||
| lot.stockOutLineStatus?.toLowerCase() === 'rejected' || | |||||
| lot.lotAvailability === 'rejected' || | |||||
| isInventoryLotLineUnavailable(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 expectedLot = | const expectedLot = | ||||
| rejectedLot || | |||||
| switchableLot || | |||||
| pickExpectedLotForSubstitution( | pickExpectedLotForSubstitution( | ||||
| allLotsForItem.filter( | allLotsForItem.filter( | ||||
| (l: any) => l.lotNo != null && String(l.lotNo).trim() !== "" | (l: any) => l.lotNo != null && String(l.lotNo).trim() !== "" | ||||
| ), | ), | ||||
| processedQrCombinations, | processedQrCombinations, | ||||
| ) || | ) || | ||||
| allLotsForItem.find( | |||||
| (lot: any) => | |||||
| isLotRowPending(lot) && | |||||
| !isStockOutLineAlreadyProcessed( | |||||
| processedQrCombinations, | |||||
| scannedItemId, | |||||
| lot.stockOutLineId, | |||||
| ), | |||||
| ) || | |||||
| allLotsForItem[0]; | allLotsForItem[0]; | ||||
| let scannedLotNo: string | null = scannedLot?.lotNo || null; | let scannedLotNo: string | null = scannedLot?.lotNo || null; | ||||
| @@ -2210,7 +2267,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| // Workbench 策略:不彈窗,直接切換到掃到的批次並提交一次掃描 | // Workbench 策略:不彈窗,直接切換到掃到的批次並提交一次掃描 | ||||
| const mismatchCheckStartTime = performance.now(); | const mismatchCheckStartTime = performance.now(); | ||||
| if ( | if ( | ||||
| !hasPendingActiveRowForStockInLine( | |||||
| !canProcessWorkbenchScanPick( | |||||
| indexes, | indexes, | ||||
| scannedItemId, | scannedItemId, | ||||
| scannedStockInLineId, | scannedStockInLineId, | ||||