|
|
|
@@ -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} |
|
|
|
|