FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 

5086 строки
181 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, {
  26. useCallback,
  27. useEffect,
  28. useState,
  29. useRef,
  30. useMemo,
  31. startTransition,
  32. } from "react";
  33. import { useTranslation } from "react-i18next";
  34. import { useRouter } from "next/navigation";
  35. import {
  36. updateStockOutLineStatus,
  37. createStockOutLine,
  38. updateStockOutLine,
  39. recordPickExecutionIssue,
  40. fetchFGPickOrders, // Add this import
  41. FGPickOrderResponse,
  42. stockReponse,
  43. PickExecutionIssueData,
  44. checkPickOrderCompletion,
  45. fetchAllPickOrderLotsHierarchical,
  46. PickOrderCompletionResponse,
  47. checkAndCompletePickOrderByConsoCode,
  48. updateSuggestedLotLineId,
  49. updateStockOutLineStatusByQRCodeAndLotNo,
  50. confirmLotSubstitution,
  51. fetchDoPickOrderDetail, // 必须添加
  52. DoPickOrderDetail, // 必须添加
  53. fetchFGPickOrdersByUserId,
  54. batchQrSubmit,
  55. batchSubmitList, // 添加:导入 batchSubmitList
  56. batchSubmitListRequest, // 添加:导入类型
  57. batchSubmitListLineRequest,
  58. batchScan,
  59. BatchScanRequest,
  60. BatchScanLineRequest,
  61. } from "@/app/api/pickOrder/actions";
  62. import FGPickOrderInfoCard from "./FGPickOrderInfoCard";
  63. import LotConfirmationModal from "./LotConfirmationModal";
  64. //import { fetchItem } from "@/app/api/settings/item";
  65. import {
  66. updateInventoryLotLineStatus,
  67. analyzeQrCode,
  68. } from "@/app/api/inventory/actions";
  69. import { fetchNameList, NameList } from "@/app/api/user/actions";
  70. import { FormProvider, useForm } from "react-hook-form";
  71. import SearchBox, { Criterion } from "../SearchBox";
  72. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  73. import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  74. import QrCodeIcon from "@mui/icons-material/QrCode";
  75. import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
  76. import { useSession } from "next-auth/react";
  77. import { SessionWithTokens } from "@/config/authConfig";
  78. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  79. import GoodPickExecutionForm from "./GoodPickExecutionForm";
  80. import FGPickOrderCard from "./FGPickOrderCard";
  81. import LinearProgressWithLabel from "../common/LinearProgressWithLabel";
  82. import ScanStatusAlert from "../common/ScanStatusAlert";
  83. import { translateLotSubstitutionFailure } from "./lotSubstitutionMessage";
  84. import LotLabelPrintModal from "@/components/InventorySearch/LotLabelPrintModal";
  85. interface Props {
  86. filterArgs: Record<string, any>;
  87. onSwitchToRecordTab?: () => void;
  88. onRefreshReleasedOrderCount?: () => void;
  89. }
  90. type LotConfirmRunContext = {
  91. expectedLotData: {
  92. lotNo: string | null;
  93. itemCode?: string;
  94. itemName?: string;
  95. };
  96. scannedLotData: {
  97. lotNo: string | null;
  98. itemCode?: string;
  99. itemName?: string;
  100. stockInLineId: number; // 必須有,API 用
  101. inventoryLotLineId?: number | null;
  102. };
  103. selectedLotForQr: any; // 與現在一樣:含 pickOrderLineId, stockOutLineId, suggestedPickLotId, itemId…
  104. };
  105. /** 同物料多行时,优先对「有建议批次号」的行做替换,避免误选「无批次/不足」行 */
  106. function pickExpectedLotForSubstitution(
  107. activeSuggestedLots: any[],
  108. ): any | null {
  109. if (!activeSuggestedLots?.length) return null;
  110. const withLotNo = activeSuggestedLots.filter(
  111. (l) => l.lotNo != null && String(l.lotNo).trim() !== "",
  112. );
  113. if (withLotNo.length === 1) return withLotNo[0];
  114. if (withLotNo.length > 1) {
  115. const pending = withLotNo.find(
  116. (l) => (l.stockOutLineStatus || "").toLowerCase() === "pending",
  117. );
  118. return pending || withLotNo[0];
  119. }
  120. return activeSuggestedLots[0];
  121. }
  122. // QR Code Modal Component (from LotTable)
  123. const QrCodeModal: React.FC<{
  124. open: boolean;
  125. onClose: () => void;
  126. lot: any | null;
  127. onQrCodeSubmit: (lotNo: string) => void;
  128. combinedLotData: any[]; // Add this prop
  129. lotConfirmationOpen: boolean;
  130. }> = ({
  131. open,
  132. onClose,
  133. lot,
  134. onQrCodeSubmit,
  135. combinedLotData,
  136. lotConfirmationOpen = false,
  137. }) => {
  138. const { t } = useTranslation("pickOrder");
  139. const {
  140. values: qrValues,
  141. isScanning,
  142. startScan,
  143. stopScan,
  144. resetScan,
  145. } = useQrCodeScannerContext();
  146. const [manualInput, setManualInput] = useState<string>("");
  147. const [manualInputSubmitted, setManualInputSubmitted] =
  148. useState<boolean>(false);
  149. const [manualInputError, setManualInputError] = useState<boolean>(false);
  150. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  151. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  152. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  153. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(
  154. new Set(),
  155. );
  156. const [scannedQrResult, setScannedQrResult] = useState<string>("");
  157. const [fgPickOrder, setFgPickOrder] = useState<FGPickOrderResponse | null>(
  158. null,
  159. );
  160. const fetchingRef = useRef<Set<number>>(new Set());
  161. useEffect(() => {
  162. // ✅ Don't process if modal is not open
  163. if (!open) {
  164. return;
  165. }
  166. // ✅ Don't process if lot confirmation modal is open
  167. if (lotConfirmationOpen) {
  168. console.log(
  169. "Lot confirmation modal is open, skipping QrCodeModal processing...",
  170. );
  171. return;
  172. }
  173. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  174. const latestQr = qrValues[qrValues.length - 1];
  175. if (processedQrCodes.has(latestQr)) {
  176. console.log("QR code already processed, skipping...");
  177. return;
  178. }
  179. try {
  180. const qrData = JSON.parse(latestQr);
  181. if (qrData.stockInLineId && qrData.itemId) {
  182. // ✅ Check if we're already fetching this stockInLineId
  183. if (fetchingRef.current.has(qrData.stockInLineId)) {
  184. console.log(
  185. ` [QR MODAL] Already fetching stockInLineId: ${qrData.stockInLineId}, skipping duplicate call`,
  186. );
  187. return;
  188. }
  189. setProcessedQrCodes((prev) => new Set(prev).add(latestQr));
  190. setIsProcessingQr(true);
  191. setQrScanFailed(false);
  192. // ✅ Mark as fetching
  193. fetchingRef.current.add(qrData.stockInLineId);
  194. const fetchStartTime = performance.now();
  195. console.log(
  196. ` [QR MODAL] Starting fetchStockInLineInfo for stockInLineId: ${qrData.stockInLineId}`,
  197. );
  198. fetchStockInLineInfo(qrData.stockInLineId)
  199. .then((stockInLineInfo) => {
  200. // ✅ Remove from fetching set
  201. fetchingRef.current.delete(qrData.stockInLineId);
  202. // ✅ Check again if modal is still open and lot confirmation is not open
  203. if (!open || lotConfirmationOpen) {
  204. console.log("Modal state changed, skipping result processing");
  205. return;
  206. }
  207. const fetchTime = performance.now() - fetchStartTime;
  208. console.log(
  209. ` [QR MODAL] fetchStockInLineInfo time: ${fetchTime.toFixed(
  210. 2,
  211. )}ms (${(fetchTime / 1000).toFixed(3)}s)`,
  212. );
  213. console.log("Stock in line info:", stockInLineInfo);
  214. setScannedQrResult(stockInLineInfo.lotNo || "Unknown lot number");
  215. if (stockInLineInfo.lotNo === lot.lotNo) {
  216. console.log(` QR Code verified for lot: ${lot.lotNo}`);
  217. setQrScanSuccess(true);
  218. onQrCodeSubmit(lot.lotNo);
  219. // onClose();
  220. //resetScan();
  221. } else {
  222. console.log(
  223. ` QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`,
  224. );
  225. setQrScanFailed(true);
  226. setManualInputError(true);
  227. setManualInputSubmitted(true);
  228. }
  229. })
  230. .catch((error) => {
  231. // ✅ Remove from fetching set
  232. fetchingRef.current.delete(qrData.stockInLineId);
  233. // ✅ Check again if modal is still open
  234. if (!open || lotConfirmationOpen) {
  235. console.log("Modal state changed, skipping error handling");
  236. return;
  237. }
  238. const fetchTime = performance.now() - fetchStartTime;
  239. console.error(
  240. `❌ [QR MODAL] fetchStockInLineInfo failed after ${fetchTime.toFixed(
  241. 2,
  242. )}ms:`,
  243. error,
  244. );
  245. setScannedQrResult("Error fetching data");
  246. setQrScanFailed(true);
  247. setManualInputError(true);
  248. setManualInputSubmitted(true);
  249. })
  250. .finally(() => {
  251. setIsProcessingQr(false);
  252. });
  253. } else {
  254. const qrContent = latestQr.replace(/[{}]/g, "");
  255. setScannedQrResult(qrContent);
  256. if (qrContent === lot.lotNo) {
  257. setQrScanSuccess(true);
  258. onQrCodeSubmit(lot.lotNo);
  259. onClose();
  260. resetScan();
  261. } else {
  262. setQrScanFailed(true);
  263. setManualInputError(true);
  264. setManualInputSubmitted(true);
  265. }
  266. }
  267. } catch (error) {
  268. console.log("QR code is not JSON format, trying direct comparison");
  269. const qrContent = latestQr.replace(/[{}]/g, "");
  270. setScannedQrResult(qrContent);
  271. if (qrContent === lot.lotNo) {
  272. setQrScanSuccess(true);
  273. onQrCodeSubmit(lot.lotNo);
  274. onClose();
  275. resetScan();
  276. } else {
  277. setQrScanFailed(true);
  278. setManualInputError(true);
  279. setManualInputSubmitted(true);
  280. }
  281. }
  282. }
  283. }, [
  284. qrValues,
  285. lot,
  286. onQrCodeSubmit,
  287. onClose,
  288. resetScan,
  289. isProcessingQr,
  290. qrScanSuccess,
  291. processedQrCodes,
  292. lotConfirmationOpen,
  293. open,
  294. ]);
  295. // Clear states when modal opens
  296. useEffect(() => {
  297. if (open) {
  298. setManualInput("");
  299. setManualInputSubmitted(false);
  300. setManualInputError(false);
  301. setIsProcessingQr(false);
  302. setQrScanFailed(false);
  303. setQrScanSuccess(false);
  304. setScannedQrResult("");
  305. setProcessedQrCodes(new Set());
  306. }
  307. }, [open]);
  308. useEffect(() => {
  309. if (lot) {
  310. setManualInput("");
  311. setManualInputSubmitted(false);
  312. setManualInputError(false);
  313. setIsProcessingQr(false);
  314. setQrScanFailed(false);
  315. setQrScanSuccess(false);
  316. setScannedQrResult("");
  317. setProcessedQrCodes(new Set());
  318. }
  319. }, [lot]);
  320. // Auto-submit manual input when it matches
  321. useEffect(() => {
  322. if (
  323. manualInput.trim() === lot?.lotNo &&
  324. manualInput.trim() !== "" &&
  325. !qrScanFailed &&
  326. !qrScanSuccess
  327. ) {
  328. console.log(" Auto-submitting manual input:", manualInput.trim());
  329. const timer = setTimeout(() => {
  330. setQrScanSuccess(true);
  331. onQrCodeSubmit(lot.lotNo);
  332. onClose();
  333. setManualInput("");
  334. setManualInputError(false);
  335. setManualInputSubmitted(false);
  336. }, 200);
  337. return () => clearTimeout(timer);
  338. }
  339. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  340. const handleManualSubmit = () => {
  341. if (manualInput.trim() === lot?.lotNo) {
  342. setQrScanSuccess(true);
  343. onQrCodeSubmit(lot.lotNo);
  344. onClose();
  345. setManualInput("");
  346. } else {
  347. setQrScanFailed(true);
  348. setManualInputError(true);
  349. setManualInputSubmitted(true);
  350. }
  351. };
  352. useEffect(() => {
  353. if (open) {
  354. startScan();
  355. }
  356. }, [open, startScan]);
  357. return (
  358. <Modal open={open} onClose={onClose}>
  359. <Box
  360. sx={{
  361. position: "absolute",
  362. top: "50%",
  363. left: "50%",
  364. transform: "translate(-50%, -50%)",
  365. bgcolor: "background.paper",
  366. p: 3,
  367. borderRadius: 2,
  368. minWidth: 400,
  369. }}
  370. >
  371. <Typography variant="h6" gutterBottom>
  372. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  373. </Typography>
  374. {isProcessingQr && (
  375. <Box
  376. sx={{ mb: 2, p: 2, backgroundColor: "#e3f2fd", borderRadius: 1 }}
  377. >
  378. <Typography variant="body2" color="primary">
  379. {t("Processing QR code...")}
  380. </Typography>
  381. </Box>
  382. )}
  383. <Box sx={{ mb: 2 }}>
  384. <Typography variant="body2" gutterBottom>
  385. <strong>{t("Manual Input")}:</strong>
  386. </Typography>
  387. <TextField
  388. fullWidth
  389. size="small"
  390. value={manualInput}
  391. onChange={(e) => {
  392. setManualInput(e.target.value);
  393. if (qrScanFailed || manualInputError) {
  394. setQrScanFailed(false);
  395. setManualInputError(false);
  396. setManualInputSubmitted(false);
  397. }
  398. }}
  399. sx={{ mb: 1 }}
  400. error={manualInputSubmitted && manualInputError}
  401. helperText={
  402. manualInputSubmitted && manualInputError
  403. ? `${t(
  404. "The input is not the same as the expected lot number.",
  405. )}`
  406. : ""
  407. }
  408. />
  409. <Button
  410. variant="contained"
  411. onClick={handleManualSubmit}
  412. disabled={!manualInput.trim()}
  413. size="small"
  414. color="primary"
  415. >
  416. {t("Submit")}
  417. </Button>
  418. </Box>
  419. {qrValues.length > 0 && (
  420. <Box
  421. sx={{
  422. mb: 2,
  423. p: 2,
  424. backgroundColor: qrScanFailed
  425. ? "#ffebee"
  426. : qrScanSuccess
  427. ? "#e8f5e8"
  428. : "#f5f5f5",
  429. borderRadius: 1,
  430. }}
  431. >
  432. <Typography
  433. variant="body2"
  434. color={
  435. qrScanFailed
  436. ? "error"
  437. : qrScanSuccess
  438. ? "success"
  439. : "text.secondary"
  440. }
  441. >
  442. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  443. </Typography>
  444. {qrScanSuccess && (
  445. <Typography variant="caption" color="success" display="block">
  446. {t("Verified successfully!")}
  447. </Typography>
  448. )}
  449. </Box>
  450. )}
  451. <Box sx={{ mt: 2, textAlign: "right" }}>
  452. <Button onClick={onClose} variant="outlined">
  453. {t("Cancel")}
  454. </Button>
  455. </Box>
  456. </Box>
  457. </Modal>
  458. );
  459. };
  460. const ManualLotConfirmationModal: React.FC<{
  461. open: boolean;
  462. onClose: () => void;
  463. onConfirm: (expectedLotNo: string, scannedLotNo: string) => void;
  464. expectedLot: {
  465. lotNo: string;
  466. itemCode: string;
  467. itemName: string;
  468. } | null;
  469. scannedLot: {
  470. lotNo: string;
  471. itemCode: string;
  472. itemName: string;
  473. } | null;
  474. isLoading?: boolean;
  475. }> = ({
  476. open,
  477. onClose,
  478. onConfirm,
  479. expectedLot,
  480. scannedLot,
  481. isLoading = false,
  482. }) => {
  483. const { t } = useTranslation("pickOrder");
  484. const [expectedLotInput, setExpectedLotInput] = useState<string>("");
  485. const [scannedLotInput, setScannedLotInput] = useState<string>("");
  486. const [error, setError] = useState<string>("");
  487. // 当模态框打开时,预填充输入框
  488. useEffect(() => {
  489. if (open) {
  490. setExpectedLotInput(expectedLot?.lotNo || "");
  491. setScannedLotInput(scannedLot?.lotNo || "");
  492. setError("");
  493. }
  494. }, [open, expectedLot, scannedLot]);
  495. const handleConfirm = () => {
  496. if (!expectedLotInput.trim() || !scannedLotInput.trim()) {
  497. setError(t("Please enter both expected and scanned lot numbers."));
  498. return;
  499. }
  500. if (expectedLotInput.trim() === scannedLotInput.trim()) {
  501. setError(t("Expected and scanned lot numbers cannot be the same."));
  502. return;
  503. }
  504. onConfirm(expectedLotInput.trim(), scannedLotInput.trim());
  505. };
  506. return (
  507. <Modal open={open} onClose={onClose}>
  508. <Box
  509. sx={{
  510. position: "absolute",
  511. top: "50%",
  512. left: "50%",
  513. transform: "translate(-50%, -50%)",
  514. bgcolor: "background.paper",
  515. p: 3,
  516. borderRadius: 2,
  517. minWidth: 500,
  518. }}
  519. >
  520. <Typography variant="h6" gutterBottom color="warning.main">
  521. {t("Manual Lot Confirmation")}
  522. </Typography>
  523. <Box sx={{ mb: 2 }}>
  524. <Typography variant="body2" gutterBottom>
  525. <strong>{t("Expected Lot Number")}:</strong>
  526. </Typography>
  527. <TextField
  528. fullWidth
  529. size="small"
  530. value={expectedLotInput}
  531. onChange={(e) => {
  532. setExpectedLotInput(e.target.value);
  533. setError("");
  534. }}
  535. placeholder={expectedLot?.lotNo || t("Enter expected lot number")}
  536. sx={{ mb: 2 }}
  537. error={!!error && !expectedLotInput.trim()}
  538. />
  539. </Box>
  540. <Box sx={{ mb: 2 }}>
  541. <Typography variant="body2" gutterBottom>
  542. <strong>{t("Scanned Lot Number")}:</strong>
  543. </Typography>
  544. <TextField
  545. fullWidth
  546. size="small"
  547. value={scannedLotInput}
  548. onChange={(e) => {
  549. setScannedLotInput(e.target.value);
  550. setError("");
  551. }}
  552. placeholder={scannedLot?.lotNo || t("Enter scanned lot number")}
  553. sx={{ mb: 2 }}
  554. error={!!error && !scannedLotInput.trim()}
  555. />
  556. </Box>
  557. {error && (
  558. <Box
  559. sx={{ mb: 2, p: 1, backgroundColor: "#ffebee", borderRadius: 1 }}
  560. >
  561. <Typography variant="body2" color="error">
  562. {error}
  563. </Typography>
  564. </Box>
  565. )}
  566. <Box
  567. sx={{ mt: 2, display: "flex", justifyContent: "flex-end", gap: 2 }}
  568. >
  569. <Button onClick={onClose} variant="outlined" disabled={isLoading}>
  570. {t("Cancel")}
  571. </Button>
  572. <Button
  573. onClick={handleConfirm}
  574. variant="contained"
  575. color="warning"
  576. disabled={
  577. isLoading || !expectedLotInput.trim() || !scannedLotInput.trim()
  578. }
  579. >
  580. {isLoading ? t("Processing...") : t("Confirm")}
  581. </Button>
  582. </Box>
  583. </Box>
  584. </Modal>
  585. );
  586. };
  587. /** 過期批號(未換有效批前):與 noLot 類似——單筆/批量預設提交量為 0,除非 Issue 改數 */
  588. function isLotAvailabilityExpired(lot: any): boolean {
  589. return String(lot?.lotAvailability || "").toLowerCase() === "expired";
  590. }
  591. /** inventory_lot_line.status = unavailable(API 可能用 lotAvailability 或 lotStatus) */
  592. function isInventoryLotLineUnavailable(lot: any): boolean {
  593. if (!lot) return false;
  594. if (lot.lotAvailability === "status_unavailable") return true;
  595. return String(lot.lotStatus || "").toLowerCase() === "unavailable";
  596. }
  597. /** Issue「改數」未寫入 SOL,刷新/換頁後需靠 session 還原,否則 Qty will submit 會回到 req */
  598. const FG_ISSUE_PICKED_KEY = (doPickOrderId: number) =>
  599. `fpsms-fg-issuePickedQty:${doPickOrderId}`;
  600. function loadIssuePickedMap(doPickOrderId: number): Record<number, number> {
  601. if (typeof window === "undefined" || !doPickOrderId) return {};
  602. try {
  603. const raw = sessionStorage.getItem(FG_ISSUE_PICKED_KEY(doPickOrderId));
  604. if (!raw) return {};
  605. const parsed = JSON.parse(raw) as Record<string, number>;
  606. const out: Record<number, number> = {};
  607. Object.entries(parsed).forEach(([k, v]) => {
  608. const n = Number(v);
  609. if (!Number.isNaN(n)) out[Number(k)] = n;
  610. });
  611. return out;
  612. } catch {
  613. return {};
  614. }
  615. }
  616. function saveIssuePickedMap(
  617. doPickOrderId: number,
  618. map: Record<number, number>,
  619. ) {
  620. if (typeof window === "undefined" || !doPickOrderId) return;
  621. try {
  622. sessionStorage.setItem(
  623. FG_ISSUE_PICKED_KEY(doPickOrderId),
  624. JSON.stringify(map),
  625. );
  626. } catch {
  627. // quota / private mode
  628. }
  629. }
  630. const PickExecution: React.FC<Props> = ({
  631. filterArgs,
  632. onSwitchToRecordTab,
  633. onRefreshReleasedOrderCount,
  634. }) => {
  635. const { t } = useTranslation("pickOrder");
  636. const router = useRouter();
  637. const { data: session } = useSession() as { data: SessionWithTokens | null };
  638. const [doPickOrderDetail, setDoPickOrderDetail] =
  639. useState<DoPickOrderDetail | null>(null);
  640. const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>(
  641. null,
  642. );
  643. const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
  644. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  645. const [allLotsCompleted, setAllLotsCompleted] = useState(false);
  646. const [combinedLotData, setCombinedLotData] = useState<any[]>([]);
  647. const [combinedDataLoading, setCombinedDataLoading] = useState(false);
  648. const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
  649. // issue form 里填的 actualPickQty(用于 batch submit 只提交实际拣到数量,而不是补拣到 required)
  650. const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState<
  651. Record<number, number>
  652. >({});
  653. const applyLocalStockOutLineUpdate = useCallback(
  654. (stockOutLineId: number, status: string, actualPickQty?: number) => {
  655. setCombinedLotData((prev) =>
  656. prev.map((lot) => {
  657. if (Number(lot.stockOutLineId) !== Number(stockOutLineId)) return lot;
  658. return {
  659. ...lot,
  660. stockOutLineStatus: status,
  661. ...(typeof actualPickQty === "number"
  662. ? { actualPickQty, stockOutLineQty: actualPickQty }
  663. : {}),
  664. };
  665. }),
  666. );
  667. },
  668. [],
  669. );
  670. // 防止重复点击(Submit / Just Completed / Issue)
  671. const [actionBusyBySolId, setActionBusyBySolId] = useState<
  672. Record<number, boolean>
  673. >({});
  674. const {
  675. values: qrValues,
  676. isScanning,
  677. startScan,
  678. stopScan,
  679. resetScan,
  680. } = useQrCodeScannerContext();
  681. const [qrScanInput, setQrScanInput] = useState<string>("");
  682. const [qrScanError, setQrScanError] = useState<boolean>(false);
  683. const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>("");
  684. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  685. const [manualLotConfirmationOpen, setManualLotConfirmationOpen] =
  686. useState(false);
  687. const [lotLabelPrintModalOpen, setLotLabelPrintModalOpen] = useState(false);
  688. const [lotLabelPrintInitialPayload, setLotLabelPrintInitialPayload] =
  689. useState<{
  690. itemId: number;
  691. stockInLineId: number;
  692. } | null>(null);
  693. const [lotLabelPrintReminderText, setLotLabelPrintReminderText] = useState<
  694. string | null
  695. >(null);
  696. const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
  697. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  698. const [paginationController, setPaginationController] = useState({
  699. pageNum: 0,
  700. pageSize: -1,
  701. });
  702. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  703. const initializationRef = useRef(false);
  704. const autoAssignRef = useRef(false);
  705. const formProps = useForm();
  706. const errors = formProps.formState.errors;
  707. // QR scanner states (always-on, no modal)
  708. const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
  709. const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
  710. const [lotConfirmationError, setLotConfirmationError] = useState<
  711. string | null
  712. >(null);
  713. /** QR 静默换批失败时显示在对应行的 Lot# 列,key = stockOutLineId */
  714. const [lotSwitchFailByStockOutLineId, setLotSwitchFailByStockOutLineId] =
  715. useState<Record<number, string>>({});
  716. const [expectedLotData, setExpectedLotData] = useState<any>(null);
  717. const [scannedLotData, setScannedLotData] = useState<any>(null);
  718. const [isConfirmingLot, setIsConfirmingLot] = useState(false);
  719. // Add GoodPickExecutionForm states
  720. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  721. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] =
  722. useState<any | null>(null);
  723. const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
  724. const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
  725. const lotFloorPrefixFilter = useMemo(() => {
  726. const storeId = String(fgPickOrders?.[0]?.storeId ?? "")
  727. .trim()
  728. .toUpperCase()
  729. .replace(/\s/g, "");
  730. // e.g. "2/F" -> "2F-", "4/F" -> "4F-"
  731. const floorKey = storeId.replace(/\//g, "");
  732. return floorKey ? `${floorKey}-` : "";
  733. }, [fgPickOrders]);
  734. const defaultLabelPrinterName = useMemo(() => {
  735. const storeId = String(fgPickOrders?.[0]?.storeId ?? "")
  736. .trim()
  737. .toUpperCase()
  738. .replace(/\s/g, "");
  739. const floorKey = storeId.replace(/\//g, "");
  740. if (floorKey === "2F") return "Label機 2F A+B";
  741. if (floorKey === "4F") return "Label機 4F 乾貨 C, D";
  742. return undefined;
  743. }, [fgPickOrders]);
  744. // Add these missing state variables after line 352
  745. const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
  746. // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling
  747. const [processedQrCombinations, setProcessedQrCombinations] = useState<
  748. Map<number, Set<number>>
  749. >(new Map());
  750. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(
  751. new Set(),
  752. );
  753. const [lastProcessedQr, setLastProcessedQr] = useState<string>("");
  754. const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
  755. const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
  756. // Cache for fetchStockInLineInfo API calls to avoid redundant requests
  757. const stockInLineInfoCache = useRef<
  758. Map<number, { lotNo: string | null; timestamp: number }>
  759. >(new Map());
  760. const CACHE_TTL = 60000; // 60 seconds cache TTL
  761. const abortControllerRef = useRef<AbortController | null>(null);
  762. const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  763. // Use refs for processed QR tracking to avoid useEffect dependency issues and delays
  764. const processedQrCodesRef = useRef<Set<string>>(new Set());
  765. const lastProcessedQrRef = useRef<string>("");
  766. // Store callbacks in refs to avoid useEffect dependency issues
  767. const processOutsideQrCodeRef = useRef<
  768. ((latestQr: string, qrScanCountAtInvoke?: number) => Promise<void>) | null
  769. >(null);
  770. const resetScanRef = useRef<(() => void) | null>(null);
  771. const lotConfirmOpenedQrCountRef = useRef<number>(0);
  772. const lotConfirmLastQrRef = useRef<string>("");
  773. const lotConfirmSkipNextScanRef = useRef<boolean>(false);
  774. const lotConfirmOpenedAtRef = useRef<number>(0);
  775. const handleLotConfirmationRef = useRef<
  776. | ((
  777. overrideScannedLot?: any,
  778. runContext?: LotConfirmRunContext,
  779. ) => Promise<void>)
  780. | null
  781. >(null);
  782. // Handle QR code button click
  783. const handleQrCodeClick = (pickOrderId: number) => {
  784. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  785. // TODO: Implement QR code functionality
  786. };
  787. const progress = useMemo(() => {
  788. if (combinedLotData.length === 0) {
  789. return { completed: 0, total: 0 };
  790. }
  791. // 與 allItemsReady 一致:noLot / 過期 / unavailable 的 pending 也算「已面對該行」可收尾
  792. const nonPendingCount = combinedLotData.filter((lot) => {
  793. const status = lot.stockOutLineStatus?.toLowerCase();
  794. if (status !== "pending") return true;
  795. if (
  796. lot.noLot === true ||
  797. isLotAvailabilityExpired(lot) ||
  798. isInventoryLotLineUnavailable(lot)
  799. )
  800. return true;
  801. return false;
  802. }).length;
  803. return {
  804. completed: nonPendingCount,
  805. total: combinedLotData.length,
  806. };
  807. }, [combinedLotData]);
  808. // Cached version of fetchStockInLineInfo to avoid redundant API calls
  809. const fetchStockInLineInfoCached = useCallback(
  810. async (stockInLineId: number): Promise<{ lotNo: string | null }> => {
  811. const now = Date.now();
  812. const cached = stockInLineInfoCache.current.get(stockInLineId);
  813. // Return cached value if still valid
  814. if (cached && now - cached.timestamp < CACHE_TTL) {
  815. console.log(
  816. `✅ [CACHE HIT] Using cached stockInLineInfo for ${stockInLineId}`,
  817. );
  818. return { lotNo: cached.lotNo };
  819. }
  820. // Cancel previous request if exists
  821. if (abortControllerRef.current) {
  822. abortControllerRef.current.abort();
  823. }
  824. // Create new abort controller for this request
  825. const abortController = new AbortController();
  826. abortControllerRef.current = abortController;
  827. try {
  828. console.log(
  829. ` [CACHE MISS] Fetching stockInLineInfo for ${stockInLineId}`,
  830. );
  831. const stockInLineInfo = await fetchStockInLineInfo(stockInLineId);
  832. // Store in cache
  833. stockInLineInfoCache.current.set(stockInLineId, {
  834. lotNo: stockInLineInfo.lotNo || null,
  835. timestamp: now,
  836. });
  837. // Limit cache size to prevent memory leaks
  838. if (stockInLineInfoCache.current.size > 100) {
  839. const firstKey = stockInLineInfoCache.current.keys().next().value;
  840. if (firstKey !== undefined) {
  841. stockInLineInfoCache.current.delete(firstKey);
  842. }
  843. }
  844. return { lotNo: stockInLineInfo.lotNo || null };
  845. } catch (error: any) {
  846. if (error.name === "AbortError") {
  847. console.log(` [CACHE] Request aborted for ${stockInLineId}`);
  848. throw error;
  849. }
  850. console.error(
  851. `❌ [CACHE] Error fetching stockInLineInfo for ${stockInLineId}:`,
  852. error,
  853. );
  854. throw error;
  855. }
  856. },
  857. [],
  858. );
  859. const handleLotMismatch = useCallback(
  860. (fullExpectedLotRow: any, scannedLot: any, qrScanCountAtOpen?: number) => {
  861. const mismatchStartTime = performance.now();
  862. console.log(` [HANDLE LOT MISMATCH START]`);
  863. console.log(` Start time: ${new Date().toISOString()}`);
  864. console.log("Lot mismatch detected:", { fullExpectedLotRow, scannedLot });
  865. lotConfirmOpenedQrCountRef.current =
  866. typeof qrScanCountAtOpen === "number" ? qrScanCountAtOpen : 1;
  867. // ✅ Use setTimeout to avoid flushSync warning - schedule state + silent substitution in next tick
  868. const setTimeoutStartTime = performance.now();
  869. console.time("setLotMismatchStateAndSubstitute");
  870. setTimeout(() => {
  871. const setStateStartTime = performance.now();
  872. const expectedForDisplay = {
  873. lotNo: fullExpectedLotRow.lotNo,
  874. itemCode: fullExpectedLotRow.itemCode,
  875. itemName: fullExpectedLotRow.itemName,
  876. };
  877. const scannedMerged = {
  878. ...scannedLot,
  879. lotNo: scannedLot.lotNo || null,
  880. };
  881. setExpectedLotData(expectedForDisplay);
  882. setScannedLotData(scannedMerged);
  883. setSelectedLotForQr(fullExpectedLotRow);
  884. // The QR that triggered mismatch must NOT be treated as confirmation rescan.
  885. lotConfirmSkipNextScanRef.current = true;
  886. lotConfirmOpenedAtRef.current = Date.now();
  887. const sid = Number(scannedLot.stockInLineId);
  888. if (!Number.isFinite(sid)) {
  889. console.error(
  890. ` [HANDLE LOT MISMATCH] Invalid stockInLineId for substitution: ${scannedLot.stockInLineId}`,
  891. );
  892. const errMsg = t(
  893. "Lot switch failed; pick line was not marked as checked.",
  894. );
  895. const rowSol = Number(fullExpectedLotRow.stockOutLineId);
  896. if (Number.isFinite(rowSol)) {
  897. setLotSwitchFailByStockOutLineId((prev) => ({
  898. ...prev,
  899. [rowSol]: errMsg,
  900. }));
  901. }
  902. setQrScanError(true);
  903. setQrScanSuccess(false);
  904. setQrScanErrorMsg(errMsg);
  905. const setStateTime = performance.now() - setStateStartTime;
  906. console.timeEnd("setLotMismatchStateAndSubstitute");
  907. console.log(
  908. ` [HANDLE LOT MISMATCH] Lot switch failed (invalid stockInLineId), setState time: ${setStateTime.toFixed(
  909. 2,
  910. )}ms`,
  911. );
  912. return;
  913. }
  914. const runContext: LotConfirmRunContext = {
  915. expectedLotData: expectedForDisplay,
  916. scannedLotData: {
  917. ...scannedMerged,
  918. stockInLineId: sid,
  919. itemCode: scannedMerged.itemCode ?? fullExpectedLotRow.itemCode,
  920. itemName: scannedMerged.itemName ?? fullExpectedLotRow.itemName,
  921. inventoryLotLineId:
  922. scannedLot.inventoryLotLineId ?? scannedLot.lotId ?? null,
  923. },
  924. selectedLotForQr: fullExpectedLotRow,
  925. };
  926. void handleLotConfirmationRef.current?.(undefined, runContext);
  927. const setStateTime = performance.now() - setStateStartTime;
  928. console.timeEnd("setLotMismatchStateAndSubstitute");
  929. console.log(
  930. ` [HANDLE LOT MISMATCH] Silent lot substitution scheduled (setState time: ${setStateTime.toFixed(
  931. 2,
  932. )}ms)`,
  933. );
  934. }, 0);
  935. const setTimeoutTime = performance.now() - setTimeoutStartTime;
  936. console.log(
  937. ` [PERF] setTimeout scheduling time: ${setTimeoutTime.toFixed(2)}ms`,
  938. );
  939. // ✅ Fetch lotNo in background ONLY for display purposes (using cached version)
  940. if (!scannedLot.lotNo && scannedLot.stockInLineId) {
  941. const stockInLineId = scannedLot.stockInLineId;
  942. if (typeof stockInLineId !== "number") {
  943. console.warn(
  944. ` [HANDLE LOT MISMATCH] Invalid stockInLineId: ${stockInLineId}`,
  945. );
  946. return;
  947. }
  948. console.log(
  949. ` [HANDLE LOT MISMATCH] Fetching lotNo in background (stockInLineId: ${stockInLineId})`,
  950. );
  951. const fetchStartTime = performance.now();
  952. fetchStockInLineInfoCached(stockInLineId)
  953. .then((stockInLineInfo) => {
  954. const fetchTime = performance.now() - fetchStartTime;
  955. console.log(
  956. ` [HANDLE LOT MISMATCH] fetchStockInLineInfoCached time: ${fetchTime.toFixed(
  957. 2,
  958. )}ms (${(fetchTime / 1000).toFixed(3)}s)`,
  959. );
  960. const updateStateStartTime = performance.now();
  961. startTransition(() => {
  962. setScannedLotData((prev: any) => ({
  963. ...prev,
  964. lotNo: stockInLineInfo.lotNo || null,
  965. }));
  966. });
  967. const updateStateTime = performance.now() - updateStateStartTime;
  968. console.log(
  969. ` [PERF] Update scanned lot data time: ${updateStateTime.toFixed(
  970. 2,
  971. )}ms`,
  972. );
  973. const totalTime = performance.now() - mismatchStartTime;
  974. console.log(
  975. ` [HANDLE LOT MISMATCH] Background fetch completed: ${totalTime.toFixed(
  976. 2,
  977. )}ms (${(totalTime / 1000).toFixed(3)}s)`,
  978. );
  979. })
  980. .catch((error) => {
  981. if (error.name !== "AbortError") {
  982. const fetchTime = performance.now() - fetchStartTime;
  983. console.error(
  984. `❌ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached failed after ${fetchTime.toFixed(
  985. 2,
  986. )}ms:`,
  987. error,
  988. );
  989. }
  990. });
  991. } else {
  992. const totalTime = performance.now() - mismatchStartTime;
  993. console.log(
  994. ` [HANDLE LOT MISMATCH END] Total time: ${totalTime.toFixed(
  995. 2,
  996. )}ms (${(totalTime / 1000).toFixed(3)}s)`,
  997. );
  998. }
  999. },
  1000. [fetchStockInLineInfoCached, t],
  1001. );
  1002. const checkAllLotsCompleted = useCallback((lotData: any[]) => {
  1003. if (lotData.length === 0) {
  1004. setAllLotsCompleted(false);
  1005. return false;
  1006. }
  1007. // Filter out rejected lots
  1008. const nonRejectedLots = lotData.filter(
  1009. (lot) =>
  1010. lot.lotAvailability !== "rejected" &&
  1011. lot.stockOutLineStatus !== "rejected",
  1012. );
  1013. if (nonRejectedLots.length === 0) {
  1014. setAllLotsCompleted(false);
  1015. return false;
  1016. }
  1017. // Check if all non-rejected lots are completed
  1018. const allCompleted = nonRejectedLots.every(
  1019. (lot) => lot.stockOutLineStatus === "completed",
  1020. );
  1021. setAllLotsCompleted(allCompleted);
  1022. return allCompleted;
  1023. }, []);
  1024. // 在 fetchAllCombinedLotData 函数中(约 446-684 行)
  1025. const fetchAllCombinedLotData = useCallback(
  1026. async (userId?: number, pickOrderIdOverride?: number) => {
  1027. setCombinedDataLoading(true);
  1028. try {
  1029. const userIdToUse = userId || currentUserId;
  1030. console.log(
  1031. " fetchAllCombinedLotData called with userId:",
  1032. userIdToUse,
  1033. );
  1034. if (!userIdToUse) {
  1035. console.warn("⚠️ No userId available, skipping API call");
  1036. setCombinedLotData([]);
  1037. setOriginalCombinedData([]);
  1038. setAllLotsCompleted(false);
  1039. setIssuePickedQtyBySolId({});
  1040. return;
  1041. }
  1042. // 获取新结构的层级数据
  1043. const hierarchicalData =
  1044. await fetchAllPickOrderLotsHierarchical(userIdToUse);
  1045. console.log(" Hierarchical data (new structure):", hierarchicalData);
  1046. // 检查数据结构
  1047. if (
  1048. !hierarchicalData.fgInfo ||
  1049. !hierarchicalData.pickOrders ||
  1050. hierarchicalData.pickOrders.length === 0
  1051. ) {
  1052. console.warn("⚠️ No FG info or pick orders found");
  1053. setCombinedLotData([]);
  1054. setOriginalCombinedData([]);
  1055. setAllLotsCompleted(false);
  1056. setIssuePickedQtyBySolId({});
  1057. return;
  1058. }
  1059. // 使用合并后的 pick order 对象(现在只有一个对象,包含所有数据)
  1060. const mergedPickOrder = hierarchicalData.pickOrders[0];
  1061. // 设置 FG info 到 fgPickOrders(用于显示 FG 信息卡片)
  1062. // 修改第 478-509 行的 fgOrder 构建逻辑:
  1063. const fgOrder: FGPickOrderResponse = {
  1064. doPickOrderId: hierarchicalData.fgInfo.doPickOrderId,
  1065. ticketNo: hierarchicalData.fgInfo.ticketNo,
  1066. storeId: hierarchicalData.fgInfo.storeId,
  1067. shopCode: hierarchicalData.fgInfo.shopCode,
  1068. shopName: hierarchicalData.fgInfo.shopName,
  1069. truckLanceCode: hierarchicalData.fgInfo.truckLanceCode,
  1070. DepartureTime: hierarchicalData.fgInfo.departureTime,
  1071. shopAddress: "",
  1072. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  1073. // 兼容字段(注意 consoCodes 是数组)
  1074. pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0,
  1075. pickOrderConsoCode: Array.isArray(mergedPickOrder.consoCodes)
  1076. ? mergedPickOrder.consoCodes[0] || ""
  1077. : "",
  1078. pickOrderTargetDate: mergedPickOrder.targetDate || "",
  1079. pickOrderStatus: mergedPickOrder.status || "",
  1080. deliveryOrderId: mergedPickOrder.doOrderIds?.[0] || 0,
  1081. deliveryNo: mergedPickOrder.deliveryOrderCodes?.[0] || "",
  1082. deliveryDate: "",
  1083. shopId: 0,
  1084. shopPoNo: "",
  1085. numberOfCartons: mergedPickOrder.pickOrderLines?.length || 0,
  1086. qrCodeData: hierarchicalData.fgInfo.doPickOrderId,
  1087. // 多个 pick orders 信息:全部保留为数组
  1088. numberOfPickOrders: mergedPickOrder.pickOrderIds?.length || 0,
  1089. pickOrderIds: mergedPickOrder.pickOrderIds || [],
  1090. pickOrderCodes: Array.isArray(mergedPickOrder.pickOrderCodes)
  1091. ? mergedPickOrder.pickOrderCodes
  1092. : [],
  1093. deliveryOrderIds: mergedPickOrder.doOrderIds || [],
  1094. deliveryNos: Array.isArray(mergedPickOrder.deliveryOrderCodes)
  1095. ? mergedPickOrder.deliveryOrderCodes
  1096. : [],
  1097. lineCountsPerPickOrder: Array.isArray(
  1098. mergedPickOrder.lineCountsPerPickOrder,
  1099. )
  1100. ? mergedPickOrder.lineCountsPerPickOrder
  1101. : [],
  1102. };
  1103. setFgPickOrders([fgOrder]);
  1104. console.log(
  1105. " DEBUG fgOrder.lineCountsPerPickOrder:",
  1106. fgOrder.lineCountsPerPickOrder,
  1107. );
  1108. console.log(" DEBUG fgOrder.pickOrderCodes:", fgOrder.pickOrderCodes);
  1109. console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
  1110. // 直接使用合并后的 pickOrderLines
  1111. console.log("🎯 Displaying merged pick order lines");
  1112. // 将层级数据转换为平铺格式(用于表格显示)
  1113. const flatLotData: any[] = [];
  1114. // 2/F 與後端 store_id 一致時需按 itemOrder;避免 API 未走 2F 分支時畫面仍亂序
  1115. const doFloorKey = String(hierarchicalData.fgInfo.storeId ?? "")
  1116. .trim()
  1117. .toUpperCase()
  1118. .replace(/\//g, "")
  1119. .replace(/\s/g, "");
  1120. const pickOrderLinesForDisplay =
  1121. doFloorKey === "2F"
  1122. ? [...(mergedPickOrder.pickOrderLines || [])].sort(
  1123. (a: any, b: any) => {
  1124. const ao = a.itemOrder != null ? Number(a.itemOrder) : 999999;
  1125. const bo = b.itemOrder != null ? Number(b.itemOrder) : 999999;
  1126. if (ao !== bo) return ao - bo;
  1127. return (Number(a.id) || 0) - (Number(b.id) || 0);
  1128. },
  1129. )
  1130. : mergedPickOrder.pickOrderLines || [];
  1131. pickOrderLinesForDisplay.forEach((line: any) => {
  1132. // 用来记录这一行已经通过 lots 出现过的 lotId
  1133. const lotIdSet = new Set<number>();
  1134. /** 已由有批次建議分配的量(加總後與 pick_order_line.requiredQty 的差額 = 無批次列應顯示的數) */
  1135. let lotsAllocatedSumForLine = 0;
  1136. // ✅ lots:按 lotId 去重并合并 requiredQty
  1137. if (line.lots && line.lots.length > 0) {
  1138. const lotMap = new Map<number, any>();
  1139. line.lots.forEach((lot: any) => {
  1140. const lotId = lot.id;
  1141. if (lotMap.has(lotId)) {
  1142. const existingLot = lotMap.get(lotId);
  1143. existingLot.requiredQty =
  1144. (existingLot.requiredQty || 0) + (lot.requiredQty || 0);
  1145. } else {
  1146. lotMap.set(lotId, { ...lot });
  1147. }
  1148. });
  1149. lotMap.forEach((lot: any) => {
  1150. lotsAllocatedSumForLine += Number(lot.requiredQty) || 0;
  1151. if (lot.id != null) {
  1152. lotIdSet.add(lot.id);
  1153. }
  1154. flatLotData.push({
  1155. pickOrderConsoCode: Array.isArray(mergedPickOrder.consoCodes)
  1156. ? mergedPickOrder.consoCodes[0] || ""
  1157. : "",
  1158. pickOrderTargetDate: mergedPickOrder.targetDate,
  1159. pickOrderStatus: mergedPickOrder.status,
  1160. pickOrderId:
  1161. line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0,
  1162. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  1163. pickOrderLineId: line.id,
  1164. pickOrderLineRequiredQty: line.requiredQty,
  1165. pickOrderLineStatus: line.status,
  1166. itemId: line.item.id,
  1167. itemCode: line.item.code,
  1168. itemName: line.item.name,
  1169. uomDesc: line.item.uomDesc,
  1170. uomShortDesc: line.item.uomShortDesc,
  1171. lotId: lot.id,
  1172. lotNo: lot.lotNo,
  1173. expiryDate: lot.expiryDate,
  1174. location: lot.location,
  1175. stockUnit: lot.stockUnit,
  1176. availableQty: lot.availableQty,
  1177. requiredQty: lot.requiredQty,
  1178. actualPickQty: lot.actualPickQty,
  1179. inQty: lot.inQty,
  1180. outQty: lot.outQty,
  1181. holdQty: lot.holdQty,
  1182. lotStatus: lot.lotStatus,
  1183. lotAvailability: lot.lotAvailability,
  1184. processingStatus: lot.processingStatus,
  1185. suggestedPickLotId: lot.suggestedPickLotId,
  1186. stockOutLineId: lot.stockOutLineId,
  1187. stockOutLineStatus: lot.stockOutLineStatus,
  1188. stockOutLineQty: lot.stockOutLineQty,
  1189. stockInLineId: lot.stockInLineId,
  1190. routerId: lot.router?.id,
  1191. routerIndex: lot.router?.index,
  1192. routerRoute: lot.router?.route,
  1193. routerArea: lot.router?.area,
  1194. noLot: false,
  1195. });
  1196. });
  1197. }
  1198. // ✅ stockouts:只保留“真正无批次 / 未在 lots 出现过”的行
  1199. if (line.stockouts && line.stockouts.length > 0) {
  1200. line.stockouts.forEach((stockout: any) => {
  1201. const hasLot = stockout.lotId != null;
  1202. const lotAlreadyInLots =
  1203. hasLot && lotIdSet.has(stockout.lotId as number);
  1204. // 有批次 & 已经通过 lots 渲染过 → 跳过,避免一条变两行
  1205. if (!stockout.noLot && lotAlreadyInLots) {
  1206. return;
  1207. }
  1208. // 只渲染:
  1209. // - noLot === true 的 Null stock 行
  1210. // - 或者 lotId 在 lots 中不存在的特殊情况
  1211. flatLotData.push({
  1212. pickOrderConsoCode: Array.isArray(mergedPickOrder.consoCodes)
  1213. ? mergedPickOrder.consoCodes[0] || ""
  1214. : "",
  1215. pickOrderTargetDate: mergedPickOrder.targetDate,
  1216. pickOrderStatus: mergedPickOrder.status,
  1217. pickOrderId:
  1218. line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0,
  1219. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  1220. pickOrderLineId: line.id,
  1221. pickOrderLineRequiredQty: line.requiredQty,
  1222. pickOrderLineStatus: line.status,
  1223. itemId: line.item.id,
  1224. itemCode: line.item.code,
  1225. itemName: line.item.name,
  1226. uomDesc: line.item.uomDesc,
  1227. uomShortDesc: line.item.uomShortDesc,
  1228. lotId: stockout.lotId || null,
  1229. lotNo: stockout.lotNo || null,
  1230. expiryDate: null,
  1231. location: stockout.location || null,
  1232. stockUnit: line.item.uomDesc,
  1233. availableQty: stockout.availableQty || 0,
  1234. // 無批次列對應 suggested_pick_lot 的缺口量(如 11),勿用整行 POL 需求(100)以免顯示成 89 / 100
  1235. requiredQty: stockout.noLot
  1236. ? Math.max(
  1237. 0,
  1238. (Number(line.requiredQty) || 0) - lotsAllocatedSumForLine,
  1239. )
  1240. : Number(line.requiredQty) || 0,
  1241. actualPickQty: stockout.qty || 0,
  1242. inQty: 0,
  1243. outQty: 0,
  1244. holdQty: 0,
  1245. lotStatus: stockout.noLot ? "unavailable" : "available",
  1246. lotAvailability: stockout.noLot
  1247. ? "insufficient_stock"
  1248. : "available",
  1249. processingStatus: stockout.status || "pending",
  1250. suggestedPickLotId: null,
  1251. stockOutLineId: stockout.id || null,
  1252. stockOutLineStatus: stockout.status || null,
  1253. stockOutLineQty: stockout.qty || 0,
  1254. routerId: null,
  1255. routerIndex: stockout.noLot ? 999999 : null,
  1256. routerRoute: null,
  1257. routerArea: null,
  1258. noLot: !!stockout.noLot,
  1259. });
  1260. });
  1261. }
  1262. });
  1263. console.log(" Transformed flat lot data:", flatLotData);
  1264. console.log(
  1265. " Total items (including null stock):",
  1266. flatLotData.length,
  1267. );
  1268. setCombinedLotData(flatLotData);
  1269. setOriginalCombinedData(flatLotData);
  1270. const doPid = hierarchicalData.fgInfo?.doPickOrderId;
  1271. if (doPid) {
  1272. setIssuePickedQtyBySolId(loadIssuePickedMap(doPid));
  1273. }
  1274. checkAllLotsCompleted(flatLotData);
  1275. } catch (error) {
  1276. console.error(" Error fetching combined lot data:", error);
  1277. setCombinedLotData([]);
  1278. setOriginalCombinedData([]);
  1279. setAllLotsCompleted(false);
  1280. setIssuePickedQtyBySolId({});
  1281. } finally {
  1282. setCombinedDataLoading(false);
  1283. }
  1284. },
  1285. [currentUserId, checkAllLotsCompleted],
  1286. ); // 移除 selectedPickOrderId 依赖
  1287. // Add effect to check completion when lot data changes
  1288. const handleManualLotConfirmation = useCallback(
  1289. async (currentLotNo: string, newLotNo: string) => {
  1290. console.log(
  1291. ` Manual lot confirmation: Current=${currentLotNo}, New=${newLotNo}`,
  1292. );
  1293. // 使用第一个输入框的 lot number 查找当前数据
  1294. const currentLot = combinedLotData.find(
  1295. (lot) => lot.lotNo && lot.lotNo === currentLotNo,
  1296. );
  1297. if (!currentLot) {
  1298. console.error(`❌ Current lot not found: ${currentLotNo}`);
  1299. alert(t("Current lot number not found. Please verify and try again."));
  1300. return;
  1301. }
  1302. if (!currentLot.stockOutLineId) {
  1303. console.error("❌ No stockOutLineId found for current lot");
  1304. alert(
  1305. t(
  1306. "No stock out line found for current lot. Please contact administrator.",
  1307. ),
  1308. );
  1309. return;
  1310. }
  1311. setIsConfirmingLot(true);
  1312. try {
  1313. // 调用 updateStockOutLineStatusByQRCodeAndLotNo API
  1314. // 第一个 lot 用于获取 pickOrderLineId, stockOutLineId, itemId
  1315. // 第二个 lot 作为 inventoryLotNo
  1316. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1317. pickOrderLineId: currentLot.pickOrderLineId,
  1318. inventoryLotNo: newLotNo, // 第二个输入框的值
  1319. stockOutLineId: currentLot.stockOutLineId,
  1320. itemId: currentLot.itemId,
  1321. status: "checked",
  1322. });
  1323. console.log("📥 updateStockOutLineStatusByQRCodeAndLotNo result:", res);
  1324. if (res.code === "checked" || res.code === "SUCCESS") {
  1325. // ✅ 更新本地状态
  1326. const entity = res.entity as any;
  1327. setCombinedLotData((prev) =>
  1328. prev.map((lot) => {
  1329. if (
  1330. lot.stockOutLineId === currentLot.stockOutLineId &&
  1331. lot.pickOrderLineId === currentLot.pickOrderLineId
  1332. ) {
  1333. return {
  1334. ...lot,
  1335. stockOutLineStatus: "checked",
  1336. stockOutLineQty: entity?.qty
  1337. ? Number(entity.qty)
  1338. : lot.stockOutLineQty,
  1339. };
  1340. }
  1341. return lot;
  1342. }),
  1343. );
  1344. setOriginalCombinedData((prev) =>
  1345. prev.map((lot) => {
  1346. if (
  1347. lot.stockOutLineId === currentLot.stockOutLineId &&
  1348. lot.pickOrderLineId === currentLot.pickOrderLineId
  1349. ) {
  1350. return {
  1351. ...lot,
  1352. stockOutLineStatus: "checked",
  1353. stockOutLineQty: entity?.qty
  1354. ? Number(entity.qty)
  1355. : lot.stockOutLineQty,
  1356. };
  1357. }
  1358. return lot;
  1359. }),
  1360. );
  1361. console.log("✅ Lot substitution completed successfully");
  1362. setQrScanSuccess(true);
  1363. setQrScanError(false);
  1364. // 关闭手动输入模态框
  1365. setManualLotConfirmationOpen(false);
  1366. // 刷新数据
  1367. await fetchAllCombinedLotData();
  1368. } else if (res.code === "LOT_NUMBER_MISMATCH") {
  1369. console.warn("⚠️ Backend reported LOT_NUMBER_MISMATCH:", res.message);
  1370. // ✅ 打开 lot confirmation modal 而不是显示 alert
  1371. // 从响应消息中提取 expected lot number(如果可能)
  1372. // 或者使用 currentLotNo 作为 expected lot
  1373. const expectedLotNo = currentLotNo; // 当前 lot 是期望的
  1374. // 查找新 lot 的信息(如果存在于 combinedLotData 中)
  1375. const newLot = combinedLotData.find(
  1376. (lot) => lot.lotNo && lot.lotNo === newLotNo,
  1377. );
  1378. // 设置 expected lot data
  1379. setExpectedLotData({
  1380. lotNo: expectedLotNo,
  1381. itemCode: currentLot.itemCode || "",
  1382. itemName: currentLot.itemName || "",
  1383. });
  1384. // 设置 scanned lot data
  1385. setScannedLotData({
  1386. lotNo: newLotNo,
  1387. itemCode: newLot?.itemCode || currentLot.itemCode || "",
  1388. itemName: newLot?.itemName || currentLot.itemName || "",
  1389. inventoryLotLineId: newLot?.lotId || null,
  1390. stockInLineId: null, // 手动输入时可能没有 stockInLineId
  1391. });
  1392. // 设置 selectedLotForQr 为当前 lot
  1393. setSelectedLotForQr(currentLot);
  1394. // 关闭手动输入模态框
  1395. setManualLotConfirmationOpen(false);
  1396. // 打开 lot confirmation modal
  1397. setLotConfirmationOpen(true);
  1398. setQrScanError(false); // 不显示错误,因为会打开确认模态框
  1399. setQrScanSuccess(false);
  1400. } else if (res.code === "ITEM_MISMATCH") {
  1401. console.warn("⚠️ Backend reported ITEM_MISMATCH:", res.message);
  1402. alert(t("Item mismatch: {message}", { message: res.message || "" }));
  1403. setQrScanError(true);
  1404. setQrScanSuccess(false);
  1405. // 关闭手动输入模态框
  1406. setManualLotConfirmationOpen(false);
  1407. } else {
  1408. console.warn("⚠️ Unexpected response code:", res.code);
  1409. alert(
  1410. t("Failed to update lot status. Response: {code}", {
  1411. code: res.code,
  1412. }),
  1413. );
  1414. setQrScanError(true);
  1415. setQrScanSuccess(false);
  1416. // 关闭手动输入模态框
  1417. setManualLotConfirmationOpen(false);
  1418. }
  1419. } catch (error) {
  1420. console.error("❌ Error in manual lot confirmation:", error);
  1421. alert(t("Failed to confirm lot substitution. Please try again."));
  1422. setQrScanError(true);
  1423. setQrScanSuccess(false);
  1424. // 关闭手动输入模态框
  1425. setManualLotConfirmationOpen(false);
  1426. } finally {
  1427. setIsConfirmingLot(false);
  1428. }
  1429. },
  1430. [combinedLotData, fetchAllCombinedLotData, t],
  1431. );
  1432. useEffect(() => {
  1433. if (combinedLotData.length > 0) {
  1434. checkAllLotsCompleted(combinedLotData);
  1435. }
  1436. }, [combinedLotData, checkAllLotsCompleted]);
  1437. // Add function to expose completion status to parent
  1438. const getCompletionStatus = useCallback(() => {
  1439. return allLotsCompleted;
  1440. }, [allLotsCompleted]);
  1441. // Expose completion status to parent component
  1442. useEffect(() => {
  1443. // Dispatch custom event with completion status
  1444. const event = new CustomEvent("pickOrderCompletionStatus", {
  1445. detail: {
  1446. allLotsCompleted,
  1447. tabIndex: 1, // 明确指定这是来自标签页 1 的事件
  1448. },
  1449. });
  1450. window.dispatchEvent(event);
  1451. }, [allLotsCompleted]);
  1452. const clearLotConfirmationState = useCallback(
  1453. (clearProcessedRefs: boolean = false) => {
  1454. setLotConfirmationOpen(false);
  1455. setLotConfirmationError(null);
  1456. setExpectedLotData(null);
  1457. setScannedLotData(null);
  1458. setSelectedLotForQr(null);
  1459. lotConfirmLastQrRef.current = "";
  1460. lotConfirmSkipNextScanRef.current = false;
  1461. lotConfirmOpenedAtRef.current = 0;
  1462. if (clearProcessedRefs) {
  1463. setTimeout(() => {
  1464. lastProcessedQrRef.current = "";
  1465. processedQrCodesRef.current.clear();
  1466. console.log(
  1467. ` [LOT CONFIRM MODAL] Cleared refs to allow reprocessing`,
  1468. );
  1469. }, 100);
  1470. }
  1471. },
  1472. [],
  1473. );
  1474. const parseQrPayload = useCallback(
  1475. (rawQr: string): { itemId: number; stockInLineId: number } | null => {
  1476. if (!rawQr) return null;
  1477. if (
  1478. (rawQr.startsWith("{2fitest") || rawQr.startsWith("{2fittest")) &&
  1479. rawQr.endsWith("}")
  1480. ) {
  1481. let content = "";
  1482. if (rawQr.startsWith("{2fittest")) {
  1483. content = rawQr.substring(9, rawQr.length - 1);
  1484. } else {
  1485. content = rawQr.substring(8, rawQr.length - 1);
  1486. }
  1487. const parts = content.split(",");
  1488. if (parts.length === 2) {
  1489. const itemId = parseInt(parts[0].trim(), 10);
  1490. const stockInLineId = parseInt(parts[1].trim(), 10);
  1491. if (!isNaN(itemId) && !isNaN(stockInLineId)) {
  1492. return { itemId, stockInLineId };
  1493. }
  1494. }
  1495. return null;
  1496. }
  1497. try {
  1498. const parsed = JSON.parse(rawQr);
  1499. if (parsed?.itemId && parsed?.stockInLineId) {
  1500. return { itemId: parsed.itemId, stockInLineId: parsed.stockInLineId };
  1501. }
  1502. return null;
  1503. } catch {
  1504. return null;
  1505. }
  1506. },
  1507. [],
  1508. );
  1509. const handleLotConfirmation = useCallback(
  1510. async (overrideScannedLot?: any, runContext?: LotConfirmRunContext) => {
  1511. const exp = runContext?.expectedLotData ?? expectedLotData;
  1512. const scan =
  1513. overrideScannedLot ?? runContext?.scannedLotData ?? scannedLotData;
  1514. const sel = runContext?.selectedLotForQr ?? selectedLotForQr;
  1515. if (!exp || !scan || !sel) return;
  1516. const newStockInLineId = scan?.stockInLineId;
  1517. if (newStockInLineId == null || Number.isNaN(Number(newStockInLineId)))
  1518. return;
  1519. const rowSolKey = Number(sel.stockOutLineId);
  1520. if (Number.isFinite(rowSolKey)) {
  1521. setLotSwitchFailByStockOutLineId((prev) => {
  1522. const next = { ...prev };
  1523. delete next[rowSolKey];
  1524. return next;
  1525. });
  1526. }
  1527. setIsConfirmingLot(true);
  1528. setLotConfirmationError(null);
  1529. try {
  1530. const substitutionResult = await confirmLotSubstitution({
  1531. pickOrderLineId: sel.pickOrderLineId,
  1532. stockOutLineId: sel.stockOutLineId,
  1533. originalSuggestedPickLotId: sel.suggestedPickLotId,
  1534. newInventoryLotNo: "",
  1535. newStockInLineId: newStockInLineId,
  1536. });
  1537. const substitutionCode = substitutionResult?.code;
  1538. const switchedToUnavailable =
  1539. substitutionCode === "SUCCESS_UNAVAILABLE" ||
  1540. substitutionCode === "BOUND_UNAVAILABLE";
  1541. if (
  1542. !substitutionResult ||
  1543. (substitutionCode !== "SUCCESS" && !switchedToUnavailable)
  1544. ) {
  1545. const errMsg = translateLotSubstitutionFailure(t, substitutionResult);
  1546. if (Number.isFinite(rowSolKey)) {
  1547. setLotSwitchFailByStockOutLineId((prev) => ({
  1548. ...prev,
  1549. [rowSolKey]: errMsg,
  1550. }));
  1551. }
  1552. setQrScanError(true);
  1553. setQrScanSuccess(false);
  1554. setQrScanErrorMsg(errMsg);
  1555. return;
  1556. }
  1557. if (switchedToUnavailable) {
  1558. const itemId = Number(sel?.itemId ?? exp?.itemId);
  1559. const stockInLineId = Number(newStockInLineId);
  1560. if (Number.isFinite(itemId) && Number.isFinite(stockInLineId)) {
  1561. setLotLabelPrintInitialPayload({ itemId, stockInLineId });
  1562. setLotLabelPrintReminderText(
  1563. "該批次不可用,請移除該Label並列印新Label。",
  1564. );
  1565. setLotLabelPrintModalOpen(true);
  1566. }
  1567. }
  1568. setQrScanError(false);
  1569. setQrScanSuccess(false);
  1570. setQrScanInput("");
  1571. resetScan();
  1572. setPickExecutionFormOpen(false);
  1573. if (sel?.stockOutLineId && !switchedToUnavailable) {
  1574. await updateStockOutLineStatus({
  1575. id: sel.stockOutLineId,
  1576. status: "checked",
  1577. qty: 0,
  1578. });
  1579. }
  1580. clearLotConfirmationState(false);
  1581. setIsRefreshingData(true);
  1582. await fetchAllCombinedLotData();
  1583. setIsRefreshingData(false);
  1584. } catch (error) {
  1585. console.error("Error confirming lot substitution:", error);
  1586. const errMsg = t("Lot confirmation failed. Please try again.");
  1587. if (Number.isFinite(rowSolKey)) {
  1588. setLotSwitchFailByStockOutLineId((prev) => ({
  1589. ...prev,
  1590. [rowSolKey]: errMsg,
  1591. }));
  1592. }
  1593. setQrScanError(true);
  1594. setQrScanErrorMsg(errMsg);
  1595. } finally {
  1596. setIsConfirmingLot(false);
  1597. }
  1598. },
  1599. [
  1600. expectedLotData,
  1601. scannedLotData,
  1602. selectedLotForQr,
  1603. fetchAllCombinedLotData,
  1604. resetScan,
  1605. clearLotConfirmationState,
  1606. t,
  1607. ],
  1608. );
  1609. useEffect(() => {
  1610. handleLotConfirmationRef.current = handleLotConfirmation;
  1611. }, [handleLotConfirmation]);
  1612. const handleLotConfirmationByRescan = useCallback(
  1613. async (rawQr: string): Promise<boolean> => {
  1614. if (
  1615. !lotConfirmationOpen ||
  1616. !selectedLotForQr ||
  1617. !expectedLotData ||
  1618. !scannedLotData
  1619. ) {
  1620. return false;
  1621. }
  1622. const payload = parseQrPayload(rawQr);
  1623. const expectedStockInLineId = Number(selectedLotForQr.stockInLineId);
  1624. const mismatchedStockInLineId = Number(scannedLotData?.stockInLineId);
  1625. if (payload) {
  1626. const rescannedStockInLineId = Number(payload.stockInLineId);
  1627. // 再扫“差异 lot” => 直接执行切换
  1628. if (
  1629. Number.isFinite(mismatchedStockInLineId) &&
  1630. rescannedStockInLineId === mismatchedStockInLineId
  1631. ) {
  1632. await handleLotConfirmation();
  1633. return true;
  1634. }
  1635. // 再扫“原建议 lot” => 关闭弹窗并按原 lot 正常记一次扫描
  1636. if (
  1637. Number.isFinite(expectedStockInLineId) &&
  1638. rescannedStockInLineId === expectedStockInLineId
  1639. ) {
  1640. clearLotConfirmationState(false);
  1641. if (processOutsideQrCodeRef.current) {
  1642. await processOutsideQrCodeRef.current(JSON.stringify(payload));
  1643. }
  1644. return true;
  1645. }
  1646. // 扫到第三个 lot(既不是当前差异 lot,也不是原建议 lot):
  1647. // 直接按“扫描到的这一批”执行切换。
  1648. await handleLotConfirmation({
  1649. lotNo: null,
  1650. itemCode: expectedLotData?.itemCode,
  1651. itemName: expectedLotData?.itemName,
  1652. inventoryLotLineId: null,
  1653. stockInLineId: rescannedStockInLineId,
  1654. });
  1655. return true;
  1656. } else {
  1657. // 兼容纯 lotNo 文本扫码
  1658. const scannedText = rawQr?.trim();
  1659. const expectedLotNo = expectedLotData?.lotNo?.trim();
  1660. const mismatchedLotNo = scannedLotData?.lotNo?.trim();
  1661. if (mismatchedLotNo && scannedText === mismatchedLotNo) {
  1662. await handleLotConfirmation();
  1663. return true;
  1664. }
  1665. if (expectedLotNo && scannedText === expectedLotNo) {
  1666. clearLotConfirmationState(false);
  1667. if (processOutsideQrCodeRef.current) {
  1668. await processOutsideQrCodeRef.current(
  1669. JSON.stringify({
  1670. itemId: selectedLotForQr.itemId,
  1671. stockInLineId: selectedLotForQr.stockInLineId,
  1672. }),
  1673. );
  1674. }
  1675. return true;
  1676. }
  1677. }
  1678. return false;
  1679. },
  1680. [
  1681. lotConfirmationOpen,
  1682. selectedLotForQr,
  1683. expectedLotData,
  1684. scannedLotData,
  1685. parseQrPayload,
  1686. handleLotConfirmation,
  1687. clearLotConfirmationState,
  1688. handleLotMismatch,
  1689. ],
  1690. );
  1691. const handleQrCodeSubmit = useCallback(
  1692. async (lotNo: string) => {
  1693. console.log(` Processing QR Code for lot: ${lotNo}`);
  1694. // 检查 lotNo 是否为 null 或 undefined(包括字符串 "null")
  1695. if (!lotNo || lotNo === "null" || lotNo.trim() === "") {
  1696. console.error(" Invalid lotNo: null, undefined, or empty");
  1697. return;
  1698. }
  1699. // Use current data without refreshing to avoid infinite loop
  1700. const currentLotData = combinedLotData;
  1701. console.log(
  1702. ` Available lots:`,
  1703. currentLotData.map((lot) => lot.lotNo),
  1704. );
  1705. // 修复:在比较前确保 lotNo 不为 null
  1706. const lotNoLower = lotNo.toLowerCase();
  1707. const matchingLots = currentLotData.filter((lot) => {
  1708. if (!lot.lotNo) return false; // 跳过 null lotNo
  1709. return lot.lotNo === lotNo || lot.lotNo.toLowerCase() === lotNoLower;
  1710. });
  1711. if (matchingLots.length === 0) {
  1712. console.error(` Lot not found: ${lotNo}`);
  1713. setQrScanError(true);
  1714. setQrScanSuccess(false);
  1715. const availableLotNos = currentLotData
  1716. .map((lot) => lot.lotNo)
  1717. .join(", ");
  1718. console.log(
  1719. ` QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`,
  1720. );
  1721. return;
  1722. }
  1723. const hasExpiredLot = matchingLots.some(
  1724. (lot: any) =>
  1725. String(lot.lotAvailability || "").toLowerCase() === "expired",
  1726. );
  1727. if (hasExpiredLot) {
  1728. console.warn(`⚠️ [QR PROCESS] Scanned lot ${lotNo} is expired`);
  1729. setQrScanError(true);
  1730. setQrScanSuccess(false);
  1731. return;
  1732. }
  1733. console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
  1734. setQrScanError(false);
  1735. try {
  1736. let successCount = 0;
  1737. let errorCount = 0;
  1738. for (const matchingLot of matchingLots) {
  1739. console.log(
  1740. `🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`,
  1741. );
  1742. if (matchingLot.stockOutLineId) {
  1743. const stockOutLineUpdate = await updateStockOutLineStatus({
  1744. id: matchingLot.stockOutLineId,
  1745. status: "checked",
  1746. qty: 0,
  1747. });
  1748. console.log(
  1749. `Update stock out line result for line ${matchingLot.pickOrderLineId}:`,
  1750. stockOutLineUpdate,
  1751. );
  1752. // Treat multiple backend shapes as success (type-safe via any)
  1753. const r: any = stockOutLineUpdate as any;
  1754. const updateOk =
  1755. r?.code === "SUCCESS" ||
  1756. typeof r?.id === "number" ||
  1757. r?.type === "checked" ||
  1758. r?.status === "checked" ||
  1759. typeof r?.entity?.id === "number" ||
  1760. r?.entity?.status === "checked";
  1761. if (updateOk) {
  1762. successCount++;
  1763. } else {
  1764. errorCount++;
  1765. }
  1766. } else {
  1767. const createStockOutLineData = {
  1768. consoCode: matchingLot.pickOrderConsoCode,
  1769. pickOrderLineId: matchingLot.pickOrderLineId,
  1770. inventoryLotLineId: matchingLot.lotId,
  1771. qty: 0,
  1772. };
  1773. const createResult = await createStockOutLine(
  1774. createStockOutLineData,
  1775. );
  1776. console.log(
  1777. `Create stock out line result for line ${matchingLot.pickOrderLineId}:`,
  1778. createResult,
  1779. );
  1780. if (createResult && createResult.code === "SUCCESS") {
  1781. // Immediately set status to checked for new line
  1782. let newSolId: number | undefined;
  1783. const anyRes: any = createResult as any;
  1784. if (typeof anyRes?.id === "number") {
  1785. newSolId = anyRes.id;
  1786. } else if (anyRes?.entity) {
  1787. newSolId = Array.isArray(anyRes.entity)
  1788. ? anyRes.entity[0]?.id
  1789. : anyRes.entity?.id;
  1790. }
  1791. if (newSolId) {
  1792. const setChecked = await updateStockOutLineStatus({
  1793. id: newSolId,
  1794. status: "checked",
  1795. qty: 0,
  1796. });
  1797. if (setChecked && setChecked.code === "SUCCESS") {
  1798. successCount++;
  1799. } else {
  1800. errorCount++;
  1801. }
  1802. } else {
  1803. console.warn(
  1804. "Created stock out line but no ID returned; cannot set to checked",
  1805. );
  1806. errorCount++;
  1807. }
  1808. } else {
  1809. errorCount++;
  1810. }
  1811. }
  1812. }
  1813. // FIXED: Set refresh flag before refreshing data
  1814. setIsRefreshingData(true);
  1815. console.log("🔄 Refreshing data after QR code processing...");
  1816. await fetchAllCombinedLotData();
  1817. if (successCount > 0) {
  1818. console.log(
  1819. ` QR Code processing completed: ${successCount} updated/created`,
  1820. );
  1821. setQrScanSuccess(true);
  1822. setQrScanError(false);
  1823. setQrScanInput(""); // Clear input after successful processing
  1824. //setIsManualScanning(false);
  1825. // stopScan();
  1826. // resetScan();
  1827. // Clear success state after a delay
  1828. //setTimeout(() => {
  1829. //setQrScanSuccess(false);
  1830. //}, 2000);
  1831. } else {
  1832. console.error(` QR Code processing failed: ${errorCount} errors`);
  1833. setQrScanError(true);
  1834. setQrScanSuccess(false);
  1835. // Clear error state after a delay
  1836. // setTimeout(() => {
  1837. // setQrScanError(false);
  1838. //}, 3000);
  1839. }
  1840. } catch (error) {
  1841. console.error(" Error processing QR code:", error);
  1842. setQrScanError(true);
  1843. setQrScanSuccess(false);
  1844. // Clear error state after a delay
  1845. setTimeout(() => {
  1846. setQrScanError(false);
  1847. }, 3000);
  1848. } finally {
  1849. // Clear refresh flag after a short delay
  1850. setTimeout(() => {
  1851. setIsRefreshingData(false);
  1852. }, 1000);
  1853. }
  1854. },
  1855. [combinedLotData],
  1856. );
  1857. const handleFastQrScan = useCallback(
  1858. async (lotNo: string) => {
  1859. const startTime = performance.now();
  1860. console.log(` [FAST SCAN START] Lot: ${lotNo}`);
  1861. console.log(` Start time: ${new Date().toISOString()}`);
  1862. // 从 combinedLotData 中找到对应的 lot
  1863. const findStartTime = performance.now();
  1864. const matchingLot = combinedLotData.find(
  1865. (lot) => lot.lotNo && lot.lotNo === lotNo,
  1866. );
  1867. const findTime = performance.now() - findStartTime;
  1868. console.log(` Find lot time: ${findTime.toFixed(2)}ms`);
  1869. if (!matchingLot || !matchingLot.stockOutLineId) {
  1870. const totalTime = performance.now() - startTime;
  1871. console.warn(
  1872. `⚠️ Fast scan: Lot ${lotNo} not found or no stockOutLineId`,
  1873. );
  1874. console.log(` Total time: ${totalTime.toFixed(2)}ms`);
  1875. return;
  1876. }
  1877. try {
  1878. // ✅ 使用快速 API
  1879. const apiStartTime = performance.now();
  1880. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1881. pickOrderLineId: matchingLot.pickOrderLineId,
  1882. inventoryLotNo: lotNo,
  1883. stockOutLineId: matchingLot.stockOutLineId,
  1884. itemId: matchingLot.itemId,
  1885. status: "checked",
  1886. });
  1887. const apiTime = performance.now() - apiStartTime;
  1888. console.log(` API call time: ${apiTime.toFixed(2)}ms`);
  1889. if (res.code === "checked" || res.code === "SUCCESS") {
  1890. // ✅ 只更新本地状态,不调用 fetchAllCombinedLotData
  1891. const updateStartTime = performance.now();
  1892. const entity = res.entity as any;
  1893. setCombinedLotData((prev) =>
  1894. prev.map((lot) => {
  1895. if (
  1896. lot.stockOutLineId === matchingLot.stockOutLineId &&
  1897. lot.pickOrderLineId === matchingLot.pickOrderLineId
  1898. ) {
  1899. return {
  1900. ...lot,
  1901. stockOutLineStatus: "checked",
  1902. stockOutLineQty: entity?.qty
  1903. ? Number(entity.qty)
  1904. : lot.stockOutLineQty,
  1905. };
  1906. }
  1907. return lot;
  1908. }),
  1909. );
  1910. setOriginalCombinedData((prev) =>
  1911. prev.map((lot) => {
  1912. if (
  1913. lot.stockOutLineId === matchingLot.stockOutLineId &&
  1914. lot.pickOrderLineId === matchingLot.pickOrderLineId
  1915. ) {
  1916. return {
  1917. ...lot,
  1918. stockOutLineStatus: "checked",
  1919. stockOutLineQty: entity?.qty
  1920. ? Number(entity.qty)
  1921. : lot.stockOutLineQty,
  1922. };
  1923. }
  1924. return lot;
  1925. }),
  1926. );
  1927. const updateTime = performance.now() - updateStartTime;
  1928. console.log(` State update time: ${updateTime.toFixed(2)}ms`);
  1929. const totalTime = performance.now() - startTime;
  1930. console.log(`✅ [FAST SCAN END] Lot: ${lotNo}`);
  1931. console.log(
  1932. ` Total time: ${totalTime.toFixed(2)}ms (${(
  1933. totalTime / 1000
  1934. ).toFixed(3)}s)`,
  1935. );
  1936. console.log(` End time: ${new Date().toISOString()}`);
  1937. } else {
  1938. const totalTime = performance.now() - startTime;
  1939. console.warn(`⚠️ Fast scan failed for ${lotNo}:`, res.code);
  1940. console.log(` Total time: ${totalTime.toFixed(2)}ms`);
  1941. }
  1942. } catch (error) {
  1943. const totalTime = performance.now() - startTime;
  1944. console.error(` Fast scan error for ${lotNo}:`, error);
  1945. console.log(` Total time: ${totalTime.toFixed(2)}ms`);
  1946. }
  1947. },
  1948. [combinedLotData, updateStockOutLineStatusByQRCodeAndLotNo],
  1949. );
  1950. // Enhanced lotDataIndexes with cached active lots for better performance
  1951. const lotDataIndexes = useMemo(() => {
  1952. const indexStartTime = performance.now();
  1953. console.log(
  1954. ` [PERF] lotDataIndexes calculation START, data length: ${combinedLotData.length}`,
  1955. );
  1956. const byItemId = new Map<number, any[]>();
  1957. const byItemCode = new Map<string, any[]>();
  1958. const byLotId = new Map<number, any>();
  1959. const byLotNo = new Map<string, any[]>();
  1960. const byStockInLineId = new Map<number, any[]>();
  1961. // Cache active lots separately to avoid filtering on every scan
  1962. const activeLotsByItemId = new Map<number, any[]>();
  1963. const rejectedStatuses = new Set(["rejected"]);
  1964. // ✅ Use for loop instead of forEach for better performance on tablets
  1965. for (let i = 0; i < combinedLotData.length; i++) {
  1966. const lot = combinedLotData[i];
  1967. const solStatus = String(lot.stockOutLineStatus || "").toLowerCase();
  1968. const lotAvailability = String(lot.lotAvailability || "").toLowerCase();
  1969. const processingStatus = String(lot.processingStatus || "").toLowerCase();
  1970. const isUnavailable = isInventoryLotLineUnavailable(lot);
  1971. const isExpired = isLotAvailabilityExpired(lot);
  1972. const isRejected =
  1973. rejectedStatuses.has(lotAvailability) ||
  1974. rejectedStatuses.has(solStatus) ||
  1975. rejectedStatuses.has(processingStatus);
  1976. const isEnded = solStatus === "checked" || solStatus === "completed";
  1977. const isPartially =
  1978. solStatus === "partially_completed" ||
  1979. solStatus === "partially_complete";
  1980. const isPending = solStatus === "pending" || solStatus === "";
  1981. const isActive =
  1982. !isRejected &&
  1983. !isUnavailable &&
  1984. !isExpired &&
  1985. !isEnded &&
  1986. (isPending || isPartially);
  1987. if (lot.itemId) {
  1988. if (!byItemId.has(lot.itemId)) {
  1989. byItemId.set(lot.itemId, []);
  1990. activeLotsByItemId.set(lot.itemId, []);
  1991. }
  1992. byItemId.get(lot.itemId)!.push(lot);
  1993. if (isActive) {
  1994. activeLotsByItemId.get(lot.itemId)!.push(lot);
  1995. }
  1996. }
  1997. if (lot.itemCode) {
  1998. if (!byItemCode.has(lot.itemCode)) {
  1999. byItemCode.set(lot.itemCode, []);
  2000. }
  2001. byItemCode.get(lot.itemCode)!.push(lot);
  2002. }
  2003. if (lot.lotId) {
  2004. byLotId.set(lot.lotId, lot);
  2005. }
  2006. if (lot.lotNo) {
  2007. if (!byLotNo.has(lot.lotNo)) {
  2008. byLotNo.set(lot.lotNo, []);
  2009. }
  2010. byLotNo.get(lot.lotNo)!.push(lot);
  2011. }
  2012. if (lot.stockInLineId) {
  2013. if (!byStockInLineId.has(lot.stockInLineId)) {
  2014. byStockInLineId.set(lot.stockInLineId, []);
  2015. }
  2016. byStockInLineId.get(lot.stockInLineId)!.push(lot);
  2017. }
  2018. }
  2019. const indexTime = performance.now() - indexStartTime;
  2020. if (indexTime > 10) {
  2021. console.log(
  2022. ` [PERF] lotDataIndexes calculation END: ${indexTime.toFixed(2)}ms (${(
  2023. indexTime / 1000
  2024. ).toFixed(3)}s)`,
  2025. );
  2026. }
  2027. return {
  2028. byItemId,
  2029. byItemCode,
  2030. byLotId,
  2031. byLotNo,
  2032. byStockInLineId,
  2033. activeLotsByItemId,
  2034. };
  2035. }, [combinedLotData.length, combinedLotData]);
  2036. // Store resetScan in ref for immediate access (update on every render)
  2037. resetScanRef.current = resetScan;
  2038. const processOutsideQrCode = useCallback(
  2039. async (latestQr: string, qrScanCountAtInvoke?: number) => {
  2040. const totalStartTime = performance.now();
  2041. console.log(
  2042. ` [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`,
  2043. );
  2044. console.log(` Start time: ${new Date().toISOString()}`);
  2045. // ✅ Measure index access time
  2046. const indexAccessStart = performance.now();
  2047. const indexes = lotDataIndexes; // Access the memoized indexes
  2048. const indexAccessTime = performance.now() - indexAccessStart;
  2049. console.log(
  2050. ` [PERF] Index access time: ${indexAccessTime.toFixed(2)}ms`,
  2051. );
  2052. // 1) Parse JSON safely (parse once, reuse)
  2053. const parseStartTime = performance.now();
  2054. let qrData: any = null;
  2055. let parseTime = 0;
  2056. try {
  2057. qrData = JSON.parse(latestQr);
  2058. parseTime = performance.now() - parseStartTime;
  2059. console.log(` [PERF] JSON parse time: ${parseTime.toFixed(2)}ms`);
  2060. } catch {
  2061. console.log(
  2062. "QR content is not JSON; skipping lotNo direct submit to avoid false matches.",
  2063. );
  2064. startTransition(() => {
  2065. setQrScanError(true);
  2066. setQrScanSuccess(false);
  2067. });
  2068. return;
  2069. }
  2070. try {
  2071. const validationStartTime = performance.now();
  2072. if (!(qrData?.stockInLineId && qrData?.itemId)) {
  2073. console.log(
  2074. "QR JSON missing required fields (itemId, stockInLineId).",
  2075. );
  2076. startTransition(() => {
  2077. setQrScanError(true);
  2078. setQrScanSuccess(false);
  2079. });
  2080. return;
  2081. }
  2082. const validationTime = performance.now() - validationStartTime;
  2083. console.log(` [PERF] Validation time: ${validationTime.toFixed(2)}ms`);
  2084. const scannedItemId = qrData.itemId;
  2085. const scannedStockInLineId = qrData.stockInLineId;
  2086. // ✅ Check if this combination was already processed
  2087. const duplicateCheckStartTime = performance.now();
  2088. const itemProcessedSet = processedQrCombinations.get(scannedItemId);
  2089. if (itemProcessedSet?.has(scannedStockInLineId)) {
  2090. const duplicateCheckTime =
  2091. performance.now() - duplicateCheckStartTime;
  2092. console.log(
  2093. ` [SKIP] Already processed combination: itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId} (check time: ${duplicateCheckTime.toFixed(
  2094. 2,
  2095. )}ms)`,
  2096. );
  2097. return;
  2098. }
  2099. const duplicateCheckTime = performance.now() - duplicateCheckStartTime;
  2100. console.log(
  2101. ` [PERF] Duplicate check time: ${duplicateCheckTime.toFixed(2)}ms`,
  2102. );
  2103. // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed)
  2104. const lookupStartTime = performance.now();
  2105. const activeSuggestedLots =
  2106. indexes.activeLotsByItemId.get(scannedItemId) || [];
  2107. // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected
  2108. const allLotsForItem = indexes.byItemId.get(scannedItemId) || [];
  2109. const lookupTime = performance.now() - lookupStartTime;
  2110. console.log(
  2111. ` [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${
  2112. activeSuggestedLots.length
  2113. } active lots, ${allLotsForItem.length} total lots`,
  2114. );
  2115. // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots
  2116. // This allows users to scan other lots even when all suggested lots are rejected
  2117. const scannedLot = allLotsForItem.find(
  2118. (lot: any) => lot.stockInLineId === scannedStockInLineId,
  2119. );
  2120. if (scannedLot) {
  2121. const isRejected =
  2122. scannedLot.stockOutLineStatus?.toLowerCase() === "rejected" ||
  2123. scannedLot.lotAvailability === "rejected" ||
  2124. isInventoryLotLineUnavailable(scannedLot);
  2125. if (isRejected) {
  2126. console.warn(
  2127. `⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`,
  2128. );
  2129. startTransition(() => {
  2130. setQrScanError(true);
  2131. setQrScanSuccess(false);
  2132. setQrScanErrorMsg(
  2133. `此批次(${
  2134. scannedLot.lotNo || scannedStockInLineId
  2135. })已被拒绝,无法使用。请扫描其他批次。`,
  2136. );
  2137. });
  2138. // Mark as processed to prevent re-processing
  2139. setProcessedQrCombinations((prev) => {
  2140. const newMap = new Map(prev);
  2141. if (!newMap.has(scannedItemId))
  2142. newMap.set(scannedItemId, new Set());
  2143. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  2144. return newMap;
  2145. });
  2146. return;
  2147. }
  2148. const isExpired =
  2149. String(scannedLot.lotAvailability || "").toLowerCase() ===
  2150. "expired";
  2151. if (isExpired) {
  2152. console.warn(
  2153. `⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is expired`,
  2154. );
  2155. startTransition(() => {
  2156. setQrScanError(true);
  2157. setQrScanSuccess(false);
  2158. setQrScanErrorMsg(
  2159. `此批次(${
  2160. scannedLot.lotNo || scannedStockInLineId
  2161. })已过期,无法使用。请扫描其他批次。`,
  2162. );
  2163. });
  2164. // Mark as processed to prevent re-processing the same expired QR repeatedly
  2165. setProcessedQrCombinations((prev) => {
  2166. const newMap = new Map(prev);
  2167. if (!newMap.has(scannedItemId))
  2168. newMap.set(scannedItemId, new Set());
  2169. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  2170. return newMap;
  2171. });
  2172. return;
  2173. }
  2174. }
  2175. // ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching
  2176. if (activeSuggestedLots.length === 0) {
  2177. // Check if there are any lots for this item (even if all are rejected)
  2178. if (allLotsForItem.length === 0) {
  2179. console.error("No lots found for this item");
  2180. startTransition(() => {
  2181. setQrScanError(true);
  2182. setQrScanSuccess(false);
  2183. setQrScanErrorMsg("当前订单中没有此物品的批次信息");
  2184. });
  2185. return;
  2186. }
  2187. // ✅ Allow lot switching: find a rejected lot as expected lot, or use first lot
  2188. // This allows users to switch to a new lot even when all suggested lots are rejected
  2189. console.log(
  2190. `⚠️ [QR PROCESS] No active suggested lots, but allowing lot switching.`,
  2191. );
  2192. // Find a rejected lot as expected lot (the one that was rejected)
  2193. const rejectedLot = allLotsForItem.find(
  2194. (lot: any) =>
  2195. lot.stockOutLineStatus?.toLowerCase() === "rejected" ||
  2196. lot.lotAvailability === "rejected" ||
  2197. isInventoryLotLineUnavailable(lot),
  2198. );
  2199. const expectedLot =
  2200. rejectedLot ||
  2201. pickExpectedLotForSubstitution(
  2202. allLotsForItem.filter(
  2203. (l: any) => l.lotNo != null && String(l.lotNo).trim() !== "",
  2204. ),
  2205. ) ||
  2206. allLotsForItem[0];
  2207. // Silent lot substitution; modal only if switch fails
  2208. console.log(
  2209. `⚠️ [QR PROCESS] Lot switch (no active lots), attempting substitution`,
  2210. );
  2211. setSelectedLotForQr(expectedLot);
  2212. handleLotMismatch(
  2213. expectedLot,
  2214. {
  2215. lotNo: scannedLot?.lotNo || null,
  2216. itemCode: expectedLot.itemCode,
  2217. itemName: expectedLot.itemName,
  2218. inventoryLotLineId: scannedLot?.lotId || null,
  2219. stockInLineId: scannedStockInLineId,
  2220. },
  2221. qrScanCountAtInvoke,
  2222. );
  2223. return;
  2224. }
  2225. // ✅ OPTIMIZATION: Direct Map lookup for stockInLineId match (O(1))
  2226. const matchStartTime = performance.now();
  2227. let exactMatch: any = null;
  2228. const stockInLineLots =
  2229. indexes.byStockInLineId.get(scannedStockInLineId) || [];
  2230. // Find exact match from stockInLineId index, then verify it's in active lots
  2231. for (let i = 0; i < stockInLineLots.length; i++) {
  2232. const lot = stockInLineLots[i];
  2233. if (
  2234. lot.itemId === scannedItemId &&
  2235. activeSuggestedLots.includes(lot)
  2236. ) {
  2237. exactMatch = lot;
  2238. break;
  2239. }
  2240. }
  2241. const matchTime = performance.now() - matchStartTime;
  2242. console.log(
  2243. ` [PERF] Find exact match time: ${matchTime.toFixed(2)}ms, found: ${
  2244. exactMatch ? "yes" : "no"
  2245. }`,
  2246. );
  2247. // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots
  2248. // This handles the case where Lot A is rejected and user scans Lot B
  2249. // Also handle case where scanned lot is not in allLotsForItem (scannedLot is undefined)
  2250. if (!exactMatch) {
  2251. // Scanned lot is not in active suggested lots, open confirmation modal
  2252. const expectedLot =
  2253. pickExpectedLotForSubstitution(activeSuggestedLots) ||
  2254. allLotsForItem[0];
  2255. if (expectedLot) {
  2256. // Check if scanned lot is different from expected, or if scannedLot is undefined (not in allLotsForItem)
  2257. const shouldOpenModal =
  2258. !scannedLot ||
  2259. scannedLot.stockInLineId !== expectedLot.stockInLineId;
  2260. if (shouldOpenModal) {
  2261. console.log(
  2262. `⚠️ [QR PROCESS] Lot switch (scanned lot ${
  2263. scannedLot?.lotNo || "not in data"
  2264. } not in active suggested lots)`,
  2265. );
  2266. setSelectedLotForQr(expectedLot);
  2267. handleLotMismatch(
  2268. expectedLot,
  2269. {
  2270. lotNo: scannedLot?.lotNo || null,
  2271. itemCode: expectedLot.itemCode,
  2272. itemName: expectedLot.itemName,
  2273. inventoryLotLineId: scannedLot?.lotId || null,
  2274. stockInLineId: scannedStockInLineId,
  2275. },
  2276. qrScanCountAtInvoke,
  2277. );
  2278. return;
  2279. }
  2280. }
  2281. }
  2282. if (exactMatch) {
  2283. // ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认
  2284. console.log(
  2285. `✅ Exact stockInLineId match found for lot: ${exactMatch.lotNo}`,
  2286. );
  2287. if (!exactMatch.stockOutLineId) {
  2288. console.warn(
  2289. "No stockOutLineId on exactMatch, cannot update status by QR.",
  2290. );
  2291. startTransition(() => {
  2292. setQrScanError(true);
  2293. setQrScanSuccess(false);
  2294. });
  2295. return;
  2296. }
  2297. try {
  2298. const apiStartTime = performance.now();
  2299. console.log(
  2300. ` [API CALL START] Calling updateStockOutLineStatusByQRCodeAndLotNo`,
  2301. );
  2302. console.log(
  2303. ` [API CALL] API start time: ${new Date().toISOString()}`,
  2304. );
  2305. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  2306. pickOrderLineId: exactMatch.pickOrderLineId,
  2307. inventoryLotNo: exactMatch.lotNo,
  2308. stockOutLineId: exactMatch.stockOutLineId,
  2309. itemId: exactMatch.itemId,
  2310. status: "checked",
  2311. });
  2312. const apiTime = performance.now() - apiStartTime;
  2313. console.log(
  2314. ` [API CALL END] Total API time: ${apiTime.toFixed(2)}ms (${(
  2315. apiTime / 1000
  2316. ).toFixed(3)}s)`,
  2317. );
  2318. console.log(
  2319. ` [API CALL] API end time: ${new Date().toISOString()}`,
  2320. );
  2321. if (res.code === "checked" || res.code === "SUCCESS") {
  2322. const entity = res.entity as any;
  2323. // ✅ Batch state updates using startTransition
  2324. const stateUpdateStartTime = performance.now();
  2325. startTransition(() => {
  2326. setQrScanError(false);
  2327. setQrScanSuccess(true);
  2328. setCombinedLotData((prev) =>
  2329. prev.map((lot) => {
  2330. if (
  2331. lot.stockOutLineId === exactMatch.stockOutLineId &&
  2332. lot.pickOrderLineId === exactMatch.pickOrderLineId
  2333. ) {
  2334. return {
  2335. ...lot,
  2336. stockOutLineStatus: "checked",
  2337. stockOutLineQty: entity?.qty ?? lot.stockOutLineQty,
  2338. };
  2339. }
  2340. return lot;
  2341. }),
  2342. );
  2343. setOriginalCombinedData((prev) =>
  2344. prev.map((lot) => {
  2345. if (
  2346. lot.stockOutLineId === exactMatch.stockOutLineId &&
  2347. lot.pickOrderLineId === exactMatch.pickOrderLineId
  2348. ) {
  2349. return {
  2350. ...lot,
  2351. stockOutLineStatus: "checked",
  2352. stockOutLineQty: entity?.qty ?? lot.stockOutLineQty,
  2353. };
  2354. }
  2355. return lot;
  2356. }),
  2357. );
  2358. });
  2359. const stateUpdateTime = performance.now() - stateUpdateStartTime;
  2360. console.log(
  2361. ` [PERF] State update time: ${stateUpdateTime.toFixed(2)}ms`,
  2362. );
  2363. // Mark this combination as processed
  2364. const markProcessedStartTime = performance.now();
  2365. setProcessedQrCombinations((prev) => {
  2366. const newMap = new Map(prev);
  2367. if (!newMap.has(scannedItemId)) {
  2368. newMap.set(scannedItemId, new Set());
  2369. }
  2370. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  2371. return newMap;
  2372. });
  2373. const markProcessedTime =
  2374. performance.now() - markProcessedStartTime;
  2375. console.log(
  2376. ` [PERF] Mark processed time: ${markProcessedTime.toFixed(
  2377. 2,
  2378. )}ms`,
  2379. );
  2380. const totalTime = performance.now() - totalStartTime;
  2381. console.log(
  2382. `✅ [PROCESS OUTSIDE QR END] Total time: ${totalTime.toFixed(
  2383. 2,
  2384. )}ms (${(totalTime / 1000).toFixed(3)}s)`,
  2385. );
  2386. console.log(` End time: ${new Date().toISOString()}`);
  2387. console.log(
  2388. `📊 Breakdown: parse=${parseTime.toFixed(
  2389. 2,
  2390. )}ms, validation=${validationTime.toFixed(
  2391. 2,
  2392. )}ms, duplicateCheck=${duplicateCheckTime.toFixed(
  2393. 2,
  2394. )}ms, lookup=${lookupTime.toFixed(
  2395. 2,
  2396. )}ms, match=${matchTime.toFixed(2)}ms, api=${apiTime.toFixed(
  2397. 2,
  2398. )}ms, stateUpdate=${stateUpdateTime.toFixed(
  2399. 2,
  2400. )}ms, markProcessed=${markProcessedTime.toFixed(2)}ms`,
  2401. );
  2402. console.log(
  2403. "✅ Status updated locally, no full data refresh needed",
  2404. );
  2405. } else {
  2406. console.warn("Unexpected response code from backend:", res.code);
  2407. startTransition(() => {
  2408. setQrScanError(true);
  2409. setQrScanSuccess(false);
  2410. });
  2411. }
  2412. } catch (e) {
  2413. const totalTime = performance.now() - totalStartTime;
  2414. console.error(
  2415. `❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(
  2416. 2,
  2417. )}ms`,
  2418. );
  2419. console.error(
  2420. "Error calling updateStockOutLineStatusByQRCodeAndLotNo:",
  2421. e,
  2422. );
  2423. startTransition(() => {
  2424. setQrScanError(true);
  2425. setQrScanSuccess(false);
  2426. });
  2427. }
  2428. return; // ✅ 直接返回,不需要确认表单
  2429. }
  2430. // ✅ Case 2: itemId 匹配但 stockInLineId 不匹配 - 显示确认表单
  2431. // Check if we should allow reopening (different stockInLineId)
  2432. const mismatchCheckStartTime = performance.now();
  2433. const itemProcessedSet2 = processedQrCombinations.get(scannedItemId);
  2434. if (itemProcessedSet2?.has(scannedStockInLineId)) {
  2435. const mismatchCheckTime = performance.now() - mismatchCheckStartTime;
  2436. console.log(
  2437. ` [SKIP] Already processed this exact combination (check time: ${mismatchCheckTime.toFixed(
  2438. 2,
  2439. )}ms)`,
  2440. );
  2441. return;
  2442. }
  2443. const mismatchCheckTime = performance.now() - mismatchCheckStartTime;
  2444. console.log(
  2445. ` [PERF] Mismatch check time: ${mismatchCheckTime.toFixed(2)}ms`,
  2446. );
  2447. // 取应被替换的活跃行(同物料多行时优先有建议批次的行)
  2448. const expectedLotStartTime = performance.now();
  2449. const expectedLot = pickExpectedLotForSubstitution(activeSuggestedLots);
  2450. if (!expectedLot) {
  2451. console.error("Could not determine expected lot for confirmation");
  2452. startTransition(() => {
  2453. setQrScanError(true);
  2454. setQrScanSuccess(false);
  2455. });
  2456. return;
  2457. }
  2458. const expectedLotTime = performance.now() - expectedLotStartTime;
  2459. console.log(
  2460. ` [PERF] Get expected lot time: ${expectedLotTime.toFixed(2)}ms`,
  2461. );
  2462. // ✅ 立即打开确认模态框,不等待其他操作
  2463. console.log(
  2464. `⚠️ Lot mismatch: Expected stockInLineId=${expectedLot.stockInLineId}, Scanned stockInLineId=${scannedStockInLineId}`,
  2465. );
  2466. // Set selected lot immediately (no transition delay)
  2467. const setSelectedLotStartTime = performance.now();
  2468. setSelectedLotForQr(expectedLot);
  2469. const setSelectedLotTime = performance.now() - setSelectedLotStartTime;
  2470. console.log(
  2471. ` [PERF] Set selected lot time: ${setSelectedLotTime.toFixed(2)}ms`,
  2472. );
  2473. const handleMismatchStartTime = performance.now();
  2474. handleLotMismatch(
  2475. expectedLot,
  2476. {
  2477. lotNo: null,
  2478. itemCode: expectedLot.itemCode,
  2479. itemName: expectedLot.itemName,
  2480. inventoryLotLineId: null,
  2481. stockInLineId: scannedStockInLineId,
  2482. },
  2483. qrScanCountAtInvoke,
  2484. );
  2485. const handleMismatchTime = performance.now() - handleMismatchStartTime;
  2486. console.log(
  2487. ` [PERF] Handle mismatch call time: ${handleMismatchTime.toFixed(
  2488. 2,
  2489. )}ms`,
  2490. );
  2491. const totalTime = performance.now() - totalStartTime;
  2492. console.log(
  2493. `⚠️ [PROCESS OUTSIDE QR MISMATCH] Total time before modal: ${totalTime.toFixed(
  2494. 2,
  2495. )}ms (${(totalTime / 1000).toFixed(3)}s)`,
  2496. );
  2497. console.log(` End time: ${new Date().toISOString()}`);
  2498. console.log(
  2499. `📊 Breakdown: parse=${parseTime.toFixed(
  2500. 2,
  2501. )}ms, validation=${validationTime.toFixed(
  2502. 2,
  2503. )}ms, duplicateCheck=${duplicateCheckTime.toFixed(
  2504. 2,
  2505. )}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(
  2506. 2,
  2507. )}ms, mismatchCheck=${mismatchCheckTime.toFixed(
  2508. 2,
  2509. )}ms, expectedLot=${expectedLotTime.toFixed(
  2510. 2,
  2511. )}ms, setSelectedLot=${setSelectedLotTime.toFixed(
  2512. 2,
  2513. )}ms, handleMismatch=${handleMismatchTime.toFixed(2)}ms`,
  2514. );
  2515. } catch (error) {
  2516. const totalTime = performance.now() - totalStartTime;
  2517. console.error(
  2518. `❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`,
  2519. );
  2520. console.error("Error during QR code processing:", error);
  2521. startTransition(() => {
  2522. setQrScanError(true);
  2523. setQrScanSuccess(false);
  2524. });
  2525. return;
  2526. }
  2527. },
  2528. [
  2529. lotDataIndexes,
  2530. handleLotMismatch,
  2531. processedQrCombinations,
  2532. combinedLotData,
  2533. fetchStockInLineInfoCached,
  2534. ],
  2535. );
  2536. // Store processOutsideQrCode in ref for immediate access (update on every render)
  2537. processOutsideQrCodeRef.current = processOutsideQrCode;
  2538. useEffect(() => {
  2539. // Skip if scanner is not active or no data available
  2540. if (
  2541. !isManualScanning ||
  2542. qrValues.length === 0 ||
  2543. combinedLotData.length === 0 ||
  2544. isRefreshingData
  2545. ) {
  2546. return;
  2547. }
  2548. const qrValuesChangeStartTime = performance.now();
  2549. console.log(
  2550. ` [QR VALUES EFFECT] Triggered at: ${new Date().toISOString()}`,
  2551. );
  2552. console.log(` [QR VALUES EFFECT] qrValues.length: ${qrValues.length}`);
  2553. console.log(` [QR VALUES EFFECT] qrValues:`, qrValues);
  2554. const latestQr = qrValues[qrValues.length - 1];
  2555. console.log(` [QR VALUES EFFECT] Latest QR: ${latestQr}`);
  2556. console.log(
  2557. ` [QR VALUES EFFECT] Latest QR detected at: ${new Date().toISOString()}`,
  2558. );
  2559. // ✅ FIXED: Handle test shortcut {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId
  2560. // Support both formats: {2fitest (2 t's) and {2fittest (3 t's)
  2561. if (
  2562. (latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) &&
  2563. latestQr.endsWith("}")
  2564. ) {
  2565. // Extract content: remove "{2fitest" or "{2fittest" and "}"
  2566. let content = "";
  2567. if (latestQr.startsWith("{2fittest")) {
  2568. content = latestQr.substring(9, latestQr.length - 1); // Remove "{2fittest" and "}"
  2569. } else if (latestQr.startsWith("{2fitest")) {
  2570. content = latestQr.substring(8, latestQr.length - 1); // Remove "{2fitest" and "}"
  2571. }
  2572. const parts = content.split(",");
  2573. if (parts.length === 2) {
  2574. const itemId = parseInt(parts[0].trim(), 10);
  2575. const stockInLineId = parseInt(parts[1].trim(), 10);
  2576. if (!isNaN(itemId) && !isNaN(stockInLineId)) {
  2577. console.log(
  2578. `%c TEST QR: Detected ${latestQr.substring(
  2579. 0,
  2580. 9,
  2581. )}... - Simulating QR input (itemId=${itemId}, stockInLineId=${stockInLineId})`,
  2582. "color: purple; font-weight: bold",
  2583. );
  2584. // ✅ Simulate QR code JSON format
  2585. const simulatedQr = JSON.stringify({
  2586. itemId: itemId,
  2587. stockInLineId: stockInLineId,
  2588. });
  2589. console.log(` [TEST QR] Simulated QR content: ${simulatedQr}`);
  2590. console.log(` [TEST QR] Start time: ${new Date().toISOString()}`);
  2591. const testStartTime = performance.now();
  2592. // ✅ Mark as processed FIRST to avoid duplicate processing
  2593. lastProcessedQrRef.current = latestQr;
  2594. processedQrCodesRef.current.add(latestQr);
  2595. if (processedQrCodesRef.current.size > 100) {
  2596. const firstValue = processedQrCodesRef.current
  2597. .values()
  2598. .next().value;
  2599. if (firstValue !== undefined) {
  2600. processedQrCodesRef.current.delete(firstValue);
  2601. }
  2602. }
  2603. setLastProcessedQr(latestQr);
  2604. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  2605. // ✅ Process immediately (bypass QR scanner delay)
  2606. if (processOutsideQrCodeRef.current) {
  2607. processOutsideQrCodeRef
  2608. .current(simulatedQr, qrValues.length)
  2609. .then(() => {
  2610. const testTime = performance.now() - testStartTime;
  2611. console.log(
  2612. ` [TEST QR] Total processing time: ${testTime.toFixed(
  2613. 2,
  2614. )}ms (${(testTime / 1000).toFixed(3)}s)`,
  2615. );
  2616. console.log(
  2617. ` [TEST QR] End time: ${new Date().toISOString()}`,
  2618. );
  2619. })
  2620. .catch((error) => {
  2621. const testTime = performance.now() - testStartTime;
  2622. console.error(
  2623. `❌ [TEST QR] Error after ${testTime.toFixed(2)}ms:`,
  2624. error,
  2625. );
  2626. });
  2627. }
  2628. // Reset scan
  2629. if (resetScanRef.current) {
  2630. resetScanRef.current();
  2631. }
  2632. const qrValuesChangeTime =
  2633. performance.now() - qrValuesChangeStartTime;
  2634. console.log(
  2635. ` [QR VALUES EFFECT] Test QR handling time: ${qrValuesChangeTime.toFixed(
  2636. 2,
  2637. )}ms`,
  2638. );
  2639. return; // ✅ IMPORTANT: Return early to prevent normal processing
  2640. } else {
  2641. console.warn(
  2642. ` [TEST QR] Invalid itemId or stockInLineId: itemId=${parts[0]}, stockInLineId=${parts[1]}`,
  2643. );
  2644. }
  2645. } else {
  2646. console.warn(
  2647. ` [TEST QR] Invalid format. Expected {2fitestx,y} or {2fittestx,y}, got: ${latestQr}`,
  2648. );
  2649. }
  2650. }
  2651. // 批次确认弹窗:须第二次扫码选择沿用建议批次或切换(不再自动确认)
  2652. if (lotConfirmationOpen) {
  2653. if (isConfirmingLot) {
  2654. return;
  2655. }
  2656. if (lotConfirmSkipNextScanRef.current) {
  2657. lotConfirmSkipNextScanRef.current = false;
  2658. lotConfirmLastQrRef.current = latestQr || "";
  2659. return;
  2660. }
  2661. if (!latestQr) {
  2662. return;
  2663. }
  2664. // Prevent auto-accept from buffered duplicate right after modal opens,
  2665. // but allow intentional second scan of the same QR after debounce window.
  2666. const sameQr = latestQr === lotConfirmLastQrRef.current;
  2667. const justOpened =
  2668. lotConfirmOpenedAtRef.current > 0 &&
  2669. Date.now() - lotConfirmOpenedAtRef.current < 800;
  2670. if (sameQr && justOpened) {
  2671. return;
  2672. }
  2673. lotConfirmLastQrRef.current = latestQr;
  2674. void (async () => {
  2675. try {
  2676. const handled = await handleLotConfirmationByRescan(latestQr);
  2677. if (handled && resetScanRef.current) {
  2678. resetScanRef.current();
  2679. }
  2680. } catch (e) {
  2681. console.error("Lot confirmation rescan failed:", e);
  2682. }
  2683. })();
  2684. return;
  2685. }
  2686. // Skip processing if manual confirmation modal is open
  2687. if (manualLotConfirmationOpen) {
  2688. // Check if this is a different QR code than what triggered the modal
  2689. const modalTriggerQr = lastProcessedQrRef.current;
  2690. if (latestQr === modalTriggerQr) {
  2691. console.log(` [QR PROCESS] Skipping - manual modal open for same QR`);
  2692. return;
  2693. }
  2694. // If it's a different QR, allow processing
  2695. console.log(
  2696. ` [QR PROCESS] Different QR detected while manual modal open, allowing processing`,
  2697. );
  2698. }
  2699. const qrDetectionStartTime = performance.now();
  2700. console.log(
  2701. ` [QR DETECTION] Latest QR detected: ${latestQr?.substring(0, 50)}...`,
  2702. );
  2703. console.log(` [QR DETECTION] Detection time: ${new Date().toISOString()}`);
  2704. console.log(
  2705. ` [QR DETECTION] Time since QR scanner set value: ${(
  2706. qrDetectionStartTime - qrValuesChangeStartTime
  2707. ).toFixed(2)}ms`,
  2708. );
  2709. // Skip if already processed (use refs to avoid dependency issues and delays)
  2710. const checkProcessedStartTime = performance.now();
  2711. if (
  2712. processedQrCodesRef.current.has(latestQr) ||
  2713. lastProcessedQrRef.current === latestQr
  2714. ) {
  2715. const checkTime = performance.now() - checkProcessedStartTime;
  2716. console.log(
  2717. ` [QR PROCESS] Already processed check time: ${checkTime.toFixed(
  2718. 2,
  2719. )}ms`,
  2720. );
  2721. return;
  2722. }
  2723. const checkTime = performance.now() - checkProcessedStartTime;
  2724. console.log(
  2725. ` [QR PROCESS] Not processed check time: ${checkTime.toFixed(2)}ms`,
  2726. );
  2727. // Handle special shortcut
  2728. if (latestQr === "{2fic}") {
  2729. console.log(
  2730. " Detected {2fic} shortcut - opening manual lot confirmation form",
  2731. );
  2732. setManualLotConfirmationOpen(true);
  2733. if (resetScanRef.current) {
  2734. resetScanRef.current();
  2735. }
  2736. lastProcessedQrRef.current = latestQr;
  2737. processedQrCodesRef.current.add(latestQr);
  2738. if (processedQrCodesRef.current.size > 100) {
  2739. const firstValue = processedQrCodesRef.current.values().next().value;
  2740. if (firstValue !== undefined) {
  2741. processedQrCodesRef.current.delete(firstValue);
  2742. }
  2743. }
  2744. setLastProcessedQr(latestQr);
  2745. setProcessedQrCodes((prev) => {
  2746. const newSet = new Set(prev);
  2747. newSet.add(latestQr);
  2748. if (newSet.size > 100) {
  2749. const firstValue = newSet.values().next().value;
  2750. if (firstValue !== undefined) {
  2751. newSet.delete(firstValue);
  2752. }
  2753. }
  2754. return newSet;
  2755. });
  2756. return;
  2757. }
  2758. // Process new QR code immediately (background mode - no modal)
  2759. // Check against refs to avoid state update delays
  2760. if (latestQr && latestQr !== lastProcessedQrRef.current) {
  2761. const processingStartTime = performance.now();
  2762. console.log(
  2763. ` [QR PROCESS] Starting processing at: ${new Date().toISOString()}`,
  2764. );
  2765. console.log(
  2766. ` [QR PROCESS] Time since detection: ${(
  2767. processingStartTime - qrDetectionStartTime
  2768. ).toFixed(2)}ms`,
  2769. );
  2770. // ✅ Process immediately for better responsiveness
  2771. // Clear any pending debounced processing
  2772. if (qrProcessingTimeoutRef.current) {
  2773. clearTimeout(qrProcessingTimeoutRef.current);
  2774. qrProcessingTimeoutRef.current = null;
  2775. }
  2776. // Log immediately (console.log is synchronous)
  2777. console.log(
  2778. ` [QR PROCESS] Processing new QR code with enhanced validation: ${latestQr}`,
  2779. );
  2780. // Update refs immediately (no state update delay) - do this FIRST
  2781. const refUpdateStartTime = performance.now();
  2782. lastProcessedQrRef.current = latestQr;
  2783. processedQrCodesRef.current.add(latestQr);
  2784. if (processedQrCodesRef.current.size > 100) {
  2785. const firstValue = processedQrCodesRef.current.values().next().value;
  2786. if (firstValue !== undefined) {
  2787. processedQrCodesRef.current.delete(firstValue);
  2788. }
  2789. }
  2790. const refUpdateTime = performance.now() - refUpdateStartTime;
  2791. console.log(
  2792. ` [QR PROCESS] Ref update time: ${refUpdateTime.toFixed(2)}ms`,
  2793. );
  2794. // Process immediately in background - no modal/form needed, no delays
  2795. // Use ref to avoid dependency issues
  2796. const processCallStartTime = performance.now();
  2797. if (processOutsideQrCodeRef.current) {
  2798. processOutsideQrCodeRef
  2799. .current(latestQr, qrValues.length)
  2800. .then(() => {
  2801. const processCallTime = performance.now() - processCallStartTime;
  2802. const totalProcessingTime = performance.now() - processingStartTime;
  2803. console.log(
  2804. ` [QR PROCESS] processOutsideQrCode call time: ${processCallTime.toFixed(
  2805. 2,
  2806. )}ms`,
  2807. );
  2808. console.log(
  2809. ` [QR PROCESS] Total processing time: ${totalProcessingTime.toFixed(
  2810. 2,
  2811. )}ms (${(totalProcessingTime / 1000).toFixed(3)}s)`,
  2812. );
  2813. })
  2814. .catch((error) => {
  2815. const processCallTime = performance.now() - processCallStartTime;
  2816. const totalProcessingTime = performance.now() - processingStartTime;
  2817. console.error(
  2818. `❌ [QR PROCESS] processOutsideQrCode error after ${processCallTime.toFixed(
  2819. 2,
  2820. )}ms:`,
  2821. error,
  2822. );
  2823. console.error(
  2824. `❌ [QR PROCESS] Total processing time before error: ${totalProcessingTime.toFixed(
  2825. 2,
  2826. )}ms`,
  2827. );
  2828. });
  2829. }
  2830. // Update state for UI (but don't block on it)
  2831. const stateUpdateStartTime = performance.now();
  2832. setLastProcessedQr(latestQr);
  2833. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  2834. const stateUpdateTime = performance.now() - stateUpdateStartTime;
  2835. console.log(
  2836. ` [QR PROCESS] State update time: ${stateUpdateTime.toFixed(2)}ms`,
  2837. );
  2838. const detectionTime = performance.now() - qrDetectionStartTime;
  2839. const totalEffectTime = performance.now() - qrValuesChangeStartTime;
  2840. console.log(
  2841. ` [QR DETECTION] Total detection time: ${detectionTime.toFixed(2)}ms`,
  2842. );
  2843. console.log(
  2844. ` [QR VALUES EFFECT] Total effect time: ${totalEffectTime.toFixed(
  2845. 2,
  2846. )}ms`,
  2847. );
  2848. }
  2849. return () => {
  2850. if (qrProcessingTimeoutRef.current) {
  2851. clearTimeout(qrProcessingTimeoutRef.current);
  2852. qrProcessingTimeoutRef.current = null;
  2853. }
  2854. };
  2855. }, [
  2856. qrValues,
  2857. isManualScanning,
  2858. isRefreshingData,
  2859. combinedLotData.length,
  2860. lotConfirmationOpen,
  2861. manualLotConfirmationOpen,
  2862. handleLotConfirmationByRescan,
  2863. isConfirmingLot,
  2864. ]);
  2865. const renderCountRef = useRef(0);
  2866. const renderStartTimeRef = useRef<number | null>(null);
  2867. // Track render performance
  2868. useEffect(() => {
  2869. renderCountRef.current++;
  2870. const now = performance.now();
  2871. if (renderStartTimeRef.current !== null) {
  2872. const renderTime = now - renderStartTimeRef.current;
  2873. if (renderTime > 100) {
  2874. // Only log slow renders (>100ms)
  2875. console.log(
  2876. ` [PERF] Render #${renderCountRef.current} took ${renderTime.toFixed(
  2877. 2,
  2878. )}ms, combinedLotData length: ${combinedLotData.length}`,
  2879. );
  2880. }
  2881. renderStartTimeRef.current = null;
  2882. }
  2883. // Track when lotConfirmationOpen changes
  2884. if (lotConfirmationOpen) {
  2885. renderStartTimeRef.current = performance.now();
  2886. console.log(` [PERF] Render triggered by lotConfirmationOpen=true`);
  2887. }
  2888. }, [combinedLotData.length, lotConfirmationOpen]);
  2889. // Auto-start scanner only once on mount
  2890. const scannerInitializedRef = useRef(false);
  2891. useEffect(() => {
  2892. if (session && currentUserId && !initializationRef.current) {
  2893. console.log(" Session loaded, initializing pick order...");
  2894. initializationRef.current = true;
  2895. // Only fetch existing data, no auto-assignment
  2896. fetchAllCombinedLotData();
  2897. }
  2898. }, [session, currentUserId, fetchAllCombinedLotData]);
  2899. // Separate effect for auto-starting scanner (only once, prevents multiple resets)
  2900. useEffect(() => {
  2901. if (session && currentUserId && !scannerInitializedRef.current) {
  2902. scannerInitializedRef.current = true;
  2903. // ✅ Auto-start scanner on mount for tablet use (background mode - no modal)
  2904. console.log("✅ Auto-starting QR scanner in background mode");
  2905. setIsManualScanning(true);
  2906. startScan();
  2907. }
  2908. }, [session, currentUserId, startScan]);
  2909. // Add event listener for manual assignment
  2910. useEffect(() => {
  2911. const handlePickOrderAssigned = () => {
  2912. console.log("🔄 Pick order assigned event received, refreshing data...");
  2913. fetchAllCombinedLotData();
  2914. };
  2915. window.addEventListener("pickOrderAssigned", handlePickOrderAssigned);
  2916. return () => {
  2917. window.removeEventListener("pickOrderAssigned", handlePickOrderAssigned);
  2918. };
  2919. }, [fetchAllCombinedLotData]);
  2920. const handleManualInputSubmit = useCallback(() => {
  2921. if (qrScanInput.trim() !== "") {
  2922. handleQrCodeSubmit(qrScanInput.trim());
  2923. }
  2924. }, [qrScanInput, handleQrCodeSubmit]);
  2925. // Handle QR code submission from modal (internal scanning)
  2926. const handleQrCodeSubmitFromModal = useCallback(
  2927. async (lotNo: string) => {
  2928. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  2929. console.log(` QR Code verified for lot: ${lotNo}`);
  2930. const requiredQty = selectedLotForQr.requiredQty;
  2931. const lotId = selectedLotForQr.lotId;
  2932. // Create stock out line
  2933. try {
  2934. const stockOutLineUpdate = await updateStockOutLineStatus({
  2935. id: selectedLotForQr.stockOutLineId,
  2936. status: "checked",
  2937. qty: selectedLotForQr.stockOutLineQty || 0,
  2938. });
  2939. console.log("Stock out line updated successfully!");
  2940. setQrScanSuccess(true);
  2941. setQrScanError(false);
  2942. // Clear selected lot (scanner stays active)
  2943. setSelectedLotForQr(null);
  2944. // Set pick quantity
  2945. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  2946. setTimeout(() => {
  2947. setPickQtyData((prev) => ({
  2948. ...prev,
  2949. [lotKey]: requiredQty,
  2950. }));
  2951. console.log(
  2952. ` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`,
  2953. );
  2954. }, 500);
  2955. } catch (error) {
  2956. console.error("Error creating stock out line:", error);
  2957. }
  2958. }
  2959. },
  2960. [selectedLotForQr],
  2961. );
  2962. const handlePickQtyChange = useCallback(
  2963. (lotKey: string, value: number | string) => {
  2964. if (value === "" || value === null || value === undefined) {
  2965. setPickQtyData((prev) => ({
  2966. ...prev,
  2967. [lotKey]: 0,
  2968. }));
  2969. return;
  2970. }
  2971. const numericValue =
  2972. typeof value === "string" ? parseFloat(value) : value;
  2973. if (isNaN(numericValue)) {
  2974. setPickQtyData((prev) => ({
  2975. ...prev,
  2976. [lotKey]: 0,
  2977. }));
  2978. return;
  2979. }
  2980. setPickQtyData((prev) => ({
  2981. ...prev,
  2982. [lotKey]: numericValue,
  2983. }));
  2984. },
  2985. [],
  2986. );
  2987. const [autoAssignStatus, setAutoAssignStatus] = useState<
  2988. "idle" | "checking" | "assigned" | "no_orders"
  2989. >("idle");
  2990. const [autoAssignMessage, setAutoAssignMessage] = useState<string>("");
  2991. const [completionStatus, setCompletionStatus] =
  2992. useState<PickOrderCompletionResponse | null>(null);
  2993. const checkAndAutoAssignNext = useCallback(async () => {
  2994. if (!currentUserId) return;
  2995. try {
  2996. const completionResponse = await checkPickOrderCompletion(currentUserId);
  2997. if (
  2998. completionResponse.code === "SUCCESS" &&
  2999. completionResponse.entity?.hasCompletedOrders
  3000. ) {
  3001. console.log("Found completed pick orders, auto-assigning next...");
  3002. // 移除前端的自动分配逻辑,因为后端已经处理了
  3003. // await handleAutoAssignAndRelease(); // 删除这个函数
  3004. }
  3005. } catch (error) {
  3006. console.error("Error checking pick order completion:", error);
  3007. }
  3008. }, [currentUserId]);
  3009. const resolveSingleSubmitQty = useCallback(
  3010. (lot: any) => {
  3011. const required = Number(
  3012. lot.requiredQty || lot.pickOrderLineRequiredQty || 0,
  3013. );
  3014. const solId = Number(lot.stockOutLineId) || 0;
  3015. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  3016. const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined;
  3017. if (issuePicked !== undefined && !Number.isNaN(Number(issuePicked))) {
  3018. return Number(issuePicked);
  3019. }
  3020. const fromPick = pickQtyData[lotKey];
  3021. if (
  3022. fromPick !== undefined &&
  3023. fromPick !== null &&
  3024. !Number.isNaN(Number(fromPick))
  3025. ) {
  3026. return Number(fromPick);
  3027. }
  3028. if (lot.noLot === true) {
  3029. return 0;
  3030. }
  3031. if (isInventoryLotLineUnavailable(lot)) {
  3032. return 0;
  3033. }
  3034. if (isLotAvailabilityExpired(lot)) {
  3035. return 0;
  3036. }
  3037. return required;
  3038. },
  3039. [issuePickedQtyBySolId, pickQtyData],
  3040. );
  3041. // Handle reject lot
  3042. // Handle pick execution form
  3043. const handlePickExecutionForm = useCallback((lot: any) => {
  3044. console.log("=== Pick Execution Form ===");
  3045. console.log("Lot data:", lot);
  3046. if (!lot) {
  3047. console.warn("No lot data provided for pick execution form");
  3048. return;
  3049. }
  3050. console.log("Opening pick execution form for lot:", lot.lotNo);
  3051. setSelectedLotForExecutionForm(lot);
  3052. setPickExecutionFormOpen(true);
  3053. console.log("Pick execution form opened for lot ID:", lot.lotId);
  3054. }, []);
  3055. const handlePickExecutionFormSubmit = useCallback(
  3056. async (data: any) => {
  3057. try {
  3058. console.log("Pick execution form submitted:", data);
  3059. const issueData = {
  3060. ...data,
  3061. type: "Do", // Delivery Order Record 类型
  3062. pickerName: session?.user?.name || "",
  3063. };
  3064. const result = await recordPickExecutionIssue(issueData);
  3065. console.log("Pick execution issue recorded:", result);
  3066. if (result && result.code === "SUCCESS") {
  3067. console.log(" Pick execution issue recorded successfully");
  3068. // 关键:issue form 只记录问题,不会更新 SOL.qty
  3069. // 但 batch submit 需要知道“实际拣到多少”,否则会按 requiredQty 补拣到满
  3070. const solId = Number(
  3071. issueData.stockOutLineId || issueData.stockOutLineId === 0
  3072. ? issueData.stockOutLineId
  3073. : data?.stockOutLineId,
  3074. );
  3075. if (solId > 0) {
  3076. const picked = Number(issueData.actualPickQty || 0);
  3077. setIssuePickedQtyBySolId((prev) => {
  3078. const next = { ...prev, [solId]: picked };
  3079. const doId = fgPickOrders[0]?.doPickOrderId;
  3080. if (doId) saveIssuePickedMap(doId, next);
  3081. return next;
  3082. });
  3083. setCombinedLotData((prev) =>
  3084. prev.map((lot) => {
  3085. if (Number(lot.stockOutLineId) === solId) {
  3086. return {
  3087. ...lot,
  3088. actualPickQty: picked,
  3089. stockOutLineQty: picked,
  3090. };
  3091. }
  3092. return lot;
  3093. }),
  3094. );
  3095. }
  3096. } else {
  3097. console.error(" Failed to record pick execution issue:", result);
  3098. }
  3099. setPickExecutionFormOpen(false);
  3100. setSelectedLotForExecutionForm(null);
  3101. setQrScanError(false);
  3102. setQrScanSuccess(false);
  3103. setQrScanInput("");
  3104. // ✅ Keep scanner active after form submission - don't stop scanning
  3105. // Only clear processed QR codes for the specific lot, not all
  3106. // setIsManualScanning(false); // Removed - keep scanner active
  3107. // stopScan(); // Removed - keep scanner active
  3108. // resetScan(); // Removed - keep scanner active
  3109. // Don't clear all processed codes - only clear for this specific lot if needed
  3110. await fetchAllCombinedLotData();
  3111. } catch (error) {
  3112. console.error("Error submitting pick execution form:", error);
  3113. }
  3114. },
  3115. [fetchAllCombinedLotData, session, fgPickOrders],
  3116. );
  3117. // Calculate remaining required quantity
  3118. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  3119. const requiredQty = lot.requiredQty || 0;
  3120. const stockOutLineQty = lot.stockOutLineQty || 0;
  3121. return Math.max(0, requiredQty - stockOutLineQty);
  3122. }, []);
  3123. // Search criteria
  3124. const searchCriteria: Criterion<any>[] = [
  3125. {
  3126. label: t("Pick Order Code"),
  3127. paramName: "pickOrderCode",
  3128. type: "text",
  3129. },
  3130. {
  3131. label: t("Item Code"),
  3132. paramName: "itemCode",
  3133. type: "text",
  3134. },
  3135. {
  3136. label: t("Item Name"),
  3137. paramName: "itemName",
  3138. type: "text",
  3139. },
  3140. {
  3141. label: t("Lot No"),
  3142. paramName: "lotNo",
  3143. type: "text",
  3144. },
  3145. ];
  3146. const handleSearch = useCallback(
  3147. (query: Record<string, any>) => {
  3148. setSearchQuery({ ...query });
  3149. console.log("Search query:", query);
  3150. if (!originalCombinedData) return;
  3151. const filtered = originalCombinedData.filter((lot: any) => {
  3152. const pickOrderCodeMatch =
  3153. !query.pickOrderCode ||
  3154. lot.pickOrderCode
  3155. ?.toLowerCase()
  3156. .includes((query.pickOrderCode || "").toLowerCase());
  3157. const itemCodeMatch =
  3158. !query.itemCode ||
  3159. lot.itemCode
  3160. ?.toLowerCase()
  3161. .includes((query.itemCode || "").toLowerCase());
  3162. const itemNameMatch =
  3163. !query.itemName ||
  3164. lot.itemName
  3165. ?.toLowerCase()
  3166. .includes((query.itemName || "").toLowerCase());
  3167. const lotNoMatch =
  3168. !query.lotNo ||
  3169. lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
  3170. return (
  3171. pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch
  3172. );
  3173. });
  3174. setCombinedLotData(filtered);
  3175. console.log("Filtered lots count:", filtered.length);
  3176. },
  3177. [originalCombinedData],
  3178. );
  3179. const handleReset = useCallback(() => {
  3180. setSearchQuery({});
  3181. if (originalCombinedData) {
  3182. setCombinedLotData(originalCombinedData);
  3183. }
  3184. }, [originalCombinedData]);
  3185. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  3186. setPaginationController((prev) => ({
  3187. ...prev,
  3188. pageNum: newPage,
  3189. }));
  3190. }, []);
  3191. const handlePageSizeChange = useCallback(
  3192. (event: React.ChangeEvent<HTMLInputElement>) => {
  3193. const newPageSize = parseInt(event.target.value, 10);
  3194. setPaginationController({
  3195. pageNum: 0,
  3196. pageSize: newPageSize === -1 ? -1 : newPageSize,
  3197. });
  3198. },
  3199. [],
  3200. );
  3201. // Pagination data with sorting by routerIndex
  3202. // Remove the sorting logic and just do pagination
  3203. // ✅ Memoize paginatedData to prevent re-renders when modal opens
  3204. const paginatedData = useMemo(() => {
  3205. if (paginationController.pageSize === -1) {
  3206. return combinedLotData; // Show all items
  3207. }
  3208. const startIndex =
  3209. paginationController.pageNum * paginationController.pageSize;
  3210. const endIndex = startIndex + paginationController.pageSize;
  3211. return combinedLotData.slice(startIndex, endIndex); // No sorting needed
  3212. }, [
  3213. combinedLotData,
  3214. paginationController.pageNum,
  3215. paginationController.pageSize,
  3216. ]);
  3217. const allItemsReady = useMemo(() => {
  3218. if (combinedLotData.length === 0) return false;
  3219. return combinedLotData.every((lot: any) => {
  3220. const status = lot.stockOutLineStatus?.toLowerCase();
  3221. const isRejected =
  3222. status === "rejected" || lot.lotAvailability === "rejected";
  3223. const isCompleted =
  3224. status === "completed" ||
  3225. status === "partially_completed" ||
  3226. status === "partially_complete";
  3227. const isChecked = status === "checked";
  3228. const isPending = status === "pending";
  3229. // ✅ FIXED: 无库存(noLot)行:pending 状态也应该被视为 ready(可以提交)
  3230. // ✅ 過期批號(未換批):與 noLot 相同,視為可收尾
  3231. if (
  3232. lot.noLot === true ||
  3233. isLotAvailabilityExpired(lot) ||
  3234. isInventoryLotLineUnavailable(lot)
  3235. ) {
  3236. return isChecked || isCompleted || isRejected || isPending;
  3237. }
  3238. // 正常 lot:必须已扫描/提交或者被拒收
  3239. return isChecked || isCompleted || isRejected;
  3240. });
  3241. }, [combinedLotData]);
  3242. const handleSubmitPickQtyWithQty = useCallback(
  3243. async (
  3244. lot: any,
  3245. submitQty: number,
  3246. source: "justComplete" | "singleSubmit",
  3247. ) => {
  3248. if (!lot.stockOutLineId) {
  3249. console.error("No stock out line found for this lot");
  3250. return;
  3251. }
  3252. const solId = Number(lot.stockOutLineId);
  3253. if (solId > 0 && actionBusyBySolId[solId]) {
  3254. console.warn("Action already in progress for stockOutLineId:", solId);
  3255. return;
  3256. }
  3257. try {
  3258. if (solId > 0)
  3259. setActionBusyBySolId((prev) => ({ ...prev, [solId]: true }));
  3260. const targetUnavailable = isInventoryLotLineUnavailable(lot);
  3261. const effectiveSubmitQty =
  3262. targetUnavailable && submitQty > 0 ? 0 : submitQty;
  3263. // Just Complete: mark checked only, real posting happens in batch submit
  3264. if (effectiveSubmitQty === 0 && source === "justComplete") {
  3265. console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
  3266. console.log(`Lot: ${lot.lotNo}`);
  3267. console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
  3268. console.log(`Setting status to 'checked' with qty: 0`);
  3269. const updateResult = await updateStockOutLineStatus({
  3270. id: lot.stockOutLineId,
  3271. status: "checked",
  3272. qty: 0,
  3273. });
  3274. console.log("Update result:", updateResult);
  3275. const r: any = updateResult as any;
  3276. const updateOk =
  3277. r?.code === "SUCCESS" ||
  3278. r?.type === "completed" ||
  3279. typeof r?.id === "number" ||
  3280. typeof r?.entity?.id === "number" ||
  3281. (r?.message && r.message.includes("successfully"));
  3282. if (!updateResult || !updateOk) {
  3283. console.error(
  3284. "Failed to update stock out line status:",
  3285. updateResult,
  3286. );
  3287. throw new Error("Failed to update stock out line status");
  3288. }
  3289. applyLocalStockOutLineUpdate(
  3290. Number(lot.stockOutLineId),
  3291. "checked",
  3292. Number(lot.actualPickQty || 0),
  3293. );
  3294. void fetchAllCombinedLotData();
  3295. console.log(
  3296. "Just Complete marked as checked successfully (waiting for batch submit).",
  3297. );
  3298. setTimeout(() => {
  3299. checkAndAutoAssignNext();
  3300. }, 1000);
  3301. return;
  3302. }
  3303. if (effectiveSubmitQty === 0 && source === "singleSubmit") {
  3304. console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
  3305. console.log(`Lot: ${lot.lotNo}`);
  3306. console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
  3307. console.log(`Setting status to 'checked' with qty: 0`);
  3308. const updateResult = await updateStockOutLineStatus({
  3309. id: lot.stockOutLineId,
  3310. status: "checked",
  3311. qty: 0,
  3312. });
  3313. console.log("Update result:", updateResult);
  3314. const r: any = updateResult as any;
  3315. const updateOk =
  3316. r?.code === "SUCCESS" ||
  3317. r?.type === "completed" ||
  3318. typeof r?.id === "number" ||
  3319. typeof r?.entity?.id === "number" ||
  3320. (r?.message && r.message.includes("successfully"));
  3321. if (!updateResult || !updateOk) {
  3322. console.error(
  3323. "Failed to update stock out line status:",
  3324. updateResult,
  3325. );
  3326. throw new Error("Failed to update stock out line status");
  3327. }
  3328. applyLocalStockOutLineUpdate(
  3329. Number(lot.stockOutLineId),
  3330. "checked",
  3331. Number(lot.actualPickQty || 0),
  3332. );
  3333. void fetchAllCombinedLotData();
  3334. console.log(
  3335. "Just Complete marked as checked successfully (waiting for batch submit).",
  3336. );
  3337. setTimeout(() => {
  3338. checkAndAutoAssignNext();
  3339. }, 1000);
  3340. return;
  3341. }
  3342. // FIXED: Calculate cumulative quantity correctly
  3343. const currentActualPickQty = lot.actualPickQty || 0;
  3344. const cumulativeQty = currentActualPickQty + effectiveSubmitQty;
  3345. // FIXED: Determine status based on cumulative quantity vs required quantity
  3346. let newStatus = "partially_completed";
  3347. if (cumulativeQty >= lot.requiredQty) {
  3348. newStatus = "completed";
  3349. } else if (cumulativeQty > 0) {
  3350. newStatus = "partially_completed";
  3351. } else {
  3352. newStatus = "checked"; // QR scanned but no quantity submitted yet
  3353. }
  3354. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  3355. console.log(`Lot: ${lot.lotNo}`);
  3356. console.log(`Required Qty: ${lot.requiredQty}`);
  3357. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  3358. console.log(`New Submitted Qty: ${effectiveSubmitQty}`);
  3359. console.log(`Cumulative Qty: ${cumulativeQty}`);
  3360. console.log(`New Status: ${newStatus}`);
  3361. console.log(`=====================================`);
  3362. await updateStockOutLineStatus({
  3363. id: lot.stockOutLineId,
  3364. status: newStatus,
  3365. // 后端 updateStatus 的 qty 是“增量 delta”,不能传 cumulativeQty(否则会重复累加导致 out/hold 大幅偏移)
  3366. qty: effectiveSubmitQty,
  3367. });
  3368. applyLocalStockOutLineUpdate(
  3369. Number(lot.stockOutLineId),
  3370. newStatus,
  3371. cumulativeQty,
  3372. );
  3373. // 注意:库存过账(hold->out)与 ledger 由后端 updateStatus 内部统一处理;
  3374. // 前端不再额外调用 updateInventoryLotLineQuantities(operation='pick'),避免 double posting。
  3375. // Check if pick order is completed when lot status becomes 'completed'
  3376. if (newStatus === "completed" && lot.pickOrderConsoCode) {
  3377. console.log(
  3378. ` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`,
  3379. );
  3380. try {
  3381. const completionResponse =
  3382. await checkAndCompletePickOrderByConsoCode(
  3383. lot.pickOrderConsoCode,
  3384. );
  3385. console.log(
  3386. ` Pick order completion check result:`,
  3387. completionResponse,
  3388. );
  3389. if (completionResponse.code === "SUCCESS") {
  3390. console.log(
  3391. ` Pick order ${lot.pickOrderConsoCode} completed successfully!`,
  3392. );
  3393. } else if (completionResponse.message === "not completed") {
  3394. console.log(
  3395. `Pick order not completed yet, more lines remaining`,
  3396. );
  3397. } else {
  3398. console.error(
  3399. ` Error checking completion: ${completionResponse.message}`,
  3400. );
  3401. }
  3402. } catch (error) {
  3403. console.error("Error checking pick order completion:", error);
  3404. }
  3405. }
  3406. void fetchAllCombinedLotData();
  3407. console.log("Pick quantity submitted successfully!");
  3408. setTimeout(() => {
  3409. checkAndAutoAssignNext();
  3410. }, 1000);
  3411. } catch (error) {
  3412. console.error("Error submitting pick quantity:", error);
  3413. } finally {
  3414. if (solId > 0)
  3415. setActionBusyBySolId((prev) => ({ ...prev, [solId]: false }));
  3416. }
  3417. },
  3418. [
  3419. fetchAllCombinedLotData,
  3420. checkAndAutoAssignNext,
  3421. actionBusyBySolId,
  3422. applyLocalStockOutLineUpdate,
  3423. ],
  3424. );
  3425. const handleSkip = useCallback(
  3426. async (lot: any) => {
  3427. try {
  3428. console.log(
  3429. "Just Complete clicked, mark checked with 0 qty for lot:",
  3430. lot.lotNo,
  3431. );
  3432. await handleSubmitPickQtyWithQty(lot, 0, "justComplete");
  3433. } catch (err) {
  3434. console.error("Error in Skip:", err);
  3435. }
  3436. },
  3437. [handleSubmitPickQtyWithQty],
  3438. );
  3439. const hasPendingBatchSubmit = useMemo(() => {
  3440. return combinedLotData.some((lot) => {
  3441. const status = String(lot.stockOutLineStatus || "").toLowerCase();
  3442. return (
  3443. status === "checked" ||
  3444. status === "pending" ||
  3445. status === "partially_completed" ||
  3446. status === "partially_complete"
  3447. );
  3448. });
  3449. }, [combinedLotData]);
  3450. useEffect(() => {
  3451. if (!hasPendingBatchSubmit) return;
  3452. const handler = (event: BeforeUnloadEvent) => {
  3453. event.preventDefault();
  3454. event.returnValue = "";
  3455. };
  3456. window.addEventListener("beforeunload", handler);
  3457. return () => window.removeEventListener("beforeunload", handler);
  3458. }, [hasPendingBatchSubmit]);
  3459. const handleStartScan = useCallback(() => {
  3460. const startTime = performance.now();
  3461. console.log(` [START SCAN] Called at: ${new Date().toISOString()}`);
  3462. console.log(` [START SCAN] Starting manual QR scan...`);
  3463. setIsManualScanning(true);
  3464. const setManualScanningTime = performance.now() - startTime;
  3465. console.log(
  3466. ` [START SCAN] setManualScanning time: ${setManualScanningTime.toFixed(
  3467. 2,
  3468. )}ms`,
  3469. );
  3470. setProcessedQrCodes(new Set());
  3471. setLastProcessedQr("");
  3472. setQrScanError(false);
  3473. setQrScanSuccess(false);
  3474. const beforeStartScanTime = performance.now();
  3475. startScan();
  3476. const startScanTime = performance.now() - beforeStartScanTime;
  3477. console.log(
  3478. ` [START SCAN] startScan() call time: ${startScanTime.toFixed(2)}ms`,
  3479. );
  3480. const totalTime = performance.now() - startTime;
  3481. console.log(
  3482. ` [START SCAN] Total start scan time: ${totalTime.toFixed(2)}ms`,
  3483. );
  3484. console.log(
  3485. ` [START SCAN] Start scan completed at: ${new Date().toISOString()}`,
  3486. );
  3487. }, [startScan]);
  3488. const handlePickOrderSwitch = useCallback(
  3489. async (pickOrderId: number) => {
  3490. if (pickOrderSwitching) return;
  3491. setPickOrderSwitching(true);
  3492. try {
  3493. console.log(" Switching to pick order:", pickOrderId);
  3494. setSelectedPickOrderId(pickOrderId);
  3495. // 强制刷新数据,确保显示正确的 pick order 数据
  3496. await fetchAllCombinedLotData(currentUserId, pickOrderId);
  3497. } catch (error) {
  3498. console.error("Error switching pick order:", error);
  3499. } finally {
  3500. setPickOrderSwitching(false);
  3501. }
  3502. },
  3503. [pickOrderSwitching, currentUserId, fetchAllCombinedLotData],
  3504. );
  3505. const handleStopScan = useCallback(() => {
  3506. console.log("⏸️ Pausing QR scanner...");
  3507. setIsManualScanning(false);
  3508. setQrScanError(false);
  3509. setQrScanSuccess(false);
  3510. stopScan();
  3511. resetScan();
  3512. }, [stopScan, resetScan]);
  3513. // ... existing code around line 1469 ...
  3514. const handlelotnull = useCallback(
  3515. async (lot: any) => {
  3516. // 优先使用 stockouts 中的 id,如果没有则使用 stockOutLineId
  3517. const stockOutLineId = lot.stockOutLineId;
  3518. if (!stockOutLineId) {
  3519. console.error(" No stockOutLineId found for lot:", lot);
  3520. return;
  3521. }
  3522. const solId = Number(stockOutLineId);
  3523. if (solId > 0 && actionBusyBySolId[solId]) {
  3524. console.warn("Action already in progress for stockOutLineId:", solId);
  3525. return;
  3526. }
  3527. try {
  3528. if (solId > 0)
  3529. setActionBusyBySolId((prev) => ({ ...prev, [solId]: true }));
  3530. // Step 1: Update stock out line status
  3531. await updateStockOutLineStatus({
  3532. id: stockOutLineId,
  3533. status: "completed",
  3534. qty: 0,
  3535. });
  3536. // Step 2: Create pick execution issue for no-lot case
  3537. // Get pick order ID from fgPickOrders or use 0 if not available
  3538. const pickOrderId =
  3539. lot.pickOrderId || fgPickOrders[0]?.pickOrderId || 0;
  3540. const pickOrderCode =
  3541. lot.pickOrderCode ||
  3542. fgPickOrders[0]?.pickOrderCode ||
  3543. lot.pickOrderConsoCode ||
  3544. "";
  3545. const issueData: PickExecutionIssueData = {
  3546. type: "Do", // Delivery Order type
  3547. pickOrderId: pickOrderId,
  3548. pickOrderCode: pickOrderCode,
  3549. pickOrderCreateDate: dayjs().format("YYYY-MM-DD"), // Use dayjs format
  3550. pickExecutionDate: dayjs().format("YYYY-MM-DD"),
  3551. pickOrderLineId: lot.pickOrderLineId,
  3552. itemId: lot.itemId,
  3553. itemCode: lot.itemCode || "",
  3554. itemDescription: lot.itemName || "",
  3555. lotId: null, // No lot available
  3556. lotNo: null, // No lot number
  3557. storeLocation: lot.location || "",
  3558. requiredQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0,
  3559. actualPickQty: 0, // No items picked (no lot available)
  3560. missQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0, // All quantity is missing
  3561. badItemQty: 0,
  3562. issueRemark: `No lot available for this item. Handled via handlelotnull.`,
  3563. pickerName: session?.user?.name || "",
  3564. };
  3565. const result = await recordPickExecutionIssue(issueData);
  3566. console.log(" Pick execution issue created for no-lot item:", result);
  3567. if (result && result.code === "SUCCESS") {
  3568. console.log(" No-lot item handled and issue recorded successfully");
  3569. } else {
  3570. console.error(" Failed to record pick execution issue:", result);
  3571. }
  3572. // Step 3: Refresh data
  3573. await fetchAllCombinedLotData();
  3574. } catch (error) {
  3575. console.error(" Error in handlelotnull:", error);
  3576. } finally {
  3577. if (solId > 0)
  3578. setActionBusyBySolId((prev) => ({ ...prev, [solId]: false }));
  3579. }
  3580. },
  3581. [
  3582. fetchAllCombinedLotData,
  3583. session,
  3584. currentUserId,
  3585. fgPickOrders,
  3586. actionBusyBySolId,
  3587. ],
  3588. );
  3589. const handleBatchScan = useCallback(async () => {
  3590. const startTime = performance.now();
  3591. console.log(` [BATCH SCAN START]`);
  3592. console.log(` Start time: ${new Date().toISOString()}`);
  3593. // 获取所有活跃批次(未扫描的)
  3594. const activeLots = combinedLotData.filter((lot) => {
  3595. return (
  3596. lot.lotAvailability !== "rejected" &&
  3597. lot.stockOutLineStatus !== "rejected" &&
  3598. lot.stockOutLineStatus !== "completed" &&
  3599. lot.stockOutLineStatus !== "checked" && // ✅ 只处理未扫描的
  3600. lot.processingStatus !== "completed" &&
  3601. lot.noLot !== true &&
  3602. lot.lotNo // ✅ 必须有 lotNo
  3603. );
  3604. });
  3605. if (activeLots.length === 0) {
  3606. console.log("No active lots to scan");
  3607. return;
  3608. }
  3609. console.log(
  3610. `📦 Batch scanning ${activeLots.length} active lots using batch API...`,
  3611. );
  3612. try {
  3613. // ✅ 转换为批量扫描 API 所需的格式
  3614. const lines: BatchScanLineRequest[] = activeLots.map((lot) => ({
  3615. pickOrderLineId: Number(lot.pickOrderLineId),
  3616. inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
  3617. pickOrderConsoCode: String(lot.pickOrderConsoCode || ""),
  3618. lotNo: lot.lotNo || null,
  3619. itemId: Number(lot.itemId),
  3620. itemCode: String(lot.itemCode || ""),
  3621. stockOutLineId: lot.stockOutLineId ? Number(lot.stockOutLineId) : null, // ✅ 新增
  3622. }));
  3623. const request: BatchScanRequest = {
  3624. userId: currentUserId || 0,
  3625. lines: lines,
  3626. };
  3627. console.log(`📤 Sending batch scan request with ${lines.length} lines`);
  3628. console.log(`📋 Request data:`, JSON.stringify(request, null, 2));
  3629. const scanStartTime = performance.now();
  3630. // ✅ 使用新的批量扫描 API(一次性处理所有请求)
  3631. const result = await batchScan(request);
  3632. const scanTime = performance.now() - scanStartTime;
  3633. console.log(
  3634. ` Batch scan API call completed in ${scanTime.toFixed(2)}ms (${(
  3635. scanTime / 1000
  3636. ).toFixed(3)}s)`,
  3637. );
  3638. console.log(`📥 Batch scan result:`, result);
  3639. // ✅ 刷新数据以获取最新的状态
  3640. const refreshStartTime = performance.now();
  3641. await fetchAllCombinedLotData();
  3642. const refreshTime = performance.now() - refreshStartTime;
  3643. console.log(
  3644. ` Data refresh time: ${refreshTime.toFixed(2)}ms (${(
  3645. refreshTime / 1000
  3646. ).toFixed(3)}s)`,
  3647. );
  3648. const totalTime = performance.now() - startTime;
  3649. console.log(` [BATCH SCAN END]`);
  3650. console.log(
  3651. ` Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(
  3652. 3,
  3653. )}s)`,
  3654. );
  3655. console.log(` End time: ${new Date().toISOString()}`);
  3656. if (result && result.code === "SUCCESS") {
  3657. setQrScanSuccess(true);
  3658. setQrScanError(false);
  3659. } else {
  3660. console.error("❌ Batch scan failed:", result);
  3661. setQrScanError(true);
  3662. setQrScanSuccess(false);
  3663. }
  3664. } catch (error) {
  3665. console.error("❌ Error in batch scan:", error);
  3666. setQrScanError(true);
  3667. setQrScanSuccess(false);
  3668. }
  3669. }, [combinedLotData, fetchAllCombinedLotData, currentUserId]);
  3670. const handleSubmitAllScanned = useCallback(async () => {
  3671. const startTime = performance.now();
  3672. console.log(` [BATCH SUBMIT START]`);
  3673. console.log(` Start time: ${new Date().toISOString()}`);
  3674. const scannedLots = combinedLotData.filter((lot) => {
  3675. const status = lot.stockOutLineStatus;
  3676. const statusLower = String(status || "").toLowerCase();
  3677. if (statusLower === "completed" || statusLower === "complete") {
  3678. return false;
  3679. }
  3680. // ✅ noLot / 過期批號:與 noLot 相同,允許 pending(未換批也可批量收尾)
  3681. if (
  3682. lot.noLot === true ||
  3683. isLotAvailabilityExpired(lot) ||
  3684. isInventoryLotLineUnavailable(lot)
  3685. ) {
  3686. return (
  3687. status === "checked" ||
  3688. status === "pending" ||
  3689. status === "partially_completed" ||
  3690. status === "PARTIALLY_COMPLETE"
  3691. );
  3692. }
  3693. return (
  3694. status === "checked" ||
  3695. status === "partially_completed" ||
  3696. status === "PARTIALLY_COMPLETE"
  3697. );
  3698. });
  3699. if (scannedLots.length === 0) {
  3700. console.log("No scanned items to submit");
  3701. return;
  3702. }
  3703. setIsSubmittingAll(true);
  3704. console.log(
  3705. `📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`,
  3706. );
  3707. try {
  3708. // 转换为 batchSubmitList 所需的格式(与后端 QrPickBatchSubmitRequest 匹配)
  3709. const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
  3710. // 1. 需求数量
  3711. const requiredQty = Number(
  3712. lot.requiredQty || lot.pickOrderLineRequiredQty || 0,
  3713. );
  3714. // 2. 当前已经拣到的数量
  3715. // issue form 不会写回 SOL.qty,所以如果这条 SOL 有 issue,就用 issue form 的 actualPickQty 作为“已拣到数量”
  3716. const solId = Number(lot.stockOutLineId) || 0;
  3717. const issuePicked =
  3718. solId > 0 ? issuePickedQtyBySolId[solId] : undefined;
  3719. const currentActualPickQty = Number(
  3720. issuePicked ?? lot.actualPickQty ?? 0,
  3721. );
  3722. // 🔹 判断是否走“只改状态模式”
  3723. // 这里先给一个简单条件示例:如果你不想再补拣,只想把当前数量标记完成,
  3724. // 就让这个条件为 true(后面你可以根据业务加 UI 开关或别的 flag)。
  3725. const onlyComplete =
  3726. lot.stockOutLineStatus === "partially_completed" ||
  3727. issuePicked !== undefined;
  3728. const expired = isLotAvailabilityExpired(lot);
  3729. const unavailable = isInventoryLotLineUnavailable(lot);
  3730. let targetActual: number;
  3731. let newStatus: string;
  3732. // ✅ 過期且未在 Issue 填實際量:與 noLot 一樣按 0 完成
  3733. if (unavailable) {
  3734. targetActual = currentActualPickQty;
  3735. newStatus = "completed";
  3736. } else if (expired && issuePicked === undefined) {
  3737. targetActual = 0;
  3738. newStatus = "completed";
  3739. } else if (onlyComplete) {
  3740. targetActual = currentActualPickQty;
  3741. newStatus = "completed";
  3742. } else {
  3743. const remainingQty = Math.max(0, requiredQty - currentActualPickQty);
  3744. const cumulativeQty = currentActualPickQty + remainingQty;
  3745. targetActual = cumulativeQty;
  3746. newStatus = "partially_completed";
  3747. if (requiredQty > 0 && cumulativeQty >= requiredQty) {
  3748. newStatus = "completed";
  3749. }
  3750. }
  3751. return {
  3752. stockOutLineId: Number(lot.stockOutLineId) || 0,
  3753. pickOrderLineId: Number(lot.pickOrderLineId),
  3754. inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
  3755. requiredQty,
  3756. // 后端用 targetActual - 当前 qty 算增量,onlyComplete 时就是 0
  3757. actualPickQty: targetActual,
  3758. stockOutLineStatus: newStatus,
  3759. pickOrderConsoCode: String(lot.pickOrderConsoCode || ""),
  3760. noLot: Boolean(lot.noLot === true),
  3761. };
  3762. });
  3763. const request: batchSubmitListRequest = {
  3764. userId: currentUserId || 0,
  3765. lines: lines,
  3766. };
  3767. console.log(`📤 Sending batch submit request with ${lines.length} lines`);
  3768. console.log(`📋 Request data:`, JSON.stringify(request, null, 2));
  3769. const submitStartTime = performance.now();
  3770. // 使用 batchSubmitList API
  3771. const result = await batchSubmitList(request);
  3772. const submitTime = performance.now() - submitStartTime;
  3773. console.log(
  3774. ` Batch submit API call completed in ${submitTime.toFixed(2)}ms (${(
  3775. submitTime / 1000
  3776. ).toFixed(3)}s)`,
  3777. );
  3778. console.log(`📥 Batch submit result:`, result);
  3779. // Refresh data once after batch submission
  3780. const refreshStartTime = performance.now();
  3781. await fetchAllCombinedLotData();
  3782. const refreshTime = performance.now() - refreshStartTime;
  3783. console.log(
  3784. ` Data refresh time: ${refreshTime.toFixed(2)}ms (${(
  3785. refreshTime / 1000
  3786. ).toFixed(3)}s)`,
  3787. );
  3788. const totalTime = performance.now() - startTime;
  3789. console.log(` [BATCH SUBMIT END]`);
  3790. console.log(
  3791. ` Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(
  3792. 3,
  3793. )}s)`,
  3794. );
  3795. console.log(` End time: ${new Date().toISOString()}`);
  3796. if (result && result.code === "SUCCESS") {
  3797. setQrScanSuccess(true);
  3798. setTimeout(() => {
  3799. setQrScanSuccess(false);
  3800. checkAndAutoAssignNext();
  3801. if (onSwitchToRecordTab) {
  3802. onSwitchToRecordTab();
  3803. }
  3804. if (onRefreshReleasedOrderCount) {
  3805. onRefreshReleasedOrderCount();
  3806. }
  3807. }, 2000);
  3808. } else {
  3809. console.error("Batch submit failed:", result);
  3810. setQrScanError(true);
  3811. }
  3812. } catch (error) {
  3813. console.error("Error submitting all scanned items:", error);
  3814. setQrScanError(true);
  3815. } finally {
  3816. setIsSubmittingAll(false);
  3817. }
  3818. }, [
  3819. combinedLotData,
  3820. fetchAllCombinedLotData,
  3821. checkAndAutoAssignNext,
  3822. currentUserId,
  3823. onSwitchToRecordTab,
  3824. onRefreshReleasedOrderCount,
  3825. issuePickedQtyBySolId,
  3826. ]);
  3827. // Calculate scanned items count
  3828. // Calculate scanned items count (should match handleSubmitAllScanned filter logic)
  3829. const scannedItemsCount = useMemo(() => {
  3830. const filtered = combinedLotData.filter((lot) => {
  3831. const status = lot.stockOutLineStatus;
  3832. const statusLower = String(status || "").toLowerCase();
  3833. if (statusLower === "completed" || statusLower === "complete") {
  3834. return false;
  3835. }
  3836. // ✅ 与 handleSubmitAllScanned 完全保持一致
  3837. if (
  3838. lot.noLot === true ||
  3839. isLotAvailabilityExpired(lot) ||
  3840. isInventoryLotLineUnavailable(lot)
  3841. ) {
  3842. return (
  3843. status === "checked" ||
  3844. status === "pending" ||
  3845. status === "partially_completed" ||
  3846. status === "PARTIALLY_COMPLETE"
  3847. );
  3848. }
  3849. return (
  3850. status === "checked" ||
  3851. status === "partially_completed" ||
  3852. status === "PARTIALLY_COMPLETE"
  3853. );
  3854. });
  3855. // 添加调试日志
  3856. const noLotCount = filtered.filter((l) => l.noLot === true).length;
  3857. const normalCount = filtered.filter((l) => l.noLot !== true).length;
  3858. console.log(
  3859. `📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`,
  3860. );
  3861. console.log(`📊 All items breakdown:`, {
  3862. total: combinedLotData.length,
  3863. noLot: combinedLotData.filter((l) => l.noLot === true).length,
  3864. normal: combinedLotData.filter((l) => l.noLot !== true).length,
  3865. });
  3866. return filtered.length;
  3867. }, [combinedLotData]);
  3868. /*
  3869. // ADD THIS: Auto-stop scan when no data available
  3870. useEffect(() => {
  3871. if (isManualScanning && combinedLotData.length === 0) {
  3872. console.log("⏹️ No data available, auto-stopping QR scan...");
  3873. handleStopScan();
  3874. }
  3875. }, [combinedLotData.length, isManualScanning, handleStopScan]);
  3876. */
  3877. // Cleanup effect
  3878. useEffect(() => {
  3879. return () => {
  3880. // Cleanup when component unmounts (e.g., when switching tabs)
  3881. if (isManualScanning) {
  3882. console.log(
  3883. "🧹 Pick execution component unmounting, stopping QR scanner...",
  3884. );
  3885. stopScan();
  3886. resetScan();
  3887. }
  3888. };
  3889. }, [isManualScanning, stopScan, resetScan]);
  3890. const getStatusMessage = useCallback(
  3891. (lot: any) => {
  3892. switch (lot.stockOutLineStatus?.toLowerCase()) {
  3893. case "pending":
  3894. return t("Please finish QR code scan and pick order.");
  3895. case "checked":
  3896. return t("Please submit the pick order.");
  3897. case "partially_completed":
  3898. return t(
  3899. "Partial quantity submitted. Please submit more or complete the order.",
  3900. );
  3901. case "completed":
  3902. return t("Pick order completed successfully!");
  3903. case "rejected":
  3904. return t("Lot has been rejected and marked as unavailable.");
  3905. case "unavailable":
  3906. return t("This order is insufficient, please pick another lot.");
  3907. default:
  3908. return t("Please finish QR code scan and pick order.");
  3909. }
  3910. },
  3911. [t],
  3912. );
  3913. return (
  3914. <TestQrCodeProvider
  3915. lotData={combinedLotData}
  3916. onScanLot={handleQrCodeSubmit}
  3917. onBatchScan={handleBatchScan}
  3918. filterActive={(lot) =>
  3919. lot.lotAvailability !== "rejected" &&
  3920. lot.stockOutLineStatus !== "rejected" &&
  3921. lot.stockOutLineStatus !== "completed"
  3922. }
  3923. >
  3924. <FormProvider {...formProps}>
  3925. <Stack spacing={2}>
  3926. <Box
  3927. sx={{
  3928. position: "fixed",
  3929. top: 0,
  3930. left: 0,
  3931. right: 0,
  3932. zIndex: 1100, // Higher than other elements
  3933. backgroundColor: "background.paper",
  3934. pt: 2,
  3935. pb: 1,
  3936. px: 2,
  3937. borderBottom: "1px solid",
  3938. borderColor: "divider",
  3939. boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
  3940. }}
  3941. >
  3942. <LinearProgressWithLabel
  3943. completed={progress.completed}
  3944. total={progress.total}
  3945. label={t("Progress")}
  3946. />
  3947. <ScanStatusAlert
  3948. error={qrScanError}
  3949. success={qrScanSuccess}
  3950. errorMessage={t(
  3951. "QR code does not match any item in current orders.",
  3952. )}
  3953. successMessage={t("QR code verified.")}
  3954. />
  3955. </Box>
  3956. {/* DO Header */}
  3957. {/* 保留:Combined Lot Table - 包含所有 QR 扫描功能 */}
  3958. <Box>
  3959. <Box
  3960. sx={{
  3961. display: "flex",
  3962. justifyContent: "space-between",
  3963. alignItems: "center",
  3964. mb: 2,
  3965. mt: 10,
  3966. }}
  3967. >
  3968. <Typography variant="h6" gutterBottom sx={{ mb: 0 }}>
  3969. {t("All Pick Order Lots")}
  3970. </Typography>
  3971. <Box sx={{ display: "flex", gap: 2, alignItems: "center" }}>
  3972. {/* Scanner status indicator (always visible) */}
  3973. {/*
  3974. <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
  3975. <QrCodeIcon
  3976. sx={{
  3977. color: isManualScanning ? '#4caf50' : '#9e9e9e',
  3978. animation: isManualScanning ? 'pulse 2s infinite' : 'none',
  3979. '@keyframes pulse': {
  3980. '0%, 100%': { opacity: 1 },
  3981. '50%': { opacity: 0.5 }
  3982. }
  3983. }}
  3984. />
  3985. <Typography variant="body2" sx={{ color: isManualScanning ? '#4caf50' : '#9e9e9e' }}>
  3986. {isManualScanning ? t("Scanner Active") : t("Scanner Inactive")}
  3987. </Typography>
  3988. </Box>
  3989. */}
  3990. {/* Pause/Resume button instead of Start/Stop */}
  3991. {isManualScanning ? (
  3992. <Button
  3993. variant="outlined"
  3994. startIcon={<QrCodeIcon />}
  3995. onClick={handleStopScan}
  3996. color="secondary"
  3997. sx={{ minWidth: "120px" }}
  3998. >
  3999. {t("Stop QR Scan")}
  4000. </Button>
  4001. ) : (
  4002. <Button
  4003. variant="contained"
  4004. startIcon={<QrCodeIcon />}
  4005. onClick={handleStartScan}
  4006. color="primary"
  4007. sx={{ minWidth: "120px" }}
  4008. >
  4009. {t("Start QR Scan")}
  4010. </Button>
  4011. )}
  4012. {/* 保留:Submit All Scanned Button */}
  4013. <Button
  4014. variant="contained"
  4015. color="success"
  4016. onClick={handleSubmitAllScanned}
  4017. disabled={scannedItemsCount === 0 || isSubmittingAll}
  4018. sx={{ minWidth: "160px" }}
  4019. >
  4020. {isSubmittingAll ? (
  4021. <>
  4022. <CircularProgress
  4023. size={16}
  4024. sx={{ mr: 1, color: "white" }}
  4025. />
  4026. {t("Submitting...")}
  4027. </>
  4028. ) : (
  4029. `${t("Submit All Scanned")} (${scannedItemsCount})`
  4030. )}
  4031. </Button>
  4032. </Box>
  4033. </Box>
  4034. {fgPickOrders.length > 0 && (
  4035. <Paper sx={{ p: 2, mb: 2 }}>
  4036. <Stack spacing={2}>
  4037. {/* 基本信息 */}
  4038. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  4039. <Typography variant="subtitle1">
  4040. <strong>{t("Shop Name")}:</strong>{" "}
  4041. {fgPickOrders[0].shopName || "-"}
  4042. </Typography>
  4043. <Typography variant="subtitle1">
  4044. <strong>{t("Store ID")}:</strong>{" "}
  4045. {fgPickOrders[0].storeId || "-"}
  4046. </Typography>
  4047. <Typography variant="subtitle1">
  4048. <strong>{t("Ticket No.")}:</strong>{" "}
  4049. {fgPickOrders[0].ticketNo || "-"}
  4050. </Typography>
  4051. <Typography variant="subtitle1">
  4052. <strong>{t("Departure Time")}:</strong>{" "}
  4053. {fgPickOrders[0].DepartureTime || "-"}
  4054. </Typography>
  4055. </Stack>
  4056. {/* 改进:三个字段显示在一起,使用表格式布局 */}
  4057. {/* 改进:三个字段合并显示 */}
  4058. {/* 改进:表格式显示每个 pick order */}
  4059. <Box
  4060. sx={{
  4061. p: 2,
  4062. backgroundColor: "#f5f5f5",
  4063. borderRadius: 1,
  4064. }}
  4065. >
  4066. <Typography
  4067. variant="subtitle2"
  4068. sx={{ mb: 1, fontWeight: "bold" }}
  4069. >
  4070. {t("Pick Orders Details")}:
  4071. </Typography>
  4072. {(() => {
  4073. const pickOrderCodes = fgPickOrders[0].pickOrderCodes as
  4074. | string[]
  4075. | string
  4076. | undefined;
  4077. const deliveryNos = fgPickOrders[0].deliveryNos as
  4078. | string[]
  4079. | string
  4080. | undefined;
  4081. const lineCounts = fgPickOrders[0].lineCountsPerPickOrder;
  4082. const pickOrderCodesArray = Array.isArray(pickOrderCodes)
  4083. ? pickOrderCodes
  4084. : typeof pickOrderCodes === "string"
  4085. ? pickOrderCodes.split(", ")
  4086. : [];
  4087. const deliveryNosArray = Array.isArray(deliveryNos)
  4088. ? deliveryNos
  4089. : typeof deliveryNos === "string"
  4090. ? deliveryNos.split(", ")
  4091. : [];
  4092. const lineCountsArray = Array.isArray(lineCounts)
  4093. ? lineCounts
  4094. : [];
  4095. const maxLength = Math.max(
  4096. pickOrderCodesArray.length,
  4097. deliveryNosArray.length,
  4098. lineCountsArray.length,
  4099. );
  4100. if (maxLength === 0) {
  4101. return (
  4102. <Typography variant="body2" color="text.secondary">
  4103. -
  4104. </Typography>
  4105. );
  4106. }
  4107. // 使用与外部基本信息相同的样式
  4108. return Array.from({ length: maxLength }, (_, idx) => (
  4109. <Stack
  4110. key={idx}
  4111. direction="row"
  4112. spacing={4}
  4113. useFlexGap
  4114. flexWrap="wrap"
  4115. sx={{ mb: idx < maxLength - 1 ? 1 : 0 }} // 除了最后一行,都添加底部间距
  4116. >
  4117. <Typography variant="subtitle1">
  4118. <strong>{t("Delivery Order")}:</strong>{" "}
  4119. {deliveryNosArray[idx] || "-"}
  4120. </Typography>
  4121. <Typography variant="subtitle1">
  4122. <strong>{t("Pick Order")}:</strong>{" "}
  4123. {pickOrderCodesArray[idx] || "-"}
  4124. </Typography>
  4125. <Typography variant="subtitle1">
  4126. <strong>{t("Finsihed good items")}:</strong>{" "}
  4127. {lineCountsArray[idx] || "-"}
  4128. <strong>{t("kinds")}</strong>
  4129. </Typography>
  4130. </Stack>
  4131. ));
  4132. })()}
  4133. </Box>
  4134. </Stack>
  4135. </Paper>
  4136. )}
  4137. <TableContainer component={Paper}>
  4138. <Table>
  4139. <TableHead>
  4140. <TableRow>
  4141. <TableCell>{t("Index")}</TableCell>
  4142. <TableCell>{t("Route")}</TableCell>
  4143. <TableCell>{t("Item Code")}</TableCell>
  4144. <TableCell>{t("Item Name")}</TableCell>
  4145. <TableCell>{t("Lot#")}</TableCell>
  4146. <TableCell align="right">
  4147. {t("Lot Required Pick Qty")}
  4148. </TableCell>
  4149. <TableCell align="center">{t("Scan Result")}</TableCell>
  4150. <TableCell align="center">{t("Qty will submit")}</TableCell>
  4151. <TableCell align="center">
  4152. {t("Submit Required Pick Qty")}
  4153. </TableCell>
  4154. </TableRow>
  4155. </TableHead>
  4156. <TableBody>
  4157. {paginatedData.length === 0 ? (
  4158. <TableRow>
  4159. <TableCell colSpan={11} align="center">
  4160. <Typography variant="body2" color="text.secondary">
  4161. {t("No data available")}
  4162. </Typography>
  4163. </TableCell>
  4164. </TableRow>
  4165. ) : (
  4166. // 在第 1797-1938 行之间,将整个 map 函数修改为:
  4167. paginatedData.map((lot, index) => {
  4168. // 检查是否是 issue lot
  4169. const isIssueLot =
  4170. lot.stockOutLineStatus === "rejected" || !lot.lotNo;
  4171. const rowSolId = Number(lot.stockOutLineId);
  4172. const lotSwitchErr = Number.isFinite(rowSolId)
  4173. ? lotSwitchFailByStockOutLineId[rowSolId]
  4174. : undefined;
  4175. return (
  4176. <TableRow
  4177. key={`${lot.pickOrderLineId}-${lot.lotId || "null"}`}
  4178. sx={{
  4179. //backgroundColor: isIssueLot ? '#fff3e0' : 'inherit',
  4180. // opacity: isIssueLot ? 0.6 : 1,
  4181. "& .MuiTableCell-root": {
  4182. //color: isIssueLot ? 'warning.main' : 'inherit'
  4183. },
  4184. }}
  4185. >
  4186. <TableCell>
  4187. <Typography variant="body2" fontWeight="bold">
  4188. {paginationController.pageNum *
  4189. paginationController.pageSize +
  4190. index +
  4191. 1}
  4192. </Typography>
  4193. </TableCell>
  4194. <TableCell>
  4195. <Typography variant="body2">
  4196. {lot.routerRoute || "-"}
  4197. </Typography>
  4198. </TableCell>
  4199. <TableCell>{lot.itemCode}</TableCell>
  4200. <TableCell>
  4201. {lot.itemName + "(" + lot.stockUnit + ")"}
  4202. </TableCell>
  4203. <TableCell>
  4204. <Box>
  4205. <Typography
  4206. sx={{
  4207. color: isInventoryLotLineUnavailable(lot)
  4208. ? "error.main"
  4209. : lot.lotAvailability === "expired"
  4210. ? "warning.main"
  4211. : "inherit",
  4212. }}
  4213. >
  4214. {lot.lotNo ? (
  4215. isInventoryLotLineUnavailable(lot) ? (
  4216. <>
  4217. {lot.lotNo}{" "}
  4218. {t(
  4219. "is unavable. Please check around have available QR code or not.",
  4220. )}
  4221. </>
  4222. ) : lot.lotAvailability === "expired" ? (
  4223. <>
  4224. {lot.lotNo}{" "}
  4225. {t(
  4226. "is expired. Please check around have available QR code or not.",
  4227. )}
  4228. </>
  4229. ) : (
  4230. lot.lotNo
  4231. )
  4232. ) : (
  4233. t(
  4234. "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.",
  4235. )
  4236. )}
  4237. </Typography>
  4238. {lotSwitchErr ? (
  4239. <Typography
  4240. variant="body2"
  4241. color="error"
  4242. sx={{
  4243. mt: 0.5,
  4244. display: "block",
  4245. fontWeight: 500,
  4246. }}
  4247. >
  4248. {lotSwitchErr}
  4249. </Typography>
  4250. ) : null}
  4251. </Box>
  4252. </TableCell>
  4253. <TableCell align="right">
  4254. {(() => {
  4255. const requiredQty = lot.requiredQty || 0;
  4256. return (
  4257. requiredQty.toLocaleString() +
  4258. "(" +
  4259. lot.uomShortDesc +
  4260. ")"
  4261. );
  4262. })()}
  4263. </TableCell>
  4264. <TableCell align="center">
  4265. {(() => {
  4266. const status =
  4267. lot.stockOutLineStatus?.toLowerCase();
  4268. const isRejected =
  4269. status === "rejected" ||
  4270. lot.lotAvailability === "rejected";
  4271. const isNoLot = !lot.lotNo;
  4272. // rejected lot:显示红色勾选(已扫描但被拒绝)
  4273. if (isRejected && !isNoLot) {
  4274. return (
  4275. <Box
  4276. sx={{
  4277. display: "flex",
  4278. justifyContent: "center",
  4279. alignItems: "center",
  4280. }}
  4281. >
  4282. <Checkbox
  4283. checked={true}
  4284. disabled={true}
  4285. readOnly={true}
  4286. size="large"
  4287. sx={{
  4288. color: "error.main",
  4289. "&.Mui-checked": {
  4290. color: "error.main",
  4291. },
  4292. transform: "scale(1.3)",
  4293. }}
  4294. />
  4295. </Box>
  4296. );
  4297. }
  4298. // 過期批號:與 noLot 同類——視為已掃到/可處理(含 pending),顯示警示色勾選
  4299. if (
  4300. isLotAvailabilityExpired(lot) &&
  4301. status !== "rejected"
  4302. ) {
  4303. return (
  4304. <Box
  4305. sx={{
  4306. display: "flex",
  4307. justifyContent: "center",
  4308. alignItems: "center",
  4309. }}
  4310. >
  4311. <Checkbox
  4312. checked={true}
  4313. disabled={true}
  4314. readOnly={true}
  4315. size="large"
  4316. sx={{
  4317. color: "warning.main",
  4318. "&.Mui-checked": {
  4319. color: "warning.main",
  4320. },
  4321. transform: "scale(1.3)",
  4322. }}
  4323. />
  4324. </Box>
  4325. );
  4326. }
  4327. // 正常 lot:已扫描(checked/partially_completed/completed)
  4328. if (
  4329. !isNoLot &&
  4330. status !== "pending" &&
  4331. status !== "rejected"
  4332. ) {
  4333. return (
  4334. <Box
  4335. sx={{
  4336. display: "flex",
  4337. justifyContent: "center",
  4338. alignItems: "center",
  4339. }}
  4340. >
  4341. <Checkbox
  4342. checked={true}
  4343. disabled={true}
  4344. readOnly={true}
  4345. size="large"
  4346. sx={{
  4347. color: "success.main",
  4348. "&.Mui-checked": {
  4349. color: "success.main",
  4350. },
  4351. transform: "scale(1.3)",
  4352. }}
  4353. />
  4354. </Box>
  4355. );
  4356. }
  4357. // noLot 且已完成/部分完成:显示红色勾选
  4358. if (
  4359. isNoLot &&
  4360. (status === "partially_completed" ||
  4361. status === "completed")
  4362. ) {
  4363. return (
  4364. <Box
  4365. sx={{
  4366. display: "flex",
  4367. justifyContent: "center",
  4368. alignItems: "center",
  4369. }}
  4370. >
  4371. <Checkbox
  4372. checked={true}
  4373. disabled={true}
  4374. readOnly={true}
  4375. size="large"
  4376. sx={{
  4377. color: "error.main",
  4378. "&.Mui-checked": {
  4379. color: "error.main",
  4380. },
  4381. transform: "scale(1.3)",
  4382. }}
  4383. />
  4384. </Box>
  4385. );
  4386. }
  4387. return null;
  4388. })()}
  4389. </TableCell>
  4390. <TableCell align="center">
  4391. {isInventoryLotLineUnavailable(lot)
  4392. ? 0
  4393. : resolveSingleSubmitQty(lot)}
  4394. </TableCell>
  4395. <TableCell align="center">
  4396. <Box
  4397. sx={{ display: "flex", justifyContent: "center" }}
  4398. >
  4399. {(() => {
  4400. const status =
  4401. lot.stockOutLineStatus?.toLowerCase();
  4402. const isRejected =
  4403. status === "rejected" ||
  4404. lot.lotAvailability === "rejected";
  4405. const isNoLot = !lot.lotNo;
  4406. const isUnavailableLot =
  4407. isInventoryLotLineUnavailable(lot);
  4408. // ✅ rejected lot:显示提示文本(换行显示)
  4409. if (isRejected && !isNoLot) {
  4410. return (
  4411. <Typography
  4412. variant="body2"
  4413. color="error.main"
  4414. sx={{
  4415. textAlign: "center",
  4416. whiteSpace: "normal",
  4417. wordBreak: "break-word",
  4418. maxWidth: "200px",
  4419. lineHeight: 1.5,
  4420. }}
  4421. >
  4422. {t(
  4423. "This lot is rejected, please scan another lot.",
  4424. )}
  4425. </Typography>
  4426. );
  4427. }
  4428. // noLot 情况:只显示 Issue 按钮
  4429. if (isNoLot) {
  4430. return (
  4431. <Button
  4432. variant="outlined"
  4433. size="small"
  4434. onClick={() => handlelotnull(lot)}
  4435. /*
  4436. disabled={
  4437. status === 'completed' ||
  4438. (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true)
  4439. }
  4440. */
  4441. disabled={true}
  4442. sx={{
  4443. fontSize: "0.7rem",
  4444. py: 0.5,
  4445. minHeight: "28px",
  4446. minWidth: "60px",
  4447. borderColor: "warning.main",
  4448. color: "warning.main",
  4449. }}
  4450. >
  4451. {t("Issue")}
  4452. </Button>
  4453. );
  4454. }
  4455. // 正常 lot:显示 Submit 和 Issue 按钮
  4456. return (
  4457. <Stack
  4458. direction="row"
  4459. spacing={1}
  4460. alignItems="center"
  4461. >
  4462. <Button
  4463. variant="contained"
  4464. onClick={() => {
  4465. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  4466. const submitQty =
  4467. resolveSingleSubmitQty(lot);
  4468. handlePickQtyChange(lotKey, submitQty);
  4469. handleSubmitPickQtyWithQty(
  4470. lot,
  4471. submitQty,
  4472. "singleSubmit",
  4473. );
  4474. }}
  4475. /*
  4476. disabled={
  4477. lot.lotAvailability === 'expired' ||
  4478. isInventoryLotLineUnavailable(lot) ||
  4479. lot.lotAvailability === 'rejected' ||
  4480. lot.stockOutLineStatus === 'completed' ||
  4481. lot.stockOutLineStatus === 'pending' ||
  4482. (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true)
  4483. }
  4484. */
  4485. disabled={true}
  4486. sx={{
  4487. fontSize: "0.75rem",
  4488. py: 0.5,
  4489. minHeight: "28px",
  4490. minWidth: "70px",
  4491. }}
  4492. >
  4493. {t("Submit")}
  4494. </Button>
  4495. <Button
  4496. variant="outlined"
  4497. size="small"
  4498. onClick={() =>
  4499. handlePickExecutionForm(lot)
  4500. }
  4501. /*
  4502. disabled={
  4503. lot.lotAvailability === 'expired' ||
  4504. lot.stockOutLineStatus === 'completed' ||
  4505. (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true)
  4506. }*/
  4507. disabled={true}
  4508. sx={{
  4509. fontSize: "0.7rem",
  4510. py: 0.5,
  4511. minHeight: "28px",
  4512. minWidth: "60px",
  4513. borderColor: "warning.main",
  4514. color: "warning.main",
  4515. }}
  4516. title="Report missing or bad items"
  4517. >
  4518. {t("Edit")}
  4519. </Button>
  4520. <Button
  4521. variant="outlined"
  4522. size="small"
  4523. onClick={() => handleSkip(lot)}
  4524. title={
  4525. isUnavailableLot
  4526. ? t(
  4527. "is unavable. Please check around have available QR code or not.",
  4528. )
  4529. : undefined
  4530. }
  4531. disabled={
  4532. lot.stockOutLineStatus ===
  4533. "completed" ||
  4534. lot.stockOutLineStatus === "checked" ||
  4535. lot.stockOutLineStatus ===
  4536. "partially_completed" ||
  4537. lot.lotAvailability === "expired" ||
  4538. isUnavailableLot ||
  4539. // 使用 issue form 後,禁用「Just Completed」(避免再次点击造成重复提交)
  4540. (Number(lot.stockOutLineId) > 0 &&
  4541. issuePickedQtyBySolId[
  4542. Number(lot.stockOutLineId)
  4543. ] !== undefined) ||
  4544. (Number(lot.stockOutLineId) > 0 &&
  4545. actionBusyBySolId[
  4546. Number(lot.stockOutLineId)
  4547. ] === true)
  4548. }
  4549. sx={{
  4550. fontSize: "0.7rem",
  4551. py: 0.5,
  4552. minHeight: "28px",
  4553. minWidth: "60px",
  4554. }}
  4555. >
  4556. {t("Just Completed")}
  4557. </Button>
  4558. </Stack>
  4559. );
  4560. })()}
  4561. </Box>
  4562. </TableCell>
  4563. </TableRow>
  4564. );
  4565. })
  4566. )}
  4567. </TableBody>
  4568. </Table>
  4569. </TableContainer>
  4570. <TablePagination
  4571. component="div"
  4572. count={combinedLotData.length}
  4573. page={paginationController.pageNum}
  4574. rowsPerPage={paginationController.pageSize}
  4575. onPageChange={handlePageChange}
  4576. onRowsPerPageChange={handlePageSizeChange}
  4577. rowsPerPageOptions={[10, 25, 50, -1]}
  4578. labelRowsPerPage={t("Rows per page")}
  4579. labelDisplayedRows={({ from, to, count }) =>
  4580. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  4581. }
  4582. />
  4583. </Box>
  4584. </Stack>
  4585. {/* QR Code Scanner works in background - no modal needed */}
  4586. <ManualLotConfirmationModal
  4587. open={manualLotConfirmationOpen}
  4588. onClose={() => {
  4589. setManualLotConfirmationOpen(false);
  4590. }}
  4591. onConfirm={handleManualLotConfirmation}
  4592. expectedLot={expectedLotData}
  4593. scannedLot={scannedLotData}
  4594. isLoading={isConfirmingLot}
  4595. />
  4596. {/* 保留:Lot Confirmation Modal */}
  4597. {lotConfirmationOpen && expectedLotData && scannedLotData && (
  4598. <LotConfirmationModal
  4599. open={lotConfirmationOpen}
  4600. onClose={() => {
  4601. console.log(
  4602. ` [LOT CONFIRM MODAL] Closing modal, reset scanner and release raw-QR dedupe`,
  4603. );
  4604. // 1) Reset scanner buffer first to avoid immediate reopen from buffered same QR.
  4605. if (resetScanRef.current) {
  4606. resetScanRef.current();
  4607. }
  4608. // 2) Close modal state.
  4609. clearLotConfirmationState(false);
  4610. // 3) Release raw-QR dedupe after a short delay so user can re-scan B/C again.
  4611. setTimeout(() => {
  4612. lastProcessedQrRef.current = "";
  4613. processedQrCodesRef.current.clear();
  4614. }, 250);
  4615. }}
  4616. onConfirm={handleLotConfirmation}
  4617. expectedLot={expectedLotData}
  4618. scannedLot={scannedLotData}
  4619. isLoading={isConfirmingLot}
  4620. errorMessage={lotConfirmationError}
  4621. />
  4622. )}
  4623. {/* 保留:Good Pick Execution Form Modal */}
  4624. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  4625. <GoodPickExecutionForm
  4626. open={pickExecutionFormOpen}
  4627. onClose={() => {
  4628. setPickExecutionFormOpen(false);
  4629. setSelectedLotForExecutionForm(null);
  4630. }}
  4631. onSubmit={handlePickExecutionFormSubmit}
  4632. selectedLot={selectedLotForExecutionForm}
  4633. selectedPickOrderLine={{
  4634. id: selectedLotForExecutionForm.pickOrderLineId,
  4635. itemId: selectedLotForExecutionForm.itemId,
  4636. itemCode: selectedLotForExecutionForm.itemCode,
  4637. itemName: selectedLotForExecutionForm.itemName,
  4638. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  4639. availableQty: selectedLotForExecutionForm.availableQty || 0,
  4640. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  4641. // uomCode: selectedLotForExecutionForm.uomCode || '',
  4642. uomDesc: selectedLotForExecutionForm.uomDesc || "",
  4643. pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
  4644. uomShortDesc: selectedLotForExecutionForm.uomShortDesc || "",
  4645. suggestedList: [],
  4646. noLotLines: [],
  4647. }}
  4648. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  4649. pickOrderCreateDate={new Date()}
  4650. />
  4651. )}
  4652. <LotLabelPrintModal
  4653. open={lotLabelPrintModalOpen}
  4654. onClose={() => {
  4655. setLotLabelPrintModalOpen(false);
  4656. setLotLabelPrintReminderText(null);
  4657. }}
  4658. initialPayload={lotLabelPrintInitialPayload}
  4659. defaultPrinterName={defaultLabelPrinterName}
  4660. hideScanSection
  4661. reminderText={lotLabelPrintReminderText ?? undefined}
  4662. statusTitleText="此批號的已用完/已過期"
  4663. warehouseCodePrefixFilter={lotFloorPrefixFilter}
  4664. hideTriggeredLot
  4665. />
  4666. </FormProvider>
  4667. </TestQrCodeProvider>
  4668. );
  4669. };
  4670. export default PickExecution;