ソースを参照

do switch lot update V1

MergeProblem1
CANCERYS\kw093 21時間前
コミット
270763a2ae
3個のファイルの変更218行の追加136行の削除
  1. +141
    -87
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  2. +65
    -49
      src/components/FinishedGoodSearch/LotConfirmationModal.tsx
  3. +12
    -0
      src/i18n/zh/pickOrder.json

+ 141
- 87
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx ファイルの表示

@@ -75,11 +75,27 @@ import GoodPickExecutionForm from "./GoodPickExecutionForm";
import FGPickOrderCard from "./FGPickOrderCard"; import FGPickOrderCard from "./FGPickOrderCard";
import LinearProgressWithLabel from "../common/LinearProgressWithLabel"; import LinearProgressWithLabel from "../common/LinearProgressWithLabel";
import ScanStatusAlert from "../common/ScanStatusAlert"; import ScanStatusAlert from "../common/ScanStatusAlert";
import { translateLotSubstitutionFailure } from "./lotSubstitutionMessage";
interface Props { interface Props {
filterArgs: Record<string, any>; filterArgs: Record<string, any>;
onSwitchToRecordTab?: () => void; onSwitchToRecordTab?: () => void;
onRefreshReleasedOrderCount?: () => void; onRefreshReleasedOrderCount?: () => void;
} }
type LotConfirmRunContext = {
expectedLotData: {
lotNo: string | null;
itemCode?: string;
itemName?: string;
};
scannedLotData: {
lotNo: string | null;
itemCode?: string;
itemName?: string;
stockInLineId: number; // 必須有,API 用
inventoryLotLineId?: number | null;
};
selectedLotForQr: any; // 與現在一樣:含 pickOrderLineId, stockOutLineId, suggestedPickLotId, itemId…
};


/** 同物料多行时,优先对「有建议批次号」的行做替换,避免误选「无批次/不足」行 */ /** 同物料多行时,优先对「有建议批次号」的行做替换,避免误选「无批次/不足」行 */
function pickExpectedLotForSubstitution(activeSuggestedLots: any[]): any | null { function pickExpectedLotForSubstitution(activeSuggestedLots: any[]): any | null {
@@ -115,12 +131,12 @@ const QrCodeModal: React.FC<{
const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false); const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
const [qrScanFailed, setQrScanFailed] = useState<boolean>(false); const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false); const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
const [scannedQrResult, setScannedQrResult] = useState<string>(''); const [scannedQrResult, setScannedQrResult] = useState<string>('');
const [fgPickOrder, setFgPickOrder] = useState<FGPickOrderResponse | null>(null); const [fgPickOrder, setFgPickOrder] = useState<FGPickOrderResponse | null>(null);
const fetchingRef = useRef<Set<number>>(new Set()); const fetchingRef = useRef<Set<number>>(new Set());
// Process scanned QR codes
useEffect(() => { useEffect(() => {
// ✅ Don't process if modal is not open // ✅ Don't process if modal is not open
if (!open) { if (!open) {
@@ -619,6 +635,10 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null); const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
const [lotConfirmationError, setLotConfirmationError] = useState<string | null>(null); const [lotConfirmationError, setLotConfirmationError] = useState<string | null>(null);
/** QR 静默换批失败时显示在对应行的 Lot# 列,key = stockOutLineId */
const [lotSwitchFailByStockOutLineId, setLotSwitchFailByStockOutLineId] = useState<
Record<number, string>
>({});
const [expectedLotData, setExpectedLotData] = useState<any>(null); const [expectedLotData, setExpectedLotData] = useState<any>(null);
const [scannedLotData, setScannedLotData] = useState<any>(null); const [scannedLotData, setScannedLotData] = useState<any>(null);
const [isConfirmingLot, setIsConfirmingLot] = useState(false); const [isConfirmingLot, setIsConfirmingLot] = useState(false);
@@ -626,7 +646,6 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null); const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]); const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);

const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false); const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
// Add these missing state variables after line 352 // Add these missing state variables after line 352
const [isManualScanning, setIsManualScanning] = useState<boolean>(false); const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
@@ -656,9 +675,10 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
const lotConfirmLastQrRef = useRef<string>(''); const lotConfirmLastQrRef = useRef<string>('');
const lotConfirmSkipNextScanRef = useRef<boolean>(false); const lotConfirmSkipNextScanRef = useRef<boolean>(false);
const lotConfirmOpenedAtRef = useRef<number>(0); const lotConfirmOpenedAtRef = useRef<number>(0);
const handleLotConfirmationRef = useRef<
((overrideScannedLot?: any, runContext?: LotConfirmRunContext) => Promise<void>) | null
>(null);

// Handle QR code button click // Handle QR code button click
const handleQrCodeClick = (pickOrderId: number) => { const handleQrCodeClick = (pickOrderId: number) => {
console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
@@ -733,33 +753,69 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
} }
}, []); }, []);


