| @@ -22,6 +22,7 @@ import { | |||
| } from "@mui/material"; | |||
| import dayjs from 'dayjs'; | |||
| import { normalizeTargetDateInput } from "@/utils/workbenchTargetDate"; | |||
| import { isWorkbenchExtraTicket } from "@/utils/workbenchReleaseType"; | |||
| import TestQrCodeProvider from "@/components/QrCodeScannerProvider/TestQrCodeProvider"; | |||
| import { fetchLotDetail } from "@/app/api/inventory/actions"; | |||
| import { formatDepartureTime } from "@/app/utils/formatUtil"; | |||
| @@ -131,37 +132,49 @@ function parseWorkbenchQrPayload( | |||
| } | |||
| } | |||
| function hasPendingActiveRowForStockInLine( | |||
| indexes: { | |||
| byStockInLineId: Map<number, any[]>; | |||
| activeLotsByItemId: Map<number, any[]>; | |||
| }, | |||
| /** | |||
| * QR entry gate: pending / partial SOL not yet processed this session. | |||
| * Includes expired & unavailable rows (still need modal /换批); excludes completed/rejected/checked. | |||
| * `processedQrCombinations` still prevents re-picking the same SOL (e.g. two lines, same lot). | |||
| */ | |||
| function isLotRowEligibleForQrEntryGate( | |||
| lot: any, | |||
| itemId: number, | |||
| processedByItemId: ProcessedStockOutLinesByItemId, | |||
| ): boolean { | |||
| if (Number(lot?.itemId) !== itemId) return false; | |||
| if (!isLotRowPending(lot)) return false; | |||
| const st = String(lot?.stockOutLineStatus ?? "").toLowerCase(); | |||
| if (st === "rejected" || st === "completed" || st === "checked") return false; | |||
| if (String(lot?.lotAvailability ?? "").toLowerCase() === "rejected") return false; | |||
| return !isStockOutLineAlreadyProcessed( | |||
| processedByItemId, | |||
| itemId, | |||
| lot.stockOutLineId, | |||
| ); | |||
| } | |||
| function hasPendingUnprocessedRowForStockInLine( | |||
| indexes: { byStockInLineId: 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), | |||
| return rows.some((lot) => | |||
| isLotRowEligibleForQrEntryGate(lot, itemId, processedByItemId), | |||
| ); | |||
| } | |||
| /** Any pending active SOL for this item (e.g. scan different location, same lot no → auto-switch). */ | |||
| function hasPendingActiveRowForItem( | |||
| indexes: { activeLotsByItemId: Map<number, any[]> }, | |||
| /** Any pending unprocessed SOL for this item (e.g. scan different stockInLineId → auto-switch). */ | |||
| function hasPendingUnprocessedRowForItem( | |||
| indexes: { byItemId: Map<number, any[]> }, | |||
| itemId: number, | |||
| processedByItemId: ProcessedStockOutLinesByItemId, | |||
| ): boolean { | |||
| const activeLots = indexes.activeLotsByItemId.get(itemId) ?? []; | |||
| return activeLots.some( | |||
| (lot) => | |||
| isLotRowPending(lot) && | |||
| !isStockOutLineAlreadyProcessed(processedByItemId, itemId, lot.stockOutLineId), | |||
| const rows = indexes.byItemId.get(itemId) ?? []; | |||
| return rows.some((lot) => | |||
| isLotRowEligibleForQrEntryGate(lot, itemId, processedByItemId), | |||
| ); | |||
| } | |||
| @@ -343,6 +356,11 @@ function isInventoryLotLineUnavailable(lot: any): boolean { | |||
| return String(lot.lotStatus || "").toLowerCase() === "unavailable"; | |||
| } | |||
| /** 過期或不可用:單筆 Just Complete / 顯示數量與批量提交一致,固定 qty=0 */ | |||
| function isWorkbenchZeroCompleteLot(lot: any): boolean { | |||
| return isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot); | |||
| } | |||
| /** 提貨台「列印標籤」彈窗頂部:依目前表格列判斷可提貨/已用畢/已過期等 */ | |||
| function isWorkbenchSourceLotExpired(lot: any): boolean { | |||
| if (!lot) return false; | |||
| @@ -419,6 +437,55 @@ function translateWorkbenchRejectMessage(raw: string, t: PickOrderT): string { | |||
| return t(msg); | |||
| } | |||
| function isExpiredWorkbenchReminderMessage(msg: string): boolean { | |||
| const trimmed = msg.trim(); | |||
| if (!trimmed) return false; | |||
| if (/^lot is expired \(expiry=/i.test(trimmed)) return true; | |||
| return /已過期/.test(trimmed) || /掃描批號已過期/.test(trimmed); | |||
| } | |||
| type UnpickableScanAvailability = "expired" | "status_unavailable"; | |||
| function inferUnpickableScanAvailability( | |||
| failMsg: string | null | undefined, | |||
| ): UnpickableScanAvailability | null { | |||
| const m = String(failMsg ?? "").trim().toLowerCase(); | |||
| if (!m) return null; | |||
| if ( | |||
| m.includes("expired") || | |||
| m.includes("过期") || | |||
| m.includes("已過期") || | |||
| /^lot is expired/.test(m) | |||
| ) { | |||
| return "expired"; | |||
| } | |||
| if ( | |||
| m.includes("unavailable") || | |||
| m.includes("not available") || | |||
| m.includes("not yet putaway") || | |||
| m.includes("不可用") || | |||
| m.includes("未上架") | |||
| ) { | |||
| return "status_unavailable"; | |||
| } | |||
| return null; | |||
| } | |||
| function buildUnpickableScanRowPatch( | |||
| scannedLot: any | null | undefined, | |||
| availability: UnpickableScanAvailability, | |||
| ): Record<string, unknown> { | |||
| const patch: Record<string, unknown> = { lotAvailability: availability }; | |||
| if (availability === "status_unavailable") { | |||
| patch.lotStatus = "unavailable"; | |||
| } | |||
| if (scannedLot?.lotNo) patch.lotNo = scannedLot.lotNo; | |||
| if (scannedLot?.stockInLineId) patch.stockInLineId = scannedLot.stockInLineId; | |||
| if (scannedLot?.expiryDate) patch.expiryDate = scannedLot.expiryDate; | |||
| if (scannedLot?.lotId) patch.lotId = scannedLot.lotId; | |||
| return patch; | |||
| } | |||
| /** | |||
| * 顯示後端拒絕原因:優先 workbench scan API 的 message(暫存於 scanRejectBySolId), | |||
| * 其次階層 API 若帶 stockOutLineRejectMessage,最後依 rejected + lotAvailability 推斷(與後端語意對齊)。 | |||
| @@ -646,10 +713,10 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); | |||
| const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]); | |||
| const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false); | |||
| const isExtraTicket = useMemo(() => { | |||
| const ticketNo = String(fgPickOrders?.[0]?.ticketNo ?? "").trim().toUpperCase(); | |||
| return ticketNo.startsWith("TI-E-"); | |||
| }, [fgPickOrders]); | |||
| const isExtraTicket = useMemo( | |||
| () => isWorkbenchExtraTicket(fgPickOrders?.[0]?.releaseType, fgPickOrders?.[0]?.ticketNo), | |||
| [fgPickOrders], | |||
| ); | |||
| const lotFloorPrefixFilter = useMemo(() => { | |||
| const storeId = String(fgPickOrders?.[0]?.storeId ?? "") | |||
| @@ -894,6 +961,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| shopName: hierarchicalData.fgInfo.shopName, | |||
| truckLanceCode: hierarchicalData.fgInfo.truckLanceCode, | |||
| DepartureTime: hierarchicalData.fgInfo.departureTime, | |||
| releaseType: hierarchicalData.fgInfo.releaseType, | |||
| shopAddress: "", | |||
| pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "", | |||
| @@ -1163,12 +1231,64 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| } else { | |||
| setWorkbenchLotLabelInitialPayload(null); | |||
| } | |||
| setWorkbenchLotLabelReminderText(reminderText ?? null); | |||
| setWorkbenchLotLabelReminderText( | |||
| reminderText ? translateWorkbenchRejectMessage(reminderText, t) : null, | |||
| ); | |||
| // Clear latched success so the lot-label modal effect cannot instantly re-close on open. | |||
| setQrScanSuccess(false); | |||
| setWorkbenchLotLabelModalOpen(true); | |||
| }, | |||
| [], | |||
| [t], | |||
| ); | |||
| /** Patch pick row locally so table shows 已過期/不可用 without full refresh. */ | |||
| const patchWorkbenchRowForUnpickableScan = useCallback( | |||
| ( | |||
| pickRow: any, | |||
| scannedLot: any | null | undefined, | |||
| availability: UnpickableScanAvailability, | |||
| ) => { | |||
| const solId = Number(pickRow?.stockOutLineId); | |||
| if (!solId) return; | |||
| const rowPatch = buildUnpickableScanRowPatch(scannedLot, availability); | |||
| const mapRows = (prev: any[]) => | |||
| prev.map((lot) => | |||
| Number(lot.stockOutLineId) === solId ? { ...lot, ...rowPatch } : lot, | |||
| ); | |||
| setCombinedLotData(mapRows); | |||
| setOriginalCombinedData(mapRows); | |||
| clearWorkbenchScanReject(solId); | |||
| }, | |||
| [clearWorkbenchScanReject], | |||
| ); | |||
| const openUnpickableScanLotLabelModal = useCallback( | |||
| ( | |||
| pickRow: any, | |||
| scannedLot: any | null | undefined, | |||
| reminderText: string, | |||
| ) => { | |||
| const fromMsg = inferUnpickableScanAvailability(reminderText); | |||
| const availability = | |||
| fromMsg ?? | |||
| (isWorkbenchSourceLotExpired(scannedLot ?? pickRow) | |||
| ? "expired" | |||
| : isInventoryLotLineUnavailable(scannedLot ?? pickRow) | |||
| ? "status_unavailable" | |||
| : null); | |||
| const mergedPickRow = | |||
| availability != null | |||
| ? { ...pickRow, ...buildUnpickableScanRowPatch(scannedLot, availability) } | |||
| : pickRow; | |||
| if (availability != null) { | |||
| patchWorkbenchRowForUnpickableScan(pickRow, scannedLot, availability); | |||
| } | |||
| openWorkbenchLotLabelModalForLot(mergedPickRow, reminderText); | |||
| }, | |||
| [ | |||
| patchWorkbenchRowForUnpickableScan, | |||
| openWorkbenchLotLabelModalForLot, | |||
| ], | |||
| ); | |||
| const shouldOpenWorkbenchLotLabelModalForFailure = useCallback( | |||
| @@ -1253,9 +1373,17 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| severity: undefined as "success" | "warning" | "error" | undefined, | |||
| }; | |||
| } | |||
| const reminder = workbenchLotLabelReminderText?.trim() ?? ""; | |||
| if (reminder && isExpiredWorkbenchReminderMessage(reminder)) { | |||
| return { text: "此批號狀態:已過期", severity: "error" as const }; | |||
| } | |||
| const s = getWorkbenchSourceLotStatusSummary(workbenchLotLabelContextLot); | |||
| return { text: s.text, severity: s.severity }; | |||
| }, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot]); | |||
| }, [ | |||
| workbenchLotLabelModalOpen, | |||
| workbenchLotLabelContextLot, | |||
| workbenchLotLabelReminderText, | |||
| ]); | |||
| const workbenchLotLabelSubmitQty = useMemo(() => { | |||
| if (!workbenchLotLabelContextLot) return 0; | |||
| @@ -1541,6 +1669,19 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| } | |||
| }; | |||
| /** Stop QR effect re-entry after unpickable modal (expired/unavailable API fail). */ | |||
| const markUnpickableScanSessionHandled = ( | |||
| itemId: number, | |||
| stockOutLineId: number | null | undefined, | |||
| ) => { | |||
| if (stockOutLineId != null) { | |||
| setProcessedQrCombinations((prev) => | |||
| markProcessedStockOutLine(prev, itemId, stockOutLineId), | |||
| ); | |||
| } | |||
| recordHandledQrScanCount(qrScanCountAtInvoke); | |||
| }; | |||
| try { | |||
| // 1) Parse JSON safely (parse once, reuse) | |||
| const parseStartTime = performance.now(); | |||
| @@ -1575,13 +1716,13 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| const scannedItemId = qrData.itemId; | |||
| const scannedStockInLineId = qrData.stockInLineId; | |||
| const hasPendingOnScannedSil = hasPendingActiveRowForStockInLine( | |||
| const hasPendingOnScannedSil = hasPendingUnprocessedRowForStockInLine( | |||
| indexes, | |||
| scannedItemId, | |||
| scannedStockInLineId, | |||
| processedQrCombinations, | |||
| ); | |||
| const hasPendingOnItem = hasPendingActiveRowForItem( | |||
| const hasPendingOnItem = hasPendingUnprocessedRowForItem( | |||
| indexes, | |||
| scannedItemId, | |||
| processedQrCombinations, | |||
| @@ -1590,6 +1731,15 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| console.log( | |||
| ` [SKIP] No pending stock-out line left for itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId}`, | |||
| ); | |||
| startTransition(() => { | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| setQrScanErrorMsg( | |||
| t( | |||
| "No pending pick line left for this item. It may already be completed or fully processed.", | |||
| ), | |||
| ); | |||
| }); | |||
| return; | |||
| } | |||
| @@ -1628,14 +1778,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。` | |||
| ); | |||
| }); | |||
| if (scannedLot?.stockOutLineId != null) { | |||
| const nextProcessed = markProcessedStockOutLine( | |||
| processedQrCombinations, | |||
| scannedItemId, | |||
| scannedLot.stockOutLineId, | |||
| ); | |||
| setProcessedQrCombinations(nextProcessed); | |||
| } | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| scannedLot.stockOutLineId, | |||
| ); | |||
| return; | |||
| } | |||
| @@ -1645,25 +1791,33 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| setQrScanError(false); | |||
| setQrScanSuccess(false); | |||
| }); | |||
| openWorkbenchLotLabelModalForLot( | |||
| openUnpickableScanLotLabelModal( | |||
| scannedLot, | |||
| scannedLot, | |||
| t("This lot is not available, please scan another lot."), | |||
| ); | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| scannedLot.stockOutLineId, | |||
| ); | |||
| return; | |||
| } | |||
| const isExpired = | |||
| String(scannedLot.lotAvailability || '').toLowerCase() === 'expired'; | |||
| if (isExpired) { | |||
| if (isWorkbenchSourceLotExpired(scannedLot)) { | |||
| console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is expired; opening lot-label modal`); | |||
| startTransition(() => { | |||
| setQrScanError(false); | |||
| setQrScanSuccess(false); | |||
| }); | |||
| openWorkbenchLotLabelModalForLot( | |||
| openUnpickableScanLotLabelModal( | |||
| scannedLot, | |||
| scannedLot, | |||
| `Lot is expired (expiry=${scannedLot.expiryDate || "-"})`, | |||
| ); | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| scannedLot.stockOutLineId, | |||
| ); | |||
| return; | |||
| } | |||
| } | |||
| @@ -1766,7 +1920,19 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && | |||
| expectedLot | |||
| ) { | |||
| openWorkbenchLotLabelModalForLot(expectedLot, failMsg); | |||
| openUnpickableScanLotLabelModal( | |||
| expectedLot, | |||
| scannedLot ?? | |||
| allLotsForItem.find( | |||
| (lot: any) => lot.stockInLineId === scannedStockInLineId, | |||
| ) ?? | |||
| null, | |||
| failMsg, | |||
| ); | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| expectedLot.stockOutLineId, | |||
| ); | |||
| return; | |||
| } | |||
| if (workbenchMode && expectedLot.stockOutLineId != null) { | |||
| @@ -1932,7 +2098,21 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && | |||
| expectedLot | |||
| ) { | |||
| openWorkbenchLotLabelModalForLot(expectedLot, failMsg); | |||
| openUnpickableScanLotLabelModal( | |||
| expectedLot, | |||
| scannedLot ?? | |||
| allLotsForItem.find( | |||
| (lot: any) => lot.stockInLineId === scannedStockInLineId, | |||
| ) ?? { | |||
| stockInLineId: scannedStockInLineId, | |||
| lotNo: scannedLotNo, | |||
| }, | |||
| failMsg, | |||
| ); | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| expectedLot.stockOutLineId, | |||
| ); | |||
| return; | |||
| } | |||
| if (workbenchMode && expectedLot.stockOutLineId != null) { | |||
| @@ -2119,7 +2299,11 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && | |||
| exactMatch | |||
| ) { | |||
| openWorkbenchLotLabelModalForLot(exactMatch, failMsg); | |||
| openUnpickableScanLotLabelModal(exactMatch, exactMatch, failMsg); | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| exactMatch.stockOutLineId, | |||
| ); | |||
| return; | |||
| } | |||
| if (workbenchMode && exactMatch.stockOutLineId != null) { | |||
| @@ -2233,7 +2417,21 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) && | |||
| expectedLot | |||
| ) { | |||
| openWorkbenchLotLabelModalForLot(expectedLot, failMsg); | |||
| openUnpickableScanLotLabelModal( | |||
| expectedLot, | |||
| scannedLot ?? | |||
| allLotsForItem.find( | |||
| (lot: any) => lot.stockInLineId === scannedStockInLineId, | |||
| ) ?? { | |||
| stockInLineId: scannedStockInLineId, | |||
| lotNo: scannedLotNo, | |||
| }, | |||
| failMsg, | |||
| ); | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| expectedLot.stockOutLineId, | |||
| ); | |||
| return; | |||
| } | |||
| if (workbenchMode && expectedLot.stockOutLineId != null) { | |||
| @@ -2340,6 +2538,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| refreshWorkbenchAfterScanPick, | |||
| workbenchScanPickQtyFromLot, | |||
| openWorkbenchLotLabelModalForLot, | |||
| openUnpickableScanLotLabelModal, | |||
| shouldOpenWorkbenchLotLabelModalForFailure, | |||
| t, | |||
| ]); | |||
| @@ -2445,6 +2644,12 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| // If it's a different QR, allow processing | |||
| console.log(` [QR PROCESS] Different QR detected while manual modal open, allowing processing`); | |||
| } | |||
| // Skip re-processing while lot-label modal is already open for this scan | |||
| if (workbenchLotLabelModalOpen && latestQr === lastProcessedQrRef.current) { | |||
| console.log(` [QR PROCESS] Skipping - lot-label modal open for same QR`); | |||
| return; | |||
| } | |||
| const qrDetectionStartTime = performance.now(); | |||
| console.log(` [QR DETECTION] Latest QR detected: ${latestQr?.substring(0, 50)}...`); | |||
| @@ -2457,7 +2662,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| const canRetrySamePhysicalLot = | |||
| qrPayload != null && | |||
| isNewScanEvent && | |||
| hasPendingActiveRowForStockInLine( | |||
| hasPendingUnprocessedRowForStockInLine( | |||
| lotDataIndexes, | |||
| qrPayload.itemId, | |||
| qrPayload.stockInLineId, | |||
| @@ -2578,6 +2783,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| isRefreshingData, | |||
| combinedLotData.length, | |||
| manualLotConfirmationOpen, | |||
| workbenchLotLabelModalOpen, | |||
| lotDataIndexes, | |||
| processedQrCombinations, | |||
| ]); | |||
| @@ -2925,8 +3131,8 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| try { | |||
| if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true })); | |||
| const targetUnavailable = isInventoryLotLineUnavailable(lot); | |||
| const effectiveSubmitQty = targetUnavailable && submitQty > 0 ? 0 : submitQty; | |||
| const targetZeroComplete = isWorkbenchZeroCompleteLot(lot); | |||
| const effectiveSubmitQty = targetZeroComplete && submitQty > 0 ? 0 : submitQty; | |||
| const canonicalLotForSol = | |||
| solId > 0 | |||
| @@ -2951,10 +3157,10 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| const qtyPayload = workbenchScanPickQtyFromLot(canonicalLotForSol); | |||
| const wbJustQty = qtyPayload.qty; | |||
| const isUnavailableForJustComplete = isInventoryLotLineUnavailable(canonicalLotForSol); | |||
| const isZeroCompleteForJustComplete = isWorkbenchZeroCompleteLot(canonicalLotForSol); | |||
| const canPostScanPick = | |||
| // unavailable lot: Just Completed must always submit qty=0, even without lotNo | |||
| isUnavailableForJustComplete || ( | |||
| // expired / unavailable: Just Completed always submits qty=0, even without lotNo | |||
| isZeroCompleteForJustComplete || ( | |||
| canonicalLotForSol.lotNo && String(canonicalLotForSol.lotNo).trim() !== "" && ( | |||
| // explicit short submit: user typed 0 (must send qty=0 to backend) | |||
| (hasExplicitSubmitOverride && | |||
| @@ -2966,7 +3172,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| ); | |||
| if (canPostScanPick) { | |||
| const qtyToSend = isUnavailableForJustComplete | |||
| const qtyToSend = isZeroCompleteForJustComplete | |||
| ? 0 | |||
| : hasExplicitSubmitOverride && explicitSubmitOverride === 0 | |||
| ? 0 | |||
| @@ -3011,7 +3217,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| return; | |||
| } | |||
| const justCompleteErr = t( | |||
| "Just Completed (workbench): requires a valid lot number and quantity; expired rows must not use this button.", | |||
| "Just Completed (workbench): requires a valid lot number and quantity.", | |||
| ); | |||
| if (solId > 0) { | |||
| rememberWorkbenchScanReject(solId, justCompleteErr); | |||
| @@ -3800,7 +4006,7 @@ paginatedData.map((row, index) => { | |||
| const solIdForKey = Number(lot.stockOutLineId) || 0; | |||
| const lotKeyForSubmitQty = | |||
| Number.isFinite(solIdForKey) && solIdForKey > 0 ? `sol:${solIdForKey}` : `${lot.pickOrderLineId}-${lot.lotId}`; | |||
| const lockedSubmitQtyDisplay = isInventoryLotLineUnavailable(lot) ? 0 : resolveSingleSubmitQty(lot); | |||
| const lockedSubmitQtyDisplay = isWorkbenchZeroCompleteLot(lot) ? 0 : resolveSingleSubmitQty(lot); | |||
| const hasPickOverride = Object.prototype.hasOwnProperty.call(pickQtyData, lotKeyForSubmitQty); | |||
| const fromPickRow = hasPickOverride ? pickQtyData[lotKeyForSubmitQty] : undefined; | |||
| const workbenchSubmitQtyDisplay = | |||