Przeglądaj źródła

update some jo qr

MergeProblem1
CANCERYS\kw093 1 tydzień temu
rodzic
commit
4fc7e87375
4 zmienionych plików z 711 dodań i 345 usunięć
  1. +0
    -107
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  2. +142
    -40
      src/components/Jodetail/JobPickExecutionForm.tsx
  3. +567
    -198
      src/components/Jodetail/newJobPickExecution.tsx
  4. +2
    -0
      src/i18n/zh/jo.json

+ 0
- 107
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx Wyświetl plik

@@ -2120,114 +2120,7 @@ useEffect(() => {
}
}, [currentUserId]);

// Handle submit pick quantity
const handleSubmitPickQty = useCallback(async (lot: any) => {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const newQty = pickQtyData[lotKey] || 0;
if (!lot.stockOutLineId) {
console.error("No stock out line found for this lot");
return;
}
try {
// FIXED: Calculate cumulative quantity correctly
const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + newQty;
// FIXED: Determine status based on cumulative quantity vs required quantity
let newStatus = 'partially_completed';
if (cumulativeQty >= lot.requiredQty) {
newStatus = 'completed';
} else if (cumulativeQty > 0) {
newStatus = 'partially_completed';
} else {
newStatus = 'checked'; // QR scanned but no quantity submitted yet
}
console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
console.log(`Lot: ${lot.lotNo}`);
console.log(`Required Qty: ${lot.requiredQty}`);
console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
console.log(`New Submitted Qty: ${newQty}`);
console.log(`Cumulative Qty: ${cumulativeQty}`);
console.log(`New Status: ${newStatus}`);
console.log(`=====================================`);
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: newStatus,
qty: cumulativeQty // Use cumulative quantity
});
if (newQty > 0) {
await updateInventoryLotLineQuantities({
inventoryLotLineId: lot.lotId,
qty: newQty,
status: 'available',
operation: 'pick'
});
}
// Check if pick order is completed when lot status becomes 'completed'
if (newStatus === 'completed' && lot.pickOrderConsoCode) {
console.log(` Lot ${lot.lotNo} completed, 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("Pick quantity submitted successfully!");
setTimeout(() => {
checkAndAutoAssignNext();
}, 1000);
} catch (error) {
console.error("Error submitting pick quantity:", error);
}
}, [pickQtyData, fetchAllCombinedLotData, checkAndAutoAssignNext]);

// Handle reject lot
const handleRejectLot = useCallback(async (lot: any) => {
if (!lot.stockOutLineId) {
console.error("No stock out line found for this lot");
return;
}
try {
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: 'rejected',
qty: 0
});
await fetchAllCombinedLotData();
console.log("Lot rejected successfully!");
setTimeout(() => {
checkAndAutoAssignNext();
}, 1000);
} catch (error) {
console.error("Error rejecting lot:", error);
}
}, [fetchAllCombinedLotData, checkAndAutoAssignNext]);

// Handle pick execution form
const handlePickExecutionForm = useCallback((lot: any) => {
console.log("=== Pick Execution Form ===");


+ 142
- 40
src/components/Jodetail/JobPickExecutionForm.tsx Wyświetl plik

@@ -70,6 +70,7 @@ interface FormErrors {
badItemQty?: string;
issueRemark?: string;
handledBy?: string;
badReason?: string;
}

const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
@@ -163,8 +164,12 @@ useEffect(() => {
actualPickQty: initialVerifiedQty,
missQty: 0,
badItemQty: 0,
issueRemark: '',
badPackageQty: 0, // Bad Package Qty (frontend only)
issueRemark: "",
pickerName: "",
handledBy: undefined,
reason: "",
badReason: "",
});
}
// 只在 open 状态改变时重新初始化,移除其他依赖
@@ -185,30 +190,51 @@ useEffect(() => {
}
}, [errors]);

// Update form validation to require either missQty > 0 OR badItemQty > 0
// Updated validation logic (same as GoodPickExecutionForm)
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
const requiredQty = selectedLot?.requiredQty || 0;
const badItemQty = formData.badItemQty || 0;
const missQty = formData.missQty || 0;
if (verifiedQty === undefined || verifiedQty < 0) {
newErrors.actualPickQty = t('Qty is required');
const ap = Number(verifiedQty) || 0;
const miss = Number(formData.missQty) || 0;
const badItem = Number(formData.badItemQty) || 0;
const badPackage = Number((formData as any).badPackageQty) || 0;
const totalBad = badItem + badPackage;
const total = ap + miss + totalBad;
const availableQty = selectedLot?.availableQty || 0;

// 1. Check actualPickQty cannot be negative
if (ap < 0) {
newErrors.actualPickQty = t("Qty cannot be negative");
}
const totalQty = verifiedQty + badItemQty + missQty;
const hasAnyValue = verifiedQty > 0 || badItemQty > 0 || missQty > 0;
// ✅ 新增:必须至少有一个 > 0
if (!hasAnyValue) {
newErrors.actualPickQty = t('At least one of Verified / Missing / Bad must be greater than 0');

// 2. Check actualPickQty cannot exceed available quantity
if (ap > availableQty) {
newErrors.actualPickQty = t("Actual pick qty cannot exceed available qty");
}
if (hasAnyValue && totalQty !== requiredQty) {
newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity');

// 3. Check missQty and both bad qtys cannot be negative
if (miss < 0) {
newErrors.missQty = t("Invalid qty");
}
if (badItem < 0 || badPackage < 0) {
newErrors.badItemQty = t("Invalid qty");
}

// 4. Total (actualPickQty + missQty + badItemQty + badPackageQty) cannot exceed lot available qty
if (total > availableQty) {
const errorMsg = t(
"Total qty (actual pick + miss + bad) cannot exceed available qty: {available}",
{ available: availableQty }
);
newErrors.actualPickQty = errorMsg;
newErrors.missQty = errorMsg;
newErrors.badItemQty = errorMsg;
}

// 5. At least one field must have a value
if (ap === 0 && miss === 0 && totalBad === 0) {
newErrors.actualPickQty = t("Enter pick qty or issue qty");
}

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
@@ -244,22 +270,38 @@ useEffect(() => {
if (!validateForm() || !formData.pickOrderId) {
return;
}

const badItem = Number(formData.badItemQty) || 0;
const badPackage = Number((formData as any).badPackageQty) || 0;
const totalBadQty = badItem + badPackage;

let badReason: string | undefined;
if (totalBadQty > 0) {
// assumption: only one of them is > 0
badReason = badPackage > 0 ? "package_problem" : "quantity_problem";
}
setLoading(true);
try {
const submissionData = {
...formData,
const submissionData: PickExecutionIssueData = {
...(formData as PickExecutionIssueData),
actualPickQty: verifiedQty,
lotId: formData.lotId || selectedLot?.lotId || 0,
lotNo: formData.lotNo || selectedLot?.lotNo || '',
pickOrderCode: formData.pickOrderCode || selectedPickOrderLine?.pickOrderCode || '',
pickerName: session?.user?.name || ''
} as PickExecutionIssueData;
pickerName: session?.user?.name || '',
badItemQty: totalBadQty,
badReason,
};
await onSubmit(submissionData);
onClose();
} catch (error) {
} catch (error: any) {
console.error('Error submitting pick execution issue:', error);
alert(
t("Failed to submit issue. Please try again.") +
(error.message ? `: ${error.message}` : "")
);
} finally {
setLoading(false);
}
@@ -321,16 +363,24 @@ useEffect(() => {
<Grid item xs={12}>
<TextField
fullWidth
label={t('Verified Qty')}
label={t('Actual Pick Qty')}
type="number"
value={verifiedQty}
inputProps={{
inputMode: "numeric",
pattern: "[0-9]*",
min: 0,
}}
value={verifiedQty ?? ""}
onChange={(e) => {
const newValue = parseFloat(e.target.value) || 0;
setVerifiedQty(newValue);
// handleInputChange('actualPickQty', newValue);
const newValue = e.target.value === ""
? undefined
: Math.max(0, Number(e.target.value) || 0);
setVerifiedQty(newValue || 0);
}}
error={!!errors.actualPickQty}
helperText={errors.actualPickQty || `${t('Max')}: ${selectedLot?.actualPickQty || 0}`} // 使用原始接收数量
helperText={
errors.actualPickQty || `${t("Max")}: ${remainingAvailableQty}`
}
variant="outlined"
/>
</Grid>
@@ -340,14 +390,21 @@ useEffect(() => {
fullWidth
label={t('Missing item Qty')}
type="number"
inputProps={{
inputMode: "numeric",
pattern: "[0-9]*",
min: 0,
}}
value={formData.missQty || 0}
onChange={(e) => {
const newMissQty = parseFloat(e.target.value) || 0;
handleInputChange('missQty', newMissQty);
// 不要自动修改其他字段
handleInputChange(
"missQty",
e.target.value === ""
? undefined
: Math.max(0, Number(e.target.value) || 0)
);
}}
error={!!errors.missQty}
helperText={errors.missQty}
variant="outlined"
/>
</Grid>
@@ -357,22 +414,67 @@ useEffect(() => {
fullWidth
label={t('Bad Item Qty')}
type="number"
inputProps={{
inputMode: "numeric",
pattern: "[0-9]*",
min: 0,
}}
value={formData.badItemQty || 0}
onChange={(e) => {
const newBadItemQty = parseFloat(e.target.value) || 0;
const newBadItemQty = e.target.value === ""
? undefined
: Math.max(0, Number(e.target.value) || 0);
handleInputChange('badItemQty', newBadItemQty);
// 不要自动修改其他字段
}}
error={!!errors.badItemQty}
helperText={errors.badItemQty}
variant="outlined"
/>
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t("Bad Package Qty")}
type="number"
inputProps={{
inputMode: "numeric",
pattern: "[0-9]*",
min: 0,
}}
value={(formData as any).badPackageQty || 0}
onChange={(e) => {
handleInputChange(
"badPackageQty",
e.target.value === ""
? undefined
: Math.max(0, Number(e.target.value) || 0)
);
}}
error={!!errors.badItemQty}
variant="outlined"
/>
</Grid>

<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel>{t("Remark")}</InputLabel>
<Select
value={formData.reason || ""}
onChange={(e) => handleInputChange("reason", e.target.value)}
label={t("Remark")}
>
<MenuItem value="">{t("Select Remark")}</MenuItem>
<MenuItem value="miss">{t("Edit")}</MenuItem>
<MenuItem value="bad">{t("Just Complete")}</MenuItem>
</Select>
</FormControl>
</Grid>
{/* Show issue description and handler fields when bad items > 0 */}
{(formData.badItemQty && formData.badItemQty > 0) ? (
{(formData.badItemQty && formData.badItemQty > 0) || ((formData as any).badPackageQty && (formData as any).badPackageQty > 0) ? (
<>
<Grid item xs={12}>
<Grid item xs={12}>
<TextField
fullWidth
id="issueRemark"
@@ -416,7 +518,7 @@ useEffect(() => {
</FormControl>
</Grid>
</>
) : (<></>)}
) : null}
</Grid>
</Box>
</DialogContent>


+ 567
- 198
src/components/Jodetail/newJobPickExecution.tsx Wyświetl plik

@@ -20,7 +20,7 @@ import {
Modal,
} from "@mui/material";
import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider';
import { useCallback, useEffect, useState, useRef, useMemo } from "react";
import { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation";
import {
@@ -69,6 +69,110 @@ interface Props {
onBackToList?: () => void;
}

// Manual Lot Confirmation Modal (align with GoodPickExecutiondetail, opened by {2fic})
const ManualLotConfirmationModal: React.FC<{
open: boolean;
onClose: () => void;
onConfirm: (expectedLotNo: string, scannedLotNo: string) => void;
expectedLot: { lotNo: string; itemCode: string; itemName: string } | null;
scannedLot: { lotNo: string; itemCode: string; itemName: string } | null;
isLoading?: boolean;
}> = ({ open, onClose, onConfirm, expectedLot, scannedLot, isLoading = false }) => {
const { t } = useTranslation("jo");
const [expectedLotInput, setExpectedLotInput] = useState<string>('');
const [scannedLotInput, setScannedLotInput] = useState<string>('');
const [error, setError] = useState<string>('');

useEffect(() => {
if (open) {
setExpectedLotInput(expectedLot?.lotNo || '');
setScannedLotInput(scannedLot?.lotNo || '');
setError('');
}
}, [open, expectedLot, scannedLot]);

const handleConfirm = () => {
if (!expectedLotInput.trim() || !scannedLotInput.trim()) {
setError(t("Please enter both expected and scanned lot numbers."));
return;
}
if (expectedLotInput.trim() === scannedLotInput.trim()) {
setError(t("Expected and scanned lot numbers cannot be the same."));
return;
}
onConfirm(expectedLotInput.trim(), scannedLotInput.trim());
};

return (
<Modal open={open} onClose={onClose}>
<Box sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
bgcolor: 'background.paper',
p: 3,
borderRadius: 2,
minWidth: 500,
}}>
<Typography variant="h6" gutterBottom color="warning.main">
{t("Manual Lot Confirmation")}
</Typography>

<Box sx={{ mb: 2 }}>
<Typography variant="body2" gutterBottom>
<strong>{t("Expected Lot Number")}:</strong>
</Typography>
<TextField
fullWidth
size="small"
value={expectedLotInput}
onChange={(e) => { setExpectedLotInput(e.target.value); setError(''); }}
sx={{ mb: 2 }}
error={!!error && !expectedLotInput.trim()}
/>
</Box>

<Box sx={{ mb: 2 }}>
<Typography variant="body2" gutterBottom>
<strong>{t("Scanned Lot Number")}:</strong>
</Typography>
<TextField
fullWidth
size="small"
value={scannedLotInput}
onChange={(e) => { setScannedLotInput(e.target.value); setError(''); }}
sx={{ mb: 2 }}
error={!!error && !scannedLotInput.trim()}
/>
</Box>

{error && (
<Box sx={{ mb: 2, p: 1, backgroundColor: '#ffebee', borderRadius: 1 }}>
<Typography variant="body2" color="error">
{error}
</Typography>
</Box>
)}

<Box sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
<Button onClick={onClose} variant="outlined" disabled={isLoading}>
{t("Cancel")}
</Button>
<Button
onClick={handleConfirm}
variant="contained"
color="warning"
disabled={isLoading || !expectedLotInput.trim() || !scannedLotInput.trim()}
>
{isLoading ? t("Processing...") : t("Confirm")}
</Button>
</Box>
</Box>
</Modal>
);
};

// QR Code Modal Component (from GoodPickExecution)
const QrCodeModal: React.FC<{
open: boolean;
@@ -345,6 +449,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const [isConfirmingLot, setIsConfirmingLot] = useState(false);
const [qrScanInput, setQrScanInput] = useState<string>('');
const [qrScanError, setQrScanError] = useState<boolean>(false);
const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>('');
const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null);
const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
@@ -379,6 +484,25 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
// Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling
const [processedQrCombinations, setProcessedQrCombinations] = useState<Map<number, Set<number>>>(new Map());

// Cache for fetchStockInLineInfo API calls to avoid redundant requests
const stockInLineInfoCache = useRef<Map<number, { lotNo: string | null; timestamp: number }>>(new Map());
const CACHE_TTL = 60000; // 60 seconds cache TTL
const abortControllerRef = useRef<AbortController | null>(null);
const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null);

// Use refs for processed QR tracking to avoid useEffect dependency issues and delays
const processedQrCodesRef = useRef<Set<string>>(new Set());
const lastProcessedQrRef = useRef<string>('');

// Store callbacks in refs to avoid useEffect dependency issues
const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | null>(null);
const resetScanRef = useRef<(() => void) | null>(null);

// Manual lot confirmation modal state (test shortcut {2fic})
const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false);
const getAllLotsFromHierarchical = useCallback((
data: JobOrderLotsHierarchicalResponse | null
): any[] => {
@@ -426,6 +550,74 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const originalCombinedData = useMemo(() => {
return getAllLotsFromHierarchical(jobOrderData);
}, [jobOrderData, getAllLotsFromHierarchical]);

// Enhanced lotDataIndexes with cached active lots for better performance (align with GoodPickExecutiondetail)
const lotDataIndexes = useMemo(() => {
const byItemId = new Map<number, any[]>();
const byItemCode = new Map<string, any[]>();
const byLotId = new Map<number, any>();
const byLotNo = new Map<string, any[]>();
const byStockInLineId = new Map<number, any[]>();
const activeLotsByItemId = new Map<number, any[]>();
const rejectedStatuses = new Set(['rejected']);

for (let i = 0; i < combinedLotData.length; i++) {
const lot = combinedLotData[i];
const isActive =
!rejectedStatuses.has(lot.lotAvailability) &&
!rejectedStatuses.has(lot.stockOutLineStatus) &&
!rejectedStatuses.has(lot.processingStatus) &&
lot.stockOutLineStatus !== 'completed';

if (lot.itemId) {
if (!byItemId.has(lot.itemId)) {
byItemId.set(lot.itemId, []);
activeLotsByItemId.set(lot.itemId, []);
}
byItemId.get(lot.itemId)!.push(lot);
if (isActive) activeLotsByItemId.get(lot.itemId)!.push(lot);
}
if (lot.itemCode) {
if (!byItemCode.has(lot.itemCode)) byItemCode.set(lot.itemCode, []);
byItemCode.get(lot.itemCode)!.push(lot);
}
if (lot.lotId) byLotId.set(lot.lotId, lot);
if (lot.lotNo) {
if (!byLotNo.has(lot.lotNo)) byLotNo.set(lot.lotNo, []);
byLotNo.get(lot.lotNo)!.push(lot);
}
if (lot.stockInLineId) {
if (!byStockInLineId.has(lot.stockInLineId)) byStockInLineId.set(lot.stockInLineId, []);
byStockInLineId.get(lot.stockInLineId)!.push(lot);
}
}

return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId };
}, [combinedLotData]);

// Cached version of fetchStockInLineInfo to avoid redundant API calls
const fetchStockInLineInfoCached = useCallback(async (stockInLineId: number): Promise<{ lotNo: string | null }> => {
const now = Date.now();
const cached = stockInLineInfoCache.current.get(stockInLineId);
if (cached && (now - cached.timestamp) < CACHE_TTL) {
return { lotNo: cached.lotNo };
}

if (abortControllerRef.current) abortControllerRef.current.abort();
const abortController = new AbortController();
abortControllerRef.current = abortController;

const stockInLineInfo = await fetchStockInLineInfo(stockInLineId);
stockInLineInfoCache.current.set(stockInLineId, {
lotNo: stockInLineInfo.lotNo || null,
timestamp: now
});
if (stockInLineInfoCache.current.size > 100) {
const firstKey = stockInLineInfoCache.current.keys().next().value;
if (firstKey !== undefined) stockInLineInfoCache.current.delete(firstKey);
}
return { lotNo: stockInLineInfo.lotNo || null };
}, []);
// 修改:加载未分配的 Job Order 订单
const loadUnassignedOrders = useCallback(async () => {
setIsLoadingUnassigned(true);
@@ -719,53 +911,119 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}
}, [combinedLotData, fetchJobOrderData]);
const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => {
console.log("Lot mismatch detected:", { expectedLot, scannedLot });
setExpectedLotData(expectedLot);
setScannedLotData(scannedLot);
setLotConfirmationOpen(true);
}, []);
console.log("⚠️ [LOT MISMATCH] Lot mismatch detected:", { expectedLot, scannedLot });
console.log("⚠️ [LOT MISMATCH] Opening confirmation modal - NO lot will be marked as scanned until user confirms");

// ✅ schedule modal open in next tick (avoid flushSync warnings on some builds)
// ✅ IMPORTANT: This function ONLY opens the modal. It does NOT process any lot.
setTimeout(() => {
setExpectedLotData(expectedLot);
setScannedLotData({
...scannedLot,
lotNo: scannedLot.lotNo || null,
});
setLotConfirmationOpen(true);
console.log("⚠️ [LOT MISMATCH] Modal opened - waiting for user confirmation");
}, 0);

// ✅ Fetch lotNo in background for display purposes (cached)
// ✅ This is ONLY for display - it does NOT process any lot
if (!scannedLot.lotNo && scannedLot.stockInLineId) {
console.log(`⚠️ [LOT MISMATCH] Fetching lotNo for display (stockInLineId: ${scannedLot.stockInLineId})`);
fetchStockInLineInfoCached(scannedLot.stockInLineId)
.then((info) => {
console.log(`⚠️ [LOT MISMATCH] Fetched lotNo for display: ${info.lotNo}`);
startTransition(() => {
setScannedLotData((prev: any) => ({
...prev,
lotNo: info.lotNo || null,
}));
});
})
.catch((error) => {
console.error(`❌ [LOT MISMATCH] Error fetching lotNo for display (stockInLineId may not exist):`, error);
// ignore display fetch errors - this does NOT affect processing
});
}
}, [fetchStockInLineInfoCached]);

// Add handleLotConfirmation function
const handleLotConfirmation = useCallback(async () => {
if (!expectedLotData || !scannedLotData || !selectedLotForQr) return;
if (!expectedLotData || !scannedLotData || !selectedLotForQr) {
console.error("❌ [LOT CONFIRM] Missing required data for lot confirmation");
return;
}
console.log("✅ [LOT CONFIRM] User confirmed lot substitution - processing now");
console.log("✅ [LOT CONFIRM] Expected lot:", expectedLotData);
console.log("✅ [LOT CONFIRM] Scanned lot:", scannedLotData);
console.log("✅ [LOT CONFIRM] Selected lot for QR:", selectedLotForQr);
setIsConfirmingLot(true);
try {
let newLotLineId = scannedLotData?.inventoryLotLineId;
if (!newLotLineId && scannedLotData?.stockInLineId) {
const ld = await fetchLotDetail(scannedLotData.stockInLineId);
newLotLineId = ld.inventoryLotLineId;
try {
console.log(`🔍 [LOT CONFIRM] Fetching lot detail for stockInLineId: ${scannedLotData.stockInLineId}`);
const ld = await fetchLotDetail(scannedLotData.stockInLineId);
newLotLineId = ld.inventoryLotLineId;
console.log(`✅ [LOT CONFIRM] Fetched lot detail: inventoryLotLineId=${newLotLineId}`);
} catch (error) {
console.error("❌ [LOT CONFIRM] Error fetching lot detail (stockInLineId may not exist):", error);
// If stockInLineId doesn't exist, we can still proceed with lotNo substitution
// The backend confirmLotSubstitution should handle this case
}
}
if (!newLotLineId) {
console.error("No inventory lot line id for scanned lot");
return;
console.warn("⚠️ [LOT CONFIRM] No inventory lot line id for scanned lot, proceeding with lotNo only");
// Continue anyway - backend may handle lotNo substitution without inventoryLotLineId
}
console.log("=== Lot Confirmation Debug ===");
console.log("=== [LOT CONFIRM] Lot Confirmation Debug ===");
console.log("Selected Lot:", selectedLotForQr);
console.log("Pick Order Line ID:", selectedLotForQr.pickOrderLineId);
console.log("Stock Out Line ID:", selectedLotForQr.stockOutLineId);
console.log("Suggested Pick Lot ID:", selectedLotForQr.suggestedPickLotId);
console.log("Lot ID (fallback):", selectedLotForQr.lotId);
console.log("New Inventory Lot Line ID:", newLotLineId);
console.log("Scanned Lot No:", scannedLotData.lotNo);
console.log("Scanned StockInLineId:", scannedLotData.stockInLineId);

// Call confirmLotSubstitution to update the suggested lot
console.log("🔄 [LOT CONFIRM] Calling confirmLotSubstitution...");
const substitutionResult = await confirmLotSubstitution({
pickOrderLineId: selectedLotForQr.pickOrderLineId,
stockOutLineId: selectedLotForQr.stockOutLineId,
originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId,
newInventoryLotNo: scannedLotData.lotNo
newInventoryLotNo: scannedLotData.lotNo || '',
// ✅ required by LotSubstitutionConfirmRequest
newStockInLineId: scannedLotData?.stockInLineId ?? null,
});
console.log(" Lot substitution result:", substitutionResult);
console.log("✅ [LOT CONFIRM] Lot substitution result:", substitutionResult);

// ✅ CRITICAL: substitution failed => DO NOT mark original stockOutLine as checked.
// 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.");
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(
substitutionResult?.message ||
`换批失败:stockInLineId ${scannedLotData?.stockInLineId ?? ""} 不存在或无法匹配`
);
return;
}
// Update stock out line status to 'checked' after substitution
if(selectedLotForQr?.stockOutLineId){
console.log(`🔄 [LOT CONFIRM] Updating stockOutLine ${selectedLotForQr.stockOutLineId} to 'checked'`);
await updateStockOutLineStatus({
id: selectedLotForQr.stockOutLineId,
status: 'checked',
qty: 0
});
console.log(" Stock out line status updated to 'checked'");
console.log(`✅ [LOT CONFIRM] Stock out line ${selectedLotForQr.stockOutLineId} status updated to 'checked'`);
}
// Close modal and clean up state BEFORE refreshing
@@ -777,6 +1035,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
// Clear QR processing state but DON'T clear processedQrCodes yet
setQrScanError(false);
setQrScanSuccess(true);
setQrScanErrorMsg('');
setQrScanInput('');
// Set refreshing flag to prevent QR processing during refresh
@@ -796,12 +1055,27 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
setLastProcessedQr('');
setQrScanSuccess(false);
setIsRefreshingData(false);
// ✅ Clear processedQrCombinations to allow reprocessing the same QR if needed
if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
setProcessedQrCombinations(prev => {
const newMap = new Map(prev);
const itemId = selectedLotForQr.itemId;
if (itemId && newMap.has(itemId)) {
newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
if (newMap.get(itemId)!.size === 0) {
newMap.delete(itemId);
}
}
return newMap;
});
}
}, 500); // Reduced from 3000ms to 500ms - just enough for UI update
} catch (error) {
console.error("Error confirming lot substitution:", error);
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg('换批发生异常,请重试或联系管理员');
// Clear refresh flag on error
setIsRefreshingData(false);
} finally {
@@ -810,182 +1084,178 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData]);

const processOutsideQrCode = useCallback(async (latestQr: string) => {
// Don't process if confirmation modal is open
if (lotConfirmationOpen) {
console.log("⏸️ Confirmation modal is open, skipping QR processing");
return;
}

// ✅ Only JSON QR supported for outside scanner (avoid false positive with lotNo)
let qrData: any = null;
try {
qrData = JSON.parse(latestQr);
} catch {
console.log("QR is not JSON format");
// Handle non-JSON QR codes as direct lot numbers
const directLotNo = latestQr.replace(/[{}]/g, '');
if (directLotNo) {
console.log(`Processing direct lot number: ${directLotNo}`);
await handleQrCodeSubmit(directLotNo);
}
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
});
return;
}

try {
// Only use the new API when we have JSON with stockInLineId + itemId
if (!(qrData?.stockInLineId && qrData?.itemId)) {
console.log("QR JSON missing required fields (itemId, stockInLineId).");
setQrScanError(true);
setQrScanSuccess(false);
return;
}
// First, fetch stock in line info to get the lot number
let stockInLineInfo: any;
try {
stockInLineInfo = await fetchStockInLineInfo(qrData.stockInLineId);
console.log("Stock in line info:", stockInLineInfo);
} catch (error) {
console.error("Error fetching stock in line info:", error);
if (!(qrData?.stockInLineId && qrData?.itemId)) {
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
return;
}

// Call new analyze-qr-code API
const analysis = await analyzeQrCode({
itemId: qrData.itemId,
stockInLineId: qrData.stockInLineId
});
if (!analysis) {
console.error("analyzeQrCode returned no data");
setQrScanError(true);
setQrScanSuccess(false);
return;
}
const {
itemId: analyzedItemId,
itemCode: analyzedItemCode,
itemName: analyzedItemName,
scanned,
} = analysis || {};
// 1) Find all lots for the same item from current expected list
const sameItemLotsInExpected = combinedLotData.filter(l =>
(l.itemId && analyzedItemId && l.itemId === analyzedItemId) ||
(l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode)
);
if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) {
// Case 3: No item code match
console.error("No item match in expected lots for scanned code");
setQrScanError(true);
setQrScanSuccess(false);
return;
}
// Find the ACTIVE suggested lot (not rejected lots)
const activeSuggestedLots = sameItemLotsInExpected.filter(lot =>
lot.lotAvailability !== 'rejected' &&
lot.stockOutLineStatus !== 'rejected' &&
lot.stockOutLineStatus !== 'completed'
);
if (activeSuggestedLots.length === 0) {
console.warn("All lots for this item are rejected or completed");
return;
}

const scannedItemId = Number(qrData.itemId);
const scannedStockInLineId = Number(qrData.stockInLineId);

// ✅ avoid duplicate processing by itemId+stockInLineId
const itemProcessedSet = processedQrCombinations.get(scannedItemId);
if (itemProcessedSet?.has(scannedStockInLineId)) return;

const indexes = lotDataIndexes;
const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || [];
if (activeSuggestedLots.length === 0) {
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
return;
});
return;
}

// ✅ direct stockInLineId match (O(1))
const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || [];
let exactMatch: any = null;
for (let i = 0; i < stockInLineLots.length; i++) {
const lot = stockInLineLots[i];
if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) {
exactMatch = lot;
break;
}
// Use the first active suggested lot as the "expected" lot
const expectedLot = activeSuggestedLots[0];
if (scanned?.lotNo === expectedLot.lotNo) {
console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`);
if (!expectedLot.stockOutLineId) {
console.warn("No stockOutLineId on expectedLot, cannot update status by QR.");
}

console.log(`🔍 [QR PROCESS] Scanned stockInLineId: ${scannedStockInLineId}, itemId: ${scannedItemId}`);
console.log(`🔍 [QR PROCESS] Found ${stockInLineLots.length} lots with stockInLineId ${scannedStockInLineId}`);
console.log(`🔍 [QR PROCESS] Exact match found: ${exactMatch ? `YES (lotNo: ${exactMatch.lotNo}, stockOutLineId: ${exactMatch.stockOutLineId})` : 'NO'}`);

if (exactMatch) {
if (!exactMatch.stockOutLineId) {
console.error(`❌ [QR PROCESS] Exact match found but no stockOutLineId`);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
return;
}
try {
const res = await updateStockOutLineStatusByQRCodeAndLotNo({
pickOrderLineId: expectedLot.pickOrderLineId,
inventoryLotNo: scanned.lotNo,
stockOutLineId: expectedLot.stockOutLineId,
itemId: expectedLot.itemId,
status: "checked",
});
return;
}

console.log(`✅ [QR PROCESS] Processing exact match: lotNo=${exactMatch.lotNo}, stockOutLineId=${exactMatch.stockOutLineId}`);
try {
const res = await updateStockOutLineStatusByQRCodeAndLotNo({
pickOrderLineId: exactMatch.pickOrderLineId,
inventoryLotNo: exactMatch.lotNo,
stockOutLineId: exactMatch.stockOutLineId,
itemId: exactMatch.itemId,
status: "checked",
});

if (res.code === "checked" || res.code === "SUCCESS") {
console.log(`✅ [QR PROCESS] Successfully updated stockOutLine ${exactMatch.stockOutLineId} to checked`);
const entity = res.entity as any;
startTransition(() => {
setQrScanError(false);
setQrScanSuccess(true);
});
const updateOk =
res?.type === "checked" ||
typeof res?.id === "number" ||
(res?.message && res.message.includes("success"));
if (updateOk) {
setQrScanError(false);
setQrScanSuccess(true);

if (
expectedLot.pickOrderId &&
expectedLot.itemId &&
(expectedLot.stockOutLineStatus?.toLowerCase?.() === "pending" ||
!expectedLot.stockOutLineStatus) &&
!expectedLot.handler
) {
await updateHandledBy(expectedLot.pickOrderId, expectedLot.itemId);
}
const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
await fetchJobOrderData(pickOrderId);
} else if (res?.code === "LOT_NUMBER_MISMATCH" || res?.code === "ITEM_MISMATCH") {
setQrScanError(true);
setQrScanSuccess(false);
} else {
console.warn("Unexpected response from backend:", res);
// mark combination processed
setProcessedQrCombinations(prev => {
const newMap = new Map(prev);
if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
newMap.get(scannedItemId)!.add(scannedStockInLineId);
return newMap;
});

// refresh to keep consistency with server & handler updates
const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
await fetchJobOrderData(pickOrderId);
} else {
console.error(`❌ [QR PROCESS] Update failed: ${res.code}`);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
}
} catch (e) {
console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e);
});
}
} catch (error) {
console.error(`❌ [QR PROCESS] Error updating stockOutLine:`, error);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
}
return; // ✅ 直接返回,不再调用后面的分支
});
}
// Case 2: Same item, different lot - show confirmation modal
console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`);
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 })));
if (activeSuggestedLots.length === 0) {
console.error(`❌ [QR PROCESS] No active suggested lots found for itemId ${scannedItemId}`);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(`当前订单中没有 itemId ${scannedItemId} 的可用批次`);
});
return;
}

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}`);
// DON'T stop scanning - just pause QR processing by showing modal
// ✅ stockInLineId exists, 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: analyzedItemCode || expectedLot.itemCode,
itemName: analyzedItemName || expectedLot.itemName
itemCode: expectedLot.itemCode,
itemName: expectedLot.itemName
},
{
lotNo: scanned?.lotNo || '',
itemCode: analyzedItemCode || expectedLot.itemCode,
itemName: analyzedItemName || expectedLot.itemName,
inventoryLotLineId: scanned?.inventoryLotLineId,
stockInLineId: qrData.stockInLineId
lotNo: stockInLineInfo.lotNo || null, // Use fetched lotNo for display
itemCode: expectedLot.itemCode,
itemName: expectedLot.itemName,
inventoryLotLineId: null,
stockInLineId: scannedStockInLineId
}
);
} catch (error) {
console.error("Error during analyzeQrCode flow:", error);
setQrScanError(true);
setQrScanSuccess(false);
return;
// ✅ 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;
});
}
}, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen, updateHandledBy]);
}, [filterArgs?.pickOrderId, fetchJobOrderData, handleLotMismatch, lotDataIndexes, processedQrCombinations]);

