Parcourir la source

update

master
CANCERYS\kw093 il y a 2 mois
Parent
révision
6e08f763ef
4 fichiers modifiés avec 440 ajouts et 29 suppressions
  1. +34
    -27
      src/components/Jodetail/JobPickExecutionForm.tsx
  2. +1
    -1
      src/components/Jodetail/JobPickExecutionsecondscan.tsx
  3. +403
    -0
      src/components/Jodetail/JobmatchForm.tsx
  4. +2
    -1
      src/i18n/zh/jo.json

+ 34
- 27
src/components/Jodetail/JobPickExecutionForm.tsx Voir le fichier

@@ -173,18 +173,21 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
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');
}
// ✅ Check if verified qty exceeds received qty
if (verifiedQty > (selectedLot?.actualPickQty || 0)) {
newErrors.actualPickQty = t('Verified quantity cannot exceed received quantity');
}
// ✅ 移除接收数量检查,因为在 JobPickExecution 阶段 receivedQty 总是 0
// if (verifiedQty > receivedQty) { ... } ← 删除
// ✅ Check if verified qty exceeds required qty
if (verifiedQty > (selectedLot?.requiredQty || 0)) {
newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty');
// ✅ 只检查总和是否等于需求数量
const totalQty = verifiedQty + badItemQty + missQty;
if (totalQty !== requiredQty) {
newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity');
}
// ✅ Require either missQty > 0 OR badItemQty > 0
@@ -199,7 +202,6 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleSubmit = async () => {
if (!validateForm() || !formData.pickOrderId) {
return;
@@ -265,30 +267,21 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label={t('Received Qty')}
value={formData.actualPickQty || 0}
disabled
variant="outlined"
// helperText={t('Available in warehouse')}
/>
</Grid>


<Grid item xs={12}>
<TextField
fullWidth
label={t('Verified Qty')}
type="number"
value={verifiedQty} // ✅ Use the separate state
value={verifiedQty}
onChange={(e) => {
const newValue = parseFloat(e.target.value) || 0;
setVerifiedQty(newValue);
handleInputChange('actualPickQty', newValue);
}}
error={!!errors.actualPickQty}
helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(selectedLot?.actualPickQty || 0, selectedLot?.requiredQty || 0)}`}
helperText={errors.actualPickQty || `${t('Max')}: ${selectedLot?.actualPickQty || 0}`} // ✅ 使用原始接收数量
variant="outlined"
/>
</Grid>
@@ -299,9 +292,13 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
label={t('Missing item Qty')}
type="number"
value={formData.missQty || 0}
onChange={(e) => handleInputChange('missQty', parseFloat(e.target.value) || 0)}
onChange={(e) => {
const newMissQty = parseFloat(e.target.value) || 0;
handleInputChange('missQty', newMissQty);
// ✅ 不要自动修改其他字段
}}
error={!!errors.missQty}
// helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')}
helperText={errors.missQty}
variant="outlined"
/>
</Grid>
@@ -312,9 +309,13 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
label={t('Bad Item Qty')}
type="number"
value={formData.badItemQty || 0}
onChange={(e) => handleInputChange('badItemQty', parseFloat(e.target.value) || 0)}
onChange={(e) => {
const newBadItemQty = parseFloat(e.target.value) || 0;
handleInputChange('badItemQty', newBadItemQty);
// ✅ 不要自动修改其他字段
}}
error={!!errors.badItemQty}
// helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')}
helperText={errors.badItemQty}
variant="outlined"
/>
</Grid>
@@ -322,7 +323,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
{/* ✅ Show issue description and handler fields when bad items > 0 */}
{(formData.badItemQty && formData.badItemQty > 0) ? (
<>
<Grid item xs={12}>
<Grid item xs={12}>
<TextField
fullWidth
id="issueRemark"
@@ -330,7 +331,10 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
multiline
rows={4}
value={formData.issueRemark || ''}
onChange={(e) => handleInputChange('issueRemark', e.target.value)}
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')}
@@ -343,7 +347,10 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
<InputLabel>{t('handler')}</InputLabel>
<Select
value={formData.handledBy ? formData.handledBy.toString() : ''}
onChange={(e) => handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined)}
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) => (


+ 1
- 1
src/components/Jodetail/JobPickExecutionsecondscan.tsx Voir le fichier

@@ -46,7 +46,7 @@ import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerP
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import { fetchStockInLineInfo } from "@/app/api/po/actions";
import GoodPickExecutionForm from "./JobPickExecutionForm";
import GoodPickExecutionForm from "./JobmatchForm";
import FGPickOrderCard from "./FGPickOrderCard";

interface Props {


+ 403
- 0
src/components/Jodetail/JobmatchForm.tsx Voir le fichier

@@ -0,0 +1,403 @@
// 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 [verifiedQty, setVerifiedQty] = useState<number>(0);
// 计算剩余可用数量
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;
}, []);
// 获取处理人员列表
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];
}
};

// ✅ 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: 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: initialVerifiedQty, // ✅ Use the initial value
missQty: 0,
badItemQty: 0,
issueRemark: '',
pickerName: '',
handledBy: undefined,
});
}
}, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate]);

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 = {};
// ✅ 使用原始的接收数量,而不是 formData 中的
const receivedQty = selectedLot?.actualPickQty || 0;
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');
}
// ✅ 验证数量不能超过原始接收数量
if (verifiedQty > receivedQty) {
newErrors.actualPickQty = t('Verified quantity cannot exceed received quantity');
}
// ✅ 只检查总和是否等于需求数量
const totalQty = verifiedQty + badItemQty + missQty;
if (totalQty !== requiredQty) {
newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity');
}
// ✅ Require either missQty > 0 OR badItemQty > 0
const hasMissQty = formData.missQty && formData.missQty > 0;
const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0;
if (!hasMissQty && !hasBadItemQty) {
newErrors.missQty = t('At least one issue must be reported');
newErrors.badItemQty = t('At least one issue must be reported');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async () => {
if (!validateForm() || !formData.pickOrderId) {
return;
}

setLoading(true);
try {
// ✅ Use the verified quantity in the submission
const submissionData = {
...formData,
actualPickQty: verifiedQty
} 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('Received Qty')}
value={selectedLot?.actualPickQty || 0}
disabled
variant="outlined"
// helperText={t('Available in warehouse')}
/>
</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;

+ 2
- 1
src/i18n/zh/jo.json Voir le fichier

@@ -270,5 +270,6 @@
"Submit All Scanned": "提交所有已掃描項目",
"Submitting...": "提交中...",
"COMPLETED": "已完成",
"success": "成功"
"success": "成功",
"Total (Verified + Bad + Missing) must equal Required quantity": "驗證數量 + 不良數量 + 缺失數量必須等於需求數量"
}

Chargement…
Annuler
Enregistrer