const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any, qrScanCountAtOpen?: number) => {
const handleLotMismatch = useCallback((fullExpectedLotRow: any, scannedLot: any, qrScanCountAtOpen?: number) => {
const mismatchStartTime = performance.now(); const mismatchStartTime = performance.now();
console.log(` [HANDLE LOT MISMATCH START]`); console.log(` [HANDLE LOT MISMATCH START]`);
console.log(` Start time: ${new Date().toISOString()}`); console.log(` Start time: ${new Date().toISOString()}`);
console.log("Lot mismatch detected:", { expectedLot, scannedLot });
console.log("Lot mismatch detected:", { fullExpectedLotRow, scannedLot });


lotConfirmOpenedQrCountRef.current = lotConfirmOpenedQrCountRef.current =
typeof qrScanCountAtOpen === "number" ? qrScanCountAtOpen : 1; typeof qrScanCountAtOpen === "number" ? qrScanCountAtOpen : 1;
// ✅ Use setTimeout to avoid flushSync warning - schedule modal update in next tick
// ✅ Use setTimeout to avoid flushSync warning - schedule state + silent substitution in next tick
const setTimeoutStartTime = performance.now(); const setTimeoutStartTime = performance.now();
console.time('setLotConfirmationOpen');
console.time('setLotMismatchStateAndSubstitute');
setTimeout(() => { setTimeout(() => {
const setStateStartTime = performance.now(); const setStateStartTime = performance.now();
setExpectedLotData(expectedLot);
setScannedLotData({
const expectedForDisplay = {
lotNo: fullExpectedLotRow.lotNo,
itemCode: fullExpectedLotRow.itemCode,
itemName: fullExpectedLotRow.itemName,
};
const scannedMerged = {
...scannedLot, ...scannedLot,
lotNo: scannedLot.lotNo || null, lotNo: scannedLot.lotNo || null,
});
// The QR that opened modal must NOT be treated as confirmation rescan.
};
setExpectedLotData(expectedForDisplay);
setScannedLotData(scannedMerged);
setSelectedLotForQr(fullExpectedLotRow);
// The QR that triggered mismatch must NOT be treated as confirmation rescan.
lotConfirmSkipNextScanRef.current = true; lotConfirmSkipNextScanRef.current = true;
lotConfirmOpenedAtRef.current = Date.now(); lotConfirmOpenedAtRef.current = Date.now();
setLotConfirmationOpen(true);

const sid = Number(scannedLot.stockInLineId);
if (!Number.isFinite(sid)) {
console.error(` [HANDLE LOT MISMATCH] Invalid stockInLineId for substitution: ${scannedLot.stockInLineId}`);
const errMsg = t("Lot switch failed; pick line was not marked as checked.");
const rowSol = Number(fullExpectedLotRow.stockOutLineId);
if (Number.isFinite(rowSol)) {
setLotSwitchFailByStockOutLineId((prev) => ({ ...prev, [rowSol]: errMsg }));
}
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(errMsg);
const setStateTime = performance.now() - setStateStartTime;
console.timeEnd('setLotMismatchStateAndSubstitute');
console.log(` [HANDLE LOT MISMATCH] Lot switch failed (invalid stockInLineId), setState time: ${setStateTime.toFixed(2)}ms`);
return;
}

const runContext: LotConfirmRunContext = {
expectedLotData: expectedForDisplay,
scannedLotData: {
...scannedMerged,
stockInLineId: sid,
itemCode: scannedMerged.itemCode ?? fullExpectedLotRow.itemCode,
itemName: scannedMerged.itemName ?? fullExpectedLotRow.itemName,
inventoryLotLineId: scannedLot.inventoryLotLineId ?? scannedLot.lotId ?? null,
},
selectedLotForQr: fullExpectedLotRow,
};
void handleLotConfirmationRef.current?.(undefined, runContext);

const setStateTime = performance.now() - setStateStartTime; const setStateTime = performance.now() - setStateStartTime;
console.timeEnd('setLotConfirmationOpen');
console.log(` [HANDLE LOT MISMATCH] Modal state set to open (setState time: ${setStateTime.toFixed(2)}ms)`);
console.log(`✅ [HANDLE LOT MISMATCH] Modal state set to open`);
console.timeEnd('setLotMismatchStateAndSubstitute');
console.log(` [HANDLE LOT MISMATCH] Silent lot substitution scheduled (setState time: ${setStateTime.toFixed(2)}ms)`);
}, 0); }, 0);
const setTimeoutTime = performance.now() - setTimeoutStartTime; const setTimeoutTime = performance.now() - setTimeoutStartTime;
console.log(` [PERF] setTimeout scheduling time: ${setTimeoutTime.toFixed(2)}ms`); console.log(` [PERF] setTimeout scheduling time: ${setTimeoutTime.toFixed(2)}ms`);
@@ -802,7 +858,7 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
const totalTime = performance.now() - mismatchStartTime; const totalTime = performance.now() - mismatchStartTime;
console.log(` [HANDLE LOT MISMATCH END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); console.log(` [HANDLE LOT MISMATCH END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
} }
}, [fetchStockInLineInfoCached]);
}, [fetchStockInLineInfoCached, t]);
const checkAllLotsCompleted = useCallback((lotData: any[]) => { const checkAllLotsCompleted = useCallback((lotData: any[]) => {
if (lotData.length === 0) { if (lotData.length === 0) {
setAllLotsCompleted(false); setAllLotsCompleted(false);
@@ -1314,74 +1370,75 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
} }
}, []); }, []);


