Ver código fonte

fix scan lot and scan not match lt and new issue handle

MergeProblem1
CANCERYS\kw093 1 semana atrás
pai
commit
8576172e8e
3 arquivos alterados com 249 adições e 287 exclusões
  1. +2
    -0
      src/app/api/pickOrder/actions.ts
  2. +120
    -174
      src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx
  3. +127
    -113
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx

+ 2
- 0
src/app/api/pickOrder/actions.ts Ver arquivo

@@ -210,6 +210,8 @@ export interface PickExecutionIssueData {
issueRemark: string;
pickerName: string;
handledBy?: number;
badReason?: string;
reason?: string;
}
export type AutoAssignReleaseResponse = {
id: number | null;


+ 120
- 174
src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx Ver arquivo

@@ -1,4 +1,4 @@
// FPSMS-frontend/src/components/PickOrderSearch/PickExecutionForm.tsx
// FPSMS-frontend/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx
"use client";

import {
@@ -53,16 +53,13 @@ interface PickExecutionFormProps {
selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
pickOrderId?: number;
pickOrderCreateDate: any;
// Remove these props since we're not handling normal cases
// onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>;
// selectedRowId?: number | null;
}

// 定义错误类型
interface FormErrors {
actualPickQty?: string;
missQty?: string;
badItemQty?: string;
badReason?: string;
issueRemark?: string;
handledBy?: string;
}
@@ -75,38 +72,21 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
selectedPickOrderLine,
pickOrderId,
pickOrderCreateDate,
// Remove these props
// onNormalPickSubmit,
// selectedRowId,
}) => {
const { t } = useTranslation("pickOrder");
const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({});
const [errors, setErrors] = useState<FormErrors>({});
const [loading, setLoading] = useState(false);
const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);
// 计算剩余可用数量
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
// 直接使用 availableQty,因为 API 没有返回 inQty 和 outQty
return lot.availableQty || 0;
}, []);
const calculateRequiredQty = useCallback((lot: LotPickData) => {
// Use the original required quantity, not subtracting actualPickQty
// The actualPickQty in the form should be independent of the database value
return lot.requiredQty || 0;
}, []);
const remaining = selectedLot ? calculateRemainingAvailableQty(selectedLot) : 0;
const req = selectedLot ? calculateRequiredQty(selectedLot) : 0;

const ap = Number(formData.actualPickQty) || 0;
const miss = Number(formData.missQty) || 0;
const bad = Number(formData.badItemQty) || 0;

// Max the user can type
const maxPick = Math.min(remaining, req);
const maxIssueTotal = Math.max(0, req - ap); // remaining room for miss+bad

const clamp0 = (v: any) => Math.max(0, Number(v) || 0);
// 获取处理人员列表
useEffect(() => {
const fetchHandlers = async () => {
try {
@@ -120,12 +100,11 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
fetchHandlers();
}, []);

const initKeyRef = useRef<string | null>(null);
const initKeyRef = useRef<string | null>(null);

useEffect(() => {
if (!open || !selectedLot || !selectedPickOrderLine || !pickOrderId) return;

// Only initialize once per (pickOrderLineId + lotId) while dialog open
const key = `${selectedPickOrderLine.id}-${selectedLot.lotId}`;
if (initKeyRef.current === key) return;

@@ -161,86 +140,75 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
issueRemark: '',
pickerName: '',
handledBy: undefined,
reason: '',
badReason: '',
});

initKeyRef.current = key;
}, [open, selectedPickOrderLine?.id, selectedLot?.lotId, pickOrderId, pickOrderCreateDate]);
// Mutually exclusive inputs: picking vs reporting issues

const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// 清除错误
if (errors[field as keyof FormErrors]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
}, [errors]);

// Update form validation to require either missQty > 0 OR badItemQty > 0
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
const req = selectedLot?.requiredQty || 0;
const ap = Number(formData.actualPickQty) || 0;
const miss = Number(formData.missQty) || 0;
const bad = Number(formData.badItemQty) || 0;
const total = ap + miss + bad;
// Updated validation logic
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
const req = selectedLot?.requiredQty || 0;
const ap = Number(formData.actualPickQty) || 0;
const miss = Number(formData.missQty) || 0;
const bad = Number(formData.badItemQty) || 0;
const total = ap + miss + bad;
const availableQty = selectedLot?.availableQty || 0;

