// FPSMS-frontend/src/components/PickOrderSearch/PickExecutionForm.tsx "use client"; import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, Grid, InputLabel, MenuItem, Select, TextField, Typography, } from "@mui/material"; 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"; import dayjs from 'dayjs'; import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; interface LotPickData { id: number; lotId: number; lotNo: string; expiryDate: string; location: string; stockUnit: string; inQty: number; outQty: number; holdQty: number; totalPickedByAllPickOrders: number; availableQty: number; requiredQty: number; actualPickQty: number; lotStatus: string; lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected'; stockOutLineId?: number; stockOutLineStatus?: string; stockOutLineQty?: number; } interface PickExecutionFormProps { open: boolean; onClose: () => void; onSubmit: (data: PickExecutionIssueData) => Promise; selectedLot: LotPickData | null; 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; // selectedRowId?: number | null; } // 定义错误类型 interface FormErrors { actualPickQty?: string; missQty?: string; badItemQty?: string; issueRemark?: string; handledBy?: string; } const PickExecutionForm: React.FC = ({ open, onClose, onSubmit, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, // Remove these props // onNormalPickSubmit, // selectedRowId, }) => { const { t } = useTranslation(); const [formData, setFormData] = useState>({}); const [errors, setErrors] = useState({}); const [loading, setLoading] = useState(false); const [handlers, setHandlers] = useState>([]); // 计算剩余可用数量 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 { const escalationCombo = await fetchEscalationCombo(); setHandlers(escalationCombo); } catch (error) { console.error("Error fetching handlers:", error); } }; fetchHandlers(); }, []); const initKeyRef = useRef(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; const getSafeDate = (dateValue: any): string => { if (!dateValue) return dayjs().format(INPUT_DATE_FORMAT); try { const date = dayjs(dateValue); if (!date.isValid()) { return dayjs().format(INPUT_DATE_FORMAT); } return date.format(INPUT_DATE_FORMAT); } catch { return dayjs().format(INPUT_DATE_FORMAT); } }; setFormData({ pickOrderId: pickOrderId, pickOrderCode: selectedPickOrderLine.pickOrderCode, pickOrderCreateDate: getSafeDate(pickOrderCreateDate), pickExecutionDate: dayjs().format(INPUT_DATE_FORMAT), 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 })); // 清除错误 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; // 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; }; const handleSubmit = async () => { // First validate the form if (!validateForm()) { console.error('Form validation failed:', errors); return; // Prevent submission, show validation errors } if (!formData.pickOrderId) { console.error('Missing pickOrderId'); return; } 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); } }; const handleClose = () => { setFormData({}); setErrors({}); onClose(); }; if (!selectedLot || !selectedPickOrderLine) { return null; } const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); const requiredQty = calculateRequiredQty(selectedLot); return ( {t('Pick Execution Issue Form')} {/* Always show issue form title */} {/* Add instruction text */} {t('Note:')} {t('This form is for reporting issues only. You must report either missing items or bad items.')} {/* Keep the existing form fields */} 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" /> handleInputChange('missQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))} error={!!errors.missQty} variant="outlined" //disabled={(formData.actualPickQty || 0) > 0} /> handleInputChange('badItemQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))} error={!!errors.badItemQty} variant="outlined" //disabled={(formData.actualPickQty || 0) > 0} /> {/* Show issue description and handler fields when bad items > 0 */} {(formData.badItemQty && formData.badItemQty > 0) ? ( <> handleInputChange('issueRemark', e.target.value)} error={!!errors.issueRemark} helperText={errors.issueRemark} //placeholder={t('Describe the issue with bad items')} variant="outlined" /> {t('handler')} {errors.handledBy && ( {errors.handledBy} )} ) : (<>)} ); }; export default PickExecutionForm;