소스 검색

update

MergeProblem1
CANCERYS\kw093 19 시간 전
부모
커밋
cdad533861
4개의 변경된 파일314개의 추가작업 그리고 115개의 파일을 삭제
  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 파일 보기

@@ -653,6 +653,9 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
>(null);
const resetScanRef = useRef<(() => void) | null>(null);
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 };
}

// 與 allItemsReady 一致:noLot / 過期批號 的 pending 也算「已面對該行」可收尾
// 與 allItemsReady 一致:noLot / 過期 / unavailable 的 pending 也算「已面對該行」可收尾
const nonPendingCount = combinedLotData.filter((lot) => {
const status = lot.stockOutLineStatus?.toLowerCase();
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;
}).length;

@@ -749,6 +752,9 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
...scannedLot,
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);
const setStateTime = performance.now() - setStateStartTime;
console.timeEnd('setLotConfirmationOpen');
@@ -1262,6 +1268,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
setExpectedLotData(null);
setScannedLotData(null);
setSelectedLotForQr(null);
lotConfirmLastQrRef.current = '';
lotConfirmSkipNextScanRef.current = false;
lotConfirmOpenedAtRef.current = 0;

if (clearProcessedRefs) {
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);
setLotConfirmationError(null);
try {
const newLotNo = scannedLotData?.lotNo;
const newLotNo = effectiveScannedLot?.lotNo;
const newStockInLineId = scannedLotData?.stockInLineId;
const newStockInLineId = effectiveScannedLot?.stockInLineId;
const substitutionResult = await confirmLotSubstitution({
pickOrderLineId: selectedLotForQr.pickOrderLineId,
@@ -1322,7 +1332,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
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 =
substitutionResult?.code === "LOT_UNAVAILABLE"
? t(
@@ -1350,7 +1363,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
// setLastProcessedQr('');
setPickExecutionFormOpen(false);
if(selectedLotForQr?.stockOutLineId){
if (selectedLotForQr?.stockOutLineId && !switchedToUnavailable) {
const stockOutLineUpdate = await updateStockOutLineStatus({
id: selectedLotForQr.stockOutLineId,
status: 'checked',
@@ -1408,6 +1421,17 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
}
return true;
}

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

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

const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
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
for (let i = 0; i < combinedLotData.length; 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 (!byItemId.has(lot.itemId)) {
@@ -2228,9 +2262,22 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
if (isConfirmingLot) {
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;
}
lotConfirmLastQrRef.current = latestQr;
void (async () => {
try {
const handled = await handleLotConfirmationByRescan(latestQr);
@@ -2538,6 +2585,9 @@ useEffect(() => {
if (lot.noLot === true) {
return 0;
}
if (isInventoryLotLineUnavailable(lot)) {
return 0;
}
if (isLotAvailabilityExpired(lot)) {
return 0;
}
@@ -2721,7 +2771,7 @@ const allItemsReady = useMemo(() => {

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

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

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

let targetActual: number;
let newStatus: string;

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

// 使用 issue form 後,禁用「Just Completed」(避免再次点击造成重复提交)
(Number(lot.stockOutLineId) > 0 && issuePickedQtyBySolId[Number(lot.stockOutLineId)] !== undefined) ||
@@ -3868,8 +3936,18 @@ paginatedData.map((lot, index) => {
<LotConfirmationModal
open={lotConfirmationOpen}
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}
expectedLot={expectedLotData}


+ 1
- 0
src/components/Jodetail/LotConfirmationModal.tsx 파일 보기

@@ -112,6 +112,7 @@ const LotConfirmationModal: React.FC<LotConfirmationModalProps> = ({
{t("If you confirm, the system will:")}
<ul style={{ margin: '8px 0 0 16px' }}>
<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>
</Alert>
</Stack>


+ 206
- 87
src/components/Jodetail/newJobPickExecution.tsx 파일 보기

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

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

const formProps = useForm();
@@ -559,6 +560,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
// Store callbacks in refs to avoid useEffect dependency issues
const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<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})
const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false);
@@ -726,11 +730,19 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {

for (let i = 0; i < combinedLotData.length; 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 (!byItemId.has(lot.itemId)) {
@@ -926,6 +938,23 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}
}, [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
useEffect(() => {
const handlePickOrderAssigned = () => {
@@ -1085,6 +1114,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
...scannedLot,
lotNo: scannedLot.lotNo || null,
});
lotConfirmSkipNextScanRef.current = true;
lotConfirmOpenedAtRef.current = Date.now();
setLotConfirmationOpen(true);
console.log("⚠️ [LOT MISMATCH] Modal opened - waiting for user confirmation");
}, 0);
@@ -1110,9 +1141,28 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}
}, [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
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");
return;
}
@@ -1125,8 +1175,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
setIsConfirmingLot(true);
setLotConfirmationError(null);
try {
let newLotLineId = scannedLotData?.inventoryLotLineId;
if (!newLotLineId && scannedLotData?.stockInLineId) {
let newLotLineId = effectiveScannedLot?.inventoryLotLineId;
if (!newLotLineId && effectiveScannedLot?.stockInLineId) {
try {
if (currentUserId && selectedLotForQr.pickOrderId && selectedLotForQr.itemId) {
try {
@@ -1136,8 +1186,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
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;
console.log(`✅ [LOT CONFIRM] Fetched lot detail: inventoryLotLineId=${newLotLineId}`);
} catch (error) {
@@ -1158,12 +1208,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
console.log("Suggested Pick Lot ID:", selectedLotForQr.suggestedPickLotId);
console.log("Lot ID (fallback):", selectedLotForQr.lotId);
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 =
selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId;

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

// ✅ CRITICAL: substitution failed => DO NOT mark original stockOutLine as checked.
// 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.");
const errMsg =
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."
)
: substitutionResult?.message ||
`换批失败:stockInLineId ${scannedLotData?.stockInLineId ?? ""} 不存在或无法匹配`;
`换批失败:stockInLineId ${effectiveScannedLot?.stockInLineId ?? ""} 不存在或无法匹配`;
setLotConfirmationError(errMsg);
setQrScanError(true);
setQrScanSuccess(false);
@@ -1227,7 +1282,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}
// 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'`);
await updateStockOutLineStatus({
id: selectedLotForQr.stockOutLineId,
@@ -1238,10 +1293,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}
// 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
setQrScanError(false);
@@ -1267,12 +1319,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
setQrScanSuccess(false);
setIsRefreshingData(false);
// ✅ Clear processedQrCombinations to allow reprocessing the same QR if needed
if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
if (effectiveScannedLot?.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);
newMap.get(itemId)!.delete(effectiveScannedLot.stockInLineId);
if (newMap.get(itemId)!.size === 0) {
newMap.delete(itemId);
}
@@ -1294,7 +1346,62 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
} finally {
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) => {
// ✅ Only JSON QR supported for outside scanner (avoid false positive with lotNo)
@@ -1676,8 +1783,33 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
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;
}

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

let targetActual: number;
let newStatus: string;

if (expired && issuePicked === undefined) {
if (unavailable) {
targetActual = currentActualPickQty;
newStatus = "completed";
} else if (expired && issuePicked === undefined) {
targetActual = 0;
newStatus = "completed";
} else if (onlyComplete) {
@@ -2174,7 +2315,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
if (statusLower === "completed" || statusLower === "complete") {
return false;
}
if (lot.noLot === true || !lot.lotId || isLotAvailabilityExpired(lot)) {
if (lot.noLot === true || !lot.lotId || isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot)) {
return (
status === "checked" ||
status === "pending" ||
@@ -2212,7 +2353,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const nonPendingCount = data.filter((lot) => {
const status = lot.stockOutLineStatus?.toLowerCase();
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;
}).length;
return { completed: nonPendingCount, total: data.length };
@@ -2418,24 +2559,9 @@ const sortedData = [...sourceData].sort((a, b) => {
}
};
}, [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) => {
if (lot?.noLot === true || lot?.lotAvailability === 'insufficient_stock') {
return t("This order is insufficient, please pick another lot.");
@@ -2641,13 +2767,22 @@ const sortedData = [...sourceData].sort((a, b) => {
<Typography
sx={{
color:
lot.lotAvailability === "expired"
isInventoryLotLineUnavailable(lot)
? "error.main"
: lot.lotAvailability === "expired"
? "warning.main"
: "inherit",
}}
>
{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}{" "}
{t(
@@ -2765,7 +2900,9 @@ const sortedData = [...sourceData].sort((a, b) => {
})()}
</TableCell>

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

<TableCell align="center">
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
@@ -2773,6 +2910,7 @@ const sortedData = [...sourceData].sort((a, b) => {
const status = lot.stockOutLineStatus?.toLowerCase();
const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
const isNoLot = !lot.lotNo;
const isUnavailableLot = isInventoryLotLineUnavailable(lot);
// ✅ rejected lot:显示提示文本(换行显示)
if (isRejected && !isNoLot) {
@@ -2894,11 +3032,13 @@ const sortedData = [...sourceData].sort((a, b) => {
lot.stockOutLineStatus === 'checked' ||
lot.stockOutLineStatus === 'partially_completed' ||
lot.lotAvailability === 'expired' ||
isUnavailableLot ||
lot.noLot === true ||
!lot.lotId ||
(Number(lot.stockOutLineId) > 0 &&
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' }}
>
{t("Just Complete")}
@@ -2938,7 +3078,7 @@ const sortedData = [...sourceData].sort((a, b) => {
onClose={() => {
setQrModalOpen(false);
setSelectedLotForQr(null);
stopScan();
// Keep scanner active like GoodPickExecutiondetail.
resetScan();
}}
lot={selectedLotForQr}
@@ -2951,36 +3091,15 @@ const sortedData = [...sourceData].sort((a, b) => {
<LotConfirmationModal
open={lotConfirmationOpen}
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(() => {
lastProcessedQrRef.current = '';
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}
expectedLot={expectedLotData}


+ 1
- 0
src/i18n/zh/pickOrder.json 파일 보기

@@ -461,6 +461,7 @@
"No released pick order records found.": "目前沒有可用的提料單。",
"EDT - Lane Code (Unassigned/Total)": "預計出發時間 - 貨車班次(未撳數/總單數)",
"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 confirmation failed. Please try again.": "確認批號失敗,請重試。",
"Lot status is unavailable. Cannot switch or bind; pick line was not updated.": "批號狀態為「不可用」,無法換批或綁定;揀貨行未更新。"

불러오는 중...
취소
저장