|
- "use client";
-
- import {
- Box,
- Button,
- Stack,
- TextField,
- Typography,
- Alert,
- CircularProgress,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
- Checkbox,
- TablePagination,
- Modal,
- } from "@mui/material";
- import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider';
- import { useCallback, useEffect, useState, useRef, useMemo } from "react";
- import { useTranslation } from "react-i18next";
- import { useRouter } from "next/navigation";
-
- // 修改:使用 Job Order API
- import {
- fetchCompletedJobOrderPickOrders,
- fetchUnassignedJobOrderPickOrders,
- assignJobOrderPickOrder,
- updateSecondQrScanStatus,
- submitSecondScanQuantity,
- recordSecondScanIssue
-
- } from "@/app/api/jo/actions";
- import { fetchNameList, NameList } from "@/app/api/user/actions";
- import {
- FormProvider,
- useForm,
- } from "react-hook-form";
- import SearchBox, { Criterion } from "../SearchBox";
- import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
- import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
- import QrCodeIcon from '@mui/icons-material/QrCode';
- import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
- import { useSession } from "next-auth/react";
- import { SessionWithTokens } from "@/config/authConfig";
- import { fetchStockInLineInfo } from "@/app/api/po/actions";
- import GoodPickExecutionForm from "./JobmatchForm";
- import FGPickOrderCard from "./FGPickOrderCard";
-
- interface Props {
- filterArgs: Record<string, any>;
- }
-
- // QR Code Modal Component (from GoodPickExecution)
- const QrCodeModal: React.FC<{
- open: boolean;
- onClose: () => void;
- lot: any | null;
- onQrCodeSubmit: (lotNo: string) => void;
- combinedLotData: any[];
- }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
- const { t } = useTranslation("jo");
- const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
- const [manualInput, setManualInput] = useState<string>('');
-
- const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
- const [manualInputError, setManualInputError] = useState<boolean>(false);
- const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
- const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
- const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
-
- const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
- const [scannedQrResult, setScannedQrResult] = useState<string>('');
- const { data: session } = useSession() as { data: SessionWithTokens | null };
- const currentUserId = session?.id ? parseInt(session.id) :
- // 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 (
- <Modal open={open} onClose={onClose}>
- <Box sx={{
- position: 'absolute',
- top: '50%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- bgcolor: 'background.paper',
- p: 3,
- borderRadius: 2,
- minWidth: 400,
- }}>
- <Typography variant="h6" gutterBottom>
- {t("QR Code Scan for Lot")}: {lot?.lotNo}
- </Typography>
-
- {isProcessingQr && (
- <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
- <Typography variant="body2" color="primary">
- {t("Processing QR code...")}
- </Typography>
- </Box>
- )}
-
- <Box sx={{ mb: 2 }}>
- <Typography variant="body2" gutterBottom>
- <strong>{t("Manual Input")}:</strong>
- </Typography>
- <TextField
- fullWidth
- size="small"
- value={manualInput}
- onChange={(e) => {
- 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.")}`
- : ''
- }
- />
- <Button
- variant="contained"
- onClick={handleManualSubmit}
- disabled={!manualInput.trim()}
- size="small"
- color="primary"
- >
- {t("Submit")}
- </Button>
- </Box>
-
- {qrValues.length > 0 && (
- <Box sx={{
- mb: 2,
- p: 2,
- backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
- borderRadius: 1
- }}>
- <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
- <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
- </Typography>
-
- {qrScanSuccess && (
- <Typography variant="caption" color="success" display="block">
- {t("Verified successfully!")}
- </Typography>
- )}
- </Box>
- )}
-
- <Box sx={{ mt: 2, textAlign: 'right' }}>
- <Button onClick={onClose} variant="outlined">
- {t("Cancel")}
- </Button>
- </Box>
- </Box>
- </Modal>
- );
- };
-
- const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
- const { t } = useTranslation("jo");
- const router = useRouter();
- const { data: session } = useSession() as { data: SessionWithTokens | null };
-
- const currentUserId = session?.id ? parseInt(session.id) : undefined;
-
- // 修改:使用 Job Order 数据结构
- const [jobOrderData, setJobOrderData] = useState<any>(null);
- const [combinedLotData, setCombinedLotData] = useState<any[]>([]);
- const [combinedDataLoading, setCombinedDataLoading] = useState(false);
- const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
-
- // 添加未分配订单状态
- const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
- const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
-
- const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
-
- const [qrScanInput, setQrScanInput] = useState<string>('');
- const [qrScanError, setQrScanError] = useState<boolean>(false);
- const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
-
- const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
- const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
- const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
- const [paginationController, setPaginationController] = useState({
- pageNum: 0,
- pageSize: 10,
- });
-
- const [usernameList, setUsernameList] = useState<NameList[]>([]);
-
- const initializationRef = useRef(false);
- const autoAssignRef = useRef(false);
-
- const formProps = useForm();
- const errors = formProps.formState.errors;
-
- // Add QR modal states
- const [qrModalOpen, setQrModalOpen] = useState(false);
- const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
-
- // Add GoodPickExecutionForm states
- const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
- const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
- // Add these missing state variables
- const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
- const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
- const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
- const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
-
- // 修改:加载未分配的 Job Order 订单
- const loadUnassignedOrders = useCallback(async () => {
- setIsLoadingUnassigned(true);
- try {
- const orders = await fetchUnassignedJobOrderPickOrders();
- setUnassignedOrders(orders);
- } catch (error) {
- console.error("Error loading unassigned orders:", error);
- } finally {
- setIsLoadingUnassigned(false);
- }
- }, []);
-
- // 修改:分配订单给当前用户
- const handleAssignOrder = useCallback(async (pickOrderId: number) => {
- if (!currentUserId) {
- console.error("Missing user id in session");
- return;
- }
-
- try {
- const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
- if (result.message === "Successfully assigned") {
- console.log(" Successfully assigned pick order");
- // 刷新数据
- window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
- // 重新加载未分配订单列表
- loadUnassignedOrders();
- } else {
- console.warn("⚠️ Assignment failed:", result.message);
- alert(`Assignment failed: ${result.message}`);
- }
- } catch (error) {
- console.error("❌ Error assigning order:", error);
- alert("Error occurred during assignment");
- }
- }, [currentUserId, loadUnassignedOrders]);
-
-
- // Handle QR code button click
- const handleQrCodeClick = (pickOrderId: number) => {
- console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
- // TODO: Implement QR code functionality
- };
-
- // 修改:使用 Job Order API 获取数据
- const fetchJobOrderData = useCallback(async (userId?: number) => {
- setCombinedDataLoading(true);
- try {
- const userIdToUse = userId || currentUserId;
-
- console.log(" fetchJobOrderData called with userId:", userIdToUse);
-
- if (!userIdToUse) {
- console.warn("⚠️ No userId available, skipping API call");
- setJobOrderData(null);
- setCombinedLotData([]);
- setOriginalCombinedData([]);
- return;
- }
- window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
- detail: {
- hasData: false,
- tabIndex: 1
- }
- }));
- // 使用 Job Order API
- const jobOrderData = await fetchCompletedJobOrderPickOrders(userIdToUse);
- console.log(" Job Order data:", jobOrderData);
- console.log(" Pick Order Code from API:", jobOrderData.pickOrder?.code);
- console.log(" Expected Pick Order Code: P-20251003-001");
-
- setJobOrderData(jobOrderData);
-
- // Transform hierarchical data to flat structure for the table
- const flatLotData: any[] = [];
-
- if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) {
- jobOrderData.pickOrderLines.forEach((line: any) => {
- if (line.lots && line.lots.length > 0) {
- line.lots.forEach((lot: any) => {
- flatLotData.push({
- // Pick order info
- pickOrderId: jobOrderData.pickOrder.id,
- pickOrderCode: jobOrderData.pickOrder.code,
- pickOrderConsoCode: jobOrderData.pickOrder.consoCode,
- pickOrderTargetDate: jobOrderData.pickOrder.targetDate,
- pickOrderType: jobOrderData.pickOrder.type,
- pickOrderStatus: jobOrderData.pickOrder.status,
- pickOrderAssignTo: jobOrderData.pickOrder.assignTo,
-
- // Pick order line info
- pickOrderLineId: line.id,
- pickOrderLineRequiredQty: line.requiredQty,
- pickOrderLineStatus: line.status,
-
- // Item info
- itemId: line.itemId,
- itemCode: line.itemCode,
- itemName: line.itemName,
- uomCode: line.uomCode,
- uomDesc: line.uomDesc,
-
- // Lot info
- lotId: lot.lotId,
- lotNo: lot.lotNo,
- expiryDate: lot.expiryDate,
- location: lot.location,
- availableQty: lot.availableQty,
- requiredQty: lot.requiredQty,
- actualPickQty: lot.actualPickQty,
- lotStatus: lot.lotStatus,
- lotAvailability: lot.lotAvailability,
- processingStatus: lot.processingStatus,
- stockOutLineId: lot.stockOutLineId,
- stockOutLineStatus: lot.stockOutLineStatus,
- stockOutLineQty: lot.stockOutLineQty,
-
- // Router info
- routerIndex: lot.routerIndex,
- matchStatus: lot.matchStatus,
- routerArea: lot.routerArea,
- routerRoute: lot.routerRoute,
- uomShortDesc: lot.uomShortDesc
- });
- });
- }
- });
- }
-
- console.log(" Transformed flat lot data:", flatLotData);
- setCombinedLotData(flatLotData);
- setOriginalCombinedData(flatLotData);
- const hasData = flatLotData.length > 0;
- window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
- detail: {
- hasData: hasData,
- tabIndex: 1
- }
- }));
- // 计算完成状态并发送事件
- const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) =>
- lot.processingStatus === 'completed'
- );
- window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
- detail: {
- allLotsCompleted: allCompleted,
- tabIndex: 1
- }
- }));
- // 发送完成状态事件,包含标签页信息
- window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
- detail: {
- allLotsCompleted: allCompleted,
- tabIndex: 0 // 明确指定这是来自标签页 0 的事件
- }
- }));
-
- } catch (error) {
- console.error("❌ Error fetching job order data:", error);
- setJobOrderData(null);
- setCombinedLotData([]);
- setOriginalCombinedData([]);
-
- // 如果加载失败,禁用打印按钮
- window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
- detail: {
- allLotsCompleted: false,
- tabIndex: 0
- }
- }));
- } finally {
- setCombinedDataLoading(false);
- }
- }, [currentUserId]);
- const handleSubmitAllScanned = useCallback(async () => {
- const scannedLots = combinedLotData.filter(lot =>
- lot.matchStatus === 'scanned' // Only submit items that are scanned but not yet submitted
- );
-
- if (scannedLots.length === 0) {
- console.log("No scanned items to submit");
- return;
- }
-
- setIsSubmittingAll(true);
- console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`);
-
- try {
- // Submit all items in parallel using Promise.all
- const submitPromises = scannedLots.map(async (lot) => {
- const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
-
- console.log(`Submitting item ${lot.itemCode}: qty=${submitQty}`);
-
- const result = await submitSecondScanQuantity(
- lot.pickOrderId,
- lot.itemId,
- {
- qty: submitQty,
- isMissing: false,
- isBad: false,
- reason: undefined
- }
- );
-
- return { success: result.code === "SUCCESS", itemCode: lot.itemCode };
- });
-
- // Wait for all submissions to complete
- const results = await Promise.all(submitPromises);
- const successCount = results.filter(r => r.success).length;
-
- console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`);
-
- // Refresh data once after all submissions
- await fetchJobOrderData();
-
- if (successCount > 0) {
- setQrScanSuccess(true);
- setTimeout(() => setQrScanSuccess(false), 2000);
- }
-
- } catch (error) {
- console.error("Error submitting all scanned items:", error);
- setQrScanError(true);
- } finally {
- setIsSubmittingAll(false);
- }
- }, [combinedLotData, fetchJobOrderData]);
-
- // Calculate scanned items count
- const scannedItemsCount = useMemo(() => {
- return combinedLotData.filter(lot => lot.matchStatus === 'scanned').length;
- }, [combinedLotData]);
-
- // 修改:初始化时加载数据
- useEffect(() => {
- if (session && currentUserId && !initializationRef.current) {
- console.log(" Session loaded, initializing job order...");
- initializationRef.current = true;
-
- // 加载 Job Order 数据
- fetchJobOrderData();
- // 加载未分配订单
- loadUnassignedOrders();
- }
- }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]);
-
- // Add event listener for manual assignment
- useEffect(() => {
- const handlePickOrderAssigned = () => {
- console.log("🔄 Pick order assigned event received, refreshing data...");
- fetchJobOrderData();
- };
-
- window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
-
- return () => {
- window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
- };
- }, [fetchJobOrderData]);
-
- // Handle QR code submission for matched lot (external scanning)
- const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
- console.log(` Processing Second QR Code for lot: ${lotNo}`);
-
- // Check if this lot was already processed recently
- const lotKey = `${lotNo}_${Date.now()}`;
- if (processedQrCodes.has(lotNo)) {
- console.log(`⏭️ Lot ${lotNo} already processed, skipping...`);
- return;
- }
-
- const currentLotData = combinedLotData;
- const matchingLots = currentLotData.filter(lot =>
- lot.lotNo === lotNo ||
- lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
- );
-
- if (matchingLots.length === 0) {
- console.error(`❌ Lot not found: ${lotNo}`);
- setQrScanError(true);
- setQrScanSuccess(false);
- return;
- }
-
- try {
- let successCount = 0;
-
- for (const matchingLot of matchingLots) {
- // Check if this specific item was already processed
- const itemKey = `${matchingLot.pickOrderId}_${matchingLot.itemId}`;
- if (processedQrCodes.has(itemKey)) {
- console.log(`⏭️ Item ${matchingLot.itemId} already processed, skipping...`);
- continue;
- }
-
- // Use the new second scan API
- const result = await updateSecondQrScanStatus(
- matchingLot.pickOrderId,
- matchingLot.itemId,
- currentUserId || 0,
- matchingLot.requiredQty || 1 // 传递实际的 required quantity
- );
-
- if (result.code === "SUCCESS") {
- successCount++;
- // Mark this item as processed
- setProcessedQrCodes(prev => new Set(prev).add(itemKey));
- console.log(` Second QR scan status updated for item ${matchingLot.itemId}`);
- } else {
- console.error(`❌ Failed to update second QR scan status: ${result.message}`);
- }
- }
-
- if (successCount > 0) {
- setQrScanSuccess(true);
- setQrScanError(false);
-
- // Set refreshing flag briefly to prevent duplicate processing
- setIsRefreshingData(true);
- await fetchJobOrderData(); // Refresh data
-
- // Clear refresh flag and success message after a short delay
- setTimeout(() => {
- setQrScanSuccess(false);
- setIsRefreshingData(false);
- }, 500);
- } else {
- setQrScanError(true);
- setQrScanSuccess(false);
- }
- } catch (error) {
- console.error("❌ Error processing second QR code:", error);
- setQrScanError(true);
- setQrScanSuccess(false);
- }
- }, [combinedLotData, fetchJobOrderData, processedQrCodes]);
-
-
- useEffect(() => {
- // Add isManualScanning and isRefreshingData checks
- if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) {
- return;
- }
-
- const latestQr = qrValues[qrValues.length - 1];
-
- // Check if this QR was already processed recently
- if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) {
- console.log("⏭️ QR code already processed, skipping...");
- return;
- }
-
- // Mark as processed
- setProcessedQrCodes(prev => new Set(prev).add(latestQr));
- setLastProcessedQr(latestQr);
-
- // Extract lot number from QR code
- let lotNo = '';
- try {
- const qrData = JSON.parse(latestQr);
- if (qrData.stockInLineId && qrData.itemId) {
- // For JSON QR codes, we need to fetch the lot number
- fetchStockInLineInfo(qrData.stockInLineId)
- .then((stockInLineInfo) => {
- console.log("Outside QR scan - Stock in line info:", stockInLineInfo);
- const extractedLotNo = stockInLineInfo.lotNo;
- if (extractedLotNo) {
- console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`);
- handleQrCodeSubmit(extractedLotNo);
- }
- })
- .catch((error) => {
- console.error("Outside QR scan - Error fetching stock in line info:", error);
- });
- return; // Exit early for JSON QR codes
- }
- } catch (error) {
- // Not JSON format, treat as direct lot number
- lotNo = latestQr.replace(/[{}]/g, '');
- }
-
- // For direct lot number QR codes
- if (lotNo) {
- console.log(`Outside QR scan detected (direct): ${lotNo}`);
- handleQrCodeSubmit(lotNo);
- }
- }, [qrValues, combinedLotData, handleQrCodeSubmit, processedQrCodes, lastProcessedQr, isManualScanning, isRefreshingData]);
-
- // ADD THIS: Cleanup effect
- useEffect(() => {
- return () => {
- // Cleanup when component unmounts (e.g., when switching tabs)
- if (isManualScanning) {
- console.log("🧹 Second scan component unmounting, stopping QR scanner...");
- stopScan();
- resetScan();
- }
- };
- }, [isManualScanning, stopScan, resetScan]);
- const handleManualInputSubmit = useCallback(() => {
- if (qrScanInput.trim() !== '') {
- handleQrCodeSubmit(qrScanInput.trim());
- }
- }, [qrScanInput, handleQrCodeSubmit]);
-
- // Handle QR code submission from modal (internal scanning)
- const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
- if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
- console.log(` QR Code verified for lot: ${lotNo}`);
-
- const requiredQty = selectedLotForQr.requiredQty;
- const lotId = selectedLotForQr.lotId;
-
- // Create stock out line
- const stockOutLineData: CreateStockOutLine = {
- consoCode: selectedLotForQr.pickOrderConsoCode,
- pickOrderLineId: selectedLotForQr.pickOrderLineId,
- inventoryLotLineId: selectedLotForQr.lotId,
- qty: 0.0
- };
-
- try {
-
-
-
- // 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 fetchJobOrderData();
- } catch (error) {
- console.error("Error creating stock out line:", error);
- }
- }
- }, [selectedLotForQr, fetchJobOrderData]);
-
- // Outside QR scanning - process QR codes from outside the page automatically
-
-
- const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
- if (value === '' || value === null || value === undefined) {
- setPickQtyData(prev => ({
- ...prev,
- [lotKey]: 0
- }));
- return;
- }
-
- const numericValue = typeof value === 'string' ? parseFloat(value) : value;
-
- if (isNaN(numericValue)) {
- setPickQtyData(prev => ({
- ...prev,
- [lotKey]: 0
- }));
- return;
- }
-
- setPickQtyData(prev => ({
- ...prev,
- [lotKey]: numericValue
- }));
- }, []);
-
- const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
- const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
-
-
-
-
- const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
- try {
- // Use the new second scan submit API
- const result = await submitSecondScanQuantity(
- lot.pickOrderId,
- lot.itemId,
- {
- qty: submitQty,
- isMissing: false,
- isBad: false,
- reason: undefined // Fix TypeScript error
- }
- );
-
-
- if (result.code === "SUCCESS") {
- console.log(` Second scan quantity submitted: ${submitQty}`);
- await fetchJobOrderData(); // Refresh data
- } else {
- console.error(`❌ Failed to submit second scan quantity: ${result.message}`);
- }
- } catch (error) {
- console.error("Error submitting second scan quantity:", error);
- }
- }, [fetchJobOrderData]);
- // Handle reject lot
-
- // Handle pick execution form
- const handlePickExecutionForm = useCallback((lot: any) => {
- console.log("=== Pick Execution Form ===");
- console.log("Lot data:", lot);
- console.log("lot.pickOrderCode:", lot.pickOrderCode); // 添加
- console.log("lot.pickOrderId:", lot.pickOrderId);
-
- 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);
- if (!currentUserId) {
- console.error("❌ No current user ID available");
- return;
- }
- const result = await recordSecondScanIssue(
- selectedLotForExecutionForm.pickOrderId,
- selectedLotForExecutionForm.itemId,
- {
- qty: data.actualPickQty, // verified qty
- missQty: data.missQty || 0, // 添加:实际的 miss qty
- badItemQty: data.badItemQty || 0, // 添加:实际的 bad item qty
- isMissing: data.missQty > 0,
- isBad: data.badItemQty > 0,
- reason: data.issueRemark || '',
- createdBy: currentUserId,
- type: "match"
- }
- );
- 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 fetchJobOrderData();
- } catch (error) {
- console.error("Error submitting pick execution form:", error);
- }
- }, [currentUserId, selectedLotForExecutionForm, fetchJobOrderData,]);
-
- // 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<any>[] = [
- {
- label: t("Pick Order Code"),
- paramName: "pickOrderCode",
- type: "text",
- },
- {
- label: t("Item Code"),
- paramName: "itemCode",
- type: "text",
- },
- {
- label: t("Item Name"),
- paramName: "itemName",
- type: "text",
- },
- {
- label: t("Lot No"),
- paramName: "lotNo",
- type: "text",
- },
- ];
-
- const handleSearch = useCallback((query: Record<string, any>) => {
- setSearchQuery({ ...query });
- console.log("Search query:", query);
-
- if (!originalCombinedData) return;
-
- const filtered = originalCombinedData.filter((lot: any) => {
- const pickOrderCodeMatch = !query.pickOrderCode ||
- lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
-
- const itemCodeMatch = !query.itemCode ||
- lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
-
- const itemNameMatch = !query.itemName ||
- lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
-
- const lotNoMatch = !query.lotNo ||
- lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
-
- return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
- });
-
- setCombinedLotData(filtered);
- console.log("Filtered lots count:", filtered.length);
- }, [originalCombinedData]);
-
- const handleReset = useCallback(() => {
- setSearchQuery({});
- if (originalCombinedData) {
- setCombinedLotData(originalCombinedData);
- }
- }, [originalCombinedData]);
-
- const handlePageChange = useCallback((event: unknown, newPage: number) => {
- setPaginationController(prev => ({
- ...prev,
- pageNum: newPage,
- }));
- }, []);
-
- const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
- const newPageSize = parseInt(event.target.value, 10);
- setPaginationController({
- pageNum: 0,
- pageSize: newPageSize,
- });
- }, []);
-
- // Pagination data with sorting by routerIndex
- const paginatedData = useMemo(() => {
- // Sort by routerIndex first, then by other criteria
- const sortedData = [...combinedLotData].sort((a, b) => {
- const aIndex = a.routerIndex || 0;
- const bIndex = b.routerIndex || 0;
-
- // Primary sort: by routerIndex
- if (aIndex !== bIndex) {
- return aIndex - bIndex;
- }
-
- // Secondary sort: by pickOrderCode if routerIndex is the same
- if (a.pickOrderCode !== b.pickOrderCode) {
- return a.pickOrderCode.localeCompare(b.pickOrderCode);
- }
-
- // Tertiary sort: by lotNo if everything else is the same
- return (a.lotNo || '').localeCompare(b.lotNo || '');
- });
-
- const startIndex = paginationController.pageNum * paginationController.pageSize;
- const endIndex = startIndex + paginationController.pageSize;
- return sortedData.slice(startIndex, endIndex);
- }, [combinedLotData, paginationController]);
-
- // Add these functions for manual scanning
- const handleStartScan = useCallback(() => {
- console.log(" Starting manual QR scan...");
- setIsManualScanning(true);
- setProcessedQrCodes(new Set());
- setLastProcessedQr('');
- setQrScanError(false);
- setQrScanSuccess(false);
- startScan();
- }, [startScan]);
-
- const handleStopScan = useCallback(() => {
- console.log("⏹️ Stopping manual QR scan...");
- setIsManualScanning(false);
- setQrScanError(false);
- setQrScanSuccess(false);
- stopScan();
- resetScan();
- }, [stopScan, resetScan]);
- useEffect(() => {
- if (isManualScanning && combinedLotData.length === 0) {
- console.log("⏹️ No data available, auto-stopping QR scan...");
- handleStopScan();
- }
- }, [combinedLotData.length, isManualScanning, handleStopScan]);
-
- // Cleanup effect
- useEffect(() => {
- return () => {
- // Cleanup when component unmounts (e.g., when switching tabs)
- if (isManualScanning) {
- console.log("🧹 Second scan component unmounting, stopping QR scanner...");
- stopScan();
- resetScan();
- }
- };
- }, [isManualScanning, stopScan, resetScan]);
- const getStatusMessage = useCallback((lot: any) => {
- switch (lot.stockOutLineStatus?.toLowerCase()) {
- case 'pending':
- return t("Please finish QR code scan and 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.");
- }
- }, [t]);
-
- return (
-
- <TestQrCodeProvider
- lotData={combinedLotData}
- onScanLot={handleQrCodeSubmit}
- filterActive={(lot) => (
- lot.matchStatus !== 'completed' &&
- lot.lotAvailability !== 'rejected'
- )}
- >
- <FormProvider {...formProps}>
- <Stack spacing={2}>
- {/* Job Order Header */}
- {jobOrderData && (
- <Paper sx={{ p: 2 }}>
- <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
- <Typography variant="subtitle1">
- <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.code || '-'}
- </Typography>
- <Typography variant="subtitle1">
- <strong>{t("Pick Order Code")}:</strong> {jobOrderData.pickOrder?.code || '-'}
- </Typography>
- <Typography variant="subtitle1">
- <strong>{t("Target Date")}:</strong> {jobOrderData.pickOrder?.targetDate || '-'}
- </Typography>
-
- </Stack>
- </Paper>
- )}
-
-
-
- {/* Combined Lot Table */}
- <Box>
- <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
-
-
- <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
- {!isManualScanning ? (
- <Button
- variant="contained"
- startIcon={<QrCodeIcon />}
- onClick={handleStartScan}
- color="primary"
- sx={{ minWidth: '120px' }}
- >
- {t("Start QR Scan")}
- </Button>
- ) : (
- <Button
- variant="outlined"
- startIcon={<QrCodeIcon />}
- onClick={handleStopScan}
- color="secondary"
- sx={{ minWidth: '120px' }}
- >
- {t("Stop QR Scan")}
- </Button>
- )}
-
- {/* ADD THIS: Submit All Scanned Button */}
- <Button
- variant="contained"
- color="success"
- onClick={handleSubmitAllScanned}
- disabled={scannedItemsCount === 0 || isSubmittingAll}
- sx={{ minWidth: '160px' }}
- >
- {isSubmittingAll ? (
- <>
- <CircularProgress size={16} sx={{ mr: 1 }} />
- {t("Submitting...")}
- </>
- ) : (
- `${t("Submit All Scanned")} (${scannedItemsCount})`
- )}
- </Button>
- </Box>
- </Box>
-
- {qrScanError && !qrScanSuccess && (
- <Alert severity="error" sx={{ mb: 2 }}>
- {t("QR code does not match any item in current orders.")}
- </Alert>
- )}
- {qrScanSuccess && (
- <Alert severity="success" sx={{ mb: 2 }}>
- {t("QR code verified.")}
- </Alert>
- )}
-
- <TableContainer component={Paper}>
- <Table>
- <TableHead>
- <TableRow>
- <TableCell>{t("Index")}</TableCell>
- <TableCell>{t("Route")}</TableCell>
- <TableCell>{t("Item Code")}</TableCell>
- <TableCell>{t("Item Name")}</TableCell>
- <TableCell>{t("Lot No")}</TableCell>
- <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
- <TableCell align="center">{t("Scan Result")}</TableCell>
- <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {paginatedData.length === 0 ? (
- <TableRow>
- <TableCell colSpan={8} align="center">
- <Typography variant="body2" color="text.secondary">
- {t("No data available")}
- </Typography>
- </TableCell>
- </TableRow>
- ) : (
- paginatedData.map((lot, index) => (
- <TableRow
- key={`${lot.pickOrderLineId}-${lot.lotId}`}
- sx={{
- backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
- opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
- '& .MuiTableCell-root': {
- color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
- }
- }}
- >
- <TableCell>
- <Typography variant="body2" fontWeight="bold">
- {index + 1}
- </Typography>
- </TableCell>
- <TableCell>
- <Typography variant="body2">
- {lot.routerRoute || '-'}
- </Typography>
- </TableCell>
- <TableCell>{lot.itemCode}</TableCell>
- <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
- <TableCell>
- <Box>
- <Typography
- sx={{
- color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
- opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1
- }}
- >
- {lot.lotNo}
- </Typography>
- </Box>
- </TableCell>
- <TableCell align="right">
- {(() => {
- const requiredQty = lot.requiredQty || 0;
- return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
- })()}
- </TableCell>
-
- <TableCell align="center">
- {lot.matchStatus?.toLowerCase() === 'scanned' ||
- lot.matchStatus?.toLowerCase() === 'completed' ? (
- <Box sx={{
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- width: '100%',
- height: '100%'
- }}>
- <Checkbox
- checked={true} // 改为 true
- disabled={true}
- readOnly={true}
- size="large"
- sx={{
- color: 'success.main', // 固定为绿色
- '&.Mui-checked': {
- color: 'success.main',
- },
- transform: 'scale(1.3)',
- '& .MuiSvgIcon-root': {
- fontSize: '1.5rem',
- }
- }}
- />
- </Box>
- ) : (
- <Typography variant="body2" color="text.secondary">
- {t(" ")}
- </Typography>
- )}
- </TableCell>
-
- <TableCell align="center">
- <Box sx={{ display: 'flex', justifyContent: 'center' }}>
- <Stack direction="row" spacing={1} alignItems="center">
- <Button
- variant="contained"
- onClick={() => {
- const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
- const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
- // Submit with default lot required pick qty
-
- handlePickQtyChange(lotKey, submitQty);
- handleSubmitPickQtyWithQty(lot, submitQty);
- }}
- disabled={
- // 修复:只有扫描过但未完成的才能提交
- lot.matchStatus !== 'scanned' || // 只有 scanned 状态才能提交
- lot.lotAvailability === 'expired' ||
- lot.lotAvailability === 'status_unavailable' ||
- lot.lotAvailability === 'rejected'
- }
- sx={{
- fontSize: '0.75rem',
- py: 0.5,
- minHeight: '28px',
- minWidth: '70px'
- }}
- >
- {t("Submit")}
- </Button>
-
- <Button
- variant="outlined"
- size="small"
- onClick={() => handlePickExecutionForm(lot)}
- disabled={
- // 修复:只有扫描过但未完成的才能报告问题
- lot.matchStatus !== 'scanned' || // 只有 scanned 状态才能报告问题
- lot.lotAvailability === 'expired' ||
- lot.lotAvailability === 'status_unavailable' ||
- lot.lotAvailability === 'rejected'
- }
- sx={{
- fontSize: '0.7rem',
- py: 0.5,
- minHeight: '28px',
- minWidth: '60px',
- borderColor: 'warning.main',
- color: 'warning.main'
- }}
- title="Report missing or bad items"
- >
- {t("Issue")}
- </Button>
- </Stack>
- </Box>
- </TableCell>
- </TableRow>
- ))
- )}
- </TableBody>
- </Table>
- </TableContainer>
-
- <TablePagination
- component="div"
- count={combinedLotData.length}
- page={paginationController.pageNum}
- rowsPerPage={paginationController.pageSize}
- onPageChange={handlePageChange}
- onRowsPerPageChange={handlePageSizeChange}
- rowsPerPageOptions={[10, 25, 50]}
- labelRowsPerPage={t("Rows per page")}
- labelDisplayedRows={({ from, to, count }) =>
- `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
- }
- />
- </Box>
- </Stack>
-
- {/* QR Code Modal */}
- <QrCodeModal
- open={qrModalOpen}
- onClose={() => {
- setQrModalOpen(false);
- setSelectedLotForQr(null);
- stopScan();
- resetScan();
- }}
- lot={selectedLotForQr}
- combinedLotData={combinedLotData}
- onQrCodeSubmit={handleQrCodeSubmitFromModal}
- />
-
- {/* Pick Execution Form Modal */}
- {pickExecutionFormOpen && selectedLotForExecutionForm && (
- <GoodPickExecutionForm
- open={pickExecutionFormOpen}
- onClose={() => {
- 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()}
- />
- )}
- </FormProvider>
- </TestQrCodeProvider>
- );
- };
-
- export default JobPickExecution
|