@@ -76,6 +76,13 @@ function isLotAvailabilityExpired(lot: any): boolean {
return String(lot?.lotAvailability || "").toLowerCase() === "expired";
}
/** inventory_lot_line.status = unavailable(API 可能用 lotAvailability 或 lotStatus) */
function isInventoryLotLineUnavailable(lot: any): boolean {
if (!lot) return false;
if (lot.lotAvailability === "status_unavailable") return true;
return String(lot.lotStatus || "").toLowerCase() === "unavailable";
}
const JO_ISSUE_PICKED_KEY = (pickOrderId: number) =>
`fpsms-jo-issuePickedQty:${pickOrderId}`;
@@ -471,6 +478,7 @@ const QrCodeModal: React.FC<{
const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const { t } = useTranslation("jo");
const { t: tPick } = useTranslation("pickOrder");
const router = useRouter();
const { data: session } = useSession() as { data: SessionWithTokens | null };
@@ -486,6 +494,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
const [lotConfirmationError, setLotConfirmationError] = useState<string | null>(null);
const [expectedLotData, setExpectedLotData] = useState<any>(null);
const [scannedLotData, setScannedLotData] = useState<any>(null);
const [isConfirmingLot, setIsConfirmingLot] = useState(false);
@@ -1114,6 +1123,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
console.log("✅ [LOT CONFIRM] Selected lot for QR:", selectedLotForQr);
setIsConfirmingLot(true);
setLotConfirmationError(null);
try {
let newLotLineId = scannedLotData?.inventoryLotLineId;
if (!newLotLineId && scannedLotData?.stockInLineId) {
@@ -1171,9 +1181,16 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
console.log("✅ [LOT CONFIRM] updateStockOutLineStatusByQRCodeAndLotNo result:", res);
const ok = res?.code === "checked" || res?.code === "SUCCESS";
if (!ok) {
const errMsg =
res?.code === "LOT_UNAVAILABLE"
? tPick(
"The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated."
)
: res?.message || tPick("Lot switch failed; pick line was not marked as checked.");
setLotConfirmationError(errMsg);
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(res?.message || "换批失败:无法更新 stock out line");
setQrScanErrorMsg(errMsg );
return;
}
} else {
@@ -1194,12 +1211,17 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
// Keep modal open so user can cancel/rescan.
if (!substitutionResult || substitutionResult.code !== "SUCCESS") {
console.error("❌ [LOT CONFIRM] Lot substitution failed. Will NOT update stockOutLine status.");
const errMsg =
substitutionResult?.code === "LOT_UNAVAILABLE"
? tPick(
"The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated."
)
: substitutionResult?.message ||
`换批失败:stockInLineId ${scannedLotData?.stockInLineId ?? ""} 不存在或无法匹配`;
setLotConfirmationError(errMsg);
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(
substitutionResult?.message ||
`换批失败:stockInLineId ${scannedLotData?.stockInLineId ?? ""} 不存在或无法匹配`
);
setQrScanErrorMsg(errMsg);
return;
}
}
@@ -1262,15 +1284,17 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
} catch (error) {
console.error("Error confirming lot substitution:", error);
const errMsg = tPick("Lot confirmation failed. Please try again.");
setLotConfirmationError(errMsg);
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg('换批发生异常,请重试或联系管理员' );
setQrScanErrorMsg(errMsg );
// Clear refresh flag on error
setIsRefreshingData(false);
} finally {
setIsConfirmingLot(false);
}
}, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData,currentUserId, updateHandledBy ]);
}, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData, currentUserId, updateHandledBy, tPick ]);
const processOutsideQrCode = useCallback(async (latestQr: string) => {
// ✅ Only JSON QR supported for outside scanner (avoid false positive with lotNo)
@@ -1315,7 +1339,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const isRejected =
scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
scannedLot.lotAvailability === 'rejected' ||
scannedLot.lotAvailability === 'status_unavailable' ;
isInventoryLotLineUnavailable(scannedLot) ;
if (isRejected) {
console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`);
@@ -1335,6 +1359,28 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
});
return;
}
const isExpired =
String(scannedLot.lotAvailability || "").toLowerCase() === "expired";
if (isExpired) {
console.warn(
`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is expired`
);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(
`此批次(${scannedLot.lotNo || scannedStockInLineId})已过期,无法使用。请扫描其他批次。`
);
});
setProcessedQrCombinations((prev) => {
const newMap = new Map(prev);
if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
newMap.get(scannedItemId)!.add(scannedStockInLineId);
return newMap;
});
return;
}
}
// ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching
@@ -1489,10 +1535,16 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
return;
}
// ✅ mismatch: validate scanned stockInLineId exists before opening confirmation modal
console.log(`⚠️ [QR PROCESS] No exact match found. Validating scanned stockInLineId ${scannedStockInLineId} for itemId ${scannedItemId}`);
console.log(`⚠️ [QR PROCESS] Active suggested lots for itemId ${scannedItemId}:`, activeSuggestedLots.map(l => ({ lotNo: l.lotNo, stockInLineId: l.stockInLineId })));
// ✅ mismatch: align with GoodPickExecutiondetail — open LotConfirmationModal first;
// handleLotMismatch loads scanned lotNo via fetchStockInLineInfoCached in the background when lotNo is null.
console.log(
`⚠️ [QR PROCESS] No exact match found (itemId ${scannedItemId}, scanned stockInLineId ${scannedStockInLineId})`
);
console.log(
`⚠️ [QR PROCESS] Active suggested lots for itemId ${scannedItemId}:`,
activeSuggestedLots.map((l) => ({ lotNo: l.lotNo, stockInLineId: l.stockInLineId }))
);
if (activeSuggestedLots.length === 0) {
console.error(`❌ [QR PROCESS] No active suggested lots found for itemId ${scannedItemId}`);
startTransition(() => {
@@ -1504,82 +1556,28 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}
const expectedLot = activeSuggestedLots[0];
console.log(`⚠️ [QR PROCESS] Expected lot: ${expectedLot.lotNo} (stockInLineId: ${expectedLot.stockInLineId}), Scanned stockInLineId: ${scannedStockInLineId}`);
// ✅ Validate scanned stockInLineId exists before opening modal
// This ensures the backend can find the lot when user confirms
try {
console.log(`🔍 [QR PROCESS] Validating scanned stockInLineId ${scannedStockInLineId} exists...`);
const stockInLineInfo = await fetchStockInLineInfoCached(scannedStockInLineId);
console.log(`✅ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} exists, lotNo: ${stockInLineInfo.lotNo}`);
// ✅ 检查扫描的批次是否已被拒绝
const scannedLot = combinedLotData.find(
(lot: any) => lot.stockInLineId === scannedStockInLineId && lot.itemId === scannedItemId
);
if (scannedLot) {
const isRejected =
scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
scannedLot.lotAvailability === 'rejected' ||
scannedLot.lotAvailability === 'status_unavailable';
if (isRejected) {
console.warn(`⚠️ [QR PROCESS] Scanned lot ${stockInLineInfo.lotNo} (stockInLineId: ${scannedStockInLineId}) is rejected or unavailable`);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(
`此批次(${stockInLineInfo.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
);
});
// Mark as processed to prevent re-processing
setProcessedQrCombinations(prev => {
const newMap = new Map(prev);
if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
newMap.get(scannedItemId)!.add(scannedStockInLineId);
return newMap;
});
return;
}
console.log(
`⚠️ [QR PROCESS] Expected lot: ${expectedLot.lotNo} (stockInLineId: ${expectedLot.stockInLineId}), Scanned stockInLineId: ${scannedStockInLineId}`
);
console.log(
`⚠️ [QR PROCESS] Opening confirmation modal - user must confirm before any lot is marked as scanned`
);
setSelectedLotForQr(expectedLot);
handleLotMismatch(
{
lotNo: expectedLot.lotNo,
itemCode: expectedLot.itemCode,
itemName: expectedLot.itemName,
},
{
lotNo: null,
itemCode: expectedLot.itemCode,
itemName: expectedLot.itemName,
inventoryLotLineId: null,
stockInLineId: scannedStockInLineId,
}
// ✅ stockInLineId exists and is not rejected, open confirmation modal
console.log(`⚠️ [QR PROCESS] Opening confirmation modal - user must confirm before any lot is marked as scanned`);
setSelectedLotForQr(expectedLot);
handleLotMismatch(
{
lotNo: expectedLot.lotNo,
itemCode: expectedLot.itemCode,
itemName: expectedLot.itemName
},
{
lotNo: stockInLineInfo.lotNo || null, // Use fetched lotNo for display
itemCode: expectedLot.itemCode,
itemName: expectedLot.itemName,
inventoryLotLineId: null,
stockInLineId: scannedStockInLineId
}
);
} catch (error) {
// ✅ stockInLineId does NOT exist, show error immediately (don't open modal)
console.error(`❌ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} does NOT exist:`, error);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(
`扫描的 stockInLineId ${scannedStockInLineId} 不存在。请检查 QR 码是否正确,或联系管理员。`
);
});
// Mark as processed to prevent re-processing
setProcessedQrCombinations(prev => {
const newMap = new Map(prev);
if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
newMap.get(scannedItemId)!.add(scannedStockInLineId);
return newMap;
});
}
}, [filterArgs?.pickOrderId, fetchJobOrderData, handleLotMismatch, lotDataIndexes, processedQrCombinations, combinedLotData, fetchStockInLineInfoCached,currentUserId, updateHandledBy ]);
);
}, [filterArgs?.pickOrderId, fetchJobOrderData, handleLotMismatch, lotDataIndexes, processedQrCombinations, combinedLotData, currentUserId, updateHandledBy]);
// Store in refs for immediate access in qrValues effect
processOutsideQrCodeRef.current = processOutsideQrCode;
@@ -2823,14 +2821,17 @@ const sortedData = [...sourceData].sort((a, b) => {
}
}
}}
/*
disabled={
(Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) ||
(lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
isInventoryLotLineUnavailable(lot) ||
lot.lotAvailability === 'rejected') ||
lot.stockOutLineStatus === 'completed' ||
lot.stockOutLineStatus === 'pending'
}
*/
disabled={true}
sx={{
fontSize: '0.75rem',
py: 0.5,
@@ -2845,6 +2846,7 @@ const sortedData = [...sourceData].sort((a, b) => {
variant="outlined"
size="small"
onClick={() => handlePickExecutionForm(lot)}
/*
disabled={
lot.lotAvailability === "expired" ||
lot.stockOutLineStatus === "completed" ||
@@ -2853,6 +2855,8 @@ const sortedData = [...sourceData].sort((a, b) => {
(Number(lot.stockOutLineId) > 0 &&
actionBusyBySolId[Number(lot.stockOutLineId)] === true)
}
*/
disabled={true}
sx={{
fontSize: '0.7rem',
py: 0.5,
@@ -2954,6 +2958,7 @@ const sortedData = [...sourceData].sort((a, b) => {
onClose={() => {
console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`);
setLotConfirmationOpen(false);
setLotConfirmationError(null);
setExpectedLotData(null);
setScannedLotData(null);
setSelectedLotForQr(null);
@@ -2986,6 +2991,7 @@ const sortedData = [...sourceData].sort((a, b) => {
expectedLot={expectedLotData}
scannedLot={scannedLotData}
isLoading={isConfirmingLot}
errorMessage={lotConfirmationError}
/>
)}