Kaynağa Gözat

update

MergeProblem1
CANCERYS\kw093 19 saat önce
ebeveyn
işleme
b01f7eda9f
8 değiştirilmiş dosya ile 274 ekleme ve 121 silme
  1. +3
    -0
      src/app/api/stockTake/actions.ts
  2. +54
    -10
      src/components/DoSearch/DoSearch.tsx
  3. +116
    -75
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  4. +3
    -6
      src/components/FinishedGoodSearch/LotConfirmationModal.tsx
  5. +43
    -12
      src/components/Jodetail/newJobPickExecution.tsx
  6. +33
    -13
      src/components/StockIssue/SearchPage.tsx
  7. +19
    -5
      src/components/StockTakeManagement/ApproverStockTakeAll.tsx
  8. +3
    -0
      src/i18n/zh/pickOrder.json

+ 3
- 0
src/app/api/stockTake/actions.ts Dosyayı Görüntüle

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


+ 54
- 10
src/components/DoSearch/DoSearch.tsx Dosyayı Görüntüle

@@ -58,6 +58,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea
const formProps = useForm<CreateConsoDoInput>({
defaultValues: {},
});
const { setValue } = formProps;
const errors = formProps.formState.errors;

@@ -68,8 +69,8 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea
console.log("🔍 DoSearch - session:", session);
console.log("🔍 DoSearch - currentUserId:", currentUserId);
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
const [rowSelectionModel, setRowSelectionModel] =
useState<GridRowSelectionModel>([]);
/** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */
const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]);

const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]);
const [totalCount, setTotalCount] = useState(0);
@@ -101,6 +102,37 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea
const [hasSearched, setHasSearched] = useState(false);
const [hasResults, setHasResults] = useState(false);

const excludedIdSet = useMemo(() => new Set(excludedRowIds), [excludedRowIds]);

const rowSelectionModel = useMemo<GridRowSelectionModel>(() => {
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<Props> = ({ 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: `
<div>
<p>${t("Selected Shop(s): ")}${allMatchingDos.length}</p>
<p>${t("Selected Shop(s): ")}${idsToRelease.length}</p>
<p style="font-size: 0.9em; color: #666; margin-top: 8px;">
${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,


+ 116
- 75
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx Dosyayı Görüntüle

@@ -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<any[]>([]);
// issue form 里填的 actualPickQty(用于 batch submit 只提交实际拣到数量,而不是补拣到 required)
const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState<Record<number, number>>({});
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<Record<number, boolean>>({});
@@ -571,12 +604,11 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
const lastProcessedQrRef = useRef<string>('');
// Store callbacks in refs to avoid useEffect dependency issues
const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | null>(null);
const processOutsideQrCodeRef = useRef<
((latestQr: string, qrScanCountAtInvoke?: number) => Promise<void>) | null
>(null);
const resetScanRef = useRef<(() => void) | null>(null);
const lotConfirmOpenedQrCountRef = useRef<number>(0);
const lotConfirmOpenedQrValueRef = useRef<string>('');
const lotConfirmInitialSameQrSkippedRef = useRef<boolean>(false);
const autoConfirmInProgressRef = useRef<boolean>(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<number | null>(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)


+ 3
- 6
src/components/FinishedGoodSearch/LotConfirmationModal.tsx Dosyayı Görüntüle

@@ -52,7 +52,7 @@ const LotConfirmationModal: React.FC<LotConfirmationModalProps> = ({
<DialogContent>
<Stack spacing={3}>
<Alert severity="warning">
{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.")}
</Alert>

<Box>
@@ -92,13 +92,10 @@ const LotConfirmationModal: React.FC<LotConfirmationModalProps> = ({
</Box>

<Alert severity="info">
{t("If you confirm, the system will:")}
<ul style={{ margin: '8px 0 0 16px' }}>
<li>{t("Update your suggested lot to the this scanned lot")}</li>
</ul>
{t("After you scan to choose, the system will update the pick line to the lot you confirmed.")}
</Alert>
<Alert severity="info">
{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).")}
</Alert>
</Stack>
</DialogContent>


+ 43
- 12
src/components/Jodetail/newJobPickExecution.tsx Dosyayı Görüntüle

@@ -464,6 +464,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
// issue form 里填的 actualPickQty(用于 submit/batch submit 不补拣到 required)
const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState<Record<number, number>>({});
const [localSolStatusById, setLocalSolStatusById] = useState<Record<number, string>>({});
// 防止同一行(以 stockOutLineId/solId 识别)被重复点击提交/完成
const [actionBusyBySolId, setActionBusyBySolId] = useState<Record<number, boolean>>({});

@@ -646,20 +647,22 @@ const JobPickExecution: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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 }));


+ 33
- 13
src/components/StockIssue/SearchPage.tsx Dosyayı Görüntüle

@@ -31,6 +31,7 @@ type SearchQuery = {
type SearchParamNames = keyof SearchQuery;

const SearchPage: React.FC<Props> = ({ dataList }) => {
const BATCH_CHUNK_SIZE = 20;
const { t } = useTranslation("inventory");
const [tab, setTab] = useState<"miss" | "bad" | "expiry">("miss");
const [search, setSearch] = useState<SearchQuery>({ lotNo: "" });
@@ -53,6 +54,7 @@ const SearchPage: React.FC<Props> = ({ dataList }) => {
const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
const [submittingIds, setSubmittingIds] = useState<Set<number>>(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<SearchParamNames>[] = useMemo(
() => [
@@ -113,7 +115,9 @@ const SearchPage: React.FC<Props> = ({ 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<Props> = ({ 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<Column<StockIssueResult>[]>(
() => [
@@ -375,7 +393,9 @@ const SearchPage: React.FC<Props> = ({ 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")}
</Button>
</Box>
)}


+ 19
- 5
src/components/StockTakeManagement/ApproverStockTakeAll.tsx Dosyayı Görüntüle

@@ -195,6 +195,14 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
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<ApproverStockTakeAllProps> = ({
<TableCell>
{t("Stock Take Qty(include Bad Qty)= Available Qty")}
</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Picker")}</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredDetails.length === 0 ? (
{sortedDetails.length === 0 ? (
<TableRow>
<TableCell colSpan={7} align="center">
<TableCell colSpan={8} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
</TableCell>
</TableRow>
) : (
filteredDetails.map((detail) => {
sortedDetails.map((detail) => {
const hasFirst =
detail.firstStockTakeQty != null && detail.firstStockTakeQty >= 0;
const hasSecond =
@@ -519,8 +529,11 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
return (
<TableRow key={detail.id}>
<TableCell>
{detail.warehouseArea || "-"}
{detail.warehouseSlot || "-"}
<Stack spacing={0.5}>
<Typography variant="body2"><strong>{detail.stockTakeSection || "-"} {detail.stockTakeSectionDescription || "-"}</strong></Typography>
<Typography variant="body2">{detail.warehouseCode || "-"}</Typography>
</Stack>
</TableCell>
<TableCell
sx={{
@@ -792,6 +805,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
/>
)}
</TableCell>
<TableCell>{detail.stockTakerName || "-"}</TableCell>
<TableCell>
{mode === "pending" && detail.stockTakeRecordId &&
detail.stockTakeRecordStatus !== "notMatch" && (


+ 3
- 0
src/i18n/zh/pickOrder.json Dosyayı Görüntüle

@@ -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":"提交狀態",


Yükleniyor…
İptal
Kaydet