"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, 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; lotNo: string; expiryDate: string; location: string; stockUnit: string; inQty: number; availableQty: number; requiredQty: number; actualPickQty: number; lotStatus: string; outQty: number; holdQty: number; totalPickedByAllPickOrders: number; lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable' | 'rejected'; // 添加 'rejected' stockOutLineId?: number; stockOutLineStatus?: string; stockOutLineQty?: number; } interface PickQtyData { [lineId: number]: { [lotId: number]: number; }; } interface LotTableProps { lotData: LotPickData[]; selectedRowId: number | null; selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string; pickOrderId: number }) | null; // 添加 pickOrderId 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; totalPickedByAllPickOrders: number; outQty: number; holdQty: number; setShowInputBody: (show: boolean) => void; selectedLotForInput: LotPickData | null; generateInputBody: () => any; onDataRefresh: () => Promise; onLotDataRefresh: () => 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(''); const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({}); // Add state to track manual input submission 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); // Add state to track processed QR codes to prevent re-processing const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); // Add state to store the scanned QR result const [scannedQrResult, setScannedQrResult] = useState(''); // Process scanned QR codes with new format useEffect(() => { if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) { const latestQr = qrValues[qrValues.length - 1]; // Check if this QR code has already been processed if (processedQrCodes.has(latestQr)) { console.log("QR code already processed, skipping..."); return; } // Add to processed set immediately to prevent re-processing setProcessedQrCodes(prev => new Set(prev).add(latestQr)); try { // Parse QR code as JSON const qrData = JSON.parse(latestQr); // Check if it has the expected structure if (qrData.stockInLineId && qrData.itemId) { setIsProcessingQr(true); setQrScanFailed(false); // Fetch stock in line info to get lotNo fetchStockInLineInfo(qrData.stockInLineId) .then((stockInLineInfo) => { console.log("Stock in line info:", stockInLineInfo); // Store the scanned result for display setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number'); // Compare lotNo from API with expected lotNo 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); // DON'T stop scanning - allow new QR codes to be processed } }) .catch((error) => { console.error("Error fetching stock in line info:", error); setScannedQrResult('Error fetching data'); setQrScanFailed(true); setManualInputError(true); setManualInputSubmitted(true); // DON'T stop scanning - allow new QR codes to be processed }) .finally(() => { setIsProcessingQr(false); }); } else { // Fallback to old format (direct lotNo comparison) const qrContent = latestQr.replace(/[{}]/g, ''); // Store the scanned result for display setScannedQrResult(qrContent); if (qrContent === lot.lotNo) { setQrScanSuccess(true); onQrCodeSubmit(lot.lotNo); onClose(); resetScan(); } else { setQrScanFailed(true); setManualInputError(true); setManualInputSubmitted(true); // DON'T stop scanning - allow new QR codes to be processed } } } catch (error) { // If JSON parsing fails, fallback to old format console.log("QR code is not JSON format, trying direct comparison"); const qrContent = latestQr.replace(/[{}]/g, ''); // Store the scanned result for display setScannedQrResult(qrContent); if (qrContent === lot.lotNo) { setQrScanSuccess(true); onQrCodeSubmit(lot.lotNo); onClose(); resetScan(); } else { setQrScanFailed(true); setManualInputError(true); setManualInputSubmitted(true); // DON'T stop scanning - allow new QR codes to be processed } } } }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes, stopScan]); // Clear states when modal opens or lot changes useEffect(() => { if (open) { setManualInput(''); setManualInputSubmitted(false); setManualInputError(false); setIsProcessingQr(false); setQrScanFailed(false); setQrScanSuccess(false); setScannedQrResult(''); // Clear scanned result // Clear processed QR codes when modal opens setProcessedQrCodes(new Set()); } }, [open]); useEffect(() => { if (lot) { setManualInput(''); setManualInputSubmitted(false); setManualInputError(false); setIsProcessingQr(false); setQrScanFailed(false); setQrScanSuccess(false); setScannedQrResult(''); // Clear scanned result // Clear processed QR codes when lot changes setProcessedQrCodes(new Set()); } }, [lot]); // Auto-submit manual input when it matches (but only if QR scan hasn't failed) 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]); // Add the missing handleManualSubmit function const handleManualSubmit = () => { if (manualInput.trim() === lot?.lotNo) { setQrScanSuccess(true); onQrCodeSubmit(lot.lotNo); onClose(); setManualInput(''); } else { setQrScanFailed(true); setManualInputError(true); setManualInputSubmitted(true); } }; // Add function to restart scanning after manual input error const handleRestartScan = () => { setQrScanFailed(false); setManualInputError(false); setManualInputSubmitted(false); setProcessedQrCodes(new Set()); // Clear processed QR codes startScan(); // Restart scanning }; useEffect(() => { if (open) { startScan(); } }, [open, startScan]); return ( {t("QR Code Scan for Lot")}: {lot?.lotNo} {/* Show processing status */} {isProcessingQr && ( {t("Processing QR code...")} )} {/* Manual Input with Submit-Triggered Helper Text */} {false &&( {t("Manual Input")}: { setManualInput(e.target.value); // Reset error states when user starts typing 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.")}` : '' } /> )} {/* Show QR Scan Status */} {qrValues.length > 0 && ( {t("QR Scan Result:")} {scannedQrResult} {qrScanSuccess && ( {t("Verified successfully!")} )} )} ); }; const LotTable: React.FC = ({ lotData, selectedRowId, selectedRow, pickQtyData, selectedLotRowId, selectedLotId, onLotSelection, onPickQtyChange, onSubmitPickQty, onCreateStockOutLine, onQcCheck, onLotSelectForInput, showInputBody, setShowInputBody, selectedLotForInput, generateInputBody, onDataRefresh, 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(); const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({}); // 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) => { switch (lot.stockOutLineStatus?.toLowerCase()) { case 'pending': return t("Please finish QR code scanand 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("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 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, }); }, []); const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { if (!selectedRowId) return lot.availableQty; const lactualPickQty = lot.actualPickQty || 0; const actualPickQty = pickQtyData[selectedRowId]?.[lot.lotId] || 0; const remainingQty = lot.inQty - lot.outQty-actualPickQty; // Ensure it doesn't go below 0 return Math.max(0, remainingQty); }, [selectedRowId, pickQtyData]); const validatePickQty = useCallback((lot: LotPickData, inputValue: number) => { const maxAllowed = Math.min(calculateRemainingAvailableQty(lot), calculateRemainingRequiredQty(lot)); if (inputValue > maxAllowed) { return `${t('Input quantity cannot exceed')} ${maxAllowed}`; } if (inputValue < 0) { return t('Quantity cannot be negative'); } return null; }, [calculateRemainingAvailableQty, calculateRemainingRequiredQty, t]); // Handle QR code submission const handleQrCodeSubmit = useCallback(async (lotNo: string) => { if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { console.log(` QR Code verified for lot: ${lotNo}`); if (!selectedLotForQr.stockOutLineId) { console.error("No stock out line ID found for this lot"); alert("No stock out line found for this lot. Please contact administrator."); return; } // Store the required quantity before creating stock out line const requiredQty = selectedLotForQr.requiredQty; const lotId = selectedLotForQr.lotId; try { // Update stock out line status to 'checked' (QR scan completed) const stockOutLineUpdate = await updateStockOutLineStatus({ id: selectedLotForQr.stockOutLineId, status: 'checked', qty: selectedLotForQr.stockOutLineQty || 0 }); console.log(" Stock out line updated to 'checked':", stockOutLineUpdate); // Close modal setQrModalOpen(false); setSelectedLotForQr(null); if (onLotDataRefresh) { await onLotDataRefresh(); } // Set pick quantity AFTER stock out line update is complete if (selectedRowId) { // Add a small delay to ensure the data refresh 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 updated successfully!"); } catch (error) { console.error("❌ Error updating stock out line status:", error); alert("Failed to update lot status. Please try again."); } } else { // Handle case where lot numbers don't match console.error("QR scan mismatch:", { scanned: lotNo, expected: selectedLotForQr?.lotNo }); alert(`QR scan mismatch! Expected: ${selectedLotForQr?.lotNo}, Scanned: ${lotNo}`); } }, [selectedLotForQr, 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 ( <> {t("Selected")} {t("Lot#")} {t("Lot Expiry Date")} {t("Lot Location")} {t("Stock Unit")} {t("Lot Required Pick Qty")} {t("Original Available Qty")} {t("Lot Actual Pick Qty")} {/*{t("Available Lot")}*/} {t("Remaining Available Qty")} {/*{t("QR Code Scan")}*/} {/*} {t("Reject")} */} {t("Action")} {paginatedLotTableData.length === 0 ? ( {t("No data available")} ) : ( paginatedLotTableData.map((lot, index) => ( 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.lotAvailability !== 'available' && ( ({lot.lotAvailability === 'expired' ? 'Expired' : lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' : lot.lotAvailability === 'rejected' ? 'Rejected' : // 添加 rejected 显示 'Unavailable'}) )} */} {lot.expiryDate} {lot.location} {lot.stockUnit} {calculateRemainingRequiredQty(lot).toLocaleString()} {(() => { const inQty = lot.inQty || 0; const outQty = lot.outQty || 0; const result = inQty - outQty; return result.toLocaleString(); })()} {/* Show QR Scan Button if not scanned, otherwise show TextField + Pick Form */} {lot.stockOutLineStatus?.toLowerCase() === 'pending' ? ( ) : ( {/* 恢复 TextField 用于正常数量输入 */} { if (selectedRowId) { const inputValue = parseFloat(e.target.value) || 0; const maxAllowed = Math.min(calculateRemainingAvailableQty(lot), calculateRemainingRequiredQty(lot)); onPickQtyChange(selectedRowId, lot.lotId, inputValue); } }} disabled={ (lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable' || lot.lotAvailability === 'rejected') || selectedLotRowId !== `row_${index}` || lot.stockOutLineStatus === 'completed' } error={!!validationErrors[`lot_${lot.lotId}`]} helperText={validationErrors[`lot_${lot.lotId}`]} inputProps={{ min: 0, max: calculateRemainingRequiredQty(lot), step: 0.01 }} sx={{ width: '60px', height: '28px', '& .MuiInputBase-input': { fontSize: '0.7rem', textAlign: 'center', padding: '6px 8px' } }} placeholder="0" /> {/* 添加 Pick Form 按钮用于问题情况 */} )} {/*{lot.availableQty.toLocaleString()}*/} {calculateRemainingAvailableQty(lot).toLocaleString()} )) )}
{/* Status Messages Display */} {paginatedLotTableData.length > 0 && ( {paginatedLotTableData.map((lot, index) => ( {t("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} /> {/* Pick Execution Form Modal */} {pickExecutionFormOpen && selectedLotForExecutionForm && selectedRow && ( { setPickExecutionFormOpen(false); setSelectedLotForExecutionForm(null); }} onSubmit={handlePickExecutionFormSubmit} selectedLot={selectedLotForExecutionForm} selectedPickOrderLine={selectedRow} pickOrderId={selectedRow.pickOrderId} pickOrderCreateDate={new Date()} /> )} ); }; export default LotTable;