Ver a proveniência

do scan same lot fix

production
CANCERYS\kw093 há 1 dia
ascendente
cometimento
28df4342ee
1 ficheiros alterados com 238 adições e 74 eliminações
  1. +238
    -74
      src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx

+ 238
- 74
src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx Ver ficheiro

@@ -101,6 +101,66 @@ function pickExpectedLotForSubstitution(activeSuggestedLots: any[]): any | null
return activeSuggestedLots[0];
}

/** Parse workbench QR / test shortcut into itemId + stockInLineId. */
function parseWorkbenchScanQrPayload(
raw: string,
): { itemId: number; stockInLineId: number } | null {
const text = String(raw || "").trim();
if (!text) return null;
if (
(text.startsWith("{2fitest") || text.startsWith("{2fittest")) &&
text.endsWith("}")
) {
let content = "";
if (text.startsWith("{2fittest")) {
content = text.substring(9, text.length - 1);
} else {
content = text.substring(8, text.length - 1);
}
const parts = content.split(",");
if (parts.length === 2) {
const itemId = parseInt(parts[0].trim(), 10);
const stockInLineId = parseInt(parts[1].trim(), 10);
if (
Number.isFinite(itemId) &&
itemId > 0 &&
Number.isFinite(stockInLineId) &&
stockInLineId > 0
) {
return { itemId, stockInLineId };
}
}
return null;
}
try {
const obj = JSON.parse(text);
const itemId = Number(obj?.itemId);
const stockInLineId = Number(obj?.stockInLineId);
if (
Number.isFinite(itemId) &&
itemId > 0 &&
Number.isFinite(stockInLineId) &&
stockInLineId > 0
) {
return { itemId, stockInLineId };
}
} catch {
/* not JSON */
}
return null;
}

/** True when another pending/partial SOL still suggests this stockInLineId (same lot across lines). */
function activeLotsStillNeedStockInLine(
activeLotsForItem: any[] | undefined,
stockInLineId: number,
): boolean {
if (!activeLotsForItem?.length) return false;
return activeLotsForItem.some(
(lot) => Number(lot?.stockInLineId) === Number(stockInLineId),
);
}

const ManualLotConfirmationModal: React.FC<{
open: boolean;
onClose: () => void;
@@ -601,6 +661,12 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
// Use refs for processed QR tracking to avoid useEffect dependency issues and delays
const processedQrCodesRef = useRef<Set<string>>(new Set());
const lastProcessedQrRef = useRef<string>('');
/** Prevent double-submit while a scan-pick API is in flight (scanner bounce). */
const qrProcessInFlightRef = useRef(false);
/** Latest lot indexes for QR re-scan allow checks outside processOutsideQrCode. */
const lotDataIndexesRef = useRef<{
activeLotsByItemId: Map<number, any[]>;
} | null>(null);
// Store callbacks in refs to avoid useEffect dependency issues
const processOutsideQrCodeRef = useRef<
@@ -1399,6 +1465,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId };
}, [combinedLotData.length, combinedLotData]);

