diff --git a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx index aef2ca4..b4df7bc 100644 --- a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx +++ b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx @@ -28,7 +28,7 @@ import { fetchPickOrderClient, autoAssignAndReleasePickOrder } from "@/app/api/p import Jobcreatitem from "./Jobcreatitem"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; - +import PickExecutionDetail from "./GoodPickExecutiondetail"; interface Props { pickOrders: PickOrderResult[]; } @@ -276,27 +276,22 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { p: 2, borderBottom: '1px solid #e0e0e0' }}> - - + + {t("Finished Good Order")} - {/* - - - {isOpenCreateModal && - - } - */} @@ -306,26 +301,10 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { borderBottom: '1px solid #e0e0e0' }}> - + + - {isAssigning && ( - - - {t("Assigning pick order...")} - - - )} + {/* Content section - NO overflow: 'auto' here */} @@ -333,6 +312,7 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { p: 2 }}> {tabIndex === 0 && } + {tabIndex === 1 && } ); diff --git a/src/components/FinishedGoodSearch/GoodPickExecution.tsx b/src/components/FinishedGoodSearch/GoodPickExecution.tsx index 0377ee6..3db47c5 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecution.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecution.tsx @@ -934,17 +934,10 @@ const PickExecution: React.FC = ({ filterArgs }) => { return ( - + {/* Search Box */} - {/* - - - {t("FG Pick Orders")} - - - */} {fgPickOrdersLoading ? ( @@ -972,9 +965,8 @@ const PickExecution: React.FC = ({ filterArgs }) => { - + {/* - {/* Combined Lot Table */} @@ -992,12 +984,11 @@ const PickExecution: React.FC = ({ filterArgs }) => { {t("Route")} {t("Item Name")} {t("Lot#")} - {t("Target Date")} - {/* {t("Lot Location")} */} + {t("Lot Required Pick Qty")} - {t("Original Available Qty")} + {t("Lot Actual Pick Qty")} - {/* {t("Remaining Available Qty")} */} + {t("Action")} @@ -1045,9 +1036,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { - {lot.pickOrderTargetDate} - {/* {lot.location} */} - {calculateRemainingRequiredQty(lot).toLocaleString()} + {(() => { const inQty = lot.inQty || 0; @@ -1057,7 +1046,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { })()} - {/* ✅ QR Scan Button if not scanned, otherwise show TextField + Issue button */} + {!lot.stockOutLineId ? ( - + + )) )} + - +*/} + {/* = ({ filterArgs }) => { - {/* ✅ QR Code Modal */} + { @@ -1206,7 +1192,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { onQrCodeSubmit={handleQrCodeSubmitFromModal} /> - {/* ✅ Good Pick Execution Form Modal */} + {pickExecutionFormOpen && selectedLotForExecutionForm && ( = ({ filterArgs }) => { pickOrderCreateDate={new Date()} /> )} + */} ); }; diff --git a/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx b/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx index 9af0197..d2eff98 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx @@ -250,34 +250,34 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)} error={!!errors.actualPickQty} - helperText={errors.actualPickQty || t('Enter the quantity actually picked')} + // helperText={errors.actualPickQty || t('Enter the quantity actually picked')} variant="outlined" /> @@ -285,12 +285,12 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { handleInputChange('missQty', parseFloat(e.target.value) || 0)} error={!!errors.missQty} - helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')} + // helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')} variant="outlined" /> @@ -298,31 +298,31 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { handleInputChange('badItemQty', parseFloat(e.target.value) || 0)} error={!!errors.badItemQty} - helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')} + // helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')} variant="outlined" /> {/* ✅ Show issue description and handler fields when bad items > 0 */} - {(formData.badItemQty && formData.badItemQty > 0) && ( + {(formData.badItemQty && formData.badItemQty > 0) ? ( <> handleInputChange('issueRemark', e.target.value)} error={!!errors.issueRemark} helperText={errors.issueRemark} - placeholder={t('Describe the issue with bad items')} + //placeholder={t('Describe the issue with bad items')} variant="outlined" /> @@ -349,7 +349,7 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { - )} + ) : (<>)} diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx new file mode 100644 index 0000000..4f41f70 --- /dev/null +++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx @@ -0,0 +1,1236 @@ +"use client"; + +import { + Box, + Button, + Stack, + TextField, + Typography, + Alert, + CircularProgress, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + TablePagination, + Modal, +} from "@mui/material"; +import { useCallback, useEffect, useState, useRef, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useRouter } from "next/navigation"; +import { + fetchALLPickOrderLineLotDetails, + updateStockOutLineStatus, + createStockOutLine, + recordPickExecutionIssue, + fetchFGPickOrders, // ✅ Add this import + FGPickOrderResponse, + autoAssignAndReleasePickOrder, + AutoAssignReleaseResponse, + checkPickOrderCompletion, + PickOrderCompletionResponse, + checkAndCompletePickOrderByConsoCode +} from "@/app/api/pickOrder/actions"; +import { fetchNameList, NameList } from "@/app/api/user/actions"; +import { + FormProvider, + useForm, +} from "react-hook-form"; +import SearchBox, { Criterion } from "../SearchBox"; +import { CreateStockOutLine } from "@/app/api/pickOrder/actions"; +import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions"; +import QrCodeIcon from '@mui/icons-material/QrCode'; +import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; +import { useSession } from "next-auth/react"; +import { SessionWithTokens } from "@/config/authConfig"; +import { fetchStockInLineInfo } from "@/app/api/po/actions"; +import GoodPickExecutionForm from "./GoodPickExecutionForm"; +import FGPickOrderCard from "./FGPickOrderCard"; +interface Props { + filterArgs: Record; +} + +// ✅ QR Code Modal Component (from LotTable) +const QrCodeModal: React.FC<{ + open: boolean; + onClose: () => void; + lot: any | null; + onQrCodeSubmit: (lotNo: string) => void; + combinedLotData: any[]; // ✅ Add this prop +}> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { + const { t } = useTranslation("pickOrder"); + const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); + const [manualInput, setManualInput] = useState(''); + + const [manualInputSubmitted, setManualInputSubmitted] = useState(false); + const [manualInputError, setManualInputError] = useState(false); + const [isProcessingQr, setIsProcessingQr] = useState(false); + const [qrScanFailed, setQrScanFailed] = useState(false); + const [qrScanSuccess, setQrScanSuccess] = useState(false); + + const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); + const [scannedQrResult, setScannedQrResult] = useState(''); + const [fgPickOrder, setFgPickOrder] = useState(null); + // Process scanned QR codes + useEffect(() => { + if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) { + const latestQr = qrValues[qrValues.length - 1]; + + if (processedQrCodes.has(latestQr)) { + console.log("QR code already processed, skipping..."); + return; + } + + setProcessedQrCodes(prev => new Set(prev).add(latestQr)); + + try { + const qrData = JSON.parse(latestQr); + + if (qrData.stockInLineId && qrData.itemId) { + setIsProcessingQr(true); + setQrScanFailed(false); + + fetchStockInLineInfo(qrData.stockInLineId) + .then((stockInLineInfo) => { + console.log("Stock in line info:", stockInLineInfo); + setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number'); + + if (stockInLineInfo.lotNo === lot.lotNo) { + console.log(`✅ QR Code verified for lot: ${lot.lotNo}`); + setQrScanSuccess(true); + onQrCodeSubmit(lot.lotNo); + onClose(); + resetScan(); + } else { + console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`); + setQrScanFailed(true); + setManualInputError(true); + setManualInputSubmitted(true); + } + }) + .catch((error) => { + console.error("Error fetching stock in line info:", error); + setScannedQrResult('Error fetching data'); + setQrScanFailed(true); + setManualInputError(true); + setManualInputSubmitted(true); + }) + .finally(() => { + setIsProcessingQr(false); + }); + } else { + const qrContent = latestQr.replace(/[{}]/g, ''); + setScannedQrResult(qrContent); + + if (qrContent === lot.lotNo) { + setQrScanSuccess(true); + onQrCodeSubmit(lot.lotNo); + onClose(); + resetScan(); + } else { + setQrScanFailed(true); + setManualInputError(true); + setManualInputSubmitted(true); + } + } + } catch (error) { + console.log("QR code is not JSON format, trying direct comparison"); + const qrContent = latestQr.replace(/[{}]/g, ''); + setScannedQrResult(qrContent); + + if (qrContent === lot.lotNo) { + setQrScanSuccess(true); + onQrCodeSubmit(lot.lotNo); + onClose(); + resetScan(); + } else { + setQrScanFailed(true); + setManualInputError(true); + setManualInputSubmitted(true); + } + } + } + }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]); + + // Clear states when modal opens + useEffect(() => { + if (open) { + setManualInput(''); + setManualInputSubmitted(false); + setManualInputError(false); + setIsProcessingQr(false); + setQrScanFailed(false); + setQrScanSuccess(false); + setScannedQrResult(''); + setProcessedQrCodes(new Set()); + } + }, [open]); + + useEffect(() => { + if (lot) { + setManualInput(''); + setManualInputSubmitted(false); + setManualInputError(false); + setIsProcessingQr(false); + setQrScanFailed(false); + setQrScanSuccess(false); + setScannedQrResult(''); + setProcessedQrCodes(new Set()); + } + }, [lot]); + + // Auto-submit manual input when it matches + useEffect(() => { + if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) { + console.log(' Auto-submitting manual input:', manualInput.trim()); + + const timer = setTimeout(() => { + setQrScanSuccess(true); + onQrCodeSubmit(lot.lotNo); + onClose(); + setManualInput(''); + setManualInputError(false); + setManualInputSubmitted(false); + }, 200); + + return () => clearTimeout(timer); + } + }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]); + + const handleManualSubmit = () => { + if (manualInput.trim() === lot?.lotNo) { + setQrScanSuccess(true); + onQrCodeSubmit(lot.lotNo); + onClose(); + setManualInput(''); + } else { + setQrScanFailed(true); + setManualInputError(true); + setManualInputSubmitted(true); + } + }; + + useEffect(() => { + if (open) { + startScan(); + } + }, [open, startScan]); + + return ( + + + + {t("QR Code Scan for Lot")}: {lot?.lotNo} + + + {isProcessingQr && ( + + + {t("Processing QR code...")} + + + )} + + + + {t("Manual Input")}: + + { + setManualInput(e.target.value); + if (qrScanFailed || manualInputError) { + setQrScanFailed(false); + setManualInputError(false); + setManualInputSubmitted(false); + } + }} + sx={{ mb: 1 }} + error={manualInputSubmitted && manualInputError} + helperText={ + manualInputSubmitted && manualInputError + ? `${t("The input is not the same as the expected lot number.")}` + : '' + } + /> + + + + {qrValues.length > 0 && ( + + + {t("QR Scan Result:")} {scannedQrResult} + + + {qrScanSuccess && ( + + ✅ {t("Verified successfully!")} + + )} + + )} + + + + + + + ); +}; + +const PickExecution: React.FC = ({ filterArgs }) => { + const { t } = useTranslation("pickOrder"); + const router = useRouter(); + const { data: session } = useSession() as { data: SessionWithTokens | null }; + + const currentUserId = session?.id ? parseInt(session.id) : undefined; + + const [combinedLotData, setCombinedLotData] = useState([]); + const [combinedDataLoading, setCombinedDataLoading] = useState(false); + const [originalCombinedData, setOriginalCombinedData] = useState([]); + + const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); + + const [qrScanInput, setQrScanInput] = useState(''); + const [qrScanError, setQrScanError] = useState(false); + const [qrScanSuccess, setQrScanSuccess] = useState(false); + + const [pickQtyData, setPickQtyData] = useState>({}); + const [searchQuery, setSearchQuery] = useState>({}); + + const [paginationController, setPaginationController] = useState({ + pageNum: 0, + pageSize: 10, + }); + + const [usernameList, setUsernameList] = useState([]); + + const initializationRef = useRef(false); + const autoAssignRef = useRef(false); + + const formProps = useForm(); + const errors = formProps.formState.errors; + + // ✅ Add QR modal states + const [qrModalOpen, setQrModalOpen] = useState(false); + const [selectedLotForQr, setSelectedLotForQr] = useState(null); + + // ✅ Add GoodPickExecutionForm states + const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); + const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState(null); + const [fgPickOrders, setFgPickOrders] = useState([]); + const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false); + const fetchFgPickOrdersData = useCallback(async () => { + if (!currentUserId) return; + + setFgPickOrdersLoading(true); + try { + // Get all pick order IDs from combinedLotData + const pickOrderIds = Array.from(new Set(combinedLotData.map(lot => lot.pickOrderId))); + + if (pickOrderIds.length === 0) { + setFgPickOrders([]); + return; + } + + // Fetch FG pick orders for each pick order ID + const fgPickOrdersPromises = pickOrderIds.map(pickOrderId => + fetchFGPickOrders(pickOrderId) + ); + + const fgPickOrdersResults = await Promise.all(fgPickOrdersPromises); + + // Flatten the results (each fetchFGPickOrders returns an array) + const allFgPickOrders = fgPickOrdersResults.flat(); + + setFgPickOrders(allFgPickOrders); + console.log("✅ Fetched FG pick orders:", allFgPickOrders); + } catch (error) { + console.error("❌ Error fetching FG pick orders:", error); + setFgPickOrders([]); + } finally { + setFgPickOrdersLoading(false); + } + }, [currentUserId, combinedLotData]); + useEffect(() => { + if (combinedLotData.length > 0) { + fetchFgPickOrdersData(); + } + }, [combinedLotData, fetchFgPickOrdersData]); + + // ✅ Handle QR code button click + const handleQrCodeClick = (pickOrderId: number) => { + console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); + // TODO: Implement QR code functionality + }; + + useEffect(() => { + startScan(); + return () => { + stopScan(); + resetScan(); + }; + }, [startScan, stopScan, resetScan]); + + const fetchAllCombinedLotData = useCallback(async (userId?: number) => { + setCombinedDataLoading(true); + try { + const userIdToUse = userId || currentUserId; + + console.log(" fetchAllCombinedLotData called with userId:", userIdToUse); + + if (!userIdToUse) { + console.warn("⚠️ No userId available, skipping API call"); + setCombinedLotData([]); + setOriginalCombinedData([]); + return; + } + + // ✅ Use the non-auto-assign endpoint - this only fetches existing data + const allLotDetails = await fetchALLPickOrderLineLotDetails(userIdToUse); + console.log("✅ All combined lot details:", allLotDetails); + setCombinedLotData(allLotDetails); + setOriginalCombinedData(allLotDetails); + } catch (error) { + console.error("❌ Error fetching combined lot data:", error); + setCombinedLotData([]); + setOriginalCombinedData([]); + } finally { + setCombinedDataLoading(false); + } + }, [currentUserId]); + + // ✅ Only fetch existing data when session is ready, no auto-assignment + useEffect(() => { + if (session && currentUserId && !initializationRef.current) { + console.log("✅ Session loaded, initializing pick order..."); + initializationRef.current = true; + + // ✅ Only fetch existing data, no auto-assignment + fetchAllCombinedLotData(); + } + }, [session, currentUserId, fetchAllCombinedLotData]); + + // ✅ Add event listener for manual assignment + useEffect(() => { + const handlePickOrderAssigned = () => { + console.log("🔄 Pick order assigned event received, refreshing data..."); + fetchAllCombinedLotData(); + }; + + window.addEventListener('pickOrderAssigned', handlePickOrderAssigned); + + return () => { + window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned); + }; + }, [fetchAllCombinedLotData]); + + // ✅ Handle QR code submission for matched lot (external scanning) + // ✅ Handle QR code submission for matched lot (external scanning) + const handleQrCodeSubmit = useCallback(async (lotNo: string) => { + console.log(`✅ Processing QR Code for lot: ${lotNo}`); + + // ✅ Use current data without refreshing to avoid infinite loop + const currentLotData = combinedLotData; + console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo)); + + const matchingLots = currentLotData.filter(lot => + lot.lotNo === lotNo || + lot.lotNo?.toLowerCase() === lotNo.toLowerCase() + ); + + if (matchingLots.length === 0) { + console.error(`❌ Lot not found: ${lotNo}`); + setQrScanError(true); + setQrScanSuccess(false); + return; + } + + console.log(`✅ Found ${matchingLots.length} matching lots:`, matchingLots); + setQrScanError(false); + + try { + let successCount = 0; + let existsCount = 0; + let errorCount = 0; + + for (const matchingLot of matchingLots) { + console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`); + + if (matchingLot.stockOutLineId) { + console.log(`✅ Stock out line already exists for line ${matchingLot.pickOrderLineId}`); + existsCount++; + } else { + const stockOutLineData: CreateStockOutLine = { + consoCode: matchingLot.pickOrderConsoCode, + pickOrderLineId: matchingLot.pickOrderLineId, + inventoryLotLineId: matchingLot.lotId, + qty: 0.0 + }; + + console.log(`Creating stock out line for pick order line ${matchingLot.pickOrderLineId}:`, stockOutLineData); + const result = await createStockOutLine(stockOutLineData); + console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, result); + + if (result && result.code === "EXISTS") { + console.log(`✅ Stock out line already exists for line ${matchingLot.pickOrderLineId}`); + existsCount++; + } else if (result && result.code === "SUCCESS") { + console.log(`✅ Stock out line created successfully for line ${matchingLot.pickOrderLineId}`); + successCount++; + } else { + console.error(`❌ Failed to create stock out line for line ${matchingLot.pickOrderLineId}:`, result); + errorCount++; + } + } + } + + // ✅ Always refresh data after processing (success or failure) + console.log("🔄 Refreshing data after QR code processing..."); + await fetchAllCombinedLotData(); + + if (successCount > 0 || existsCount > 0) { + console.log(`✅ QR Code processing completed: ${successCount} created, ${existsCount} already existed`); + setQrScanSuccess(true); + setQrScanInput(''); // Clear input after successful processing + + // ✅ Clear success state after a delay + setTimeout(() => { + setQrScanSuccess(false); + }, 2000); + } else { + console.error(`❌ QR Code processing failed: ${errorCount} errors`); + setQrScanError(true); + setQrScanSuccess(false); + + // ✅ Clear error state after a delay + setTimeout(() => { + setQrScanError(false); + }, 3000); + } + } catch (error) { + console.error("❌ Error processing QR code:", error); + setQrScanError(true); + setQrScanSuccess(false); + + // ✅ Still refresh data even on error + await fetchAllCombinedLotData(); + + // ✅ Clear error state after a delay + setTimeout(() => { + setQrScanError(false); + }, 3000); + } + }, [combinedLotData, fetchAllCombinedLotData]); + + const handleManualInputSubmit = useCallback(() => { + if (qrScanInput.trim() !== '') { + handleQrCodeSubmit(qrScanInput.trim()); + } + }, [qrScanInput, handleQrCodeSubmit]); + + // ✅ Handle QR code submission from modal (internal scanning) + const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => { + if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { + console.log(`✅ QR Code verified for lot: ${lotNo}`); + + const requiredQty = selectedLotForQr.requiredQty; + const lotId = selectedLotForQr.lotId; + + // Create stock out line + const stockOutLineData: CreateStockOutLine = { + consoCode: selectedLotForQr.pickOrderConsoCode, // ✅ Use pickOrderConsoCode instead of pickOrderCode + pickOrderLineId: selectedLotForQr.pickOrderLineId, + inventoryLotLineId: selectedLotForQr.lotId, + qty: 0.0 + }; + + try { + await createStockOutLine(stockOutLineData); + console.log("Stock out line created successfully!"); + + // Close modal + setQrModalOpen(false); + setSelectedLotForQr(null); + + // Set pick quantity + const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`; + setTimeout(() => { + setPickQtyData(prev => ({ + ...prev, + [lotKey]: requiredQty + })); + console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); + }, 500); + + // Refresh data + await fetchAllCombinedLotData(); + } catch (error) { + console.error("Error creating stock out line:", error); + } + } + }, [selectedLotForQr, fetchAllCombinedLotData]); + + // ✅ Outside QR scanning - process QR codes from outside the page automatically + useEffect(() => { + if (qrValues.length > 0 && combinedLotData.length > 0) { + const latestQr = qrValues[qrValues.length - 1]; + + // Extract lot number from QR code + let lotNo = ''; + try { + const qrData = JSON.parse(latestQr); + if (qrData.stockInLineId && qrData.itemId) { + // For JSON QR codes, we need to fetch the lot number + fetchStockInLineInfo(qrData.stockInLineId) + .then((stockInLineInfo) => { + console.log("Outside QR scan - Stock in line info:", stockInLineInfo); + const extractedLotNo = stockInLineInfo.lotNo; + if (extractedLotNo) { + console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`); + handleQrCodeSubmit(extractedLotNo); + } + }) + .catch((error) => { + console.error("Outside QR scan - Error fetching stock in line info:", error); + }); + return; // Exit early for JSON QR codes + } + } catch (error) { + // Not JSON format, treat as direct lot number + lotNo = latestQr.replace(/[{}]/g, ''); + } + + // For direct lot number QR codes + if (lotNo) { + console.log(`Outside QR scan detected (direct): ${lotNo}`); + handleQrCodeSubmit(lotNo); + } + } + }, [qrValues, combinedLotData, handleQrCodeSubmit]); + + + const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { + if (value === '' || value === null || value === undefined) { + setPickQtyData(prev => ({ + ...prev, + [lotKey]: 0 + })); + return; + } + + const numericValue = typeof value === 'string' ? parseFloat(value) : value; + + if (isNaN(numericValue)) { + setPickQtyData(prev => ({ + ...prev, + [lotKey]: 0 + })); + return; + } + + setPickQtyData(prev => ({ + ...prev, + [lotKey]: numericValue + })); + }, []); + + const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle'); + const [autoAssignMessage, setAutoAssignMessage] = useState(''); + const [completionStatus, setCompletionStatus] = useState(null); + + const checkAndAutoAssignNext = useCallback(async () => { + if (!currentUserId) return; + + try { + const completionResponse = await checkPickOrderCompletion(currentUserId); + + if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) { + console.log("Found completed pick orders, auto-assigning next..."); + // ✅ 移除前端的自动分配逻辑,因为后端已经处理了 + // await handleAutoAssignAndRelease(); // 删除这个函数 + } + } catch (error) { + console.error("Error checking pick order completion:", error); + } + }, [currentUserId]); + + // ✅ Handle submit pick quantity + const handleSubmitPickQty = useCallback(async (lot: any) => { + const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; + const newQty = pickQtyData[lotKey] || 0; + + if (!lot.stockOutLineId) { + console.error("No stock out line found for this lot"); + return; + } + + try { + const currentActualPickQty = lot.actualPickQty || 0; + const cumulativeQty = currentActualPickQty + newQty; + + let newStatus = 'partially_completed'; + + if (cumulativeQty >= lot.requiredQty) { + newStatus = 'completed'; + } + + console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`); + console.log(`Lot: ${lot.lotNo}`); + console.log(`Required Qty: ${lot.requiredQty}`); + console.log(`Current Actual Pick Qty: ${currentActualPickQty}`); + console.log(`New Submitted Qty: ${newQty}`); + console.log(`Cumulative Qty: ${cumulativeQty}`); + console.log(`New Status: ${newStatus}`); + console.log(`=====================================`); + + await updateStockOutLineStatus({ + id: lot.stockOutLineId, + status: newStatus, + qty: cumulativeQty + }); + + if (newQty > 0) { + await updateInventoryLotLineQuantities({ + inventoryLotLineId: lot.lotId, + qty: newQty, + status: 'available', + operation: 'pick' + }); + } + + // ✅ FIXED: Use the proper API function instead of direct fetch + if (newStatus === 'completed' && lot.pickOrderConsoCode) { + console.log(`✅ Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`); + + try { + // ✅ Use the imported API function instead of direct fetch + const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode); + console.log(`✅ Pick order completion check result:`, completionResponse); + + if (completionResponse.code === "SUCCESS") { + console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`); + } else if (completionResponse.message === "not completed") { + console.log(`⏳ Pick order not completed yet, more lines remaining`); + } else { + console.error(`❌ Error checking completion: ${completionResponse.message}`); + } + } catch (error) { + console.error("Error checking pick order completion:", error); + } + } + + await fetchAllCombinedLotData(); + console.log("Pick quantity submitted successfully!"); + + setTimeout(() => { + checkAndAutoAssignNext(); + }, 1000); + + } catch (error) { + console.error("Error submitting pick quantity:", error); + } + }, [pickQtyData, fetchAllCombinedLotData, checkAndAutoAssignNext]); + + // ✅ Handle reject lot + const handleRejectLot = useCallback(async (lot: any) => { + if (!lot.stockOutLineId) { + console.error("No stock out line found for this lot"); + return; + } + + try { + await updateStockOutLineStatus({ + id: lot.stockOutLineId, + status: 'rejected', + qty: 0 + }); + + await fetchAllCombinedLotData(); + console.log("Lot rejected successfully!"); + + setTimeout(() => { + checkAndAutoAssignNext(); + }, 1000); + + } catch (error) { + console.error("Error rejecting lot:", error); + } + }, [fetchAllCombinedLotData, checkAndAutoAssignNext]); + + // ✅ Handle pick execution form + const handlePickExecutionForm = useCallback((lot: any) => { + console.log("=== Pick Execution Form ==="); + console.log("Lot data:", lot); + + if (!lot) { + console.warn("No lot data provided for pick execution form"); + return; + } + + console.log("Opening pick execution form for lot:", lot.lotNo); + + setSelectedLotForExecutionForm(lot); + setPickExecutionFormOpen(true); + + console.log("Pick execution form opened for lot ID:", lot.lotId); + }, []); + + const handlePickExecutionFormSubmit = useCallback(async (data: any) => { + try { + console.log("Pick execution form submitted:", data); + + const result = await recordPickExecutionIssue(data); + console.log("Pick execution issue recorded:", result); + + if (result && result.code === "SUCCESS") { + console.log("✅ Pick execution issue recorded successfully"); + } else { + console.error("❌ Failed to record pick execution issue:", result); + } + + setPickExecutionFormOpen(false); + setSelectedLotForExecutionForm(null); + + await fetchAllCombinedLotData(); + } catch (error) { + console.error("Error submitting pick execution form:", error); + } + }, [fetchAllCombinedLotData]); + + // ✅ Calculate remaining required quantity + const calculateRemainingRequiredQty = useCallback((lot: any) => { + const requiredQty = lot.requiredQty || 0; + const stockOutLineQty = lot.stockOutLineQty || 0; + return Math.max(0, requiredQty - stockOutLineQty); + }, []); + + // Search criteria + const searchCriteria: Criterion[] = [ + { + label: t("Pick Order Code"), + paramName: "pickOrderCode", + type: "text", + }, + { + label: t("Item Code"), + paramName: "itemCode", + type: "text", + }, + { + label: t("Item Name"), + paramName: "itemName", + type: "text", + }, + { + label: t("Lot No"), + paramName: "lotNo", + type: "text", + }, + ]; + + const handleSearch = useCallback((query: Record) => { + setSearchQuery({ ...query }); + console.log("Search query:", query); + + if (!originalCombinedData) return; + + const filtered = originalCombinedData.filter((lot: any) => { + const pickOrderCodeMatch = !query.pickOrderCode || + lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); + + const itemCodeMatch = !query.itemCode || + lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); + + const itemNameMatch = !query.itemName || + lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase()); + + const lotNoMatch = !query.lotNo || + lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase()); + + return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch; + }); + + setCombinedLotData(filtered); + console.log("Filtered lots count:", filtered.length); + }, [originalCombinedData]); + + const handleReset = useCallback(() => { + setSearchQuery({}); + if (originalCombinedData) { + setCombinedLotData(originalCombinedData); + } + }, [originalCombinedData]); + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setPaginationController(prev => ({ + ...prev, + pageNum: newPage, + })); + }, []); + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + setPaginationController({ + pageNum: 0, + pageSize: newPageSize, + }); + }, []); + + // Pagination data with sorting by routerIndex + const paginatedData = useMemo(() => { + // ✅ Sort by routerIndex first, then by other criteria + const sortedData = [...combinedLotData].sort((a, b) => { + const aIndex = a.routerIndex || 0; + const bIndex = b.routerIndex || 0; + + // Primary sort: by routerIndex + if (aIndex !== bIndex) { + return aIndex - bIndex; + } + + // Secondary sort: by pickOrderCode if routerIndex is the same + if (a.pickOrderCode !== b.pickOrderCode) { + return a.pickOrderCode.localeCompare(b.pickOrderCode); + } + + // Tertiary sort: by lotNo if everything else is the same + return (a.lotNo || '').localeCompare(b.lotNo || ''); + }); + + const startIndex = paginationController.pageNum * paginationController.pageSize; + const endIndex = startIndex + paginationController.pageSize; + return sortedData.slice(startIndex, endIndex); + }, [combinedLotData, paginationController]); + + return ( + + + {/* Search Box */} + {/* + + + {fgPickOrdersLoading ? ( + + + + ) : ( + + {fgPickOrders.length === 0 ? ( + + + {t("No FG pick orders found")} + + + ) : ( + fgPickOrders.map((fgOrder) => ( + + )) + )} + + )} + +*/} + + + + + {/* Combined Lot Table */} + + + + {t("All Pick Order Lots")} + + + + + + + + + + {t("Index")} + {t("Route")} + {t("Item Name")} + {t("Lot#")} + {/* {t("Target Date")} */} + {/* {t("Lot Location")} */} + {t("Lot Required Pick Qty")} + {/* {t("Original Available Qty")} */} + {t("Lot Actual Pick Qty")} + {/* {t("Remaining Available Qty")} */} + {t("Action")} + + + + {paginatedData.length === 0 ? ( + + + + {t("No data available")} + + + + ) : ( + paginatedData.map((lot, index) => ( + + + + {lot.routerIndex || index + 1} + + + + + {lot.routerRoute || '-'} + + + {lot.itemName} + + + + {lot.lotNo} + + + + {/* {lot.pickOrderTargetDate} */} + {/* {lot.location} */} + {/* {calculateRemainingRequiredQty(lot).toLocaleString()} */} + + {(() => { + const inQty = lot.inQty || 0; + const outQty = lot.outQty || 0; + const result = inQty - outQty; + return result.toLocaleString(); + })()} + + + {/* ✅ QR Scan Button if not scanned, otherwise show TextField + Issue button */} + {!lot.stockOutLineId ? ( + + ) : ( + // ✅ When stockOutLineId exists, show TextField + Issue button + + { + const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; + handlePickQtyChange(lotKey, parseFloat(e.target.value) || 0); + }} + disabled={ + (lot.lotAvailability === 'expired' || + lot.lotAvailability === 'status_unavailable' || + lot.lotAvailability === 'rejected') || + lot.stockOutLineStatus === 'completed' + } + inputProps={{ + min: 0, + max: calculateRemainingRequiredQty(lot), + step: 0.01 + }} + sx={{ + width: '60px', + height: '28px', + '& .MuiInputBase-input': { + fontSize: '0.7rem', + textAlign: 'center', + padding: '6px 8px' + } + }} + placeholder="0" + /> + + + + )} + + {/* + {(() => { + const inQty = lot.inQty || 0; + const outQty = lot.outQty || 0; + const result = inQty - outQty; + return result.toLocaleString(); + })()} + */} + + + + + + + )) + )} + +
+
+ + + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } + /> +
+
+ + {/* ✅ QR Code Modal */} + { + setQrModalOpen(false); + setSelectedLotForQr(null); + stopScan(); + resetScan(); + }} + lot={selectedLotForQr} + combinedLotData={combinedLotData} // ✅ Add this prop + onQrCodeSubmit={handleQrCodeSubmitFromModal} + /> + + {/* ✅ Good Pick Execution Form Modal */} + {pickExecutionFormOpen && selectedLotForExecutionForm && ( + { + setPickExecutionFormOpen(false); + setSelectedLotForExecutionForm(null); + }} + onSubmit={handlePickExecutionFormSubmit} + selectedLot={selectedLotForExecutionForm} + selectedPickOrderLine={{ + id: selectedLotForExecutionForm.pickOrderLineId, + itemId: selectedLotForExecutionForm.itemId, + itemCode: selectedLotForExecutionForm.itemCode, + itemName: selectedLotForExecutionForm.itemName, + pickOrderCode: selectedLotForExecutionForm.pickOrderCode, + // ✅ Add missing required properties from GetPickOrderLineInfo interface + availableQty: selectedLotForExecutionForm.availableQty || 0, + requiredQty: selectedLotForExecutionForm.requiredQty || 0, + uomCode: selectedLotForExecutionForm.uomCode || '', + uomDesc: selectedLotForExecutionForm.uomDesc || '', + pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // ✅ Use pickedQty instead of actualPickQty + suggestedList: [] // ✅ Add required suggestedList property + }} + pickOrderId={selectedLotForExecutionForm.pickOrderId} + pickOrderCreateDate={new Date()} + /> + )} +
+ ); +}; + +export default PickExecution; \ No newline at end of file diff --git a/src/components/PickOrderSearch/LotTable.tsx b/src/components/PickOrderSearch/LotTable.tsx index 60bd9c3..a047c8d 100644 --- a/src/components/PickOrderSearch/LotTable.tsx +++ b/src/components/PickOrderSearch/LotTable.tsx @@ -718,56 +718,56 @@ const LotTable: React.FC = ({ {/* ✅ 恢复 TextField 用于正常数量输入 */} { - if (selectedRowId) { - const inputValue = parseFloat(e.target.value) || 0; - const maxAllowed = Math.min(calculateRemainingAvailableQty(lot), calculateRemainingRequiredQty(lot)); - {/* - // ✅ Validate input - if (inputValue > maxAllowed) { - // Set validation error for this lot - setValidationErrors(prev => ({ ...prev, [`lot_${lot.lotId}`]: `${t('Input quantity cannot exceed')} ${maxAllowed}` })); - return; - } else { - // Clear validation error if valid - setValidationErrors(prev => { - const newErrors = { ...prev }; - delete newErrors[`lot_${lot.lotId}`]; - return newErrors; - }); - */} - - - onPickQtyChange(selectedRowId, lot.lotId, inputValue); - } - }} - disabled={ - (lot.lotAvailability === 'expired' || - lot.lotAvailability === 'status_unavailable' || - lot.lotAvailability === 'rejected') || - selectedLotRowId !== `row_${index}` || - lot.stockOutLineStatus === 'completed' - } - error={!!validationErrors[`lot_${lot.lotId}`]} // ✅ Show red border when error - helperText={validationErrors[`lot_${lot.lotId}`]} // ✅ Show red error text below - inputProps={{ - min: 0, - max: calculateRemainingRequiredQty(lot), - step: 0.01 - }} - sx={{ - width: '60px', - height: '28px', - '& .MuiInputBase-input': { - fontSize: '0.7rem', - textAlign: 'center', - padding: '6px 8px' - } - }} - placeholder="0" + type="number" + size="small" + value={pickQtyData[selectedRowId!]?.[lot.lotId] || ''} + onChange={(e) => { + if (selectedRowId) { + const inputValue = parseFloat(e.target.value) || 0; + const maxAllowed = Math.min(calculateRemainingAvailableQty(lot), calculateRemainingRequiredQty(lot)); + {/* + // ✅ Validate input + if (inputValue > maxAllowed) { + // Set validation error for this lot + setValidationErrors(prev => ({ ...prev, [`lot_${lot.lotId}`]: `${t('Input quantity cannot exceed')} ${maxAllowed}` })); + return; + } else { + // Clear validation error if valid + setValidationErrors(prev => { + const newErrors = { ...prev }; + delete newErrors[`lot_${lot.lotId}`]; + return newErrors; + }); + */} + + + onPickQtyChange(selectedRowId, lot.lotId, inputValue); + } + }} + disabled={ + (lot.lotAvailability === 'expired' || + lot.lotAvailability === 'status_unavailable' || + lot.lotAvailability === 'rejected') || + selectedLotRowId !== `row_${index}` || + lot.stockOutLineStatus === 'completed' + } + error={!!validationErrors[`lot_${lot.lotId}`]} // ✅ Show red border when error + helperText={validationErrors[`lot_${lot.lotId}`]} // ✅ Show red error text below + inputProps={{ + min: 0, + max: calculateRemainingRequiredQty(lot), + step: 0.01 + }} + sx={{ + width: '60px', + height: '28px', + '& .MuiInputBase-input': { + fontSize: '0.7rem', + textAlign: 'center', + padding: '6px 8px' + } + }} + placeholder="0" /> {/* ✅ 添加 Pick Form 按钮用于问题情况 */} @@ -775,6 +775,12 @@ const LotTable: React.FC = ({ variant="outlined" size="small" onClick={() => handlePickExecutionForm(lot)} + disabled={ + (lot.lotAvailability === 'expired' || + lot.lotAvailability === 'status_unavailable' || + lot.lotAvailability === 'rejected') || + selectedLotRowId !== `row_${index}` + } sx={{ fontSize: '0.7rem', py: 0.5, diff --git a/src/components/PickOrderSearch/PickExecutionForm.tsx b/src/components/PickOrderSearch/PickExecutionForm.tsx index fe67afd..e51ea71 100644 --- a/src/components/PickOrderSearch/PickExecutionForm.tsx +++ b/src/components/PickOrderSearch/PickExecutionForm.tsx @@ -301,9 +301,9 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { variant="outlined" /> - + {/* ✅ Show issue description and handler fields when bad items > 0 */} - {(formData.badItemQty && formData.badItemQty > 0) && ( + {(formData.badItemQty && formData.badItemQty > 0) ? ( <> { - )} - + ) : (<>)} + + diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index 2ed7ef7..8820a70 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -227,6 +227,8 @@ "This form is for reporting issues only. You must report either missing items or bad items.":"此表單僅用於報告問題。您必須報告缺少的貨品或不良貨品。", "Bad item Qty":"不良貨品數量", "Missing item Qty":"缺少貨品數量", + "Bad Item Qty":"不良貨品數量", + "Missing Item Qty":"缺少貨品數量", "Actual Pick Qty":"實際提料數量", "Required Qty":"所需數量", "Issue Remark":"問題描述", @@ -234,5 +236,11 @@ "Qty is required":"必需輸入數量", "Qty is not allowed to be greater than remaining available qty":"輸入數量不能大於剩餘可用數量", "Qty is not allowed to be greater than required qty":"輸入數量不能大於所需數量", - "At least one issue must be reported":"至少需要報告一個問題" + "At least one issue must be reported":"至少需要報告一個問題", + "issueRemark":"問題描述是必需的", + "handler":"處理者", + "Max":"最大值", + "Route":"路線", + "Index":"編號", + "No FG pick orders found":"沒有成品提料單" } \ No newline at end of file