// Store in refs for immediate access in qrValues effect
processOutsideQrCodeRef.current = processOutsideQrCode;
resetScanRef.current = resetScan;


const handleManualInputSubmit = useCallback(() => {
@@ -1039,26 +1309,84 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {

useEffect(() => {
// Add isManualScanning check
if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData || lotConfirmationOpen) {
return;
}
// Skip if scanner not active or no data or currently refreshing
if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) return;

const latestQr = qrValues[qrValues.length - 1];
if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) {
console.log("QR code already processed, skipping...");
return;

// ✅ Test shortcut: {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId
if ((latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) && latestQr.endsWith("}")) {
let content = '';
if (latestQr.startsWith("{2fittest")) content = latestQr.substring(9, latestQr.length - 1);
else content = latestQr.substring(8, latestQr.length - 1);

const parts = content.split(',');
if (parts.length === 2) {
const itemId = parseInt(parts[0].trim(), 10);
const stockInLineId = parseInt(parts[1].trim(), 10);
if (!isNaN(itemId) && !isNaN(stockInLineId)) {
const simulatedQr = JSON.stringify({ itemId, stockInLineId });

lastProcessedQrRef.current = latestQr;
processedQrCodesRef.current.add(latestQr);
setLastProcessedQr(latestQr);
setProcessedQrCodes(new Set(processedQrCodesRef.current));

processOutsideQrCodeRef.current?.(simulatedQr);
resetScanRef.current?.();
return;
}
}
}
if (latestQr && latestQr !== lastProcessedQr) {
console.log(`🔍 Processing new QR code with enhanced validation: ${latestQr}`);

// ✅ Shortcut: {2fic} open manual lot confirmation modal
if (latestQr === "{2fic}") {
setManualLotConfirmationOpen(true);
resetScanRef.current?.();
lastProcessedQrRef.current = latestQr;
processedQrCodesRef.current.add(latestQr);
setLastProcessedQr(latestQr);
setProcessedQrCodes(prev => new Set(prev).add(latestQr));
processOutsideQrCode(latestQr);
setProcessedQrCodes(new Set(processedQrCodesRef.current));
return;
}

// Skip processing if modal open for same QR
if (lotConfirmationOpen || manualLotConfirmationOpen) {
if (latestQr === lastProcessedQrRef.current) return;
}

// Skip if already processed (refs)
if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) return;

// Mark processed immediately
lastProcessedQrRef.current = latestQr;
processedQrCodesRef.current.add(latestQr);
if (processedQrCodesRef.current.size > 100) {
const firstValue = processedQrCodesRef.current.values().next().value;
if (firstValue !== undefined) processedQrCodesRef.current.delete(firstValue);
}
}, [qrValues, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode, combinedLotData, isManualScanning, lotConfirmationOpen]);

// Process immediately
if (qrProcessingTimeoutRef.current) {
clearTimeout(qrProcessingTimeoutRef.current);
qrProcessingTimeoutRef.current = null;
}

processOutsideQrCodeRef.current?.(latestQr);

// UI state updates (non-blocking)
startTransition(() => {
setLastProcessedQr(latestQr);
setProcessedQrCodes(new Set(processedQrCodesRef.current));
});

return () => {
if (qrProcessingTimeoutRef.current) {
clearTimeout(qrProcessingTimeoutRef.current);
qrProcessingTimeoutRef.current = null;
}
};
}, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]);

