// 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) => { const remainingQty = lot.inQty - lot.outQty; return Math.max(0, remainingQty); }, []); 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 = 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'); } 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; }; const handleSubmit = async () => { if (!validateForm() || !formData.pickOrderId) { return; } setLoading(true); try { await onSubmit(formData as PickExecutionIssueData); onClose(); } catch (error) { console.error('Error submitting pick execution issue:', error); } 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;