Browse Source

update

MergeProblem1
CANCERYS\kw093 22 hours ago
parent
commit
cdad533861
4 changed files with 314 additions and 115 deletions
  1. +106
    -28
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  2. +1
    -0
      src/components/Jodetail/LotConfirmationModal.tsx
  3. +206
    -87
      src/components/Jodetail/newJobPickExecution.tsx
  4. +1
    -0
      src/i18n/zh/pickOrder.json

+ 106
- 28
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx View File

@@ -653,6 +653,9 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
>(null); >(null);
const resetScanRef = useRef<(() => void) | null>(null); const resetScanRef = useRef<(() => void) | null>(null);
const lotConfirmOpenedQrCountRef = useRef<number>(0); const lotConfirmOpenedQrCountRef = useRef<number>(0);
const lotConfirmLastQrRef = useRef<string>('');
const lotConfirmSkipNextScanRef = useRef<boolean>(false);
const lotConfirmOpenedAtRef = useRef<number>(0);
@@ -666,11 +669,11 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
return { completed: 0, total: 0 }; return { completed: 0, total: 0 };
} }


// 與 allItemsReady 一致:noLot / 過期批號 的 pending 也算「已面對該行」可收尾
// 與 allItemsReady 一致:noLot / 過期 / unavailable 的 pending 也算「已面對該行」可收尾
const nonPendingCount = combinedLotData.filter((lot) => { const nonPendingCount = combinedLotData.filter((lot) => {
const status = lot.stockOutLineStatus?.toLowerCase(); const status = lot.stockOutLineStatus?.toLowerCase();
if (status !== "pending") return true; if (status !== "pending") return true;
if (lot.noLot === true || isLotAvailabilityExpired(lot)) return true;
if (lot.noLot === true || isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot)) return true;
return false; return false;
}).length; }).length;


@@ -749,6 +752,9 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
...scannedLot, ...scannedLot,
lotNo: scannedLot.lotNo || null, lotNo: scannedLot.lotNo || null,
}); });
// The QR that opened modal must NOT be treated as confirmation rescan.
lotConfirmSkipNextScanRef.current = true;
lotConfirmOpenedAtRef.current = Date.now();
setLotConfirmationOpen(true); setLotConfirmationOpen(true);
const setStateTime = performance.now() - setStateStartTime; const setStateTime = performance.now() - setStateStartTime;
console.timeEnd('setLotConfirmationOpen'); console.timeEnd('setLotConfirmationOpen');
@@ -1262,6 +1268,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
setExpectedLotData(null); setExpectedLotData(null);
setScannedLotData(null); setScannedLotData(null);
setSelectedLotForQr(null); setSelectedLotForQr(null);
lotConfirmLastQrRef.current = '';
lotConfirmSkipNextScanRef.current = false;
lotConfirmOpenedAtRef.current = 0;


if (clearProcessedRefs) { if (clearProcessedRefs) {
setTimeout(() => { setTimeout(() => {
@@ -1305,14 +1314,15 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
} }
}, []); }, []);


