From b01f7eda9f30303e393e94e232634b292518f029 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 24 Mar 2026 21:38:42 +0800 Subject: [PATCH] update --- src/app/api/stockTake/actions.ts | 3 + src/components/DoSearch/DoSearch.tsx | 64 +++++- .../GoodPickExecutiondetail.tsx | 191 +++++++++++------- .../LotConfirmationModal.tsx | 9 +- .../Jodetail/newJobPickExecution.tsx | 55 +++-- src/components/StockIssue/SearchPage.tsx | 46 +++-- .../ApproverStockTakeAll.tsx | 24 ++- src/i18n/zh/pickOrder.json | 3 + 8 files changed, 274 insertions(+), 121 deletions(-) diff --git a/src/app/api/stockTake/actions.ts b/src/app/api/stockTake/actions.ts index 2cee98e..98f3233 100644 --- a/src/app/api/stockTake/actions.ts +++ b/src/app/api/stockTake/actions.ts @@ -41,6 +41,9 @@ export interface InventoryLotDetailResponse { approverBadQty: number | null; finalQty: number | null; bookQty: number | null; + stockTakeSection?: string | null; + stockTakeSectionDescription?: string | null; + stockTakerName?: string | null; } export const getInventoryLotDetailsBySection = async ( diff --git a/src/components/DoSearch/DoSearch.tsx b/src/components/DoSearch/DoSearch.tsx index 278f97b..fa99cb0 100644 --- a/src/components/DoSearch/DoSearch.tsx +++ b/src/components/DoSearch/DoSearch.tsx @@ -58,6 +58,7 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea const formProps = useForm({ defaultValues: {}, }); + const { setValue } = formProps; const errors = formProps.formState.errors; @@ -68,8 +69,8 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea console.log("🔍 DoSearch - session:", session); console.log("🔍 DoSearch - currentUserId:", currentUserId); const [searchTimeout, setSearchTimeout] = useState(null); - const [rowSelectionModel, setRowSelectionModel] = - useState([]); + /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */ + const [excludedRowIds, setExcludedRowIds] = useState([]); const [searchAllDos, setSearchAllDos] = useState([]); const [totalCount, setTotalCount] = useState(0); @@ -101,6 +102,37 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea const [hasSearched, setHasSearched] = useState(false); const [hasResults, setHasResults] = useState(false); + const excludedIdSet = useMemo(() => new Set(excludedRowIds), [excludedRowIds]); + + const rowSelectionModel = useMemo(() => { + return searchAllDos + .map((r) => r.id) + .filter((id) => !excludedIdSet.has(id)); + }, [searchAllDos, excludedIdSet]); + + const applyRowSelectionChange = useCallback( + (newModel: GridRowSelectionModel) => { + const pageIds = searchAllDos.map((r) => r.id); + const selectedSet = new Set( + newModel.map((id) => (typeof id === "string" ? Number(id) : id)), + ); + setExcludedRowIds((prev) => { + const next = new Set(prev); + for (const id of pageIds) { + next.delete(id); + } + for (const id of pageIds) { + if (!selectedSet.has(id)) { + next.add(id); + } + } + return Array.from(next); + }); + setValue("ids", newModel); + }, + [searchAllDos, setValue], + ); + // 当搜索条件变化时,重置到第一页 useEffect(() => { setPagingController(p => ({ @@ -140,6 +172,7 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea setTotalCount(0); setHasSearched(false); setHasResults(false); + setExcludedRowIds([]); setPagingController({ pageNum: 1, pageSize: 10 }); } catch (error) { @@ -289,6 +322,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { setTotalCount(response.total); // 设置总记录数 setHasSearched(true); setHasResults(response.records.length > 0); + setExcludedRowIds([]); } catch (error) { console.error("Error: ", error); @@ -296,6 +330,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { setTotalCount(0); setHasSearched(true); setHasResults(false); + setExcludedRowIds([]); } }, [pagingController]); @@ -494,6 +529,20 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { }); return; } + + const idsToRelease = allMatchingDos + .map((d) => d.id) + .filter((id) => !excludedIdSet.has(id)); + + if (idsToRelease.length === 0) { + await Swal.fire({ + icon: "warning", + title: t("No Records"), + text: t("No delivery orders selected for batch release. Uncheck orders you want to exclude, or search again to reset selection."), + confirmButtonText: t("OK"), + }); + return; + } // 显示确认对话框 const result = await Swal.fire({ @@ -501,7 +550,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { title: t("Batch Release"), html: `
-

${t("Selected Shop(s): ")}${allMatchingDos.length}

+

${t("Selected Shop(s): ")}${idsToRelease.length}

${currentSearchParams.code ? `${t("Code")}: ${currentSearchParams.code} ` : ""} @@ -519,8 +568,6 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { }); if (result.isConfirmed) { - const idsToRelease = allMatchingDos.map(d => d.id); - try { const startRes = await startBatchReleaseAsync({ ids: idsToRelease, userId: currentUserId ?? 1 }); const jobId = startRes?.entity?.jobId; @@ -595,7 +642,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { confirmButtonText: t("OK") }); } - }, [t, currentUserId, currentSearchParams, handleSearch]); + }, [t, currentUserId, currentSearchParams, handleSearch, excludedIdSet]); return ( <> @@ -629,10 +676,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { columns={columns} checkboxSelection rowSelectionModel={rowSelectionModel} - onRowSelectionModelChange={(newRowSelectionModel) => { - setRowSelectionModel(newRowSelectionModel); - formProps.setValue("ids", newRowSelectionModel); - }} + onRowSelectionModelChange={applyRowSelectionChange} slots={{ footer: FooterToolbar, noRowsOverlay: NoRowsOverlay, diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx index f07576e..0639b44 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx @@ -80,6 +80,23 @@ interface Props { onSwitchToRecordTab?: () => void; onRefreshReleasedOrderCount?: () => void; } + +/** 同物料多行时,优先对「有建议批次号」的行做替换,避免误选「无批次/不足」行 */ +function pickExpectedLotForSubstitution(activeSuggestedLots: any[]): any | null { + if (!activeSuggestedLots?.length) return null; + const withLotNo = activeSuggestedLots.filter( + (l) => l.lotNo != null && String(l.lotNo).trim() !== "" + ); + if (withLotNo.length === 1) return withLotNo[0]; + if (withLotNo.length > 1) { + const pending = withLotNo.find( + (l) => (l.stockOutLineStatus || "").toLowerCase() === "pending" + ); + return pending || withLotNo[0]; + } + return activeSuggestedLots[0]; +} + // QR Code Modal Component (from LotTable) const QrCodeModal: React.FC<{ open: boolean; @@ -513,6 +530,22 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); const [originalCombinedData, setOriginalCombinedData] = useState([]); // issue form 里填的 actualPickQty(用于 batch submit 只提交实际拣到数量,而不是补拣到 required) const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState>({}); + const applyLocalStockOutLineUpdate = useCallback(( + stockOutLineId: number, + status: string, + actualPickQty?: number + ) => { + setCombinedLotData(prev => prev.map((lot) => { + if (Number(lot.stockOutLineId) !== Number(stockOutLineId)) return lot; + return { + ...lot, + stockOutLineStatus: status, + ...(typeof actualPickQty === "number" + ? { actualPickQty, stockOutLineQty: actualPickQty } + : {}), + }; + })); + }, []); // 防止重复点击(Submit / Just Completed / Issue) const [actionBusyBySolId, setActionBusyBySolId] = useState>({}); @@ -571,12 +604,11 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); const lastProcessedQrRef = useRef(''); // Store callbacks in refs to avoid useEffect dependency issues - const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise) | null>(null); + const processOutsideQrCodeRef = useRef< + ((latestQr: string, qrScanCountAtInvoke?: number) => Promise) | null + >(null); const resetScanRef = useRef<(() => void) | null>(null); const lotConfirmOpenedQrCountRef = useRef(0); - const lotConfirmOpenedQrValueRef = useRef(''); - const lotConfirmInitialSameQrSkippedRef = useRef(false); - const autoConfirmInProgressRef = useRef(false); @@ -651,11 +683,14 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); } }, []); - const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => { + const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any, qrScanCountAtOpen?: number) => { const mismatchStartTime = performance.now(); console.log(`⏱️ [HANDLE LOT MISMATCH START]`); console.log(`⏰ Start time: ${new Date().toISOString()}`); console.log("Lot mismatch detected:", { expectedLot, scannedLot }); + + lotConfirmOpenedQrCountRef.current = + typeof qrScanCountAtOpen === "number" ? qrScanCountAtOpen : 1; // ✅ Use setTimeout to avoid flushSync warning - schedule modal update in next tick const setTimeoutStartTime = performance.now(); @@ -1299,34 +1334,6 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO return false; }, [lotConfirmationOpen, selectedLotForQr, expectedLotData, scannedLotData, parseQrPayload, handleLotConfirmation, clearLotConfirmationState]); - useEffect(() => { - if (!lotConfirmationOpen || !expectedLotData || !scannedLotData || !selectedLotForQr) { - autoConfirmInProgressRef.current = false; - return; - } - - if (autoConfirmInProgressRef.current || isConfirmingLot) { - return; - } - - autoConfirmInProgressRef.current = true; - handleLotConfirmation() - .catch((error) => { - console.error("Auto confirm lot substitution failed:", error); - }) - .finally(() => { - autoConfirmInProgressRef.current = false; - }); - }, [lotConfirmationOpen, expectedLotData, scannedLotData, selectedLotForQr, isConfirmingLot, handleLotConfirmation]); - - useEffect(() => { - if (lotConfirmationOpen) { - // 记录弹窗打开时的扫码数量,避免把“触发弹窗的同一次扫码”当作二次确认 - lotConfirmOpenedQrCountRef.current = qrValues.length; - lotConfirmOpenedQrValueRef.current = qrValues[qrValues.length - 1] || ''; - lotConfirmInitialSameQrSkippedRef.current = true; - } - }, [lotConfirmationOpen, qrValues.length]); const handleQrCodeSubmit = useCallback(async (lotNo: string) => { console.log(` Processing QR Code for lot: ${lotNo}`); @@ -1624,7 +1631,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO // Store resetScan in ref for immediate access (update on every render) resetScanRef.current = resetScan; - const processOutsideQrCode = useCallback(async (latestQr: string) => { + const processOutsideQrCode = useCallback(async (latestQr: string, qrScanCountAtInvoke?: number) => { const totalStartTime = performance.now(); console.log(`⏱️ [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`); console.log(`⏰ Start time: ${new Date().toISOString()}`); @@ -1742,7 +1749,14 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO lot.lotAvailability === 'rejected' || lot.lotAvailability === 'status_unavailable' ); - const expectedLot = rejectedLot || allLotsForItem[0]; // Use rejected lot if exists, otherwise first lot + const expectedLot = + rejectedLot || + pickExpectedLotForSubstitution( + allLotsForItem.filter( + (l: any) => l.lotNo != null && String(l.lotNo).trim() !== "" + ) + ) || + allLotsForItem[0]; // ✅ Always open confirmation modal when no active lots (user needs to confirm switching) // handleLotMismatch will fetch lotNo from backend using stockInLineId if needed @@ -1760,7 +1774,8 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO itemName: expectedLot.itemName, inventoryLotLineId: scannedLot?.lotId || null, stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo - } + }, + qrScanCountAtInvoke ); return; } @@ -1785,7 +1800,8 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO // Also handle case where scanned lot is not in allLotsForItem (scannedLot is undefined) if (!exactMatch) { // Scanned lot is not in active suggested lots, open confirmation modal - const expectedLot = activeSuggestedLots[0] || allLotsForItem[0]; // Use first active lot or first lot as expected + const expectedLot = + pickExpectedLotForSubstitution(activeSuggestedLots) || allLotsForItem[0]; if (expectedLot) { // Check if scanned lot is different from expected, or if scannedLot is undefined (not in allLotsForItem) const shouldOpenModal = !scannedLot || (scannedLot.stockInLineId !== expectedLot.stockInLineId); @@ -1804,7 +1820,8 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO itemName: expectedLot.itemName, inventoryLotLineId: scannedLot?.lotId || null, stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo - } + }, + qrScanCountAtInvoke ); return; } @@ -1925,9 +1942,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO const mismatchCheckTime = performance.now() - mismatchCheckStartTime; console.log(`⏱️ [PERF] Mismatch check time: ${mismatchCheckTime.toFixed(2)}ms`); - // 取第一个活跃的 lot 作为期望的 lot + // 取应被替换的活跃行(同物料多行时优先有建议批次的行) const expectedLotStartTime = performance.now(); - const expectedLot = activeSuggestedLots[0]; + const expectedLot = pickExpectedLotForSubstitution(activeSuggestedLots); if (!expectedLot) { console.error("Could not determine expected lot for confirmation"); startTransition(() => { @@ -1963,7 +1980,8 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO itemName: expectedLot.itemName, inventoryLotLineId: null, stockInLineId: scannedStockInLineId // ✅ 传递 stockInLineId - } + }, + qrScanCountAtInvoke ); const handleMismatchTime = performance.now() - handleMismatchStartTime; console.log(`⏱️ [PERF] Handle mismatch call time: ${handleMismatchTime.toFixed(2)}ms`); @@ -2048,7 +2066,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO // ✅ Process immediately (bypass QR scanner delay) if (processOutsideQrCodeRef.current) { - processOutsideQrCodeRef.current(simulatedQr).then(() => { + processOutsideQrCodeRef.current(simulatedQr, qrValues.length).then(() => { const testTime = performance.now() - testStartTime; console.log(`⏱️ [TEST QR] Total processing time: ${testTime.toFixed(2)}ms (${(testTime / 1000).toFixed(3)}s)`); console.log(`⏱️ [TEST QR] End time: ${new Date().toISOString()}`); @@ -2074,9 +2092,24 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO } } - // lot confirm 弹窗打开时,允许通过“再次扫码”决定走向(切换或继续原 lot) + // 批次确认弹窗:须第二次扫码选择沿用建议批次或切换(不再自动确认) if (lotConfirmationOpen) { - // 已改回自动确认:弹窗打开时不再等待二次扫码 + if (isConfirmingLot) { + return; + } + if (qrValues.length <= lotConfirmOpenedQrCountRef.current) { + return; + } + void (async () => { + try { + const handled = await handleLotConfirmationByRescan(latestQr); + if (handled && resetScanRef.current) { + resetScanRef.current(); + } + } catch (e) { + console.error("Lot confirmation rescan failed:", e); + } + })(); return; } @@ -2171,7 +2204,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO // Use ref to avoid dependency issues const processCallStartTime = performance.now(); if (processOutsideQrCodeRef.current) { - processOutsideQrCodeRef.current(latestQr).then(() => { + processOutsideQrCodeRef.current(latestQr, qrValues.length).then(() => { const processCallTime = performance.now() - processCallStartTime; const totalProcessingTime = performance.now() - processingStartTime; console.log(`⏱️ [QR PROCESS] processOutsideQrCode call time: ${processCallTime.toFixed(2)}ms`); @@ -2203,7 +2236,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO qrProcessingTimeoutRef.current = null; } }; - }, [qrValues, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen, handleLotConfirmationByRescan]); + }, [qrValues, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen, handleLotConfirmationByRescan, isConfirmingLot]); const renderCountRef = useRef(0); const renderStartTimeRef = useRef(null); @@ -2550,16 +2583,16 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe try { if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true })); - // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0 + // Just Complete: mark checked only, real posting happens in batch submit if (submitQty === 0) { console.log(`=== SUBMITTING ALL ZEROS CASE ===`); console.log(`Lot: ${lot.lotNo}`); console.log(`Stock Out Line ID: ${lot.stockOutLineId}`); - console.log(`Setting status to 'completed' with qty: 0`); + console.log(`Setting status to 'checked' with qty: 0`); const updateResult = await updateStockOutLineStatus({ id: lot.stockOutLineId, - status: 'completed', + status: 'checked', qty: 0 }); @@ -2575,29 +2608,10 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe 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)); - // Check if pick order is completed - if (lot.pickOrderConsoCode) { - console.log(` Lot ${lot.lotNo} completed (all zeros), checking if pick order ${lot.pickOrderConsoCode} is complete...`); - - try { - const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode); - console.log(` Pick order completion check result:`, completionResponse); - - if (completionResponse.code === "SUCCESS") { - console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`); - } else if (completionResponse.message === "not completed") { - console.log(`⏳ Pick order not completed yet, more lines remaining`); - } else { - console.error(` Error checking completion: ${completionResponse.message}`); - } - } catch (error) { - console.error("Error checking pick order completion:", error); - } - } - - await fetchAllCombinedLotData(); - console.log("All zeros submission completed successfully!"); + void fetchAllCombinedLotData(); + console.log("Just Complete marked as checked successfully (waiting for batch submit)."); setTimeout(() => { checkAndAutoAssignNext(); @@ -2635,6 +2649,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe status: newStatus, qty: cumulativeQty // Use cumulative quantity }); + applyLocalStockOutLineUpdate(Number(lot.stockOutLineId), newStatus, cumulativeQty); if (submitQty > 0) { await updateInventoryLotLineQuantities({ @@ -2665,7 +2680,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe } } - await fetchAllCombinedLotData(); + void fetchAllCombinedLotData(); console.log("Pick quantity submitted successfully!"); setTimeout(() => { @@ -2677,16 +2692,31 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe } finally { if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: false })); } -}, [fetchAllCombinedLotData, checkAndAutoAssignNext, actionBusyBySolId]); +}, [fetchAllCombinedLotData, checkAndAutoAssignNext, actionBusyBySolId, applyLocalStockOutLineUpdate]); const handleSkip = useCallback(async (lot: any) => { try { - console.log("Skip clicked, submit lot required qty for lot:", lot.lotNo); - await handleSubmitPickQtyWithQty(lot, lot.requiredQty); + console.log("Just Complete clicked, mark checked with 0 qty for lot:", lot.lotNo); + await handleSubmitPickQtyWithQty(lot, 0); } catch (err) { console.error("Error in Skip:", err); } }, [handleSubmitPickQtyWithQty]); +const hasPendingBatchSubmit = useMemo(() => { + return combinedLotData.some((lot) => { + const status = String(lot.stockOutLineStatus || "").toLowerCase(); + return status === "checked" || status === "pending" || status === "partially_completed" || status === "partially_complete"; + }); +}, [combinedLotData]); +useEffect(() => { + if (!hasPendingBatchSubmit) return; + const handler = (event: BeforeUnloadEvent) => { + event.preventDefault(); + event.returnValue = ""; + }; + window.addEventListener("beforeunload", handler); + return () => window.removeEventListener("beforeunload", handler); +}, [hasPendingBatchSubmit]); const handleStartScan = useCallback(() => { const startTime = performance.now(); console.log(`⏱️ [START SCAN] Called at: ${new Date().toISOString()}`); @@ -2890,6 +2920,10 @@ const handleSubmitAllScanned = useCallback(async () => { const scannedLots = combinedLotData.filter(lot => { const status = lot.stockOutLineStatus; + const statusLower = String(status || "").toLowerCase(); + if (statusLower === "completed" || statusLower === "complete") { + return false; + } // ✅ noLot 情况:允许 checked / pending / partially_completed / PARTIALLY_COMPLETE if (lot.noLot === true) { return status === 'checked' || @@ -3021,6 +3055,10 @@ const handleSubmitAllScanned = useCallback(async () => { const scannedItemsCount = useMemo(() => { const filtered = combinedLotData.filter(lot => { const status = lot.stockOutLineStatus; + const statusLower = String(status || "").toLowerCase(); + if (statusLower === "completed" || statusLower === "complete") { + return false; + } // ✅ 与 handleSubmitAllScanned 完全保持一致 if (lot.noLot === true) { return status === 'checked' || @@ -3528,6 +3566,9 @@ paginatedData.map((lot, index) => { onClick={() => handleSkip(lot)} disabled={ lot.stockOutLineStatus === 'completed' || + lot.stockOutLineStatus === 'checked' || + lot.stockOutLineStatus === 'partially_completed' || + // 使用 issue form 後,禁用「Just Completed」(避免再次点击造成重复提交) (Number(lot.stockOutLineId) > 0 && issuePickedQtyBySolId[Number(lot.stockOutLineId)] !== undefined) || (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) diff --git a/src/components/FinishedGoodSearch/LotConfirmationModal.tsx b/src/components/FinishedGoodSearch/LotConfirmationModal.tsx index d5c60eb..405976c 100644 --- a/src/components/FinishedGoodSearch/LotConfirmationModal.tsx +++ b/src/components/FinishedGoodSearch/LotConfirmationModal.tsx @@ -52,7 +52,7 @@ const LotConfirmationModal: React.FC = ({ - {t("The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?")} + {t("The scanned item matches the expected item, but the lot number is different. Scan again to confirm: scan the expected lot QR to keep the suggested lot, or scan the other lot QR again to switch.")} @@ -92,13 +92,10 @@ const LotConfirmationModal: React.FC = ({ - {t("If you confirm, the system will:")} -

    -
  • {t("Update your suggested lot to the this scanned lot")}
  • -
+ {t("After you scan to choose, the system will update the pick line to the lot you confirmed.")} - {t("You can also scan again to confirm: scan the scanned lot again to switch, or scan the expected lot to continue with current lot.")} + {t("Or use the Confirm button below if you cannot scan again (same as scanning the other lot again).")} diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx index 1666809..870adf7 100644 --- a/src/components/Jodetail/newJobPickExecution.tsx +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -464,6 +464,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const [searchQuery, setSearchQuery] = useState>({}); // issue form 里填的 actualPickQty(用于 submit/batch submit 不补拣到 required) const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState>({}); + const [localSolStatusById, setLocalSolStatusById] = useState>({}); // 防止同一行(以 stockOutLineId/solId 识别)被重复点击提交/完成 const [actionBusyBySolId, setActionBusyBySolId] = useState>({}); @@ -646,20 +647,22 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { // 前端覆盖:issue form/submit0 不会立刻改写后端 qty 时,用本地缓存让 UI 与 batch submit 计算一致 return lots.map((lot: any) => { const solId = Number(lot.stockOutLineId) || 0; - if (solId > 0 && Object.prototype.hasOwnProperty.call(issuePickedQtyBySolId, solId)) { - const picked = Number(issuePickedQtyBySolId[solId] ?? 0); - const status = String(lot.stockOutLineStatus || '').toLowerCase(); + if (solId > 0) { + const hasPickedOverride = Object.prototype.hasOwnProperty.call(issuePickedQtyBySolId, solId); + const picked = Number(issuePickedQtyBySolId[solId] ?? lot.actualPickQty ?? 0); + const statusRaw = localSolStatusById[solId] ?? lot.stockOutLineStatus ?? ""; + const status = String(statusRaw).toLowerCase(); const isEnded = status === 'completed' || status === 'rejected'; return { ...lot, - actualPickQty: picked, - stockOutLineQty: picked, - stockOutLineStatus: isEnded ? lot.stockOutLineStatus : 'checked', + actualPickQty: hasPickedOverride ? picked : lot.actualPickQty, + stockOutLineQty: hasPickedOverride ? picked : lot.stockOutLineQty, + stockOutLineStatus: isEnded ? statusRaw : (statusRaw || "checked"), }; } return lot; }); - }, [jobOrderData, getAllLotsFromHierarchical, issuePickedQtyBySolId]); + }, [jobOrderData, getAllLotsFromHierarchical, issuePickedQtyBySolId, localSolStatusById]); const originalCombinedData = useMemo(() => { return getAllLotsFromHierarchical(jobOrderData); @@ -1802,6 +1805,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { console.error("No stock out line found for this lot"); return; } + const solId = Number(lot.stockOutLineId) || 0; try { if (currentUserId && lot.pickOrderId && lot.itemId) { @@ -1842,13 +1846,13 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { // 记录该 SOL 的“目标实际拣货量=0”,让 batch submit 走 onlyComplete(不补拣到 required) - const solId = Number(lot.stockOutLineId) || 0; if (solId > 0) { setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: 0 })); + setLocalSolStatusById(prev => ({ ...prev, [solId]: 'checked' })); } const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; - await fetchJobOrderData(pickOrderId); + void fetchJobOrderData(pickOrderId); console.log("All zeros submission marked as checked successfully (waiting for batch submit)."); setTimeout(() => { @@ -1887,6 +1891,10 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { status: newStatus, qty: cumulativeQty }); + if (solId > 0) { + setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: cumulativeQty })); + setLocalSolStatusById(prev => ({ ...prev, [solId]: newStatus })); + } if (submitQty > 0) { await updateInventoryLotLineQuantities({ @@ -1923,7 +1931,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { } const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; - await fetchJobOrderData(pickOrderId); + void fetchJobOrderData(pickOrderId); console.log("Pick quantity submitted successfully!"); setTimeout(() => { @@ -1936,15 +1944,34 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]); const handleSkip = useCallback(async (lot: any) => { try { - console.log("Skip clicked, submit 0 qty for lot:", lot.lotNo); + console.log("Just Complete clicked, mark checked with 0 qty for lot:", lot.lotNo); await handleSubmitPickQtyWithQty(lot, 0); } catch (err) { console.error("Error in Skip:", err); } }, [handleSubmitPickQtyWithQty]); + const hasPendingBatchSubmit = useMemo(() => { + return combinedLotData.some((lot) => { + const status = String(lot.stockOutLineStatus || "").toLowerCase(); + return status === "checked" || status === "pending" || status === "partially_completed" || status === "partially_complete"; + }); + }, [combinedLotData]); + useEffect(() => { + if (!hasPendingBatchSubmit) return; + const handler = (event: BeforeUnloadEvent) => { + event.preventDefault(); + event.returnValue = ""; + }; + window.addEventListener("beforeunload", handler); + return () => window.removeEventListener("beforeunload", handler); + }, [hasPendingBatchSubmit]); const handleSubmitAllScanned = useCallback(async () => { 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 @@ -2093,6 +2120,10 @@ if (onlyComplete) { const scannedItemsCount = useMemo(() => { 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) { @@ -2722,7 +2753,7 @@ const sortedData = [...sourceData].sort((a, b) => { console.error("❌ Error updating handler (non-critical):", error); } } - await handleSubmitPickQtyWithQty(lot, lot.requiredQty || lot.pickOrderLineRequiredQty || 0); + await handleSubmitPickQtyWithQty(lot, 0); } finally { if (solId > 0) { setActionBusyBySolId(prev => ({ ...prev, [solId]: false })); diff --git a/src/components/StockIssue/SearchPage.tsx b/src/components/StockIssue/SearchPage.tsx index b900f80..e509a13 100644 --- a/src/components/StockIssue/SearchPage.tsx +++ b/src/components/StockIssue/SearchPage.tsx @@ -31,6 +31,7 @@ type SearchQuery = { type SearchParamNames = keyof SearchQuery; const SearchPage: React.FC = ({ dataList }) => { + const BATCH_CHUNK_SIZE = 20; const { t } = useTranslation("inventory"); const [tab, setTab] = useState<"miss" | "bad" | "expiry">("miss"); const [search, setSearch] = useState({ lotNo: "" }); @@ -53,6 +54,7 @@ const SearchPage: React.FC = ({ dataList }) => { const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]); const [submittingIds, setSubmittingIds] = useState>(new Set()); const [batchSubmitting, setBatchSubmitting] = useState(false); + const [batchProgress, setBatchProgress] = useState<{ done: number; total: number } | null>(null); const [paging, setPaging] = useState({ pageNum: 1, pageSize: 10 }); const searchCriteria: Criterion[] = useMemo( () => [ @@ -113,7 +115,9 @@ const SearchPage: React.FC = ({ dataList }) => { // setExpiryItems(prev => prev.filter(i => i.id !== id)); window.location.reload(); } catch (e) { - alert(t("Failed to submit expiry item")); + console.error("submitExpiryItem failed:", e); + const errMsg = e instanceof Error ? e.message : t("Unknown error"); + alert(`${t("Failed to submit expiry item")}: ${errMsg}`); } return; // 记得 return,避免再走到下面的 lotId/itemId 分支 } @@ -160,26 +164,40 @@ const SearchPage: React.FC = ({ dataList }) => { if (allIds.length === 0) return; setBatchSubmitting(true); + setBatchProgress({ done: 0, total: allIds.length }); try { - if (tab === "miss") { - await batchSubmitMissItem(allIds, currentUserId); - setMissItems((prev) => prev.filter((i) => !allIds.includes(i.id))); - } else if (tab === "bad") { - await batchSubmitBadItem(allIds, currentUserId); - setBadItems((prev) => prev.filter((i) => !allIds.includes(i.id))); - } else { - await batchSubmitExpiryItem(allIds, currentUserId); - setExpiryItems((prev) => prev.filter((i) => !allIds.includes(i.id))); + for (let i = 0; i < allIds.length; i += BATCH_CHUNK_SIZE) { + const chunkIds = allIds.slice(i, i + BATCH_CHUNK_SIZE); + + if (tab === "miss") { + await batchSubmitMissItem(chunkIds, currentUserId); + setMissItems((prev) => prev.filter((item) => !chunkIds.includes(item.id))); + } else if (tab === "bad") { + await batchSubmitBadItem(chunkIds, currentUserId); + setBadItems((prev) => prev.filter((item) => !chunkIds.includes(item.id))); + } else { + await batchSubmitExpiryItem(chunkIds, currentUserId); + setExpiryItems((prev) => prev.filter((item) => !chunkIds.includes(item.id))); + } + + setBatchProgress({ + done: Math.min(i + chunkIds.length, allIds.length), + total: allIds.length, + }); } setSelectedIds([]); } catch (error) { console.error("Failed to submit selected items:", error); - alert(`Failed to submit: ${error instanceof Error ? error.message : "Unknown error"}`); + const partialDone = batchProgress?.done ?? 0; + alert( + `${t("Failed to submit")}: ${error instanceof Error ? error.message : "Unknown error"} (${partialDone}/${allIds.length})` + ); } finally { setBatchSubmitting(false); + setBatchProgress(null); } - }, [tab, currentUserId, missItems, badItems, expiryItems, filterBySearch]); + }, [tab, currentUserId, missItems, badItems, expiryItems, filterBySearch, batchProgress, t]); const missColumns = useMemo[]>( () => [ @@ -375,7 +393,9 @@ const SearchPage: React.FC = ({ dataList }) => { onClick={handleSubmitSelected} disabled={batchSubmitting || !currentUserId} > - {batchSubmitting ? t("Disposing...") : t("Batch Disposed All")} + {batchSubmitting + ? `${t("Disposing...")} ${batchProgress ? `(${batchProgress.done}/${batchProgress.total})` : ""}` + : t("Batch Disposed All")} )} diff --git a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx index 861d152..e7e7588 100644 --- a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx +++ b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx @@ -195,6 +195,14 @@ const ApproverStockTakeAll: React.FC = ({ calculateDifference, ]); + const sortedDetails = useMemo(() => { + return [...filteredDetails].sort((a, b) => { + const sectionA = (a.stockTakeSection || "").trim(); + const sectionB = (b.stockTakeSection || "").trim(); + return sectionA.localeCompare(sectionB, undefined, { numeric: true, sensitivity: "base" }); + }); + }, [filteredDetails]); + const handleSaveApproverStockTake = useCallback( async (detail: InventoryLotDetailResponse) => { if (mode === "approved") return; @@ -493,22 +501,24 @@ const ApproverStockTakeAll: React.FC = ({ {t("Stock Take Qty(include Bad Qty)= Available Qty")} + {t("Remark")} {t("Record Status")} + {t("Picker")} {t("Action")} - {filteredDetails.length === 0 ? ( + {sortedDetails.length === 0 ? ( - + {t("No data")} ) : ( - filteredDetails.map((detail) => { + sortedDetails.map((detail) => { const hasFirst = detail.firstStockTakeQty != null && detail.firstStockTakeQty >= 0; const hasSecond = @@ -519,8 +529,11 @@ const ApproverStockTakeAll: React.FC = ({ return ( - {detail.warehouseArea || "-"} - {detail.warehouseSlot || "-"} + + {detail.stockTakeSection || "-"} {detail.stockTakeSectionDescription || "-"} + + {detail.warehouseCode || "-"} + = ({ /> )} + {detail.stockTakerName || "-"} {mode === "pending" && detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && ( diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index f6c307e..8325296 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -314,6 +314,7 @@ "QR code does not match any item in current orders.":"QR 碼不符合當前訂單中的任何貨品。", "Lot Number Mismatch":"批次號碼不符", "The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?":"掃描的貨品與預期的貨品相同,但批次號碼不同。您是否要繼續使用不同的批次?", + "The scanned item matches the expected item, but the lot number is different. Scan again to confirm: scan the expected lot QR to keep the suggested lot, or scan the other lot QR again to switch.":"掃描貨品相同但批次不同。請再掃描一次以確認:掃描「建議批次」的 QR 可沿用該批次;再掃描「另一批次」的 QR 則切換為該批次。", "Expected Lot:":"預期批次:", "Scanned Lot:":"掃描批次:", "Confirm":"確認", @@ -324,6 +325,8 @@ "Print DN Label":"列印送貨單標籤", "Print All Draft" : "列印全部草稿", "If you confirm, the system will:":"如果您確認,系統將:", + "After you scan to choose, the system will update the pick line to the lot you confirmed.":"確認後,系統會將您選擇的批次套用到對應提料行。", + "Or use the Confirm button below if you cannot scan again (same as scanning the other lot again).":"若無法再掃描,可按下「確認」以切換為剛才掃描到的批次(與再掃一次該批次 QR 相同)。", "QR code verified.":"QR 碼驗證成功。", "Order Finished":"訂單完成", "Submitted Status":"提交狀態",