|
|
|
@@ -1,4 +1,3 @@ |
|
|
|
// FPSMS-frontend/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx |
|
|
|
"use client"; |
|
|
|
|
|
|
|
import { |
|
|
|
@@ -16,16 +15,18 @@ import { |
|
|
|
TextField, |
|
|
|
Typography, |
|
|
|
} from "@mui/material"; |
|
|
|
import { useCallback, useEffect, useState } from "react"; |
|
|
|
import { useCallback, useEffect, useState, useRef } from "react"; |
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions"; |
|
|
|
import { |
|
|
|
GetPickOrderLineInfo, |
|
|
|
PickExecutionIssueData, |
|
|
|
} from "@/app/api/pickOrder/actions"; |
|
|
|
import { fetchEscalationCombo } from "@/app/api/user/actions"; |
|
|
|
import { useRef } from "react"; |
|
|
|
import dayjs from 'dayjs'; |
|
|
|
import dayjs from "dayjs"; |
|
|
|
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; |
|
|
|
|
|
|
|
interface LotPickData { |
|
|
|
id: number; |
|
|
|
id: number; |
|
|
|
lotId: number; |
|
|
|
lotNo: string; |
|
|
|
expiryDate: string; |
|
|
|
@@ -39,7 +40,12 @@ interface LotPickData { |
|
|
|
requiredQty: number; |
|
|
|
actualPickQty: number; |
|
|
|
lotStatus: string; |
|
|
|
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected'; |
|
|
|
lotAvailability: |
|
|
|
| "available" |
|
|
|
| "insufficient_stock" |
|
|
|
| "expired" |
|
|
|
| "status_unavailable" |
|
|
|
| "rejected"; |
|
|
|
stockOutLineId?: number; |
|
|
|
stockOutLineStatus?: string; |
|
|
|
stockOutLineQty?: number; |
|
|
|
@@ -77,12 +83,14 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ |
|
|
|
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 [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>( |
|
|
|
[] |
|
|
|
); |
|
|
|
|
|
|
|
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { |
|
|
|
return lot.availableQty || 0; |
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const calculateRequiredQty = useCallback((lot: LotPickData) => { |
|
|
|
return lot.requiredQty || 0; |
|
|
|
}, []); |
|
|
|
@@ -96,7 +104,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ |
|
|
|
console.error("Error fetching handlers:", error); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
fetchHandlers(); |
|
|
|
}, []); |
|
|
|
|
|
|
|
@@ -136,92 +144,119 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ |
|
|
|
requiredQty: selectedLot.requiredQty, |
|
|
|
actualPickQty: selectedLot.actualPickQty || 0, |
|
|
|
missQty: 0, |
|
|
|
badItemQty: 0, |
|
|
|
issueRemark: '', |
|
|
|
pickerName: '', |
|
|
|
badItemQty: 0, // Bad Item Qty |
|
|
|
badPackageQty: 0, // Bad Package Qty (frontend only) |
|
|
|
issueRemark: "", |
|
|
|
pickerName: "", |
|
|
|
handledBy: undefined, |
|
|
|
reason: '', |
|
|
|
badReason: '', |
|
|
|
reason: "", |
|
|
|
badReason: "", |
|
|
|
}); |
|
|
|
|
|
|
|
initKeyRef.current = key; |
|
|
|
}, [open, selectedPickOrderLine?.id, selectedLot?.lotId, pickOrderId, pickOrderCreateDate]); |
|
|
|
}, [ |
|
|
|
open, |
|
|
|
selectedPickOrderLine?.id, |
|
|
|
selectedLot?.lotId, |
|
|
|
pickOrderId, |
|
|
|
pickOrderCreateDate, |
|
|
|
]); |
|
|
|
|
|
|
|
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]); |
|
|
|
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] |
|
|
|
); |
|
|
|
|
|
|
|
// Updated validation logic |
|
|
|
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; |
|
|
|
const badItem = Number(formData.badItemQty) || 0; |
|
|
|
const badPackage = Number((formData as any).badPackageQty) || 0; |
|
|
|
const totalBad = badItem + badPackage; |
|
|
|
const total = ap + miss + totalBad; |
|
|
|
const availableQty = selectedLot?.availableQty || 0; |
|
|
|
|
|
|
|
// 1. Check actualPickQty cannot be negative |
|
|
|
if (ap < 0) { |
|
|
|
newErrors.actualPickQty = t('Qty cannot be negative'); |
|
|
|
newErrors.actualPickQty = t("Qty cannot be negative"); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 2. Check actualPickQty cannot exceed available quantity |
|
|
|
if (ap > availableQty) { |
|
|
|
newErrors.actualPickQty = t('Actual pick qty cannot exceed available qty'); |
|
|
|
newErrors.actualPickQty = t("Actual pick qty cannot exceed available qty"); |
|
|
|
} |
|
|
|
|
|
|
|
// 3. Check missQty and badItemQty cannot be negative |
|
|
|
|
|
|
|
// 3. Check missQty and both bad qtys cannot be negative |
|
|
|
if (miss < 0) { |
|
|
|
newErrors.missQty = t('Invalid qty'); |
|
|
|
newErrors.missQty = t("Invalid qty"); |
|
|
|
} |
|
|
|
if (bad < 0) { |
|
|
|
newErrors.badItemQty = t('Invalid qty'); |
|
|
|
if (badItem < 0 || badPackage < 0) { |
|
|
|
newErrors.badItemQty = t("Invalid qty"); |
|
|
|
} |
|
|
|
|
|
|
|
// 4. NEW: Total (actualPickQty + missQty + badItemQty) cannot exceed lot available qty |
|
|
|
|
|
|
|
// 4. Total (actualPickQty + missQty + badItemQty + badPackageQty) cannot exceed lot available qty |
|
|
|
if (total > availableQty) { |
|
|
|
const errorMsg = t('Total qty (actual pick + miss + bad) cannot exceed available qty: {available}', { available: availableQty }); |
|
|
|
const errorMsg = t( |
|
|
|
"Total qty (actual pick + miss + bad) cannot exceed available qty: {available}", |
|
|
|
{ available: availableQty } |
|
|
|
); |
|
|
|
newErrors.actualPickQty = errorMsg; |
|
|
|
newErrors.missQty = errorMsg; |
|
|
|
newErrors.badItemQty = errorMsg; |
|
|
|
} |
|
|
|
|
|
|
|
// 5. If badItemQty > 0, badReason is required |
|
|
|
if (bad > 0 && !formData.badReason) { |
|
|
|
newErrors.badReason = t('Bad reason is required when bad item qty > 0'); |
|
|
|
newErrors.badItemQty = t('Bad reason is required'); |
|
|
|
} |
|
|
|
|
|
|
|
// 6. At least one field must have a value |
|
|
|
if (ap === 0 && miss === 0 && bad === 0) { |
|
|
|
newErrors.actualPickQty = t('Enter pick qty or issue qty'); |
|
|
|
|
|
|
|
// 5. At least one field must have a value |
|
|
|
if (ap === 0 && miss === 0 && totalBad === 0) { |
|
|
|
newErrors.actualPickQty = t("Enter pick qty or issue qty"); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
setErrors(newErrors); |
|
|
|
return Object.keys(newErrors).length === 0; |
|
|
|
}; |
|
|
|
|
|
|
|
const handleSubmit = async () => { |
|
|
|
if (!validateForm()) { |
|
|
|
console.error('Form validation failed:', errors); |
|
|
|
console.error("Form validation failed:", errors); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!formData.pickOrderId) { |
|
|
|
console.error('Missing pickOrderId'); |
|
|
|
console.error("Missing pickOrderId"); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const badItem = Number(formData.badItemQty) || 0; |
|
|
|
const badPackage = Number((formData as any).badPackageQty) || 0; |
|
|
|
const totalBadQty = badItem + badPackage; |
|
|
|
|
|
|
|
let badReason: string | undefined; |
|
|
|
if (totalBadQty > 0) { |
|
|
|
// assumption: only one of them is > 0 |
|
|
|
badReason = badPackage > 0 ? "package_problem" : "quantity_problem"; |
|
|
|
} |
|
|
|
|
|
|
|
const submitData: PickExecutionIssueData = { |
|
|
|
...(formData as PickExecutionIssueData), |
|
|
|
badItemQty: totalBadQty, |
|
|
|
badReason, |
|
|
|
}; |
|
|
|
|
|
|
|
setLoading(true); |
|
|
|
try { |
|
|
|
await onSubmit(formData as PickExecutionIssueData); |
|
|
|
await onSubmit(submitData); |
|
|
|
} catch (error: any) { |
|
|
|
console.error('Error submitting pick execution issue:', error); |
|
|
|
alert(t('Failed to submit issue. Please try again.') + (error.message ? `: ${error.message}` : '')); |
|
|
|
console.error("Error submitting pick execution issue:", error); |
|
|
|
alert( |
|
|
|
t("Failed to submit issue. Please try again.") + |
|
|
|
(error.message ? `: ${error.message}` : "") |
|
|
|
); |
|
|
|
} finally { |
|
|
|
setLoading(false); |
|
|
|
} |
|
|
|
@@ -239,11 +274,13 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ |
|
|
|
|
|
|
|
const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); |
|
|
|
const requiredQty = calculateRequiredQty(selectedLot); |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth> |
|
|
|
<DialogTitle> |
|
|
|
{t('Pick Execution Issue Form')} |
|
|
|
{t("Pick Execution Issue Form") + " - "+selectedPickOrderLine.itemCode+ " "+ selectedPickOrderLine.itemName} |
|
|
|
<br /> |
|
|
|
{selectedLot.lotNo} |
|
|
|
</DialogTitle> |
|
|
|
<DialogContent> |
|
|
|
<Box sx={{ mt: 2 }}> |
|
|
|
@@ -251,17 +288,17 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ |
|
|
|
<Grid item xs={6}> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t('Required Qty')} |
|
|
|
value={selectedLot?.requiredQty || 0} |
|
|
|
label={t("Required Qty")} |
|
|
|
value={requiredQty} |
|
|
|
disabled |
|
|
|
variant="outlined" |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
|
|
|
|
|
|
|
|
<Grid item xs={6}> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t('Remaining Available Qty')} |
|
|
|
label={t("Remaining Available Qty")} |
|
|
|
value={remainingAvailableQty} |
|
|
|
disabled |
|
|
|
variant="outlined" |
|
|
|
@@ -269,31 +306,44 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ |
|
|
|
</Grid> |
|
|
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t('Actual Pick Qty')} |
|
|
|
type="number" |
|
|
|
inputProps={{ inputMode: 'numeric', pattern: '[0-9]*', min: 0 }} |
|
|
|
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')}: ${remainingAvailableQty}`} |
|
|
|
variant="outlined" |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t("Actual Pick Qty")} |
|
|
|
type="number" |
|
|
|
inputProps={{ |
|
|
|
inputMode: "numeric", |
|
|
|
pattern: "[0-9]*", |
|
|
|
min: 0, |
|
|
|
}} |
|
|
|
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")}: ${remainingAvailableQty}` |
|
|
|
} |
|
|
|
variant="outlined" |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
<FormControl fullWidth> |
|
|
|
<InputLabel>{t('Reason')}</InputLabel> |
|
|
|
<InputLabel>{t("Reason")}</InputLabel> |
|
|
|
<Select |
|
|
|
value={formData.reason || ''} |
|
|
|
onChange={(e) => handleInputChange('reason', e.target.value)} |
|
|
|
label={t('Reason')} |
|
|
|
value={formData.reason || ""} |
|
|
|
onChange={(e) => handleInputChange("reason", e.target.value)} |
|
|
|
label={t("Reason")} |
|
|
|
> |
|
|
|
<MenuItem value="">{t('Select Reason')}</MenuItem> |
|
|
|
<MenuItem value="miss">{t('Edit')}</MenuItem> |
|
|
|
<MenuItem value="bad">{t('Just Complete')}</MenuItem> |
|
|
|
|
|
|
|
<MenuItem value="">{t("Select Reason")}</MenuItem> |
|
|
|
<MenuItem value="miss">{t("Edit")}</MenuItem> |
|
|
|
<MenuItem value="bad">{t("Just Complete")}</MenuItem> |
|
|
|
</Select> |
|
|
|
</FormControl> |
|
|
|
</Grid> |
|
|
|
@@ -301,11 +351,22 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ |
|
|
|
<Grid item xs={12}> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t('Missing item Qty')} |
|
|
|
label={t("Missing item Qty")} |
|
|
|
type="number" |
|
|
|
inputProps={{ inputMode: 'numeric', pattern: '[0-9]*', min: 0 }} |
|
|
|
inputProps={{ |
|
|
|
inputMode: "numeric", |
|
|
|
pattern: "[0-9]*", |
|
|
|
min: 0, |
|
|
|
}} |
|
|
|
value={formData.missQty || 0} |
|
|
|
onChange={(e) => handleInputChange('missQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))} |
|
|
|
onChange={(e) => |
|
|
|
handleInputChange( |
|
|
|
"missQty", |
|
|
|
e.target.value === "" |
|
|
|
? undefined |
|
|
|
: Math.max(0, Number(e.target.value) || 0) |
|
|
|
) |
|
|
|
} |
|
|
|
error={!!errors.missQty} |
|
|
|
variant="outlined" |
|
|
|
/> |
|
|
|
@@ -314,53 +375,61 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ |
|
|
|
<Grid item xs={12}> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t('Bad Item Qty')} |
|
|
|
label={t("Bad Item Qty")} |
|
|
|
type="number" |
|
|
|
inputProps={{ inputMode: 'numeric', pattern: '[0-9]*', min: 0 }} |
|
|
|
inputProps={{ |
|
|
|
inputMode: "numeric", |
|
|
|
pattern: "[0-9]*", |
|
|
|
min: 0, |
|
|
|
}} |
|
|
|
value={formData.badItemQty || 0} |
|
|
|
onChange={(e) => handleInputChange('badItemQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))} |
|
|
|
onChange={(e) => |
|
|
|
handleInputChange( |
|
|
|
"badItemQty", |
|
|
|
e.target.value === "" |
|
|
|
? undefined |
|
|
|
: Math.max(0, Number(e.target.value) || 0) |
|
|
|
) |
|
|
|
} |
|
|
|
error={!!errors.badItemQty} |
|
|
|
//helperText={t("Quantity Problem")} |
|
|
|
variant="outlined" |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
|
|
|
|
{/* Show bad reason dropdown when badItemQty > 0 */} |
|
|
|
{(formData.badItemQty && formData.badItemQty > 0) ? ( |
|
|
|
<Grid item xs={12}> |
|
|
|
<FormControl fullWidth error={!!errors.badReason}> |
|
|
|
<InputLabel>{t('Bad Reason')}</InputLabel> |
|
|
|
<Select |
|
|
|
value={formData.badReason || ''} |
|
|
|
onChange={(e) => handleInputChange('badReason', e.target.value)} |
|
|
|
label={t('Bad Reason')} |
|
|
|
> |
|
|
|
<MenuItem value="">{t('Select Bad Reason')}</MenuItem> |
|
|
|
<MenuItem value="quantity_problem">{t('Quantity Problem')}</MenuItem> |
|
|
|
<MenuItem value="package_problem">{t('Package Problem')}</MenuItem> |
|
|
|
</Select> |
|
|
|
{errors.badReason && ( |
|
|
|
<Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 1.75 }}> |
|
|
|
{errors.badReason} |
|
|
|
</Typography> |
|
|
|
)} |
|
|
|
</FormControl> |
|
|
|
</Grid> |
|
|
|
) : null} |
|
|
|
|
|
|
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t("Bad Package Qty")} |
|
|
|
type="number" |
|
|
|
inputProps={{ |
|
|
|
inputMode: "numeric", |
|
|
|
pattern: "[0-9]*", |
|
|
|
min: 0, |
|
|
|
}} |
|
|
|
value={(formData as any).badPackageQty || 0} |
|
|
|
onChange={(e) => |
|
|
|
handleInputChange( |
|
|
|
"badPackageQty", |
|
|
|
e.target.value === "" |
|
|
|
? undefined |
|
|
|
: Math.max(0, Number(e.target.value) || 0) |
|
|
|
) |
|
|
|
} |
|
|
|
error={!!errors.badItemQty} |
|
|
|
//helperText={t("Package Problem")} |
|
|
|
variant="outlined" |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
</Grid> |
|
|
|
</Box> |
|
|
|
</DialogContent> |
|
|
|
<DialogActions> |
|
|
|
<Button onClick={handleClose} disabled={loading}> |
|
|
|
{t('Cancel')} |
|
|
|
{t("Cancel")} |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
onClick={handleSubmit} |
|
|
|
variant="contained" |
|
|
|
disabled={loading} |
|
|
|
> |
|
|
|
{loading ? t('submitting') : t('submit')} |
|
|
|
<Button onClick={handleSubmit} variant="contained" disabled={loading}> |
|
|
|
{loading ? t("submitting") : t("submit")} |
|
|
|
</Button> |
|
|
|
</DialogActions> |
|
|
|
</Dialog> |
|
|
|
|