From 32a5a541b92931f45f3ca9f9177a1143d9691fee Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Mon, 15 Sep 2025 11:48:42 +0800 Subject: [PATCH] update --- src/app/api/pickOrder/actions.ts | 217 ++++- .../FinishedGoodSearch/FinishedGoodSearch.tsx | 6 +- .../FinishedGoodSearch/GoodPickExecution.tsx | 786 ++++++++++++++++-- .../GoodPickExecutionForm.tsx | 372 +++++++++ .../FinishedGoodSearch/LotTable.tsx | 737 ---------------- .../PickQcStockInModalVer3.tsx | 2 +- .../FinishedGoodSearch/newcreatitem.tsx | 2 +- src/components/PickOrderSearch/LotTable.tsx | 406 +++++---- .../PickOrderSearch/PickExecution.tsx | 457 +++++----- .../PickOrderSearch/PickExecutionForm.tsx | 372 +++++++++ .../PickOrderSearch/PickOrderDetailsTable.tsx | 196 +++++ .../PickQcStockInModalVer3.tsx | 2 +- .../PickOrderSearch/newcreatitem.tsx | 2 +- src/i18n/zh/pickOrder.json | 2 +- 14 files changed, 2280 insertions(+), 1279 deletions(-) create mode 100644 src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx delete mode 100644 src/components/FinishedGoodSearch/LotTable.tsx create mode 100644 src/components/PickOrderSearch/PickExecutionForm.tsx create mode 100644 src/components/PickOrderSearch/PickOrderDetailsTable.tsx diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index 9552aa5..54050b9 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -94,10 +94,12 @@ export interface GetPickOrderInfoResponse { export interface GetPickOrderInfo { id: number; code: string; - targetDate: string; + consoCode: string | null; // ✅ 添加 consoCode 属性 + targetDate: string | number[]; // ✅ Support both formats type: string; status: string; assignTo: number; + groupName: string; // ✅ Add this field pickOrderLines: GetPickOrderLineInfo[]; } @@ -157,9 +159,126 @@ export interface LotDetailWithStockOutLine { stockOutLineStatus?: string; stockOutLineQty?: number; } +export interface PickAnotherLotFormData { + pickOrderLineId: number; + lotId: number; + qty: number; + type: string; + handlerId?: number; + category?: string; + releasedBy?: number; + recordDate?: string; +} +export const recordFailLot = async (data: PickAnotherLotFormData) => { + const result = await serverFetchJson( + `${BASE_API_URL}/suggestedPickLot/recordFailLot`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + revalidateTag("pickorder"); + return result; +}; +export interface PickExecutionIssueData { + pickOrderId: number; + pickOrderCode: string; + pickOrderCreateDate: string; + pickExecutionDate: string; + pickOrderLineId: number; + itemId: number; + itemCode: string; + itemDescription: string; + lotId: number; + lotNo: string; + storeLocation: string; + requiredQty: number; + actualPickQty: number; + missQty: number; + badItemQty: number; + issueRemark: string; + pickerName: string; + handledBy?: number; +} +export interface AutoAssignReleaseResponse { + id: number | null; + name: string; + code: string; + type?: string; + message: string | null; + errorPosition: string; + entity?: { + consoCode?: string; + pickOrderIds?: number[]; + hasActiveOrders: boolean; + }; +} +export interface PickOrderCompletionResponse { + id: number | null; + name: string; + code: string; + type?: string; + message: string | null; + errorPosition: string; + entity?: { + hasCompletedOrders: boolean; + completedOrders: Array<{ + pickOrderId: number; + pickOrderCode: string; + consoCode: string; + isCompleted: boolean; + stockOutStatus: string; + totalLines: number; + unfinishedLines: number; + }>; + allOrders: Array<{ + pickOrderId: number; + pickOrderCode: string; + consoCode: string; + isCompleted: boolean; + stockOutStatus: string; + totalLines: number; + unfinishedLines: number; + }>; + }; +} +export const autoAssignAndReleasePickOrder = async (userId: number): Promise => { + const response = await serverFetchJson( + `${BASE_API_URL}/pickOrder/auto-assign-release/${userId}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); + revalidateTag("pickorder"); + return response; +}; +export const checkPickOrderCompletion = async (userId: number): Promise => { + const response = await serverFetchJson( + `${BASE_API_URL}/pickOrder/check-pick-completion/${userId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + return response; +}; +export const recordPickExecutionIssue = async (data: PickExecutionIssueData) => { + const result = await serverFetchJson( + `${BASE_API_URL}/pickExecution/recordIssue`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + revalidateTag("pickorder"); + return result; +}; export const resuggestPickOrder = async (pickOrderId: number) => { console.log("Resuggesting pick order:", pickOrderId); const result = await serverFetchJson( @@ -286,7 +405,7 @@ export interface PickOrderLotDetailResponse { actualPickQty: number; suggestedPickLotId: number; lotStatus: string; - lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; + lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected'; } interface ALLPickOrderLotDetailResponse { // Pick Order Information @@ -315,24 +434,62 @@ interface ALLPickOrderLotDetailResponse { lotNo: string; expiryDate: string; location: string; + outQty: number; + holdQty: number; stockUnit: string; availableQty: number; requiredQty: number; actualPickQty: number; + totalPickedByAllPickOrders: number; suggestedPickLotId: number; lotStatus: string; stockOutLineId?: number; stockOutLineStatus?: string; stockOutLineQty?: number; - lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; + lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected'; processingStatus: string; } -export const fetchALLPickOrderLineLotDetails = cache(async (userId?: number) => { +interface SuggestionWithStatus { + suggestionId: number; + suggestionQty: number; + suggestionCreated: string; + lotLineId: number; + lotNo: string; + expiryDate: string; + location: string; + stockOutLineId?: number; + stockOutLineStatus?: string; + stockOutLineQty?: number; + suggestionStatus: 'active' | 'completed' | 'rejected' | 'in_progress' | 'unknown'; +} +export interface CheckCompleteResponse { + id: number | null; + name: string; + code: string; + type?: string; + message: string | null; + errorPosition: string; +} + +export const checkAndCompletePickOrderByConsoCode = async (consoCode: string): Promise => { + const response = await serverFetchJson( + `${BASE_API_URL}/pickOrder/check-complete/${consoCode}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }, + ); + revalidateTag("pickorder"); + return response; +}; +export const fetchPickOrderDetailsOptimized = cache(async (userId?: number) => { const url = userId - ? `${BASE_API_URL}/pickOrder/all-lots-with-details?userId=${userId}` - : `${BASE_API_URL}/pickOrder/all-lots-with-details`; + ? `${BASE_API_URL}/pickOrder/detail-optimized?userId=${userId}` + : `${BASE_API_URL}/pickOrder/detail-optimized`; - return serverFetchJson( + return serverFetchJson( url, { method: "GET", @@ -340,11 +497,49 @@ export const fetchALLPickOrderLineLotDetails = cache(async (userId?: number) => }, ); }); -export const fetchAllPickOrderDetails = cache(async (userId?: number) => { - const url = userId - ? `${BASE_API_URL}/pickOrder/detail?userId=${userId}` - : `${BASE_API_URL}/pickOrder/detail`; +const fetchSuggestionsWithStatus = async (pickOrderLineId: number) => { + try { + const response = await fetch(`/api/suggestedPickLot/suggestions-with-status/${pickOrderLineId}`); + const suggestions: SuggestionWithStatus[] = await response.json(); + return suggestions; + } catch (error) { + console.error('Error fetching suggestions with status:', error); + return []; + } +}; + +export const fetchALLPickOrderLineLotDetails = cache(async (userId: number): Promise => { + try { + console.log("🔍 Fetching all pick order line lot details for userId:", userId); + + // ✅ 使用 serverFetchJson 而不是直接的 fetch + const data = await serverFetchJson( + `${BASE_API_URL}/pickOrder/all-lots-with-details/${userId}`, + { + method: 'GET', + next: { tags: ["pickorder"] }, + } + ); + console.log("✅ API Response:", data); + return data; + } catch (error) { + console.error("❌ Error fetching all pick order line lot details:", error); + throw error; + } +}); +export const fetchAllPickOrderDetails = cache(async (userId?: number) => { + if (!userId) { + return { + consoCode: null, + pickOrders: [], + items: [] + }; + } + + // ✅ Use the correct endpoint with userId in the path + const url = `${BASE_API_URL}/pickOrder/detail-optimized/${userId}`; + return serverFetchJson( url, { diff --git a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx index 391ff8a..43c171c 100644 --- a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx +++ b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx @@ -271,8 +271,6 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { borderBottom: '1px solid #e0e0e0' }}> - - @@ -281,9 +279,7 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { - {tabIndex === 2 && } - {tabIndex === 0 && } - {tabIndex === 1 && } + {tabIndex === 0 && } ); diff --git a/src/components/FinishedGoodSearch/GoodPickExecution.tsx b/src/components/FinishedGoodSearch/GoodPickExecution.tsx index ff70b6b..c38a5c6 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecution.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecution.tsx @@ -6,14 +6,26 @@ import { Stack, TextField, Typography, + Alert, + CircularProgress, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + TablePagination, + Modal, } from "@mui/material"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState, useRef, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/navigation"; import { fetchALLPickOrderLineLotDetails, updateStockOutLineStatus, createStockOutLine, + recordPickExecutionIssue, } from "@/app/api/pickOrder/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; import { @@ -25,54 +37,316 @@ 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 CombinedLotTable from './CombinedLotTable'; import { useSession } from "next-auth/react"; -import { SessionWithTokens } from "@/config/authConfig"; // ✅ Import the custom session type +import { SessionWithTokens } from "@/config/authConfig"; +import { + autoAssignAndReleasePickOrder, + AutoAssignReleaseResponse, + checkPickOrderCompletion, + PickOrderCompletionResponse, + checkAndCompletePickOrderByConsoCode +} from "@/app/api/pickOrder/actions"; +import { fetchStockInLineInfo } from "@/app/api/po/actions"; +import GoodPickExecutionForm from "./GoodPickExecutionForm"; 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; +}> = ({ open, onClose, lot, onQrCodeSubmit }) => { + 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(''); + + // 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 }; // ✅ Cast to custom type + const { data: session } = useSession() as { data: SessionWithTokens | null }; - // ✅ Get current user ID from session with proper typing const currentUserId = session?.id ? parseInt(session.id) : undefined; - // ✅ Combined approach states const [combinedLotData, setCombinedLotData] = useState([]); const [combinedDataLoading, setCombinedDataLoading] = useState(false); const [originalCombinedData, setOriginalCombinedData] = useState([]); - // ✅ QR Scanner context const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); - // ✅ QR scan input states const [qrScanInput, setQrScanInput] = useState(''); const [qrScanError, setQrScanError] = useState(false); const [qrScanSuccess, setQrScanSuccess] = useState(false); - // ✅ Pick quantity states const [pickQtyData, setPickQtyData] = useState>({}); - - // ✅ Search states const [searchQuery, setSearchQuery] = useState>({}); - // ✅ Add pagination state const [paginationController, setPaginationController] = useState({ pageNum: 0, pageSize: 10, }); - // ✅ Keep only essential states const [usernameList, setUsernameList] = useState([]); + const initializationRef = useRef(false); + const autoAssignRef = useRef(false); + const formProps = useForm(); const errors = formProps.formState.errors; - // ✅ Start QR scanning on component mount + // ✅ 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); + useEffect(() => { startScan(); return () => { @@ -81,37 +355,53 @@ const PickExecution: React.FC = ({ filterArgs }) => { }; }, [startScan, stopScan, resetScan]); - // ✅ Fetch all combined lot data - Updated to use current user ID const fetchAllCombinedLotData = useCallback(async (userId?: number) => { setCombinedDataLoading(true); try { - // ✅ Use passed userId or current user ID const userIdToUse = userId || currentUserId; + + console.log(" fetchAllCombinedLotData called with userId:", userIdToUse); + + if (!userIdToUse) { + console.warn("⚠️ No userId available, skipping API call"); + setCombinedLotData([]); + setOriginalCombinedData([]); + return; + } + + // ✅ 使用新的API路径,后端会自动处理分配逻辑 const allLotDetails = await fetchALLPickOrderLineLotDetails(userIdToUse); - console.log("All combined lot details:", allLotDetails); + console.log("✅ All combined lot details:", allLotDetails); setCombinedLotData(allLotDetails); - setOriginalCombinedData(allLotDetails); // Store original for filtering + setOriginalCombinedData(allLotDetails); } catch (error) { - console.error("Error fetching combined lot data:", error); + console.error("❌ Error fetching combined lot data:", error); setCombinedLotData([]); setOriginalCombinedData([]); } finally { setCombinedDataLoading(false); } - }, [currentUserId]); // ✅ Add currentUserId as dependency - + }, [currentUserId]); - // ✅ Load data on component mount - Now uses current user ID + // ✅ 简化初始化逻辑,移除前端的自动分配检查 useEffect(() => { - fetchAllCombinedLotData(); // This will now use currentUserId - }, [fetchAllCombinedLotData]); + if (session && currentUserId && !initializationRef.current) { + console.log("✅ Session loaded, initializing pick order..."); + initializationRef.current = true; + + // ✅ 直接获取数据,后端会自动处理分配逻辑 + fetchAllCombinedLotData(); + } + }, [session, currentUserId, fetchAllCombinedLotData]); + + // ✅ 移除前端的自动分配逻辑,因为后端已经处理了 + // const handleAutoAssignAndRelease = useCallback(async () => { ... }); // 删除这个函数 - // ✅ Handle QR code submission for matched lot - FIXED: Handle multiple pick order lines + // ✅ Handle QR code submission for matched lot (external scanning) const handleQrCodeSubmit = useCallback(async (lotNo: string) => { console.log(`✅ Processing QR Code for lot: ${lotNo}`); console.log(`🔍 Available lots:`, combinedLotData.map(lot => lot.lotNo)); - // Find ALL matching lots (same lot number can be used by multiple pick order lines) const matchingLots = combinedLotData.filter(lot => lot.lotNo === lotNo || lot.lotNo?.toLowerCase() === lotNo.toLowerCase() @@ -119,7 +409,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { if (matchingLots.length === 0) { console.error(`❌ Lot not found: ${lotNo}`); - console.error(`❌ Available lots:`, combinedLotData.map(lot => lot.lotNo)); setQrScanError(true); setQrScanSuccess(false); return; @@ -133,18 +422,15 @@ const PickExecution: React.FC = ({ filterArgs }) => { let existsCount = 0; let errorCount = 0; - // ✅ Process each matching lot (each pick order line that uses this lot) for (const matchingLot of matchingLots) { console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`); - // ✅ Check if stockOutLineId is null before creating if (matchingLot.stockOutLineId) { console.log(`✅ Stock out line already exists for line ${matchingLot.pickOrderLineId}`); existsCount++; } else { - // Create stock out line for this specific pick order line const stockOutLineData: CreateStockOutLine = { - consoCode: matchingLot.pickOrderCode, // Use pick order code as conso code + consoCode: matchingLot.pickOrderConsoCode, // ✅ Use pickOrderConsoCode instead of pickOrderCode pickOrderLineId: matchingLot.pickOrderLineId, inventoryLotLineId: matchingLot.lotId, qty: 0.0 @@ -154,7 +440,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { const result = await createStockOutLine(stockOutLineData); console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, result); - // ✅ Handle different response codes if (result && result.code === "EXISTS") { console.log(`✅ Stock out line already exists for line ${matchingLot.pickOrderLineId}`); existsCount++; @@ -167,7 +452,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { } } - // Auto-set pick quantity to required quantity for this specific line const lotKey = `${matchingLot.pickOrderLineId}-${matchingLot.lotId}`; setPickQtyData(prev => ({ ...prev, @@ -175,7 +459,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { })); } - // ✅ Set success state if at least one operation succeeded if (successCount > 0 || existsCount > 0) { setQrScanSuccess(true); setQrScanError(false); @@ -187,10 +470,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { return; } - // Refresh data await fetchAllCombinedLotData(); - - // Clear input after successful match setQrScanInput(''); console.log("Stock out line process completed successfully!"); @@ -201,28 +481,66 @@ const PickExecution: React.FC = ({ filterArgs }) => { } }, [combinedLotData, fetchAllCombinedLotData]); - // ✅ Process scanned QR codes automatically - FIXED: Only process when data is loaded + // ✅ 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]); + + // ✅ External QR scanning - process QR codes from outside the page useEffect(() => { if (qrValues.length > 0 && combinedLotData.length > 0) { const latestQr = qrValues[qrValues.length - 1]; const qrContent = latestQr.replace(/[{}]/g, ''); setQrScanInput(qrContent); - // Auto-process the QR code handleQrCodeSubmit(qrContent); } }, [qrValues, combinedLotData, handleQrCodeSubmit]); - // ✅ Handle manual input submission const handleManualInputSubmit = useCallback(() => { if (qrScanInput.trim() !== '') { handleQrCodeSubmit(qrScanInput.trim()); } }, [qrScanInput, handleQrCodeSubmit]); - // ✅ Handle pick quantity change - FIXED: Better input handling const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { - // ✅ Handle empty string as 0 if (value === '' || value === null || value === undefined) { setPickQtyData(prev => ({ ...prev, @@ -231,10 +549,8 @@ const PickExecution: React.FC = ({ filterArgs }) => { return; } - // ✅ Convert to number properly const numericValue = typeof value === 'string' ? parseFloat(value) : value; - // ✅ Handle NaN case if (isNaN(numericValue)) { setPickQtyData(prev => ({ ...prev, @@ -249,6 +565,26 @@ const PickExecution: React.FC = ({ filterArgs }) => { })); }, []); + 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}`; @@ -260,11 +596,9 @@ const PickExecution: React.FC = ({ filterArgs }) => { } try { - // ✅ FIXED: Calculate cumulative quantity const currentActualPickQty = lot.actualPickQty || 0; const cumulativeQty = currentActualPickQty + newQty; - // ✅ FIXED: Check cumulative quantity against required quantity let newStatus = 'partially_completed'; if (cumulativeQty >= lot.requiredQty) { @@ -283,26 +617,50 @@ const PickExecution: React.FC = ({ filterArgs }) => { await updateStockOutLineStatus({ id: lot.stockOutLineId, status: newStatus, - qty: cumulativeQty // ✅ Submit the cumulative quantity + qty: cumulativeQty }); - // Update inventory if (newQty > 0) { await updateInventoryLotLineQuantities({ inventoryLotLineId: lot.lotId, - qty: newQty, // ✅ Only update inventory with the new quantity + qty: newQty, status: 'available', operation: 'pick' }); } - // Refresh data + // ✅ 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]); + }, [pickQtyData, fetchAllCombinedLotData, checkAndAutoAssignNext]); // ✅ Handle reject lot const handleRejectLot = useCallback(async (lot: any) => { @@ -318,15 +676,66 @@ const PickExecution: React.FC = ({ filterArgs }) => { qty: 0 }); - // Refresh data 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]); - // ✅ Search criteria + // ✅ 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"), @@ -350,7 +759,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { }, ]; - // ✅ Search handler const handleSearch = useCallback((query: Record) => { setSearchQuery({ ...query }); console.log("Search query:", query); @@ -377,7 +785,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { console.log("Filtered lots count:", filtered.length); }, [originalCombinedData]); - // ✅ Reset handler const handleReset = useCallback(() => { setSearchQuery({}); if (originalCombinedData) { @@ -385,7 +792,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { } }, [originalCombinedData]); - // ✅ Pagination handlers const handlePageChange = useCallback((event: unknown, newPage: number) => { setPaginationController(prev => ({ ...prev, @@ -401,6 +807,13 @@ const PickExecution: React.FC = ({ filterArgs }) => { }); }, []); + // Pagination data + const paginatedData = useMemo(() => { + const startIndex = paginationController.pageNum * paginationController.pageSize; + const endIndex = startIndex + paginationController.pageSize; + return combinedLotData.slice(startIndex, endIndex); + }, [combinedLotData, paginationController]); + return ( @@ -413,12 +826,17 @@ const PickExecution: React.FC = ({ filterArgs }) => { /> - {/* Combined Lot Table with QR Scan Input */} + + + + + {/* Combined Lot Table */} {t("All Pick Order Lots")} + {/* ✅ External QR scan input - for scanning from outside the page */} = ({ filterArgs }) => { - + + + + {t("Pick Order Code")} + {t("Item Code")} + {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.pickOrderCode} + {lot.itemCode} + {lot.itemName} + + + + {lot.lotNo} + + {lot.lotAvailability !== 'available' && ( + + ({lot.lotAvailability === 'expired' ? 'Expired' : + lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' : + lot.lotAvailability === 'rejected' ? 'Rejected' : + 'Unavailable'}) + + )} + + + {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: '80px', + '& .MuiInputBase-input': { + fontSize: '0.75rem', + textAlign: 'center', + padding: '8px 4px' + } + }} + 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} + 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()} + /> + )}
); }; diff --git a/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx b/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx new file mode 100644 index 0000000..9af0197 --- /dev/null +++ b/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx @@ -0,0 +1,372 @@ +// 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; + 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; + // selectedRowId?: number | null; +} + +// 定义错误类型 +interface FormErrors { + actualPickQty?: string; + missQty?: string; + badItemQty?: string; + issueRemark?: string; + handledBy?: string; +} + +const PickExecutionForm: React.FC = ({ + open, + onClose, + onSubmit, + selectedLot, + selectedPickOrderLine, + pickOrderId, + pickOrderCreateDate, + // ✅ Remove these props + // onNormalPickSubmit, + // selectedRowId, +}) => { + const { t } = useTranslation(); + const [formData, setFormData] = useState>({}); + const [errors, setErrors] = useState({}); + const [loading, setLoading] = useState(false); + const [handlers, setHandlers] = useState>([]); + + // 计算剩余可用数量 + const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { + const remainingQty = lot.inQty - lot.outQty; + return Math.max(0, remainingQty); + }, []); +const calculateRequiredQty = useCallback((lot: LotPickData) => { + const requiredQty = lot.requiredQty-(lot.actualPickQty||0); + return Math.max(0, requiredQty); +}, []); + // 获取处理人员列表 + 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]; + } + }; + + // 计算剩余可用数量 + const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); + const requiredQty = calculateRequiredQty(selectedLot); + console.log("=== PickExecutionForm Debug ==="); + console.log("selectedLot:", selectedLot); + console.log("inQty:", selectedLot.inQty); + console.log("outQty:", selectedLot.outQty); + console.log("holdQty:", selectedLot.holdQty); + console.log("availableQty:", selectedLot.availableQty); + console.log("calculated remainingAvailableQty:", remainingAvailableQty); + 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: selectedLot.actualPickQty || 0, + missQty: 0, + badItemQty: 0, // 初始化为 0,用户需要手动输入 + issueRemark: '', + pickerName: '', + handledBy: undefined, + }); + } + }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]); + + 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 = {}; + + if (formData.actualPickQty === undefined || formData.actualPickQty < 0) { + newErrors.actualPickQty = t('pickOrder.validation.actualPickQtyRequired'); + } + + // ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported) + const hasMissQty = formData.missQty && formData.missQty > 0; + const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0; + + if (!hasMissQty && !hasBadItemQty) { + newErrors.missQty = t('pickOrder.validation.mustReportMissOrBadItems'); + newErrors.badItemQty = t('pickOrder.validation.mustReportMissOrBadItems'); + } + + if (formData.missQty && formData.missQty < 0) { + newErrors.missQty = t('pickOrder.validation.missQtyInvalid'); + } + + if (formData.badItemQty && formData.badItemQty < 0) { + newErrors.badItemQty = t('pickOrder.validation.badItemQtyInvalid'); + } + + if (formData.badItemQty && formData.badItemQty > 0 && !formData.issueRemark) { + newErrors.issueRemark = t('pickOrder.validation.issueRemarkRequired'); + } + + if (formData.badItemQty && formData.badItemQty > 0 && !formData.handledBy) { + newErrors.handledBy = t('pickOrder.validation.handlerRequired'); + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + if (!validateForm() || !formData.pickOrderId) { + return; + } + + setLoading(true); + try { + await onSubmit(formData as PickExecutionIssueData); + onClose(); + } catch (error) { + console.error('Error submitting pick execution issue:', error); + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + setFormData({}); + setErrors({}); + onClose(); + }; + + if (!selectedLot || !selectedPickOrderLine) { + return null; + } + + const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); + const requiredQty = calculateRequiredQty(selectedLot); + + return ( + + + {t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */} + + + + {/* ✅ Add instruction text */} + + + + + {t('Note:')} {t('This form is for reporting issues only. You must report either missing items or bad items.')} + + + + + {/* ✅ Keep the existing form fields */} + + + + + + + + + + handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)} + error={!!errors.actualPickQty} + helperText={errors.actualPickQty || t('Enter the quantity actually picked')} + variant="outlined" + /> + + + + handleInputChange('missQty', parseFloat(e.target.value) || 0)} + error={!!errors.missQty} + helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')} + variant="outlined" + /> + + + + handleInputChange('badItemQty', parseFloat(e.target.value) || 0)} + error={!!errors.badItemQty} + 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) && ( + <> + + handleInputChange('issueRemark', e.target.value)} + error={!!errors.issueRemark} + helperText={errors.issueRemark} + placeholder={t('Describe the issue with bad items')} + variant="outlined" + /> + + + + + {t('handler')} + + {errors.handledBy && ( + + {errors.handledBy} + + )} + + + + )} + + + + + + + + + ); +}; + +export default PickExecutionForm; \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/LotTable.tsx b/src/components/FinishedGoodSearch/LotTable.tsx deleted file mode 100644 index 8ae15df..0000000 --- a/src/components/FinishedGoodSearch/LotTable.tsx +++ /dev/null @@ -1,737 +0,0 @@ -"use client"; - -import { - Box, - Button, - Checkbox, - Paper, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Typography, - TablePagination, - Modal, -} from "@mui/material"; -import { useCallback, useMemo, useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import QrCodeIcon from '@mui/icons-material/QrCode'; -import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; -import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; -import { updateInventoryLotLineStatus } from "@/app/api/inventory/actions"; -import { updateStockOutLineStatus } from "@/app/api/pickOrder/actions"; -interface LotPickData { - id: number; - lotId: number; - lotNo: string; - expiryDate: string; - location: string; - stockUnit: string; - availableQty: number; - requiredQty: number; - actualPickQty: number; - lotStatus: string; - lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; - stockOutLineId?: number; - stockOutLineStatus?: string; - stockOutLineQty?: number; -} - -interface PickQtyData { - [lineId: number]: { - [lotId: number]: number; - }; -} - -interface LotTableProps { - lotData: LotPickData[]; - selectedRowId: number | null; - selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string }) | null; - pickQtyData: PickQtyData; - selectedLotRowId: string | null; - selectedLotId: number | null; - onLotSelection: (uniqueLotId: string, lotId: number) => void; - onPickQtyChange: (lineId: number, lotId: number, value: number) => void; - onSubmitPickQty: (lineId: number, lotId: number) => void; - onCreateStockOutLine: (inventoryLotLineId: number) => void; - onQcCheck: (line: GetPickOrderLineInfo, pickOrderCode: string) => void; - onLotSelectForInput: (lot: LotPickData) => void; - showInputBody: boolean; - setShowInputBody: (show: boolean) => void; - selectedLotForInput: LotPickData | null; - generateInputBody: () => any; - onDataRefresh: () => Promise; -} - -// ✅ QR Code Modal Component -const QrCodeModal: React.FC<{ - open: boolean; - onClose: () => void; - lot: LotPickData | null; - onQrCodeSubmit: (lotNo: string) => void; -}> = ({ open, onClose, lot, onQrCodeSubmit }) => { - const { t } = useTranslation("pickOrder"); - const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); - const [manualInput, setManualInput] = useState(''); - - // ✅ Add state to track manual input submission - const [manualInputSubmitted, setManualInputSubmitted] = useState(false); - const [manualInputError, setManualInputError] = useState(false); - - // ✅ Process scanned QR codes - useEffect(() => { - if (qrValues.length > 0 && lot) { - const latestQr = qrValues[qrValues.length - 1]; - const qrContent = latestQr.replace(/[{}]/g, ''); - - if (qrContent === lot.lotNo) { - onQrCodeSubmit(lot.lotNo); - onClose(); - resetScan(); - } else { - // ✅ Set error state for helper text - setManualInputError(true); - setManualInputSubmitted(true); - } - } - }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan]); - - // ✅ Clear states when modal opens or lot changes - useEffect(() => { - if (open) { - setManualInput(''); - setManualInputSubmitted(false); - setManualInputError(false); - } - }, [open]); - - useEffect(() => { - if (lot) { - setManualInput(''); - setManualInputSubmitted(false); - setManualInputError(false); - } - }, [lot]); - -{/* -const handleManualSubmit = () => { - if (manualInput.trim() === lot?.lotNo) { - // ✅ Success - no error helper text needed - onQrCodeSubmit(lot.lotNo); - onClose(); - setManualInput(''); - } else { - // ✅ Show error helper text after submit - setManualInputError(true); - setManualInputSubmitted(true); - // Don't clear input - let user see what they typed - } - }; - - return ( - - - - {t("QR Code Scan for Lot")}: {lot?.lotNo} - - - - - Scanner Status: {isScanning ? 'Scanning...' : 'Ready'} - - - - - - - - - - - Manual Input: - - setManualInput(e.target.value)} - sx={{ mb: 1 }} - // ✅ Only show error after submit button is clicked - error={manualInputSubmitted && manualInputError} - helperText={ - // ✅ Show helper text only after submit with error - manualInputSubmitted && manualInputError - ? `The input is not the same as the expected lot number. Expected: ${lot?.lotNo}` - : '' - } - /> - - - - {qrValues.length > 0 && ( - - - QR Scan Result: {qrValues[qrValues.length - 1]} - - {manualInputError && ( - - ❌ Mismatch! Expected: {lot?.lotNo} - - )} - - )} - - - - - - - ); -}; -*/} -useEffect(() => { - if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '') { - // Auto-submit when manual input matches the expected lot number - console.log('🔄 Auto-submitting manual input:', manualInput.trim()); - - // Add a small delay to ensure proper execution order - const timer = setTimeout(() => { - onQrCodeSubmit(lot.lotNo); - onClose(); - setManualInput(''); - setManualInputError(false); - setManualInputSubmitted(false); - }, 200); // 200ms delay - - return () => clearTimeout(timer); - } -}, [manualInput, lot, onQrCodeSubmit, onClose]); - const handleManualSubmit = () => { - if (manualInput.trim() === lot?.lotNo) { - // ✅ Success - no error helper text needed - onQrCodeSubmit(lot.lotNo); - onClose(); - setManualInput(''); - } else { - // ✅ Show error helper text after submit - setManualInputError(true); - setManualInputSubmitted(true); - // Don't clear input - let user see what they typed - } - }; -useEffect(() => { - if (open) { - startScan(); - } - }, [open, startScan]); - return ( - - - - QR Code Scan for Lot: {lot?.lotNo} - - - {/* Manual Input with Submit-Triggered Helper Text */} - - - Manual Input: - - setManualInput(e.target.value)} - sx={{ mb: 1 }} - error={manualInputSubmitted && manualInputError} - helperText={ - manualInputSubmitted && manualInputError - ? `The input is not the same as the expected lot number.` - : '' - } - /> - - - - {/* Show QR Scan Status */} - {qrValues.length > 0 && ( - - - QR Scan Result: {qrValues[qrValues.length - 1]} - - {manualInputError && ( - - ❌ Mismatch! Expected! - - )} - - )} - - - - - - - ); -}; - - -const LotTable: React.FC = ({ - lotData, - selectedRowId, - selectedRow, - pickQtyData, - selectedLotRowId, - selectedLotId, - onLotSelection, - onPickQtyChange, - onSubmitPickQty, - onCreateStockOutLine, - onQcCheck, - onLotSelectForInput, - showInputBody, - setShowInputBody, - selectedLotForInput, - generateInputBody, - onDataRefresh, -}) => { - const { t } = useTranslation("pickOrder"); - - // ✅ Add QR scanner context - const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); - - // ✅ Add state for QR input modal - const [qrModalOpen, setQrModalOpen] = useState(false); - const [selectedLotForQr, setSelectedLotForQr] = useState(null); - const [manualQrInput, setManualQrInput] = useState(''); - - // 分页控制器 - const [lotTablePagingController, setLotTablePagingController] = useState({ - pageNum: 0, - pageSize: 10, - }); - - // ✅ 添加状态消息生成函数 - const getStatusMessage = useCallback((lot: LotPickData) => { - if (!lot.stockOutLineId) { - return t("Please finish QR code scan, QC check and pick order."); - } - - switch (lot.stockOutLineStatus?.toLowerCase()) { - case 'pending': - return t("Please submit pick order."); - case 'checked': - return t("Please submit the pick order."); - case 'partially_completed': - return t("Partial quantity submitted. Please submit more or complete the order.") ; - case 'completed': - return t("Pick order completed successfully!"); - case 'rejected': - return t("QC check failed. Lot has been rejected and marked as unavailable."); - case 'unavailable': - return t("This order is insufficient, please pick another lot."); - default: - return t("Please finish QR code scan, QC check and pick order."); - } - }, []); - - const prepareLotTableData = useMemo(() => { - return lotData.map((lot) => ({ - ...lot, - id: lot.lotId, - })); - }, [lotData]); - - // 分页数据 - const paginatedLotTableData = useMemo(() => { - const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize; - const endIndex = startIndex + lotTablePagingController.pageSize; - return prepareLotTableData.slice(startIndex, endIndex); - }, [prepareLotTableData, lotTablePagingController]); - - // 分页处理函数 - const handleLotTablePageChange = useCallback((event: unknown, newPage: number) => { - setLotTablePagingController(prev => ({ - ...prev, - pageNum: newPage, - })); - }, []); - - const handleLotTablePageSizeChange = useCallback((event: React.ChangeEvent) => { - const newPageSize = parseInt(event.target.value, 10); - setLotTablePagingController({ - pageNum: 0, - pageSize: newPageSize, - }); - }, []); - - // ✅ Handle QR code submission - const handleQrCodeSubmit = useCallback(async (lotNo: string) => { - if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { - console.log(`✅ QR Code verified for lot: ${lotNo}`); - - // ✅ Store the required quantity before creating stock out line - const requiredQty = selectedLotForQr.requiredQty; - const lotId = selectedLotForQr.lotId; - - // ✅ Create stock out line and wait for it to complete - await onCreateStockOutLine(selectedLotForQr.lotId); - - // ✅ Close modal - setQrModalOpen(false); - setSelectedLotForQr(null); - - // ✅ Set pick quantity AFTER stock out line creation and refresh is complete - if (selectedRowId) { - // Add a small delay to ensure the data refresh from onCreateStockOutLine is complete - setTimeout(() => { - onPickQtyChange(selectedRowId, lotId, requiredQty); - console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); - }, 500); // 500ms delay to ensure refresh is complete - } - - // ✅ Show success message - console.log("Stock out line created successfully!"); - } - }, [selectedLotForQr, onCreateStockOutLine, selectedRowId, onPickQtyChange]); - - return ( - <> - - - - - {t("Selected")} - {t("Lot#")} - {t("Lot Expiry Date")} - {t("Lot Location")} - {t("Available Lot")} - {t("Lot Required Pick Qty")} - {t("Stock Unit")} - {t("QR Code Scan")} - {t("QC Check")} - {t("Lot Actual Pick Qty")} - {t("Submit")} - - - - {paginatedLotTableData.length === 0 ? ( - - - - {t("No data available")} - - - - ) : ( - paginatedLotTableData.map((lot, index) => ( - - - onLotSelection(`row_${index}`, lot.lotId)} - // ✅ Allow selection of available AND insufficient_stock lots - //disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'} - value={`row_${index}`} - name="lot-selection" - /> - - - - {lot.lotNo} - {lot.lotAvailability !== 'available' && ( - - ({lot.lotAvailability === 'expired' ? 'Expired' : - lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' : - 'Unavailable'}) - - )} - - - {lot.expiryDate} - {lot.location} - {lot.availableQty.toLocaleString()} - {lot.requiredQty.toLocaleString()} - {lot.stockUnit} - - {/* QR Code Scan Button */} - - - - - - - - - {/* QC Check Button */} - {/* - - - */} - - - {/* Lot Actual Pick Qty */} - - { - if (selectedRowId) { - const inputValue = e.target.value; - // ✅ Fixed: Handle empty string and prevent leading zeros - if (inputValue === '') { - // Allow empty input (user can backspace to clear) - onPickQtyChange(selectedRowId, lot.lotId, 0); - } else { - // Parse the number and prevent leading zeros - const numValue = parseInt(inputValue, 10); - if (!isNaN(numValue)) { - onPickQtyChange(selectedRowId, lot.lotId, numValue); - } - } - } - }} - onBlur={(e) => { - // ✅ Fixed: When input loses focus, ensure we have a valid number - if (selectedRowId) { - const currentValue = pickQtyData[selectedRowId]?.[lot.lotId]; - if (currentValue === undefined || currentValue === null) { - // Set to 0 if no value - onPickQtyChange(selectedRowId, lot.lotId, 0); - } - } - }} - inputProps={{ - min: 0, - max: lot.availableQty, - step: 1 // Allow only whole numbers - }} - // ✅ Allow input for available AND insufficient_stock lots - disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'} - sx={{ width: '80px' }} - placeholder="0" // Show placeholder instead of default value - /> - - - - - {/* Submit Button */} - - - - - )) - )} - -
-
- - {/* ✅ Status Messages Display */} - {paginatedLotTableData.length > 0 && ( - - {paginatedLotTableData.map((lot, index) => ( - - - Lot {lot.lotNo}: {getStatusMessage(lot)} - - - ))} - - )} - - - - - `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` - } - /> - - {/* ✅ QR Code Modal */} - { - setQrModalOpen(false); - setSelectedLotForQr(null); - stopScan(); - resetScan(); - }} - lot={selectedLotForQr} - onQrCodeSubmit={handleQrCodeSubmit} - /> - - ); -}; - -export default LotTable; \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx b/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx index 6857bfb..f3d8d25 100644 --- a/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx +++ b/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx @@ -120,7 +120,7 @@ interface LotPickData { requiredQty: number; actualPickQty: number; lotStatus: string; - lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; + lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected'; stockOutLineId?: number; stockOutLineStatus?: string; stockOutLineQty?: number; diff --git a/src/components/FinishedGoodSearch/newcreatitem.tsx b/src/components/FinishedGoodSearch/newcreatitem.tsx index 4e6b58c..344687b 100644 --- a/src/components/FinishedGoodSearch/newcreatitem.tsx +++ b/src/components/FinishedGoodSearch/newcreatitem.tsx @@ -687,7 +687,7 @@ const handleQtyBlur = useCallback((itemId: number) => { formProps.reset(); setHasSearched(false); setFilteredItems([]); - alert(t("All pick orders created successfully")); + // alert(t("All pick orders created successfully")); // 通知父组件切换到 Assign & Release 标签页 if (onPickOrderCreated) { diff --git a/src/components/PickOrderSearch/LotTable.tsx b/src/components/PickOrderSearch/LotTable.tsx index 0e0165a..d0e1d29 100644 --- a/src/components/PickOrderSearch/LotTable.tsx +++ b/src/components/PickOrderSearch/LotTable.tsx @@ -20,11 +20,12 @@ import { import { useCallback, useMemo, useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import QrCodeIcon from '@mui/icons-material/QrCode'; -import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; +import { GetPickOrderLineInfo, recordPickExecutionIssue } from "@/app/api/pickOrder/actions"; import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; import { updateInventoryLotLineStatus } from "@/app/api/inventory/actions"; import { updateStockOutLineStatus } from "@/app/api/pickOrder/actions"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; // ✅ Add this import +import PickExecutionForm from "./PickExecutionForm"; interface LotPickData { id: number; lotId: number; @@ -37,7 +38,10 @@ interface LotPickData { requiredQty: number; actualPickQty: number; lotStatus: string; - lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; + outQty: number; + holdQty: number; + totalPickedByAllPickOrders: number; + lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable' | 'rejected'; // ✅ 添加 'rejected' stockOutLineId?: number; stockOutLineStatus?: string; stockOutLineQty?: number; @@ -52,7 +56,7 @@ interface PickQtyData { interface LotTableProps { lotData: LotPickData[]; selectedRowId: number | null; - selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string }) | null; + selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string; pickOrderId: number }) | null; // ✅ 添加 pickOrderId pickQtyData: PickQtyData; selectedLotRowId: string | null; selectedLotId: number | null; @@ -63,6 +67,9 @@ interface LotTableProps { onQcCheck: (line: GetPickOrderLineInfo, pickOrderCode: string) => void; onLotSelectForInput: (lot: LotPickData) => void; showInputBody: boolean; + totalPickedByAllPickOrders: number; + outQty: number; + holdQty: number; setShowInputBody: (show: boolean) => void; selectedLotForInput: LotPickData | null; generateInputBody: () => any; @@ -383,7 +390,11 @@ const LotTable: React.FC = ({ onLotDataRefresh, }) => { const { t } = useTranslation("pickOrder"); - + const calculateRemainingRequiredQty = useCallback((lot: LotPickData) => { + const requiredQty = lot.requiredQty || 0; + const stockOutLineQty = lot.stockOutLineQty || 0; + return Math.max(0, requiredQty - stockOutLineQty); + }, []); // ✅ Add QR scanner context const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); @@ -414,7 +425,7 @@ const LotTable: React.FC = ({ case 'completed': return t("Pick order completed successfully!"); case 'rejected': - return t("QC check failed. Lot has been rejected and marked as unavailable."); + return t("Lot has been rejected and marked as unavailable."); case 'unavailable': return t("This order is insufficient, please pick another lot."); default: @@ -455,7 +466,7 @@ const LotTable: React.FC = ({ if (!selectedRowId) return lot.availableQty; const lactualPickQty = lot.actualPickQty || 0; const actualPickQty = pickQtyData[selectedRowId]?.[lot.lotId] || 0; - const remainingQty = lot.inQty - actualPickQty - lactualPickQty; + const remainingQty = lot.inQty - lot.outQty; // Ensure it doesn't go below 0 return Math.max(0, remainingQty); @@ -490,6 +501,57 @@ const LotTable: React.FC = ({ } }, [selectedLotForQr, onCreateStockOutLine, selectedRowId, onPickQtyChange]); + // ✅ 添加 PickExecutionForm 相关的状态 + const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); + const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState(null); + + // ✅ 添加处理函数 + const handlePickExecutionForm = useCallback((lot: LotPickData) => { + 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); + + // ✅ 调用 API 提交数据 + 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); + + // ✅ 刷新数据 + if (onDataRefresh) { + await onDataRefresh(); + } + if (onLotDataRefresh) { + await onLotDataRefresh(); + } + } catch (error) { + console.error("Error submitting pick execution form:", error); + } + }, [onDataRefresh, onLotDataRefresh]); + return ( <> @@ -526,24 +588,43 @@ const LotTable: React.FC = ({ ) : ( paginatedLotTableData.map((lot, index) => ( - + - onLotSelection(`row_${index}`, lot.lotId)} - // ✅ Allow selection of available AND insufficient_stock lots - //disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'} - value={`row_${index}`} - name="lot-selection" - /> - + onLotSelection(`row_${index}`, lot.lotId)} + // ✅ 禁用 rejected、expired 和 status_unavailable 的批次 + disabled={lot.lotAvailability === 'expired' || + lot.lotAvailability === 'status_unavailable' || + lot.lotAvailability === 'rejected'} // ✅ 添加 rejected + value={`row_${index}`} + name="lot-selection" + /> + - {lot.lotNo} + + {lot.lotNo} + {lot.lotAvailability !== 'available' && ( ({lot.lotAvailability === 'expired' ? 'Expired' : lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' : + lot.lotAvailability === 'rejected' ? 'Rejected' : // ✅ 添加 rejected 显示 'Unavailable'}) )} @@ -552,174 +633,112 @@ const LotTable: React.FC = ({ {lot.expiryDate} {lot.location} {lot.stockUnit} - {lot.requiredQty.toLocaleString()} - {lot.inQty.toLocaleString()??'0'} - - {/* Show QR Scan Button if not scanned, otherwise show TextField */} - {!lot.stockOutLineId ? ( - - ) : ( - { - if (selectedRowId) { - const inputValue = e.target.value; - // ✅ Fixed: Handle empty string and prevent leading zeros - if (inputValue === '') { - // Allow empty input (user can backspace to clear) - onPickQtyChange(selectedRowId, lot.lotId, 0); - } else { - // Parse the number and prevent leading zeros - const numValue = parseInt(inputValue, 10); - if (!isNaN(numValue)) { - onPickQtyChange(selectedRowId, lot.lotId, numValue); - } - } - } - }} - onBlur={(e) => { - // ✅ Fixed: When input loses focus, ensure we have a valid number - if (selectedRowId) { - const currentValue = pickQtyData[selectedRowId]?.[lot.lotId]; - if (currentValue === undefined || currentValue === null) { - // Set to 0 if no value - onPickQtyChange(selectedRowId, lot.lotId, 0); - } - } - }} - inputProps={{ - min: 0, - max: lot.availableQty, - step: 1 // Allow only whole numbers - }} - // ✅ Allow input for available AND insufficient_stock lots - disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'} - sx={{ - width: '80px', - '& .MuiInputBase-root': { - height: '40px', // ✅ Match table cell height - }, - '& .MuiInputBase-input': { - height: '40px', - padding: '8px 12px', // ✅ Adjust padding to center text vertically - } - }} - placeholder="0" // Show placeholder instead of default value - /> - )} - - {/*{lot.availableQty.toLocaleString()}*/} - {calculateRemainingAvailableQty(lot).toLocaleString()} - {/* {lot.stockUnit} */} + {calculateRemainingRequiredQty(lot).toLocaleString()} + + {(() => { + const inQty = lot.inQty || 0; + const outQty = lot.outQty || 0; - {/* QR Code Scan Button */} - {/* - - - - - - + + const result = inQty - outQty; + return result.toLocaleString(); + })()} - */} - {/* QC Check Button */} - {/* - - */} - - - {/* Lot Actual Pick Qty */} - + {/* Show QR Scan Button if not scanned, otherwise show TextField + Pick Form */} + {!lot.stockOutLineId ? ( + + ) : ( + // ✅ 当有 stockOutLineId 时,显示 TextField + Pick Form 按钮 + + {/* ✅ 恢复 TextField 用于正常数量输入 */} + { + if (selectedRowId) { + onPickQtyChange(selectedRowId, lot.lotId, parseFloat(e.target.value) || 0); + } + }} + disabled={ + (lot.lotAvailability === 'expired' || + lot.lotAvailability === 'status_unavailable' || + lot.lotAvailability === 'rejected') || + selectedLotRowId !== `row_${index}` || + lot.stockOutLineStatus === 'completed' // ✅ 完成时禁用输入 + } + inputProps={{ + min: 0, + max: calculateRemainingRequiredQty(lot), + step: 0.01 + }} + sx={{ + width: '80px', + '& .MuiInputBase-input': { + fontSize: '0.75rem', + textAlign: 'center', + padding: '8px 4px' + } + }} + placeholder="0" + /> + + {/* ✅ 添加 Pick Form 按钮用于问题情况 */} + + + )} + + {/*{lot.availableQty.toLocaleString()}*/} + {calculateRemainingAvailableQty(lot).toLocaleString()} + + {/* + */} {/*} @@ -774,10 +794,12 @@ const LotTable: React.FC = ({ }} disabled={ - (lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') || + (lot.lotAvailability === 'expired' || + lot.lotAvailability === 'status_unavailable' || + lot.lotAvailability === 'rejected') || // ✅ 添加 rejected !pickQtyData[selectedRowId!]?.[lot.lotId] || - !lot.stockOutLineStatus || // Must have stock out line - !['pending','checked', 'partially_completed'].includes(lot.stockOutLineStatus.toLowerCase()) // Only these statuses + !lot.stockOutLineStatus || + !['pending','checked', 'partially_completed'].includes(lot.stockOutLineStatus.toLowerCase()) } // ✅ Allow submission for available AND insufficient_stock lots sx={{ @@ -838,6 +860,22 @@ const LotTable: React.FC = ({ lot={selectedLotForQr} onQrCodeSubmit={handleQrCodeSubmit} /> + + {/* ✅ Pick Execution Form Modal */} + {pickExecutionFormOpen && selectedLotForExecutionForm && selectedRow && ( + { + setPickExecutionFormOpen(false); + setSelectedLotForExecutionForm(null); + }} + onSubmit={handlePickExecutionFormSubmit} + selectedLot={selectedLotForExecutionForm} + selectedPickOrderLine={selectedRow} + pickOrderId={selectedRow.pickOrderId} + pickOrderCreateDate={new Date()} + /> + )} ); }; diff --git a/src/components/PickOrderSearch/PickExecution.tsx b/src/components/PickOrderSearch/PickExecution.tsx index 41503da..f002fd0 100644 --- a/src/components/PickOrderSearch/PickExecution.tsx +++ b/src/components/PickOrderSearch/PickExecution.tsx @@ -46,6 +46,7 @@ import { createStockOutLine, updateStockOutLineStatus, resuggestPickOrder, + checkAndCompletePickOrderByConsoCode, } from "@/app/api/pickOrder/actions"; import { EditNote } from "@mui/icons-material"; import { fetchNameList, NameList } from "@/app/api/user/actions"; @@ -69,9 +70,10 @@ import dayjs from "dayjs"; import { dummyQCData } from "../PoDetail/dummyQcTemplate"; import { CreateStockOutLine } from "@/app/api/pickOrder/actions"; import LotTable from './LotTable'; +import PickOrderDetailsTable from './PickOrderDetailsTable'; // ✅ Import the new component import { updateInventoryLotLineStatus, updateInventoryStatus, updateInventoryLotLineQuantities } from "@/app/api/inventory/actions"; -import { useSession } from "next-auth/react"; // ✅ Add session import -import { SessionWithTokens } from "@/config/authConfig"; // ✅ Add custom session type +import { useSession } from "next-auth/react"; +import { SessionWithTokens } from "@/config/authConfig"; interface Props { filterArgs: Record; @@ -85,11 +87,14 @@ interface LotPickData { 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'; + lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected'; stockOutLineId?: number; stockOutLineStatus?: string; stockOutLineQty?: number; @@ -104,9 +109,8 @@ interface PickQtyData { const PickExecution: React.FC = ({ filterArgs }) => { const { t } = useTranslation("pickOrder"); const router = useRouter(); - const { data: session } = useSession() as { data: SessionWithTokens | null }; // ✅ Add session + const { data: session } = useSession() as { data: SessionWithTokens | null }; - // ✅ Get current user ID from session with proper typing const currentUserId = session?.id ? parseInt(session.id) : undefined; const [filteredPickOrders, setFilteredPickOrders] = useState( @@ -141,11 +145,10 @@ const PickExecution: React.FC = ({ filterArgs }) => { } | null>(null); const [selectedLotForQc, setSelectedLotForQc] = useState(null); - // ✅ Add lot selection state variables const [selectedLotRowId, setSelectedLotRowId] = useState(null); const [selectedLotId, setSelectedLotId] = useState(null); - // 新增:分页控制器 + // ✅ Keep only the main table paging controller const [mainTablePagingController, setMainTablePagingController] = useState({ pageNum: 0, pageSize: 10, @@ -155,7 +158,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { pageSize: 10, }); - // Add missing search state variables const [searchQuery, setSearchQuery] = useState>({}); const [originalPickOrderData, setOriginalPickOrderData] = useState(null); @@ -199,6 +201,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { useEffect(() => { fetchNewPageConsoPickOrder({ limit: 10, offset: 0 }, filterArgs); }, [fetchNewPageConsoPickOrder, filterArgs]); + const handleUpdateStockOutLineStatus = useCallback(async ( stockOutLineId: number, status: string, @@ -217,16 +220,15 @@ const PickExecution: React.FC = ({ filterArgs }) => { if (result) { console.log("Stock out line status updated successfully:", result); - // Refresh lot data to show updated status if (selectedRowId) { handleRowSelect(selectedRowId); } } } catch (error) { console.error("Error updating stock out line status:", error); - } }, [selectedRowId]); + const isReleasable = useCallback((itemList: ByItemsSummary[]): boolean => { let isReleasable = true; for (const item of itemList) { @@ -260,26 +262,52 @@ const PickExecution: React.FC = ({ filterArgs }) => { const handleFetchAllPickOrderDetails = useCallback(async () => { setDetailLoading(true); try { - // ✅ Use current user ID for filtering const data = await fetchAllPickOrderDetails(currentUserId); - setPickOrderDetails(data); - setOriginalPickOrderData(data); // Store original data for filtering console.log("All Pick Order Details for user:", currentUserId, data); - const initialPickQtyData: PickQtyData = {}; - data.pickOrders.forEach((pickOrder: any) => { - pickOrder.pickOrderLines.forEach((line: any) => { - initialPickQtyData[line.id] = {}; + if (data && data.pickOrders) { + setPickOrderDetails(data); + setOriginalPickOrderData(data); + + const initialPickQtyData: PickQtyData = {}; + data.pickOrders.forEach((pickOrder: any) => { + pickOrder.pickOrderLines.forEach((line: any) => { + initialPickQtyData[line.id] = {}; + }); }); - }); - setPickQtyData(initialPickQtyData); + setPickQtyData(initialPickQtyData); + } else { + console.log("No pick order data returned"); + setPickOrderDetails({ + consoCode: null, + pickOrders: [], + items: [] + }); + setOriginalPickOrderData({ + consoCode: null, + pickOrders: [], + items: [] + }); + setPickQtyData({}); + } } catch (error) { console.error("Error fetching all pick order details:", error); + setPickOrderDetails({ + consoCode: null, + pickOrders: [], + items: [] + }); + setOriginalPickOrderData({ + consoCode: null, + pickOrders: [], + items: [] + }); + setPickQtyData({}); } finally { setDetailLoading(false); } - }, [currentUserId]); // ✅ Add currentUserId as dependency + }, [currentUserId]); useEffect(() => { handleFetchAllPickOrderDetails(); @@ -331,7 +359,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { const handlePickQtyChange = useCallback((lineId: number, lotId: number, value: number | string) => { console.log("Changing pick qty:", { lineId, lotId, value }); - // ✅ Handle both number and string values const numericValue = typeof value === 'string' ? (value === '' ? 0 : parseInt(value, 10)) : value; setPickQtyData(prev => { @@ -351,24 +378,28 @@ const PickExecution: React.FC = ({ filterArgs }) => { const qty = pickQtyData[lineId]?.[lotId] || 0; console.log(`提交拣货数量: Line ${lineId}, Lot ${lotId}, Qty ${qty}`); - // ✅ Find the stock out line for this lot const selectedLot = lotData.find(lot => lot.lotId === lotId); if (!selectedLot?.stockOutLineId) { return; } try { - // ✅ Only two statuses: partially_completed or completed - let newStatus = 'partially_completed'; // Default status + // ✅ FIXED: 计算累计拣货数量 + const totalPickedForThisLot = (selectedLot.actualPickQty || 0) + qty; + console.log(" DEBUG - Previous picked:", selectedLot.actualPickQty || 0); + console.log("🔍 DEBUG - Current submit:", qty); + console.log("🔍 DEBUG - Total picked:", totalPickedForThisLot); + console.log("�� DEBUG - Required qty:", selectedLot.requiredQty); - if (qty >= selectedLot.requiredQty) { - newStatus = 'completed'; // Full quantity picked + // ✅ FIXED: 状态应该基于累计拣货数量 + let newStatus = 'partially_completed'; + if (totalPickedForThisLot >= selectedLot.requiredQty) { + newStatus = 'completed'; } - // If qty < requiredQty, stays as 'partially_completed' - // ✅ Function 1: Update stock out line with new status and quantity + console.log("�� DEBUG - Calculated status:", newStatus); + try { - // ✅ Function 1: Update stock out line with new status and quantity const stockOutLineUpdate = await updateStockOutLineStatus({ id: selectedLot.stockOutLineId, status: newStatus, @@ -379,10 +410,9 @@ const PickExecution: React.FC = ({ filterArgs }) => { } catch (error) { console.error("❌ Error updating stock out line:", error); - return; // Stop execution if this fails + return; } - // ✅ Function 2: Update inventory lot line (balance hold_qty and out_qty) if (qty > 0) { const inventoryLotLineUpdate = await updateInventoryLotLineQuantities({ inventoryLotLineId: lotId, @@ -394,26 +424,74 @@ const PickExecution: React.FC = ({ filterArgs }) => { console.log("Inventory lot line updated:", inventoryLotLineUpdate); } - // ✅ Function 3: Handle inventory table onhold if needed + // ✅ RE-ENABLE: Check if pick order should be completed if (newStatus === 'completed') { - // All required quantity picked - might need to update inventory status - // Note: We'll handle inventory update in a separate function or after selectedRow is available - console.log("Completed status - inventory update needed but selectedRow not available yet"); + console.log("✅ Stock out line completed, checking if entire pick order is complete..."); + + // ✅ 添加调试日志来查看所有 pick orders 的 consoCode + console.log("📋 DEBUG - All pick orders and their consoCodes:"); + if (pickOrderDetails) { + pickOrderDetails.pickOrders.forEach((pickOrder, index) => { + console.log(` Pick Order ${index + 1}: ID=${pickOrder.id}, Code=${pickOrder.code}, ConsoCode=${pickOrder.consoCode}`); + }); + } + + // ✅ FIXED: 直接查找 consoCode,不依赖 selectedRow + if (pickOrderDetails) { + let currentConsoCode: string | null = null; + + // 找到当前选中行所属的 pick order + for (const pickOrder of pickOrderDetails.pickOrders) { + const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId); + if (foundLine) { + // ✅ 直接使用 pickOrder.code 作为 consoCode + currentConsoCode = pickOrder.consoCode; + console.log(`�� DEBUG - Found consoCode for line ${selectedRowId}: ${currentConsoCode} (from pick order ${pickOrder.id})`); + break; + } + } + + if (currentConsoCode) { + try { + console.log(`🔍 Checking completion for consoCode: ${currentConsoCode}`); + const completionResponse = await checkAndCompletePickOrderByConsoCode(currentConsoCode); + + console.log("�� Completion response:", completionResponse); + + if (completionResponse.message === "completed") { + console.log("🎉 Pick order completed successfully!"); + + await handleFetchAllPickOrderDetails(); + // 刷新当前选中的行数据 + if (selectedRowId) { + await handleRowSelect(selectedRowId, true); + } + + } 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); + } + } else { + console.warn("⚠️ No consoCode found for current pick order, cannot check completion"); + } + } } console.log("All updates completed successfully"); - // ✅ Refresh lot data to show updated quantities if (selectedRowId) { await handleRowSelect(selectedRowId, true); - // Note: We'll handle refresh after the function is properly defined console.log("Data refresh needed but handleRowSelect not available yet"); } await handleFetchAllPickOrderDetails(); } catch (error) { console.error("Error updating pick quantity:", error); } - }, [pickQtyData, lotData, selectedRowId]); + }, [pickQtyData, lotData, selectedRowId, pickOrderDetails, handleFetchAllPickOrderDetails]); const getTotalPickedQty = useCallback((lineId: number) => { const lineData = pickQtyData[lineId]; @@ -421,30 +499,22 @@ const PickExecution: React.FC = ({ filterArgs }) => { return Object.values(lineData).reduce((sum, qty) => sum + qty, 0); }, [pickQtyData]); - - const handleQcCheck = useCallback(async (line: GetPickOrderLineInfo, pickOrderCode: string) => { - // ✅ Get the selected lot for QC if (!selectedLotId) { - return; } const selectedLot = lotData.find(lot => lot.lotId === selectedLotId); if (!selectedLot) { - //alert("Selected lot not found in lot data"); return; } - // ✅ Check if stock out line exists if (!selectedLot.stockOutLineId) { - //alert("Please create a stock out line first before performing QC check"); return; } setSelectedLotForQc(selectedLot); - // ✅ ALWAYS use dummy data for consistent behavior const transformedDummyData = dummyQCData.map(item => ({ id: item.id, code: item.code, @@ -453,7 +523,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { lowerLimit: undefined, upperLimit: undefined, description: item.qcDescription, - // ✅ Always reset QC result properties to undefined for fresh start qcPassed: undefined, failQty: undefined, remarks: undefined @@ -461,7 +530,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { setQcItems(transformedDummyData as QcItemWithChecks[]); - // ✅ Get existing QC results if any (for display purposes only) let qcResult: any[] = []; try { const rawQcResult = await fetchPickOrderQcResult(line.id); @@ -491,7 +559,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { setSelectedItemForQc(item); }, []); - // 新增:处理分页变化 + // ✅ Main table pagination handlers const handleMainTablePageChange = useCallback((event: unknown, newPage: number) => { setMainTablePagingController(prev => ({ ...prev, @@ -522,31 +590,27 @@ const PickExecution: React.FC = ({ filterArgs }) => { }); }, []); - // ✅ Fix lot selection logic const handleLotSelection = useCallback((uniqueLotId: string, lotId: number) => { console.log("=== DEBUG: Lot Selection ==="); console.log("uniqueLotId:", uniqueLotId); console.log("lotId (inventory lot line ID):", lotId); - // Find the selected lot data const selectedLot = lotData.find(lot => lot.lotId === lotId); console.log("Selected lot data:", selectedLot); - // If clicking the same lot, unselect it + if (selectedLotRowId === uniqueLotId) { setSelectedLotRowId(null); setSelectedLotId(null); } else { - // Select the new lot setSelectedLotRowId(uniqueLotId); setSelectedLotId(lotId); + } }, [selectedLotRowId]); - // ✅ Add function to handle row selection that resets lot selection const handleRowSelect = useCallback(async (lineId: number, preserveLotSelection: boolean = false) => { setSelectedRowId(lineId); - // ✅ Only reset lot selection if not preserving if (!preserveLotSelection) { setSelectedLotRowId(null); setSelectedLotId(null); @@ -557,13 +621,16 @@ const PickExecution: React.FC = ({ filterArgs }) => { console.log("Lot details from API:", lotDetails); const realLotData: LotPickData[] = lotDetails.map((lot: any) => ({ - id: lot.id, // This should be the unique row ID for the table - lotId: lot.lotId, // This is the inventory lot line ID + id: lot.id, + lotId: lot.lotId, lotNo: lot.lotNo, expiryDate: lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A', location: lot.location, stockUnit: lot.stockUnit, inQty: lot.inQty, + outQty: lot.outQty, + holdQty: lot.holdQty, + totalPickedByAllPickOrders: lot.totalPickedByAllPickOrders, availableQty: lot.availableQty, requiredQty: lot.requiredQty, actualPickQty: lot.actualPickQty || 0, @@ -581,33 +648,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { } }, []); - const prepareMainTableData = useMemo(() => { - if (!pickOrderDetails) return []; - - return pickOrderDetails.pickOrders.flatMap((pickOrder) => - pickOrder.pickOrderLines.map((line) => { - // 修复:处理 availableQty 可能为 null 的情况 - const availableQty = line.availableQty ?? 0; - const balanceToPick = availableQty - line.requiredQty; - - // ✅ 使用 dayjs 进行一致的日期格式化 - const formattedTargetDate = pickOrder.targetDate - ? dayjs(pickOrder.targetDate).format('YYYY-MM-DD') - : 'N/A'; - - return { - ...line, - pickOrderCode: pickOrder.code, - targetDate: formattedTargetDate, // ✅ 使用 dayjs 格式化的日期 - balanceToPick: balanceToPick, - pickedQty: line.pickedQty, - // 确保 availableQty 不为 null - availableQty: availableQty, - }; - }) - ); -}, [pickOrderDetails]); - const prepareLotTableData = useMemo(() => { return lotData.map((lot) => ({ ...lot, @@ -615,13 +655,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { })); }, [lotData]); - // 新增:分页数据 - const paginatedMainTableData = useMemo(() => { - const startIndex = mainTablePagingController.pageNum * mainTablePagingController.pageSize; - const endIndex = startIndex + mainTablePagingController.pageSize; - return prepareMainTableData.slice(startIndex, endIndex); - }, [prepareMainTableData, mainTablePagingController]); - const paginatedLotTableData = useMemo(() => { const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize; const endIndex = startIndex + lotTablePagingController.pageSize; @@ -634,11 +667,13 @@ const PickExecution: React.FC = ({ filterArgs }) => { for (const pickOrder of pickOrderDetails.pickOrders) { const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId); if (foundLine) { - return { ...foundLine, pickOrderCode: pickOrder.code }; + return { ...foundLine, pickOrderCode: pickOrder.code, + pickOrderId: pickOrder.id }; } } return null; }, [selectedRowId, pickOrderDetails]); + const handleInventoryUpdate = useCallback(async (itemId: number, lotId: number, qty: number) => { try { const inventoryUpdate = await updateInventoryStatus({ @@ -653,16 +688,17 @@ const PickExecution: React.FC = ({ filterArgs }) => { console.error("Error updating inventory status:", error); } }, []); + const handleLotDataRefresh = useCallback(async () => { if (selectedRowId) { try { - await handleRowSelect(selectedRowId, true); // Preserve lot selection + await handleRowSelect(selectedRowId, true); } catch (error) { console.error("Error refreshing lot data:", error); } } }, [selectedRowId, handleRowSelect]); - // ✅ Add this function after handleRowSelect is defined + const handleDataRefresh = useCallback(async () => { if (selectedRowId) { try { @@ -672,15 +708,14 @@ const PickExecution: React.FC = ({ filterArgs }) => { } } }, [selectedRowId, handleRowSelect]); + const handleInsufficientStock = useCallback(async () => { console.log("Insufficient stock - testing resuggest API"); if (!selectedRowId || !pickOrderDetails) { - // alert("Please select a pick order line first"); return; } - // Find the pick order ID from the selected row let pickOrderId: number | null = null; for (const pickOrder of pickOrderDetails.pickOrders) { const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId); @@ -691,55 +726,41 @@ const PickExecution: React.FC = ({ filterArgs }) => { } if (!pickOrderId) { - // alert("Could not find pick order ID for selected line"); return; } try { console.log(`Calling resuggest API for pick order ID: ${pickOrderId}`); - // Call the resuggest API const result = await resuggestPickOrder(pickOrderId); console.log("Resuggest API result:", result); if (result.code === "SUCCESS") { - //alert(`✅ Resuggest successful!\n\nMessage: ${result.message}\n\nRemoved: ${result.message?.includes('Removed') ? 'Yes' : 'No'}\nCreated: ${result.message?.includes('created') ? 'Yes' : 'No'}`); - - // Refresh the lot data to show the new suggestions if (selectedRowId) { await handleRowSelect(selectedRowId); } - // Also refresh the main pick order details await handleFetchAllPickOrderDetails(); - - } else { - //alert(`❌ Resuggest failed!\n\nError: ${result.message}`); } } catch (error) { console.error("Error calling resuggest API:", error); - //alert(`❌ Error calling resuggest API:\n\n${error instanceof Error ? error.message : 'Unknown error'}`); } }, [selectedRowId, pickOrderDetails, handleRowSelect, handleFetchAllPickOrderDetails]); - // Add this function (around line 350) const hasSelectedLots = useCallback((lineId: number) => { return selectedLotRowId !== null; }, [selectedLotRowId]); - // Add state for showing input body const [showInputBody, setShowInputBody] = useState(false); const [selectedLotForInput, setSelectedLotForInput] = useState(null); - // Add function to handle lot selection for input body display const handleLotSelectForInput = useCallback((lot: LotPickData) => { setSelectedLotForInput(lot); setShowInputBody(true); }, []); - // Add function to generate input body const generateInputBody = useCallback((): CreateStockOutLine | null => { if (!selectedLotForInput || !selectedRowId || !selectedRow || !pickOrderDetails?.consoCode) { return null; @@ -753,9 +774,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { }; }, [selectedLotForInput, selectedRowId, selectedRow, pickOrderDetails?.consoCode]); - - - // Add function to handle create stock out line const handleCreateStockOutLine = useCallback(async (inventoryLotLineId: number) => { if (!selectedRowId || !pickOrderDetails?.consoCode) { console.error("Missing required data for creating stock out line."); @@ -763,12 +781,21 @@ const PickExecution: React.FC = ({ filterArgs }) => { } try { - // ✅ Store current lot selection before refresh const currentSelectedLotRowId = selectedLotRowId; const currentSelectedLotId = selectedLotId; - + let correctConsoCode: string | null = null; + if (pickOrderDetails && selectedRowId) { + for (const pickOrder of pickOrderDetails.pickOrders) { + const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId); + if (foundLine) { + correctConsoCode = pickOrder.consoCode; + console.log(`🔍 Found consoCode for line ${selectedRowId}: ${correctConsoCode} (from pick order ${pickOrder.id})`); + break; + } + } + } const stockOutLineData: CreateStockOutLine = { - consoCode: pickOrderDetails.consoCode, + consoCode: correctConsoCode || pickOrderDetails?.consoCode || "", // ✅ 使用正确的 consoCode pickOrderLineId: selectedRowId, inventoryLotLineId: inventoryLotLineId, qty: 0.0 @@ -777,7 +804,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { console.log("=== STOCK OUT LINE CREATION DEBUG ==="); console.log("Input Body:", JSON.stringify(stockOutLineData, null, 2)); - // ✅ Use the correct API function const result = await createStockOutLine(stockOutLineData); console.log("Stock Out Line created:", result); @@ -785,16 +811,13 @@ const PickExecution: React.FC = ({ filterArgs }) => { if (result) { console.log("Stock out line created successfully:", result); - // ✅ Auto-refresh data after successful creation console.log("🔄 Refreshing data after stock out line creation..."); try { - // ✅ Refresh lot data for the selected row (maintains selection) if (selectedRowId) { - await handleRowSelect(selectedRowId, true); // ✅ Preserve lot selection + await handleRowSelect(selectedRowId, true); } - // ✅ Refresh main pick order details await handleFetchAllPickOrderDetails(); console.log("✅ Data refresh completed - lot selection maintained!"); @@ -802,7 +825,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { console.error("❌ Error refreshing data:", refreshError); } - setShowInputBody(false); // Hide preview after successful creation + setShowInputBody(false); } else { console.error("Failed to create stock out line: No response"); } @@ -811,22 +834,17 @@ const PickExecution: React.FC = ({ filterArgs }) => { } }, [selectedRowId, pickOrderDetails?.consoCode, handleRowSelect, handleFetchAllPickOrderDetails, selectedLotRowId, selectedLotId]); - // ✅ New function to refresh data while preserving lot selection const handleRefreshDataPreserveSelection = useCallback(async () => { if (!selectedRowId) return; - // ✅ Store current lot selection const currentSelectedLotRowId = selectedLotRowId; const currentSelectedLotId = selectedLotId; try { - // ✅ Refresh lot data - await handleRowSelect(selectedRowId, true); // ✅ Preserve selection + await handleRowSelect(selectedRowId, true); - // ✅ Refresh main pick order details await handleFetchAllPickOrderDetails(); - // ✅ Restore lot selection setSelectedLotRowId(currentSelectedLotRowId); setSelectedLotId(currentSelectedLotId); @@ -836,106 +854,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { } }, [selectedRowId, selectedLotRowId, selectedLotId, handleRowSelect, handleFetchAllPickOrderDetails]); - // 自定义主表格组件 - const CustomMainTable = () => { - return ( - <> - - - - - {t("Selected")} - {t("Pick Order Code")} - {t("Item Code")} - {t("Item Name")} - {t("Order Quantity")} - {t("Current Stock")} - {t("Qty Already Picked")} - {t("Stock Unit")} - {t("Target Date")} - - - - {paginatedMainTableData.length === 0 ? ( - - - - {t("No data available")} - - - - ) : ( - paginatedMainTableData.map((line) => { - // 修复:处理 availableQty 可能为 null 的情况,并确保负值显示为 0 - const availableQty = line.availableQty ?? 0; - const balanceToPick = Math.max(0, availableQty - line.requiredQty); // 确保不为负数 - const totalPickedQty = getTotalPickedQty(line.id); - const actualPickedQty = line.pickedQty ?? 0; - return ( - *": { borderBottom: "unset" }, - color: "black", - backgroundColor: selectedRowId === line.id ? "action.selected" : "inherit", - cursor: "pointer", - "&:hover": { - backgroundColor: "action.hover", - }, - }} - > - - { - if (e.target.checked) { - handleRowSelect(line.id); - } else { - setSelectedRowId(null); - setLotData([]); - } - }} - onClick={(e) => e.stopPropagation()} - /> - - {line.pickOrderCode} - {line.itemCode} - {line.itemName} - {line.requiredQty} - = line.requiredQty ? 'success.main' : 'error.main', - }}> - {availableQty.toLocaleString()} {/* 添加千位分隔符 */} - - {actualPickedQty} - {line.uomDesc} - {line.targetDate} - - ); - }) - )} - -
-
- - - `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` - } - /> - - ); - }; - - // Add search criteria + // ✅ Search criteria const searchCriteria: Criterion[] = useMemo( () => [ { @@ -963,7 +882,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { [t], ); - // Add search handler + // ✅ Search handler const handleSearch = useCallback((query: Record) => { setSearchQuery({ ...query }); console.log("Search query:", query); @@ -971,7 +890,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { if (!originalPickOrderData) return; const filtered = originalPickOrderData.pickOrders.filter((pickOrder) => { - // Check if any line in this pick order matches the search criteria return pickOrder.pickOrderLines.some((line) => { const itemCodeMatch = !query.itemCode || line.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); @@ -982,12 +900,10 @@ const PickExecution: React.FC = ({ filterArgs }) => { const pickOrderCodeMatch = !query.pickOrderCode || pickOrder.code?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); - - return itemCodeMatch && itemNameMatch && pickOrderCodeMatch ; + return itemCodeMatch && itemNameMatch && pickOrderCodeMatch; }); }); - // Create filtered data structure const filteredData: GetPickOrderInfoResponse = { ...originalPickOrderData, pickOrders: filtered @@ -997,7 +913,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { console.log("Filtered pick orders count:", filtered.length); }, [originalPickOrderData, t]); - // Add reset handler + // ✅ Reset handler const handleReset = useCallback(() => { setSearchQuery({}); if (originalPickOrderData) { @@ -1005,7 +921,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { } }, [originalPickOrderData]); - // Add this to debug the lot data + // ✅ Debug the lot data useEffect(() => { console.log("Lot data:", lotData); console.log("Pick Qty Data:", pickQtyData); @@ -1023,56 +939,55 @@ const PickExecution: React.FC = ({ filterArgs }) => { />
- {/* 主表格 */} + {/* ✅ Main table using the new component */} {t("Pick Order Details")} - {detailLoading ? ( - - - - ) : pickOrderDetails ? ( - - ) : ( - - - {t("Loading data...")} - - - )} + - {/* 批次表格 - 放在主表格下方 */} + {/* Lot table - below main table */} {selectedRow && ( {t("Item lot to be Pick:")} {selectedRow.pickOrderCode} - {selectedRow.itemName} - {/* 检查是否有可用的批次数据 */} {lotData.length > 0 ? ( + lotData={lotData} + selectedRowId={selectedRowId} + selectedRow={selectedRow} + pickQtyData={pickQtyData} + selectedLotRowId={selectedLotRowId} + selectedLotId={selectedLotId} + onLotSelection={handleLotSelection} + onPickQtyChange={handlePickQtyChange} + onSubmitPickQty={handleSubmitPickQty} + onCreateStockOutLine={handleCreateStockOutLine} + onQcCheck={handleQcCheck} + onDataRefresh={handleFetchAllPickOrderDetails} + onLotDataRefresh={handleLotDataRefresh} + onLotSelectForInput={handleLotSelectForInput} + showInputBody={showInputBody} + setShowInputBody={setShowInputBody} + selectedLotForInput={selectedLotForInput} + generateInputBody={generateInputBody} + // ✅ Add missing props + totalPickedByAllPickOrders={0} // You can calculate this from lotData if needed + outQty={0} // You can calculate this from lotData if needed + holdQty={0} // You can calculate this from lotData if needed + /> ) : ( void; + onSubmit: (data: PickExecutionIssueData) => Promise; + 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; + // selectedRowId?: number | null; +} + +// 定义错误类型 +interface FormErrors { + actualPickQty?: string; + missQty?: string; + badItemQty?: string; + issueRemark?: string; + handledBy?: string; +} + +const PickExecutionForm: React.FC = ({ + open, + onClose, + onSubmit, + selectedLot, + selectedPickOrderLine, + pickOrderId, + pickOrderCreateDate, + // ✅ Remove these props + // onNormalPickSubmit, + // selectedRowId, +}) => { + const { t } = useTranslation(); + const [formData, setFormData] = useState>({}); + const [errors, setErrors] = useState({}); + const [loading, setLoading] = useState(false); + const [handlers, setHandlers] = useState>([]); + + // 计算剩余可用数量 + const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { + const remainingQty = lot.inQty - lot.outQty; + return Math.max(0, remainingQty); + }, []); +const calculateRequiredQty = useCallback((lot: LotPickData) => { + const requiredQty = lot.requiredQty-(lot.actualPickQty||0); + return Math.max(0, requiredQty); +}, []); + // 获取处理人员列表 + 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]; + } + }; + + // 计算剩余可用数量 + const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); + const requiredQty = calculateRequiredQty(selectedLot); + console.log("=== PickExecutionForm Debug ==="); + console.log("selectedLot:", selectedLot); + console.log("inQty:", selectedLot.inQty); + console.log("outQty:", selectedLot.outQty); + console.log("holdQty:", selectedLot.holdQty); + console.log("availableQty:", selectedLot.availableQty); + console.log("calculated remainingAvailableQty:", remainingAvailableQty); + 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: selectedLot.actualPickQty || 0, + missQty: 0, + badItemQty: 0, // 初始化为 0,用户需要手动输入 + issueRemark: '', + pickerName: '', + handledBy: undefined, + }); + } + }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]); + + 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 = {}; + + if (formData.actualPickQty === undefined || formData.actualPickQty < 0) { + newErrors.actualPickQty = t('pickOrder.validation.actualPickQtyRequired'); + } + + // ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported) + const hasMissQty = formData.missQty && formData.missQty > 0; + const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0; + + if (!hasMissQty && !hasBadItemQty) { + newErrors.missQty = t('pickOrder.validation.mustReportMissOrBadItems'); + newErrors.badItemQty = t('pickOrder.validation.mustReportMissOrBadItems'); + } + + if (formData.missQty && formData.missQty < 0) { + newErrors.missQty = t('pickOrder.validation.missQtyInvalid'); + } + + if (formData.badItemQty && formData.badItemQty < 0) { + newErrors.badItemQty = t('pickOrder.validation.badItemQtyInvalid'); + } + + if (formData.badItemQty && formData.badItemQty > 0 && !formData.issueRemark) { + newErrors.issueRemark = t('pickOrder.validation.issueRemarkRequired'); + } + + if (formData.badItemQty && formData.badItemQty > 0 && !formData.handledBy) { + newErrors.handledBy = t('pickOrder.validation.handlerRequired'); + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + if (!validateForm() || !formData.pickOrderId) { + return; + } + + setLoading(true); + try { + await onSubmit(formData as PickExecutionIssueData); + onClose(); + } catch (error) { + console.error('Error submitting pick execution issue:', error); + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + setFormData({}); + setErrors({}); + onClose(); + }; + + if (!selectedLot || !selectedPickOrderLine) { + return null; + } + + const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); + const requiredQty = calculateRequiredQty(selectedLot); + + return ( + + + {t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */} + + + + {/* ✅ Add instruction text */} + + + + + {t('Note:')} {t('This form is for reporting issues only. You must report either missing items or bad items.')} + + + + + {/* ✅ Keep the existing form fields */} + + + + + + + + + + handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)} + error={!!errors.actualPickQty} + helperText={errors.actualPickQty || t('Enter the quantity actually picked')} + variant="outlined" + /> + + + + handleInputChange('missQty', parseFloat(e.target.value) || 0)} + error={!!errors.missQty} + helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')} + variant="outlined" + /> + + + + handleInputChange('badItemQty', parseFloat(e.target.value) || 0)} + error={!!errors.badItemQty} + 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) && ( + <> + + handleInputChange('issueRemark', e.target.value)} + error={!!errors.issueRemark} + helperText={errors.issueRemark} + placeholder={t('Describe the issue with bad items')} + variant="outlined" + /> + + + + + {t('handler')} + + {errors.handledBy && ( + + {errors.handledBy} + + )} + + + + )} + + + + + + + + + ); +}; + +export default PickExecutionForm; \ No newline at end of file diff --git a/src/components/PickOrderSearch/PickOrderDetailsTable.tsx b/src/components/PickOrderSearch/PickOrderDetailsTable.tsx new file mode 100644 index 0000000..0c7d8a7 --- /dev/null +++ b/src/components/PickOrderSearch/PickOrderDetailsTable.tsx @@ -0,0 +1,196 @@ +"use client"; + +import { + Box, + Checkbox, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TablePagination, + Typography, + Paper, +} from "@mui/material"; +import { useMemo, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { GetPickOrderInfoResponse, GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; +import dayjs from "dayjs"; + +interface PickOrderDetailsTableProps { + pickOrderDetails: GetPickOrderInfoResponse | null; + detailLoading: boolean; + selectedRowId: number | null; + onRowSelect: (lineId: number) => void; + onPageChange: (event: unknown, newPage: number) => void; + onPageSizeChange: (event: React.ChangeEvent) => void; + pageNum: number; + pageSize: number; +} + +const PickOrderDetailsTable: React.FC = ({ + pickOrderDetails, + detailLoading, + selectedRowId, + onRowSelect, + onPageChange, + onPageSizeChange, + pageNum, + pageSize, +}) => { + const { t } = useTranslation("pickOrder"); + + const prepareMainTableData = useMemo(() => { + if (!pickOrderDetails) return []; + + return pickOrderDetails.pickOrders.flatMap((pickOrder) => + pickOrder.pickOrderLines.map((line) => { + const availableQty = line.availableQty ?? 0; + const balanceToPick = availableQty - line.requiredQty; + + // ✅ Handle both string and array date formats from the optimized API + let formattedTargetDate = 'N/A'; + if (pickOrder.targetDate) { + if (typeof pickOrder.targetDate === 'string') { + formattedTargetDate = dayjs(pickOrder.targetDate).format('YYYY-MM-DD'); + } else if (Array.isArray(pickOrder.targetDate)) { + // Handle array format [2025, 9, 29, 0, 0] from optimized API + const [year, month, day] = pickOrder.targetDate; + formattedTargetDate = dayjs(`${year}-${month}-${day}`).format('YYYY-MM-DD'); + } + } + + return { + ...line, + pickOrderCode: pickOrder.code, + targetDate: formattedTargetDate, + balanceToPick: balanceToPick, + pickedQty: line.pickedQty, // ✅ This now comes from the optimized API + availableQty: availableQty, + }; + }) + ); + }, [pickOrderDetails]); + + // ✅ Paginated data + const paginatedMainTableData = useMemo(() => { + const startIndex = pageNum * pageSize; + const endIndex = startIndex + pageSize; + return prepareMainTableData.slice(startIndex, endIndex); + }, [prepareMainTableData, pageNum, pageSize]); + + if (detailLoading) { + return ( + + + {t("Loading data...")} + + + ); + } + + if (!pickOrderDetails) { + return ( + + + {t("No data available")} + + + ); + } + + return ( + <> + + + + + {t("Selected")} + {t("Pick Order Code")} + {t("Item Code")} + {t("Item Name")} + {t("Order Quantity")} + {t("Current Stock")} + {t("Qty Already Picked")} + {t("Stock Unit")} + {t("Target Date")} + + + + {paginatedMainTableData.length === 0 ? ( + + + + {t("No data available")} + + + + ) : ( + paginatedMainTableData.map((line) => { + const availableQty = line.availableQty ?? 0; + const balanceToPick = Math.max(0, availableQty - line.requiredQty); + const actualPickedQty = line.pickedQty ?? 0; + + return ( + *": { borderBottom: "unset" }, + color: "black", + backgroundColor: selectedRowId === line.id ? "action.selected" : "inherit", + cursor: "pointer", + "&:hover": { + backgroundColor: "action.hover", + }, + }} + > + + { + if (e.target.checked) { + onRowSelect(line.id); + } + }} + onClick={(e) => e.stopPropagation()} + /> + + {line.pickOrderCode} + {line.itemCode} + {line.itemName} + {line.requiredQty} + = line.requiredQty ? 'success.main' : 'error.main', + }}> + {availableQty.toLocaleString()} + + {actualPickedQty} + {line.uomDesc} + {line.targetDate} + + ); + }) + )} + +
+
+ + + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } + /> + + ); +}; + +export default PickOrderDetailsTable; \ No newline at end of file diff --git a/src/components/PickOrderSearch/PickQcStockInModalVer3.tsx b/src/components/PickOrderSearch/PickQcStockInModalVer3.tsx index 6857bfb..f3d8d25 100644 --- a/src/components/PickOrderSearch/PickQcStockInModalVer3.tsx +++ b/src/components/PickOrderSearch/PickQcStockInModalVer3.tsx @@ -120,7 +120,7 @@ interface LotPickData { requiredQty: number; actualPickQty: number; lotStatus: string; - lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; + lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected'; stockOutLineId?: number; stockOutLineStatus?: string; stockOutLineQty?: number; diff --git a/src/components/PickOrderSearch/newcreatitem.tsx b/src/components/PickOrderSearch/newcreatitem.tsx index 682bb90..17eb9bc 100644 --- a/src/components/PickOrderSearch/newcreatitem.tsx +++ b/src/components/PickOrderSearch/newcreatitem.tsx @@ -687,7 +687,7 @@ const handleQtyBlur = useCallback((itemId: number) => { formProps.reset(); setHasSearched(false); setFilteredItems([]); - alert(t("All pick orders created successfully")); + // alert(t("All pick orders created successfully")); // 通知父组件切换到 Assign & Release 标签页 if (onPickOrderCreated) { diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index 93be626..cd6963e 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -175,7 +175,7 @@ "Pick Order Details": "提料單詳情", "Partial quantity submitted. Please submit more or complete the order.": "已提料部分數量。請提交更多或完成訂單。", "Pick order completed successfully!": "提料單完成成功!", - "QC check failed. Lot has been rejected and marked as unavailable.": "QC 檢查失敗。批號已拒絕並標記為不可用。", + "Lot has been rejected and marked as unavailable.": "批號已拒絕並標記為不可用。", "This order is insufficient, please pick another lot.": "此訂單不足,請選擇其他批號。", "Please finish QR code scan, QC check and pick order.": "請完成 QR 碼掃描、QC 檢查和提料。", "No data available": "沒有資料",