Przeglądaj źródła

do/jo/cmb pick, if scan not suggestion lot, will block and tell user this is not suggest lot

debrand
CANCERYS\kw093 5 godzin temu
rodzic
commit
d39bc41ca9
5 zmienionych plików z 105 dodań i 1186 usunięć
  1. +50
    -642
      src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx
  2. +33
    -527
      src/components/JoWorkbench/newJobPickExecution.tsx
  3. +18
    -17
      src/components/PickOrderSearch/WorkbenchPickExecution.tsx
  4. +2
    -0
      src/i18n/en/pickOrder.json
  5. +2
    -0
      src/i18n/zh/pickOrder.json

+ 50
- 642
src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx Wyświetl plik

@@ -43,7 +43,6 @@ import {
PickOrderCompletionResponse,
updateSuggestedLotLineId,
updateStockOutLineStatusByQRCodeAndLotNo,
confirmLotSubstitution,
fetchDoPickOrderDetail, // 必须添加
DoPickOrderDetail, // 必须添加
batchScan,
@@ -68,7 +67,6 @@ import QrCodeIcon from '@mui/icons-material/QrCode';
import { useQrCodeScannerContext } from "@/components/QrCodeScannerProvider/QrCodeScannerProvider";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import { fetchStockInLineInfo } from "@/app/api/po/actions";
import GoodPickExecutionForm from "@/components/FinishedGoodSearch/GoodPickExecutionForm";
import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal";
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(
indexes: {
byStockInLineId: Map<number, any[]>;
@@ -237,42 +235,43 @@ function findExactActiveMatchForStockInLine(
scannedItemId: number,
activeSuggestedLots: any[],
processedByItemId: ProcessedStockOutLinesByItemId,
scannedStockInLineId?: number,
): any | null {
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(
(lot) =>
lot.itemId === scannedItemId &&
activeSet.has(lot) &&
Number(lot.itemId) === Number(scannedItemId) &&
isSuggested(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() !== "",
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<{
@@ -777,10 +776,6 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
const [isRefreshingData, setIsRefreshingData] = 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);
// Use refs for processed QR tracking to avoid useEffect dependency issues and delays
@@ -834,56 +829,6 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
};
}, [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[]) => {
if (lotData.length === 0) {
setAllLotsCompleted(false);
@@ -1805,6 +1750,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
scannedItemId,
activeSuggestedLots,
processedQrCombinations,
scannedStockInLineId,
) ??
allLotsForItem.find(
(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) {
// Check if there are any lots for this item (even if all are rejected)
if (allLotsForItem.length === 0) {
@@ -1882,184 +1828,17 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
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(() => {
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;
}
@@ -2071,170 +1850,24 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
scannedItemId,
activeSuggestedLots,
processedQrCombinations,
scannedStockInLineId,
);
const matchTime = performance.now() - matchStartTime;
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) {
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) {
@@ -2384,213 +2017,6 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
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) {
const totalTime = performance.now() - totalStartTime;
console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`);
@@ -2608,7 +2034,6 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
lotDataIndexes,
processedQrCombinations,
combinedLotData,
fetchStockInLineInfoCached,
workbenchMode,
currentUserId,
clearWorkbenchScanReject,
@@ -4346,23 +3771,6 @@ paginatedData.map((row, index) => {
})()}
</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>
</TableCell>



+ 33
- 527
src/components/JoWorkbench/newJobPickExecution.tsx Wyświetl plik

@@ -105,24 +105,6 @@ function isInventoryLotLineUnavailable(lot: any): boolean {
return String(lot.lotStatus || "").toLowerCase() === "unavailable";
}

/** 同 DO Workbench:多行時優先替換有建議批號的行 */
function pickExpectedLotForSubstitution(
activeSuggestedLots: any[],
): any | null {
if (!activeSuggestedLots?.length) return null;
const withLotNo = activeSuggestedLots.filter(
(l) => l.lotNo != null && String(l.lotNo).trim() !== "",
);
if (withLotNo.length === 1) return withLotNo[0];
if (withLotNo.length > 1) {
const pending = withLotNo.find(
(l) => String(l.stockOutLineStatus || "").toLowerCase() === "pending",
);
return pending || withLotNo[0];
}
return activeSuggestedLots[0];
}

type PickOrderT = (key: string, options?: Record<string, unknown>) => string;

/** 與 DO Workbench:優先顯示 scan-pick 暫存拒絕訊息 */
@@ -867,12 +849,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
Map<number, Set<number>>
>(new Map());

// Cache for fetchStockInLineInfo API calls to avoid redundant requests
const stockInLineInfoCache = useRef<
Map<number, { lotNo: string | null; timestamp: number }>
>(new Map());
const CACHE_TTL = 60000; // 60 seconds cache TTL
const abortControllerRef = useRef<AbortController | null>(null);
const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null);

// Use refs for processed QR tracking to avoid useEffect dependency issues and delays
@@ -1366,33 +1342,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
};
}, [combinedLotData]);

// Cached version of fetchStockInLineInfo to avoid redundant API calls
const fetchStockInLineInfoCached = useCallback(
async (stockInLineId: number): Promise<{ lotNo: string | null }> => {
const now = Date.now();
const cached = stockInLineInfoCache.current.get(stockInLineId);
if (cached && now - cached.timestamp < CACHE_TTL) {
return { lotNo: cached.lotNo };
}

if (abortControllerRef.current) abortControllerRef.current.abort();
const abortController = new AbortController();
abortControllerRef.current = abortController;

const stockInLineInfo = await fetchStockInLineInfo(stockInLineId);
stockInLineInfoCache.current.set(stockInLineId, {
lotNo: stockInLineInfo.lotNo || null,
timestamp: now,
});
if (stockInLineInfoCache.current.size > 100) {
const firstKey = stockInLineInfoCache.current.keys().next().value;
if (firstKey !== undefined)
stockInLineInfoCache.current.delete(firstKey);
}
return { lotNo: stockInLineInfo.lotNo || null };
},
[],
);
// 修改:加载未分配的 Job Order 订单
const loadUnassignedOrders = useCallback(async () => {
setIsLoadingUnassigned(true);
@@ -2165,7 +2114,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
}
}

// ✅ 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) {
// Check if there are any lots for this item (even if all are rejected)
if (allLotsForItem.length === 0) {
@@ -2178,144 +2127,18 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
return;
}

console.log(
`⚠️ [QR PROCESS] No active suggested lots, auto-switching to scanned lot.`,
const notSuggestedMsg = L(
"此批次不是建議批次,已拒絕。請掃描建議批次。",
"Scanned lot is not the suggested lot. Please scan the suggested 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),
console.warn(
`⚠️ [QR PROCESS] No active suggested lot for scanned stockInLineId=${scannedStockInLineId}`,
);
const expectedLot =
rejectedLot ||
pickExpectedLotForSubstitution(
allLotsForItem.filter(
(l: any) => l.lotNo != null && String(l.lotNo).trim() !== "",
),
) ||
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(
tPick(
"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 ||
tPick("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: workbenchStoreId,
userId: currentUserId ?? 1,
});
const ok = res.code === "SUCCESS";
if (!ok) {
const failMsg =
(res as { message?: string })?.message ||
tPick("Workbench scan-pick failed.");
if (
shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) &&
expectedLot
) {
openWorkbenchLotLabelModalForLot(expectedLot, failMsg);
return;
}
if (workbenchMode && expectedLot.stockOutLineId != null) {
rememberWorkbenchScanReject(
Number(expectedLot.stockOutLineId),
failMsg,
);
}
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(failMsg);
});
return;
}

clearWorkbenchScanReject(Number(expectedLot.stockOutLineId));

startTransition(() => {
setQrScanError(false);
setQrScanSuccess(true);
});

setProcessedQrCombinations((prev) => {
const newMap = new Map(prev);
if (!newMap.has(scannedItemId))
newMap.set(scannedItemId, new Set());
newMap.get(scannedItemId)!.add(scannedStockInLineId);
return newMap;
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(notSuggestedMsg);
});

if (workbenchMode) {
await refreshWorkbenchAfterScanPick();
}

return;
}

@@ -2342,143 +2165,32 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
}`,
);

// ✅ 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) {
const expectedLot =
pickExpectedLotForSubstitution(activeSuggestedLots) ||
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(
tPick(
"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 ||
tPick("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: workbenchStoreId,
userId: currentUserId ?? 1,
});
const ok = res.code === "SUCCESS";
if (!ok) {
const failMsg =
(res as { message?: string })?.message ||
tPick("Workbench scan-pick failed.");
if (
shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) &&
expectedLot
) {
openWorkbenchLotLabelModalForLot(expectedLot, failMsg);
return;
}
if (workbenchMode && expectedLot.stockOutLineId != null) {
rememberWorkbenchScanReject(
Number(expectedLot.stockOutLineId),
failMsg,
);
}
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(failMsg);
});
return;
}

clearWorkbenchScanReject(Number(expectedLot.stockOutLineId));

startTransition(() => {
setQrScanError(false);
setQrScanSuccess(true);
});
exactMatch =
activeSuggestedLots.find(
(lot: any) =>
Number(lot?.itemId) === Number(scannedItemId) &&
Number(lot?.stockInLineId) === Number(scannedStockInLineId) &&
Number(scannedStockInLineId) > 0,
) ?? null;
}

setProcessedQrCombinations((prev) => {
const newMap = new Map(prev);
if (!newMap.has(scannedItemId))
newMap.set(scannedItemId, new Set());
newMap.get(scannedItemId)!.add(scannedStockInLineId);
return newMap;
});
if (workbenchMode) {
await refreshWorkbenchAfterScanPick();
}
return;
}
}
if (!exactMatch) {
const notSuggestedMsg = L(
"此批次不是建議批次,已拒絕。請掃描建議批次。",
"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) {
@@ -2642,183 +2354,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
return; // ✅ 直接返回,不需要确认表单
}

// ✅ Case 2: itemId 匹配但 stockInLineId 不匹配
// 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 expectedLot = pickExpectedLotForSubstitution(activeSuggestedLots);
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 = tPick(
"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 ||
tPick("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: workbenchStoreId,
userId: currentUserId ?? 1,
});

const ok = res.code === "SUCCESS";

if (!ok) {
const failMsg =
(res as { message?: string })?.message ||
tPick("Workbench scan-pick failed.");
if (
shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) &&
expectedLot
) {
openWorkbenchLotLabelModalForLot(expectedLot, failMsg);
return;
}
if (workbenchMode && expectedLot.stockOutLineId != null) {
rememberWorkbenchScanReject(
Number(expectedLot.stockOutLineId),
failMsg,
);
}
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(failMsg);
return;
}

clearWorkbenchScanReject(Number(expectedLot.stockOutLineId));

startTransition(() => {
setQrScanError(false);
setQrScanSuccess(true);
});

setProcessedQrCombinations((prev) => {
const newMap = new Map(prev);
if (!newMap.has(scannedItemId))
newMap.set(scannedItemId, new Set());
newMap.get(scannedItemId)!.add(scannedStockInLineId);
return newMap;
});

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, 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`,
);
} catch (error) {
const totalTime = performance.now() - totalStartTime;
console.error(
@@ -2836,7 +2371,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
lotDataIndexes,
processedQrCombinations,
combinedLotData,
fetchStockInLineInfoCached,
workbenchMode,
currentUserId,
clearWorkbenchScanReject,
@@ -2845,7 +2379,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
workbenchScanPickQtyFromLot,
tPick,
workbenchStoreId,
confirmLotSubstitution,
openWorkbenchLotLabelModalForLot,
shouldOpenWorkbenchLotLabelModalForFailure,
],
@@ -4213,33 +3746,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
)}
</Typography>
</Box>
{Number(lot.stockOutLineId) > 0 &&
Number(lot.itemId) > 0 ? (
<Button
variant="outlined"
size="small"
onClick={() =>
openWorkbenchLotLabelModalForLot(lot)
}
disabled={
(Number(lot.stockOutLineId) > 0 &&
actionBusyBySolId[
Number(lot.stockOutLineId)
] === true)
}
sx={{
flexShrink: 0,
fontSize: "0.7rem",
py: 0.25,
minWidth: "auto",
px: 1,
whiteSpace: "nowrap",
}}
>
{L("挑號 QR 碼", "Pick-lot QR code")}
</Button>
) : null}
</Stack>
</TableCell>
<TableCell align="right">


+ 18
- 17
src/components/PickOrderSearch/WorkbenchPickExecution.tsx Wyświetl plik

@@ -1405,16 +1405,27 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}

if (
Number(expectedRow.stockInLineId) > 0 &&
Number(scannedState.stockInLineId) > 0 &&
Number(expectedRow.stockInLineId) === Number(scannedState.stockInLineId)
) {
await submitRow(expectedRow);
const scannedSil = Number(scannedState.stockInLineId);
const suggestedRow =
scannedSil > 0
? activeForUom.find((r) => Number(r.stockInLineId) === scannedSil)
: undefined;
if (suggestedRow) {
await submitRow(suggestedRow);
return;
}

await handleLotConfirmation(scannedState, toConfirmLotState(expectedRow));
const notSuggestedMsg = t(
"Scanned lot is not the suggested lot. Please scan the suggested lot.",
);
setError(notSuggestedMsg);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(notSuggestedMsg);
});
releaseProcessedQr(latest);
return;
},
[
handleLotConfirmation,
@@ -1707,16 +1718,6 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
</Box>
)}
</Typography>
{r.stockOutLineId > 0 ? (
<Button
variant="outlined"
size="small"
onClick={() => openWorkbenchLotLabelModalForLot(r)}
sx={{ flexShrink: 0, fontSize: "0.7rem", py: 0.25, minWidth: "auto", px: 1, whiteSpace: "nowrap" }}
>
{t("lot QR code")}
</Button>
) : null}
</Stack>
</TableCell>
<TableCell align="right">{`${r.requiredQty.toLocaleString()}(${r.uomDesc || ""})`}</TableCell>


+ 2
- 0
src/i18n/en/pickOrder.json Wyświetl plik

@@ -16,6 +16,8 @@
"Loading...": "Loading...",
"Suggestion success": "Suggestion success",
"Scan pick success": "Scan pick success",
"Scanned lot is not the suggested lot. Please scan the suggested lot.": "Scanned lot is not the suggested lot. Please scan the suggested lot.",
"remaining quantity allocated to next stock-out line": "remaining quantity allocated to next stock-out line",
"Remark": "Remark",
"Available Qty": "Available Qty",
"Picked Qty": "Picked Qty",


+ 2
- 0
src/i18n/zh/pickOrder.json Wyświetl plik

@@ -17,6 +17,8 @@
"Loading...": "載入中...",
"Suggestion success": "建議成功",
"Scan pick success": "掃描提料成功",
"Scanned lot is not the suggested lot. Please scan the suggested lot.": "此批次不是建議批次,已拒絕。請掃描建議批次。",
"remaining quantity allocated to next stock-out line": "剩餘數量分配到下一個出庫行",
"Remark": "備註",
"Available Qty": "可用數量",
"Picked Qty": "已提料數量",


Ładowanie…
Anuluj
Zapisz