const handleLotConfirmation = useCallback(async (overrideScannedLot?: any) => {
const effectiveScannedLot = overrideScannedLot ?? scannedLotData;
if (!expectedLotData || !effectiveScannedLot || !selectedLotForQr) return;
const handleLotConfirmation = useCallback(async (overrideScannedLot?: any, runContext?: LotConfirmRunContext) => {
const exp = runContext?.expectedLotData ?? expectedLotData;
const scan = overrideScannedLot ?? runContext?.scannedLotData ?? scannedLotData;
const sel = runContext?.selectedLotForQr ?? selectedLotForQr;
if (!exp || !scan || !sel) return;

const newStockInLineId = scan?.stockInLineId;
if (newStockInLineId == null || Number.isNaN(Number(newStockInLineId))) return;

const rowSolKey = Number(sel.stockOutLineId);
if (Number.isFinite(rowSolKey)) {
setLotSwitchFailByStockOutLineId((prev) => {
const next = { ...prev };
delete next[rowSolKey];
return next;
});
}

setIsConfirmingLot(true); setIsConfirmingLot(true);
setLotConfirmationError(null); setLotConfirmationError(null);
try { try {
const newLotNo = effectiveScannedLot?.lotNo;
const newStockInLineId = effectiveScannedLot?.stockInLineId;
const substitutionResult = await confirmLotSubstitution({ const substitutionResult = await confirmLotSubstitution({
pickOrderLineId: selectedLotForQr.pickOrderLineId,
stockOutLineId: selectedLotForQr.stockOutLineId,
originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId,
newInventoryLotNo: "",
newStockInLineId: newStockInLineId
pickOrderLineId: sel.pickOrderLineId,
stockOutLineId: sel.stockOutLineId,
originalSuggestedPickLotId: sel.suggestedPickLotId,
newInventoryLotNo: "",
newStockInLineId: newStockInLineId,
}); });


const substitutionCode = substitutionResult?.code; const substitutionCode = substitutionResult?.code;
const switchedToUnavailable = const switchedToUnavailable =
substitutionCode === "SUCCESS_UNAVAILABLE" || substitutionCode === "BOUND_UNAVAILABLE"; substitutionCode === "SUCCESS_UNAVAILABLE" || substitutionCode === "BOUND_UNAVAILABLE";
if (!substitutionResult || (substitutionCode !== "SUCCESS" && !switchedToUnavailable)) { if (!substitutionResult || (substitutionCode !== "SUCCESS" && !switchedToUnavailable)) {
const errMsg =
substitutionResult?.code === "LOT_UNAVAILABLE"
? t(
"The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated."
)
: substitutionResult?.message ||
t("Lot switch failed; pick line was not marked as checked.");
setLotConfirmationError(errMsg);
const errMsg = translateLotSubstitutionFailure(t, substitutionResult);
if (Number.isFinite(rowSolKey)) {
setLotSwitchFailByStockOutLineId((prev) => ({ ...prev, [rowSolKey]: errMsg }));
}
setQrScanError(true); setQrScanError(true);
setQrScanSuccess(false); setQrScanSuccess(false);
setQrScanErrorMsg(errMsg); setQrScanErrorMsg(errMsg);
return; return;
} }
setQrScanError(false); setQrScanError(false);
setQrScanSuccess(false); setQrScanSuccess(false);
setQrScanInput('');
// ✅ 修复:在确认后重置扫描状态,避免重复处理
setQrScanInput("");

resetScan(); resetScan();
// ✅ 修复:不要清空 processedQrCodes,而是保留当前 QR code 的标记
// 或者如果确实需要清空,应该在重置扫描后再清空
// setProcessedQrCodes(new Set());
// setLastProcessedQr('');

setPickExecutionFormOpen(false); setPickExecutionFormOpen(false);
if (selectedLotForQr?.stockOutLineId && !switchedToUnavailable) {
const stockOutLineUpdate = await updateStockOutLineStatus({
id: selectedLotForQr.stockOutLineId,
status: 'checked',
qty: 0
if (sel?.stockOutLineId && !switchedToUnavailable) {
await updateStockOutLineStatus({
id: sel.stockOutLineId,
status: "checked",
qty: 0,
}); });
} }
// ✅ 修复:先关闭 modal 和清空状态,再刷新数据

clearLotConfirmationState(false); clearLotConfirmationState(false);
// ✅ 修复:刷新数据前设置刷新标志,避免在刷新期间处理新的 QR code

setIsRefreshingData(true); setIsRefreshingData(true);
await fetchAllCombinedLotData(); await fetchAllCombinedLotData();
setIsRefreshingData(false); setIsRefreshingData(false);
} catch (error) { } catch (error) {
console.error("Error confirming lot substitution:", error); console.error("Error confirming lot substitution:", error);
const errMsg = t("Lot confirmation failed. Please try again."); const errMsg = t("Lot confirmation failed. Please try again.");
setLotConfirmationError(errMsg);
if (Number.isFinite(rowSolKey)) {
setLotSwitchFailByStockOutLineId((prev) => ({ ...prev, [rowSolKey]: errMsg }));
}
setQrScanError(true); setQrScanError(true);
setQrScanErrorMsg(errMsg); setQrScanErrorMsg(errMsg);
} finally { } finally {
@@ -1389,6 +1446,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
} }
}, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData, resetScan, clearLotConfirmationState, t]); }, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData, resetScan, clearLotConfirmationState, t]);


