|
- // 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";
-
- 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<void>;
- 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<void>;
- // selectedRowId?: number | null;
- }
-
- // 定义错误类型
- interface FormErrors {
- actualPickQty?: string;
- missQty?: string;
- badItemQty?: string;
- issueRemark?: string;
- handledBy?: string;
- }
-
- const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
- open,
- onClose,
- onSubmit,
- selectedLot,
- selectedPickOrderLine,
- pickOrderId,
- pickOrderCreateDate,
- // ✅ Remove these props
- // onNormalPickSubmit,
- // selectedRowId,
- }) => {
- const { t } = useTranslation();
- 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) => {
- const remainingQty = lot.inQty - lot.outQty;
- return Math.max(0, remainingQty);
- }, []);
- const calculateRequiredQty = useCallback((lot: LotPickData) => {
- const requiredQty = lot.requiredQty-(lot.actualPickQty||0);
- return Math.max(0, requiredQty);
- }, []);
- // 获取处理人员列表
- useEffect(() => {
- const fetchHandlers = async () => {
- try {
- const escalationCombo = await fetchEscalationCombo();
- setHandlers(escalationCombo);
- } catch (error) {
- console.error("Error fetching handlers:", error);
- }
- };
-
- fetchHandlers();
- }, []);
-
- // 初始化表单数据 - 每次打开时都重新初始化
- 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];
- }
- };
-
- // 计算剩余可用数量
- 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]);
-
- 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 = {};
-
- if (formData.actualPickQty === undefined || formData.actualPickQty < 0) {
- newErrors.actualPickQty = t('pickOrder.validation.actualPickQtyRequired');
- }
-
- // ✅ 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('pickOrder.validation.mustReportMissOrBadItems');
- newErrors.badItemQty = t('pickOrder.validation.mustReportMissOrBadItems');
- }
-
- if (formData.missQty && formData.missQty < 0) {
- newErrors.missQty = t('pickOrder.validation.missQtyInvalid');
- }
-
- if (formData.badItemQty && formData.badItemQty < 0) {
- newErrors.badItemQty = t('pickOrder.validation.badItemQtyInvalid');
- }
-
- if (formData.badItemQty && formData.badItemQty > 0 && !formData.issueRemark) {
- newErrors.issueRemark = t('pickOrder.validation.issueRemarkRequired');
- }
-
- if (formData.badItemQty && formData.badItemQty > 0 && !formData.handledBy) {
- newErrors.handledBy = t('pickOrder.validation.handlerRequired');
- }
-
- 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 (
- <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
- <DialogTitle>
- {t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */}
- </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
- label={t('requiredQty')}
- value={requiredQty || 0}
- disabled
- variant="outlined"
- helperText={t('Still need to pick')}
- />
- </Grid>
-
- <Grid item xs={6}>
- <TextField
- fullWidth
- label={t('remainingAvailableQty')}
- value={remainingAvailableQty}
- disabled
- variant="outlined"
- helperText={t('Available in warehouse')}
- />
- </Grid>
-
- <Grid item xs={12}>
- <TextField
- fullWidth
- label={t('actualPickQty')}
- type="number"
- value={formData.actualPickQty || 0}
- onChange={(e) => handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)}
- error={!!errors.actualPickQty}
- helperText={errors.actualPickQty || t('Enter the quantity actually picked')}
- variant="outlined"
- />
- </Grid>
-
- <Grid item xs={12}>
- <TextField
- fullWidth
- label={t('missQty')}
- 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"
- />
- </Grid>
-
- <Grid item xs={12}>
- <TextField
- fullWidth
- label={t('badItemQty')}
- 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"
- />
- </Grid>
-
- {/* ✅ Show issue description and handler fields when bad items > 0 */}
- {(formData.badItemQty && formData.badItemQty > 0) && (
- <>
- <Grid item xs={12}>
- <TextField
- fullWidth
- id="issueRemark"
- label={t('issueRemark')}
- 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>
- </Box>
- </DialogContent>
- <DialogActions>
- <Button onClick={handleClose} disabled={loading}>
- {t('cancel')}
- </Button>
- <Button
- onClick={handleSubmit}
- variant="contained"
- disabled={loading}
- >
- {loading ? t('submitting') : t('submit')}
- </Button>
- </DialogActions>
- </Dialog>
- );
- };
-
- export default PickExecutionForm;
|