FPSMS-frontend
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 

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