// 1. 检查 actualPickQty 不能为负数
if (ap < 0) {
newErrors.actualPickQty = t('Qty cannot be negative');
}
// 2. 检查 actualPickQty 不能超过可用数量或需求数量
if (ap > Math.min(req)) {
newErrors.actualPickQty = t('Qty is not allowed to be greater than required/available qty');
}
// 3. 检查 missQty 和 badItemQty 不能为负数
if (miss < 0) {
newErrors.missQty = t('Invalid qty');
}
if (bad < 0) {
newErrors.badItemQty = t('Invalid qty');
}
// 4. 🔥 关键验证:总和必须等于 Required Qty(不能多也不能少)
if (total !== req) {
const diff = req - total;
const errorMsg = diff > 0
? t('Total must equal Required Qty. Missing: {diff}', { diff })
: t('Total must equal Required Qty. Exceeds by: {diff}', { diff: Math.abs(diff) });
newErrors.actualPickQty = errorMsg;
newErrors.missQty = errorMsg;
newErrors.badItemQty = errorMsg;
}
// 5. 🔥 关键验证:如果只有 actualPickQty 有值,而 missQty 和 badItemQty 都为 0,不允许提交
// 这意味着如果 actualPickQty < requiredQty,必须报告问题(missQty 或 badItemQty > 0)
if (ap > 0 && miss === 0 && bad === 0 && ap < req) {
newErrors.missQty = t('If Actual Pick Qty is less than Required Qty, you must report Missing Qty or Bad Item Qty');
newErrors.badItemQty = t('If Actual Pick Qty is less than Required Qty, you must report Missing Qty or Bad Item Qty');
}
// 6. 如果所有值都为 0,不允许提交
if (ap === 0 && miss === 0 && bad === 0) {
newErrors.actualPickQty = t('Enter pick qty or issue qty');
newErrors.missQty = t('Enter pick qty or issue qty');
}
// 7. 如果 actualPickQty = requiredQty,missQty 和 badItemQty 必须都为 0
if (ap === req && (miss > 0 || bad > 0)) {
newErrors.missQty = t('If Actual Pick Qty equals Required Qty, Missing Qty and Bad Item Qty must be 0');
newErrors.badItemQty = t('If Actual Pick Qty equals Required Qty, Missing Qty and Bad Item Qty must be 0');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 1. Check actualPickQty cannot be negative
if (ap < 0) {
newErrors.actualPickQty = t('Qty cannot be negative');
}
// 2. Check actualPickQty cannot exceed available quantity
if (ap > availableQty) {
newErrors.actualPickQty = t('Actual pick qty cannot exceed available qty');
}
// 3. Check missQty and badItemQty cannot be negative
if (miss < 0) {
newErrors.missQty = t('Invalid qty');
}
if (bad < 0) {
newErrors.badItemQty = t('Invalid qty');
}
// 4. NEW: Total (actualPickQty + missQty + badItemQty) 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. If badItemQty > 0, badReason is required
if (bad > 0 && !formData.badReason) {
newErrors.badReason = t('Bad reason is required when bad item qty > 0');
newErrors.badItemQty = t('Bad reason is required');
}
// 6. At least one field must have a value
if (ap === 0 && miss === 0 && bad === 0) {
newErrors.actualPickQty = t('Enter pick qty or issue qty');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleSubmit = async () => {
// First validate the form
if (!validateForm()) {
console.error('Form validation failed:', errors);
return; // Prevent submission, show validation errors
return;
}
if (!formData.pickOrderId) {
@@ -251,11 +219,8 @@ const validateForm = (): boolean => {
setLoading(true);
try {
await onSubmit(formData as PickExecutionIssueData);
// Automatically closed when successful (handled by onClose)
} catch (error: any) {
console.error('Error submitting pick execution issue:', error);
// Show error message (can be passed to parent component via props or state)
// 或者在这里显示 toast/alert
alert(t('Failed to submit issue. Please try again.') + (error.message ? `: ${error.message}` : ''));
} finally {
setLoading(false);
@@ -278,21 +243,11 @@ const validateForm = (): boolean => {
return (
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>
{t('Pick Execution Issue Form')} {/* Always show issue form title */}
{t('Pick Execution Issue Form')}
</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2 }}>
{/* Add instruction text */}
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
<Typography variant="body2" color="warning.main">
<strong>{t('Note:')}</strong> {t('This form is for reporting issues only. You must report either missing items or bad items.')}
</Typography>
</Box>
</Grid>
{/* Keep the existing form fields */}
<Grid item xs={6}>
<TextField
fullWidth
@@ -300,7 +255,6 @@ const validateForm = (): boolean => {
value={selectedLot?.requiredQty || 0}
disabled
variant="outlined"
// helperText={t('Still need to pick')}
/>
</Grid>
@@ -311,7 +265,6 @@ const validateForm = (): boolean => {
value={remainingAvailableQty}
disabled
variant="outlined"
// helperText={t('Available in warehouse')}
/>
</Grid>

@@ -320,88 +273,81 @@ const validateForm = (): boolean => {
fullWidth
label={t('Actual Pick Qty')}
type="number"
inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }}
inputProps={{ inputMode: 'numeric', pattern: '[0-9]*', min: 0 }}
value={formData.actualPickQty ?? ''}
onChange={(e) => handleInputChange('actualPickQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
error={!!errors.actualPickQty}
helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`}
helperText={errors.actualPickQty || `${t('Max')}: ${remainingAvailableQty}`}
variant="outlined"
/>
</Grid>

<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel>{t('Reason')}</InputLabel>
<Select
value={formData.reason || ''}
onChange={(e) => handleInputChange('reason', e.target.value)}
label={t('Reason')}
>
<MenuItem value="">{t('Select Reason')}</MenuItem>
<MenuItem value="miss">{t('Edit')}</MenuItem>
<MenuItem value="bad">{t('Just Complete')}</MenuItem>

</Select>
</FormControl>
</Grid>

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

<Grid item xs={12}>
<TextField
fullWidth
label={t('Bad Item Qty')}
type="number"
inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }}
value={formData.badItemQty || 0}
onChange={(e) => handleInputChange('badItemQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
error={!!errors.badItemQty}
variant="outlined"
//disabled={(formData.actualPickQty || 0) > 0}
/>
<TextField
fullWidth
label={t('Bad Item Qty')}
type="number"
inputProps={{ inputMode: 'numeric', pattern: '[0-9]*', min: 0 }}
value={formData.badItemQty || 0}
onChange={(e) => handleInputChange('badItemQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
error={!!errors.badItemQty}
variant="outlined"
/>
</Grid>
{/* Show issue description and handler fields when bad items > 0 */}

{/* Show bad reason dropdown when badItemQty > 0 */}
{(formData.badItemQty && formData.badItemQty > 0) ? (
<>
<Grid item xs={12}>
<TextField
fullWidth
id="issueRemark"
label={t('Issue Remark')}
multiline
rows={4}
value={formData.issueRemark || ''}
onChange={(e) => handleInputChange('issueRemark', e.target.value)}
error={!!errors.issueRemark}
helperText={errors.issueRemark}
//placeholder={t('Describe the issue with bad items')}
variant="outlined"
/>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth error={!!errors.handledBy}>
<InputLabel>{t('handler')}</InputLabel>
<Select
value={formData.handledBy ? formData.handledBy.toString() : ''}
onChange={(e) => handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined)}
label={t('handler')}
>
{handlers.map((handler) => (
<MenuItem key={handler.id} value={handler.id.toString()}>
{handler.name}
</MenuItem>
))}
</Select>
{errors.handledBy && (
<Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 1.75 }}>
{errors.handledBy}
</Typography>
)}
</FormControl>
</Grid>
</>
) : (<></>)}
<Grid item xs={12}>
<FormControl fullWidth error={!!errors.badReason}>
<InputLabel>{t('Bad Reason')}</InputLabel>
<Select
value={formData.badReason || ''}
onChange={(e) => handleInputChange('badReason', e.target.value)}
label={t('Bad Reason')}
>
<MenuItem value="">{t('Select Bad Reason')}</MenuItem>
<MenuItem value="quantity_problem">{t('Quantity Problem')}</MenuItem>
<MenuItem value="package_problem">{t('Package Problem')}</MenuItem>
</Select>
{errors.badReason && (
<Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 1.75 }}>
{errors.badReason}
</Typography>
)}
</FormControl>
</Grid>
) : null}


</Grid>
</Box>
</DialogContent>


+ 127
- 113
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx Ver arquivo

@@ -690,7 +690,7 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
stockOutLineId: lot.stockOutLineId,
stockOutLineStatus: lot.stockOutLineStatus,
stockOutLineQty: lot.stockOutLineQty,
stockInLineId: lot.stockInLineId,
routerId: lot.router?.id,
routerIndex: lot.router?.index,
routerRoute: lot.router?.route,
@@ -1217,7 +1217,50 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`);
}
}, [combinedLotData, updateStockOutLineStatusByQRCodeAndLotNo]);

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[]>(); // ✅ 新增:按 stockInLineId 索引
combinedLotData.forEach(lot => {
if (lot.itemId) {
if (!byItemId.has(lot.itemId)) {
byItemId.set(lot.itemId, []);
}
byItemId.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);
}
// ✅ 新增:按 stockInLineId 索引
if (lot.stockInLineId) {
if (!byStockInLineId.has(lot.stockInLineId)) {
byStockInLineId.set(lot.stockInLineId, []);
}
byStockInLineId.get(lot.stockInLineId)!.push(lot);
}
});
return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId }; // ✅ 添加 byStockInLineId
}, [combinedLotData]);

const processOutsideQrCode = useCallback(async (latestQr: string) => {
// 1) Parse JSON safely
@@ -1232,7 +1275,7 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
}
try {
// Only use the new API when we have JSON with stockInLineId + itemId
// ✅ OPTIMIZATION: 直接使用 QR 数据,不需要调用 analyzeQrCode API
if (!(qrData?.stockInLineId && qrData?.itemId)) {
console.log("QR JSON missing required fields (itemId, stockInLineId).");
setQrScanError(true);
@@ -1240,45 +1283,30 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
return;
}
// Call new analyze-qr-code API
const analysis = await analyzeQrCode({
itemId: qrData.itemId,
stockInLineId: qrData.stockInLineId
});
const scannedItemId = qrData.itemId;
const scannedStockInLineId = qrData.stockInLineId;
if (!analysis) {
console.error("analyzeQrCode returned no data");
setQrScanError(true);
setQrScanSuccess(false);
return;
// ✅ OPTIMIZATION: 使用索引快速查找相同 item 的 lots
const sameItemLots: any[] = [];
// 使用索引快速查找
if (lotDataIndexes.byItemId.has(scannedItemId)) {
sameItemLots.push(...lotDataIndexes.byItemId.get(scannedItemId)!);
}
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
if (sameItemLots.length === 0) {
console.error("No item match in expected lots for scanned code");
setQrScanError(true);
setQrScanSuccess(false);
return;
}
// FIXED: Find the ACTIVE suggested lot (not rejected lots)
const activeSuggestedLots = sameItemLotsInExpected.filter(lot =>
lot.lotAvailability !== 'rejected' &&
lot.stockOutLineStatus !== 'rejected' &&
lot.processingStatus !== 'rejected'
// ✅ OPTIMIZATION: 过滤出活跃的 lots(非 rejected)
const rejectedStatuses = new Set(['rejected']);
const activeSuggestedLots = sameItemLots.filter(lot =>
!rejectedStatuses.has(lot.lotAvailability) &&
!rejectedStatuses.has(lot.stockOutLineStatus) &&
!rejectedStatuses.has(lot.processingStatus)
);
if (activeSuggestedLots.length === 0) {
@@ -1288,77 +1316,63 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
return;
}
// 2) Check if scanned lot is exactly in active suggested lots
const exactLotMatch = activeSuggestedLots.find(l =>
(scanned?.inventoryLotLineId && l.lotId === scanned.inventoryLotLineId) ||
(scanned?.lotNo && l.lotNo === scanned.lotNo)
// ✅ OPTIMIZATION: 按优先级查找匹配的 lot
// 1. 首先查找 stockInLineId 完全匹配的(正确的 lot)
let exactMatch = activeSuggestedLots.find(lot =>
lot.stockInLineId === scannedStockInLineId
);
if (exactLotMatch && scanned?.lotNo) {
// ✅ Case 1: 使用 updateStockOutLineStatusByQRCodeAndLotNo API(更快)
console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`);
if (exactMatch) {
// ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认
console.log(`✅ Exact stockInLineId match found for lot: ${exactMatch.lotNo}`);
if (!exactLotMatch.stockOutLineId) {
console.warn("No stockOutLineId on exactLotMatch, cannot update status by QR.");
if (!exactMatch.stockOutLineId) {
console.warn("No stockOutLineId on exactMatch, cannot update status by QR.");
setQrScanError(true);
setQrScanSuccess(false);
return;
}
try {
// ✅ 直接调用后端 API,后端会处理所有匹配逻辑
const res = await updateStockOutLineStatusByQRCodeAndLotNo({
pickOrderLineId: exactLotMatch.pickOrderLineId,
inventoryLotNo: scanned.lotNo,
stockOutLineId: exactLotMatch.stockOutLineId,
itemId: exactLotMatch.itemId,
pickOrderLineId: exactMatch.pickOrderLineId,
inventoryLotNo: exactMatch.lotNo,
stockOutLineId: exactMatch.stockOutLineId,
itemId: exactMatch.itemId,
status: "checked",
});
console.log("updateStockOutLineStatusByQRCodeAndLotNo result:", res);
// 后端返回三种 code:checked / LOT_NUMBER_MISMATCH / ITEM_MISMATCH
if (res.code === "checked" || res.code === "SUCCESS") {
// ✅ 完全匹配 - 只更新本地状态,不调用 fetchAllCombinedLotData
setQrScanError(false);
setQrScanSuccess(true);
// ✅ 更新本地状态
const entity = res.entity as any;
setCombinedLotData(prev => prev.map(lot => {
if (lot.stockOutLineId === exactLotMatch.stockOutLineId &&
lot.pickOrderLineId === exactLotMatch.pickOrderLineId) {
if (lot.stockOutLineId === exactMatch.stockOutLineId &&
lot.pickOrderLineId === exactMatch.pickOrderLineId) {
return {
...lot,
stockOutLineStatus: 'checked',
stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
stockOutLineQty: entity?.qty ?? lot.stockOutLineQty,
};
}
return lot;
}));
setOriginalCombinedData(prev => prev.map(lot => {
if (lot.stockOutLineId === exactLotMatch.stockOutLineId &&
lot.pickOrderLineId === exactLotMatch.pickOrderLineId) {
if (lot.stockOutLineId === exactMatch.stockOutLineId &&
lot.pickOrderLineId === exactMatch.pickOrderLineId) {
return {
...lot,
stockOutLineStatus: 'checked',
stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
stockOutLineQty: entity?.qty ?? lot.stockOutLineQty,
};
}
return lot;
}));
console.log("✅ Status updated locally, no full data refresh needed");
} else if (res.code === "LOT_NUMBER_MISMATCH") {
console.warn("Backend reported LOT_NUMBER_MISMATCH:", res.message);
setQrScanError(true);
setQrScanSuccess(false);
} else if (res.code === "ITEM_MISMATCH") {
console.warn("Backend reported ITEM_MISMATCH:", res.message);
setQrScanError(true);
setQrScanSuccess(false);
} else {
console.warn("Unexpected response code from backend:", res.code);
setQrScanError(true);
@@ -1370,10 +1384,11 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
setQrScanSuccess(false);
}
return; // ✅ 直接返回,不再调用 handleQrCodeSubmit
return; // ✅ 直接返回,不需要确认表单
}
// Case 2: Item matches but lot number differs -> open confirmation modal
// ✅ Case 2: itemId 匹配但 stockInLineId 不匹配 - 显示确认表单
// 取第一个活跃的 lot 作为期望的 lot
const expectedLot = activeSuggestedLots[0];
if (!expectedLot) {
console.error("Could not determine expected lot for confirmation");
@@ -1382,39 +1397,38 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
return;
}
// Check if the expected lot is already the scanned lot (after substitution)
if (expectedLot.lotNo === scanned?.lotNo) {
console.log(`Lot already substituted, proceeding with ${scanned.lotNo}`);
handleQrCodeSubmit(scanned.lotNo);
return;
}
console.log(` Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`);
// ✅ 立即打开确认模态框,不等待其他操作
console.log(`⚠️ Lot mismatch: Expected stockInLineId=${expectedLot.stockInLineId}, Scanned stockInLineId=${scannedStockInLineId}`);
setSelectedLotForQr(expectedLot);
// ✅ 获取扫描的 lot 信息(从 QR 数据中提取,或使用默认值)
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: null, // 扫描的 lotNo 未知,需要从后端获取或显示为未知
itemCode: expectedLot.itemCode,
itemName: expectedLot.itemName,
inventoryLotLineId: null,
stockInLineId: scannedStockInLineId // ✅ 传递 stockInLineId
}
);
} catch (error) {
console.error("Error during analyzeQrCode flow:", error);
console.error("Error during QR code processing:", error);
setQrScanError(true);
setQrScanSuccess(false);
return;
}
}, [combinedLotData, handleQrCodeSubmit, handleLotMismatch]);
// Update the outside QR scanning effect to use enhanced processing
// Update the outside QR scanning effect to use enhanced processing
}, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotDataIndexes, updateStockOutLineStatusByQRCodeAndLotNo]);
useEffect(() => {
if (lotConfirmationOpen || manualLotConfirmationOpen) {
console.log("Confirmation modal is open, skipping QR processing...");
return;
}
if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) {
return;
}
@@ -1965,8 +1979,8 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe

const handleSkip = useCallback(async (lot: any) => {
try {
console.log("Skip clicked, submit 0 qty for lot:", lot.lotNo);
await handleSubmitPickQtyWithQty(lot, 0);
console.log("Skip clicked, submit lot required qty for lot:", lot.lotNo);
await handleSubmitPickQtyWithQty(lot, lot.requiredQty);
} catch (err) {
console.error("Error in Skip:", err);
}
@@ -2749,27 +2763,27 @@ paginatedData.map((lot, index) => {
/>
{/* 保留:Lot Confirmation Modal */}
{lotConfirmationOpen && expectedLotData && scannedLotData && (
<LotConfirmationModal
open={lotConfirmationOpen}
onClose={() => {
setLotConfirmationOpen(false);
setExpectedLotData(null);
setScannedLotData(null);
if (lastProcessedQr) {
setProcessedQrCodes(prev => {
const newSet = new Set(prev);
newSet.delete(lastProcessedQr);
return newSet;
});
setLastProcessedQr('');
}
}}
onConfirm={handleLotConfirmation}
expectedLot={expectedLotData}
scannedLot={scannedLotData}
isLoading={isConfirmingLot}
/>
)}
<LotConfirmationModal
open={lotConfirmationOpen}
onClose={() => {
setLotConfirmationOpen(false);
setExpectedLotData(null);
setScannedLotData(null);
setSelectedLotForQr(null); // ✅ 新增:清除选中的 lot
// ✅ 修复:不要清除 processedQrCodes,而是保留它,避免重复处理
// 或者,如果确实需要清除,应该在清除后立即重新标记为已处理
if (lastProcessedQr) {
setLastProcessedQr('');
}
}}
onConfirm={handleLotConfirmation}
expectedLot={expectedLotData}
scannedLot={scannedLotData}
isLoading={isConfirmingLot}
/>
)}
{/* 保留:Good Pick Execution Form Modal */}
{pickExecutionFormOpen && selectedLotForExecutionForm && (


Carregando…
Cancelar
Salvar