FPSMS-frontend
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 

3401 righe
130 KiB

  1. "use client";
  2. import {
  3. Box,
  4. Button,
  5. Stack,
  6. TextField,
  7. Typography,
  8. Alert,
  9. CircularProgress,
  10. Table,
  11. TableBody,
  12. TableCell,
  13. TableContainer,
  14. TableHead,
  15. TableRow,
  16. Paper,
  17. Checkbox,
  18. TablePagination,
  19. Modal,
  20. Chip,
  21. } from "@mui/material";
  22. import dayjs from 'dayjs';
  23. import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider';
  24. import { fetchLotDetail } from "@/app/api/inventory/actions";
  25. import React, { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react";
  26. import { useTranslation } from "react-i18next";
  27. import { useRouter } from "next/navigation";
  28. import {
  29. updateStockOutLineStatus,
  30. createStockOutLine,
  31. updateStockOutLine,
  32. recordPickExecutionIssue,
  33. fetchFGPickOrders, // Add this import
  34. FGPickOrderResponse,
  35. stockReponse,
  36. PickExecutionIssueData,
  37. checkPickOrderCompletion,
  38. fetchAllPickOrderLotsHierarchical,
  39. PickOrderCompletionResponse,
  40. checkAndCompletePickOrderByConsoCode,
  41. updateSuggestedLotLineId,
  42. updateStockOutLineStatusByQRCodeAndLotNo,
  43. confirmLotSubstitution,
  44. fetchDoPickOrderDetail, // 必须添加
  45. DoPickOrderDetail, // 必须添加
  46. fetchFGPickOrdersByUserId ,
  47. batchQrSubmit,
  48. batchSubmitList, // 添加:导入 batchSubmitList
  49. batchSubmitListRequest, // 添加:导入类型
  50. batchSubmitListLineRequest,
  51. batchScan,
  52. BatchScanRequest,
  53. BatchScanLineRequest,
  54. } from "@/app/api/pickOrder/actions";
  55. import FGPickOrderInfoCard from "./FGPickOrderInfoCard";
  56. import LotConfirmationModal from "./LotConfirmationModal";
  57. //import { fetchItem } from "@/app/api/settings/item";
  58. import { updateInventoryLotLineStatus, analyzeQrCode } from "@/app/api/inventory/actions";
  59. import { fetchNameList, NameList } from "@/app/api/user/actions";
  60. import {
  61. FormProvider,
  62. useForm,
  63. } from "react-hook-form";
  64. import SearchBox, { Criterion } from "../SearchBox";
  65. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  66. import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  67. import QrCodeIcon from '@mui/icons-material/QrCode';
  68. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  69. import { useSession } from "next-auth/react";
  70. import { SessionWithTokens } from "@/config/authConfig";
  71. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  72. import GoodPickExecutionForm from "./GoodPickExecutionForm";
  73. import FGPickOrderCard from "./FGPickOrderCard";
  74. import LinearProgressWithLabel from "../common/LinearProgressWithLabel";
  75. import ScanStatusAlert from "../common/ScanStatusAlert";
  76. interface Props {
  77. filterArgs: Record<string, any>;
  78. onSwitchToRecordTab?: () => void;
  79. onRefreshReleasedOrderCount?: () => void;
  80. }
  81. // QR Code Modal Component (from LotTable)
  82. const QrCodeModal: React.FC<{
  83. open: boolean;
  84. onClose: () => void;
  85. lot: any | null;
  86. onQrCodeSubmit: (lotNo: string) => void;
  87. combinedLotData: any[]; // Add this prop
  88. lotConfirmationOpen: boolean;
  89. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData,lotConfirmationOpen = false }) => {
  90. const { t } = useTranslation("pickOrder");
  91. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  92. const [manualInput, setManualInput] = useState<string>('');
  93. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  94. const [manualInputError, setManualInputError] = useState<boolean>(false);
  95. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  96. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  97. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  98. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  99. const [scannedQrResult, setScannedQrResult] = useState<string>('');
  100. const [fgPickOrder, setFgPickOrder] = useState<FGPickOrderResponse | null>(null);
  101. const fetchingRef = useRef<Set<number>>(new Set());
  102. // Process scanned QR codes
  103. useEffect(() => {
  104. // ✅ Don't process if modal is not open
  105. if (!open) {
  106. return;
  107. }
  108. // ✅ Don't process if lot confirmation modal is open
  109. if (lotConfirmationOpen) {
  110. console.log("Lot confirmation modal is open, skipping QrCodeModal processing...");
  111. return;
  112. }
  113. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  114. const latestQr = qrValues[qrValues.length - 1];
  115. if (processedQrCodes.has(latestQr)) {
  116. console.log("QR code already processed, skipping...");
  117. return;
  118. }
  119. try {
  120. const qrData = JSON.parse(latestQr);
  121. if (qrData.stockInLineId && qrData.itemId) {
  122. // ✅ Check if we're already fetching this stockInLineId
  123. if (fetchingRef.current.has(qrData.stockInLineId)) {
  124. console.log(`⏱️ [QR MODAL] Already fetching stockInLineId: ${qrData.stockInLineId}, skipping duplicate call`);
  125. return;
  126. }
  127. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  128. setIsProcessingQr(true);
  129. setQrScanFailed(false);
  130. // ✅ Mark as fetching
  131. fetchingRef.current.add(qrData.stockInLineId);
  132. const fetchStartTime = performance.now();
  133. console.log(`⏱️ [QR MODAL] Starting fetchStockInLineInfo for stockInLineId: ${qrData.stockInLineId}`);
  134. fetchStockInLineInfo(qrData.stockInLineId)
  135. .then((stockInLineInfo) => {
  136. // ✅ Remove from fetching set
  137. fetchingRef.current.delete(qrData.stockInLineId);
  138. // ✅ Check again if modal is still open and lot confirmation is not open
  139. if (!open || lotConfirmationOpen) {
  140. console.log("Modal state changed, skipping result processing");
  141. return;
  142. }
  143. const fetchTime = performance.now() - fetchStartTime;
  144. console.log(`⏱️ [QR MODAL] fetchStockInLineInfo time: ${fetchTime.toFixed(2)}ms (${(fetchTime / 1000).toFixed(3)}s)`);
  145. console.log("Stock in line info:", stockInLineInfo);
  146. setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
  147. if (stockInLineInfo.lotNo === lot.lotNo) {
  148. console.log(` QR Code verified for lot: ${lot.lotNo}`);
  149. setQrScanSuccess(true);
  150. onQrCodeSubmit(lot.lotNo);
  151. onClose();
  152. resetScan();
  153. } else {
  154. console.log(` QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
  155. setQrScanFailed(true);
  156. setManualInputError(true);
  157. setManualInputSubmitted(true);
  158. }
  159. })
  160. .catch((error) => {
  161. // ✅ Remove from fetching set
  162. fetchingRef.current.delete(qrData.stockInLineId);
  163. // ✅ Check again if modal is still open
  164. if (!open || lotConfirmationOpen) {
  165. console.log("Modal state changed, skipping error handling");
  166. return;
  167. }
  168. const fetchTime = performance.now() - fetchStartTime;
  169. console.error(`❌ [QR MODAL] fetchStockInLineInfo failed after ${fetchTime.toFixed(2)}ms:`, error);
  170. setScannedQrResult('Error fetching data');
  171. setQrScanFailed(true);
  172. setManualInputError(true);
  173. setManualInputSubmitted(true);
  174. })
  175. .finally(() => {
  176. setIsProcessingQr(false);
  177. });
  178. } else {
  179. const qrContent = latestQr.replace(/[{}]/g, '');
  180. setScannedQrResult(qrContent);
  181. if (qrContent === lot.lotNo) {
  182. setQrScanSuccess(true);
  183. onQrCodeSubmit(lot.lotNo);
  184. onClose();
  185. resetScan();
  186. } else {
  187. setQrScanFailed(true);
  188. setManualInputError(true);
  189. setManualInputSubmitted(true);
  190. }
  191. }
  192. } catch (error) {
  193. console.log("QR code is not JSON format, trying direct comparison");
  194. const qrContent = latestQr.replace(/[{}]/g, '');
  195. setScannedQrResult(qrContent);
  196. if (qrContent === lot.lotNo) {
  197. setQrScanSuccess(true);
  198. onQrCodeSubmit(lot.lotNo);
  199. onClose();
  200. resetScan();
  201. } else {
  202. setQrScanFailed(true);
  203. setManualInputError(true);
  204. setManualInputSubmitted(true);
  205. }
  206. }
  207. }
  208. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes, lotConfirmationOpen, open]);
  209. // Clear states when modal opens
  210. useEffect(() => {
  211. if (open) {
  212. setManualInput('');
  213. setManualInputSubmitted(false);
  214. setManualInputError(false);
  215. setIsProcessingQr(false);
  216. setQrScanFailed(false);
  217. setQrScanSuccess(false);
  218. setScannedQrResult('');
  219. setProcessedQrCodes(new Set());
  220. }
  221. }, [open]);
  222. useEffect(() => {
  223. if (lot) {
  224. setManualInput('');
  225. setManualInputSubmitted(false);
  226. setManualInputError(false);
  227. setIsProcessingQr(false);
  228. setQrScanFailed(false);
  229. setQrScanSuccess(false);
  230. setScannedQrResult('');
  231. setProcessedQrCodes(new Set());
  232. }
  233. }, [lot]);
  234. // Auto-submit manual input when it matches
  235. useEffect(() => {
  236. if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
  237. console.log(' Auto-submitting manual input:', manualInput.trim());
  238. const timer = setTimeout(() => {
  239. setQrScanSuccess(true);
  240. onQrCodeSubmit(lot.lotNo);
  241. onClose();
  242. setManualInput('');
  243. setManualInputError(false);
  244. setManualInputSubmitted(false);
  245. }, 200);
  246. return () => clearTimeout(timer);
  247. }
  248. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  249. const handleManualSubmit = () => {
  250. if (manualInput.trim() === lot?.lotNo) {
  251. setQrScanSuccess(true);
  252. onQrCodeSubmit(lot.lotNo);
  253. onClose();
  254. setManualInput('');
  255. } else {
  256. setQrScanFailed(true);
  257. setManualInputError(true);
  258. setManualInputSubmitted(true);
  259. }
  260. };
  261. useEffect(() => {
  262. if (open) {
  263. startScan();
  264. }
  265. }, [open, startScan]);
  266. return (
  267. <Modal open={open} onClose={onClose}>
  268. <Box sx={{
  269. position: 'absolute',
  270. top: '50%',
  271. left: '50%',
  272. transform: 'translate(-50%, -50%)',
  273. bgcolor: 'background.paper',
  274. p: 3,
  275. borderRadius: 2,
  276. minWidth: 400,
  277. }}>
  278. <Typography variant="h6" gutterBottom>
  279. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  280. </Typography>
  281. {isProcessingQr && (
  282. <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
  283. <Typography variant="body2" color="primary">
  284. {t("Processing QR code...")}
  285. </Typography>
  286. </Box>
  287. )}
  288. <Box sx={{ mb: 2 }}>
  289. <Typography variant="body2" gutterBottom>
  290. <strong>{t("Manual Input")}:</strong>
  291. </Typography>
  292. <TextField
  293. fullWidth
  294. size="small"
  295. value={manualInput}
  296. onChange={(e) => {
  297. setManualInput(e.target.value);
  298. if (qrScanFailed || manualInputError) {
  299. setQrScanFailed(false);
  300. setManualInputError(false);
  301. setManualInputSubmitted(false);
  302. }
  303. }}
  304. sx={{ mb: 1 }}
  305. error={manualInputSubmitted && manualInputError}
  306. helperText={
  307. manualInputSubmitted && manualInputError
  308. ? `${t("The input is not the same as the expected lot number.")}`
  309. : ''
  310. }
  311. />
  312. <Button
  313. variant="contained"
  314. onClick={handleManualSubmit}
  315. disabled={!manualInput.trim()}
  316. size="small"
  317. color="primary"
  318. >
  319. {t("Submit")}
  320. </Button>
  321. </Box>
  322. {qrValues.length > 0 && (
  323. <Box sx={{
  324. mb: 2,
  325. p: 2,
  326. backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
  327. borderRadius: 1
  328. }}>
  329. <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
  330. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  331. </Typography>
  332. {qrScanSuccess && (
  333. <Typography variant="caption" color="success" display="block">
  334. {t("Verified successfully!")}
  335. </Typography>
  336. )}
  337. </Box>
  338. )}
  339. <Box sx={{ mt: 2, textAlign: 'right' }}>
  340. <Button onClick={onClose} variant="outlined">
  341. {t("Cancel")}
  342. </Button>
  343. </Box>
  344. </Box>
  345. </Modal>
  346. );
  347. };
  348. const ManualLotConfirmationModal: React.FC<{
  349. open: boolean;
  350. onClose: () => void;
  351. onConfirm: (expectedLotNo: string, scannedLotNo: string) => void;
  352. expectedLot: {
  353. lotNo: string;
  354. itemCode: string;
  355. itemName: string;
  356. } | null;
  357. scannedLot: {
  358. lotNo: string;
  359. itemCode: string;
  360. itemName: string;
  361. } | null;
  362. isLoading?: boolean;
  363. }> = ({ open, onClose, onConfirm, expectedLot, scannedLot, isLoading = false }) => {
  364. const { t } = useTranslation("pickOrder");
  365. const [expectedLotInput, setExpectedLotInput] = useState<string>('');
  366. const [scannedLotInput, setScannedLotInput] = useState<string>('');
  367. const [error, setError] = useState<string>('');
  368. // 当模态框打开时,预填充输入框
  369. useEffect(() => {
  370. if (open) {
  371. setExpectedLotInput(expectedLot?.lotNo || '');
  372. setScannedLotInput(scannedLot?.lotNo || '');
  373. setError('');
  374. }
  375. }, [open, expectedLot, scannedLot]);
  376. const handleConfirm = () => {
  377. if (!expectedLotInput.trim() || !scannedLotInput.trim()) {
  378. setError(t("Please enter both expected and scanned lot numbers."));
  379. return;
  380. }
  381. if (expectedLotInput.trim() === scannedLotInput.trim()) {
  382. setError(t("Expected and scanned lot numbers cannot be the same."));
  383. return;
  384. }
  385. onConfirm(expectedLotInput.trim(), scannedLotInput.trim());
  386. };
  387. return (
  388. <Modal open={open} onClose={onClose}>
  389. <Box sx={{
  390. position: 'absolute',
  391. top: '50%',
  392. left: '50%',
  393. transform: 'translate(-50%, -50%)',
  394. bgcolor: 'background.paper',
  395. p: 3,
  396. borderRadius: 2,
  397. minWidth: 500,
  398. }}>
  399. <Typography variant="h6" gutterBottom color="warning.main">
  400. {t("Manual Lot Confirmation")}
  401. </Typography>
  402. <Box sx={{ mb: 2 }}>
  403. <Typography variant="body2" gutterBottom>
  404. <strong>{t("Expected Lot Number")}:</strong>
  405. </Typography>
  406. <TextField
  407. fullWidth
  408. size="small"
  409. value={expectedLotInput}
  410. onChange={(e) => {
  411. setExpectedLotInput(e.target.value);
  412. setError('');
  413. }}
  414. placeholder={expectedLot?.lotNo || t("Enter expected lot number")}
  415. sx={{ mb: 2 }}
  416. error={!!error && !expectedLotInput.trim()}
  417. />
  418. </Box>
  419. <Box sx={{ mb: 2 }}>
  420. <Typography variant="body2" gutterBottom>
  421. <strong>{t("Scanned Lot Number")}:</strong>
  422. </Typography>
  423. <TextField
  424. fullWidth
  425. size="small"
  426. value={scannedLotInput}
  427. onChange={(e) => {
  428. setScannedLotInput(e.target.value);
  429. setError('');
  430. }}
  431. placeholder={scannedLot?.lotNo || t("Enter scanned lot number")}
  432. sx={{ mb: 2 }}
  433. error={!!error && !scannedLotInput.trim()}
  434. />
  435. </Box>
  436. {error && (
  437. <Box sx={{ mb: 2, p: 1, backgroundColor: '#ffebee', borderRadius: 1 }}>
  438. <Typography variant="body2" color="error">
  439. {error}
  440. </Typography>
  441. </Box>
  442. )}
  443. <Box sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
  444. <Button onClick={onClose} variant="outlined" disabled={isLoading}>
  445. {t("Cancel")}
  446. </Button>
  447. <Button
  448. onClick={handleConfirm}
  449. variant="contained"
  450. color="warning"
  451. disabled={isLoading || !expectedLotInput.trim() || !scannedLotInput.trim()}
  452. >
  453. {isLoading ? t("Processing...") : t("Confirm")}
  454. </Button>
  455. </Box>
  456. </Box>
  457. </Modal>
  458. );
  459. };
  460. const PickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab, onRefreshReleasedOrderCount }) => {
  461. const { t } = useTranslation("pickOrder");
  462. const router = useRouter();
  463. const { data: session } = useSession() as { data: SessionWithTokens | null };
  464. const [doPickOrderDetail, setDoPickOrderDetail] = useState<DoPickOrderDetail | null>(null);
  465. const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>(null);
  466. const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
  467. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  468. const [allLotsCompleted, setAllLotsCompleted] = useState(false);
  469. const [combinedLotData, setCombinedLotData] = useState<any[]>([]);
  470. const [combinedDataLoading, setCombinedDataLoading] = useState(false);
  471. const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
  472. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  473. const [qrScanInput, setQrScanInput] = useState<string>('');
  474. const [qrScanError, setQrScanError] = useState<boolean>(false);
  475. const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>('');
  476. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  477. const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false);
  478. const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
  479. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  480. const [paginationController, setPaginationController] = useState({
  481. pageNum: 0,
  482. pageSize: -1,
  483. });
  484. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  485. const initializationRef = useRef(false);
  486. const autoAssignRef = useRef(false);
  487. const formProps = useForm();
  488. const errors = formProps.formState.errors;
  489. // QR scanner states (always-on, no modal)
  490. const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
  491. const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
  492. const [expectedLotData, setExpectedLotData] = useState<any>(null);
  493. const [scannedLotData, setScannedLotData] = useState<any>(null);
  494. const [isConfirmingLot, setIsConfirmingLot] = useState(false);
  495. // Add GoodPickExecutionForm states
  496. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  497. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
  498. const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
  499. const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
  500. // Add these missing state variables after line 352
  501. const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
  502. // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling
  503. const [processedQrCombinations, setProcessedQrCombinations] = useState<Map<number, Set<number>>>(new Map());
  504. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  505. const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
  506. const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
  507. const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
  508. // Cache for fetchStockInLineInfo API calls to avoid redundant requests
  509. const stockInLineInfoCache = useRef<Map<number, { lotNo: string | null; timestamp: number }>>(new Map());
  510. const CACHE_TTL = 60000; // 60 seconds cache TTL
  511. const abortControllerRef = useRef<AbortController | null>(null);
  512. const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  513. // Use refs for processed QR tracking to avoid useEffect dependency issues and delays
  514. const processedQrCodesRef = useRef<Set<string>>(new Set());
  515. const lastProcessedQrRef = useRef<string>('');
  516. // Store callbacks in refs to avoid useEffect dependency issues
  517. const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | null>(null);
  518. const resetScanRef = useRef<(() => void) | null>(null);
  519. // Handle QR code button click
  520. const handleQrCodeClick = (pickOrderId: number) => {
  521. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  522. // TODO: Implement QR code functionality
  523. };
  524. const progress = useMemo(() => {
  525. if (combinedLotData.length === 0) {
  526. return { completed: 0, total: 0 };
  527. }
  528. const nonPendingCount = combinedLotData.filter(lot => {
  529. const status = lot.stockOutLineStatus?.toLowerCase();
  530. return status !== 'pending';
  531. }).length;
  532. return {
  533. completed: nonPendingCount,
  534. total: combinedLotData.length
  535. };
  536. }, [combinedLotData]);
  537. // Cached version of fetchStockInLineInfo to avoid redundant API calls
  538. const fetchStockInLineInfoCached = useCallback(async (stockInLineId: number): Promise<{ lotNo: string | null }> => {
  539. const now = Date.now();
  540. const cached = stockInLineInfoCache.current.get(stockInLineId);
  541. // Return cached value if still valid
  542. if (cached && (now - cached.timestamp) < CACHE_TTL) {
  543. console.log(`✅ [CACHE HIT] Using cached stockInLineInfo for ${stockInLineId}`);
  544. return { lotNo: cached.lotNo };
  545. }
  546. // Cancel previous request if exists
  547. if (abortControllerRef.current) {
  548. abortControllerRef.current.abort();
  549. }
  550. // Create new abort controller for this request
  551. const abortController = new AbortController();
  552. abortControllerRef.current = abortController;
  553. try {
  554. console.log(`⏱️ [CACHE MISS] Fetching stockInLineInfo for ${stockInLineId}`);
  555. const stockInLineInfo = await fetchStockInLineInfo(stockInLineId);
  556. // Store in cache
  557. stockInLineInfoCache.current.set(stockInLineId, {
  558. lotNo: stockInLineInfo.lotNo || null,
  559. timestamp: now
  560. });
  561. // Limit cache size to prevent memory leaks
  562. if (stockInLineInfoCache.current.size > 100) {
  563. const firstKey = stockInLineInfoCache.current.keys().next().value;
  564. if (firstKey !== undefined) {
  565. stockInLineInfoCache.current.delete(firstKey);
  566. }
  567. }
  568. return { lotNo: stockInLineInfo.lotNo || null };
  569. } catch (error: any) {
  570. if (error.name === 'AbortError') {
  571. console.log(`⏱️ [CACHE] Request aborted for ${stockInLineId}`);
  572. throw error;
  573. }
  574. console.error(`❌ [CACHE] Error fetching stockInLineInfo for ${stockInLineId}:`, error);
  575. throw error;
  576. }
  577. }, []);
  578. const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => {
  579. const mismatchStartTime = performance.now();
  580. console.log(`⏱️ [HANDLE LOT MISMATCH START]`);
  581. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  582. console.log("Lot mismatch detected:", { expectedLot, scannedLot });
  583. // ✅ Use setTimeout to avoid flushSync warning - schedule modal update in next tick
  584. const setTimeoutStartTime = performance.now();
  585. console.time('setLotConfirmationOpen');
  586. setTimeout(() => {
  587. const setStateStartTime = performance.now();
  588. setExpectedLotData(expectedLot);
  589. setScannedLotData({
  590. ...scannedLot,
  591. lotNo: scannedLot.lotNo || null,
  592. });
  593. setLotConfirmationOpen(true);
  594. const setStateTime = performance.now() - setStateStartTime;
  595. console.timeEnd('setLotConfirmationOpen');
  596. console.log(`⏱️ [HANDLE LOT MISMATCH] Modal state set to open (setState time: ${setStateTime.toFixed(2)}ms)`);
  597. console.log(`✅ [HANDLE LOT MISMATCH] Modal state set to open`);
  598. }, 0);
  599. const setTimeoutTime = performance.now() - setTimeoutStartTime;
  600. console.log(`⏱️ [PERF] setTimeout scheduling time: ${setTimeoutTime.toFixed(2)}ms`);
  601. // ✅ Fetch lotNo in background ONLY for display purposes (using cached version)
  602. if (!scannedLot.lotNo && scannedLot.stockInLineId) {
  603. const stockInLineId = scannedLot.stockInLineId;
  604. if (typeof stockInLineId !== 'number') {
  605. console.warn(`⏱️ [HANDLE LOT MISMATCH] Invalid stockInLineId: ${stockInLineId}`);
  606. return;
  607. }
  608. console.log(`⏱️ [HANDLE LOT MISMATCH] Fetching lotNo in background (stockInLineId: ${stockInLineId})`);
  609. const fetchStartTime = performance.now();
  610. fetchStockInLineInfoCached(stockInLineId)
  611. .then((stockInLineInfo) => {
  612. const fetchTime = performance.now() - fetchStartTime;
  613. console.log(`⏱️ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached time: ${fetchTime.toFixed(2)}ms (${(fetchTime / 1000).toFixed(3)}s)`);
  614. const updateStateStartTime = performance.now();
  615. startTransition(() => {
  616. setScannedLotData((prev: any) => ({
  617. ...prev,
  618. lotNo: stockInLineInfo.lotNo || null,
  619. }));
  620. });
  621. const updateStateTime = performance.now() - updateStateStartTime;
  622. console.log(`⏱️ [PERF] Update scanned lot data time: ${updateStateTime.toFixed(2)}ms`);
  623. const totalTime = performance.now() - mismatchStartTime;
  624. console.log(`⏱️ [HANDLE LOT MISMATCH] Background fetch completed: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  625. })
  626. .catch((error) => {
  627. if (error.name !== 'AbortError') {
  628. const fetchTime = performance.now() - fetchStartTime;
  629. console.error(`❌ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached failed after ${fetchTime.toFixed(2)}ms:`, error);
  630. }
  631. });
  632. } else {
  633. const totalTime = performance.now() - mismatchStartTime;
  634. console.log(`⏱️ [HANDLE LOT MISMATCH END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  635. }
  636. }, [fetchStockInLineInfoCached]);
  637. const checkAllLotsCompleted = useCallback((lotData: any[]) => {
  638. if (lotData.length === 0) {
  639. setAllLotsCompleted(false);
  640. return false;
  641. }
  642. // Filter out rejected lots
  643. const nonRejectedLots = lotData.filter(lot =>
  644. lot.lotAvailability !== 'rejected' &&
  645. lot.stockOutLineStatus !== 'rejected'
  646. );
  647. if (nonRejectedLots.length === 0) {
  648. setAllLotsCompleted(false);
  649. return false;
  650. }
  651. // Check if all non-rejected lots are completed
  652. const allCompleted = nonRejectedLots.every(lot =>
  653. lot.stockOutLineStatus === 'completed'
  654. );
  655. setAllLotsCompleted(allCompleted);
  656. return allCompleted;
  657. }, []);
  658. // 在 fetchAllCombinedLotData 函数中(约 446-684 行)
  659. const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdOverride?: number) => {
  660. setCombinedDataLoading(true);
  661. try {
  662. const userIdToUse = userId || currentUserId;
  663. console.log(" fetchAllCombinedLotData called with userId:", userIdToUse);
  664. if (!userIdToUse) {
  665. console.warn("⚠️ No userId available, skipping API call");
  666. setCombinedLotData([]);
  667. setOriginalCombinedData([]);
  668. setAllLotsCompleted(false);
  669. return;
  670. }
  671. // 获取新结构的层级数据
  672. const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse);
  673. console.log(" Hierarchical data (new structure):", hierarchicalData);
  674. // 检查数据结构
  675. if (!hierarchicalData.fgInfo || !hierarchicalData.pickOrders || hierarchicalData.pickOrders.length === 0) {
  676. console.warn("⚠️ No FG info or pick orders found");
  677. setCombinedLotData([]);
  678. setOriginalCombinedData([]);
  679. setAllLotsCompleted(false);
  680. return;
  681. }
  682. // 使用合并后的 pick order 对象(现在只有一个对象,包含所有数据)
  683. const mergedPickOrder = hierarchicalData.pickOrders[0];
  684. // 设置 FG info 到 fgPickOrders(用于显示 FG 信息卡片)
  685. // 修改第 478-509 行的 fgOrder 构建逻辑:
  686. const fgOrder: FGPickOrderResponse = {
  687. doPickOrderId: hierarchicalData.fgInfo.doPickOrderId,
  688. ticketNo: hierarchicalData.fgInfo.ticketNo,
  689. storeId: hierarchicalData.fgInfo.storeId,
  690. shopCode: hierarchicalData.fgInfo.shopCode,
  691. shopName: hierarchicalData.fgInfo.shopName,
  692. truckLanceCode: hierarchicalData.fgInfo.truckLanceCode,
  693. DepartureTime: hierarchicalData.fgInfo.departureTime,
  694. shopAddress: "",
  695. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  696. // 兼容字段
  697. pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0,
  698. pickOrderConsoCode: mergedPickOrder.consoCode || "",
  699. pickOrderTargetDate: mergedPickOrder.targetDate || "",
  700. pickOrderStatus: mergedPickOrder.status || "",
  701. deliveryOrderId: mergedPickOrder.doOrderIds?.[0] || 0,
  702. deliveryNo: mergedPickOrder.deliveryOrderCodes?.[0] || "",
  703. deliveryDate: "",
  704. shopId: 0,
  705. shopPoNo: "",
  706. numberOfCartons: mergedPickOrder.pickOrderLines?.length || 0,
  707. qrCodeData: hierarchicalData.fgInfo.doPickOrderId,
  708. // 新增:多个 pick orders 信息 - 保持数组格式,不要 join
  709. numberOfPickOrders: mergedPickOrder.pickOrderIds?.length || 0,
  710. pickOrderIds: mergedPickOrder.pickOrderIds || [],
  711. pickOrderCodes: Array.isArray(mergedPickOrder.pickOrderCodes)
  712. ? mergedPickOrder.pickOrderCodes
  713. : [], // 改:保持数组
  714. deliveryOrderIds: mergedPickOrder.doOrderIds || [],
  715. deliveryNos: Array.isArray(mergedPickOrder.deliveryOrderCodes)
  716. ? mergedPickOrder.deliveryOrderCodes
  717. : [], // 改:保持数组
  718. lineCountsPerPickOrder: Array.isArray(mergedPickOrder.lineCountsPerPickOrder)
  719. ? mergedPickOrder.lineCountsPerPickOrder
  720. : []
  721. };
  722. setFgPickOrders([fgOrder]);
  723. console.log(" DEBUG fgOrder.lineCountsPerPickOrder:", fgOrder.lineCountsPerPickOrder);
  724. console.log(" DEBUG fgOrder.pickOrderCodes:", fgOrder.pickOrderCodes);
  725. console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
  726. // 移除:不需要 doPickOrderDetail 和 switcher 逻辑
  727. // if (hierarchicalData.pickOrders.length > 1) { ... }
  728. // 直接使用合并后的 pickOrderLines
  729. console.log("🎯 Displaying merged pick order lines");
  730. // 将层级数据转换为平铺格式(用于表格显示)
  731. const flatLotData: any[] = [];
  732. mergedPickOrder.pickOrderLines.forEach((line: any) => {
  733. // ✅ FIXED: 处理 lots(如果有)
  734. if (line.lots && line.lots.length > 0) {
  735. // 修复:先对 lots 按 lotId 去重并合并 requiredQty
  736. const lotMap = new Map<number, any>();
  737. line.lots.forEach((lot: any) => {
  738. const lotId = lot.id;
  739. if (lotMap.has(lotId)) {
  740. // 如果已存在,合并 requiredQty
  741. const existingLot = lotMap.get(lotId);
  742. existingLot.requiredQty = (existingLot.requiredQty || 0) + (lot.requiredQty || 0);
  743. // 保留其他字段(使用第一个遇到的 lot 的字段)
  744. } else {
  745. // 首次遇到,添加到 map
  746. lotMap.set(lotId, { ...lot });
  747. }
  748. });
  749. // 遍历去重后的 lots
  750. lotMap.forEach((lot: any) => {
  751. flatLotData.push({
  752. // 使用合并后的数据
  753. pickOrderConsoCode: mergedPickOrder.consoCode,
  754. pickOrderTargetDate: mergedPickOrder.targetDate,
  755. pickOrderStatus: mergedPickOrder.status,
  756. pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0, // 使用第一个 pickOrderId
  757. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  758. pickOrderLineId: line.id,
  759. pickOrderLineRequiredQty: line.requiredQty,
  760. pickOrderLineStatus: line.status,
  761. itemId: line.item.id,
  762. itemCode: line.item.code,
  763. itemName: line.item.name,
  764. uomDesc: line.item.uomDesc,
  765. uomShortDesc: line.item.uomShortDesc,
  766. lotId: lot.id,
  767. lotNo: lot.lotNo,
  768. expiryDate: lot.expiryDate,
  769. location: lot.location,
  770. stockUnit: lot.stockUnit,
  771. availableQty: lot.availableQty,
  772. requiredQty: lot.requiredQty, // 使用合并后的 requiredQty
  773. actualPickQty: lot.actualPickQty,
  774. inQty: lot.inQty,
  775. outQty: lot.outQty,
  776. holdQty: lot.holdQty,
  777. lotStatus: lot.lotStatus,
  778. lotAvailability: lot.lotAvailability,
  779. processingStatus: lot.processingStatus,
  780. suggestedPickLotId: lot.suggestedPickLotId,
  781. stockOutLineId: lot.stockOutLineId,
  782. stockOutLineStatus: lot.stockOutLineStatus,
  783. stockOutLineQty: lot.stockOutLineQty,
  784. stockInLineId: lot.stockInLineId,
  785. routerId: lot.router?.id,
  786. routerIndex: lot.router?.index,
  787. routerRoute: lot.router?.route,
  788. routerArea: lot.router?.area,
  789. noLot: false,
  790. });
  791. });
  792. }
  793. // ✅ FIXED: 同时处理 stockouts(无论是否有 lots)
  794. if (line.stockouts && line.stockouts.length > 0) {
  795. // ✅ FIXED: 处理所有 stockouts,而不仅仅是第一个
  796. line.stockouts.forEach((stockout: any) => {
  797. flatLotData.push({
  798. pickOrderConsoCode: mergedPickOrder.consoCodes?.[0] || "",
  799. pickOrderTargetDate: mergedPickOrder.targetDate,
  800. pickOrderStatus: mergedPickOrder.status,
  801. pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0,
  802. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  803. pickOrderLineId: line.id,
  804. pickOrderLineRequiredQty: line.requiredQty,
  805. pickOrderLineStatus: line.status,
  806. itemId: line.item.id,
  807. itemCode: line.item.code,
  808. itemName: line.item.name,
  809. uomDesc: line.item.uomDesc,
  810. uomShortDesc: line.item.uomShortDesc,
  811. // Null stock 字段 - 从 stockouts 数组中获取
  812. lotId: stockout.lotId || null,
  813. lotNo: stockout.lotNo || null,
  814. expiryDate: null,
  815. location: stockout.location || null,
  816. stockUnit: line.item.uomDesc,
  817. availableQty: stockout.availableQty || 0,
  818. requiredQty: line.requiredQty,
  819. actualPickQty: stockout.qty || 0,
  820. inQty: 0,
  821. outQty: 0,
  822. holdQty: 0,
  823. lotStatus: 'unavailable',
  824. lotAvailability: 'insufficient_stock',
  825. processingStatus: stockout.status || 'pending',
  826. suggestedPickLotId: null,
  827. stockOutLineId: stockout.id || null, // 使用 stockouts 数组中的 id
  828. stockOutLineStatus: stockout.status || null,
  829. stockOutLineQty: stockout.qty || 0,
  830. routerId: null,
  831. routerIndex: 999999,
  832. routerRoute: null,
  833. routerArea: null,
  834. noLot: true,
  835. });
  836. });
  837. }
  838. });
  839. console.log(" Transformed flat lot data:", flatLotData);
  840. console.log(" Total items (including null stock):", flatLotData.length);
  841. setCombinedLotData(flatLotData);
  842. setOriginalCombinedData(flatLotData);
  843. checkAllLotsCompleted(flatLotData);
  844. } catch (error) {
  845. console.error(" Error fetching combined lot data:", error);
  846. setCombinedLotData([]);
  847. setOriginalCombinedData([]);
  848. setAllLotsCompleted(false);
  849. } finally {
  850. setCombinedDataLoading(false);
  851. }
  852. }, [currentUserId, checkAllLotsCompleted]); // 移除 selectedPickOrderId 依赖
  853. // Add effect to check completion when lot data changes
  854. const handleManualLotConfirmation = useCallback(async (currentLotNo: string, newLotNo: string) => {
  855. console.log(` Manual lot confirmation: Current=${currentLotNo}, New=${newLotNo}`);
  856. // 使用第一个输入框的 lot number 查找当前数据
  857. const currentLot = combinedLotData.find(lot =>
  858. lot.lotNo && lot.lotNo === currentLotNo
  859. );
  860. if (!currentLot) {
  861. console.error(`❌ Current lot not found: ${currentLotNo}`);
  862. alert(t("Current lot number not found. Please verify and try again."));
  863. return;
  864. }
  865. if (!currentLot.stockOutLineId) {
  866. console.error("❌ No stockOutLineId found for current lot");
  867. alert(t("No stock out line found for current lot. Please contact administrator."));
  868. return;
  869. }
  870. setIsConfirmingLot(true);
  871. try {
  872. // 调用 updateStockOutLineStatusByQRCodeAndLotNo API
  873. // 第一个 lot 用于获取 pickOrderLineId, stockOutLineId, itemId
  874. // 第二个 lot 作为 inventoryLotNo
  875. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  876. pickOrderLineId: currentLot.pickOrderLineId,
  877. inventoryLotNo: newLotNo, // 第二个输入框的值
  878. stockOutLineId: currentLot.stockOutLineId,
  879. itemId: currentLot.itemId,
  880. status: "checked",
  881. });
  882. console.log("📥 updateStockOutLineStatusByQRCodeAndLotNo result:", res);
  883. if (res.code === "checked" || res.code === "SUCCESS") {
  884. // ✅ 更新本地状态
  885. const entity = res.entity as any;
  886. setCombinedLotData(prev => prev.map(lot => {
  887. if (lot.stockOutLineId === currentLot.stockOutLineId &&
  888. lot.pickOrderLineId === currentLot.pickOrderLineId) {
  889. return {
  890. ...lot,
  891. stockOutLineStatus: 'checked',
  892. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  893. };
  894. }
  895. return lot;
  896. }));
  897. setOriginalCombinedData(prev => prev.map(lot => {
  898. if (lot.stockOutLineId === currentLot.stockOutLineId &&
  899. lot.pickOrderLineId === currentLot.pickOrderLineId) {
  900. return {
  901. ...lot,
  902. stockOutLineStatus: 'checked',
  903. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  904. };
  905. }
  906. return lot;
  907. }));
  908. console.log("✅ Lot substitution completed successfully");
  909. setQrScanSuccess(true);
  910. setQrScanError(false);
  911. // 关闭手动输入模态框
  912. setManualLotConfirmationOpen(false);
  913. // 刷新数据
  914. await fetchAllCombinedLotData();
  915. } else if (res.code === "LOT_NUMBER_MISMATCH") {
  916. console.warn("⚠️ Backend reported LOT_NUMBER_MISMATCH:", res.message);
  917. // ✅ 打开 lot confirmation modal 而不是显示 alert
  918. // 从响应消息中提取 expected lot number(如果可能)
  919. // 或者使用 currentLotNo 作为 expected lot
  920. const expectedLotNo = currentLotNo; // 当前 lot 是期望的
  921. // 查找新 lot 的信息(如果存在于 combinedLotData 中)
  922. const newLot = combinedLotData.find(lot =>
  923. lot.lotNo && lot.lotNo === newLotNo
  924. );
  925. // 设置 expected lot data
  926. setExpectedLotData({
  927. lotNo: expectedLotNo,
  928. itemCode: currentLot.itemCode || '',
  929. itemName: currentLot.itemName || ''
  930. });
  931. // 设置 scanned lot data
  932. setScannedLotData({
  933. lotNo: newLotNo,
  934. itemCode: newLot?.itemCode || currentLot.itemCode || '',
  935. itemName: newLot?.itemName || currentLot.itemName || '',
  936. inventoryLotLineId: newLot?.lotId || null,
  937. stockInLineId: null // 手动输入时可能没有 stockInLineId
  938. });
  939. // 设置 selectedLotForQr 为当前 lot
  940. setSelectedLotForQr(currentLot);
  941. // 关闭手动输入模态框
  942. setManualLotConfirmationOpen(false);
  943. // 打开 lot confirmation modal
  944. setLotConfirmationOpen(true);
  945. setQrScanError(false); // 不显示错误,因为会打开确认模态框
  946. setQrScanSuccess(false);
  947. } else if (res.code === "ITEM_MISMATCH") {
  948. console.warn("⚠️ Backend reported ITEM_MISMATCH:", res.message);
  949. alert(t("Item mismatch: {message}", { message: res.message || "" }));
  950. setQrScanError(true);
  951. setQrScanSuccess(false);
  952. // 关闭手动输入模态框
  953. setManualLotConfirmationOpen(false);
  954. } else {
  955. console.warn("⚠️ Unexpected response code:", res.code);
  956. alert(t("Failed to update lot status. Response: {code}", { code: res.code }));
  957. setQrScanError(true);
  958. setQrScanSuccess(false);
  959. // 关闭手动输入模态框
  960. setManualLotConfirmationOpen(false);
  961. }
  962. } catch (error) {
  963. console.error("❌ Error in manual lot confirmation:", error);
  964. alert(t("Failed to confirm lot substitution. Please try again."));
  965. setQrScanError(true);
  966. setQrScanSuccess(false);
  967. // 关闭手动输入模态框
  968. setManualLotConfirmationOpen(false);
  969. } finally {
  970. setIsConfirmingLot(false);
  971. }
  972. }, [combinedLotData, fetchAllCombinedLotData, t]);
  973. useEffect(() => {
  974. if (combinedLotData.length > 0) {
  975. checkAllLotsCompleted(combinedLotData);
  976. }
  977. }, [combinedLotData, checkAllLotsCompleted]);
  978. // Add function to expose completion status to parent
  979. const getCompletionStatus = useCallback(() => {
  980. return allLotsCompleted;
  981. }, [allLotsCompleted]);
  982. // Expose completion status to parent component
  983. useEffect(() => {
  984. // Dispatch custom event with completion status
  985. const event = new CustomEvent('pickOrderCompletionStatus', {
  986. detail: {
  987. allLotsCompleted,
  988. tabIndex: 1 // 明确指定这是来自标签页 1 的事件
  989. }
  990. });
  991. window.dispatchEvent(event);
  992. }, [allLotsCompleted]);
  993. const handleLotConfirmation = useCallback(async () => {
  994. if (!expectedLotData || !scannedLotData || !selectedLotForQr) return;
  995. setIsConfirmingLot(true);
  996. try {
  997. const newLotNo = scannedLotData?.lotNo;
  998. const newStockInLineId = scannedLotData?.stockInLineId;
  999. await confirmLotSubstitution({
  1000. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  1001. stockOutLineId: selectedLotForQr.stockOutLineId,
  1002. originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId,
  1003. newInventoryLotNo: "",
  1004. newStockInLineId: newStockInLineId
  1005. });
  1006. setQrScanError(false);
  1007. setQrScanSuccess(false);
  1008. setQrScanInput('');
  1009. // ✅ 修复:在确认后重置扫描状态,避免重复处理
  1010. resetScan();
  1011. // ✅ 修复:不要清空 processedQrCodes,而是保留当前 QR code 的标记
  1012. // 或者如果确实需要清空,应该在重置扫描后再清空
  1013. // setProcessedQrCodes(new Set());
  1014. // setLastProcessedQr('');
  1015. setPickExecutionFormOpen(false);
  1016. if(selectedLotForQr?.stockOutLineId){
  1017. const stockOutLineUpdate = await updateStockOutLineStatus({
  1018. id: selectedLotForQr.stockOutLineId,
  1019. status: 'checked',
  1020. qty: 0
  1021. });
  1022. }
  1023. // ✅ 修复:先关闭 modal 和清空状态,再刷新数据
  1024. setLotConfirmationOpen(false);
  1025. setExpectedLotData(null);
  1026. setScannedLotData(null);
  1027. setSelectedLotForQr(null);
  1028. // ✅ 修复:刷新数据前设置刷新标志,避免在刷新期间处理新的 QR code
  1029. setIsRefreshingData(true);
  1030. await fetchAllCombinedLotData();
  1031. setIsRefreshingData(false);
  1032. } catch (error) {
  1033. console.error("Error confirming lot substitution:", error);
  1034. } finally {
  1035. setIsConfirmingLot(false);
  1036. }
  1037. }, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData, resetScan]);
  1038. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  1039. console.log(` Processing QR Code for lot: ${lotNo}`);
  1040. // 检查 lotNo 是否为 null 或 undefined(包括字符串 "null")
  1041. if (!lotNo || lotNo === 'null' || lotNo.trim() === '') {
  1042. console.error(" Invalid lotNo: null, undefined, or empty");
  1043. return;
  1044. }
  1045. // Use current data without refreshing to avoid infinite loop
  1046. const currentLotData = combinedLotData;
  1047. console.log(` Available lots:`, currentLotData.map(lot => lot.lotNo));
  1048. // 修复:在比较前确保 lotNo 不为 null
  1049. const lotNoLower = lotNo.toLowerCase();
  1050. const matchingLots = currentLotData.filter(lot => {
  1051. if (!lot.lotNo) return false; // 跳过 null lotNo
  1052. return lot.lotNo === lotNo || lot.lotNo.toLowerCase() === lotNoLower;
  1053. });
  1054. if (matchingLots.length === 0) {
  1055. console.error(` Lot not found: ${lotNo}`);
  1056. setQrScanError(true);
  1057. setQrScanSuccess(false);
  1058. const availableLotNos = currentLotData.map(lot => lot.lotNo).join(', ');
  1059. console.log(` QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`);
  1060. return;
  1061. }
  1062. console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
  1063. setQrScanError(false);
  1064. try {
  1065. let successCount = 0;
  1066. let errorCount = 0;
  1067. for (const matchingLot of matchingLots) {
  1068. console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
  1069. if (matchingLot.stockOutLineId) {
  1070. const stockOutLineUpdate = await updateStockOutLineStatus({
  1071. id: matchingLot.stockOutLineId,
  1072. status: 'checked',
  1073. qty: 0
  1074. });
  1075. console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate);
  1076. // Treat multiple backend shapes as success (type-safe via any)
  1077. const r: any = stockOutLineUpdate as any;
  1078. const updateOk =
  1079. r?.code === 'SUCCESS' ||
  1080. typeof r?.id === 'number' ||
  1081. r?.type === 'checked' ||
  1082. r?.status === 'checked' ||
  1083. typeof r?.entity?.id === 'number' ||
  1084. r?.entity?.status === 'checked';
  1085. if (updateOk) {
  1086. successCount++;
  1087. } else {
  1088. errorCount++;
  1089. }
  1090. } else {
  1091. const createStockOutLineData = {
  1092. consoCode: matchingLot.pickOrderConsoCode,
  1093. pickOrderLineId: matchingLot.pickOrderLineId,
  1094. inventoryLotLineId: matchingLot.lotId,
  1095. qty: 0
  1096. };
  1097. const createResult = await createStockOutLine(createStockOutLineData);
  1098. console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult);
  1099. if (createResult && createResult.code === "SUCCESS") {
  1100. // Immediately set status to checked for new line
  1101. let newSolId: number | undefined;
  1102. const anyRes: any = createResult as any;
  1103. if (typeof anyRes?.id === 'number') {
  1104. newSolId = anyRes.id;
  1105. } else if (anyRes?.entity) {
  1106. newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id;
  1107. }
  1108. if (newSolId) {
  1109. const setChecked = await updateStockOutLineStatus({
  1110. id: newSolId,
  1111. status: 'checked',
  1112. qty: 0
  1113. });
  1114. if (setChecked && setChecked.code === "SUCCESS") {
  1115. successCount++;
  1116. } else {
  1117. errorCount++;
  1118. }
  1119. } else {
  1120. console.warn("Created stock out line but no ID returned; cannot set to checked");
  1121. errorCount++;
  1122. }
  1123. } else {
  1124. errorCount++;
  1125. }
  1126. }
  1127. }
  1128. // FIXED: Set refresh flag before refreshing data
  1129. setIsRefreshingData(true);
  1130. console.log("🔄 Refreshing data after QR code processing...");
  1131. await fetchAllCombinedLotData();
  1132. if (successCount > 0) {
  1133. console.log(` QR Code processing completed: ${successCount} updated/created`);
  1134. setQrScanSuccess(true);
  1135. setQrScanError(false);
  1136. setQrScanInput(''); // Clear input after successful processing
  1137. //setIsManualScanning(false);
  1138. // stopScan();
  1139. // resetScan();
  1140. // Clear success state after a delay
  1141. //setTimeout(() => {
  1142. //setQrScanSuccess(false);
  1143. //}, 2000);
  1144. } else {
  1145. console.error(` QR Code processing failed: ${errorCount} errors`);
  1146. setQrScanError(true);
  1147. setQrScanSuccess(false);
  1148. // Clear error state after a delay
  1149. // setTimeout(() => {
  1150. // setQrScanError(false);
  1151. //}, 3000);
  1152. }
  1153. } catch (error) {
  1154. console.error(" Error processing QR code:", error);
  1155. setQrScanError(true);
  1156. setQrScanSuccess(false);
  1157. // Clear error state after a delay
  1158. setTimeout(() => {
  1159. setQrScanError(false);
  1160. }, 3000);
  1161. } finally {
  1162. // Clear refresh flag after a short delay
  1163. setTimeout(() => {
  1164. setIsRefreshingData(false);
  1165. }, 1000);
  1166. }
  1167. }, [combinedLotData]);
  1168. const handleFastQrScan = useCallback(async (lotNo: string) => {
  1169. const startTime = performance.now();
  1170. console.log(`⏱️ [FAST SCAN START] Lot: ${lotNo}`);
  1171. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  1172. // 从 combinedLotData 中找到对应的 lot
  1173. const findStartTime = performance.now();
  1174. const matchingLot = combinedLotData.find(lot =>
  1175. lot.lotNo && lot.lotNo === lotNo
  1176. );
  1177. const findTime = performance.now() - findStartTime;
  1178. console.log(`⏱️ Find lot time: ${findTime.toFixed(2)}ms`);
  1179. if (!matchingLot || !matchingLot.stockOutLineId) {
  1180. const totalTime = performance.now() - startTime;
  1181. console.warn(`⚠️ Fast scan: Lot ${lotNo} not found or no stockOutLineId`);
  1182. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`);
  1183. return;
  1184. }
  1185. try {
  1186. // ✅ 使用快速 API
  1187. const apiStartTime = performance.now();
  1188. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1189. pickOrderLineId: matchingLot.pickOrderLineId,
  1190. inventoryLotNo: lotNo,
  1191. stockOutLineId: matchingLot.stockOutLineId,
  1192. itemId: matchingLot.itemId,
  1193. status: "checked",
  1194. });
  1195. const apiTime = performance.now() - apiStartTime;
  1196. console.log(`⏱️ API call time: ${apiTime.toFixed(2)}ms`);
  1197. if (res.code === "checked" || res.code === "SUCCESS") {
  1198. // ✅ 只更新本地状态,不调用 fetchAllCombinedLotData
  1199. const updateStartTime = performance.now();
  1200. const entity = res.entity as any;
  1201. setCombinedLotData(prev => prev.map(lot => {
  1202. if (lot.stockOutLineId === matchingLot.stockOutLineId &&
  1203. lot.pickOrderLineId === matchingLot.pickOrderLineId) {
  1204. return {
  1205. ...lot,
  1206. stockOutLineStatus: 'checked',
  1207. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  1208. };
  1209. }
  1210. return lot;
  1211. }));
  1212. setOriginalCombinedData(prev => prev.map(lot => {
  1213. if (lot.stockOutLineId === matchingLot.stockOutLineId &&
  1214. lot.pickOrderLineId === matchingLot.pickOrderLineId) {
  1215. return {
  1216. ...lot,
  1217. stockOutLineStatus: 'checked',
  1218. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  1219. };
  1220. }
  1221. return lot;
  1222. }));
  1223. const updateTime = performance.now() - updateStartTime;
  1224. console.log(`⏱️ State update time: ${updateTime.toFixed(2)}ms`);
  1225. const totalTime = performance.now() - startTime;
  1226. console.log(`✅ [FAST SCAN END] Lot: ${lotNo}`);
  1227. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  1228. console.log(`⏰ End time: ${new Date().toISOString()}`);
  1229. } else {
  1230. const totalTime = performance.now() - startTime;
  1231. console.warn(`⚠️ Fast scan failed for ${lotNo}:`, res.code);
  1232. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`);
  1233. }
  1234. } catch (error) {
  1235. const totalTime = performance.now() - startTime;
  1236. console.error(` Fast scan error for ${lotNo}:`, error);
  1237. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`);
  1238. }
  1239. }, [combinedLotData, updateStockOutLineStatusByQRCodeAndLotNo]);
  1240. // Enhanced lotDataIndexes with cached active lots for better performance
  1241. const lotDataIndexes = useMemo(() => {
  1242. const indexStartTime = performance.now();
  1243. console.log(`⏱️ [PERF] lotDataIndexes calculation START, data length: ${combinedLotData.length}`);
  1244. const byItemId = new Map<number, any[]>();
  1245. const byItemCode = new Map<string, any[]>();
  1246. const byLotId = new Map<number, any>();
  1247. const byLotNo = new Map<string, any[]>();
  1248. const byStockInLineId = new Map<number, any[]>();
  1249. // Cache active lots separately to avoid filtering on every scan
  1250. const activeLotsByItemId = new Map<number, any[]>();
  1251. const rejectedStatuses = new Set(['rejected']);
  1252. // ✅ Use for loop instead of forEach for better performance on tablets
  1253. for (let i = 0; i < combinedLotData.length; i++) {
  1254. const lot = combinedLotData[i];
  1255. const isActive = !rejectedStatuses.has(lot.lotAvailability) &&
  1256. !rejectedStatuses.has(lot.stockOutLineStatus) &&
  1257. !rejectedStatuses.has(lot.processingStatus);
  1258. if (lot.itemId) {
  1259. if (!byItemId.has(lot.itemId)) {
  1260. byItemId.set(lot.itemId, []);
  1261. activeLotsByItemId.set(lot.itemId, []);
  1262. }
  1263. byItemId.get(lot.itemId)!.push(lot);
  1264. if (isActive) {
  1265. activeLotsByItemId.get(lot.itemId)!.push(lot);
  1266. }
  1267. }
  1268. if (lot.itemCode) {
  1269. if (!byItemCode.has(lot.itemCode)) {
  1270. byItemCode.set(lot.itemCode, []);
  1271. }
  1272. byItemCode.get(lot.itemCode)!.push(lot);
  1273. }
  1274. if (lot.lotId) {
  1275. byLotId.set(lot.lotId, lot);
  1276. }
  1277. if (lot.lotNo) {
  1278. if (!byLotNo.has(lot.lotNo)) {
  1279. byLotNo.set(lot.lotNo, []);
  1280. }
  1281. byLotNo.get(lot.lotNo)!.push(lot);
  1282. }
  1283. if (lot.stockInLineId) {
  1284. if (!byStockInLineId.has(lot.stockInLineId)) {
  1285. byStockInLineId.set(lot.stockInLineId, []);
  1286. }
  1287. byStockInLineId.get(lot.stockInLineId)!.push(lot);
  1288. }
  1289. }
  1290. const indexTime = performance.now() - indexStartTime;
  1291. if (indexTime > 10) {
  1292. console.log(`⏱️ [PERF] lotDataIndexes calculation END: ${indexTime.toFixed(2)}ms (${(indexTime / 1000).toFixed(3)}s)`);
  1293. }
  1294. return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId };
  1295. }, [combinedLotData.length, combinedLotData]);
  1296. // Store resetScan in ref for immediate access (update on every render)
  1297. resetScanRef.current = resetScan;
  1298. const processOutsideQrCode = useCallback(async (latestQr: string) => {
  1299. const totalStartTime = performance.now();
  1300. console.log(`⏱️ [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`);
  1301. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  1302. // ✅ Measure index access time
  1303. const indexAccessStart = performance.now();
  1304. const indexes = lotDataIndexes; // Access the memoized indexes
  1305. const indexAccessTime = performance.now() - indexAccessStart;
  1306. console.log(`⏱️ [PERF] Index access time: ${indexAccessTime.toFixed(2)}ms`);
  1307. // 1) Parse JSON safely (parse once, reuse)
  1308. const parseStartTime = performance.now();
  1309. let qrData: any = null;
  1310. let parseTime = 0;
  1311. try {
  1312. qrData = JSON.parse(latestQr);
  1313. parseTime = performance.now() - parseStartTime;
  1314. console.log(`⏱️ [PERF] JSON parse time: ${parseTime.toFixed(2)}ms`);
  1315. } catch {
  1316. console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches.");
  1317. startTransition(() => {
  1318. setQrScanError(true);
  1319. setQrScanSuccess(false);
  1320. });
  1321. return;
  1322. }
  1323. try {
  1324. const validationStartTime = performance.now();
  1325. if (!(qrData?.stockInLineId && qrData?.itemId)) {
  1326. console.log("QR JSON missing required fields (itemId, stockInLineId).");
  1327. startTransition(() => {
  1328. setQrScanError(true);
  1329. setQrScanSuccess(false);
  1330. });
  1331. return;
  1332. }
  1333. const validationTime = performance.now() - validationStartTime;
  1334. console.log(`⏱️ [PERF] Validation time: ${validationTime.toFixed(2)}ms`);
  1335. const scannedItemId = qrData.itemId;
  1336. const scannedStockInLineId = qrData.stockInLineId;
  1337. // ✅ Check if this combination was already processed
  1338. const duplicateCheckStartTime = performance.now();
  1339. const itemProcessedSet = processedQrCombinations.get(scannedItemId);
  1340. if (itemProcessedSet?.has(scannedStockInLineId)) {
  1341. const duplicateCheckTime = performance.now() - duplicateCheckStartTime;
  1342. console.log(`⏱️ [SKIP] Already processed combination: itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId} (check time: ${duplicateCheckTime.toFixed(2)}ms)`);
  1343. return;
  1344. }
  1345. const duplicateCheckTime = performance.now() - duplicateCheckStartTime;
  1346. console.log(`⏱️ [PERF] Duplicate check time: ${duplicateCheckTime.toFixed(2)}ms`);
  1347. // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed)
  1348. const lookupStartTime = performance.now();
  1349. const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || [];
  1350. // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected
  1351. const allLotsForItem = indexes.byItemId.get(scannedItemId) || [];
  1352. const lookupTime = performance.now() - lookupStartTime;
  1353. console.log(`⏱️ [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${activeSuggestedLots.length} active lots, ${allLotsForItem.length} total lots`);
  1354. // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots
  1355. // This allows users to scan other lots even when all suggested lots are rejected
  1356. const scannedLot = allLotsForItem.find(
  1357. (lot: any) => lot.stockInLineId === scannedStockInLineId
  1358. );
  1359. if (scannedLot) {
  1360. const isRejected =
  1361. scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1362. scannedLot.lotAvailability === 'rejected' ||
  1363. scannedLot.lotAvailability === 'status_unavailable';
  1364. if (isRejected) {
  1365. console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`);
  1366. startTransition(() => {
  1367. setQrScanError(true);
  1368. setQrScanSuccess(false);
  1369. setQrScanErrorMsg(
  1370. `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
  1371. );
  1372. });
  1373. // Mark as processed to prevent re-processing
  1374. setProcessedQrCombinations(prev => {
  1375. const newMap = new Map(prev);
  1376. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1377. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1378. return newMap;
  1379. });
  1380. return;
  1381. }
  1382. }
  1383. // ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching
  1384. if (activeSuggestedLots.length === 0) {
  1385. // Check if there are any lots for this item (even if all are rejected)
  1386. if (allLotsForItem.length === 0) {
  1387. console.error("No lots found for this item");
  1388. startTransition(() => {
  1389. setQrScanError(true);
  1390. setQrScanSuccess(false);
  1391. setQrScanErrorMsg("当前订单中没有此物品的批次信息");
  1392. });
  1393. return;
  1394. }
  1395. // ✅ Allow lot switching: find a rejected lot as expected lot, or use first lot
  1396. // This allows users to switch to a new lot even when all suggested lots are rejected
  1397. console.log(`⚠️ [QR PROCESS] No active suggested lots, but allowing lot switching.`);
  1398. // Find a rejected lot as expected lot (the one that was rejected)
  1399. const rejectedLot = allLotsForItem.find((lot: any) =>
  1400. lot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1401. lot.lotAvailability === 'rejected' ||
  1402. lot.lotAvailability === 'status_unavailable'
  1403. );
  1404. const expectedLot = rejectedLot || allLotsForItem[0]; // Use rejected lot if exists, otherwise first lot
  1405. // ✅ Always open confirmation modal when no active lots (user needs to confirm switching)
  1406. // handleLotMismatch will fetch lotNo from backend using stockInLineId if needed
  1407. console.log(`⚠️ [QR PROCESS] Opening confirmation modal for lot switch (no active lots)`);
  1408. setSelectedLotForQr(expectedLot);
  1409. handleLotMismatch(
  1410. {
  1411. lotNo: expectedLot.lotNo,
  1412. itemCode: expectedLot.itemCode,
  1413. itemName: expectedLot.itemName
  1414. },
  1415. {
  1416. lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null
  1417. itemCode: expectedLot.itemCode,
  1418. itemName: expectedLot.itemName,
  1419. inventoryLotLineId: scannedLot?.lotId || null,
  1420. stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo
  1421. }
  1422. );
  1423. return;
  1424. }
  1425. // ✅ OPTIMIZATION: Direct Map lookup for stockInLineId match (O(1))
  1426. const matchStartTime = performance.now();
  1427. let exactMatch: any = null;
  1428. const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || [];
  1429. // Find exact match from stockInLineId index, then verify it's in active lots
  1430. for (let i = 0; i < stockInLineLots.length; i++) {
  1431. const lot = stockInLineLots[i];
  1432. if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) {
  1433. exactMatch = lot;
  1434. break;
  1435. }
  1436. }
  1437. const matchTime = performance.now() - matchStartTime;
  1438. console.log(`⏱️ [PERF] Find exact match time: ${matchTime.toFixed(2)}ms, found: ${exactMatch ? 'yes' : 'no'}`);
  1439. // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots
  1440. // This handles the case where Lot A is rejected and user scans Lot B
  1441. // Also handle case where scanned lot is not in allLotsForItem (scannedLot is undefined)
  1442. if (!exactMatch) {
  1443. // Scanned lot is not in active suggested lots, open confirmation modal
  1444. const expectedLot = activeSuggestedLots[0] || allLotsForItem[0]; // Use first active lot or first lot as expected
  1445. if (expectedLot) {
  1446. // Check if scanned lot is different from expected, or if scannedLot is undefined (not in allLotsForItem)
  1447. const shouldOpenModal = !scannedLot || (scannedLot.stockInLineId !== expectedLot.stockInLineId);
  1448. if (shouldOpenModal) {
  1449. console.log(`⚠️ [QR PROCESS] Opening confirmation modal (scanned lot ${scannedLot?.lotNo || 'not in data'} is not in active suggested lots)`);
  1450. setSelectedLotForQr(expectedLot);
  1451. handleLotMismatch(
  1452. {
  1453. lotNo: expectedLot.lotNo,
  1454. itemCode: expectedLot.itemCode,
  1455. itemName: expectedLot.itemName
  1456. },
  1457. {
  1458. lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null
  1459. itemCode: expectedLot.itemCode,
  1460. itemName: expectedLot.itemName,
  1461. inventoryLotLineId: scannedLot?.lotId || null,
  1462. stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo
  1463. }
  1464. );
  1465. return;
  1466. }
  1467. }
  1468. }
  1469. if (exactMatch) {
  1470. // ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认
  1471. console.log(`✅ Exact stockInLineId match found for lot: ${exactMatch.lotNo}`);
  1472. if (!exactMatch.stockOutLineId) {
  1473. console.warn("No stockOutLineId on exactMatch, cannot update status by QR.");
  1474. startTransition(() => {
  1475. setQrScanError(true);
  1476. setQrScanSuccess(false);
  1477. });
  1478. return;
  1479. }
  1480. try {
  1481. const apiStartTime = performance.now();
  1482. console.log(`⏱️ [API CALL START] Calling updateStockOutLineStatusByQRCodeAndLotNo`);
  1483. console.log(`⏰ [API CALL] API start time: ${new Date().toISOString()}`);
  1484. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1485. pickOrderLineId: exactMatch.pickOrderLineId,
  1486. inventoryLotNo: exactMatch.lotNo,
  1487. stockOutLineId: exactMatch.stockOutLineId,
  1488. itemId: exactMatch.itemId,
  1489. status: "checked",
  1490. });
  1491. const apiTime = performance.now() - apiStartTime;
  1492. console.log(`⏱️ [API CALL END] Total API time: ${apiTime.toFixed(2)}ms (${(apiTime / 1000).toFixed(3)}s)`);
  1493. console.log(`⏰ [API CALL] API end time: ${new Date().toISOString()}`);
  1494. if (res.code === "checked" || res.code === "SUCCESS") {
  1495. const entity = res.entity as any;
  1496. // ✅ Batch state updates using startTransition
  1497. const stateUpdateStartTime = performance.now();
  1498. startTransition(() => {
  1499. setQrScanError(false);
  1500. setQrScanSuccess(true);
  1501. setCombinedLotData(prev => prev.map(lot => {
  1502. if (lot.stockOutLineId === exactMatch.stockOutLineId &&
  1503. lot.pickOrderLineId === exactMatch.pickOrderLineId) {
  1504. return {
  1505. ...lot,
  1506. stockOutLineStatus: 'checked',
  1507. stockOutLineQty: entity?.qty ?? lot.stockOutLineQty,
  1508. };
  1509. }
  1510. return lot;
  1511. }));
  1512. setOriginalCombinedData(prev => prev.map(lot => {
  1513. if (lot.stockOutLineId === exactMatch.stockOutLineId &&
  1514. lot.pickOrderLineId === exactMatch.pickOrderLineId) {
  1515. return {
  1516. ...lot,
  1517. stockOutLineStatus: 'checked',
  1518. stockOutLineQty: entity?.qty ?? lot.stockOutLineQty,
  1519. };
  1520. }
  1521. return lot;
  1522. }));
  1523. });
  1524. const stateUpdateTime = performance.now() - stateUpdateStartTime;
  1525. console.log(`⏱️ [PERF] State update time: ${stateUpdateTime.toFixed(2)}ms`);
  1526. // Mark this combination as processed
  1527. const markProcessedStartTime = performance.now();
  1528. setProcessedQrCombinations(prev => {
  1529. const newMap = new Map(prev);
  1530. if (!newMap.has(scannedItemId)) {
  1531. newMap.set(scannedItemId, new Set());
  1532. }
  1533. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1534. return newMap;
  1535. });
  1536. const markProcessedTime = performance.now() - markProcessedStartTime;
  1537. console.log(`⏱️ [PERF] Mark processed time: ${markProcessedTime.toFixed(2)}ms`);
  1538. const totalTime = performance.now() - totalStartTime;
  1539. console.log(`✅ [PROCESS OUTSIDE QR END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  1540. console.log(`⏰ End time: ${new Date().toISOString()}`);
  1541. console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, api=${apiTime.toFixed(2)}ms, stateUpdate=${stateUpdateTime.toFixed(2)}ms, markProcessed=${markProcessedTime.toFixed(2)}ms`);
  1542. console.log("✅ Status updated locally, no full data refresh needed");
  1543. } else {
  1544. console.warn("Unexpected response code from backend:", res.code);
  1545. startTransition(() => {
  1546. setQrScanError(true);
  1547. setQrScanSuccess(false);
  1548. });
  1549. }
  1550. } catch (e) {
  1551. const totalTime = performance.now() - totalStartTime;
  1552. console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`);
  1553. console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e);
  1554. startTransition(() => {
  1555. setQrScanError(true);
  1556. setQrScanSuccess(false);
  1557. });
  1558. }
  1559. return; // ✅ 直接返回,不需要确认表单
  1560. }
  1561. // ✅ Case 2: itemId 匹配但 stockInLineId 不匹配 - 显示确认表单
  1562. // Check if we should allow reopening (different stockInLineId)
  1563. const mismatchCheckStartTime = performance.now();
  1564. const itemProcessedSet2 = processedQrCombinations.get(scannedItemId);
  1565. if (itemProcessedSet2?.has(scannedStockInLineId)) {
  1566. const mismatchCheckTime = performance.now() - mismatchCheckStartTime;
  1567. console.log(`⏱️ [SKIP] Already processed this exact combination (check time: ${mismatchCheckTime.toFixed(2)}ms)`);
  1568. return;
  1569. }
  1570. const mismatchCheckTime = performance.now() - mismatchCheckStartTime;
  1571. console.log(`⏱️ [PERF] Mismatch check time: ${mismatchCheckTime.toFixed(2)}ms`);
  1572. // 取第一个活跃的 lot 作为期望的 lot
  1573. const expectedLotStartTime = performance.now();
  1574. const expectedLot = activeSuggestedLots[0];
  1575. if (!expectedLot) {
  1576. console.error("Could not determine expected lot for confirmation");
  1577. startTransition(() => {
  1578. setQrScanError(true);
  1579. setQrScanSuccess(false);
  1580. });
  1581. return;
  1582. }
  1583. const expectedLotTime = performance.now() - expectedLotStartTime;
  1584. console.log(`⏱️ [PERF] Get expected lot time: ${expectedLotTime.toFixed(2)}ms`);
  1585. // ✅ 立即打开确认模态框,不等待其他操作
  1586. console.log(`⚠️ Lot mismatch: Expected stockInLineId=${expectedLot.stockInLineId}, Scanned stockInLineId=${scannedStockInLineId}`);
  1587. // Set selected lot immediately (no transition delay)
  1588. const setSelectedLotStartTime = performance.now();
  1589. setSelectedLotForQr(expectedLot);
  1590. const setSelectedLotTime = performance.now() - setSelectedLotStartTime;
  1591. console.log(`⏱️ [PERF] Set selected lot time: ${setSelectedLotTime.toFixed(2)}ms`);
  1592. // ✅ 获取扫描的 lot 信息(从 QR 数据中提取,或使用默认值)
  1593. // Call handleLotMismatch immediately - it will open the modal
  1594. const handleMismatchStartTime = performance.now();
  1595. handleLotMismatch(
  1596. {
  1597. lotNo: expectedLot.lotNo,
  1598. itemCode: expectedLot.itemCode,
  1599. itemName: expectedLot.itemName
  1600. },
  1601. {
  1602. lotNo: null, // 扫描的 lotNo 未知,需要从后端获取或显示为未知
  1603. itemCode: expectedLot.itemCode,
  1604. itemName: expectedLot.itemName,
  1605. inventoryLotLineId: null,
  1606. stockInLineId: scannedStockInLineId // ✅ 传递 stockInLineId
  1607. }
  1608. );
  1609. const handleMismatchTime = performance.now() - handleMismatchStartTime;
  1610. console.log(`⏱️ [PERF] Handle mismatch call time: ${handleMismatchTime.toFixed(2)}ms`);
  1611. const totalTime = performance.now() - totalStartTime;
  1612. console.log(`⚠️ [PROCESS OUTSIDE QR MISMATCH] Total time before modal: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  1613. console.log(`⏰ End time: ${new Date().toISOString()}`);
  1614. console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, mismatchCheck=${mismatchCheckTime.toFixed(2)}ms, expectedLot=${expectedLotTime.toFixed(2)}ms, setSelectedLot=${setSelectedLotTime.toFixed(2)}ms, handleMismatch=${handleMismatchTime.toFixed(2)}ms`);
  1615. } catch (error) {
  1616. const totalTime = performance.now() - totalStartTime;
  1617. console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`);
  1618. console.error("Error during QR code processing:", error);
  1619. startTransition(() => {
  1620. setQrScanError(true);
  1621. setQrScanSuccess(false);
  1622. });
  1623. return;
  1624. }
  1625. }, [lotDataIndexes, handleLotMismatch, processedQrCombinations, combinedLotData, fetchStockInLineInfoCached]);
  1626. // Store processOutsideQrCode in ref for immediate access (update on every render)
  1627. processOutsideQrCodeRef.current = processOutsideQrCode;
  1628. useEffect(() => {
  1629. // Skip if scanner is not active or no data available
  1630. if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) {
  1631. return;
  1632. }
  1633. const qrValuesChangeStartTime = performance.now();
  1634. console.log(`⏱️ [QR VALUES EFFECT] Triggered at: ${new Date().toISOString()}`);
  1635. console.log(`⏱️ [QR VALUES EFFECT] qrValues.length: ${qrValues.length}`);
  1636. console.log(`⏱️ [QR VALUES EFFECT] qrValues:`, qrValues);
  1637. const latestQr = qrValues[qrValues.length - 1];
  1638. console.log(`⏱️ [QR VALUES EFFECT] Latest QR: ${latestQr}`);
  1639. console.log(`⏰ [QR VALUES EFFECT] Latest QR detected at: ${new Date().toISOString()}`);
  1640. // ✅ FIXED: Handle test shortcut {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId
  1641. // Support both formats: {2fitest (2 t's) and {2fittest (3 t's)
  1642. if ((latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) && latestQr.endsWith("}")) {
  1643. // Extract content: remove "{2fitest" or "{2fittest" and "}"
  1644. let content = '';
  1645. if (latestQr.startsWith("{2fittest")) {
  1646. content = latestQr.substring(9, latestQr.length - 1); // Remove "{2fittest" and "}"
  1647. } else if (latestQr.startsWith("{2fitest")) {
  1648. content = latestQr.substring(8, latestQr.length - 1); // Remove "{2fitest" and "}"
  1649. }
  1650. const parts = content.split(',');
  1651. if (parts.length === 2) {
  1652. const itemId = parseInt(parts[0].trim(), 10);
  1653. const stockInLineId = parseInt(parts[1].trim(), 10);
  1654. if (!isNaN(itemId) && !isNaN(stockInLineId)) {
  1655. console.log(
  1656. `%c TEST QR: Detected ${latestQr.substring(0, 9)}... - Simulating QR input (itemId=${itemId}, stockInLineId=${stockInLineId})`,
  1657. "color: purple; font-weight: bold"
  1658. );
  1659. // ✅ Simulate QR code JSON format
  1660. const simulatedQr = JSON.stringify({
  1661. itemId: itemId,
  1662. stockInLineId: stockInLineId
  1663. });
  1664. console.log(`⏱️ [TEST QR] Simulated QR content: ${simulatedQr}`);
  1665. console.log(`⏱️ [TEST QR] Start time: ${new Date().toISOString()}`);
  1666. const testStartTime = performance.now();
  1667. // ✅ Mark as processed FIRST to avoid duplicate processing
  1668. lastProcessedQrRef.current = latestQr;
  1669. processedQrCodesRef.current.add(latestQr);
  1670. if (processedQrCodesRef.current.size > 100) {
  1671. const firstValue = processedQrCodesRef.current.values().next().value;
  1672. if (firstValue !== undefined) {
  1673. processedQrCodesRef.current.delete(firstValue);
  1674. }
  1675. }
  1676. setLastProcessedQr(latestQr);
  1677. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1678. // ✅ Process immediately (bypass QR scanner delay)
  1679. if (processOutsideQrCodeRef.current) {
  1680. processOutsideQrCodeRef.current(simulatedQr).then(() => {
  1681. const testTime = performance.now() - testStartTime;
  1682. console.log(`⏱️ [TEST QR] Total processing time: ${testTime.toFixed(2)}ms (${(testTime / 1000).toFixed(3)}s)`);
  1683. console.log(`⏱️ [TEST QR] End time: ${new Date().toISOString()}`);
  1684. }).catch((error) => {
  1685. const testTime = performance.now() - testStartTime;
  1686. console.error(`❌ [TEST QR] Error after ${testTime.toFixed(2)}ms:`, error);
  1687. });
  1688. }
  1689. // Reset scan
  1690. if (resetScanRef.current) {
  1691. resetScanRef.current();
  1692. }
  1693. const qrValuesChangeTime = performance.now() - qrValuesChangeStartTime;
  1694. console.log(`⏱️ [QR VALUES EFFECT] Test QR handling time: ${qrValuesChangeTime.toFixed(2)}ms`);
  1695. return; // ✅ IMPORTANT: Return early to prevent normal processing
  1696. } else {
  1697. console.warn(`⏱️ [TEST QR] Invalid itemId or stockInLineId: itemId=${parts[0]}, stockInLineId=${parts[1]}`);
  1698. }
  1699. } else {
  1700. console.warn(`⏱️ [TEST QR] Invalid format. Expected {2fitestx,y} or {2fittestx,y}, got: ${latestQr}`);
  1701. }
  1702. }
  1703. // Skip processing if confirmation modals are open
  1704. // BUT: Allow processing if modal was just closed (to allow reopening for different stockInLineId)
  1705. if (lotConfirmationOpen || manualLotConfirmationOpen) {
  1706. // Check if this is a different QR code than what triggered the modal
  1707. const modalTriggerQr = lastProcessedQrRef.current;
  1708. if (latestQr === modalTriggerQr) {
  1709. console.log(`⏱️ [QR PROCESS] Skipping - modal open for same QR: lotConfirmation=${lotConfirmationOpen}, manual=${manualLotConfirmationOpen}`);
  1710. return;
  1711. }
  1712. // If it's a different QR, allow processing (user might have canceled and scanned different lot)
  1713. console.log(`⏱️ [QR PROCESS] Different QR detected while modal open, allowing processing`);
  1714. }
  1715. const qrDetectionStartTime = performance.now();
  1716. console.log(`⏱️ [QR DETECTION] Latest QR detected: ${latestQr?.substring(0, 50)}...`);
  1717. console.log(`⏰ [QR DETECTION] Detection time: ${new Date().toISOString()}`);
  1718. console.log(`⏱️ [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`);
  1719. // Skip if already processed (use refs to avoid dependency issues and delays)
  1720. const checkProcessedStartTime = performance.now();
  1721. if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) {
  1722. const checkTime = performance.now() - checkProcessedStartTime;
  1723. console.log(`⏱️ [QR PROCESS] Already processed check time: ${checkTime.toFixed(2)}ms`);
  1724. return;
  1725. }
  1726. const checkTime = performance.now() - checkProcessedStartTime;
  1727. console.log(`⏱️ [QR PROCESS] Not processed check time: ${checkTime.toFixed(2)}ms`);
  1728. // Handle special shortcut
  1729. if (latestQr === "{2fic}") {
  1730. console.log(" Detected {2fic} shortcut - opening manual lot confirmation form");
  1731. setManualLotConfirmationOpen(true);
  1732. if (resetScanRef.current) {
  1733. resetScanRef.current();
  1734. }
  1735. lastProcessedQrRef.current = latestQr;
  1736. processedQrCodesRef.current.add(latestQr);
  1737. if (processedQrCodesRef.current.size > 100) {
  1738. const firstValue = processedQrCodesRef.current.values().next().value;
  1739. if (firstValue !== undefined) {
  1740. processedQrCodesRef.current.delete(firstValue);
  1741. }
  1742. }
  1743. setLastProcessedQr(latestQr);
  1744. setProcessedQrCodes(prev => {
  1745. const newSet = new Set(prev);
  1746. newSet.add(latestQr);
  1747. if (newSet.size > 100) {
  1748. const firstValue = newSet.values().next().value;
  1749. if (firstValue !== undefined) {
  1750. newSet.delete(firstValue);
  1751. }
  1752. }
  1753. return newSet;
  1754. });
  1755. return;
  1756. }
  1757. // Process new QR code immediately (background mode - no modal)
  1758. // Check against refs to avoid state update delays
  1759. if (latestQr && latestQr !== lastProcessedQrRef.current) {
  1760. const processingStartTime = performance.now();
  1761. console.log(`⏱️ [QR PROCESS] Starting processing at: ${new Date().toISOString()}`);
  1762. console.log(`⏱️ [QR PROCESS] Time since detection: ${(processingStartTime - qrDetectionStartTime).toFixed(2)}ms`);
  1763. // ✅ Process immediately for better responsiveness
  1764. // Clear any pending debounced processing
  1765. if (qrProcessingTimeoutRef.current) {
  1766. clearTimeout(qrProcessingTimeoutRef.current);
  1767. qrProcessingTimeoutRef.current = null;
  1768. }
  1769. // Log immediately (console.log is synchronous)
  1770. console.log(`⏱️ [QR PROCESS] Processing new QR code with enhanced validation: ${latestQr}`);
  1771. // Update refs immediately (no state update delay) - do this FIRST
  1772. const refUpdateStartTime = performance.now();
  1773. lastProcessedQrRef.current = latestQr;
  1774. processedQrCodesRef.current.add(latestQr);
  1775. if (processedQrCodesRef.current.size > 100) {
  1776. const firstValue = processedQrCodesRef.current.values().next().value;
  1777. if (firstValue !== undefined) {
  1778. processedQrCodesRef.current.delete(firstValue);
  1779. }
  1780. }
  1781. const refUpdateTime = performance.now() - refUpdateStartTime;
  1782. console.log(`⏱️ [QR PROCESS] Ref update time: ${refUpdateTime.toFixed(2)}ms`);
  1783. // Process immediately in background - no modal/form needed, no delays
  1784. // Use ref to avoid dependency issues
  1785. const processCallStartTime = performance.now();
  1786. if (processOutsideQrCodeRef.current) {
  1787. processOutsideQrCodeRef.current(latestQr).then(() => {
  1788. const processCallTime = performance.now() - processCallStartTime;
  1789. const totalProcessingTime = performance.now() - processingStartTime;
  1790. console.log(`⏱️ [QR PROCESS] processOutsideQrCode call time: ${processCallTime.toFixed(2)}ms`);
  1791. console.log(`⏱️ [QR PROCESS] Total processing time: ${totalProcessingTime.toFixed(2)}ms (${(totalProcessingTime / 1000).toFixed(3)}s)`);
  1792. }).catch((error) => {
  1793. const processCallTime = performance.now() - processCallStartTime;
  1794. const totalProcessingTime = performance.now() - processingStartTime;
  1795. console.error(`❌ [QR PROCESS] processOutsideQrCode error after ${processCallTime.toFixed(2)}ms:`, error);
  1796. console.error(`❌ [QR PROCESS] Total processing time before error: ${totalProcessingTime.toFixed(2)}ms`);
  1797. });
  1798. }
  1799. // Update state for UI (but don't block on it)
  1800. const stateUpdateStartTime = performance.now();
  1801. setLastProcessedQr(latestQr);
  1802. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1803. const stateUpdateTime = performance.now() - stateUpdateStartTime;
  1804. console.log(`⏱️ [QR PROCESS] State update time: ${stateUpdateTime.toFixed(2)}ms`);
  1805. const detectionTime = performance.now() - qrDetectionStartTime;
  1806. const totalEffectTime = performance.now() - qrValuesChangeStartTime;
  1807. console.log(`⏱️ [QR DETECTION] Total detection time: ${detectionTime.toFixed(2)}ms`);
  1808. console.log(`⏱️ [QR VALUES EFFECT] Total effect time: ${totalEffectTime.toFixed(2)}ms`);
  1809. }
  1810. return () => {
  1811. if (qrProcessingTimeoutRef.current) {
  1812. clearTimeout(qrProcessingTimeoutRef.current);
  1813. qrProcessingTimeoutRef.current = null;
  1814. }
  1815. };
  1816. }, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]);
  1817. const renderCountRef = useRef(0);
  1818. const renderStartTimeRef = useRef<number | null>(null);
  1819. // Track render performance
  1820. useEffect(() => {
  1821. renderCountRef.current++;
  1822. const now = performance.now();
  1823. if (renderStartTimeRef.current !== null) {
  1824. const renderTime = now - renderStartTimeRef.current;
  1825. if (renderTime > 100) { // Only log slow renders (>100ms)
  1826. console.log(`⏱️ [PERF] Render #${renderCountRef.current} took ${renderTime.toFixed(2)}ms, combinedLotData length: ${combinedLotData.length}`);
  1827. }
  1828. renderStartTimeRef.current = null;
  1829. }
  1830. // Track when lotConfirmationOpen changes
  1831. if (lotConfirmationOpen) {
  1832. renderStartTimeRef.current = performance.now();
  1833. console.log(`⏱️ [PERF] Render triggered by lotConfirmationOpen=true`);
  1834. }
  1835. }, [combinedLotData.length, lotConfirmationOpen]);
  1836. // Auto-start scanner only once on mount
  1837. const scannerInitializedRef = useRef(false);
  1838. useEffect(() => {
  1839. if (session && currentUserId && !initializationRef.current) {
  1840. console.log(" Session loaded, initializing pick order...");
  1841. initializationRef.current = true;
  1842. // Only fetch existing data, no auto-assignment
  1843. fetchAllCombinedLotData();
  1844. }
  1845. }, [session, currentUserId, fetchAllCombinedLotData]);
  1846. // Separate effect for auto-starting scanner (only once, prevents multiple resets)
  1847. useEffect(() => {
  1848. if (session && currentUserId && !scannerInitializedRef.current) {
  1849. scannerInitializedRef.current = true;
  1850. // ✅ Auto-start scanner on mount for tablet use (background mode - no modal)
  1851. console.log("✅ Auto-starting QR scanner in background mode");
  1852. setIsManualScanning(true);
  1853. startScan();
  1854. }
  1855. }, [session, currentUserId, startScan]);
  1856. // Add event listener for manual assignment
  1857. useEffect(() => {
  1858. const handlePickOrderAssigned = () => {
  1859. console.log("🔄 Pick order assigned event received, refreshing data...");
  1860. fetchAllCombinedLotData();
  1861. };
  1862. window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
  1863. return () => {
  1864. window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
  1865. };
  1866. }, [fetchAllCombinedLotData]);
  1867. const handleManualInputSubmit = useCallback(() => {
  1868. if (qrScanInput.trim() !== '') {
  1869. handleQrCodeSubmit(qrScanInput.trim());
  1870. }
  1871. }, [qrScanInput, handleQrCodeSubmit]);
  1872. // Handle QR code submission from modal (internal scanning)
  1873. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  1874. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  1875. console.log(` QR Code verified for lot: ${lotNo}`);
  1876. const requiredQty = selectedLotForQr.requiredQty;
  1877. const lotId = selectedLotForQr.lotId;
  1878. // Create stock out line
  1879. try {
  1880. const stockOutLineUpdate = await updateStockOutLineStatus({
  1881. id: selectedLotForQr.stockOutLineId,
  1882. status: 'checked',
  1883. qty: selectedLotForQr.stockOutLineQty || 0
  1884. });
  1885. console.log("Stock out line updated successfully!");
  1886. setQrScanSuccess(true);
  1887. setQrScanError(false);
  1888. // Clear selected lot (scanner stays active)
  1889. setSelectedLotForQr(null);
  1890. // Set pick quantity
  1891. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  1892. setTimeout(() => {
  1893. setPickQtyData(prev => ({
  1894. ...prev,
  1895. [lotKey]: requiredQty
  1896. }));
  1897. console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  1898. }, 500);
  1899. } catch (error) {
  1900. console.error("Error creating stock out line:", error);
  1901. }
  1902. }
  1903. }, [selectedLotForQr]);
  1904. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  1905. if (value === '' || value === null || value === undefined) {
  1906. setPickQtyData(prev => ({
  1907. ...prev,
  1908. [lotKey]: 0
  1909. }));
  1910. return;
  1911. }
  1912. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  1913. if (isNaN(numericValue)) {
  1914. setPickQtyData(prev => ({
  1915. ...prev,
  1916. [lotKey]: 0
  1917. }));
  1918. return;
  1919. }
  1920. setPickQtyData(prev => ({
  1921. ...prev,
  1922. [lotKey]: numericValue
  1923. }));
  1924. }, []);
  1925. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  1926. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  1927. const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
  1928. const checkAndAutoAssignNext = useCallback(async () => {
  1929. if (!currentUserId) return;
  1930. try {
  1931. const completionResponse = await checkPickOrderCompletion(currentUserId);
  1932. if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
  1933. console.log("Found completed pick orders, auto-assigning next...");
  1934. // 移除前端的自动分配逻辑,因为后端已经处理了
  1935. // await handleAutoAssignAndRelease(); // 删除这个函数
  1936. }
  1937. } catch (error) {
  1938. console.error("Error checking pick order completion:", error);
  1939. }
  1940. }, [currentUserId]);
  1941. // Handle reject lot
  1942. // Handle pick execution form
  1943. const handlePickExecutionForm = useCallback((lot: any) => {
  1944. console.log("=== Pick Execution Form ===");
  1945. console.log("Lot data:", lot);
  1946. if (!lot) {
  1947. console.warn("No lot data provided for pick execution form");
  1948. return;
  1949. }
  1950. console.log("Opening pick execution form for lot:", lot.lotNo);
  1951. setSelectedLotForExecutionForm(lot);
  1952. setPickExecutionFormOpen(true);
  1953. console.log("Pick execution form opened for lot ID:", lot.lotId);
  1954. }, []);
  1955. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  1956. try {
  1957. console.log("Pick execution form submitted:", data);
  1958. const issueData = {
  1959. ...data,
  1960. type: "Do", // Delivery Order Record 类型
  1961. pickerName: session?.user?.name || '',
  1962. };
  1963. const result = await recordPickExecutionIssue(issueData);
  1964. console.log("Pick execution issue recorded:", result);
  1965. if (result && result.code === "SUCCESS") {
  1966. console.log(" Pick execution issue recorded successfully");
  1967. } else {
  1968. console.error(" Failed to record pick execution issue:", result);
  1969. }
  1970. setPickExecutionFormOpen(false);
  1971. setSelectedLotForExecutionForm(null);
  1972. setQrScanError(false);
  1973. setQrScanSuccess(false);
  1974. setQrScanInput('');
  1975. // ✅ Keep scanner active after form submission - don't stop scanning
  1976. // Only clear processed QR codes for the specific lot, not all
  1977. // setIsManualScanning(false); // Removed - keep scanner active
  1978. // stopScan(); // Removed - keep scanner active
  1979. // resetScan(); // Removed - keep scanner active
  1980. // Don't clear all processed codes - only clear for this specific lot if needed
  1981. await fetchAllCombinedLotData();
  1982. } catch (error) {
  1983. console.error("Error submitting pick execution form:", error);
  1984. }
  1985. }, [fetchAllCombinedLotData]);
  1986. // Calculate remaining required quantity
  1987. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  1988. const requiredQty = lot.requiredQty || 0;
  1989. const stockOutLineQty = lot.stockOutLineQty || 0;
  1990. return Math.max(0, requiredQty - stockOutLineQty);
  1991. }, []);
  1992. // Search criteria
  1993. const searchCriteria: Criterion<any>[] = [
  1994. {
  1995. label: t("Pick Order Code"),
  1996. paramName: "pickOrderCode",
  1997. type: "text",
  1998. },
  1999. {
  2000. label: t("Item Code"),
  2001. paramName: "itemCode",
  2002. type: "text",
  2003. },
  2004. {
  2005. label: t("Item Name"),
  2006. paramName: "itemName",
  2007. type: "text",
  2008. },
  2009. {
  2010. label: t("Lot No"),
  2011. paramName: "lotNo",
  2012. type: "text",
  2013. },
  2014. ];
  2015. const handleSearch = useCallback((query: Record<string, any>) => {
  2016. setSearchQuery({ ...query });
  2017. console.log("Search query:", query);
  2018. if (!originalCombinedData) return;
  2019. const filtered = originalCombinedData.filter((lot: any) => {
  2020. const pickOrderCodeMatch = !query.pickOrderCode ||
  2021. lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  2022. const itemCodeMatch = !query.itemCode ||
  2023. lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
  2024. const itemNameMatch = !query.itemName ||
  2025. lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
  2026. const lotNoMatch = !query.lotNo ||
  2027. lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
  2028. return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
  2029. });
  2030. setCombinedLotData(filtered);
  2031. console.log("Filtered lots count:", filtered.length);
  2032. }, [originalCombinedData]);
  2033. const handleReset = useCallback(() => {
  2034. setSearchQuery({});
  2035. if (originalCombinedData) {
  2036. setCombinedLotData(originalCombinedData);
  2037. }
  2038. }, [originalCombinedData]);
  2039. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  2040. setPaginationController(prev => ({
  2041. ...prev,
  2042. pageNum: newPage,
  2043. }));
  2044. }, []);
  2045. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  2046. const newPageSize = parseInt(event.target.value, 10);
  2047. setPaginationController({
  2048. pageNum: 0,
  2049. pageSize: newPageSize === -1 ? -1 : newPageSize,
  2050. });
  2051. }, []);
  2052. // Pagination data with sorting by routerIndex
  2053. // Remove the sorting logic and just do pagination
  2054. // ✅ Memoize paginatedData to prevent re-renders when modal opens
  2055. const paginatedData = useMemo(() => {
  2056. if (paginationController.pageSize === -1) {
  2057. return combinedLotData; // Show all items
  2058. }
  2059. const startIndex = paginationController.pageNum * paginationController.pageSize;
  2060. const endIndex = startIndex + paginationController.pageSize;
  2061. return combinedLotData.slice(startIndex, endIndex); // No sorting needed
  2062. }, [combinedLotData, paginationController.pageNum, paginationController.pageSize]);
  2063. const allItemsReady = useMemo(() => {
  2064. if (combinedLotData.length === 0) return false;
  2065. return combinedLotData.every((lot: any) => {
  2066. const status = lot.stockOutLineStatus?.toLowerCase();
  2067. const isRejected =
  2068. status === 'rejected' || lot.lotAvailability === 'rejected';
  2069. const isCompleted =
  2070. status === 'completed' || status === 'partially_completed' || status === 'partially_complete';
  2071. const isChecked = status === 'checked';
  2072. const isPending = status === 'pending';
  2073. // ✅ FIXED: 无库存(noLot)行:pending 状态也应该被视为 ready(可以提交)
  2074. if (lot.noLot === true) {
  2075. return isChecked || isCompleted || isRejected || isPending;
  2076. }
  2077. // 正常 lot:必须已扫描/提交或者被拒收
  2078. return isChecked || isCompleted || isRejected;
  2079. });
  2080. }, [combinedLotData]);
  2081. const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
  2082. if (!lot.stockOutLineId) {
  2083. console.error("No stock out line found for this lot");
  2084. return;
  2085. }
  2086. try {
  2087. // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0
  2088. if (submitQty === 0) {
  2089. console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
  2090. console.log(`Lot: ${lot.lotNo}`);
  2091. console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
  2092. console.log(`Setting status to 'completed' with qty: 0`);
  2093. const updateResult = await updateStockOutLineStatus({
  2094. id: lot.stockOutLineId,
  2095. status: 'completed',
  2096. qty: 0
  2097. });
  2098. console.log('Update result:', updateResult);
  2099. const r: any = updateResult as any;
  2100. const updateOk =
  2101. r?.code === 'SUCCESS' ||
  2102. r?.type === 'completed' ||
  2103. typeof r?.id === 'number' ||
  2104. typeof r?.entity?.id === 'number' ||
  2105. (r?.message && r.message.includes('successfully'));
  2106. if (!updateResult || !updateOk) {
  2107. console.error('Failed to update stock out line status:', updateResult);
  2108. throw new Error('Failed to update stock out line status');
  2109. }
  2110. // Check if pick order is completed
  2111. if (lot.pickOrderConsoCode) {
  2112. console.log(` Lot ${lot.lotNo} completed (all zeros), checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  2113. try {
  2114. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  2115. console.log(` Pick order completion check result:`, completionResponse);
  2116. if (completionResponse.code === "SUCCESS") {
  2117. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  2118. } else if (completionResponse.message === "not completed") {
  2119. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  2120. } else {
  2121. console.error(` Error checking completion: ${completionResponse.message}`);
  2122. }
  2123. } catch (error) {
  2124. console.error("Error checking pick order completion:", error);
  2125. }
  2126. }
  2127. await fetchAllCombinedLotData();
  2128. console.log("All zeros submission completed successfully!");
  2129. setTimeout(() => {
  2130. checkAndAutoAssignNext();
  2131. }, 1000);
  2132. return;
  2133. }
  2134. // FIXED: Calculate cumulative quantity correctly
  2135. const currentActualPickQty = lot.actualPickQty || 0;
  2136. const cumulativeQty = currentActualPickQty + submitQty;
  2137. // FIXED: Determine status based on cumulative quantity vs required quantity
  2138. let newStatus = 'partially_completed';
  2139. if (cumulativeQty >= lot.requiredQty) {
  2140. newStatus = 'completed';
  2141. } else if (cumulativeQty > 0) {
  2142. newStatus = 'partially_completed';
  2143. } else {
  2144. newStatus = 'checked'; // QR scanned but no quantity submitted yet
  2145. }
  2146. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  2147. console.log(`Lot: ${lot.lotNo}`);
  2148. console.log(`Required Qty: ${lot.requiredQty}`);
  2149. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  2150. console.log(`New Submitted Qty: ${submitQty}`);
  2151. console.log(`Cumulative Qty: ${cumulativeQty}`);
  2152. console.log(`New Status: ${newStatus}`);
  2153. console.log(`=====================================`);
  2154. await updateStockOutLineStatus({
  2155. id: lot.stockOutLineId,
  2156. status: newStatus,
  2157. qty: cumulativeQty // Use cumulative quantity
  2158. });
  2159. if (submitQty > 0) {
  2160. await updateInventoryLotLineQuantities({
  2161. inventoryLotLineId: lot.lotId,
  2162. qty: submitQty,
  2163. status: 'available',
  2164. operation: 'pick'
  2165. });
  2166. }
  2167. // Check if pick order is completed when lot status becomes 'completed'
  2168. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  2169. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  2170. try {
  2171. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  2172. console.log(` Pick order completion check result:`, completionResponse);
  2173. if (completionResponse.code === "SUCCESS") {
  2174. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  2175. } else if (completionResponse.message === "not completed") {
  2176. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  2177. } else {
  2178. console.error(` Error checking completion: ${completionResponse.message}`);
  2179. }
  2180. } catch (error) {
  2181. console.error("Error checking pick order completion:", error);
  2182. }
  2183. }
  2184. await fetchAllCombinedLotData();
  2185. console.log("Pick quantity submitted successfully!");
  2186. setTimeout(() => {
  2187. checkAndAutoAssignNext();
  2188. }, 1000);
  2189. } catch (error) {
  2190. console.error("Error submitting pick quantity:", error);
  2191. }
  2192. }, [fetchAllCombinedLotData, checkAndAutoAssignNext]);
  2193. const handleSkip = useCallback(async (lot: any) => {
  2194. try {
  2195. console.log("Skip clicked, submit lot required qty for lot:", lot.lotNo);
  2196. await handleSubmitPickQtyWithQty(lot, lot.requiredQty);
  2197. } catch (err) {
  2198. console.error("Error in Skip:", err);
  2199. }
  2200. }, [handleSubmitPickQtyWithQty]);
  2201. const handleStartScan = useCallback(() => {
  2202. const startTime = performance.now();
  2203. console.log(`⏱️ [START SCAN] Called at: ${new Date().toISOString()}`);
  2204. console.log(`⏱️ [START SCAN] Starting manual QR scan...`);
  2205. setIsManualScanning(true);
  2206. const setManualScanningTime = performance.now() - startTime;
  2207. console.log(`⏱️ [START SCAN] setManualScanning time: ${setManualScanningTime.toFixed(2)}ms`);
  2208. setProcessedQrCodes(new Set());
  2209. setLastProcessedQr('');
  2210. setQrScanError(false);
  2211. setQrScanSuccess(false);
  2212. const beforeStartScanTime = performance.now();
  2213. startScan();
  2214. const startScanTime = performance.now() - beforeStartScanTime;
  2215. console.log(`⏱️ [START SCAN] startScan() call time: ${startScanTime.toFixed(2)}ms`);
  2216. const totalTime = performance.now() - startTime;
  2217. console.log(`⏱️ [START SCAN] Total start scan time: ${totalTime.toFixed(2)}ms`);
  2218. console.log(`⏰ [START SCAN] Start scan completed at: ${new Date().toISOString()}`);
  2219. }, [startScan]);
  2220. const handlePickOrderSwitch = useCallback(async (pickOrderId: number) => {
  2221. if (pickOrderSwitching) return;
  2222. setPickOrderSwitching(true);
  2223. try {
  2224. console.log(" Switching to pick order:", pickOrderId);
  2225. setSelectedPickOrderId(pickOrderId);
  2226. // 强制刷新数据,确保显示正确的 pick order 数据
  2227. await fetchAllCombinedLotData(currentUserId, pickOrderId);
  2228. } catch (error) {
  2229. console.error("Error switching pick order:", error);
  2230. } finally {
  2231. setPickOrderSwitching(false);
  2232. }
  2233. }, [pickOrderSwitching, currentUserId, fetchAllCombinedLotData]);
  2234. const handleStopScan = useCallback(() => {
  2235. console.log("⏸️ Pausing QR scanner...");
  2236. setIsManualScanning(false);
  2237. setQrScanError(false);
  2238. setQrScanSuccess(false);
  2239. stopScan();
  2240. resetScan();
  2241. }, [stopScan, resetScan]);
  2242. // ... existing code around line 1469 ...
  2243. const handlelotnull = useCallback(async (lot: any) => {
  2244. // 优先使用 stockouts 中的 id,如果没有则使用 stockOutLineId
  2245. const stockOutLineId = lot.stockOutLineId;
  2246. if (!stockOutLineId) {
  2247. console.error(" No stockOutLineId found for lot:", lot);
  2248. return;
  2249. }
  2250. try {
  2251. // Step 1: Update stock out line status
  2252. await updateStockOutLineStatus({
  2253. id: stockOutLineId,
  2254. status: 'completed',
  2255. qty: 0
  2256. });
  2257. // Step 2: Create pick execution issue for no-lot case
  2258. // Get pick order ID from fgPickOrders or use 0 if not available
  2259. const pickOrderId = lot.pickOrderId || fgPickOrders[0]?.pickOrderId || 0;
  2260. const pickOrderCode = lot.pickOrderCode || fgPickOrders[0]?.pickOrderCode || lot.pickOrderConsoCode || '';
  2261. const issueData: PickExecutionIssueData = {
  2262. type: "Do", // Delivery Order type
  2263. pickOrderId: pickOrderId,
  2264. pickOrderCode: pickOrderCode,
  2265. pickOrderCreateDate: dayjs().format('YYYY-MM-DD'), // Use dayjs format
  2266. pickExecutionDate: dayjs().format('YYYY-MM-DD'),
  2267. pickOrderLineId: lot.pickOrderLineId,
  2268. itemId: lot.itemId,
  2269. itemCode: lot.itemCode || '',
  2270. itemDescription: lot.itemName || '',
  2271. lotId: null, // No lot available
  2272. lotNo: null, // No lot number
  2273. storeLocation: lot.location || '',
  2274. requiredQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0,
  2275. actualPickQty: 0, // No items picked (no lot available)
  2276. missQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0, // All quantity is missing
  2277. badItemQty: 0,
  2278. issueRemark: `No lot available for this item. Handled via handlelotnull.`,
  2279. pickerName: session?.user?.name || '',
  2280. };
  2281. const result = await recordPickExecutionIssue(issueData);
  2282. console.log(" Pick execution issue created for no-lot item:", result);
  2283. if (result && result.code === "SUCCESS") {
  2284. console.log(" No-lot item handled and issue recorded successfully");
  2285. } else {
  2286. console.error(" Failed to record pick execution issue:", result);
  2287. }
  2288. // Step 3: Refresh data
  2289. await fetchAllCombinedLotData();
  2290. } catch (error) {
  2291. console.error(" Error in handlelotnull:", error);
  2292. }
  2293. }, [fetchAllCombinedLotData, session, currentUserId, fgPickOrders]);
  2294. const handleBatchScan = useCallback(async () => {
  2295. const startTime = performance.now();
  2296. console.log(`⏱️ [BATCH SCAN START]`);
  2297. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  2298. // 获取所有活跃批次(未扫描的)
  2299. const activeLots = combinedLotData.filter(lot => {
  2300. return (
  2301. lot.lotAvailability !== 'rejected' &&
  2302. lot.stockOutLineStatus !== 'rejected' &&
  2303. lot.stockOutLineStatus !== 'completed' &&
  2304. lot.stockOutLineStatus !== 'checked' && // ✅ 只处理未扫描的
  2305. lot.processingStatus !== 'completed' &&
  2306. lot.noLot !== true &&
  2307. lot.lotNo // ✅ 必须有 lotNo
  2308. );
  2309. });
  2310. if (activeLots.length === 0) {
  2311. console.log("No active lots to scan");
  2312. return;
  2313. }
  2314. console.log(`📦 Batch scanning ${activeLots.length} active lots using batch API...`);
  2315. try {
  2316. // ✅ 转换为批量扫描 API 所需的格式
  2317. const lines: BatchScanLineRequest[] = activeLots.map((lot) => ({
  2318. pickOrderLineId: Number(lot.pickOrderLineId),
  2319. inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
  2320. pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
  2321. lotNo: lot.lotNo || null,
  2322. itemId: Number(lot.itemId),
  2323. itemCode: String(lot.itemCode || ''),
  2324. stockOutLineId: lot.stockOutLineId ? Number(lot.stockOutLineId) : null, // ✅ 新增
  2325. }));
  2326. const request: BatchScanRequest = {
  2327. userId: currentUserId || 0,
  2328. lines: lines
  2329. };
  2330. console.log(`📤 Sending batch scan request with ${lines.length} lines`);
  2331. console.log(`📋 Request data:`, JSON.stringify(request, null, 2));
  2332. const scanStartTime = performance.now();
  2333. // ✅ 使用新的批量扫描 API(一次性处理所有请求)
  2334. const result = await batchScan(request);
  2335. const scanTime = performance.now() - scanStartTime;
  2336. console.log(`⏱️ Batch scan API call completed in ${scanTime.toFixed(2)}ms (${(scanTime / 1000).toFixed(3)}s)`);
  2337. console.log(`📥 Batch scan result:`, result);
  2338. // ✅ 刷新数据以获取最新的状态
  2339. const refreshStartTime = performance.now();
  2340. await fetchAllCombinedLotData();
  2341. const refreshTime = performance.now() - refreshStartTime;
  2342. console.log(`⏱️ Data refresh time: ${refreshTime.toFixed(2)}ms (${(refreshTime / 1000).toFixed(3)}s)`);
  2343. const totalTime = performance.now() - startTime;
  2344. console.log(`⏱️ [BATCH SCAN END]`);
  2345. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  2346. console.log(`⏰ End time: ${new Date().toISOString()}`);
  2347. if (result && result.code === "SUCCESS") {
  2348. setQrScanSuccess(true);
  2349. setQrScanError(false);
  2350. } else {
  2351. console.error("❌ Batch scan failed:", result);
  2352. setQrScanError(true);
  2353. setQrScanSuccess(false);
  2354. }
  2355. } catch (error) {
  2356. console.error("❌ Error in batch scan:", error);
  2357. setQrScanError(true);
  2358. setQrScanSuccess(false);
  2359. }
  2360. }, [combinedLotData, fetchAllCombinedLotData, currentUserId]);
  2361. const handleSubmitAllScanned = useCallback(async () => {
  2362. const startTime = performance.now();
  2363. console.log(`⏱️ [BATCH SUBMIT START]`);
  2364. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  2365. const scannedLots = combinedLotData.filter(lot => {
  2366. // 如果是 noLot 情况,检查状态是否为 pending 或 partially_complete
  2367. if (lot.noLot === true) {
  2368. return lot.stockOutLineStatus === 'checked' ||
  2369. lot.stockOutLineStatus === 'pending' ||
  2370. lot.stockOutLineStatus === 'partially_completed' ||
  2371. lot.stockOutLineStatus === 'PARTIALLY_COMPLETE';
  2372. }
  2373. // 正常情况:只包含 checked 状态
  2374. return lot.stockOutLineStatus === 'checked';
  2375. });
  2376. if (scannedLots.length === 0) {
  2377. console.log("No scanned items to submit");
  2378. return;
  2379. }
  2380. setIsSubmittingAll(true);
  2381. console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`);
  2382. try {
  2383. // 转换为 batchSubmitList 所需的格式(与后端 QrPickBatchSubmitRequest 匹配)
  2384. const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
  2385. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty || 0;
  2386. const currentActualPickQty = lot.actualPickQty || 0;
  2387. const cumulativeQty = currentActualPickQty + submitQty;
  2388. let newStatus = 'partially_completed';
  2389. if (cumulativeQty >= (lot.requiredQty || 0)) {
  2390. newStatus = 'completed';
  2391. }
  2392. return {
  2393. stockOutLineId: Number(lot.stockOutLineId) || 0,
  2394. pickOrderLineId: Number(lot.pickOrderLineId),
  2395. inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
  2396. requiredQty: Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0),
  2397. actualPickQty: Number(cumulativeQty),
  2398. stockOutLineStatus: newStatus,
  2399. pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
  2400. noLot: Boolean(lot.noLot === true)
  2401. };
  2402. });
  2403. const request: batchSubmitListRequest = {
  2404. userId: currentUserId || 0,
  2405. lines: lines
  2406. };
  2407. console.log(`📤 Sending batch submit request with ${lines.length} lines`);
  2408. console.log(`📋 Request data:`, JSON.stringify(request, null, 2));
  2409. const submitStartTime = performance.now();
  2410. // 使用 batchSubmitList API
  2411. const result = await batchSubmitList(request);
  2412. const submitTime = performance.now() - submitStartTime;
  2413. console.log(`⏱️ Batch submit API call completed in ${submitTime.toFixed(2)}ms (${(submitTime / 1000).toFixed(3)}s)`);
  2414. console.log(`📥 Batch submit result:`, result);
  2415. // Refresh data once after batch submission
  2416. const refreshStartTime = performance.now();
  2417. await fetchAllCombinedLotData();
  2418. const refreshTime = performance.now() - refreshStartTime;
  2419. console.log(`⏱️ Data refresh time: ${refreshTime.toFixed(2)}ms (${(refreshTime / 1000).toFixed(3)}s)`);
  2420. const totalTime = performance.now() - startTime;
  2421. console.log(`⏱️ [BATCH SUBMIT END]`);
  2422. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  2423. console.log(`⏰ End time: ${new Date().toISOString()}`);
  2424. if (result && result.code === "SUCCESS") {
  2425. setQrScanSuccess(true);
  2426. setTimeout(() => {
  2427. setQrScanSuccess(false);
  2428. checkAndAutoAssignNext();
  2429. if (onSwitchToRecordTab) {
  2430. onSwitchToRecordTab();
  2431. }
  2432. if (onRefreshReleasedOrderCount) {
  2433. onRefreshReleasedOrderCount();
  2434. }
  2435. }, 2000);
  2436. } else {
  2437. console.error("Batch submit failed:", result);
  2438. setQrScanError(true);
  2439. }
  2440. } catch (error) {
  2441. console.error("Error submitting all scanned items:", error);
  2442. setQrScanError(true);
  2443. } finally {
  2444. setIsSubmittingAll(false);
  2445. }
  2446. }, [combinedLotData, fetchAllCombinedLotData, checkAndAutoAssignNext, currentUserId, onSwitchToRecordTab, onRefreshReleasedOrderCount]);
  2447. // Calculate scanned items count
  2448. // Calculate scanned items count (should match handleSubmitAllScanned filter logic)
  2449. const scannedItemsCount = useMemo(() => {
  2450. const filtered = combinedLotData.filter(lot => {
  2451. // ✅ FIXED: 使用与 handleSubmitAllScanned 相同的过滤逻辑
  2452. if (lot.noLot === true) {
  2453. // ✅ 只包含可以提交的状态(与 handleSubmitAllScanned 保持一致)
  2454. return lot.stockOutLineStatus === 'checked' ||
  2455. lot.stockOutLineStatus === 'pending' ||
  2456. lot.stockOutLineStatus === 'partially_completed' ||
  2457. lot.stockOutLineStatus === 'PARTIALLY_COMPLETE';
  2458. }
  2459. // 正常情况:只包含 checked 状态
  2460. return lot.stockOutLineStatus === 'checked';
  2461. });
  2462. // 添加调试日志
  2463. const noLotCount = filtered.filter(l => l.noLot === true).length;
  2464. const normalCount = filtered.filter(l => l.noLot !== true).length;
  2465. console.log(`📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`);
  2466. console.log(`📊 All items breakdown:`, {
  2467. total: combinedLotData.length,
  2468. noLot: combinedLotData.filter(l => l.noLot === true).length,
  2469. normal: combinedLotData.filter(l => l.noLot !== true).length
  2470. });
  2471. return filtered.length;
  2472. }, [combinedLotData]);
  2473. // ADD THIS: Auto-stop scan when no data available
  2474. useEffect(() => {
  2475. if (isManualScanning && combinedLotData.length === 0) {
  2476. console.log("⏹️ No data available, auto-stopping QR scan...");
  2477. handleStopScan();
  2478. }
  2479. }, [combinedLotData.length, isManualScanning, handleStopScan]);
  2480. // Cleanup effect
  2481. useEffect(() => {
  2482. return () => {
  2483. // Cleanup when component unmounts (e.g., when switching tabs)
  2484. if (isManualScanning) {
  2485. console.log("🧹 Pick execution component unmounting, stopping QR scanner...");
  2486. stopScan();
  2487. resetScan();
  2488. }
  2489. };
  2490. }, [isManualScanning, stopScan, resetScan]);
  2491. const getStatusMessage = useCallback((lot: any) => {
  2492. switch (lot.stockOutLineStatus?.toLowerCase()) {
  2493. case 'pending':
  2494. return t("Please finish QR code scan and pick order.");
  2495. case 'checked':
  2496. return t("Please submit the pick order.");
  2497. case 'partially_completed':
  2498. return t("Partial quantity submitted. Please submit more or complete the order.");
  2499. case 'completed':
  2500. return t("Pick order completed successfully!");
  2501. case 'rejected':
  2502. return t("Lot has been rejected and marked as unavailable.");
  2503. case 'unavailable':
  2504. return t("This order is insufficient, please pick another lot.");
  2505. default:
  2506. return t("Please finish QR code scan and pick order.");
  2507. }
  2508. }, [t]);
  2509. return (
  2510. <TestQrCodeProvider
  2511. lotData={combinedLotData}
  2512. onScanLot={handleQrCodeSubmit}
  2513. onBatchScan={handleBatchScan}
  2514. filterActive={(lot) => (
  2515. lot.lotAvailability !== 'rejected' &&
  2516. lot.stockOutLineStatus !== 'rejected' &&
  2517. lot.stockOutLineStatus !== 'completed'
  2518. )}
  2519. >
  2520. <FormProvider {...formProps}>
  2521. <Stack spacing={2}>
  2522. <Box
  2523. sx={{
  2524. position: 'fixed',
  2525. top: 0,
  2526. left: 0,
  2527. right: 0,
  2528. zIndex: 1100, // Higher than other elements
  2529. backgroundColor: 'background.paper',
  2530. pt: 2,
  2531. pb: 1,
  2532. px: 2,
  2533. borderBottom: '1px solid',
  2534. borderColor: 'divider',
  2535. boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
  2536. }}
  2537. >
  2538. <LinearProgressWithLabel
  2539. completed={progress.completed}
  2540. total={progress.total}
  2541. label={t("Progress")}
  2542. />
  2543. <ScanStatusAlert
  2544. error={qrScanError}
  2545. success={qrScanSuccess}
  2546. errorMessage={t("QR code does not match any item in current orders.")}
  2547. successMessage={t("QR code verified.")}
  2548. />
  2549. </Box>
  2550. {/* DO Header */}
  2551. {/* 保留:Combined Lot Table - 包含所有 QR 扫描功能 */}
  2552. <Box>
  2553. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, mt: 10 }}>
  2554. <Typography variant="h6" gutterBottom sx={{ mb: 0 }}>
  2555. {t("All Pick Order Lots")}
  2556. </Typography>
  2557. <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
  2558. {/* Scanner status indicator (always visible) */}
  2559. {/*
  2560. <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
  2561. <QrCodeIcon
  2562. sx={{
  2563. color: isManualScanning ? '#4caf50' : '#9e9e9e',
  2564. animation: isManualScanning ? 'pulse 2s infinite' : 'none',
  2565. '@keyframes pulse': {
  2566. '0%, 100%': { opacity: 1 },
  2567. '50%': { opacity: 0.5 }
  2568. }
  2569. }}
  2570. />
  2571. <Typography variant="body2" sx={{ color: isManualScanning ? '#4caf50' : '#9e9e9e' }}>
  2572. {isManualScanning ? t("Scanner Active") : t("Scanner Inactive")}
  2573. </Typography>
  2574. </Box>
  2575. */}
  2576. {/* Pause/Resume button instead of Start/Stop */}
  2577. {isManualScanning ? (
  2578. <Button
  2579. variant="outlined"
  2580. startIcon={<QrCodeIcon />}
  2581. onClick={handleStopScan}
  2582. color="secondary"
  2583. sx={{ minWidth: '120px' }}
  2584. >
  2585. {t("Stop QR Scan")}
  2586. </Button>
  2587. ) : (
  2588. <Button
  2589. variant="contained"
  2590. startIcon={<QrCodeIcon />}
  2591. onClick={handleStartScan}
  2592. color="primary"
  2593. sx={{ minWidth: '120px' }}
  2594. >
  2595. {t("Start QR Scan")}
  2596. </Button>
  2597. )}
  2598. {/* 保留:Submit All Scanned Button */}
  2599. <Button
  2600. variant="contained"
  2601. color="success"
  2602. onClick={handleSubmitAllScanned}
  2603. disabled={
  2604. // scannedItemsCount === 0
  2605. !allItemsReady
  2606. || isSubmittingAll}
  2607. sx={{ minWidth: '160px' }}
  2608. >
  2609. {isSubmittingAll ? (
  2610. <>
  2611. <CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
  2612. {t("Submitting...")}
  2613. </>
  2614. ) : (
  2615. `${t("Submit All Scanned")} (${scannedItemsCount})`
  2616. )}
  2617. </Button>
  2618. </Box>
  2619. </Box>
  2620. {fgPickOrders.length > 0 && (
  2621. <Paper sx={{ p: 2, mb: 2 }}>
  2622. <Stack spacing={2}>
  2623. {/* 基本信息 */}
  2624. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  2625. <Typography variant="subtitle1">
  2626. <strong>{t("Shop Name")}:</strong> {fgPickOrders[0].shopName || '-'}
  2627. </Typography>
  2628. <Typography variant="subtitle1">
  2629. <strong>{t("Store ID")}:</strong> {fgPickOrders[0].storeId || '-'}
  2630. </Typography>
  2631. <Typography variant="subtitle1">
  2632. <strong>{t("Ticket No.")}:</strong> {fgPickOrders[0].ticketNo || '-'}
  2633. </Typography>
  2634. <Typography variant="subtitle1">
  2635. <strong>{t("Departure Time")}:</strong> {fgPickOrders[0].DepartureTime || '-'}
  2636. </Typography>
  2637. </Stack>
  2638. {/* 改进:三个字段显示在一起,使用表格式布局 */}
  2639. {/* 改进:三个字段合并显示 */}
  2640. {/* 改进:表格式显示每个 pick order */}
  2641. <Box sx={{
  2642. p: 2,
  2643. backgroundColor: '#f5f5f5',
  2644. borderRadius: 1
  2645. }}>
  2646. <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
  2647. {t("Pick Orders Details")}:
  2648. </Typography>
  2649. {(() => {
  2650. const pickOrderCodes = fgPickOrders[0].pickOrderCodes as string[] | string | undefined;
  2651. const deliveryNos = fgPickOrders[0].deliveryNos as string[] | string | undefined;
  2652. const lineCounts = fgPickOrders[0].lineCountsPerPickOrder;
  2653. const pickOrderCodesArray = Array.isArray(pickOrderCodes)
  2654. ? pickOrderCodes
  2655. : (typeof pickOrderCodes === 'string' ? pickOrderCodes.split(', ') : []);
  2656. const deliveryNosArray = Array.isArray(deliveryNos)
  2657. ? deliveryNos
  2658. : (typeof deliveryNos === 'string' ? deliveryNos.split(', ') : []);
  2659. const lineCountsArray = Array.isArray(lineCounts) ? lineCounts : [];
  2660. const maxLength = Math.max(
  2661. pickOrderCodesArray.length,
  2662. deliveryNosArray.length,
  2663. lineCountsArray.length
  2664. );
  2665. if (maxLength === 0) {
  2666. return <Typography variant="body2" color="text.secondary">-</Typography>;
  2667. }
  2668. // 使用与外部基本信息相同的样式
  2669. return Array.from({ length: maxLength }, (_, idx) => (
  2670. <Stack
  2671. key={idx}
  2672. direction="row"
  2673. spacing={4}
  2674. useFlexGap
  2675. flexWrap="wrap"
  2676. sx={{ mb: idx < maxLength - 1 ? 1 : 0 }} // 除了最后一行,都添加底部间距
  2677. >
  2678. <Typography variant="subtitle1">
  2679. <strong>{t("Delivery Order")}:</strong> {deliveryNosArray[idx] || '-'}
  2680. </Typography>
  2681. <Typography variant="subtitle1">
  2682. <strong>{t("Pick Order")}:</strong> {pickOrderCodesArray[idx] || '-'}
  2683. </Typography>
  2684. <Typography variant="subtitle1">
  2685. <strong>{t("Finsihed good items")}:</strong> {lineCountsArray[idx] || '-'}<strong>{t("kinds")}</strong>
  2686. </Typography>
  2687. </Stack>
  2688. ));
  2689. })()}
  2690. </Box>
  2691. </Stack>
  2692. </Paper>
  2693. )}
  2694. <TableContainer component={Paper}>
  2695. <Table>
  2696. <TableHead>
  2697. <TableRow>
  2698. <TableCell>{t("Index")}</TableCell>
  2699. <TableCell>{t("Route")}</TableCell>
  2700. <TableCell>{t("Item Code")}</TableCell>
  2701. <TableCell>{t("Item Name")}</TableCell>
  2702. <TableCell>{t("Lot#")}</TableCell>
  2703. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  2704. <TableCell align="center">{t("Scan Result")}</TableCell>
  2705. <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
  2706. </TableRow>
  2707. </TableHead>
  2708. <TableBody>
  2709. {paginatedData.length === 0 ? (
  2710. <TableRow>
  2711. <TableCell colSpan={11} align="center">
  2712. <Typography variant="body2" color="text.secondary">
  2713. {t("No data available")}
  2714. </Typography>
  2715. </TableCell>
  2716. </TableRow>
  2717. ) : (
  2718. // 在第 1797-1938 行之间,将整个 map 函数修改为:
  2719. paginatedData.map((lot, index) => {
  2720. // 检查是否是 issue lot
  2721. const isIssueLot = lot.stockOutLineStatus === 'rejected' || !lot.lotNo;
  2722. return (
  2723. <TableRow
  2724. key={`${lot.pickOrderLineId}-${lot.lotId || 'null'}`}
  2725. sx={{
  2726. //backgroundColor: isIssueLot ? '#fff3e0' : 'inherit',
  2727. // opacity: isIssueLot ? 0.6 : 1,
  2728. '& .MuiTableCell-root': {
  2729. //color: isIssueLot ? 'warning.main' : 'inherit'
  2730. }
  2731. }}
  2732. >
  2733. <TableCell>
  2734. <Typography variant="body2" fontWeight="bold">
  2735. {paginationController.pageNum * paginationController.pageSize + index + 1}
  2736. </Typography>
  2737. </TableCell>
  2738. <TableCell>
  2739. <Typography variant="body2">
  2740. {lot.routerRoute || '-'}
  2741. </Typography>
  2742. </TableCell>
  2743. <TableCell>{lot.itemCode}</TableCell>
  2744. <TableCell>{lot.itemName + '(' + lot.stockUnit + ')'}</TableCell>
  2745. <TableCell>
  2746. <Box>
  2747. <Typography
  2748. sx={{
  2749. // color: isIssueLot ? 'warning.main' : lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
  2750. }}
  2751. >
  2752. {lot.lotNo ||
  2753. t('No Stock Available')}
  2754. </Typography>
  2755. </Box>
  2756. </TableCell>
  2757. <TableCell align="right">
  2758. {(() => {
  2759. const requiredQty = lot.requiredQty || 0;
  2760. return requiredQty.toLocaleString() + '(' + lot.uomShortDesc + ')';
  2761. })()}
  2762. </TableCell>
  2763. <TableCell align="center">
  2764. {(() => {
  2765. const status = lot.stockOutLineStatus?.toLowerCase();
  2766. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  2767. const isNoLot = !lot.lotNo;
  2768. // rejected lot:显示红色勾选(已扫描但被拒绝)
  2769. if (isRejected && !isNoLot) {
  2770. return (
  2771. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2772. <Checkbox
  2773. checked={true}
  2774. disabled={true}
  2775. readOnly={true}
  2776. size="large"
  2777. sx={{
  2778. color: 'error.main',
  2779. '&.Mui-checked': { color: 'error.main' },
  2780. transform: 'scale(1.3)',
  2781. }}
  2782. />
  2783. </Box>
  2784. );
  2785. }
  2786. // 正常 lot:已扫描(checked/partially_completed/completed)
  2787. if (!isNoLot && status !== 'pending' && status !== 'rejected') {
  2788. return (
  2789. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2790. <Checkbox
  2791. checked={true}
  2792. disabled={true}
  2793. readOnly={true}
  2794. size="large"
  2795. sx={{
  2796. color: 'success.main',
  2797. '&.Mui-checked': { color: 'success.main' },
  2798. transform: 'scale(1.3)',
  2799. }}
  2800. />
  2801. </Box>
  2802. );
  2803. }
  2804. // noLot 且已完成/部分完成:显示红色勾选
  2805. if (isNoLot && (status === 'partially_completed' || status === 'completed')) {
  2806. return (
  2807. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2808. <Checkbox
  2809. checked={true}
  2810. disabled={true}
  2811. readOnly={true}
  2812. size="large"
  2813. sx={{
  2814. color: 'error.main',
  2815. '&.Mui-checked': { color: 'error.main' },
  2816. transform: 'scale(1.3)',
  2817. }}
  2818. />
  2819. </Box>
  2820. );
  2821. }
  2822. return null;
  2823. })()}
  2824. </TableCell>
  2825. <TableCell align="center">
  2826. <Box sx={{ display: 'flex', justifyContent: 'center' }}>
  2827. {(() => {
  2828. const status = lot.stockOutLineStatus?.toLowerCase();
  2829. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  2830. const isNoLot = !lot.lotNo;
  2831. // rejected lot:不显示任何按钮
  2832. if (isRejected && !isNoLot) {
  2833. return null;
  2834. }
  2835. // noLot 情况:只显示 Issue 按钮
  2836. if (isNoLot) {
  2837. return (
  2838. <Button
  2839. variant="outlined"
  2840. size="small"
  2841. onClick={() => handlelotnull(lot)}
  2842. disabled={status === 'completed'}
  2843. sx={{
  2844. fontSize: '0.7rem',
  2845. py: 0.5,
  2846. minHeight: '28px',
  2847. minWidth: '60px',
  2848. borderColor: 'warning.main',
  2849. color: 'warning.main'
  2850. }}
  2851. >
  2852. {t("Issue")}
  2853. </Button>
  2854. );
  2855. }
  2856. // 正常 lot:显示 Submit 和 Issue 按钮
  2857. return (
  2858. <Stack direction="row" spacing={1} alignItems="center">
  2859. <Button
  2860. variant="contained"
  2861. onClick={() => {
  2862. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  2863. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  2864. handlePickQtyChange(lotKey, submitQty);
  2865. handleSubmitPickQtyWithQty(lot, submitQty);
  2866. }}
  2867. disabled={
  2868. lot.lotAvailability === 'expired' ||
  2869. lot.lotAvailability === 'status_unavailable' ||
  2870. lot.lotAvailability === 'rejected' ||
  2871. lot.stockOutLineStatus === 'completed' ||
  2872. lot.stockOutLineStatus === 'pending'
  2873. }
  2874. sx={{ fontSize: '0.75rem', py: 0.5, minHeight: '28px', minWidth: '70px' }}
  2875. >
  2876. {t("Submit")}
  2877. </Button>
  2878. <Button
  2879. variant="outlined"
  2880. size="small"
  2881. onClick={() => handlePickExecutionForm(lot)}
  2882. disabled={
  2883. lot.lotAvailability === 'expired' ||
  2884. //lot.lotAvailability === 'status_unavailable' ||
  2885. // lot.lotAvailability === 'rejected' ||
  2886. lot.stockOutLineStatus === 'completed'
  2887. //lot.stockOutLineStatus === 'pending'
  2888. }
  2889. sx={{
  2890. fontSize: '0.7rem',
  2891. py: 0.5,
  2892. minHeight: '28px',
  2893. minWidth: '60px',
  2894. borderColor: 'warning.main',
  2895. color: 'warning.main'
  2896. }}
  2897. title="Report missing or bad items"
  2898. >
  2899. {t("Edit")}
  2900. </Button>
  2901. <Button
  2902. variant="outlined"
  2903. size="small"
  2904. onClick={() => handleSkip(lot)}
  2905. disabled={lot.stockOutLineStatus === 'completed'}
  2906. sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }}
  2907. >
  2908. {t("Just Completed")}
  2909. </Button>
  2910. </Stack>
  2911. );
  2912. })()}
  2913. </Box>
  2914. </TableCell>
  2915. </TableRow>
  2916. );
  2917. })
  2918. )}
  2919. </TableBody>
  2920. </Table>
  2921. </TableContainer>
  2922. <TablePagination
  2923. component="div"
  2924. count={combinedLotData.length}
  2925. page={paginationController.pageNum}
  2926. rowsPerPage={paginationController.pageSize}
  2927. onPageChange={handlePageChange}
  2928. onRowsPerPageChange={handlePageSizeChange}
  2929. rowsPerPageOptions={[10, 25, 50,-1]}
  2930. labelRowsPerPage={t("Rows per page")}
  2931. labelDisplayedRows={({ from, to, count }) =>
  2932. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  2933. }
  2934. />
  2935. </Box>
  2936. </Stack>
  2937. {/* QR Code Scanner works in background - no modal needed */}
  2938. <ManualLotConfirmationModal
  2939. open={manualLotConfirmationOpen}
  2940. onClose={() => {
  2941. setManualLotConfirmationOpen(false);
  2942. }}
  2943. onConfirm={handleManualLotConfirmation}
  2944. expectedLot={expectedLotData}
  2945. scannedLot={scannedLotData}
  2946. isLoading={isConfirmingLot}
  2947. />
  2948. {/* 保留:Lot Confirmation Modal */}
  2949. {lotConfirmationOpen && expectedLotData && scannedLotData && (
  2950. <LotConfirmationModal
  2951. open={lotConfirmationOpen}
  2952. onClose={() => {
  2953. console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`);
  2954. setLotConfirmationOpen(false);
  2955. setExpectedLotData(null);
  2956. setScannedLotData(null);
  2957. setSelectedLotForQr(null);
  2958. // ✅ IMPORTANT: Clear refs to allow reprocessing the same QR code if user cancels and scans again
  2959. // This allows the modal to reopen for the same itemId with a different stockInLineId
  2960. setTimeout(() => {
  2961. lastProcessedQrRef.current = '';
  2962. processedQrCodesRef.current.clear();
  2963. console.log(`⏱️ [LOT CONFIRM MODAL] Cleared refs to allow reprocessing`);
  2964. }, 100);
  2965. // ✅ Don't clear processedQrCombinations - it tracks by itemId+stockInLineId,
  2966. // so reopening for same itemId but different stockInLineId is allowed
  2967. }}
  2968. onConfirm={handleLotConfirmation}
  2969. expectedLot={expectedLotData}
  2970. scannedLot={scannedLotData}
  2971. isLoading={isConfirmingLot}
  2972. />
  2973. )}
  2974. {/* 保留:Good Pick Execution Form Modal */}
  2975. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  2976. <GoodPickExecutionForm
  2977. open={pickExecutionFormOpen}
  2978. onClose={() => {
  2979. setPickExecutionFormOpen(false);
  2980. setSelectedLotForExecutionForm(null);
  2981. }}
  2982. onSubmit={handlePickExecutionFormSubmit}
  2983. selectedLot={selectedLotForExecutionForm}
  2984. selectedPickOrderLine={{
  2985. id: selectedLotForExecutionForm.pickOrderLineId,
  2986. itemId: selectedLotForExecutionForm.itemId,
  2987. itemCode: selectedLotForExecutionForm.itemCode,
  2988. itemName: selectedLotForExecutionForm.itemName,
  2989. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  2990. availableQty: selectedLotForExecutionForm.availableQty || 0,
  2991. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  2992. // uomCode: selectedLotForExecutionForm.uomCode || '',
  2993. uomDesc: selectedLotForExecutionForm.uomDesc || '',
  2994. pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
  2995. uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
  2996. suggestedList: [],
  2997. noLotLines: [],
  2998. }}
  2999. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  3000. pickOrderCreateDate={new Date()}
  3001. />
  3002. )}
  3003. </FormProvider>
  3004. </TestQrCodeProvider>
  3005. );
  3006. };
  3007. export default PickExecution;