|
- // 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<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) => {
- // ✅ 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<string | null>(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 (
- <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"
- // helperText={t('Available in warehouse')}
- />
- </Grid>
-
- <Grid item xs={12}>
- <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', 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', 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 */}
- {(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)}
- 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;
|