|
- // 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 { useSession } from "next-auth/react";
- import { SessionWithTokens } from "@/config/authConfig";
- 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;
- pickOrderLineId?: number;
- pickOrderId?: number;
- pickOrderCode?: string;
- }
-
- interface PickExecutionFormProps {
- open: boolean;
- onClose: () => void;
- onSubmit: (data: PickExecutionIssueData) => Promise<void>;
- selectedLot: LotPickData | null;
- selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
- pickOrderId?: number;
- pickOrderCreateDate: any;
- onNormalPickSubmit?: (lot: LotPickData, submitQty: number) => Promise<void>;
- // 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,
- onNormalPickSubmit,
-
- }) => {
- 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 [verifiedQty, setVerifiedQty] = useState<number>(0);
- const { data: session } = useSession() as { data: SessionWithTokens | null };
-
- const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
- 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;
- }, []);
- useEffect(() => {
- console.log('PickExecutionForm props:', {
- open,
- onNormalPickSubmit: typeof onNormalPickSubmit,
- hasOnNormalPickSubmit: !!onNormalPickSubmit,
- onSubmit: typeof onSubmit,
- });
- }, [open, onNormalPickSubmit, onSubmit]);
-
- // 获取处理人员列表
- 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 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);
- }
- };
-
- // Initialize verified quantity to the received quantity (actualPickQty)
- const initialVerifiedQty = selectedLot.actualPickQty || 0;
- setVerifiedQty(initialVerifiedQty);
-
- console.log("=== PickExecutionForm Debug ===");
- console.log("selectedLot:", selectedLot);
- console.log("initialVerifiedQty:", initialVerifiedQty);
- console.log("=== End Debug ===");
-
- 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: initialVerifiedQty,
- missQty: 0,
- badItemQty: 0,
- issueRemark: '',
- handledBy: undefined,
- });
- }
- // 只在 open 状态改变时重新初始化,移除其他依赖
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [open]);
-
- const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
- setFormData(prev => ({ ...prev, [field]: value }));
-
- // Update verified quantity state when actualPickQty changes
- if (field === 'actualPickQty') {
- setVerifiedQty(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 requiredQty = selectedLot?.requiredQty || 0;
- const badItemQty = formData.badItemQty || 0;
- const missQty = formData.missQty || 0;
-
- if (verifiedQty === undefined || verifiedQty < 0) {
- newErrors.actualPickQty = t('Qty is required');
- }
-
- const totalQty = verifiedQty + badItemQty + missQty;
- const hasAnyValue = verifiedQty > 0 || badItemQty > 0 || missQty > 0;
-
- // ✅ 新增:必须至少有一个 > 0
- if (!hasAnyValue) {
- newErrors.actualPickQty = t('At least one of Verified / Missing / Bad must be greater than 0');
- }
-
- if (hasAnyValue && totalQty !== requiredQty) {
- newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity');
- }
-
- setErrors(newErrors);
- return Object.keys(newErrors).length === 0;
- };
- const handleSubmit = async () => {
- if (!formData.pickOrderId || !selectedLot) {
- return;
- }
-
- // ✅ 只允许 Verified>0 且没有问题时,走 normal pick
- const isNormalPick = verifiedQty > 0
- && formData.missQty == 0
- && formData.badItemQty == 0;
-
- if (isNormalPick) {
- if (onNormalPickSubmit) {
- setLoading(true);
- try {
- console.log('Calling onNormalPickSubmit with:', { lot: selectedLot, submitQty: verifiedQty });
- await onNormalPickSubmit(selectedLot, verifiedQty);
- onClose();
- } catch (error) {
- console.error('Error submitting normal pick:', error);
- } finally {
- setLoading(false);
- }
- } else {
- console.warn('onNormalPickSubmit callback not provided');
- }
- return;
- }
-
- // ❌ 有问题(或全部为 0)才进入 Issue 提报流程
- if (!validateForm() || !formData.pickOrderId) {
- return;
- }
-
- setLoading(true);
- try {
- const submissionData = {
- ...formData,
- actualPickQty: verifiedQty,
- lotId: formData.lotId || selectedLot?.lotId || 0,
- lotNo: formData.lotNo || selectedLot?.lotNo || '',
- pickOrderCode: formData.pickOrderCode || selectedPickOrderLine?.pickOrderCode || '',
- pickerName: session?.user?.name || ''
- } as PickExecutionIssueData;
-
- await onSubmit(submissionData);
- onClose();
- } catch (error) {
- console.error('Error submitting pick execution issue:', error);
- } finally {
- setLoading(false);
- }
- };
-
- const handleClose = () => {
- setFormData({});
- setErrors({});
- setVerifiedQty(0);
- 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('Required Qty')}
- value={selectedLot?.requiredQty || 0}
- disabled
- variant="outlined"
- // helperText={t('Still need to pick')}
- />
- </Grid>
- <Grid item xs={6}>
- <TextField
- fullWidth
- label={t('Remaining Available Qty')}
- value={remainingAvailableQty}
- disabled
- variant="outlined"
- />
- </Grid>
-
-
- <Grid item xs={12}>
- <TextField
- fullWidth
- label={t('Verified Qty')}
- type="number"
- value={verifiedQty}
- onChange={(e) => {
- const newValue = parseFloat(e.target.value) || 0;
- setVerifiedQty(newValue);
- // handleInputChange('actualPickQty', newValue);
- }}
- error={!!errors.actualPickQty}
- helperText={errors.actualPickQty || `${t('Max')}: ${selectedLot?.actualPickQty || 0}`} // 使用原始接收数量
- variant="outlined"
- />
- </Grid>
-
- <Grid item xs={12}>
- <TextField
- fullWidth
- label={t('Missing item Qty')}
- type="number"
- value={formData.missQty || 0}
- onChange={(e) => {
- const newMissQty = parseFloat(e.target.value) || 0;
- handleInputChange('missQty', newMissQty);
- // 不要自动修改其他字段
- }}
- error={!!errors.missQty}
- helperText={errors.missQty}
- variant="outlined"
- />
- </Grid>
-
- <Grid item xs={12}>
- <TextField
- fullWidth
- label={t('Bad Item Qty')}
- type="number"
- value={formData.badItemQty || 0}
- onChange={(e) => {
- const newBadItemQty = parseFloat(e.target.value) || 0;
- handleInputChange('badItemQty', newBadItemQty);
- // 不要自动修改其他字段
- }}
- error={!!errors.badItemQty}
- helperText={errors.badItemQty}
- 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('Issue Remark')}
- multiline
- rows={4}
- value={formData.issueRemark || ''}
- onChange={(e) => {
- handleInputChange('issueRemark', e.target.value);
- // Don't reset badItemQty when typing in issue remark
- }}
- 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);
- // Don't reset badItemQty when selecting handler
- }}
- 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;
|