useEffect(() => {
handleLotConfirmationRef.current = handleLotConfirmation;
}, [handleLotConfirmation]);

const handleLotConfirmationByRescan = useCallback(async (rawQr: string): Promise<boolean> => { const handleLotConfirmationByRescan = useCallback(async (rawQr: string): Promise<boolean> => {
if (!lotConfirmationOpen || !selectedLotForQr || !expectedLotData || !scannedLotData) { if (!lotConfirmationOpen || !selectedLotForQr || !expectedLotData || !scannedLotData) {
return false; return false;
@@ -1923,22 +1984,17 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
) || ) ||
allLotsForItem[0]; 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
console.log(`⚠️ [QR PROCESS] Opening confirmation modal for lot switch (no active lots)`);
// Silent lot substitution; modal only if switch fails
console.log(`⚠️ [QR PROCESS] Lot switch (no active lots), attempting substitution`);
setSelectedLotForQr(expectedLot); setSelectedLotForQr(expectedLot);
handleLotMismatch( handleLotMismatch(
expectedLot,
{ {
lotNo: expectedLot.lotNo,
itemCode: expectedLot.itemCode,
itemName: expectedLot.itemName
},
{
lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null
lotNo: scannedLot?.lotNo || null,
itemCode: expectedLot.itemCode, itemCode: expectedLot.itemCode,
itemName: expectedLot.itemName, itemName: expectedLot.itemName,
inventoryLotLineId: scannedLot?.lotId || null, inventoryLotLineId: scannedLot?.lotId || null,
stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo
stockInLineId: scannedStockInLineId,
}, },
qrScanCountAtInvoke qrScanCountAtInvoke
); );
@@ -1971,20 +2027,16 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
// Check if scanned lot is different from expected, or if scannedLot is undefined (not in allLotsForItem) // Check if scanned lot is different from expected, or if scannedLot is undefined (not in allLotsForItem)
const shouldOpenModal = !scannedLot || (scannedLot.stockInLineId !== expectedLot.stockInLineId); const shouldOpenModal = !scannedLot || (scannedLot.stockInLineId !== expectedLot.stockInLineId);
if (shouldOpenModal) { if (shouldOpenModal) {
console.log(`⚠️ [QR PROCESS] Opening confirmation modal (scanned lot ${scannedLot?.lotNo || 'not in data'} is not in active suggested lots)`);
console.log(`⚠️ [QR PROCESS] Lot switch (scanned lot ${scannedLot?.lotNo || 'not in data'} not in active suggested lots)`);
setSelectedLotForQr(expectedLot); setSelectedLotForQr(expectedLot);
handleLotMismatch( handleLotMismatch(
expectedLot,
{ {
lotNo: expectedLot.lotNo,
itemCode: expectedLot.itemCode,
itemName: expectedLot.itemName
},
{
lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null
lotNo: scannedLot?.lotNo || null,
itemCode: expectedLot.itemCode, itemCode: expectedLot.itemCode,
itemName: expectedLot.itemName, itemName: expectedLot.itemName,
inventoryLotLineId: scannedLot?.lotId || null, inventoryLotLineId: scannedLot?.lotId || null,
stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo
stockInLineId: scannedStockInLineId,
}, },
qrScanCountAtInvoke qrScanCountAtInvoke
); );
@@ -2130,21 +2182,15 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
const setSelectedLotTime = performance.now() - setSelectedLotStartTime; const setSelectedLotTime = performance.now() - setSelectedLotStartTime;
console.log(` [PERF] Set selected lot time: ${setSelectedLotTime.toFixed(2)}ms`); console.log(` [PERF] Set selected lot time: ${setSelectedLotTime.toFixed(2)}ms`);
// ✅ 获取扫描的 lot 信息(从 QR 数据中提取,或使用默认值)
// Call handleLotMismatch immediately - it will open the modal
const handleMismatchStartTime = performance.now(); const handleMismatchStartTime = performance.now();
handleLotMismatch( handleLotMismatch(
expectedLot,
{ {
lotNo: expectedLot.lotNo,
itemCode: expectedLot.itemCode,
itemName: expectedLot.itemName
},
{
lotNo: null, // 扫描的 lotNo 未知,需要从后端获取或显示为未知
lotNo: null,
itemCode: expectedLot.itemCode, itemCode: expectedLot.itemCode,
itemName: expectedLot.itemName, itemName: expectedLot.itemName,
inventoryLotLineId: null, inventoryLotLineId: null,
stockInLineId: scannedStockInLineId // ✅ 传递 stockInLineId
stockInLineId: scannedStockInLineId,
}, },
qrScanCountAtInvoke qrScanCountAtInvoke
); );
@@ -3608,7 +3654,10 @@ const handleSubmitAllScanned = useCallback(async () => {
paginatedData.map((lot, index) => { paginatedData.map((lot, index) => {
// 检查是否是 issue lot // 检查是否是 issue lot
const isIssueLot = lot.stockOutLineStatus === 'rejected' || !lot.lotNo; const isIssueLot = lot.stockOutLineStatus === 'rejected' || !lot.lotNo;
const rowSolId = Number(lot.stockOutLineId);
const lotSwitchErr =
Number.isFinite(rowSolId) ? lotSwitchFailByStockOutLineId[rowSolId] : undefined;

return ( return (
<TableRow <TableRow
key={`${lot.pickOrderLineId}-${lot.lotId || 'null'}`} key={`${lot.pickOrderLineId}-${lot.lotId || 'null'}`}
@@ -3664,6 +3713,11 @@ paginatedData.map((lot, index) => {
) )
)} )}
</Typography> </Typography>
{lotSwitchErr ? (
<Typography variant="body2" color="error" sx={{ mt: 0.5, display: 'block', fontWeight: 500 }}>
{lotSwitchErr}
</Typography>
) : null}
</Box> </Box>
</TableCell> </TableCell>
<TableCell align="right"> <TableCell align="right">


+ 65
- 49
src/components/FinishedGoodSearch/LotConfirmationModal.tsx ファイルの表示

@@ -19,14 +19,14 @@ interface LotConfirmationModalProps {
onClose: () => void; onClose: () => void;
onConfirm: () => void; onConfirm: () => void;
expectedLot: { expectedLot: {
lotNo: string;
itemCode: string;
itemName: string;
lotNo: string | null;
itemCode?: string;
itemName?: string;
}; };
scannedLot: { scannedLot: {
lotNo: string;
itemCode: string;
itemName: string;
lotNo: string | null;
itemCode?: string;
itemName?: string;
}; };
isLoading?: boolean; isLoading?: boolean;
/** Shown inside the dialog when confirm/switch API fails (e.g. LOT_UNAVAILABLE). */ /** Shown inside the dialog when confirm/switch API fails (e.g. LOT_UNAVAILABLE). */
@@ -43,24 +43,25 @@ const LotConfirmationModal: React.FC<LotConfirmationModalProps> = ({
errorMessage = null, errorMessage = null,
}) => { }) => {
const { t } = useTranslation("pickOrder"); const { t } = useTranslation("pickOrder");
const isFailure = Boolean(errorMessage);


return ( return (
<Dialog <Dialog
open={open}
onClose={onClose}
maxWidth="md"
fullWidth
disableScrollLock
disableAutoFocus
disableEnforceFocus
disableRestoreFocus
>
open={open}
onClose={onClose}
maxWidth="md"
fullWidth
disableScrollLock
disableAutoFocus
disableEnforceFocus
disableRestoreFocus
>
<DialogTitle> <DialogTitle>
<Typography variant="h6" component="div" color="warning.main">
{t("Lot Number Mismatch")}
</Typography>
</DialogTitle>
<Typography variant="h6" component="div" color={isFailure ? "error" : "warning.main"}>
{isFailure ? t("Lot switch failed") : t("Lot Number Mismatch")}
</Typography>
</DialogTitle>
<DialogContent> <DialogContent>
<Stack spacing={3}> <Stack spacing={3}>
{errorMessage ? ( {errorMessage ? (
@@ -68,23 +69,33 @@ const LotConfirmationModal: React.FC<LotConfirmationModalProps> = ({
{t(errorMessage)} {t(errorMessage)}
</Alert> </Alert>
) : null} ) : null}
<Alert severity="warning">
{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>
{isFailure ? (
<Alert severity="warning">
{t(
"The system could not switch to the scanned lot. Review the lots below, then tap Confirm to retry."
)}
</Alert>
) : (
<Alert severity="warning">
{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> <Box>
<Typography variant="subtitle1" gutterBottom color="primary"> <Typography variant="subtitle1" gutterBottom color="primary">
{t("Expected Lot:")} {t("Expected Lot:")}
</Typography> </Typography>
<Box sx={{ pl: 2, py: 1, backgroundColor: 'grey.50', borderRadius: 1 }}>
<Box sx={{ pl: 2, py: 1, backgroundColor: "grey.50", borderRadius: 1 }}>
<Typography variant="body2"> <Typography variant="body2">
<strong>{t("Item Code")}:</strong> {expectedLot.itemCode}
<strong>{t("Item Code")}:</strong> {expectedLot.itemCode ?? "—"}
</Typography> </Typography>
<Typography variant="body2"> <Typography variant="body2">
<strong>{t("Item Name")}:</strong> {expectedLot.itemName}
<strong>{t("Item Name")}:</strong> {expectedLot.itemName ?? "—"}
</Typography> </Typography>
<Typography variant="body2"> <Typography variant="body2">
<strong>{t("Lot No")}:</strong> {expectedLot.lotNo}
<strong>{t("Lot No")}:</strong> {expectedLot.lotNo ?? "—"}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@@ -95,42 +106,47 @@ const LotConfirmationModal: React.FC<LotConfirmationModalProps> = ({
<Typography variant="subtitle1" gutterBottom color="warning.main"> <Typography variant="subtitle1" gutterBottom color="warning.main">
{t("Scanned Lot:")} {t("Scanned Lot:")}
</Typography> </Typography>
<Box sx={{ pl: 2, py: 1, backgroundColor: 'warning.50', borderRadius: 1 }}>
<Box sx={{ pl: 2, py: 1, backgroundColor: "warning.50", borderRadius: 1 }}>
<Typography variant="body2"> <Typography variant="body2">
<strong>{t("Item Code")}:</strong> {scannedLot.itemCode}
<strong>{t("Item Code")}:</strong> {scannedLot.itemCode ?? "—"}
</Typography> </Typography>
<Typography variant="body2"> <Typography variant="body2">
<strong>{t("Item Name")}:</strong> {scannedLot.itemName}
<strong>{t("Item Name")}:</strong> {scannedLot.itemName ?? "—"}
</Typography> </Typography>
<Typography variant="body2"> <Typography variant="body2">
<strong>{t("Lot No")}:</strong> {scannedLot.lotNo}
<strong>{t("Lot No")}:</strong> {scannedLot.lotNo ?? "—"}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>


<Alert severity="info">
{t("After you scan to choose, the system will update the pick line to the lot you confirmed.")}
</Alert>
<Alert severity="info">
{t("Or use the Confirm button below if you cannot scan again (same as scanning the other lot again).")}
</Alert>
{isFailure ? (
<Alert severity="info">
{t(
"You can also scan again: expected lot QR keeps the suggested line; scanned lot QR retries the switch."
)}
</Alert>
) : (
<>
<Alert severity="info">
{t(
"After you scan to choose, the system will update the pick line to the lot you confirmed."
)}
</Alert>
<Alert severity="info">
{t(
"Or use the Confirm button below if you cannot scan again (same as scanning the other lot again)."
)}
</Alert>
</>
)}
</Stack> </Stack>
</DialogContent> </DialogContent>


<DialogActions> <DialogActions>
<Button
onClick={onClose}
variant="outlined"
disabled={isLoading}
>
<Button onClick={onClose} variant="outlined" disabled={isLoading}>
{t("Cancel")} {t("Cancel")}
</Button> </Button>
<Button
onClick={onConfirm}
variant="contained"
color="warning"
disabled={isLoading}
>
<Button onClick={onConfirm} variant="contained" color="warning" disabled={isLoading}>
{isLoading ? t("Processing...") : t("Confirm")} {isLoading ? t("Processing...") : t("Confirm")}
</Button> </Button>
</DialogActions> </DialogActions>
@@ -138,4 +154,4 @@ const LotConfirmationModal: React.FC<LotConfirmationModalProps> = ({
); );
}; };


export default LotConfirmationModal;
export default LotConfirmationModal;

+ 12
- 0
src/i18n/zh/pickOrder.json ファイルの表示

@@ -329,6 +329,9 @@
"If you confirm, the system will:":"如果您確認,系統將:", "If you confirm, the system will:":"如果您確認,系統將:",
"After you scan to choose, the system will update the pick line to the lot you confirmed.":"確認後,系統會將您選擇的批次套用到對應提料行。", "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 相同)。", "Or use the Confirm button below if you cannot scan again (same as scanning the other lot again).":"若無法再掃描,可按下「確認」以切換為剛才掃描到的批次(與再掃一次該批次 QR 相同)。",
"Lot switch failed":"批次切換失敗",
"The system could not switch to the scanned lot. Review the lots below, then tap Confirm to retry.":"系統無法切換至掃描的批次。請核對下方批次後按「確認」重試。",
"You can also scan again: expected lot QR keeps the suggested line; scanned lot QR retries the switch.":"您也可以再掃描:掃描建議批次 QR 可保留該行;掃描欲切換批次 QR 可再次嘗試切換。",
"QR code verified.":"QR 碼驗證成功。", "QR code verified.":"QR 碼驗證成功。",
"Order Finished":"訂單完成", "Order Finished":"訂單完成",
"Submitted Status":"提交狀態", "Submitted Status":"提交狀態",
@@ -465,5 +468,14 @@
"Lot switch failed; pick line was not marked as checked.": "換批失敗;揀貨行未標為已核對。", "Lot switch failed; pick line was not marked as checked.": "換批失敗;揀貨行未標為已核對。",
"Lot confirmation failed. Please try again.": "確認批號失敗,請重試。", "Lot confirmation failed. Please try again.": "確認批號失敗,請重試。",
"Powder Mixture": "箱料粉", "Powder Mixture": "箱料粉",
"This lot is not yet putaway": "此批次尚未上架",
"Cannot resolve new inventory lot line": "無法解析新批號庫存行(請確認已上架且資料正確)。",
"Pick order line item is null": "提料單行未關聯物料。",
"New lot line item does not match pick order line item": "新批號行的物料與提料單行不一致。",
"Pick order line {{id}} not found": "找不到有關提料單行的資料。",
"SuggestedPickLot not found for pickOrderLineId {{polId}}": "找不到該提料單行的建議揀貨批",
"SuggestedPickLot qty is invalid: {{qty}}": "建議揀貨數量無效:{{qty}}。",
"Reject switch lot: available {{available}} less than required {{required}}": "此批次貨品已被其他送貨單留起,請掃描其他批次。",
"Reject switch lot: picked {{picked}} already greater or equal required {{required}}": "換批被拒:已揀數量({{picked}})已達或超過建議量({{required}}),無法再拆分換批。",
"Lot status is unavailable. Cannot switch or bind; pick line was not updated.": "批號狀態為「不可用」,無法換批或綁定;揀貨行未更新。" "Lot status is unavailable. Cannot switch or bind; pick line was not updated.": "批號狀態為「不可用」,無法換批或綁定;揀貨行未更新。"
} }

読み込み中…
キャンセル
保存