|
- "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, startTransition } from "react";
- import { useTranslation } from "react-i18next";
- import { useRouter } from "next/navigation";
- import {
- updateStockOutLineStatus,
- createStockOutLine,
- recordPickExecutionIssue,
- fetchFGPickOrders,
- FGPickOrderResponse,
- autoAssignAndReleasePickOrder,
- AutoAssignReleaseResponse,
- checkPickOrderCompletion,
- PickOrderCompletionResponse,
- checkAndCompletePickOrderByConsoCode,
- confirmLotSubstitution,
- updateStockOutLineStatusByQRCodeAndLotNo, // ✅ 添加
- batchSubmitList, // ✅ 添加
- batchSubmitListRequest, // ✅ 添加
- batchSubmitListLineRequest,
- } from "@/app/api/pickOrder/actions";
- // 修改:使用 Job Order API
- import {
- assignJobOrderPickOrder,
- fetchJobOrderLotsHierarchicalByPickOrderId,
- updateJoPickOrderHandledBy,
- JobOrderLotsHierarchicalResponse,
- } 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, analyzeQrCode, fetchLotDetail } 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 "./JobPickExecutionForm";
- import FGPickOrderCard from "./FGPickOrderCard";
- import LotConfirmationModal from "./LotConfirmationModal";
- import LinearProgressWithLabel from "../common/LinearProgressWithLabel";
- import ScanStatusAlert from "../common/ScanStatusAlert";
- interface Props {
- filterArgs: Record<string, any>;
- //onSwitchToRecordTab: () => void;
- onBackToList?: () => void;
- }
-
- // Manual Lot Confirmation Modal (align with GoodPickExecutiondetail, opened by {2fic})
- const ManualLotConfirmationModal: React.FC<{
- open: boolean;
- onClose: () => void;
- onConfirm: (expectedLotNo: string, scannedLotNo: string) => void;
- expectedLot: { lotNo: string; itemCode: string; itemName: string } | null;
- scannedLot: { lotNo: string; itemCode: string; itemName: string } | null;
- isLoading?: boolean;
- }> = ({ open, onClose, onConfirm, expectedLot, scannedLot, isLoading = false }) => {
- const { t } = useTranslation("jo");
- const [expectedLotInput, setExpectedLotInput] = useState<string>('');
- const [scannedLotInput, setScannedLotInput] = useState<string>('');
- const [error, setError] = useState<string>('');
-
- useEffect(() => {
- if (open) {
- setExpectedLotInput(expectedLot?.lotNo || '');
- setScannedLotInput(scannedLot?.lotNo || '');
- setError('');
- }
- }, [open, expectedLot, scannedLot]);
-
- const handleConfirm = () => {
- if (!expectedLotInput.trim() || !scannedLotInput.trim()) {
- setError(t("Please enter both expected and scanned lot numbers."));
- return;
- }
- if (expectedLotInput.trim() === scannedLotInput.trim()) {
- setError(t("Expected and scanned lot numbers cannot be the same."));
- return;
- }
- onConfirm(expectedLotInput.trim(), scannedLotInput.trim());
- };
-
- 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: 500,
- }}>
- <Typography variant="h6" gutterBottom color="warning.main">
- {t("Manual Lot Confirmation")}
- </Typography>
-
- <Box sx={{ mb: 2 }}>
- <Typography variant="body2" gutterBottom>
- <strong>{t("Expected Lot Number")}:</strong>
- </Typography>
- <TextField
- fullWidth
- size="small"
- value={expectedLotInput}
- onChange={(e) => { setExpectedLotInput(e.target.value); setError(''); }}
- sx={{ mb: 2 }}
- error={!!error && !expectedLotInput.trim()}
- />
- </Box>
-
- <Box sx={{ mb: 2 }}>
- <Typography variant="body2" gutterBottom>
- <strong>{t("Scanned Lot Number")}:</strong>
- </Typography>
- <TextField
- fullWidth
- size="small"
- value={scannedLotInput}
- onChange={(e) => { setScannedLotInput(e.target.value); setError(''); }}
- sx={{ mb: 2 }}
- error={!!error && !scannedLotInput.trim()}
- />
- </Box>
-
- {error && (
- <Box sx={{ mb: 2, p: 1, backgroundColor: '#ffebee', borderRadius: 1 }}>
- <Typography variant="body2" color="error">
- {error}
- </Typography>
- </Box>
- )}
-
- <Box sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
- <Button onClick={onClose} variant="outlined" disabled={isLoading}>
- {t("Cancel")}
- </Button>
- <Button
- onClick={handleConfirm}
- variant="contained"
- color="warning"
- disabled={isLoading || !expectedLotInput.trim() || !scannedLotInput.trim()}
- >
- {isLoading ? t("Processing...") : t("Confirm")}
- </Button>
- </Box>
- </Box>
- </Modal>
- );
- };
-
- // 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>('');
-
- // 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, onBackToList }) => {
- 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 [combinedDataLoading, setCombinedDataLoading] = useState(false);
-
- // 添加未分配订单状态
- const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
- const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
-
- const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
- const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
- const [expectedLotData, setExpectedLotData] = useState<any>(null);
- const [scannedLotData, setScannedLotData] = useState<any>(null);
- const [isConfirmingLot, setIsConfirmingLot] = useState(false);
- const [qrScanInput, setQrScanInput] = useState<string>('');
- const [qrScanError, setQrScanError] = useState<boolean>(false);
- const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>('');
- const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
- const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null);
- const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
- const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
-
- 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;
- const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
-
- // 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);
- const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
- const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
-
- // 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);
- // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling
- const [processedQrCombinations, setProcessedQrCombinations] = useState<Map<number, Set<number>>>(new Map());
-
- // Cache for fetchStockInLineInfo API calls to avoid redundant requests
- const stockInLineInfoCache = useRef<Map<number, { lotNo: string | null; timestamp: number }>>(new Map());
- const CACHE_TTL = 60000; // 60 seconds cache TTL
- const abortControllerRef = useRef<AbortController | null>(null);
- const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
-
- // Use refs for processed QR tracking to avoid useEffect dependency issues and delays
- const processedQrCodesRef = useRef<Set<string>>(new Set());
- const lastProcessedQrRef = useRef<string>('');
-
- // Store callbacks in refs to avoid useEffect dependency issues
- const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | null>(null);
- const resetScanRef = useRef<(() => void) | null>(null);
-
- // Manual lot confirmation modal state (test shortcut {2fic})
- const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false);
- const getAllLotsFromHierarchical = useCallback((
- data: JobOrderLotsHierarchicalResponse | null
- ): any[] => {
- if (!data || !data.pickOrder || !data.pickOrderLines) {
- return [];
- }
-
- const allLots: any[] = [];
-
- data.pickOrderLines.forEach((line) => {
- if (line.lots && line.lots.length > 0) {
- line.lots.forEach((lot) => {
- allLots.push({
- ...lot,
- pickOrderLineId: line.id,
- itemId: line.itemId,
- itemCode: line.itemCode,
- itemName: line.itemName,
- uomCode: line.uomCode,
- uomDesc: line.uomDesc,
- pickOrderLineRequiredQty: line.requiredQty,
- pickOrderLineStatus: line.status,
- jobOrderId: data.pickOrder.jobOrder.id,
- jobOrderCode: data.pickOrder.jobOrder.code,
- // 添加 pickOrder 信息(如果需要)
- pickOrderId: data.pickOrder.id,
- pickOrderCode: data.pickOrder.code,
- pickOrderConsoCode: data.pickOrder.consoCode,
- pickOrderTargetDate: data.pickOrder.targetDate,
- pickOrderType: data.pickOrder.type,
- pickOrderStatus: data.pickOrder.status,
- pickOrderAssignTo: data.pickOrder.assignTo,
- handler: line.handler,
- });
- });
- }
- });
-
- return allLots;
- }, []);
- const combinedLotData = useMemo(() => {
- return getAllLotsFromHierarchical(jobOrderData);
- }, [jobOrderData, getAllLotsFromHierarchical]);
-
- const originalCombinedData = useMemo(() => {
- return getAllLotsFromHierarchical(jobOrderData);
- }, [jobOrderData, getAllLotsFromHierarchical]);
-
- // Enhanced lotDataIndexes with cached active lots for better performance (align with GoodPickExecutiondetail)
- const lotDataIndexes = useMemo(() => {
- const byItemId = new Map<number, any[]>();
- const byItemCode = new Map<string, any[]>();
- const byLotId = new Map<number, any>();
- const byLotNo = new Map<string, any[]>();
- const byStockInLineId = new Map<number, any[]>();
- const activeLotsByItemId = new Map<number, any[]>();
- const rejectedStatuses = new Set(['rejected']);
-
- for (let i = 0; i < combinedLotData.length; i++) {
- const lot = combinedLotData[i];
- const isActive =
- !rejectedStatuses.has(lot.lotAvailability) &&
- !rejectedStatuses.has(lot.stockOutLineStatus) &&
- !rejectedStatuses.has(lot.processingStatus) &&
- lot.stockOutLineStatus !== 'completed';
-
- if (lot.itemId) {
- if (!byItemId.has(lot.itemId)) {
- byItemId.set(lot.itemId, []);
- activeLotsByItemId.set(lot.itemId, []);
- }
- byItemId.get(lot.itemId)!.push(lot);
- if (isActive) activeLotsByItemId.get(lot.itemId)!.push(lot);
- }
- if (lot.itemCode) {
- if (!byItemCode.has(lot.itemCode)) byItemCode.set(lot.itemCode, []);
- byItemCode.get(lot.itemCode)!.push(lot);
- }
- if (lot.lotId) byLotId.set(lot.lotId, lot);
- if (lot.lotNo) {
- if (!byLotNo.has(lot.lotNo)) byLotNo.set(lot.lotNo, []);
- byLotNo.get(lot.lotNo)!.push(lot);
- }
- if (lot.stockInLineId) {
- if (!byStockInLineId.has(lot.stockInLineId)) byStockInLineId.set(lot.stockInLineId, []);
- byStockInLineId.get(lot.stockInLineId)!.push(lot);
- }
- }
-
- return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId };
- }, [combinedLotData]);
-
- // Cached version of fetchStockInLineInfo to avoid redundant API calls
- const fetchStockInLineInfoCached = useCallback(async (stockInLineId: number): Promise<{ lotNo: string | null }> => {
- const now = Date.now();
- const cached = stockInLineInfoCache.current.get(stockInLineId);
- if (cached && (now - cached.timestamp) < CACHE_TTL) {
- return { lotNo: cached.lotNo };
- }
-
- if (abortControllerRef.current) abortControllerRef.current.abort();
- const abortController = new AbortController();
- abortControllerRef.current = abortController;
-
- const stockInLineInfo = await fetchStockInLineInfo(stockInLineId);
- stockInLineInfoCache.current.set(stockInLineId, {
- lotNo: stockInLineInfo.lotNo || null,
- timestamp: now
- });
- if (stockInLineInfoCache.current.size > 100) {
- const firstKey = stockInLineInfoCache.current.keys().next().value;
- if (firstKey !== undefined) stockInLineInfoCache.current.delete(firstKey);
- }
- return { lotNo: stockInLineInfo.lotNo || null };
- }, []);
- // 修改:加载未分配的 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]);
-
- const fetchFgPickOrdersData = useCallback(async () => {
- if (!currentUserId) return;
-
- setFgPickOrdersLoading(true);
- try {
- // Get all pick order IDs from combinedLotData
- const pickOrderIds = Array.from(new Set(combinedLotData.map(lot => lot.pickOrderId)));
-
- if (pickOrderIds.length === 0) {
- setFgPickOrders([]);
- return;
- }
-
- // Fetch FG pick orders for each pick order ID
- const fgPickOrdersPromises = pickOrderIds.map(pickOrderId =>
- fetchFGPickOrders(pickOrderId)
- );
-
- const fgPickOrdersResults = await Promise.all(fgPickOrdersPromises);
-
- // Flatten the results (each fetchFGPickOrders returns an array)
- const allFgPickOrders = fgPickOrdersResults.flat();
-
- setFgPickOrders(allFgPickOrders);
- console.log(" Fetched FG pick orders:", allFgPickOrders);
- } catch (error) {
- console.error("❌ Error fetching FG pick orders:", error);
- setFgPickOrders([]);
- } finally {
- setFgPickOrdersLoading(false);
- }
- }, [currentUserId, combinedLotData]);
-
- useEffect(() => {
- if (combinedLotData.length > 0) {
- fetchFgPickOrdersData();
- }
- }, [combinedLotData, fetchFgPickOrdersData]);
-
- // Handle QR code button click
- const handleQrCodeClick = (pickOrderId: number) => {
- console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
- // TODO: Implement QR code functionality
- };
-
- // 修改:使用 Job Order API 获取数据
- const fetchJobOrderData = useCallback(async (pickOrderId?: number) => {
- setCombinedDataLoading(true);
- try {
- if (!pickOrderId) {
- console.warn("⚠️ No pickOrderId provided, skipping API call");
- setJobOrderData(null);
- return;
- }
-
- // 直接使用类型化的响应
- const jobOrderData = await fetchJobOrderLotsHierarchicalByPickOrderId(pickOrderId);
- console.log("✅ Job Order data (hierarchical):", jobOrderData);
-
- setJobOrderData(jobOrderData);
-
- // 使用辅助函数获取所有 lots(不再扁平化)
- const allLots = getAllLotsFromHierarchical(jobOrderData);
-
- // ... 其他逻辑保持不变 ...
-
- } catch (error) {
- console.error("❌ Error fetching job order data:", error);
- setJobOrderData(null);
- } finally {
- setCombinedDataLoading(false);
- }
- }, [getAllLotsFromHierarchical]);
-
- const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => {
- if (!currentUserId || !pickOrderId || !itemId) {
- return;
- }
-
- try {
- console.log(`Updating JoPickOrder.handledBy for pickOrderId: ${pickOrderId}, itemId: ${itemId}, userId: ${currentUserId}`);
- await updateJoPickOrderHandledBy({
- pickOrderId: pickOrderId,
- itemId: itemId,
- userId: currentUserId
- });
- console.log("✅ JoPickOrder.handledBy updated successfully");
- } catch (error) {
- console.error("❌ Error updating JoPickOrder.handledBy:", error);
- // Don't throw - this is not critical for the main flow
- }
- }, [currentUserId]);
- // 修改:初始化时加载数据
- useEffect(() => {
- if (session && currentUserId && !initializationRef.current) {
- console.log("✅ Session loaded, initializing job order...");
- initializationRef.current = true;
-
- // Get pickOrderId from filterArgs if available (when viewing from list)
- const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
- if (pickOrderId) {
- fetchJobOrderData(pickOrderId);
- }
- loadUnassignedOrders();
- }
- }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders, filterArgs?.pickOrderId]);
-
- // Add event listener for manual assignment
- useEffect(() => {
- const handlePickOrderAssigned = () => {
- console.log("🔄 Pick order assigned event received, refreshing data...");
- const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
- if (pickOrderId) {
- fetchJobOrderData(pickOrderId);
- }
- };
-
- window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
-
- return () => {
- window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
- };
- }, [fetchJobOrderData, filterArgs?.pickOrderId]);
-
- // Handle QR code submission for matched lot (external scanning)
- const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
- console.log(` Processing QR Code for lot: ${lotNo}`);
-
- // Use current data without refreshing to avoid infinite loop
- const currentLotData = combinedLotData;
- console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
-
- const matchingLots = currentLotData.filter(lot =>
- lot.lotNo === lotNo ||
- lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
- );
-
- if (matchingLots.length === 0) {
- console.error(`❌ Lot not found: ${lotNo}`);
- setQrScanError(true);
- setQrScanSuccess(false);
- const availableLotNos = currentLotData.map(lot => lot.lotNo).join(', ');
- console.log(`❌ QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`);
- return;
- }
-
- console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
- setQrScanError(false);
-
- try {
- let successCount = 0;
- let errorCount = 0;
-
- for (const matchingLot of matchingLots) {
- console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
-
- if (matchingLot.stockOutLineId) {
- const stockOutLineUpdate = await updateStockOutLineStatus({
- id: matchingLot.stockOutLineId,
- status: 'checked',
- qty: 0
- });
- console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate);
-
- // Treat multiple backend shapes as success (type-safe via any)
- const r: any = stockOutLineUpdate as any;
- const updateOk =
- r?.code === 'SUCCESS' ||
- typeof r?.id === 'number' ||
- r?.type === 'checked' ||
- r?.status === 'checked' ||
- typeof r?.entity?.id === 'number' ||
- r?.entity?.status === 'checked';
-
- if (updateOk) {
- successCount++;
- } else {
- errorCount++;
- }
- } else {
- const createStockOutLineData = {
- consoCode: matchingLot.pickOrderConsoCode,
- pickOrderLineId: matchingLot.pickOrderLineId,
- inventoryLotLineId: matchingLot.lotId,
- qty: 0
- };
-
- const createResult = await createStockOutLine(createStockOutLineData);
- console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult);
-
- if (createResult && createResult.code === "SUCCESS") {
- // Immediately set status to checked for new line
- let newSolId: number | undefined;
- const anyRes: any = createResult as any;
- if (typeof anyRes?.id === 'number') {
- newSolId = anyRes.id;
- } else if (anyRes?.entity) {
- newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id;
- }
-
- if (newSolId) {
- const setChecked = await updateStockOutLineStatus({
- id: newSolId,
- status: 'checked',
- qty: 0
- });
- if (setChecked && setChecked.code === "SUCCESS") {
- successCount++;
- } else {
- errorCount++;
- }
- } else {
- console.warn("Created stock out line but no ID returned; cannot set to checked");
- errorCount++;
- }
- } else {
- errorCount++;
- }
- }
- }
-
- // FIXED: Set refresh flag before refreshing data
- setIsRefreshingData(true);
- console.log("🔄 Refreshing data after QR code processing...");
- const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
- await fetchJobOrderData(pickOrderId);
-
- if (successCount > 0) {
- console.log(` QR Code processing completed: ${successCount} updated/created`);
- setQrScanSuccess(true);
- setQrScanError(false);
- setQrScanInput(''); // Clear input after successful processing
-
- } else {
- console.error(`❌ QR Code processing failed: ${errorCount} errors`);
- setQrScanError(true);
- setQrScanSuccess(false);
- }
- } catch (error) {
- console.error("❌ Error processing QR code:", error);
- setQrScanError(true);
- setQrScanSuccess(false);
-
- // Still refresh data even on error
- setIsRefreshingData(true);
- const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
- await fetchJobOrderData( pickOrderId);
- } finally {
- // Clear refresh flag after a short delay
- setTimeout(() => {
- setIsRefreshingData(false);
- }, 1000);
- }
- }, [combinedLotData, fetchJobOrderData]);
- const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => {
- console.log("⚠️ [LOT MISMATCH] Lot mismatch detected:", { expectedLot, scannedLot });
- console.log("⚠️ [LOT MISMATCH] Opening confirmation modal - NO lot will be marked as scanned until user confirms");
-
- // ✅ schedule modal open in next tick (avoid flushSync warnings on some builds)
- // ✅ IMPORTANT: This function ONLY opens the modal. It does NOT process any lot.
- setTimeout(() => {
- setExpectedLotData(expectedLot);
- setScannedLotData({
- ...scannedLot,
- lotNo: scannedLot.lotNo || null,
- });
- setLotConfirmationOpen(true);
- console.log("⚠️ [LOT MISMATCH] Modal opened - waiting for user confirmation");
- }, 0);
-
- // ✅ Fetch lotNo in background for display purposes (cached)
- // ✅ This is ONLY for display - it does NOT process any lot
- if (!scannedLot.lotNo && scannedLot.stockInLineId) {
- console.log(`⚠️ [LOT MISMATCH] Fetching lotNo for display (stockInLineId: ${scannedLot.stockInLineId})`);
- fetchStockInLineInfoCached(scannedLot.stockInLineId)
- .then((info) => {
- console.log(`⚠️ [LOT MISMATCH] Fetched lotNo for display: ${info.lotNo}`);
- startTransition(() => {
- setScannedLotData((prev: any) => ({
- ...prev,
- lotNo: info.lotNo || null,
- }));
- });
- })
- .catch((error) => {
- console.error(`❌ [LOT MISMATCH] Error fetching lotNo for display (stockInLineId may not exist):`, error);
- // ignore display fetch errors - this does NOT affect processing
- });
- }
- }, [fetchStockInLineInfoCached]);
-
- // Add handleLotConfirmation function
- const handleLotConfirmation = useCallback(async () => {
- if (!expectedLotData || !scannedLotData || !selectedLotForQr) {
- console.error("❌ [LOT CONFIRM] Missing required data for lot confirmation");
- return;
- }
-
- console.log("✅ [LOT CONFIRM] User confirmed lot substitution - processing now");
- console.log("✅ [LOT CONFIRM] Expected lot:", expectedLotData);
- console.log("✅ [LOT CONFIRM] Scanned lot:", scannedLotData);
- console.log("✅ [LOT CONFIRM] Selected lot for QR:", selectedLotForQr);
-
- setIsConfirmingLot(true);
- try {
- let newLotLineId = scannedLotData?.inventoryLotLineId;
- if (!newLotLineId && scannedLotData?.stockInLineId) {
- try {
- console.log(`🔍 [LOT CONFIRM] Fetching lot detail for stockInLineId: ${scannedLotData.stockInLineId}`);
- const ld = await fetchLotDetail(scannedLotData.stockInLineId);
- newLotLineId = ld.inventoryLotLineId;
- console.log(`✅ [LOT CONFIRM] Fetched lot detail: inventoryLotLineId=${newLotLineId}`);
- } catch (error) {
- console.error("❌ [LOT CONFIRM] Error fetching lot detail (stockInLineId may not exist):", error);
- // If stockInLineId doesn't exist, we can still proceed with lotNo substitution
- // The backend confirmLotSubstitution should handle this case
- }
- }
- if (!newLotLineId) {
- console.warn("⚠️ [LOT CONFIRM] No inventory lot line id for scanned lot, proceeding with lotNo only");
- // Continue anyway - backend may handle lotNo substitution without inventoryLotLineId
- }
-
- console.log("=== [LOT CONFIRM] Lot Confirmation Debug ===");
- console.log("Selected Lot:", selectedLotForQr);
- console.log("Pick Order Line ID:", selectedLotForQr.pickOrderLineId);
- console.log("Stock Out Line ID:", selectedLotForQr.stockOutLineId);
- console.log("Suggested Pick Lot ID:", selectedLotForQr.suggestedPickLotId);
- console.log("Lot ID (fallback):", selectedLotForQr.lotId);
- console.log("New Inventory Lot Line ID:", newLotLineId);
- console.log("Scanned Lot No:", scannedLotData.lotNo);
- console.log("Scanned StockInLineId:", scannedLotData.stockInLineId);
-
- // Call confirmLotSubstitution to update the suggested lot
- console.log("🔄 [LOT CONFIRM] Calling confirmLotSubstitution...");
- const substitutionResult = await confirmLotSubstitution({
- pickOrderLineId: selectedLotForQr.pickOrderLineId,
- stockOutLineId: selectedLotForQr.stockOutLineId,
- originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId,
- newInventoryLotNo: scannedLotData.lotNo || '',
- // ✅ required by LotSubstitutionConfirmRequest
- newStockInLineId: scannedLotData?.stockInLineId ?? null,
- });
-
- console.log("✅ [LOT CONFIRM] Lot substitution result:", substitutionResult);
-
- // ✅ CRITICAL: substitution failed => DO NOT mark original stockOutLine as checked.
- // Keep modal open so user can cancel/rescan.
- if (!substitutionResult || substitutionResult.code !== "SUCCESS") {
- console.error("❌ [LOT CONFIRM] Lot substitution failed. Will NOT update stockOutLine status.");
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(
- substitutionResult?.message ||
- `换批失败:stockInLineId ${scannedLotData?.stockInLineId ?? ""} 不存在或无法匹配`
- );
- return;
- }
-
- // Update stock out line status to 'checked' after substitution
- if(selectedLotForQr?.stockOutLineId){
- console.log(`🔄 [LOT CONFIRM] Updating stockOutLine ${selectedLotForQr.stockOutLineId} to 'checked'`);
- await updateStockOutLineStatus({
- id: selectedLotForQr.stockOutLineId,
- status: 'checked',
- qty: 0
- });
- console.log(`✅ [LOT CONFIRM] Stock out line ${selectedLotForQr.stockOutLineId} status updated to 'checked'`);
- }
-
- // Close modal and clean up state BEFORE refreshing
- setLotConfirmationOpen(false);
- setExpectedLotData(null);
- setScannedLotData(null);
- setSelectedLotForQr(null);
-
- // Clear QR processing state but DON'T clear processedQrCodes yet
- setQrScanError(false);
- setQrScanSuccess(true);
- setQrScanErrorMsg('');
- setQrScanInput('');
-
- // Set refreshing flag to prevent QR processing during refresh
- setIsRefreshingData(true);
-
- // Refresh data to show updated lot
- console.log("🔄 Refreshing job order data...");
- const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
- await fetchJobOrderData(pickOrderId);
- console.log(" Lot substitution confirmed and data refreshed");
-
- // Clear processed QR codes and flags immediately after refresh
- // This allows new QR codes to be processed right away
- setTimeout(() => {
- console.log(" Clearing processed QR codes and resuming scan");
- setProcessedQrCodes(new Set());
- setLastProcessedQr('');
- setQrScanSuccess(false);
- setIsRefreshingData(false);
- // ✅ Clear processedQrCombinations to allow reprocessing the same QR if needed
- if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
- setProcessedQrCombinations(prev => {
- const newMap = new Map(prev);
- const itemId = selectedLotForQr.itemId;
- if (itemId && newMap.has(itemId)) {
- newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
- if (newMap.get(itemId)!.size === 0) {
- newMap.delete(itemId);
- }
- }
- return newMap;
- });
- }
- }, 500); // Reduced from 3000ms to 500ms - just enough for UI update
-
- } catch (error) {
- console.error("Error confirming lot substitution:", error);
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg('换批发生异常,请重试或联系管理员');
- // Clear refresh flag on error
- setIsRefreshingData(false);
- } finally {
- setIsConfirmingLot(false);
- }
- }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData]);
-
- const processOutsideQrCode = useCallback(async (latestQr: string) => {
- // ✅ Only JSON QR supported for outside scanner (avoid false positive with lotNo)
- let qrData: any = null;
- try {
- qrData = JSON.parse(latestQr);
- } catch {
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- });
- return;
- }
-
- if (!(qrData?.stockInLineId && qrData?.itemId)) {
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- });
- return;
- }
-
- const scannedItemId = Number(qrData.itemId);
- const scannedStockInLineId = Number(qrData.stockInLineId);
-
- // ✅ avoid duplicate processing by itemId+stockInLineId
- const itemProcessedSet = processedQrCombinations.get(scannedItemId);
- if (itemProcessedSet?.has(scannedStockInLineId)) return;
-
- const indexes = lotDataIndexes;
- const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || [];
- // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected
- const allLotsForItem = indexes.byItemId.get(scannedItemId) || [];
-
- // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots
- // This allows users to scan other lots even when all suggested lots are rejected
- const scannedLot = allLotsForItem.find(
- (lot: any) => lot.stockInLineId === scannedStockInLineId
- );
-
- if (scannedLot) {
- const isRejected =
- scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
- scannedLot.lotAvailability === 'rejected' ||
- scannedLot.lotAvailability === 'status_unavailable';
-
- if (isRejected) {
- console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(
- `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
- );
- });
- // Mark as processed to prevent re-processing
- setProcessedQrCombinations(prev => {
- const newMap = new Map(prev);
- if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
- newMap.get(scannedItemId)!.add(scannedStockInLineId);
- return newMap;
- });
- return;
- }
- }
-
- // ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching
- if (activeSuggestedLots.length === 0) {
- // Check if there are any lots for this item (even if all are rejected)
- if (allLotsForItem.length === 0) {
- console.error("No lots found for this item");
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg("当前订单中没有此物品的批次信息");
- });
- return;
- }
-
- // ✅ Allow lot switching: find a rejected lot as expected lot, or use first lot
- // This allows users to switch to a new lot even when all suggested lots are rejected
- console.log(`⚠️ [QR PROCESS] No active suggested lots, but allowing lot switching. Scanned lot is not rejected.`);
-
- // Find a rejected lot as expected lot (the one that was rejected)
- const rejectedLot = allLotsForItem.find((lot: any) =>
- lot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
- lot.lotAvailability === 'rejected' ||
- lot.lotAvailability === 'status_unavailable'
- );
- const expectedLot = rejectedLot || allLotsForItem[0]; // Use rejected lot if exists, otherwise first lot
-
- // ✅ Always open confirmation modal when no active lots (user needs to confirm switching)
- // handleLotMismatch will fetch lotNo from backend using stockInLineId if needed
- console.log(`⚠️ [QR PROCESS] Opening confirmation modal for lot switch (no active lots)`);
- setSelectedLotForQr(expectedLot);
- handleLotMismatch(
- {
- lotNo: expectedLot.lotNo,
- itemCode: expectedLot.itemCode,
- itemName: expectedLot.itemName
- },
- {
- lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null
- itemCode: expectedLot.itemCode,
- itemName: expectedLot.itemName,
- inventoryLotLineId: scannedLot?.lotId || null,
- stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo
- }
- );
- return;
- }
-
- // ✅ direct stockInLineId match (O(1))
- const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || [];
- let exactMatch: any = null;
- for (let i = 0; i < stockInLineLots.length; i++) {
- const lot = stockInLineLots[i];
- if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) {
- exactMatch = lot;
- break;
- }
- }
-
- console.log(`🔍 [QR PROCESS] Scanned stockInLineId: ${scannedStockInLineId}, itemId: ${scannedItemId}`);
- console.log(`🔍 [QR PROCESS] Found ${stockInLineLots.length} lots with stockInLineId ${scannedStockInLineId}`);
- console.log(`🔍 [QR PROCESS] Exact match found: ${exactMatch ? `YES (lotNo: ${exactMatch.lotNo}, stockOutLineId: ${exactMatch.stockOutLineId})` : 'NO'}`);
-
- // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots
- // This handles the case where Lot A is rejected and user scans Lot B
- if (!exactMatch && scannedLot && !activeSuggestedLots.includes(scannedLot)) {
- // Scanned lot is not in active suggested lots, open confirmation modal
- const expectedLot = activeSuggestedLots[0] || allLotsForItem[0]; // Use first active lot or first lot as expected
- if (expectedLot && scannedLot.stockInLineId !== expectedLot.stockInLineId) {
- console.log(`⚠️ [QR PROCESS] Scanned lot ${scannedLot.lotNo} is not in active suggested lots, opening confirmation modal`);
- setSelectedLotForQr(expectedLot);
- handleLotMismatch(
- {
- lotNo: expectedLot.lotNo,
- itemCode: expectedLot.itemCode,
- itemName: expectedLot.itemName
- },
- {
- lotNo: scannedLot.lotNo || null,
- itemCode: expectedLot.itemCode,
- itemName: expectedLot.itemName,
- inventoryLotLineId: scannedLot.lotId || null,
- stockInLineId: scannedStockInLineId
- }
- );
- return;
- }
- }
-
- if (exactMatch) {
- if (!exactMatch.stockOutLineId) {
- console.error(`❌ [QR PROCESS] Exact match found but no stockOutLineId`);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- });
- return;
- }
-
- console.log(`✅ [QR PROCESS] Processing exact match: lotNo=${exactMatch.lotNo}, stockOutLineId=${exactMatch.stockOutLineId}`);
- try {
- const res = await updateStockOutLineStatusByQRCodeAndLotNo({
- pickOrderLineId: exactMatch.pickOrderLineId,
- inventoryLotNo: exactMatch.lotNo,
- stockOutLineId: exactMatch.stockOutLineId,
- itemId: exactMatch.itemId,
- status: "checked",
- });
-
- if (res.code === "checked" || res.code === "SUCCESS") {
- console.log(`✅ [QR PROCESS] Successfully updated stockOutLine ${exactMatch.stockOutLineId} to checked`);
- const entity = res.entity as any;
- startTransition(() => {
- setQrScanError(false);
- setQrScanSuccess(true);
- });
-
- // mark combination processed
- setProcessedQrCombinations(prev => {
- const newMap = new Map(prev);
- if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
- newMap.get(scannedItemId)!.add(scannedStockInLineId);
- return newMap;
- });
-
- // refresh to keep consistency with server & handler updates
- const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
- await fetchJobOrderData(pickOrderId);
- } else {
- console.error(`❌ [QR PROCESS] Update failed: ${res.code}`);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- });
- }
- } catch (error) {
- console.error(`❌ [QR PROCESS] Error updating stockOutLine:`, error);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- });
- }
- return;
- }
-
- // ✅ mismatch: validate scanned stockInLineId exists before opening confirmation modal
- console.log(`⚠️ [QR PROCESS] No exact match found. Validating scanned stockInLineId ${scannedStockInLineId} for itemId ${scannedItemId}`);
- console.log(`⚠️ [QR PROCESS] Active suggested lots for itemId ${scannedItemId}:`, activeSuggestedLots.map(l => ({ lotNo: l.lotNo, stockInLineId: l.stockInLineId })));
-
- if (activeSuggestedLots.length === 0) {
- console.error(`❌ [QR PROCESS] No active suggested lots found for itemId ${scannedItemId}`);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(`当前订单中没有 itemId ${scannedItemId} 的可用批次`);
- });
- return;
- }
-
- const expectedLot = activeSuggestedLots[0];
- console.log(`⚠️ [QR PROCESS] Expected lot: ${expectedLot.lotNo} (stockInLineId: ${expectedLot.stockInLineId}), Scanned stockInLineId: ${scannedStockInLineId}`);
-
- // ✅ Validate scanned stockInLineId exists before opening modal
- // This ensures the backend can find the lot when user confirms
- try {
- console.log(`🔍 [QR PROCESS] Validating scanned stockInLineId ${scannedStockInLineId} exists...`);
- const stockInLineInfo = await fetchStockInLineInfoCached(scannedStockInLineId);
- console.log(`✅ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} exists, lotNo: ${stockInLineInfo.lotNo}`);
-
- // ✅ 检查扫描的批次是否已被拒绝
- const scannedLot = combinedLotData.find(
- (lot: any) => lot.stockInLineId === scannedStockInLineId && lot.itemId === scannedItemId
- );
-
- if (scannedLot) {
- const isRejected =
- scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
- scannedLot.lotAvailability === 'rejected' ||
- scannedLot.lotAvailability === 'status_unavailable';
-
- if (isRejected) {
- console.warn(`⚠️ [QR PROCESS] Scanned lot ${stockInLineInfo.lotNo} (stockInLineId: ${scannedStockInLineId}) is rejected or unavailable`);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(
- `此批次(${stockInLineInfo.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
- );
- });
- // Mark as processed to prevent re-processing
- setProcessedQrCombinations(prev => {
- const newMap = new Map(prev);
- if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
- newMap.get(scannedItemId)!.add(scannedStockInLineId);
- return newMap;
- });
- return;
- }
- }
-
- // ✅ stockInLineId exists and is not rejected, open confirmation modal
- console.log(`⚠️ [QR PROCESS] Opening confirmation modal - user must confirm before any lot is marked as scanned`);
- setSelectedLotForQr(expectedLot);
- handleLotMismatch(
- {
- lotNo: expectedLot.lotNo,
- itemCode: expectedLot.itemCode,
- itemName: expectedLot.itemName
- },
- {
- lotNo: stockInLineInfo.lotNo || null, // Use fetched lotNo for display
- itemCode: expectedLot.itemCode,
- itemName: expectedLot.itemName,
- inventoryLotLineId: null,
- stockInLineId: scannedStockInLineId
- }
- );
- } catch (error) {
- // ✅ stockInLineId does NOT exist, show error immediately (don't open modal)
- console.error(`❌ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} does NOT exist:`, error);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(
- `扫描的 stockInLineId ${scannedStockInLineId} 不存在。请检查 QR 码是否正确,或联系管理员。`
- );
- });
- // Mark as processed to prevent re-processing
- setProcessedQrCombinations(prev => {
- const newMap = new Map(prev);
- if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
- newMap.get(scannedItemId)!.add(scannedStockInLineId);
- return newMap;
- });
- }
- }, [filterArgs?.pickOrderId, fetchJobOrderData, handleLotMismatch, lotDataIndexes, processedQrCombinations, combinedLotData, fetchStockInLineInfoCached]);
-
- // Store in refs for immediate access in qrValues effect
- processOutsideQrCodeRef.current = processOutsideQrCode;
- resetScanRef.current = 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 {
- 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
- const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
- await fetchJobOrderData(pickOrderId);
- } catch (error) {
- console.error("Error creating stock out line:", error);
- }
- }
- }, [selectedLotForQr, fetchJobOrderData]);
-
-
- useEffect(() => {
- // Skip if scanner not active or no data or currently refreshing
- if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) return;
-
- const latestQr = qrValues[qrValues.length - 1];
-
- // ✅ Test shortcut: {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId
- if ((latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) && latestQr.endsWith("}")) {
- let content = '';
- if (latestQr.startsWith("{2fittest")) content = latestQr.substring(9, latestQr.length - 1);
- else content = latestQr.substring(8, latestQr.length - 1);
-
- const parts = content.split(',');
- if (parts.length === 2) {
- const itemId = parseInt(parts[0].trim(), 10);
- const stockInLineId = parseInt(parts[1].trim(), 10);
- if (!isNaN(itemId) && !isNaN(stockInLineId)) {
- const simulatedQr = JSON.stringify({ itemId, stockInLineId });
-
- lastProcessedQrRef.current = latestQr;
- processedQrCodesRef.current.add(latestQr);
- setLastProcessedQr(latestQr);
- setProcessedQrCodes(new Set(processedQrCodesRef.current));
-
- processOutsideQrCodeRef.current?.(simulatedQr);
- resetScanRef.current?.();
- return;
- }
- }
- }
-
- // ✅ Shortcut: {2fic} open manual lot confirmation modal
- if (latestQr === "{2fic}") {
- setManualLotConfirmationOpen(true);
- resetScanRef.current?.();
- lastProcessedQrRef.current = latestQr;
- processedQrCodesRef.current.add(latestQr);
- setLastProcessedQr(latestQr);
- setProcessedQrCodes(new Set(processedQrCodesRef.current));
- return;
- }
-
- // Skip processing if modal open for same QR
- if (lotConfirmationOpen || manualLotConfirmationOpen) {
- if (latestQr === lastProcessedQrRef.current) return;
- }
-
- // Skip if already processed (refs)
- if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) return;
-
- // Mark processed immediately
- lastProcessedQrRef.current = latestQr;
- processedQrCodesRef.current.add(latestQr);
- if (processedQrCodesRef.current.size > 100) {
- const firstValue = processedQrCodesRef.current.values().next().value;
- if (firstValue !== undefined) processedQrCodesRef.current.delete(firstValue);
- }
-
- // Process immediately
- if (qrProcessingTimeoutRef.current) {
- clearTimeout(qrProcessingTimeoutRef.current);
- qrProcessingTimeoutRef.current = null;
- }
-
- processOutsideQrCodeRef.current?.(latestQr);
-
- // UI state updates (non-blocking)
- startTransition(() => {
- setLastProcessedQr(latestQr);
- setProcessedQrCodes(new Set(processedQrCodesRef.current));
- });
-
- return () => {
- if (qrProcessingTimeoutRef.current) {
- clearTimeout(qrProcessingTimeoutRef.current);
- qrProcessingTimeoutRef.current = null;
- }
- };
- }, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]);
-
- 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 [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
-
- const checkAndAutoAssignNext = useCallback(async () => {
- if (!currentUserId) return;
-
- try {
- const completionResponse = await checkPickOrderCompletion(currentUserId);
-
- if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
- console.log("Found completed pick orders, auto-assigning next...");
- // 移除前端的自动分配逻辑,因为后端已经处理了
- // await handleAutoAssignAndRelease(); // 删除这个函数
- }
- } catch (error) {
- console.error("Error checking pick order completion:", error);
- }
- }, [currentUserId]);
-
- // Handle submit pick quantity
- const handleSubmitPickQty = useCallback(async (lot: any) => {
- const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
- const newQty = pickQtyData[lotKey] || 0;
-
- if (!lot.stockOutLineId) {
- console.error("No stock out line found for this lot");
- return;
- }
-
- try {
- const currentActualPickQty = lot.actualPickQty || 0;
- const cumulativeQty = currentActualPickQty + newQty;
-
- let newStatus = 'partially_completed';
-
- if (cumulativeQty >= lot.requiredQty) {
- newStatus = 'completed';
- }
-
- console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
- console.log(`Lot: ${lot.lotNo}`);
- console.log(`Required Qty: ${lot.requiredQty}`);
- console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
- console.log(`New Submitted Qty: ${newQty}`);
- console.log(`Cumulative Qty: ${cumulativeQty}`);
- console.log(`New Status: ${newStatus}`);
- console.log(`=====================================`);
-
- await updateStockOutLineStatus({
- id: lot.stockOutLineId,
- status: newStatus,
- qty: cumulativeQty
- });
-
- if (newQty > 0) {
- await updateInventoryLotLineQuantities({
- inventoryLotLineId: lot.lotId,
- qty: newQty,
- status: 'available',
- operation: 'pick'
- });
- }
-
- // FIXED: Use the proper API function instead of direct fetch
- if (newStatus === 'completed' && lot.pickOrderConsoCode) {
- console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
-
- try {
- // Use the imported API function instead of direct fetch
- const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
- console.log(` Pick order completion check result:`, completionResponse);
-
- if (completionResponse.code === "SUCCESS") {
- console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
- } else if (completionResponse.message === "not completed") {
- console.log(`⏳ Pick order not completed yet, more lines remaining`);
- } else {
- console.error(`❌ Error checking completion: ${completionResponse.message}`);
- }
- } catch (error) {
- console.error("Error checking pick order completion:", error);
- }
- }
-
- const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
- await fetchJobOrderData(pickOrderId);
- console.log("Pick quantity submitted successfully!");
-
- setTimeout(() => {
- checkAndAutoAssignNext();
- }, 1000);
-
- } catch (error) {
- console.error("Error submitting pick quantity:", error);
- }
- }, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]);
- const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
- if (!lot.stockOutLineId) {
- console.error("No stock out line found for this lot");
- return;
- }
-
- try {
- // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0
- if (submitQty === 0) {
- console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
- console.log(`Lot: ${lot.lotNo}`);
- console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
- console.log(`Setting status to 'completed' with qty: 0`);
-
- const updateResult = await updateStockOutLineStatus({
- id: lot.stockOutLineId,
- status: 'completed',
- qty: 0
- });
-
- console.log('Update result:', updateResult);
- const r: any = updateResult as any;
- const updateOk =
- r?.code === 'SUCCESS' ||
- r?.type === 'completed' ||
- typeof r?.id === 'number' ||
- typeof r?.entity?.id === 'number' ||
- (r?.message && r.message.includes('successfully'));
- if (!updateResult || !updateOk) {
- console.error('Failed to update stock out line status:', updateResult);
- throw new Error('Failed to update stock out line status');
- }
-
-
- // Check if pick order is completed
- if (lot.pickOrderConsoCode) {
- console.log(` Lot ${lot.lotNo} completed (all zeros), checking if pick order ${lot.pickOrderConsoCode} is complete...`);
-
- try {
- 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!`);
- setTimeout(() => {
- if (onBackToList) {
- onBackToList();
- }
- }, 1500);
- } 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);
- }
- }
-
- const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
- await fetchJobOrderData(pickOrderId);
- console.log("All zeros submission completed successfully!");
-
- setTimeout(() => {
- checkAndAutoAssignNext();
- }, 1000);
-
- return;
- }
-
- // Normal case: Calculate cumulative quantity correctly
- const currentActualPickQty = lot.actualPickQty || 0;
- const cumulativeQty = currentActualPickQty + submitQty;
-
- // Determine status based on cumulative quantity vs required quantity
- let newStatus = 'partially_completed';
-
- if (cumulativeQty >= lot.requiredQty) {
- newStatus = 'completed';
- } else if (cumulativeQty > 0) {
- newStatus = 'partially_completed';
- } else {
- newStatus = 'checked'; // QR scanned but no quantity submitted yet
- }
-
- console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
- console.log(`Lot: ${lot.lotNo}`);
- console.log(`Required Qty: ${lot.requiredQty}`);
- console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
- console.log(`New Submitted Qty: ${submitQty}`);
- console.log(`Cumulative Qty: ${cumulativeQty}`);
- console.log(`New Status: ${newStatus}`);
- console.log(`=====================================`);
-
- await updateStockOutLineStatus({
- id: lot.stockOutLineId,
- status: newStatus,
- qty: cumulativeQty
- });
-
- if (submitQty > 0) {
- await updateInventoryLotLineQuantities({
- inventoryLotLineId: lot.lotId,
- qty: submitQty,
- status: 'available',
- operation: 'pick'
- });
- }
-
- // Check if pick order is completed when lot status becomes 'completed'
- if (newStatus === 'completed' && lot.pickOrderConsoCode) {
- console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
-
- try {
- 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!`);
- setTimeout(() => {
- if (onBackToList) {
- onBackToList();
- }
- }, 1500);
- } 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);
- }
- }
-
- const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
- await fetchJobOrderData(pickOrderId);
- console.log("Pick quantity submitted successfully!");
-
- setTimeout(() => {
- checkAndAutoAssignNext();
- }, 1000);
-
- } catch (error) {
- console.error("Error submitting pick quantity:", error);
- }
- }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]);
- const handleSkip = useCallback(async (lot: any) => {
- try {
- console.log("Skip clicked, submit 0 qty for lot:", lot.lotNo);
- await handleSubmitPickQtyWithQty(lot, 0);
- } catch (err) {
- console.error("Error in Skip:", err);
- }
- }, [handleSubmitPickQtyWithQty]);
- const handleSubmitAllScanned = useCallback(async () => {
- const scannedLots = combinedLotData.filter(lot =>
- lot.stockOutLineStatus === 'checked'
- );
-
- if (scannedLots.length === 0) {
- console.log("No scanned items to submit");
- return;
- }
-
- setIsSubmittingAll(true);
- console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`);
-
- try {
- // ✅ 转换为 batchSubmitList 所需的格式
- const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
- const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty || 0;
- const currentActualPickQty = lot.actualPickQty || 0;
- const cumulativeQty = currentActualPickQty + submitQty;
-
- let newStatus = 'partially_completed';
- if (cumulativeQty >= (lot.requiredQty || 0)) {
- newStatus = 'completed';
- }
-
- return {
- stockOutLineId: Number(lot.stockOutLineId) || 0,
- pickOrderLineId: Number(lot.pickOrderLineId),
- inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
- requiredQty: Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0),
- actualPickQty: Number(cumulativeQty),
- stockOutLineStatus: newStatus,
- pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
- noLot: Boolean(false) // Job Order 通常都有 lot
- };
- });
-
- const request: batchSubmitListRequest = {
- userId: currentUserId || 0,
- lines: lines
- };
-
- // ✅ 使用 batchSubmitList API
- const result = await batchSubmitList(request);
- console.log(`📥 Batch submit result:`, result);
-
- // 刷新数据
- const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
- await fetchJobOrderData(pickOrderId);
-
- if (result && result.code === "SUCCESS") {
- setQrScanSuccess(true);
- setTimeout(() => {
- setQrScanSuccess(false);
- checkAndAutoAssignNext();
- if (onBackToList) {
- onBackToList();
- }
- }, 2000);
- } else {
- console.error("Batch submit failed:", result);
- setQrScanError(true);
- }
-
- } catch (error) {
- console.error("Error submitting all scanned items:", error);
- setQrScanError(true);
- } finally {
- setIsSubmittingAll(false);
- }
- }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onBackToList])
-
- // Calculate scanned items count
- const scannedItemsCount = useMemo(() => {
- return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length;
- }, [combinedLotData]);
-
- // Progress bar data (align with Finished Good execution detail)
- const progress = useMemo(() => {
- if (combinedLotData.length === 0) {
- return { completed: 0, total: 0 };
- }
-
- const nonPendingCount = combinedLotData.filter((lot) => {
- const status = lot.stockOutLineStatus?.toLowerCase();
- return status !== 'pending';
- }).length;
-
- return {
- completed: nonPendingCount,
- total: combinedLotData.length,
- };
- }, [combinedLotData]);
- // Handle reject lot
- const handleRejectLot = useCallback(async (lot: any) => {
- if (!lot.stockOutLineId) {
- console.error("No stock out line found for this lot");
- return;
- }
-
- try {
- await updateStockOutLineStatus({
- id: lot.stockOutLineId,
- status: 'rejected',
- qty: 0
- });
-
- const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
- await fetchJobOrderData(pickOrderId);
- console.log("Lot rejected successfully!");
-
- setTimeout(() => {
- checkAndAutoAssignNext();
- }, 1000);
-
- } catch (error) {
- console.error("Error rejecting lot:", error);
- }
- }, [fetchJobOrderData, 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 issueData = {
- ...data,
- type: "Jo", // Delivery Order Record 类型
- };
-
- const result = await recordPickExecutionIssue(issueData);
- 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);
-
- const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
- await fetchJobOrderData(pickOrderId);
- } catch (error) {
- console.error("Error submitting pick execution form:", error);
- }
- }, [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 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(() => {
- return () => {
- // Cleanup when component unmounts (e.g., when switching tabs)
- if (isManualScanning) {
- console.log("🧹 Component unmounting, stopping QR scanner...");
- stopScan();
- resetScan();
- }
- };
- }, [isManualScanning, 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("🧹 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.lotAvailability !== 'rejected' &&
- lot.stockOutLineStatus !== 'rejected' &&
- lot.stockOutLineStatus !== 'completed'
- )}
- >
- <FormProvider {...formProps}>
- <Stack spacing={2}>
- {/* Progress bar + scan status fixed at top */}
- <Box
- sx={{
- position: 'fixed',
- top: 0,
- left: 0,
- right: 0,
- zIndex: 1100,
- backgroundColor: 'background.paper',
- pt: 2,
- pb: 1,
- px: 2,
- borderBottom: '1px solid',
- borderColor: 'divider',
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
- }}
- >
- <LinearProgressWithLabel
- completed={progress.completed}
- total={progress.total}
- label={t("Progress")}
- />
- <ScanStatusAlert
- error={qrScanError}
- success={qrScanSuccess}
- errorMessage={qrScanErrorMsg || t("QR code does not match any item in current orders.")}
- successMessage={t("QR code verified.")}
- />
- </Box>
-
- {/* 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 sx={{ mt: 10 }}>
- <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>
-
- <TableContainer component={Paper}>
- <Table>
- <TableHead>
- <TableRow>
- <TableCell>{t("Index")}</TableCell>
- <TableCell>{t("Route")}</TableCell>
- <TableCell>{t("Handler")}</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.handler || '-'}</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">
- {(() => {
- const status = lot.stockOutLineStatus?.toLowerCase();
- const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
- const isNoLot = !lot.lotNo;
-
- // ✅ rejected lot:显示红色勾选(已扫描但被拒绝)
- if (isRejected && !isNoLot) {
- return (
- <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
- <Checkbox
- checked={true}
- disabled={true}
- readOnly={true}
- size="large"
- sx={{
- color: 'error.main',
- '&.Mui-checked': { color: 'error.main' },
- transform: 'scale(1.3)',
- }}
- />
- </Box>
- );
- }
-
- // ✅ 正常 lot:已扫描(checked/partially_completed/completed)
- if (!isNoLot && status !== 'pending' && status !== 'rejected') {
- return (
- <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
- <Checkbox
- checked={true}
- disabled={true}
- readOnly={true}
- size="large"
- sx={{
- color: 'success.main',
- '&.Mui-checked': { color: 'success.main' },
- transform: 'scale(1.3)',
- }}
- />
- </Box>
- );
- }
-
- return null;
- })()}
- </TableCell>
-
- <TableCell align="center">
- <Box sx={{ display: 'flex', justifyContent: 'center' }}>
- {(() => {
- const status = lot.stockOutLineStatus?.toLowerCase();
- const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
- const isNoLot = !lot.lotNo;
-
- // ✅ rejected lot:显示提示文本(换行显示)
- if (isRejected && !isNoLot) {
- return (
- <Typography
- variant="body2"
- color="error.main"
- sx={{
- textAlign: 'center',
- whiteSpace: 'normal',
- wordBreak: 'break-word',
- maxWidth: '200px',
- lineHeight: 1.5
- }}
- >
- {t("This lot is rejected, please scan another lot.")}
- </Typography>
- );
- }
-
- // 正常 lot:显示按钮
- return (
- <Stack direction="row" spacing={1} alignItems="center">
- <Button
- variant="contained"
- onClick={() => {
- const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
- const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
- handlePickQtyChange(lotKey, submitQty);
- handleSubmitPickQtyWithQty(lot, submitQty);
- updateHandledBy(lot.pickOrderId, lot.itemId);
- }}
- disabled={
- (lot.lotAvailability === 'expired' ||
- lot.lotAvailability === 'status_unavailable' ||
- lot.lotAvailability === 'rejected') ||
- lot.stockOutLineStatus === 'completed' ||
- lot.stockOutLineStatus === 'pending'
- }
- sx={{
- fontSize: '0.75rem',
- py: 0.5,
- minHeight: '28px',
- minWidth: '70px'
- }}
- >
- {t("Submit")}
- </Button>
-
- <Button
- variant="outlined"
- size="small"
- onClick={() => handlePickExecutionForm(lot)}
- disabled={
- lot.stockOutLineStatus === 'completed'
- }
- sx={{
- fontSize: '0.7rem',
- py: 0.5,
- minHeight: '28px',
- minWidth: '60px',
- borderColor: 'warning.main',
- color: 'warning.main'
- }}
- title="Report missing or bad items"
- >
- {t("Edit")}
- </Button>
- <Button
- variant="outlined"
- size="small"
- onClick={() => handleSubmitPickQtyWithQty(lot, lot.requiredQty || lot.pickOrderLineRequiredQty || 0)}
- disabled={lot.stockOutLineStatus === 'completed'}
- sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }}
- >
- {t("Just Complete")}
- </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 */}
- {!lotConfirmationOpen && (
- <QrCodeModal
- open={qrModalOpen}
- onClose={() => {
- setQrModalOpen(false);
- setSelectedLotForQr(null);
- stopScan();
- resetScan();
- }}
- lot={selectedLotForQr}
- combinedLotData={combinedLotData}
- onQrCodeSubmit={handleQrCodeSubmitFromModal}
- />
- )}
- {/* Add Lot Confirmation Modal */}
- {lotConfirmationOpen && expectedLotData && scannedLotData && (
- <LotConfirmationModal
- open={lotConfirmationOpen}
- onClose={() => {
- console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`);
- setLotConfirmationOpen(false);
- setExpectedLotData(null);
- setScannedLotData(null);
- setSelectedLotForQr(null);
-
- // ✅ IMPORTANT: Clear refs and processedQrCombinations to allow reprocessing the same QR code
- // This allows the modal to reopen if user cancels and scans the same QR again
- setTimeout(() => {
- lastProcessedQrRef.current = '';
- processedQrCodesRef.current.clear();
-
- // Clear processedQrCombinations for this itemId+stockInLineId combination
- if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
- setProcessedQrCombinations(prev => {
- const newMap = new Map(prev);
- const itemId = selectedLotForQr.itemId;
- if (itemId && newMap.has(itemId)) {
- newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
- if (newMap.get(itemId)!.size === 0) {
- newMap.delete(itemId);
- }
- }
- return newMap;
- });
- }
-
- console.log(`⏱️ [LOT CONFIRM MODAL] Cleared refs and processedQrCombinations to allow reprocessing`);
- }, 100);
- }}
- onConfirm={handleLotConfirmation}
- expectedLot={expectedLotData}
- scannedLot={scannedLotData}
- isLoading={isConfirmingLot}
- />
- )}
-
- {/* Manual Lot Confirmation Modal (test shortcut {2fic}) */}
- <ManualLotConfirmationModal
- open={manualLotConfirmationOpen}
- onClose={() => setManualLotConfirmationOpen(false)}
- // Reuse existing handler: expectedLotInput=current lot, scannedLotInput=new lot
- onConfirm={(currentLotNo, newLotNo) => {
- // Use existing manual flow from handleManualLotConfirmation in other screens:
- // Here we route through updateStockOutLineStatusByQRCodeAndLotNo via handleManualLotConfirmation-like inline logic.
- // For now: open LotConfirmationModal path by setting expected/scanned and letting user confirm substitution.
- setExpectedLotData({ lotNo: currentLotNo, itemCode: '', itemName: '' });
- setScannedLotData({ lotNo: newLotNo, itemCode: '', itemName: '', inventoryLotLineId: null, stockInLineId: null });
- setManualLotConfirmationOpen(false);
- setLotConfirmationOpen(true);
- }}
- expectedLot={expectedLotData}
- scannedLot={scannedLotData}
- isLoading={isConfirmingLot}
- />
- {/* 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,
- uomDesc: selectedLotForExecutionForm.uomDesc || '',
- uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
- pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
- suggestedList: [],
- noLotLines: []
- }}
- pickOrderId={selectedLotForExecutionForm.pickOrderId}
- pickOrderCreateDate={new Date()}
- onNormalPickSubmit={async (lot, submitQty) => {
- console.log('onNormalPickSubmit called in newJobPickExecution:', { lot, submitQty });
- if (!lot) {
- console.error('Lot is null or undefined');
- return;
- }
- const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
- handlePickQtyChange(lotKey, submitQty);
- await handleSubmitPickQtyWithQty(lot, submitQty);
- }}
- />
- )}
- </FormProvider>
- </TestQrCodeProvider>
- );
- };
-
- export default JobPickExecution
|