diff --git a/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx b/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx index 9ab4802..03af4de 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx @@ -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 = ({ const [formData, setFormData] = useState>({}); const [errors, setErrors] = useState({}); const [loading, setLoading] = useState(false); - const [handlers, setHandlers] = useState>([]); - + const [handlers, setHandlers] = useState>( + [] + ); + 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 = ({ console.error("Error fetching handlers:", error); } }; - + fetchHandlers(); }, []); @@ -136,92 +144,119 @@ const PickExecutionForm: React.FC = ({ 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 = ({ const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); const requiredQty = calculateRequiredQty(selectedLot); - + return ( - {t('Pick Execution Issue Form')} + {t("Pick Execution Issue Form") + " - "+selectedPickOrderLine.itemCode+ " "+ selectedPickOrderLine.itemName} +
+ {selectedLot.lotNo}
@@ -251,17 +288,17 @@ const PickExecutionForm: React.FC = ({ - + = ({ - 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" - /> - + + 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" + /> + - {t('Reason')} + {t("Reason")} @@ -301,11 +351,22 @@ const PickExecutionForm: React.FC = ({ 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 = ({ 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" /> - {/* Show bad reason dropdown when badItemQty > 0 */} - {(formData.badItemQty && formData.badItemQty > 0) ? ( - - - {t('Bad Reason')} - - {errors.badReason && ( - - {errors.badReason} - - )} - - - ) : null} - - + + + handleInputChange( + "badPackageQty", + e.target.value === "" + ? undefined + : Math.max(0, Number(e.target.value) || 0) + ) + } + error={!!errors.badItemQty} + //helperText={t("Package Problem")} + variant="outlined" + /> + -
diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx index 1f3a3f7..10acde5 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx @@ -937,18 +937,15 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); setIsConfirmingLot(true); try { const newLotNo = scannedLotData?.lotNo; - if (!newLotNo) { - console.error("No lot number for scanned lot"); - alert(t("Cannot find lot number for scanned lot. Please verify the lot number is correct.")); - setIsConfirmingLot(false); - return; - } + const newStockInLineId = scannedLotData?.stockInLineId; + await confirmLotSubstitution({ pickOrderLineId: selectedLotForQr.pickOrderLineId, stockOutLineId: selectedLotForQr.stockOutLineId, originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId, - newInventoryLotNo: newLotNo + newInventoryLotNo: "", + newStockInLineId: newStockInLineId }); setQrScanError(false); diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index d63e378..05efb57 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -257,9 +257,11 @@ "Pick Execution Issue Form":"提料問題表單", "This form is for reporting issues only. You must report either missing items or bad items.":"此表單僅用於報告問題。您必須報告缺少的貨品或不良貨品。", "Bad item Qty":"不良貨品數量", - "Missing item Qty":"缺少貨品數量", + "Missing item Qty":"貨品遺失數量", + "Missing Item Qty":"貨品遺失數量", "Bad Item Qty":"不良貨品數量", - "Missing Item Qty":"缺少貨品數量", + "Bad Package Qty":"不良包裝數量", + "Actual Pick Qty":"實際提料數量", "Required Qty":"所需數量", "Issue Remark":"問題描述",