const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
if (value === '' || value === null || value === undefined) {
@@ -1715,7 +2043,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
backgroundColor: "error.light",
}}
>
{t("QR code does not match any item in current orders.")}
{qrScanErrorMsg || t("QR code does not match any item in current orders.")}
</Alert>
)}
{qrScanSuccess && (
@@ -1878,34 +2206,32 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
<Button
variant="outlined"
size="small"
onClick={() => handlePickExecutionForm(lot)}
disabled={
(lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected') ||
lot.stockOutLineStatus === 'completed' || // Disable when finished
lot.stockOutLineStatus === 'pending' // Disable when QR scan not passed
}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '28px',
minWidth: '60px',
borderColor: 'warning.main',
color: 'warning.main'
}}
title="Report missing or bad items"
>
{t("Issue")}
onClick={() => handlePickExecutionForm(lot)}
disabled={
// ✅ align with GoodPickExecutiondetail: Edit only disabled when completed
lot.stockOutLineStatus === 'completed'
}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '28px',
minWidth: '60px',
borderColor: 'warning.main',
color: 'warning.main'
}}
title="Report missing or bad items"
>
{t("Edit")}
</Button>
<Button
variant="outlined"
size="small"
onClick={() => handleSkip(lot)}
// ✅ align with GoodPickExecutiondetail: Just Complete submits requiredQty (not 0)
onClick={() => handleSubmitPickQtyWithQty(lot, lot.requiredQty || lot.pickOrderLineRequiredQty || 0)}
disabled={lot.stockOutLineStatus === 'completed'}
sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }}
sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }}
>
{t("Skip")}
{t("Just Complete")}
</Button>
</Stack>
</Box>
@@ -1953,11 +2279,35 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
<LotConfirmationModal
open={lotConfirmationOpen}
onClose={() => {
console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`);
setLotConfirmationOpen(false);
setExpectedLotData(null);
setScannedLotData(null);
setSelectedLotForQr(null);
// ✅ IMPORTANT: Clear refs and processedQrCombinations to allow reprocessing the same QR code
// This allows the modal to reopen if user cancels and scans the same QR again
setTimeout(() => {
lastProcessedQrRef.current = '';
processedQrCodesRef.current.clear();
// Clear processedQrCombinations for this itemId+stockInLineId combination
if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
setProcessedQrCombinations(prev => {
const newMap = new Map(prev);
const itemId = selectedLotForQr.itemId;
if (itemId && newMap.has(itemId)) {
newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
if (newMap.get(itemId)!.size === 0) {
newMap.delete(itemId);
}
}
return newMap;
});
}
console.log(`⏱️ [LOT CONFIRM MODAL] Cleared refs and processedQrCombinations to allow reprocessing`);
}, 100);
}}
onConfirm={handleLotConfirmation}
expectedLot={expectedLotData}
@@ -1965,6 +2315,25 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
isLoading={isConfirmingLot}
/>
)}

{/* Manual Lot Confirmation Modal (test shortcut {2fic}) */}
<ManualLotConfirmationModal
open={manualLotConfirmationOpen}
onClose={() => setManualLotConfirmationOpen(false)}
// Reuse existing handler: expectedLotInput=current lot, scannedLotInput=new lot
onConfirm={(currentLotNo, newLotNo) => {
// Use existing manual flow from handleManualLotConfirmation in other screens:
// Here we route through updateStockOutLineStatusByQRCodeAndLotNo via handleManualLotConfirmation-like inline logic.
// For now: open LotConfirmationModal path by setting expected/scanned and letting user confirm substitution.
setExpectedLotData({ lotNo: currentLotNo, itemCode: '', itemName: '' });
setScannedLotData({ lotNo: newLotNo, itemCode: '', itemName: '', inventoryLotLineId: null, stockInLineId: null });
setManualLotConfirmationOpen(false);
setLotConfirmationOpen(true);
}}
expectedLot={expectedLotData}
scannedLot={scannedLotData}
isLoading={isConfirmingLot}
/>
{/* Pick Execution Form Modal */}
{pickExecutionFormOpen && selectedLotForExecutionForm && (
<GoodPickExecutionForm


+ 2
- 0
src/i18n/zh/jo.json Wyświetl plik

@@ -12,6 +12,8 @@
"Confirm All": "確認所有提料",
"Wait Time [minutes]": "等待時間(分鐘)",
"Job Process Status": "工單流程狀態",
"Edit": "改數",
"Just Complete": "已完成",
"Stock Req. Qty": "需求數(庫存單位)",
"Search Job Order/ Create Job Order":"搜尋工單/建立工單",
"UoM": "銷售單位",


Ładowanie…
Anuluj
Zapisz