|
|
|
@@ -20,7 +20,7 @@ import { useCallback, useEffect, useState } from "react"; |
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions"; |
|
|
|
import { fetchEscalationCombo } from "@/app/api/user/actions"; |
|
|
|
|
|
|
|
import { useRef } from "react"; |
|
|
|
interface LotPickData { |
|
|
|
id: number; |
|
|
|
lotId: number; |
|
|
|
@@ -81,7 +81,6 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ |
|
|
|
const [errors, setErrors] = useState<FormErrors>({}); |
|
|
|
const [loading, setLoading] = useState(false); |
|
|
|
const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]); |
|
|
|
|
|
|
|
// 计算剩余可用数量 |
|
|
|
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { |
|
|
|
const remainingQty = lot.inQty - lot.outQty; |
|
|
|
@@ -92,7 +91,18 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ |
|
|
|
// 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 () => { |
|
|
|
@@ -107,55 +117,49 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ |
|
|
|
fetchHandlers(); |
|
|
|
}, []); |
|
|
|
|
|
|
|
// 初始化表单数据 - 每次打开时都重新初始化 |
|
|
|
const initKeyRef = useRef<string | null>(null); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (open && selectedLot && selectedPickOrderLine && pickOrderId) { |
|
|
|
const getSafeDate = (dateValue: any): string => { |
|
|
|
if (!dateValue) return new Date().toISOString().split('T')[0]; |
|
|
|
try { |
|
|
|
const date = new Date(dateValue); |
|
|
|
if (isNaN(date.getTime())) { |
|
|
|
return new Date().toISOString().split('T')[0]; |
|
|
|
} |
|
|
|
return date.toISOString().split('T')[0]; |
|
|
|
} catch { |
|
|
|
return new Date().toISOString().split('T')[0]; |
|
|
|
} |
|
|
|
}; |
|
|
|
if (!open || !selectedLot || !selectedPickOrderLine || !pickOrderId) return; |
|
|
|
|
|
|
|
// 计算剩余可用数量 |
|
|
|
const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); |
|
|
|
const requiredQty = calculateRequiredQty(selectedLot); |
|
|
|
console.log("=== PickExecutionForm Debug ==="); |
|
|
|
console.log("selectedLot:", selectedLot); |
|
|
|
console.log("inQty:", selectedLot.inQty); |
|
|
|
console.log("outQty:", selectedLot.outQty); |
|
|
|
console.log("holdQty:", selectedLot.holdQty); |
|
|
|
console.log("availableQty:", selectedLot.availableQty); |
|
|
|
console.log("calculated remainingAvailableQty:", remainingAvailableQty); |
|
|
|
console.log("=== End Debug ==="); |
|
|
|
setFormData({ |
|
|
|
pickOrderId: pickOrderId, |
|
|
|
pickOrderCode: selectedPickOrderLine.pickOrderCode, |
|
|
|
pickOrderCreateDate: getSafeDate(pickOrderCreateDate), |
|
|
|
pickExecutionDate: new Date().toISOString().split('T')[0], |
|
|
|
pickOrderLineId: selectedPickOrderLine.id, |
|
|
|
itemId: selectedPickOrderLine.itemId, |
|
|
|
itemCode: selectedPickOrderLine.itemCode, |
|
|
|
itemDescription: selectedPickOrderLine.itemName, |
|
|
|
lotId: selectedLot.lotId, |
|
|
|
lotNo: selectedLot.lotNo, |
|
|
|
storeLocation: selectedLot.location, |
|
|
|
requiredQty: selectedLot.requiredQty, |
|
|
|
actualPickQty: selectedLot.actualPickQty || 0, |
|
|
|
missQty: 0, |
|
|
|
badItemQty: 0, // 初始化为 0,用户需要手动输入 |
|
|
|
issueRemark: '', |
|
|
|
pickerName: '', |
|
|
|
handledBy: undefined, |
|
|
|
}); |
|
|
|
} |
|
|
|
}, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]); |
|
|
|
// Only initialize once per (pickOrderLineId + lotId) while dialog open |
|
|
|
const key = `${selectedPickOrderLine.id}-${selectedLot.lotId}`; |
|
|
|
if (initKeyRef.current === key) return; |
|
|
|
|
|
|
|
const getSafeDate = (dateValue: any): string => { |
|
|
|
if (!dateValue) return new Date().toISOString().split('T')[0]; |
|
|
|
try { |
|
|
|
const d = new Date(dateValue); |
|
|
|
return isNaN(d.getTime()) ? new Date().toISOString().split('T')[0] : d.toISOString().split('T')[0]; |
|
|
|
} catch { |
|
|
|
return new Date().toISOString().split('T')[0]; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
setFormData({ |
|
|
|
pickOrderId: pickOrderId, |
|
|
|
pickOrderCode: selectedPickOrderLine.pickOrderCode, |
|
|
|
pickOrderCreateDate: getSafeDate(pickOrderCreateDate), |
|
|
|
pickExecutionDate: new Date().toISOString().split('T')[0], |
|
|
|
pickOrderLineId: selectedPickOrderLine.id, |
|
|
|
itemId: selectedPickOrderLine.itemId, |
|
|
|
itemCode: selectedPickOrderLine.itemCode, |
|
|
|
itemDescription: selectedPickOrderLine.itemName, |
|
|
|
lotId: selectedLot.lotId, |
|
|
|
lotNo: selectedLot.lotNo, |
|
|
|
storeLocation: selectedLot.location, |
|
|
|
requiredQty: selectedLot.requiredQty, |
|
|
|
actualPickQty: selectedLot.actualPickQty || 0, |
|
|
|
missQty: 0, |
|
|
|
badItemQty: 0, |
|
|
|
issueRemark: '', |
|
|
|
pickerName: '', |
|
|
|
handledBy: undefined, |
|
|
|
}); |
|
|
|
|
|
|
|
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 })); |
|
|
|
@@ -168,30 +172,23 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ |
|
|
|
// ✅ Update form validation to require either missQty > 0 OR badItemQty > 0 |
|
|
|
const validateForm = (): boolean => { |
|
|
|
const newErrors: FormErrors = {}; |
|
|
|
|
|
|
|
if (formData.actualPickQty === undefined || formData.actualPickQty < 0) { |
|
|
|
newErrors.actualPickQty = t('Qty is required'); |
|
|
|
} |
|
|
|
|
|
|
|
// ✅ FIXED: Check if actual pick qty exceeds remaining available qty |
|
|
|
if (formData.actualPickQty && formData.actualPickQty > remainingAvailableQty) { |
|
|
|
newErrors.actualPickQty = t('Qty is not allowed to be greater than remaining available qty'); |
|
|
|
} |
|
|
|
|
|
|
|
// ✅ FIXED: Check if actual pick qty exceeds required qty (use original required qty) |
|
|
|
if (formData.actualPickQty && formData.actualPickQty > (selectedLot?.requiredQty || 0)) { |
|
|
|
newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty'); |
|
|
|
const req = selectedLot?.requiredQty || 0; |
|
|
|
const ap = formData.actualPickQty || 0; |
|
|
|
const miss = formData.missQty || 0; |
|
|
|
const bad = formData.badItemQty || 0; |
|
|
|
|
|
|
|
if (ap < 0) newErrors.actualPickQty = t('Qty is required'); |
|
|
|
if (ap > Math.min(remainingAvailableQty, req)) newErrors.actualPickQty = t('Qty is not allowed to be greater than required/available qty'); |
|
|
|
if (miss < 0) newErrors.missQty = t('Invalid qty'); |
|
|
|
if (bad < 0) newErrors.badItemQty = t('Invalid qty'); |
|
|
|
if (ap + miss + bad > req) { |
|
|
|
newErrors.actualPickQty = t('Total exceeds required qty'); |
|
|
|
newErrors.missQty = t('Total exceeds required qty'); |
|
|
|
} |
|
|
|
|
|
|
|
// ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported) |
|
|
|
const hasMissQty = formData.missQty && formData.missQty > 0; |
|
|
|
const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0; |
|
|
|
|
|
|
|
if (!hasMissQty && !hasBadItemQty) { |
|
|
|
newErrors.missQty = t('At least one issue must be reported'); |
|
|
|
newErrors.badItemQty = t('At least one issue must be reported'); |
|
|
|
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'); |
|
|
|
} |
|
|
|
|
|
|
|
setErrors(newErrors); |
|
|
|
return Object.keys(newErrors).length === 0; |
|
|
|
}; |
|
|
|
@@ -266,42 +263,42 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ |
|
|
|
</Grid> |
|
|
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t('Actual Pick Qty')} |
|
|
|
type="number" |
|
|
|
value={formData.actualPickQty || 0} |
|
|
|
onChange={(e) => handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)} |
|
|
|
error={!!errors.actualPickQty} |
|
|
|
helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`} |
|
|
|
variant="outlined" |
|
|
|
/> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t('Actual Pick Qty')} |
|
|
|
type="number" |
|
|
|
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)}`} |
|
|
|
variant="outlined" |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t('Missing item Qty')} |
|
|
|
type="number" |
|
|
|
value={formData.missQty || 0} |
|
|
|
onChange={(e) => handleInputChange('missQty', parseFloat(e.target.value) || 0)} |
|
|
|
error={!!errors.missQty} |
|
|
|
// helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')} |
|
|
|
variant="outlined" |
|
|
|
/> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t('Missing item Qty')} |
|
|
|
type="number" |
|
|
|
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} |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t('Bad Item Qty')} |
|
|
|
type="number" |
|
|
|
value={formData.badItemQty || 0} |
|
|
|
onChange={(e) => handleInputChange('badItemQty', parseFloat(e.target.value) || 0)} |
|
|
|
error={!!errors.badItemQty} |
|
|
|
// helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')} |
|
|
|
variant="outlined" |
|
|
|
/> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t('Bad Item Qty')} |
|
|
|
type="number" |
|
|
|
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} |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
|
|
|
|
{/* ✅ Show issue description and handler fields when bad items > 0 */} |
|
|
|
|