From e79b060f324941f84299d315c90233a4343a0878 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Sun, 29 Mar 2026 18:41:58 +0800 Subject: [PATCH] update expiry lot handle in jo/do and show qty will submit and no partly compelte by fronetend --- .../GoodPickExecutiondetail.tsx | 232 ++++++++++--- .../Jodetail/newJobPickExecution.tsx | 317 +++++++++++++----- src/i18n/zh/jo.json | 2 + src/i18n/zh/pickOrder.json | 1 + 4 files changed, 413 insertions(+), 139 deletions(-) diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx index 2e2a3bf..19bba38 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx @@ -516,6 +516,42 @@ const ManualLotConfirmationModal: React.FC<{ ); }; + +/** 過期批號(未換有效批前):與 noLot 類似——單筆/批量預設提交量為 0,除非 Issue 改數 */ +function isLotAvailabilityExpired(lot: any): boolean { + return String(lot?.lotAvailability || "").toLowerCase() === "expired"; +} + +/** Issue「改數」未寫入 SOL,刷新/換頁後需靠 session 還原,否則 Qty will submit 會回到 req */ +const FG_ISSUE_PICKED_KEY = (doPickOrderId: number) => + `fpsms-fg-issuePickedQty:${doPickOrderId}`; + +function loadIssuePickedMap(doPickOrderId: number): Record { + if (typeof window === "undefined" || !doPickOrderId) return {}; + try { + const raw = sessionStorage.getItem(FG_ISSUE_PICKED_KEY(doPickOrderId)); + if (!raw) return {}; + const parsed = JSON.parse(raw) as Record; + const out: Record = {}; + Object.entries(parsed).forEach(([k, v]) => { + const n = Number(v); + if (!Number.isNaN(n)) out[Number(k)] = n; + }); + return out; + } catch { + return {}; + } +} + +function saveIssuePickedMap(doPickOrderId: number, map: Record) { + if (typeof window === "undefined" || !doPickOrderId) return; + try { + sessionStorage.setItem(FG_ISSUE_PICKED_KEY(doPickOrderId), JSON.stringify(map)); + } catch { + // quota / private mode + } +} + const PickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab, onRefreshReleasedOrderCount }) => { const { t } = useTranslation("pickOrder"); const router = useRouter(); @@ -621,15 +657,18 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); if (combinedLotData.length === 0) { return { completed: 0, total: 0 }; } - - const nonPendingCount = combinedLotData.filter(lot => { + + // 與 allItemsReady 一致:noLot / 過期批號 的 pending 也算「已面對該行」可收尾 + const nonPendingCount = combinedLotData.filter((lot) => { const status = lot.stockOutLineStatus?.toLowerCase(); - return status !== 'pending'; + if (status !== "pending") return true; + if (lot.noLot === true || isLotAvailabilityExpired(lot)) return true; + return false; }).length; - + return { completed: nonPendingCount, - total: combinedLotData.length + total: combinedLotData.length, }; }, [combinedLotData]); @@ -789,6 +828,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO setCombinedLotData([]); setOriginalCombinedData([]); setAllLotsCompleted(false); + setIssuePickedQtyBySolId({}); return; } @@ -802,6 +842,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO setCombinedLotData([]); setOriginalCombinedData([]); setAllLotsCompleted(false); + setIssuePickedQtyBySolId({}); return; } @@ -1000,12 +1041,17 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO setCombinedLotData(flatLotData); setOriginalCombinedData(flatLotData); + const doPid = hierarchicalData.fgInfo?.doPickOrderId; + if (doPid) { + setIssuePickedQtyBySolId(loadIssuePickedMap(doPid)); + } checkAllLotsCompleted(flatLotData); } catch (error) { console.error(" Error fetching combined lot data:", error); setCombinedLotData([]); setOriginalCombinedData([]); setAllLotsCompleted(false); + setIssuePickedQtyBySolId({}); } finally { setCombinedDataLoading(false); } @@ -2422,7 +2468,29 @@ useEffect(() => { console.error("Error checking pick order completion:", error); } }, [currentUserId]); - + const resolveSingleSubmitQty = useCallback( + (lot: any) => { + const required = Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0); + const solId = Number(lot.stockOutLineId) || 0; + const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; + const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined; + if (issuePicked !== undefined && !Number.isNaN(Number(issuePicked))) { + return Number(issuePicked); + } + const fromPick = pickQtyData[lotKey]; + if (fromPick !== undefined && fromPick !== null && !Number.isNaN(Number(fromPick))) { + return Number(fromPick); + } + if (lot.noLot === true) { + return 0; + } + if (isLotAvailabilityExpired(lot)) { + return 0; + } + return required; + }, + [issuePickedQtyBySolId, pickQtyData] + ); // Handle reject lot // Handle pick execution form const handlePickExecutionForm = useCallback((lot: any) => { @@ -2461,7 +2529,12 @@ useEffect(() => { const solId = Number(issueData.stockOutLineId || issueData.stockOutLineId === 0 ? issueData.stockOutLineId : data?.stockOutLineId); if (solId > 0) { const picked = Number(issueData.actualPickQty || 0); - setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: picked })); + setIssuePickedQtyBySolId((prev) => { + const next = { ...prev, [solId]: picked }; + const doId = fgPickOrders[0]?.doPickOrderId; + if (doId) saveIssuePickedMap(doId, next); + return next; + }); setCombinedLotData(prev => prev.map(lot => { if (Number(lot.stockOutLineId) === solId) { return { ...lot, actualPickQty: picked, stockOutLineQty: picked }; @@ -2488,7 +2561,7 @@ useEffect(() => { } catch (error) { console.error("Error submitting pick execution form:", error); } - }, [fetchAllCombinedLotData, session]); + }, [fetchAllCombinedLotData, session, fgPickOrders]); // Calculate remaining required quantity const calculateRemainingRequiredQty = useCallback((lot: any) => { @@ -2593,7 +2666,8 @@ const allItemsReady = useMemo(() => { const isPending = status === 'pending'; // ✅ FIXED: 无库存(noLot)行:pending 状态也应该被视为 ready(可以提交) - if (lot.noLot === true) { + // ✅ 過期批號(未換批):與 noLot 相同,視為可收尾 + if (lot.noLot === true || isLotAvailabilityExpired(lot)) { return isChecked || isCompleted || isRejected || isPending; } @@ -2601,7 +2675,7 @@ const allItemsReady = useMemo(() => { return isChecked || isCompleted || isRejected; }); }, [combinedLotData]); -const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => { +const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number, source: 'justComplete' | 'singleSubmit') => { if (!lot.stockOutLineId) { console.error("No stock out line found for this lot"); return; @@ -2615,7 +2689,42 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe try { if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true })); // Just Complete: mark checked only, real posting happens in batch submit - if (submitQty === 0) { + if (submitQty === 0 && source === 'justComplete') { + console.log(`=== SUBMITTING ALL ZEROS CASE ===`); + console.log(`Lot: ${lot.lotNo}`); + console.log(`Stock Out Line ID: ${lot.stockOutLineId}`); + console.log(`Setting status to 'checked' with qty: 0`); + + const updateResult = await updateStockOutLineStatus({ + id: lot.stockOutLineId, + status: 'checked', + qty: 0 + }); + + console.log('Update result:', updateResult); + const r: any = updateResult as any; + const updateOk = + r?.code === 'SUCCESS' || + r?.type === 'completed' || + typeof r?.id === 'number' || + typeof r?.entity?.id === 'number' || + (r?.message && r.message.includes('successfully')); + if (!updateResult || !updateOk) { + console.error('Failed to update stock out line status:', updateResult); + throw new Error('Failed to update stock out line status'); + } + applyLocalStockOutLineUpdate(Number(lot.stockOutLineId), "checked", Number(lot.actualPickQty || 0)); + + void fetchAllCombinedLotData(); + console.log("Just Complete marked as checked successfully (waiting for batch submit)."); + + setTimeout(() => { + checkAndAutoAssignNext(); + }, 1000); + + return; + } + if (submitQty === 0 && source === 'singleSubmit') { console.log(`=== SUBMITTING ALL ZEROS CASE ===`); console.log(`Lot: ${lot.lotNo}`); console.log(`Stock Out Line ID: ${lot.stockOutLineId}`); @@ -2650,7 +2759,6 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe return; } - // FIXED: Calculate cumulative quantity correctly const currentActualPickQty = lot.actualPickQty || 0; const cumulativeQty = currentActualPickQty + submitQty; @@ -2661,7 +2769,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe if (cumulativeQty >= lot.requiredQty) { newStatus = 'completed'; } else if (cumulativeQty > 0) { - newStatus = 'partially_completed'; + newStatus = 'completed'; } else { newStatus = 'checked'; // QR scanned but no quantity submitted yet } @@ -2728,7 +2836,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe const handleSkip = useCallback(async (lot: any) => { try { console.log("Just Complete clicked, mark checked with 0 qty for lot:", lot.lotNo); - await handleSubmitPickQtyWithQty(lot, 0); + await handleSubmitPickQtyWithQty(lot, 0, 'justComplete'); } catch (err) { console.error("Error in Skip:", err); } @@ -2955,18 +3063,20 @@ const handleSubmitAllScanned = useCallback(async () => { if (statusLower === "completed" || statusLower === "complete") { return false; } - // ✅ noLot 情况:允许 checked / pending / partially_completed / PARTIALLY_COMPLETE - if (lot.noLot === true) { - return status === 'checked' || - - status === 'partially_completed' || - status === 'PARTIALLY_COMPLETE'; + // ✅ noLot / 過期批號:與 noLot 相同,允許 pending(未換批也可批量收尾) + if (lot.noLot === true || isLotAvailabilityExpired(lot)) { + return ( + status === "checked" || + status === "pending" || + status === "partially_completed" || + status === "PARTIALLY_COMPLETE" + ); } - // ✅ 正常 lot:也放宽为允许 checked / pending / partially_completed / PARTIALLY_COMPLETE - // 这样即使用户先改数(状态变为 pending / partially_completed),仍然可以批量提交 - return status === 'checked' || - status === 'partially_completed' || - status === 'PARTIALLY_COMPLETE'; + return ( + status === "checked" || + status === "partially_completed" || + status === "PARTIALLY_COMPLETE" + ); }); if (scannedLots.length === 0) { @@ -2993,29 +3103,33 @@ const handleSubmitAllScanned = useCallback(async () => { // 🔹 判断是否走“只改状态模式” // 这里先给一个简单条件示例:如果你不想再补拣,只想把当前数量标记完成, // 就让这个条件为 true(后面你可以根据业务加 UI 开关或别的 flag)。 - const onlyComplete = lot.stockOutLineStatus === "partially_completed" || issuePicked !== undefined; - // lot.stockOutLineStatus === "partially_completed" && false === true; - + const onlyComplete = + lot.stockOutLineStatus === "partially_completed" || issuePicked !== undefined; + + const expired = isLotAvailabilityExpired(lot); + let targetActual: number; let newStatus: string; - - if (onlyComplete) { - // ✅ 只改状态:目标数量 = 当前数量,不再补拣 + + // ✅ 過期且未在 Issue 填實際量:與 noLot 一樣按 0 完成 + if (expired && issuePicked === undefined) { + targetActual = 0; + newStatus = "completed"; + } else if (onlyComplete) { targetActual = currentActualPickQty; newStatus = "completed"; } else { - // ✅ 补拣模式:把剩余全部拣完 const remainingQty = Math.max(0, requiredQty - currentActualPickQty); const cumulativeQty = currentActualPickQty + remainingQty; - + targetActual = cumulativeQty; - + newStatus = "partially_completed"; if (requiredQty > 0 && cumulativeQty >= requiredQty) { newStatus = "completed"; } } - + return { stockOutLineId: Number(lot.stockOutLineId) || 0, pickOrderLineId: Number(lot.pickOrderLineId), @@ -3091,16 +3205,19 @@ const handleSubmitAllScanned = useCallback(async () => { return false; } // ✅ 与 handleSubmitAllScanned 完全保持一致 - if (lot.noLot === true) { - return status === 'checked' || - - status === 'partially_completed' || - status === 'PARTIALLY_COMPLETE'; + if (lot.noLot === true || isLotAvailabilityExpired(lot)) { + return ( + status === "checked" || + status === "pending" || + status === "partially_completed" || + status === "PARTIALLY_COMPLETE" + ); } - return status === 'checked' || - - status === 'partially_completed' || - status === 'PARTIALLY_COMPLETE'; + return ( + status === "checked" || + status === "partially_completed" || + status === "PARTIALLY_COMPLETE" + ); }); // 添加调试日志 @@ -3369,6 +3486,7 @@ const handleSubmitAllScanned = useCallback(async () => { {t("Lot#")} {t("Lot Required Pick Qty")} {t("Scan Result")} + {t("Qty will submit")} {t("Submit Required Pick Qty")} @@ -3468,7 +3586,26 @@ paginatedData.map((lot, index) => { ); } - + + // 過期批號:與 noLot 同類——視為已掃到/可處理(含 pending),顯示警示色勾選 + if (isLotAvailabilityExpired(lot) && status !== "rejected") { + return ( + + + + ); + } + // 正常 lot:已扫描(checked/partially_completed/completed) if (!isNoLot && status !== 'pending' && status !== 'rejected') { return ( @@ -3510,6 +3647,7 @@ paginatedData.map((lot, index) => { return null; })()} +{resolveSingleSubmitQty(lot)} {(() => { @@ -3568,9 +3706,9 @@ paginatedData.map((lot, index) => { variant="contained" onClick={() => { const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; - const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; + const submitQty = resolveSingleSubmitQty(lot); handlePickQtyChange(lotKey, submitQty); - handleSubmitPickQtyWithQty(lot, submitQty); + handleSubmitPickQtyWithQty(lot, submitQty, 'singleSubmit'); }} disabled={ lot.lotAvailability === 'expired' || diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx index 4f63509..73360b9 100644 --- a/src/components/Jodetail/newJobPickExecution.tsx +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -71,6 +71,40 @@ interface Props { onBackToList?: () => void; } +/** 過期批號:與 noLot 類似——單筆/批量預設 0,除非 Issue 改數(對齊 GoodPickExecutiondetail) */ +function isLotAvailabilityExpired(lot: any): boolean { + return String(lot?.lotAvailability || "").toLowerCase() === "expired"; +} + +const JO_ISSUE_PICKED_KEY = (pickOrderId: number) => + `fpsms-jo-issuePickedQty:${pickOrderId}`; + +function loadIssuePickedMapJo(pickOrderId: number): Record { + if (typeof window === "undefined" || !pickOrderId) return {}; + try { + const raw = sessionStorage.getItem(JO_ISSUE_PICKED_KEY(pickOrderId)); + if (!raw) return {}; + const parsed = JSON.parse(raw) as Record; + const out: Record = {}; + Object.entries(parsed).forEach(([k, v]) => { + const n = Number(v); + if (!Number.isNaN(n)) out[Number(k)] = n; + }); + return out; + } catch { + return {}; + } +} + +function saveIssuePickedMapJo(pickOrderId: number, map: Record) { + if (typeof window === "undefined" || !pickOrderId) return; + try { + sessionStorage.setItem(JO_ISSUE_PICKED_KEY(pickOrderId), JSON.stringify(map)); + } catch { + // ignore quota / private mode + } +} + // Manual Lot Confirmation Modal (align with GoodPickExecutiondetail, opened by {2fic}) const ManualLotConfirmationModal: React.FC<{ open: boolean; @@ -481,6 +515,9 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const formProps = useForm(); const errors = formProps.formState.errors; const [isSubmittingAll, setIsSubmittingAll] = useState(false); + const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle'); + const [completionStatus, setCompletionStatus] = useState(null); + const [autoAssignMessage, setAutoAssignMessage] = useState(''); // Add QR modal states const [qrModalOpen, setQrModalOpen] = useState(false); @@ -825,6 +862,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { if (!pickOrderId) { console.warn("⚠️ No pickOrderId provided, skipping API call"); setJobOrderData(null); + setIssuePickedQtyBySolId({}); return; } @@ -833,14 +871,16 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { console.log("✅ Job Order data (hierarchical):", jobOrderData); setJobOrderData(jobOrderData); + setIssuePickedQtyBySolId(loadIssuePickedMapJo(pickOrderId)); // 使用辅助函数获取所有 lots(不再扁平化) - const allLots = getAllLotsFromHierarchical(jobOrderData); + getAllLotsFromHierarchical(jobOrderData); } catch (error) { console.error("❌ Error fetching job order data:", error); setJobOrderData(null); + setIssuePickedQtyBySolId({}); } finally { setCombinedDataLoading(false); } @@ -1703,9 +1743,30 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { })); }, []); - const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle'); - const [autoAssignMessage, setAutoAssignMessage] = useState(''); - const [completionStatus, setCompletionStatus] = useState(null); + /** 单笔「提交」数量:Issue 改数 → pickQtyData → noLot/過期 → 0 → 否则 required(對齊 GoodPickExecutiondetail) */ + const resolveSingleSubmitQty = useCallback( + (lot: any) => { + const required = Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0); + const solId = Number(lot.stockOutLineId) || 0; + const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; + const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined; + if (issuePicked !== undefined && !Number.isNaN(Number(issuePicked))) { + return Number(issuePicked); + } + const fromPick = pickQtyData[lotKey]; + if (fromPick !== undefined && fromPick !== null && !Number.isNaN(Number(fromPick))) { + return Number(fromPick); + } + if (lot.noLot === true || !lot.lotId) { + return 0; + } + if (isLotAvailabilityExpired(lot)) { + return 0; + } + return required; + }, + [issuePickedQtyBySolId, pickQtyData] + ); const checkAndAutoAssignNext = useCallback(async () => { if (!currentUserId) return; @@ -1847,7 +1908,12 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { // 记录该 SOL 的“目标实际拣货量=0”,让 batch submit 走 onlyComplete(不补拣到 required) if (solId > 0) { - setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: 0 })); + setIssuePickedQtyBySolId((prev) => { + const next = { ...prev, [solId]: 0 }; + const pid = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + if (pid) saveIssuePickedMapJo(pid, next); + return next; + }); setLocalSolStatusById(prev => ({ ...prev, [solId]: 'checked' })); } @@ -1866,15 +1932,14 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const currentActualPickQty = lot.actualPickQty || 0; const cumulativeQty = currentActualPickQty + submitQty; - // Determine status based on cumulative quantity vs required quantity - let newStatus = 'partially_completed'; - + // 短拣一次 completed(對齊 GoodPickExecutiondetail) + let newStatus = "partially_completed"; if (cumulativeQty >= lot.requiredQty) { - newStatus = 'completed'; + newStatus = "completed"; } else if (cumulativeQty > 0) { - newStatus = 'partially_completed'; + newStatus = "completed"; } else { - newStatus = 'checked'; // QR scanned but no quantity submitted yet + newStatus = "checked"; } console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`); @@ -1892,7 +1957,12 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { qty: cumulativeQty }); if (solId > 0) { - setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: cumulativeQty })); + setIssuePickedQtyBySolId((prev) => { + const next = { ...prev, [solId]: cumulativeQty }; + const pid = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + if (pid) saveIssuePickedMapJo(pid, next); + return next; + }); setLocalSolStatusById(prev => ({ ...prev, [solId]: newStatus })); } @@ -1966,29 +2036,25 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { return () => window.removeEventListener("beforeunload", handler); }, [hasPendingBatchSubmit]); const handleSubmitAllScanned = useCallback(async () => { - const scannedLots = combinedLotData.filter(lot => { + const scannedLots = combinedLotData.filter((lot) => { const status = lot.stockOutLineStatus; const statusLower = String(status || "").toLowerCase(); if (statusLower === "completed" || statusLower === "complete") { return false; } - console.log("lot.noLot:", lot.noLot); - console.log("lot.status:", lot.stockOutLineStatus); - // ✅ no-lot:允許 pending / checked / partially_completed / PARTIALLY_COMPLETE - if (lot.noLot === true || !lot.lotId) { + // noLot / 過期批號:允許 pending(對齊 GoodPickExecutiondetail) + if (lot.noLot === true || !lot.lotId || isLotAvailabilityExpired(lot)) { return ( - status === 'checked' || - status === 'pending' || - status === 'partially_completed' || - status === 'PARTIALLY_COMPLETE' + status === "checked" || + status === "pending" || + status === "partially_completed" || + status === "PARTIALLY_COMPLETE" ); } - - // ✅ 有 lot:維持原本規則 return ( - status === 'checked' || - status === 'partially_completed' || - status === 'PARTIALLY_COMPLETE' + status === "checked" || + status === "partially_completed" || + status === "PARTIALLY_COMPLETE" ); }); if (scannedLots.length === 0) { @@ -2016,44 +2082,43 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { console.log(`✅ Updated handlers for ${uniqueItemIds.size} unique items`); } - // ✅ 转换为 batchSubmitList 所需的格式 const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => { const requiredQty = Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0); -const solId = Number(lot.stockOutLineId) || 0; -const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined; -const currentActualPickQty = Number(issuePicked ?? lot.actualPickQty ?? 0); -const isNoLot = lot.noLot === true || !lot.lotId; - -// ✅ 只改狀態模式:有 issuePicked 或 noLot -const onlyComplete = - lot.stockOutLineStatus === 'partially_completed' || - issuePicked !== undefined || - isNoLot; - -let targetActual: number; -let newStatus: string; - -if (onlyComplete) { - targetActual = currentActualPickQty; // no‑lot = 0,一律只改狀態 - newStatus = 'completed'; -} else { - const remainingQty = Math.max(0, requiredQty - currentActualPickQty); - targetActual = currentActualPickQty + remainingQty; - newStatus = - requiredQty > 0 && targetActual >= requiredQty - ? 'completed' - : 'partially_completed'; -} - + const solId = Number(lot.stockOutLineId) || 0; + const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined; + const currentActualPickQty = Number(issuePicked ?? lot.actualPickQty ?? 0); + const onlyComplete = + lot.stockOutLineStatus === "partially_completed" || issuePicked !== undefined; + const expired = isLotAvailabilityExpired(lot); + + let targetActual: number; + let newStatus: string; + + if (expired && issuePicked === undefined) { + targetActual = 0; + newStatus = "completed"; + } else if (onlyComplete) { + targetActual = currentActualPickQty; + newStatus = "completed"; + } else { + const remainingQty = Math.max(0, requiredQty - currentActualPickQty); + const cumulativeQty = currentActualPickQty + remainingQty; + targetActual = cumulativeQty; + newStatus = "partially_completed"; + if (requiredQty > 0 && cumulativeQty >= requiredQty) { + newStatus = "completed"; + } + } + return { stockOutLineId: Number(lot.stockOutLineId) || 0, pickOrderLineId: Number(lot.pickOrderLineId), inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null, requiredQty, - actualPickQty: Number(targetActual), + actualPickQty: targetActual, stockOutLineStatus: newStatus, - pickOrderConsoCode: String(lot.pickOrderConsoCode || ''), - noLot: Boolean(lot.noLot === true) + pickOrderConsoCode: String(lot.pickOrderConsoCode || ""), + noLot: Boolean(lot.noLot === true), }; }); @@ -2118,29 +2183,24 @@ if (onlyComplete) { } }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onBackToList, updateHandledBy, issuePickedQtyBySolId]) const scannedItemsCount = useMemo(() => { - return combinedLotData.filter(lot => { + return combinedLotData.filter((lot) => { const status = lot.stockOutLineStatus; const statusLower = String(status || "").toLowerCase(); if (statusLower === "completed" || statusLower === "complete") { return false; } - const isNoLot = lot.noLot === true || !lot.lotId; - - if (isNoLot) { - // no-lot:pending / checked / partially_completed 都算「已掃描」 + if (lot.noLot === true || !lot.lotId || isLotAvailabilityExpired(lot)) { return ( - status === 'pending' || - status === 'checked' || - status === 'partially_completed' || - status === 'PARTIALLY_COMPLETE' + status === "checked" || + status === "pending" || + status === "partially_completed" || + status === "PARTIALLY_COMPLETE" ); } - - // 有 lot:維持原規則 return ( - status === 'checked' || - status === 'partially_completed' || - status === 'PARTIALLY_COMPLETE' + status === "checked" || + status === "partially_completed" || + status === "PARTIALLY_COMPLETE" ); }).length; }, [combinedLotData]); @@ -2160,13 +2220,16 @@ if (onlyComplete) { return combinedLotData.filter(lot => extractFloor(lot) === selectedFloor); }, [combinedLotData, selectedFloor]); - // Progress bar data - 现在可以正确引用 filteredByFloor + // 與批量篩選一致:noLot / 過期 的 pending 也算已處理(對齊 GoodPickExecutiondetail) const progress = useMemo(() => { const data = selectedFloor ? filteredByFloor : combinedLotData; if (data.length === 0) return { completed: 0, total: 0 }; - const nonPendingCount = data.filter(lot => - lot.stockOutLineStatus?.toLowerCase() !== 'pending' - ).length; + const nonPendingCount = data.filter((lot) => { + const status = lot.stockOutLineStatus?.toLowerCase(); + if (status !== "pending") return true; + if (lot.noLot === true || isLotAvailabilityExpired(lot)) return true; + return false; + }).length; return { completed: nonPendingCount, total: data.length }; }, [selectedFloor, filteredByFloor, combinedLotData]); // Handle reject lot @@ -2240,7 +2303,12 @@ if (onlyComplete) { const solId = Number(issueData.stockOutLineId || data?.stockOutLineId); if (solId > 0) { const picked = Number(issueData.actualPickQty || 0); - setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: picked })); + setIssuePickedQtyBySolId((prev) => { + const next = { ...prev, [solId]: picked }; + const pid = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + if (pid) saveIssuePickedMapJo(pid, next); + return next; + }); } } else { console.error("❌ Failed to record pick execution issue:", result); @@ -2254,7 +2322,7 @@ if (onlyComplete) { } catch (error) { console.error("Error submitting pick execution form:", error); } - }, [fetchJobOrderData, currentUserId, selectedLotForExecutionForm, updateHandledBy, filterArgs?.pickOrderId]); + }, [fetchJobOrderData, currentUserId, selectedLotForExecutionForm, updateHandledBy, filterArgs?.pickOrderId, filterArgs]); // Calculate remaining required quantity const calculateRemainingRequiredQty = useCallback((lot: any) => { @@ -2542,13 +2610,14 @@ const sortedData = [...sourceData].sort((a, b) => { {t("Lot Required Pick Qty")} {t("Available Qty")} {t("Scan Result")} + {t("Qty will submit")} {t("Submit Required Pick Qty")} {paginatedData.length === 0 ? ( - + {t("No data available")} @@ -2580,10 +2649,34 @@ const sortedData = [...sourceData].sort((a, b) => { {lot.itemCode} {lot.itemName+'('+lot.uomDesc+')'} - {lot.noLot === true || !lot.lotId - ? t("Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.") // i18n key,下一步加文案 - : (lot.lotNo || '-')} - + + + {lot.lotNo ? ( + lot.lotAvailability === "expired" ? ( + <> + {lot.lotNo}{" "} + {t( + "is expired. Please check around have available QR code or not.", + )} + + ) : ( + lot.lotNo + ) + ) : ( + t( + "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.", + ) + )} + + + {(() => { const requiredQty = lot.requiredQty || 0; @@ -2607,8 +2700,7 @@ const sortedData = [...sourceData].sort((a, b) => { const status = lot.stockOutLineStatus?.toLowerCase(); const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected'; const isNoLot = !lot.lotNo; - - // ✅ rejected lot:显示红色勾选(已扫描但被拒绝) + if (isRejected && !isNoLot) { return ( @@ -2626,8 +2718,25 @@ const sortedData = [...sourceData].sort((a, b) => { ); } - - // ✅ 正常 lot:已扫描(checked/partially_completed/completed) + + if (isLotAvailabilityExpired(lot) && status !== "rejected") { + return ( + + + + ); + } + if (!isNoLot && status !== 'pending' && status !== 'rejected') { return ( @@ -2645,11 +2754,31 @@ const sortedData = [...sourceData].sort((a, b) => { ); } - + + if (isNoLot && (status === 'partially_completed' || status === 'completed')) { + return ( + + + + ); + } + return null; })()} + {resolveSingleSubmitQty(lot)} + {(() => { @@ -2689,7 +2818,7 @@ const sortedData = [...sourceData].sort((a, b) => { } try { const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; - const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; + const submitQty = resolveSingleSubmitQty(lot); handlePickQtyChange(lotKey, submitQty); await handleSubmitPickQtyWithQty(lot, submitQty); @@ -2722,7 +2851,12 @@ const sortedData = [...sourceData].sort((a, b) => { size="small" onClick={() => handlePickExecutionForm(lot)} disabled={ - lot.stockOutLineStatus === 'completed' || lot.noLot === true || !lot.lotId + lot.lotAvailability === "expired" || + lot.stockOutLineStatus === "completed" || + lot.noLot === true || + !lot.lotId || + (Number(lot.stockOutLineId) > 0 && + actionBusyBySolId[Number(lot.stockOutLineId)] === true) } sx={{ fontSize: '0.7rem', @@ -2764,13 +2898,12 @@ const sortedData = [...sourceData].sort((a, b) => { (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) || lot.stockOutLineStatus === 'completed' || lot.stockOutLineStatus === 'checked' || + lot.stockOutLineStatus === 'partially_completed' || + lot.lotAvailability === 'expired' || lot.noLot === true || !lot.lotId || (Number(lot.stockOutLineId) > 0 && - Object.prototype.hasOwnProperty.call( - issuePickedQtyBySolId, - Number(lot.stockOutLineId) - )) + issuePickedQtyBySolId[Number(lot.stockOutLineId)] !== undefined) } sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }} > diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index dd76879..3c3ecfd 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -11,6 +11,7 @@ "Name": "成品/半成品名稱", "Picked Qty": "已提料數量", "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "請檢查周圍是否有QR碼,可能是剛剛入庫或轉移入庫或轉移出庫。", + "is expired. Please check around have available QR code or not.": "已過期。請檢查周圍是否有可用的 QR 碼。", "Confirm All": "確認所有提料", "Wait Time [minutes]": "等待時間(分鐘)", "Job Process Status Dashboard": "儀表板 - 工單狀態", @@ -25,6 +26,7 @@ "UoM": "銷售單位", "Select Another Bag Lot":"選擇另一個包裝袋", "No": "沒有", + "Qty will submit": "提交數量", "Packaging":"提料中", "Overall Time Remaining": "總剩餘時間", "User not found with staffNo:": "用戶不存在", diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index 9903481..1bd58f1 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -33,6 +33,7 @@ "Pick Order Code(s)": "提料單編號", "Delivery Order Code(s)": "提料單編號", "Start Success": "開始成功", + "Qty will submit": "提交數量", "Truck Lance Code": "車線號碼", "Pick Order Codes": "提料單編號", "Pick Order Lines": "提料單行數",