|
- // 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) => {
- // 直接使用 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<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 = 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 (
- <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;
|