const handleLotConfirmation = useCallback(async () => {
if (!expectedLotData || !scannedLotData || !selectedLotForQr) return;
const handleLotConfirmation = useCallback(async (overrideScannedLot?: any) => {
const effectiveScannedLot = overrideScannedLot ?? scannedLotData;
if (!expectedLotData || !effectiveScannedLot || !selectedLotForQr) return;
setIsConfirmingLot(true); setIsConfirmingLot(true);
setLotConfirmationError(null); setLotConfirmationError(null);
try { try {
const newLotNo = scannedLotData?.lotNo;
const newLotNo = effectiveScannedLot?.lotNo;
const newStockInLineId = scannedLotData?.stockInLineId;
const newStockInLineId = effectiveScannedLot?.stockInLineId;
const substitutionResult = await confirmLotSubstitution({ const substitutionResult = await confirmLotSubstitution({
pickOrderLineId: selectedLotForQr.pickOrderLineId, pickOrderLineId: selectedLotForQr.pickOrderLineId,
@@ -1322,7 +1332,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
newStockInLineId: newStockInLineId newStockInLineId: newStockInLineId
}); });


if (!substitutionResult || substitutionResult.code !== "SUCCESS") {
const substitutionCode = substitutionResult?.code;
const switchedToUnavailable =
substitutionCode === "SUCCESS_UNAVAILABLE" || substitutionCode === "BOUND_UNAVAILABLE";
if (!substitutionResult || (substitutionCode !== "SUCCESS" && !switchedToUnavailable)) {
const errMsg = const errMsg =
substitutionResult?.code === "LOT_UNAVAILABLE" substitutionResult?.code === "LOT_UNAVAILABLE"
? t( ? t(
@@ -1350,7 +1363,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
// setLastProcessedQr(''); // setLastProcessedQr('');
setPickExecutionFormOpen(false); setPickExecutionFormOpen(false);
if(selectedLotForQr?.stockOutLineId){
if (selectedLotForQr?.stockOutLineId && !switchedToUnavailable) {
const stockOutLineUpdate = await updateStockOutLineStatus({ const stockOutLineUpdate = await updateStockOutLineStatus({
id: selectedLotForQr.stockOutLineId, id: selectedLotForQr.stockOutLineId,
status: 'checked', status: 'checked',
@@ -1408,6 +1421,17 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
} }
return true; return true;
} }

// 扫到第三个 lot(既不是当前差异 lot,也不是原建议 lot):
// 直接按“扫描到的这一批”执行切换。
await handleLotConfirmation({
lotNo: null,
itemCode: expectedLotData?.itemCode,
itemName: expectedLotData?.itemName,
inventoryLotLineId: null,
stockInLineId: rescannedStockInLineId
});
return true;
} else { } else {
// 兼容纯 lotNo 文本扫码 // 兼容纯 lotNo 文本扫码
const scannedText = rawQr?.trim(); const scannedText = rawQr?.trim();
@@ -1432,7 +1456,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
} }


return false; return false;
}, [lotConfirmationOpen, selectedLotForQr, expectedLotData, scannedLotData, parseQrPayload, handleLotConfirmation, clearLotConfirmationState]);
}, [lotConfirmationOpen, selectedLotForQr, expectedLotData, scannedLotData, parseQrPayload, handleLotConfirmation, clearLotConfirmationState, handleLotMismatch]);


const handleQrCodeSubmit = useCallback(async (lotNo: string) => { const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
console.log(` Processing QR Code for lot: ${lotNo}`); console.log(` Processing QR Code for lot: ${lotNo}`);
@@ -1689,9 +1713,19 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
// ✅ Use for loop instead of forEach for better performance on tablets // ✅ Use for loop instead of forEach for better performance on tablets
for (let i = 0; i < combinedLotData.length; i++) { for (let i = 0; i < combinedLotData.length; i++) {
const lot = combinedLotData[i]; const lot = combinedLotData[i];
const isActive = !rejectedStatuses.has(lot.lotAvailability) &&
!rejectedStatuses.has(lot.stockOutLineStatus) &&
!rejectedStatuses.has(lot.processingStatus);
const solStatus = String(lot.stockOutLineStatus || "").toLowerCase();
const lotAvailability = String(lot.lotAvailability || "").toLowerCase();
const processingStatus = String(lot.processingStatus || "").toLowerCase();
const isUnavailable = isInventoryLotLineUnavailable(lot);
const isExpired = isLotAvailabilityExpired(lot);
const isRejected =
rejectedStatuses.has(lotAvailability) ||
rejectedStatuses.has(solStatus) ||
rejectedStatuses.has(processingStatus);
const isEnded = solStatus === "checked" || solStatus === "completed";
const isPartially = solStatus === "partially_completed" || solStatus === "partially_complete";
const isPending = solStatus === "pending" || solStatus === "";
const isActive = !isRejected && !isUnavailable && !isExpired && !isEnded && (isPending || isPartially);
if (lot.itemId) { if (lot.itemId) {
if (!byItemId.has(lot.itemId)) { if (!byItemId.has(lot.itemId)) {
@@ -2228,9 +2262,22 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
if (isConfirmingLot) { if (isConfirmingLot) {
return; return;
} }
if (qrValues.length <= lotConfirmOpenedQrCountRef.current) {
if (lotConfirmSkipNextScanRef.current) {
lotConfirmSkipNextScanRef.current = false;
lotConfirmLastQrRef.current = latestQr || '';
return;
}
if (!latestQr) {
return;
}
// Prevent auto-accept from buffered duplicate right after modal opens,
// but allow intentional second scan of the same QR after debounce window.
const sameQr = latestQr === lotConfirmLastQrRef.current;
const justOpened = lotConfirmOpenedAtRef.current > 0 && (Date.now() - lotConfirmOpenedAtRef.current) < 800;
if (sameQr && justOpened) {
return; return;
} }
lotConfirmLastQrRef.current = latestQr;
void (async () => { void (async () => {
try { try {
const handled = await handleLotConfirmationByRescan(latestQr); const handled = await handleLotConfirmationByRescan(latestQr);
@@ -2538,6 +2585,9 @@ useEffect(() => {
if (lot.noLot === true) { if (lot.noLot === true) {
return 0; return 0;
} }
if (isInventoryLotLineUnavailable(lot)) {
return 0;
}
if (isLotAvailabilityExpired(lot)) { if (isLotAvailabilityExpired(lot)) {
return 0; return 0;
} }
@@ -2721,7 +2771,7 @@ const allItemsReady = useMemo(() => {


// ✅ FIXED: 无库存(noLot)行:pending 状态也应该被视为 ready(可以提交) // ✅ FIXED: 无库存(noLot)行:pending 状态也应该被视为 ready(可以提交)
// ✅ 過期批號(未換批):與 noLot 相同,視為可收尾 // ✅ 過期批號(未換批):與 noLot 相同,視為可收尾
if (lot.noLot === true || isLotAvailabilityExpired(lot)) {
if (lot.noLot === true || isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot)) {
return isChecked || isCompleted || isRejected || isPending; return isChecked || isCompleted || isRejected || isPending;
} }


@@ -2742,8 +2792,10 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
try { try {
if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true })); if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true }));
const targetUnavailable = isInventoryLotLineUnavailable(lot);
const effectiveSubmitQty = targetUnavailable && submitQty > 0 ? 0 : submitQty;
// Just Complete: mark checked only, real posting happens in batch submit // Just Complete: mark checked only, real posting happens in batch submit
if (submitQty === 0 && source === 'justComplete') {
if (effectiveSubmitQty === 0 && source === 'justComplete') {
console.log(`=== SUBMITTING ALL ZEROS CASE ===`); console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
console.log(`Lot: ${lot.lotNo}`); console.log(`Lot: ${lot.lotNo}`);
console.log(`Stock Out Line ID: ${lot.stockOutLineId}`); console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
@@ -2778,7 +2830,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
return; return;
} }
if (submitQty === 0 && source === 'singleSubmit') {
if (effectiveSubmitQty === 0 && source === 'singleSubmit') {
console.log(`=== SUBMITTING ALL ZEROS CASE ===`); console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
console.log(`Lot: ${lot.lotNo}`); console.log(`Lot: ${lot.lotNo}`);
console.log(`Stock Out Line ID: ${lot.stockOutLineId}`); console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
@@ -2815,7 +2867,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
} }
// FIXED: Calculate cumulative quantity correctly // FIXED: Calculate cumulative quantity correctly
const currentActualPickQty = lot.actualPickQty || 0; const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty;
const cumulativeQty = currentActualPickQty + effectiveSubmitQty;
// FIXED: Determine status based on cumulative quantity vs required quantity // FIXED: Determine status based on cumulative quantity vs required quantity
let newStatus = 'partially_completed'; let newStatus = 'partially_completed';
@@ -2832,7 +2884,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
console.log(`Lot: ${lot.lotNo}`); console.log(`Lot: ${lot.lotNo}`);
console.log(`Required Qty: ${lot.requiredQty}`); console.log(`Required Qty: ${lot.requiredQty}`);
console.log(`Current Actual Pick Qty: ${currentActualPickQty}`); console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
console.log(`New Submitted Qty: ${submitQty}`);
console.log(`New Submitted Qty: ${effectiveSubmitQty}`);
console.log(`Cumulative Qty: ${cumulativeQty}`); console.log(`Cumulative Qty: ${cumulativeQty}`);
console.log(`New Status: ${newStatus}`); console.log(`New Status: ${newStatus}`);
console.log(`=====================================`); console.log(`=====================================`);
@@ -2841,7 +2893,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
id: lot.stockOutLineId, id: lot.stockOutLineId,
status: newStatus, status: newStatus,
// 后端 updateStatus 的 qty 是“增量 delta”,不能传 cumulativeQty(否则会重复累加导致 out/hold 大幅偏移) // 后端 updateStatus 的 qty 是“增量 delta”,不能传 cumulativeQty(否则会重复累加导致 out/hold 大幅偏移)
qty: submitQty
qty: effectiveSubmitQty
}); });
applyLocalStockOutLineUpdate(Number(lot.stockOutLineId), newStatus, cumulativeQty); applyLocalStockOutLineUpdate(Number(lot.stockOutLineId), newStatus, cumulativeQty);
// 注意:库存过账(hold->out)与 ledger 由后端 updateStatus 内部统一处理; // 注意:库存过账(hold->out)与 ledger 由后端 updateStatus 内部统一处理;
@@ -3112,7 +3164,7 @@ const handleSubmitAllScanned = useCallback(async () => {
return false; return false;
} }
// ✅ noLot / 過期批號:與 noLot 相同,允許 pending(未換批也可批量收尾) // ✅ noLot / 過期批號:與 noLot 相同,允許 pending(未換批也可批量收尾)
if (lot.noLot === true || isLotAvailabilityExpired(lot)) {
if (lot.noLot === true || isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot)) {
return ( return (
status === "checked" || status === "checked" ||
status === "pending" || status === "pending" ||
@@ -3155,12 +3207,16 @@ const handleSubmitAllScanned = useCallback(async () => {
lot.stockOutLineStatus === "partially_completed" || issuePicked !== undefined; lot.stockOutLineStatus === "partially_completed" || issuePicked !== undefined;


const expired = isLotAvailabilityExpired(lot); const expired = isLotAvailabilityExpired(lot);
const unavailable = isInventoryLotLineUnavailable(lot);


let targetActual: number; let targetActual: number;
let newStatus: string; let newStatus: string;


// ✅ 過期且未在 Issue 填實際量:與 noLot 一樣按 0 完成 // ✅ 過期且未在 Issue 填實際量:與 noLot 一樣按 0 完成
if (expired && issuePicked === undefined) {
if (unavailable) {
targetActual = currentActualPickQty;
newStatus = "completed";
} else if (expired && issuePicked === undefined) {
targetActual = 0; targetActual = 0;
newStatus = "completed"; newStatus = "completed";
} else if (onlyComplete) { } else if (onlyComplete) {
@@ -3253,7 +3309,7 @@ const handleSubmitAllScanned = useCallback(async () => {
return false; return false;
} }
// ✅ 与 handleSubmitAllScanned 完全保持一致 // ✅ 与 handleSubmitAllScanned 完全保持一致
if (lot.noLot === true || isLotAvailabilityExpired(lot)) {
if (lot.noLot === true || isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot)) {
return ( return (
status === "checked" || status === "checked" ||
status === "pending" || status === "pending" ||
@@ -3581,13 +3637,20 @@ paginatedData.map((lot, index) => {
<Typography <Typography
sx={{ sx={{
color: color:
lot.lotAvailability === 'expired'
isInventoryLotLineUnavailable(lot)
? 'error.main'
: lot.lotAvailability === 'expired'
? 'warning.main' ? 'warning.main'
: /* isIssueLot ? 'warning.main' : lot.lotAvailability === 'rejected' ? 'text.disabled' : */ 'inherit',
: 'inherit',
}} }}
> >
{lot.lotNo ? ( {lot.lotNo ? (
lot.lotAvailability === 'expired' ? (
isInventoryLotLineUnavailable(lot) ? (
<>
{lot.lotNo}{' '}
{t('is unavable. Please check around have available QR code or not.')}
</>
) : lot.lotAvailability === 'expired' ? (
<> <>
{lot.lotNo}{' '} {lot.lotNo}{' '}
{t('is expired. Please check around have available QR code or not.')} {t('is expired. Please check around have available QR code or not.')}
@@ -3695,13 +3758,16 @@ paginatedData.map((lot, index) => {
return null; return null;
})()} })()}
</TableCell> </TableCell>
<TableCell align="center">{resolveSingleSubmitQty(lot)}</TableCell>
<TableCell align="center">
{isInventoryLotLineUnavailable(lot) ? 0 : resolveSingleSubmitQty(lot)}
</TableCell>
<TableCell align="center"> <TableCell align="center">
<Box sx={{ display: 'flex', justifyContent: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'center' }}>
{(() => { {(() => {
const status = lot.stockOutLineStatus?.toLowerCase(); const status = lot.stockOutLineStatus?.toLowerCase();
const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected'; const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
const isNoLot = !lot.lotNo; const isNoLot = !lot.lotNo;
const isUnavailableLot = isInventoryLotLineUnavailable(lot);
// ✅ rejected lot:显示提示文本(换行显示) // ✅ rejected lot:显示提示文本(换行显示)
if (isRejected && !isNoLot) { if (isRejected && !isNoLot) {
@@ -3806,12 +3872,14 @@ paginatedData.map((lot, index) => {
variant="outlined" variant="outlined"
size="small" size="small"
onClick={() => handleSkip(lot)} onClick={() => handleSkip(lot)}
title={isUnavailableLot ? t('is unavable. Please check around have available QR code or not.') : undefined}
disabled={ disabled={
lot.stockOutLineStatus === 'completed' || lot.stockOutLineStatus === 'completed' ||
lot.stockOutLineStatus === 'checked' || lot.stockOutLineStatus === 'checked' ||
lot.stockOutLineStatus === 'partially_completed' || lot.stockOutLineStatus === 'partially_completed' ||
lot.lotAvailability === 'expired' || lot.lotAvailability === 'expired' ||
isUnavailableLot ||


// 使用 issue form 後,禁用「Just Completed」(避免再次点击造成重复提交) // 使用 issue form 後,禁用「Just Completed」(避免再次点击造成重复提交)
(Number(lot.stockOutLineId) > 0 && issuePickedQtyBySolId[Number(lot.stockOutLineId)] !== undefined) || (Number(lot.stockOutLineId) > 0 && issuePickedQtyBySolId[Number(lot.stockOutLineId)] !== undefined) ||
@@ -3868,8 +3936,18 @@ paginatedData.map((lot, index) => {
<LotConfirmationModal <LotConfirmationModal
open={lotConfirmationOpen} open={lotConfirmationOpen}
onClose={() => { onClose={() => {
console.log(` [LOT CONFIRM MODAL] Closing modal, clearing state`);
clearLotConfirmationState(true);
console.log(` [LOT CONFIRM MODAL] Closing modal, reset scanner and release raw-QR dedupe`);
// 1) Reset scanner buffer first to avoid immediate reopen from buffered same QR.
if (resetScanRef.current) {
resetScanRef.current();
}
// 2) Close modal state.
clearLotConfirmationState(false);
// 3) Release raw-QR dedupe after a short delay so user can re-scan B/C again.
setTimeout(() => {
lastProcessedQrRef.current = '';
processedQrCodesRef.current.clear();
}, 250);
}} }}
onConfirm={handleLotConfirmation} onConfirm={handleLotConfirmation}
expectedLot={expectedLotData} expectedLot={expectedLotData}


+ 1
- 0
src/components/Jodetail/LotConfirmationModal.tsx View File

@@ -112,6 +112,7 @@ const LotConfirmationModal: React.FC<LotConfirmationModalProps> = ({
{t("If you confirm, the system will:")} {t("If you confirm, the system will:")}
<ul style={{ margin: '8px 0 0 16px' }}> <ul style={{ margin: '8px 0 0 16px' }}>
<li>{t("Update your suggested lot to the this scanned lot")}</li> <li>{t("Update your suggested lot to the this scanned lot")}</li>
<li>{t("You can also scan expected lot again to cancel this switch")}</li>
</ul> </ul>
</Alert> </Alert>
</Stack> </Stack>


+ 206
- 87
src/components/Jodetail/newJobPickExecution.tsx View File

@@ -519,6 +519,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const [usernameList, setUsernameList] = useState<NameList[]>([]); const [usernameList, setUsernameList] = useState<NameList[]>([]);


const initializationRef = useRef(false); const initializationRef = useRef(false);
const scannerInitializedRef = useRef(false);
const autoAssignRef = useRef(false); const autoAssignRef = useRef(false);


const formProps = useForm(); const formProps = useForm();
@@ -559,6 +560,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
// Store callbacks in refs to avoid useEffect dependency issues // Store callbacks in refs to avoid useEffect dependency issues
const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | null>(null); const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | null>(null);
const resetScanRef = useRef<(() => void) | null>(null); const resetScanRef = useRef<(() => void) | null>(null);
const lotConfirmLastQrRef = useRef<string>('');
const lotConfirmSkipNextScanRef = useRef<boolean>(false);
const lotConfirmOpenedAtRef = useRef<number>(0);


// Manual lot confirmation modal state (test shortcut {2fic}) // Manual lot confirmation modal state (test shortcut {2fic})
const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false); const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false);
@@ -726,11 +730,19 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {


for (let i = 0; i < combinedLotData.length; i++) { for (let i = 0; i < combinedLotData.length; i++) {
const lot = combinedLotData[i]; const lot = combinedLotData[i];
const isActive =
!rejectedStatuses.has(lot.lotAvailability) &&
!rejectedStatuses.has(lot.stockOutLineStatus) &&
!rejectedStatuses.has(lot.processingStatus) &&
lot.stockOutLineStatus !== 'completed';
const solStatus = String(lot.stockOutLineStatus || "").toLowerCase();
const lotAvailability = String(lot.lotAvailability || "").toLowerCase();
const processingStatus = String(lot.processingStatus || "").toLowerCase();
const isUnavailable = isInventoryLotLineUnavailable(lot);
const isExpired = isLotAvailabilityExpired(lot);
const isRejected =
rejectedStatuses.has(lotAvailability) ||
rejectedStatuses.has(solStatus) ||
rejectedStatuses.has(processingStatus);
const isEnded = solStatus === "checked" || solStatus === "completed";
const isPartially = solStatus === "partially_completed" || solStatus === "partially_complete";
const isPending = solStatus === "pending" || solStatus === "";
const isActive = !isRejected && !isUnavailable && !isExpired && !isEnded && (isPending || isPartially);


if (lot.itemId) { if (lot.itemId) {
if (!byItemId.has(lot.itemId)) { if (!byItemId.has(lot.itemId)) {
@@ -926,6 +938,23 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
} }
}, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders, filterArgs?.pickOrderId]); }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders, filterArgs?.pickOrderId]);


// 與 GoodPickExecutiondetail 一致:session 就緒後自動開啟背景掃碼(平板現場用)
useEffect(() => {
if (session && currentUserId && !scannerInitializedRef.current) {
scannerInitializedRef.current = true;
console.log("✅ [JO] Auto-starting QR scanner in background mode");
setIsManualScanning(true);
startScan();
}
}, [session, currentUserId, startScan]);

// 僅在元件卸載時重置,讓 React Strict Mode 二次掛載仍能再走一次自動開掃(不因 startScan 引用變化而重複開掃)
useEffect(() => {
return () => {
scannerInitializedRef.current = false;
};
}, []);

// Add event listener for manual assignment // Add event listener for manual assignment
useEffect(() => { useEffect(() => {
const handlePickOrderAssigned = () => { const handlePickOrderAssigned = () => {
@@ -1085,6 +1114,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
...scannedLot, ...scannedLot,
lotNo: scannedLot.lotNo || null, lotNo: scannedLot.lotNo || null,
}); });
lotConfirmSkipNextScanRef.current = true;
lotConfirmOpenedAtRef.current = Date.now();
setLotConfirmationOpen(true); setLotConfirmationOpen(true);
console.log("⚠️ [LOT MISMATCH] Modal opened - waiting for user confirmation"); console.log("⚠️ [LOT MISMATCH] Modal opened - waiting for user confirmation");
}, 0); }, 0);
@@ -1110,9 +1141,28 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
} }
}, [fetchStockInLineInfoCached]); }, [fetchStockInLineInfoCached]);


const clearLotConfirmationState = useCallback((clearProcessedRefs: boolean = false) => {
setLotConfirmationOpen(false);
setLotConfirmationError(null);
setExpectedLotData(null);
setScannedLotData(null);
setSelectedLotForQr(null);
lotConfirmLastQrRef.current = '';
lotConfirmSkipNextScanRef.current = false;
lotConfirmOpenedAtRef.current = 0;

if (clearProcessedRefs) {
setTimeout(() => {
lastProcessedQrRef.current = '';
processedQrCodesRef.current.clear();
}, 100);
}
}, []);

// Add handleLotConfirmation function // Add handleLotConfirmation function
const handleLotConfirmation = useCallback(async () => {
if (!expectedLotData || !scannedLotData || !selectedLotForQr) {
const handleLotConfirmation = useCallback(async (overrideScannedLot?: any) => {
const effectiveScannedLot = overrideScannedLot ?? scannedLotData;
if (!expectedLotData || !effectiveScannedLot || !selectedLotForQr) {
console.error("❌ [LOT CONFIRM] Missing required data for lot confirmation"); console.error("❌ [LOT CONFIRM] Missing required data for lot confirmation");
return; return;
} }
@@ -1125,8 +1175,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
setIsConfirmingLot(true); setIsConfirmingLot(true);
setLotConfirmationError(null); setLotConfirmationError(null);
try { try {
let newLotLineId = scannedLotData?.inventoryLotLineId;
if (!newLotLineId && scannedLotData?.stockInLineId) {
let newLotLineId = effectiveScannedLot?.inventoryLotLineId;
if (!newLotLineId && effectiveScannedLot?.stockInLineId) {
try { try {
if (currentUserId && selectedLotForQr.pickOrderId && selectedLotForQr.itemId) { if (currentUserId && selectedLotForQr.pickOrderId && selectedLotForQr.itemId) {
try { try {
@@ -1136,8 +1186,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
console.error(`❌ [LOT CONFIRM] Error updating handler (non-critical):`, error); console.error(`❌ [LOT CONFIRM] Error updating handler (non-critical):`, error);
} }
} }
console.log(`🔍 [LOT CONFIRM] Fetching lot detail for stockInLineId: ${scannedLotData.stockInLineId}`);
const ld = await fetchLotDetail(scannedLotData.stockInLineId);
console.log(`🔍 [LOT CONFIRM] Fetching lot detail for stockInLineId: ${effectiveScannedLot.stockInLineId}`);
const ld = await fetchLotDetail(effectiveScannedLot.stockInLineId);
newLotLineId = ld.inventoryLotLineId; newLotLineId = ld.inventoryLotLineId;
console.log(`✅ [LOT CONFIRM] Fetched lot detail: inventoryLotLineId=${newLotLineId}`); console.log(`✅ [LOT CONFIRM] Fetched lot detail: inventoryLotLineId=${newLotLineId}`);
} catch (error) { } catch (error) {
@@ -1158,12 +1208,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
console.log("Suggested Pick Lot ID:", selectedLotForQr.suggestedPickLotId); console.log("Suggested Pick Lot ID:", selectedLotForQr.suggestedPickLotId);
console.log("Lot ID (fallback):", selectedLotForQr.lotId); console.log("Lot ID (fallback):", selectedLotForQr.lotId);
console.log("New Inventory Lot Line ID:", newLotLineId); console.log("New Inventory Lot Line ID:", newLotLineId);
console.log("Scanned Lot No:", scannedLotData.lotNo);
console.log("Scanned StockInLineId:", scannedLotData.stockInLineId);
console.log("Scanned Lot No:", effectiveScannedLot.lotNo);
console.log("Scanned StockInLineId:", effectiveScannedLot.stockInLineId);


const originalSuggestedPickLotId = const originalSuggestedPickLotId =
selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId; selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId;


let switchedToUnavailable = false;
// noLot / missing suggestedPickLotId 场景:没有 originalSuggestedPickLotId,改用 updateStockOutLineStatusByQRCodeAndLotNo // noLot / missing suggestedPickLotId 场景:没有 originalSuggestedPickLotId,改用 updateStockOutLineStatusByQRCodeAndLotNo
if (!originalSuggestedPickLotId) { if (!originalSuggestedPickLotId) {
if (!selectedLotForQr?.stockOutLineId) { if (!selectedLotForQr?.stockOutLineId) {
@@ -1172,14 +1223,15 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
console.log("🔄 [LOT CONFIRM] No originalSuggestedPickLotId, using updateStockOutLineStatusByQRCodeAndLotNo..."); console.log("🔄 [LOT CONFIRM] No originalSuggestedPickLotId, using updateStockOutLineStatusByQRCodeAndLotNo...");
const res = await updateStockOutLineStatusByQRCodeAndLotNo({ const res = await updateStockOutLineStatusByQRCodeAndLotNo({
pickOrderLineId: selectedLotForQr.pickOrderLineId, pickOrderLineId: selectedLotForQr.pickOrderLineId,
inventoryLotNo: scannedLotData.lotNo || '',
stockInLineId: scannedLotData?.stockInLineId ?? null,
inventoryLotNo: effectiveScannedLot.lotNo || '',
stockInLineId: effectiveScannedLot?.stockInLineId ?? null,
stockOutLineId: selectedLotForQr.stockOutLineId, stockOutLineId: selectedLotForQr.stockOutLineId,
itemId: selectedLotForQr.itemId, itemId: selectedLotForQr.itemId,
status: "checked", status: "checked",
}); });
console.log("✅ [LOT CONFIRM] updateStockOutLineStatusByQRCodeAndLotNo result:", res); console.log("✅ [LOT CONFIRM] updateStockOutLineStatusByQRCodeAndLotNo result:", res);
const ok = res?.code === "checked" || res?.code === "SUCCESS";
switchedToUnavailable = res?.code === "BOUND_UNAVAILABLE";
const ok = res?.code === "checked" || res?.code === "SUCCESS" || switchedToUnavailable;
if (!ok) { if (!ok) {
const errMsg = const errMsg =
res?.code === "LOT_UNAVAILABLE" res?.code === "LOT_UNAVAILABLE"
@@ -1200,16 +1252,19 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
pickOrderLineId: selectedLotForQr.pickOrderLineId, pickOrderLineId: selectedLotForQr.pickOrderLineId,
stockOutLineId: selectedLotForQr.stockOutLineId, stockOutLineId: selectedLotForQr.stockOutLineId,
originalSuggestedPickLotId, originalSuggestedPickLotId,
newInventoryLotNo: scannedLotData.lotNo || '',
newInventoryLotNo: effectiveScannedLot.lotNo || '',
// ✅ required by LotSubstitutionConfirmRequest // ✅ required by LotSubstitutionConfirmRequest
newStockInLineId: scannedLotData?.stockInLineId ?? null,
newStockInLineId: effectiveScannedLot?.stockInLineId ?? null,
}); });
console.log("✅ [LOT CONFIRM] Lot substitution result:", substitutionResult); console.log("✅ [LOT CONFIRM] Lot substitution result:", substitutionResult);


// ✅ CRITICAL: substitution failed => DO NOT mark original stockOutLine as checked. // ✅ CRITICAL: substitution failed => DO NOT mark original stockOutLine as checked.
// Keep modal open so user can cancel/rescan. // Keep modal open so user can cancel/rescan.
if (!substitutionResult || substitutionResult.code !== "SUCCESS") {
switchedToUnavailable =
substitutionResult?.code === "SUCCESS_UNAVAILABLE" ||
substitutionResult?.code === "BOUND_UNAVAILABLE";
if (!substitutionResult || (substitutionResult.code !== "SUCCESS" && !switchedToUnavailable)) {
console.error("❌ [LOT CONFIRM] Lot substitution failed. Will NOT update stockOutLine status."); console.error("❌ [LOT CONFIRM] Lot substitution failed. Will NOT update stockOutLine status.");
const errMsg = const errMsg =
substitutionResult?.code === "LOT_UNAVAILABLE" substitutionResult?.code === "LOT_UNAVAILABLE"
@@ -1217,7 +1272,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
"The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated." "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated."
) )
: substitutionResult?.message || : substitutionResult?.message ||
`换批失败:stockInLineId ${scannedLotData?.stockInLineId ?? ""} 不存在或无法匹配`;
`换批失败:stockInLineId ${effectiveScannedLot?.stockInLineId ?? ""} 不存在或无法匹配`;
setLotConfirmationError(errMsg); setLotConfirmationError(errMsg);
setQrScanError(true); setQrScanError(true);
setQrScanSuccess(false); setQrScanSuccess(false);
@@ -1227,7 +1282,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
} }
// Update stock out line status to 'checked' after substitution // Update stock out line status to 'checked' after substitution
if(selectedLotForQr?.stockOutLineId){
if (selectedLotForQr?.stockOutLineId && !switchedToUnavailable) {
console.log(`🔄 [LOT CONFIRM] Updating stockOutLine ${selectedLotForQr.stockOutLineId} to 'checked'`); console.log(`🔄 [LOT CONFIRM] Updating stockOutLine ${selectedLotForQr.stockOutLineId} to 'checked'`);
await updateStockOutLineStatus({ await updateStockOutLineStatus({
id: selectedLotForQr.stockOutLineId, id: selectedLotForQr.stockOutLineId,
@@ -1238,10 +1293,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
} }
// Close modal and clean up state BEFORE refreshing // Close modal and clean up state BEFORE refreshing
setLotConfirmationOpen(false);
setExpectedLotData(null);
setScannedLotData(null);
setSelectedLotForQr(null);
clearLotConfirmationState(false);
// Clear QR processing state but DON'T clear processedQrCodes yet // Clear QR processing state but DON'T clear processedQrCodes yet
setQrScanError(false); setQrScanError(false);
@@ -1267,12 +1319,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
setQrScanSuccess(false); setQrScanSuccess(false);
setIsRefreshingData(false); setIsRefreshingData(false);
// ✅ Clear processedQrCombinations to allow reprocessing the same QR if needed // ✅ Clear processedQrCombinations to allow reprocessing the same QR if needed
if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
if (effectiveScannedLot?.stockInLineId && selectedLotForQr?.itemId) {
setProcessedQrCombinations(prev => { setProcessedQrCombinations(prev => {
const newMap = new Map(prev); const newMap = new Map(prev);
const itemId = selectedLotForQr.itemId; const itemId = selectedLotForQr.itemId;
if (itemId && newMap.has(itemId)) { if (itemId && newMap.has(itemId)) {
newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
newMap.get(itemId)!.delete(effectiveScannedLot.stockInLineId);
if (newMap.get(itemId)!.size === 0) { if (newMap.get(itemId)!.size === 0) {
newMap.delete(itemId); newMap.delete(itemId);
} }
@@ -1294,7 +1346,62 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
} finally { } finally {
setIsConfirmingLot(false); setIsConfirmingLot(false);
} }
}, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData, currentUserId, updateHandledBy, tPick]);
}, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData, currentUserId, updateHandledBy, tPick, clearLotConfirmationState]);

const handleLotConfirmationByRescan = useCallback(async (rawQr: string): Promise<boolean> => {
if (!lotConfirmationOpen || !selectedLotForQr || !expectedLotData || !scannedLotData) {
return false;
}
let payload: any = null;
try {
payload = JSON.parse(rawQr);
} catch {
payload = null;
}
const expectedStockInLineId = Number(selectedLotForQr.stockInLineId);
const mismatchedStockInLineId = Number(scannedLotData?.stockInLineId);
if (payload?.stockInLineId && payload?.itemId) {
const rescannedStockInLineId = Number(payload.stockInLineId);
if (Number.isFinite(expectedStockInLineId) && rescannedStockInLineId === expectedStockInLineId) {
clearLotConfirmationState(false);
if (processOutsideQrCodeRef.current) {
await processOutsideQrCodeRef.current(JSON.stringify(payload));
}
return true;
}
if (Number.isFinite(mismatchedStockInLineId) && rescannedStockInLineId === mismatchedStockInLineId) {
await handleLotConfirmation();
return true;
}
await handleLotConfirmation({
lotNo: null,
itemCode: expectedLotData?.itemCode,
itemName: expectedLotData?.itemName,
inventoryLotLineId: null,
stockInLineId: rescannedStockInLineId
});
return true;
} else {
const scannedText = rawQr?.trim();
const expectedLotNo = expectedLotData?.lotNo?.trim();
const mismatchedLotNo = scannedLotData?.lotNo?.trim();
if (expectedLotNo && scannedText === expectedLotNo) {
clearLotConfirmationState(false);
if (processOutsideQrCodeRef.current) {
await processOutsideQrCodeRef.current(JSON.stringify({
itemId: selectedLotForQr.itemId,
stockInLineId: selectedLotForQr.stockInLineId,
}));
}
return true;
}
if (mismatchedLotNo && scannedText === mismatchedLotNo) {
await handleLotConfirmation();
return true;
}
}
return false;
}, [lotConfirmationOpen, selectedLotForQr, expectedLotData, scannedLotData, handleLotConfirmation, clearLotConfirmationState]);


const processOutsideQrCode = useCallback(async (latestQr: string) => { const processOutsideQrCode = useCallback(async (latestQr: string) => {
// ✅ Only JSON QR supported for outside scanner (avoid false positive with lotNo) // ✅ Only JSON QR supported for outside scanner (avoid false positive with lotNo)
@@ -1676,8 +1783,33 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
return; return;
} }


// Skip processing if modal open for same QR
if (lotConfirmationOpen || manualLotConfirmationOpen) {
if (lotConfirmationOpen) {
if (isConfirmingLot) return;
if (lotConfirmSkipNextScanRef.current) {
lotConfirmSkipNextScanRef.current = false;
lotConfirmLastQrRef.current = latestQr || '';
return;
}
if (!latestQr) return;
const sameQr = latestQr === lotConfirmLastQrRef.current;
const justOpened =
lotConfirmOpenedAtRef.current > 0 && (Date.now() - lotConfirmOpenedAtRef.current) < 800;
if (sameQr && justOpened) return;
lotConfirmLastQrRef.current = latestQr;
void (async () => {
try {
const handled = await handleLotConfirmationByRescan(latestQr);
if (handled && resetScanRef.current) {
resetScanRef.current();
}
} catch (e) {
console.error("Lot confirmation rescan failed:", e);
}
})();
return;
}
// Skip processing if manual modal open for same QR
if (manualLotConfirmationOpen) {
if (latestQr === lastProcessedQrRef.current) return; if (latestQr === lastProcessedQrRef.current) return;
} }


@@ -1712,7 +1844,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
qrProcessingTimeoutRef.current = null; qrProcessingTimeoutRef.current = null;
} }
}; };
}, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]);
}, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen, handleLotConfirmationByRescan, isConfirmingLot]);


const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
if (value === '' || value === null || value === undefined) { if (value === '' || value === null || value === undefined) {
@@ -1753,6 +1885,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
if (fromPick !== undefined && fromPick !== null && !Number.isNaN(Number(fromPick))) { if (fromPick !== undefined && fromPick !== null && !Number.isNaN(Number(fromPick))) {
return Number(fromPick); return Number(fromPick);
} }
if (isInventoryLotLineUnavailable(lot)) {
return 0;
}
if (lot.noLot === true || !lot.lotId) { if (lot.noLot === true || !lot.lotId) {
return 0; return 0;
} }
@@ -1873,11 +2008,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
// Continue even if handler update fails // Continue even if handler update fails
} }
} }
const unavailableLot = isInventoryLotLineUnavailable(lot);
const effectiveSubmitQty = unavailableLot ? 0 : submitQty;
// ✅ 两步完成(与 DO 对齐): // ✅ 两步完成(与 DO 对齐):
// 1) Skip/Submit0 只把 SOL 标记为 checked(不直接 completed) // 1) Skip/Submit0 只把 SOL 标记为 checked(不直接 completed)
// 2) 之后由 batch submit 把 SOL 推到 completed(允许 0) // 2) 之后由 batch submit 把 SOL 推到 completed(允许 0)
if (submitQty === 0) {
if (effectiveSubmitQty === 0) {
console.log(`=== SUBMITTING ALL ZEROS CASE ===`); console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
console.log(`Lot: ${lot.lotNo}`); console.log(`Lot: ${lot.lotNo}`);
console.log(`Stock Out Line ID: ${lot.stockOutLineId}`); console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
@@ -1921,7 +2058,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
// Normal case: Calculate cumulative quantity correctly // Normal case: Calculate cumulative quantity correctly
const currentActualPickQty = lot.actualPickQty || 0; const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty;
const cumulativeQty = currentActualPickQty + effectiveSubmitQty;
// 短拣一次 completed(對齊 GoodPickExecutiondetail) // 短拣一次 completed(對齊 GoodPickExecutiondetail)
let newStatus = "partially_completed"; let newStatus = "partially_completed";
@@ -1937,7 +2074,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
console.log(`Lot: ${lot.lotNo}`); console.log(`Lot: ${lot.lotNo}`);
console.log(`Required Qty: ${lot.requiredQty}`); console.log(`Required Qty: ${lot.requiredQty}`);
console.log(`Current Actual Pick Qty: ${currentActualPickQty}`); console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
console.log(`New Submitted Qty: ${submitQty}`);
console.log(`New Submitted Qty: ${effectiveSubmitQty}`);
console.log(`Cumulative Qty: ${cumulativeQty}`); console.log(`Cumulative Qty: ${cumulativeQty}`);
console.log(`New Status: ${newStatus}`); console.log(`New Status: ${newStatus}`);
console.log(`=====================================`); console.log(`=====================================`);
@@ -1946,7 +2083,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
id: lot.stockOutLineId, id: lot.stockOutLineId,
status: newStatus, status: newStatus,
// 后端 updateStatus 的 qty 是“增量 delta”,不能传 cumulativeQty(否则会重复累加导致 out/hold 大幅偏移) // 后端 updateStatus 的 qty 是“增量 delta”,不能传 cumulativeQty(否则会重复累加导致 out/hold 大幅偏移)
qty: submitQty
qty: effectiveSubmitQty
}); });
if (solId > 0) { if (solId > 0) {
setIssuePickedQtyBySolId((prev) => { setIssuePickedQtyBySolId((prev) => {
@@ -2028,7 +2165,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
return false; return false;
} }
// noLot / 過期批號:允許 pending(對齊 GoodPickExecutiondetail) // noLot / 過期批號:允許 pending(對齊 GoodPickExecutiondetail)
if (lot.noLot === true || !lot.lotId || isLotAvailabilityExpired(lot)) {
if (lot.noLot === true || !lot.lotId || isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot)) {
return ( return (
status === "checked" || status === "checked" ||
status === "pending" || status === "pending" ||
@@ -2075,11 +2212,15 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const onlyComplete = const onlyComplete =
lot.stockOutLineStatus === "partially_completed" || issuePicked !== undefined; lot.stockOutLineStatus === "partially_completed" || issuePicked !== undefined;
const expired = isLotAvailabilityExpired(lot); const expired = isLotAvailabilityExpired(lot);
const unavailable = isInventoryLotLineUnavailable(lot);


let targetActual: number; let targetActual: number;
let newStatus: string; let newStatus: string;


if (expired && issuePicked === undefined) {
if (unavailable) {
targetActual = currentActualPickQty;
newStatus = "completed";
} else if (expired && issuePicked === undefined) {
targetActual = 0; targetActual = 0;
newStatus = "completed"; newStatus = "completed";
} else if (onlyComplete) { } else if (onlyComplete) {
@@ -2174,7 +2315,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
if (statusLower === "completed" || statusLower === "complete") { if (statusLower === "completed" || statusLower === "complete") {
return false; return false;
} }
if (lot.noLot === true || !lot.lotId || isLotAvailabilityExpired(lot)) {
if (lot.noLot === true || !lot.lotId || isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot)) {
return ( return (
status === "checked" || status === "checked" ||
status === "pending" || status === "pending" ||
@@ -2212,7 +2353,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const nonPendingCount = data.filter((lot) => { const nonPendingCount = data.filter((lot) => {
const status = lot.stockOutLineStatus?.toLowerCase(); const status = lot.stockOutLineStatus?.toLowerCase();
if (status !== "pending") return true; if (status !== "pending") return true;
if (lot.noLot === true || isLotAvailabilityExpired(lot)) return true;
if (lot.noLot === true || isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot)) return true;
return false; return false;
}).length; }).length;
return { completed: nonPendingCount, total: data.length }; return { completed: nonPendingCount, total: data.length };
@@ -2418,24 +2559,9 @@ const sortedData = [...sourceData].sort((a, b) => {
} }
}; };
}, [isManualScanning, stopScan, resetScan]); }, [isManualScanning, stopScan, resetScan]);
useEffect(() => {
if (isManualScanning && combinedLotData.length === 0) {
console.log(" No data available, auto-stopping QR scan...");
handleStopScan();
}
}, [combinedLotData.length, isManualScanning, handleStopScan]);
// 勿在 combinedLotData 仍為空時自動停掃:API 未回傳前會誤觸,與 GoodPickExecutiondetail(已註解掉同段)一致。
// 無資料時 qrValues effect 本來就不會處理掃碼;真正無單據可再手動按停止。


// Cleanup effect
useEffect(() => {
return () => {
// Cleanup when component unmounts (e.g., when switching tabs)
if (isManualScanning) {
console.log("🧹 Component unmounting, stopping QR scanner...");
stopScan();
resetScan();
}
};
}, [isManualScanning, stopScan, resetScan]);
const getStatusMessage = useCallback((lot: any) => { const getStatusMessage = useCallback((lot: any) => {
if (lot?.noLot === true || lot?.lotAvailability === 'insufficient_stock') { if (lot?.noLot === true || lot?.lotAvailability === 'insufficient_stock') {
return t("This order is insufficient, please pick another lot."); return t("This order is insufficient, please pick another lot.");
@@ -2641,13 +2767,22 @@ const sortedData = [...sourceData].sort((a, b) => {
<Typography <Typography
sx={{ sx={{
color: color:
lot.lotAvailability === "expired"
isInventoryLotLineUnavailable(lot)
? "error.main"
: lot.lotAvailability === "expired"
? "warning.main" ? "warning.main"
: "inherit", : "inherit",
}} }}
> >
{lot.lotNo ? ( {lot.lotNo ? (
lot.lotAvailability === "expired" ? (
isInventoryLotLineUnavailable(lot) ? (
<>
{lot.lotNo}{" "}
{t(
"is unavable. Please check around have available QR code or not.",
)}
</>
) : lot.lotAvailability === "expired" ? (
<> <>
{lot.lotNo}{" "} {lot.lotNo}{" "}
{t( {t(
@@ -2765,7 +2900,9 @@ const sortedData = [...sourceData].sort((a, b) => {
})()} })()}
</TableCell> </TableCell>


<TableCell align="center">{resolveSingleSubmitQty(lot)}</TableCell>
<TableCell align="center">
{isInventoryLotLineUnavailable(lot) ? 0 : resolveSingleSubmitQty(lot)}
</TableCell>


<TableCell align="center"> <TableCell align="center">
<Box sx={{ display: 'flex', justifyContent: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'center' }}>
@@ -2773,6 +2910,7 @@ const sortedData = [...sourceData].sort((a, b) => {
const status = lot.stockOutLineStatus?.toLowerCase(); const status = lot.stockOutLineStatus?.toLowerCase();
const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected'; const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
const isNoLot = !lot.lotNo; const isNoLot = !lot.lotNo;
const isUnavailableLot = isInventoryLotLineUnavailable(lot);
// ✅ rejected lot:显示提示文本(换行显示) // ✅ rejected lot:显示提示文本(换行显示)
if (isRejected && !isNoLot) { if (isRejected && !isNoLot) {
@@ -2894,11 +3032,13 @@ const sortedData = [...sourceData].sort((a, b) => {
lot.stockOutLineStatus === 'checked' || lot.stockOutLineStatus === 'checked' ||
lot.stockOutLineStatus === 'partially_completed' || lot.stockOutLineStatus === 'partially_completed' ||
lot.lotAvailability === 'expired' || lot.lotAvailability === 'expired' ||
isUnavailableLot ||
lot.noLot === true || lot.noLot === true ||
!lot.lotId || !lot.lotId ||
(Number(lot.stockOutLineId) > 0 && (Number(lot.stockOutLineId) > 0 &&
issuePickedQtyBySolId[Number(lot.stockOutLineId)] !== undefined) issuePickedQtyBySolId[Number(lot.stockOutLineId)] !== undefined)
} }
title={isUnavailableLot ? t("is unavable. Please check around have available QR code or not.") : undefined}
sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }} sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }}
> >
{t("Just Complete")} {t("Just Complete")}
@@ -2938,7 +3078,7 @@ const sortedData = [...sourceData].sort((a, b) => {
onClose={() => { onClose={() => {
setQrModalOpen(false); setQrModalOpen(false);
setSelectedLotForQr(null); setSelectedLotForQr(null);
stopScan();
// Keep scanner active like GoodPickExecutiondetail.
resetScan(); resetScan();
}} }}
lot={selectedLotForQr} lot={selectedLotForQr}
@@ -2951,36 +3091,15 @@ const sortedData = [...sourceData].sort((a, b) => {
<LotConfirmationModal <LotConfirmationModal
open={lotConfirmationOpen} open={lotConfirmationOpen}
onClose={() => { onClose={() => {
console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`);
setLotConfirmationOpen(false);
setLotConfirmationError(null);
setExpectedLotData(null);
setScannedLotData(null);
setSelectedLotForQr(null);
// ✅ IMPORTANT: Clear refs and processedQrCombinations to allow reprocessing the same QR code
// This allows the modal to reopen if user cancels and scans the same QR again
console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, reset scanner and release raw-QR dedupe`);
if (resetScanRef.current) {
resetScanRef.current();
}
clearLotConfirmationState(false);
setTimeout(() => { setTimeout(() => {
lastProcessedQrRef.current = ''; lastProcessedQrRef.current = '';
processedQrCodesRef.current.clear(); processedQrCodesRef.current.clear();
// Clear processedQrCombinations for this itemId+stockInLineId combination
if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
setProcessedQrCombinations(prev => {
const newMap = new Map(prev);
const itemId = selectedLotForQr.itemId;
if (itemId && newMap.has(itemId)) {
newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
if (newMap.get(itemId)!.size === 0) {
newMap.delete(itemId);
}
}
return newMap;
});
}
console.log(`⏱️ [LOT CONFIRM MODAL] Cleared refs and processedQrCombinations to allow reprocessing`);
}, 100);
}, 250);
}} }}
onConfirm={handleLotConfirmation} onConfirm={handleLotConfirmation}
expectedLot={expectedLotData} expectedLot={expectedLotData}


+ 1
- 0
src/i18n/zh/pickOrder.json View File

@@ -461,6 +461,7 @@
"No released pick order records found.": "目前沒有可用的提料單。", "No released pick order records found.": "目前沒有可用的提料單。",
"EDT - Lane Code (Unassigned/Total)": "預計出發時間 - 貨車班次(未撳數/總單數)", "EDT - Lane Code (Unassigned/Total)": "預計出發時間 - 貨車班次(未撳數/總單數)",
"The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.": "掃描的庫存批行為「不可用」,無法換批或綁定;揀貨行未更新。", "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.": "掃描的庫存批行為「不可用」,無法換批或綁定;揀貨行未更新。",
"is unavable. Please check around have available QR code or not.": "此批號不可用,請檢查周圍是否有可用的 QR 碼。",
"Lot switch failed; pick line was not marked as checked.": "換批失敗;揀貨行未標為已核對。", "Lot switch failed; pick line was not marked as checked.": "換批失敗;揀貨行未標為已核對。",
"Lot confirmation failed. Please try again.": "確認批號失敗,請重試。", "Lot confirmation failed. Please try again.": "確認批號失敗,請重試。",
"Lot status is unavailable. Cannot switch or bind; pick line was not updated.": "批號狀態為「不可用」,無法換批或綁定;揀貨行未更新。" "Lot status is unavailable. Cannot switch or bind; pick line was not updated.": "批號狀態為「不可用」,無法換批或綁定;揀貨行未更新。"

Loading…
Cancel
Save