lotDataIndexesRef.current = {
activeLotsByItemId: lotDataIndexes.activeLotsByItemId,
};
// Store resetScan in ref for immediate access (update on every render)
resetScanRef.current = resetScan;
@@ -1447,10 +1517,19 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
const scannedItemId = qrData.itemId;
const scannedStockInLineId = qrData.stockInLineId;
// ✅ Check if this combination was already processed
// ✅ Skip only when this lot was already processed AND no other active line still needs it.
// Same item + same stockInLineId across different pick-order lines must be re-scannable.
const duplicateCheckStartTime = performance.now();
const itemProcessedSet = processedQrCombinations.get(scannedItemId);
if (itemProcessedSet?.has(scannedStockInLineId)) {
const activeSuggestedLotsEarly =
indexes.activeLotsByItemId.get(scannedItemId) || [];
if (
itemProcessedSet?.has(scannedStockInLineId) &&
!activeLotsStillNeedStockInLine(
activeSuggestedLotsEarly,
scannedStockInLineId,
)
) {
const duplicateCheckTime = performance.now() - duplicateCheckStartTime;
console.log(` [SKIP] Already processed combination: itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId} (check time: ${duplicateCheckTime.toFixed(2)}ms)`);
return;
@@ -1460,7 +1539,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
// ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed)
const lookupStartTime = performance.now();
const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || [];
const activeSuggestedLots = activeSuggestedLotsEarly;
// ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected
const allLotsForItem = indexes.byItemId.get(scannedItemId) || [];
const lookupTime = performance.now() - lookupStartTime;
@@ -2007,7 +2086,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
// Workbench 策略:不彈窗,直接切換到掃到的批次並提交一次掃描
const mismatchCheckStartTime = performance.now();
const itemProcessedSet2 = processedQrCombinations.get(scannedItemId);
if (itemProcessedSet2?.has(scannedStockInLineId)) {
if (
itemProcessedSet2?.has(scannedStockInLineId) &&
!activeLotsStillNeedStockInLine(activeSuggestedLots, scannedStockInLineId)
) {
const mismatchCheckTime = performance.now() - mismatchCheckStartTime;
console.log(
` [SKIP] Already processed this exact combination (check time: ${mismatchCheckTime.toFixed(2)}ms)`,
@@ -2272,6 +2354,11 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
// ✅ Process immediately (bypass QR scanner delay)
if (processOutsideQrCodeRef.current) {
if (qrProcessInFlightRef.current) {
console.log(` [TEST QR] Skipping — scan-pick already in flight`);
return;
}
qrProcessInFlightRef.current = true;
processOutsideQrCodeRef.current(simulatedQr, qrValues.length).then(() => {
const testTime = performance.now() - testStartTime;
console.log(` [TEST QR] Total processing time: ${testTime.toFixed(2)}ms (${(testTime / 1000).toFixed(3)}s)`);
@@ -2279,6 +2366,27 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
}).catch((error) => {
const testTime = performance.now() - testStartTime;
console.error(`❌ [TEST QR] Error after ${testTime.toFixed(2)}ms:`, error);
}).finally(async () => {
await new Promise((r) => setTimeout(r, 50));
try {
const activeForItem =
lotDataIndexesRef.current?.activeLotsByItemId.get(itemId);
if (
activeLotsStillNeedStockInLine(activeForItem, stockInLineId)
) {
processedQrCodesRef.current.delete(latestQr);
if (lastProcessedQrRef.current === latestQr) {
lastProcessedQrRef.current = "";
}
setProcessedQrCodes(new Set(processedQrCodesRef.current));
setLastProcessedQr("");
console.log(
` [TEST QR] Cleared processed test QR so same lot can be scanned for remaining line(s)`,
);
}
} finally {
qrProcessInFlightRef.current = false;
}
});
}
@@ -2315,11 +2423,30 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
console.log(` [QR DETECTION] Detection time: ${new Date().toISOString()}`);
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)
// Skip if already processed — unless another active line still needs the same lot (cross-POL same lot).
const checkProcessedStartTime = performance.now();
if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) {
const checkTime = performance.now() - checkProcessedStartTime;
console.log(` [QR PROCESS] Already processed check time: ${checkTime.toFixed(2)}ms`);
const alreadySeenQr =
processedQrCodesRef.current.has(latestQr) ||
lastProcessedQrRef.current === latestQr;
if (alreadySeenQr) {
const payload = parseWorkbenchScanQrPayload(latestQr);
const activeForItem = payload
? lotDataIndexesRef.current?.activeLotsByItemId.get(payload.itemId)
: undefined;
const allowSameLotRescan =
!!payload &&
activeLotsStillNeedStockInLine(activeForItem, payload.stockInLineId);
if (!allowSameLotRescan) {
const checkTime = performance.now() - checkProcessedStartTime;
console.log(` [QR PROCESS] Already processed check time: ${checkTime.toFixed(2)}ms`);
return;
}
console.log(
` [QR PROCESS] Allowing re-scan of same lot QR for remaining active pick line(s)`,
);
}
if (qrProcessInFlightRef.current) {
console.log(` [QR PROCESS] Skipping — scan-pick already in flight`);
return;
}
const checkTime = performance.now() - checkProcessedStartTime;
@@ -2389,17 +2516,46 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
// Use ref to avoid dependency issues
const processCallStartTime = performance.now();
if (processOutsideQrCodeRef.current) {
processOutsideQrCodeRef.current(latestQr, qrValues.length).then(() => {
const processCallTime = performance.now() - processCallStartTime;
const totalProcessingTime = performance.now() - processingStartTime;
console.log(` [QR PROCESS] processOutsideQrCode call time: ${processCallTime.toFixed(2)}ms`);
console.log(` [QR PROCESS] Total processing time: ${totalProcessingTime.toFixed(2)}ms (${(totalProcessingTime / 1000).toFixed(3)}s)`);
}).catch((error) => {
const processCallTime = performance.now() - processCallStartTime;
const totalProcessingTime = performance.now() - processingStartTime;
console.error(`❌ [QR PROCESS] processOutsideQrCode error after ${processCallTime.toFixed(2)}ms:`, error);
console.error(`❌ [QR PROCESS] Total processing time before error: ${totalProcessingTime.toFixed(2)}ms`);
});
qrProcessInFlightRef.current = true;
processOutsideQrCodeRef.current(latestQr, qrValues.length)
.then(() => {
const processCallTime = performance.now() - processCallStartTime;
const totalProcessingTime = performance.now() - processingStartTime;
console.log(` [QR PROCESS] processOutsideQrCode call time: ${processCallTime.toFixed(2)}ms`);
console.log(` [QR PROCESS] Total processing time: ${totalProcessingTime.toFixed(2)}ms (${(totalProcessingTime / 1000).toFixed(3)}s)`);
})
.catch((error) => {
const processCallTime = performance.now() - processCallStartTime;
const totalProcessingTime = performance.now() - processingStartTime;
console.error(`❌ [QR PROCESS] processOutsideQrCode error after ${processCallTime.toFixed(2)}ms:`, error);
console.error(`❌ [QR PROCESS] Total processing time before error: ${totalProcessingTime.toFixed(2)}ms`);
})
.finally(async () => {
// Wait a tick so refresh/local status updates commit into lotDataIndexesRef.
await new Promise((r) => setTimeout(r, 50));
try {
const payload = parseWorkbenchScanQrPayload(latestQr);
const activeForItem = payload
? lotDataIndexesRef.current?.activeLotsByItemId.get(payload.itemId)
: undefined;
if (
payload &&
activeLotsStillNeedStockInLine(activeForItem, payload.stockInLineId)
) {
processedQrCodesRef.current.delete(latestQr);
if (lastProcessedQrRef.current === latestQr) {
lastProcessedQrRef.current = "";
}
setProcessedQrCodes(new Set(processedQrCodesRef.current));
setLastProcessedQr("");
console.log(
` [QR PROCESS] Cleared processed QR so same lot can be scanned for remaining line(s)`,
);
}
} finally {
qrProcessInFlightRef.current = false;
}
});
}
// Update state for UI (but don't block on it)
@@ -3053,6 +3209,10 @@ const handleStartScan = useCallback(() => {
setProcessedQrCodes(new Set());
setLastProcessedQr('');
setProcessedQrCombinations(new Map());
processedQrCodesRef.current = new Set();
lastProcessedQrRef.current = '';
qrProcessInFlightRef.current = false;
setQrScanError(false);
setQrScanSuccess(false);
@@ -3742,16 +3902,22 @@ paginatedData.map((row, index) => {
const solSt = String(lot.stockOutLineStatus || "").toLowerCase();
const isSolRejected =
solSt === "rejected" || String(lot.lotAvailability || "").toLowerCase() === "rejected";
const groupRowBg =
row.groupDisplayIndex % 2 === 0 ? "action.hover" : "background.paper";
const nextRow = paginatedData[index + 1];
const isLastInGroup =
!nextRow || nextRow.groupDisplayIndex !== row.groupDisplayIndex;

return (
<TableRow
key={`${lot.pickOrderLineId}-${lot.lotId || 'null'}`}
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,
...(!isLastInGroup ? { borderBottom: "none" } : {}),
},
}}
>
<TableCell>
@@ -4003,58 +4169,56 @@ paginatedData.map((row, index) => {
? String(pickQtyData[lotKey])
: String(displayedSubmitQty)
: String(displayedSubmitQty);
const pickStatus = String(lot.stockOutLineStatus ?? "").toLowerCase();
const isRowPicked =
pickStatus === "completed" ||
pickStatus === "checked" ||
pickStatus === "partially_completed" ||
pickStatus === "partially_complete";

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' },
}}
/>
<Stack direction="row" spacing={1} alignItems="center" justifyContent="center">
{isRowPicked ? (
<Typography
variant="body2"
sx={{
width: 96,
textAlign: "center",
fontSize: "1rem",
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" },
}}
/>
)}

<Button
variant="outlined"


Carregando…
Cancelar
Guardar