FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

2749 lines
104 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. } from "@mui/material";
  21. import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider';
  22. import { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react";
  23. import { useTranslation } from "react-i18next";
  24. import { useRouter } from "next/navigation";
  25. import {
  26. updateStockOutLineStatus,
  27. createStockOutLine,
  28. recordPickExecutionIssue,
  29. fetchFGPickOrders,
  30. FGPickOrderResponse,
  31. autoAssignAndReleasePickOrder,
  32. AutoAssignReleaseResponse,
  33. checkPickOrderCompletion,
  34. PickOrderCompletionResponse,
  35. checkAndCompletePickOrderByConsoCode,
  36. confirmLotSubstitution,
  37. updateStockOutLineStatusByQRCodeAndLotNo,
  38. batchSubmitList,
  39. batchSubmitListRequest,
  40. batchSubmitListLineRequest,
  41. } from "@/app/api/pickOrder/actions";
  42. // 修改:使用 Job Order API
  43. import {
  44. fetchJobOrderLotsHierarchical,
  45. fetchUnassignedJobOrderPickOrders,
  46. assignJobOrderPickOrder,
  47. updateJo,
  48. JobOrderLotsHierarchicalResponse,
  49. } from "@/app/api/jo/actions";
  50. import { fetchNameList, NameList } from "@/app/api/user/actions";
  51. import {
  52. FormProvider,
  53. useForm,
  54. } from "react-hook-form";
  55. import SearchBox, { Criterion } from "../SearchBox";
  56. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  57. import { updateInventoryLotLineQuantities, analyzeQrCode, fetchLotDetail } from "@/app/api/inventory/actions";
  58. import QrCodeIcon from '@mui/icons-material/QrCode';
  59. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  60. import { useSession } from "next-auth/react";
  61. import { SessionWithTokens } from "@/config/authConfig";
  62. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  63. import GoodPickExecutionForm from "./JobPickExecutionForm";
  64. import FGPickOrderCard from "./FGPickOrderCard";
  65. import LotConfirmationModal from "./LotConfirmationModal";
  66. interface Props {
  67. filterArgs: Record<string, any>;
  68. }
  69. // Manual Lot Confirmation Modal Component
  70. const ManualLotConfirmationModal: React.FC<{
  71. open: boolean;
  72. onClose: () => void;
  73. onConfirm: (expectedLotNo: string, scannedLotNo: string) => void;
  74. expectedLot: {
  75. lotNo: string;
  76. itemCode: string;
  77. itemName: string;
  78. } | null;
  79. scannedLot: {
  80. lotNo: string;
  81. itemCode: string;
  82. itemName: string;
  83. } | null;
  84. isLoading?: boolean;
  85. }> = ({ open, onClose, onConfirm, expectedLot, scannedLot, isLoading = false }) => {
  86. const { t } = useTranslation("jo");
  87. const [expectedLotInput, setExpectedLotInput] = useState<string>('');
  88. const [scannedLotInput, setScannedLotInput] = useState<string>('');
  89. const [error, setError] = useState<string>('');
  90. // 当模态框打开时,预填充输入框
  91. useEffect(() => {
  92. if (open) {
  93. setExpectedLotInput(expectedLot?.lotNo || '');
  94. setScannedLotInput(scannedLot?.lotNo || '');
  95. setError('');
  96. }
  97. }, [open, expectedLot, scannedLot]);
  98. const handleConfirm = () => {
  99. if (!expectedLotInput.trim() || !scannedLotInput.trim()) {
  100. setError(t("Please enter both expected and scanned lot numbers."));
  101. return;
  102. }
  103. if (expectedLotInput.trim() === scannedLotInput.trim()) {
  104. setError(t("Expected and scanned lot numbers cannot be the same."));
  105. return;
  106. }
  107. onConfirm(expectedLotInput.trim(), scannedLotInput.trim());
  108. };
  109. return (
  110. <Modal open={open} onClose={onClose}>
  111. <Box sx={{
  112. position: 'absolute',
  113. top: '50%',
  114. left: '50%',
  115. transform: 'translate(-50%, -50%)',
  116. bgcolor: 'background.paper',
  117. p: 3,
  118. borderRadius: 2,
  119. minWidth: 500,
  120. }}>
  121. <Typography variant="h6" gutterBottom color="warning.main">
  122. {t("Manual Lot Confirmation")}
  123. </Typography>
  124. <Box sx={{ mb: 2 }}>
  125. <Typography variant="body2" gutterBottom>
  126. <strong>{t("Expected Lot Number")}:</strong>
  127. </Typography>
  128. <TextField
  129. fullWidth
  130. size="small"
  131. value={expectedLotInput}
  132. onChange={(e) => {
  133. setExpectedLotInput(e.target.value);
  134. setError('');
  135. }}
  136. placeholder={expectedLot?.lotNo || t("Enter expected lot number")}
  137. sx={{ mb: 2 }}
  138. error={!!error && !expectedLotInput.trim()}
  139. />
  140. </Box>
  141. <Box sx={{ mb: 2 }}>
  142. <Typography variant="body2" gutterBottom>
  143. <strong>{t("Scanned Lot Number")}:</strong>
  144. </Typography>
  145. <TextField
  146. fullWidth
  147. size="small"
  148. value={scannedLotInput}
  149. onChange={(e) => {
  150. setScannedLotInput(e.target.value);
  151. setError('');
  152. }}
  153. placeholder={scannedLot?.lotNo || t("Enter scanned lot number")}
  154. sx={{ mb: 2 }}
  155. error={!!error && !scannedLotInput.trim()}
  156. />
  157. </Box>
  158. {error && (
  159. <Box sx={{ mb: 2, p: 1, backgroundColor: '#ffebee', borderRadius: 1 }}>
  160. <Typography variant="body2" color="error">
  161. {error}
  162. </Typography>
  163. </Box>
  164. )}
  165. <Box sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
  166. <Button onClick={onClose} variant="outlined" disabled={isLoading}>
  167. {t("Cancel")}
  168. </Button>
  169. <Button
  170. onClick={handleConfirm}
  171. variant="contained"
  172. color="warning"
  173. disabled={isLoading || !expectedLotInput.trim() || !scannedLotInput.trim()}
  174. >
  175. {isLoading ? t("Processing...") : t("Confirm")}
  176. </Button>
  177. </Box>
  178. </Box>
  179. </Modal>
  180. );
  181. };
  182. // QR Code Modal Component (from GoodPickExecution)
  183. const QrCodeModal: React.FC<{
  184. open: boolean;
  185. onClose: () => void;
  186. lot: any | null;
  187. onQrCodeSubmit: (lotNo: string) => void;
  188. combinedLotData: any[];
  189. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
  190. const { t } = useTranslation("jo");
  191. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  192. const [manualInput, setManualInput] = useState<string>('');
  193. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  194. const [manualInputError, setManualInputError] = useState<boolean>(false);
  195. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  196. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  197. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  198. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  199. const [scannedQrResult, setScannedQrResult] = useState<string>('');
  200. // Process scanned QR codes
  201. useEffect(() => {
  202. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  203. const latestQr = qrValues[qrValues.length - 1];
  204. if (processedQrCodes.has(latestQr)) {
  205. console.log("QR code already processed, skipping...");
  206. return;
  207. }
  208. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  209. try {
  210. const qrData = JSON.parse(latestQr);
  211. if (qrData.stockInLineId && qrData.itemId) {
  212. setIsProcessingQr(true);
  213. setQrScanFailed(false);
  214. fetchStockInLineInfo(qrData.stockInLineId)
  215. .then((stockInLineInfo) => {
  216. console.log("Stock in line info:", stockInLineInfo);
  217. setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
  218. if (stockInLineInfo.lotNo === lot.lotNo) {
  219. console.log(` QR Code verified for lot: ${lot.lotNo}`);
  220. setQrScanSuccess(true);
  221. onQrCodeSubmit(lot.lotNo);
  222. onClose();
  223. resetScan();
  224. } else {
  225. console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
  226. setQrScanFailed(true);
  227. setManualInputError(true);
  228. setManualInputSubmitted(true);
  229. }
  230. })
  231. .catch((error) => {
  232. console.error("Error fetching stock in line info:", error);
  233. setScannedQrResult('Error fetching data');
  234. setQrScanFailed(true);
  235. setManualInputError(true);
  236. setManualInputSubmitted(true);
  237. })
  238. .finally(() => {
  239. setIsProcessingQr(false);
  240. });
  241. } else {
  242. const qrContent = latestQr.replace(/[{}]/g, '');
  243. setScannedQrResult(qrContent);
  244. if (qrContent === lot.lotNo) {
  245. setQrScanSuccess(true);
  246. onQrCodeSubmit(lot.lotNo);
  247. onClose();
  248. resetScan();
  249. } else {
  250. setQrScanFailed(true);
  251. setManualInputError(true);
  252. setManualInputSubmitted(true);
  253. }
  254. }
  255. } catch (error) {
  256. console.log("QR code is not JSON format, trying direct comparison");
  257. const qrContent = latestQr.replace(/[{}]/g, '');
  258. setScannedQrResult(qrContent);
  259. if (qrContent === lot.lotNo) {
  260. setQrScanSuccess(true);
  261. onQrCodeSubmit(lot.lotNo);
  262. onClose();
  263. resetScan();
  264. } else {
  265. setQrScanFailed(true);
  266. setManualInputError(true);
  267. setManualInputSubmitted(true);
  268. }
  269. }
  270. }
  271. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]);
  272. // Clear states when modal opens
  273. useEffect(() => {
  274. if (open) {
  275. setManualInput('');
  276. setManualInputSubmitted(false);
  277. setManualInputError(false);
  278. setIsProcessingQr(false);
  279. setQrScanFailed(false);
  280. setQrScanSuccess(false);
  281. setScannedQrResult('');
  282. setProcessedQrCodes(new Set());
  283. }
  284. }, [open]);
  285. useEffect(() => {
  286. if (lot) {
  287. setManualInput('');
  288. setManualInputSubmitted(false);
  289. setManualInputError(false);
  290. setIsProcessingQr(false);
  291. setQrScanFailed(false);
  292. setQrScanSuccess(false);
  293. setScannedQrResult('');
  294. setProcessedQrCodes(new Set());
  295. }
  296. }, [lot]);
  297. // Auto-submit manual input when it matches
  298. useEffect(() => {
  299. if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
  300. console.log(' Auto-submitting manual input:', manualInput.trim());
  301. const timer = setTimeout(() => {
  302. setQrScanSuccess(true);
  303. onQrCodeSubmit(lot.lotNo);
  304. onClose();
  305. setManualInput('');
  306. setManualInputError(false);
  307. setManualInputSubmitted(false);
  308. }, 200);
  309. return () => clearTimeout(timer);
  310. }
  311. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  312. const handleManualSubmit = () => {
  313. if (manualInput.trim() === lot?.lotNo) {
  314. setQrScanSuccess(true);
  315. onQrCodeSubmit(lot.lotNo);
  316. onClose();
  317. setManualInput('');
  318. } else {
  319. setQrScanFailed(true);
  320. setManualInputError(true);
  321. setManualInputSubmitted(true);
  322. }
  323. };
  324. useEffect(() => {
  325. if (open) {
  326. startScan();
  327. }
  328. }, [open, startScan]);
  329. return (
  330. <Modal open={open} onClose={onClose}>
  331. <Box sx={{
  332. position: 'absolute',
  333. top: '50%',
  334. left: '50%',
  335. transform: 'translate(-50%, -50%)',
  336. bgcolor: 'background.paper',
  337. p: 3,
  338. borderRadius: 2,
  339. minWidth: 400,
  340. }}>
  341. <Typography variant="h6" gutterBottom>
  342. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  343. </Typography>
  344. {isProcessingQr && (
  345. <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
  346. <Typography variant="body2" color="primary">
  347. {t("Processing QR code...")}
  348. </Typography>
  349. </Box>
  350. )}
  351. <Box sx={{ mb: 2 }}>
  352. <Typography variant="body2" gutterBottom>
  353. <strong>{t("Manual Input")}:</strong>
  354. </Typography>
  355. <TextField
  356. fullWidth
  357. size="small"
  358. value={manualInput}
  359. onChange={(e) => {
  360. setManualInput(e.target.value);
  361. if (qrScanFailed || manualInputError) {
  362. setQrScanFailed(false);
  363. setManualInputError(false);
  364. setManualInputSubmitted(false);
  365. }
  366. }}
  367. sx={{ mb: 1 }}
  368. error={manualInputSubmitted && manualInputError}
  369. helperText={
  370. manualInputSubmitted && manualInputError
  371. ? `${t("The input is not the same as the expected lot number.")}`
  372. : ''
  373. }
  374. />
  375. <Button
  376. variant="contained"
  377. onClick={handleManualSubmit}
  378. disabled={!manualInput.trim()}
  379. size="small"
  380. color="primary"
  381. >
  382. {t("Submit")}
  383. </Button>
  384. </Box>
  385. {qrValues.length > 0 && (
  386. <Box sx={{
  387. mb: 2,
  388. p: 2,
  389. backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
  390. borderRadius: 1
  391. }}>
  392. <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
  393. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  394. </Typography>
  395. {qrScanSuccess && (
  396. <Typography variant="caption" color="success" display="block">
  397. {t("Verified successfully!")}
  398. </Typography>
  399. )}
  400. </Box>
  401. )}
  402. <Box sx={{ mt: 2, textAlign: 'right' }}>
  403. <Button onClick={onClose} variant="outlined">
  404. {t("Cancel")}
  405. </Button>
  406. </Box>
  407. </Box>
  408. </Modal>
  409. );
  410. };
  411. const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
  412. const { t } = useTranslation("jo");
  413. const router = useRouter();
  414. const { data: session } = useSession() as { data: SessionWithTokens | null };
  415. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  416. const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null);
  417. // 修改:使用 Job Order 数据结构
  418. const [filteredLotData, setFilteredLotData] = useState<any[]>([]);
  419. const [combinedDataLoading, setCombinedDataLoading] = useState(false);
  420. const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
  421. const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
  422. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  423. const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
  424. const [expectedLotData, setExpectedLotData] = useState<any>(null);
  425. const [scannedLotData, setScannedLotData] = useState<any>(null);
  426. const [isConfirmingLot, setIsConfirmingLot] = useState(false);
  427. const [qrScanInput, setQrScanInput] = useState<string>('');
  428. const [qrScanError, setQrScanError] = useState<boolean>(false);
  429. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  430. const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
  431. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  432. const [paginationController, setPaginationController] = useState({
  433. pageNum: 0,
  434. pageSize: 10,
  435. });
  436. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  437. const initializationRef = useRef(false);
  438. const autoAssignRef = useRef(false);
  439. const formProps = useForm();
  440. const errors = formProps.formState.errors;
  441. const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
  442. // Add QR modal states
  443. const [qrModalOpen, setQrModalOpen] = useState(false);
  444. const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
  445. // Add GoodPickExecutionForm states
  446. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  447. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
  448. const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
  449. const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
  450. // Add these missing state variables
  451. const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
  452. // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling
  453. const [processedQrCombinations, setProcessedQrCombinations] = useState<Map<number, Set<number>>>(new Map());
  454. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  455. const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
  456. const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
  457. // Cache for fetchStockInLineInfo API calls to avoid redundant requests
  458. const stockInLineInfoCache = useRef<Map<number, { lotNo: string | null; timestamp: number }>>(new Map());
  459. const CACHE_TTL = 60000; // 60 seconds cache TTL
  460. const abortControllerRef = useRef<AbortController | null>(null);
  461. const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  462. // Use refs for processed QR tracking to avoid useEffect dependency issues and delays
  463. const processedQrCodesRef = useRef<Set<string>>(new Set());
  464. const lastProcessedQrRef = useRef<string>('');
  465. // Store callbacks in refs to avoid useEffect dependency issues
  466. const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | null>(null);
  467. const resetScanRef = useRef<(() => void) | null>(null);
  468. // Add Manual Lot Confirmation Modal state
  469. const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false);
  470. const getAllLotsFromHierarchical = useCallback((
  471. data: JobOrderLotsHierarchicalResponse | null
  472. ): any[] => {
  473. if (!data || !data.pickOrder || !data.pickOrderLines) {
  474. return [];
  475. }
  476. const allLots: any[] = [];
  477. data.pickOrderLines.forEach((line) => {
  478. if (line.lots && line.lots.length > 0) {
  479. line.lots.forEach((lot) => {
  480. allLots.push({
  481. ...lot,
  482. pickOrderLineId: line.id,
  483. itemId: line.itemId,
  484. itemCode: line.itemCode,
  485. itemName: line.itemName,
  486. uomCode: line.uomCode,
  487. uomDesc: line.uomDesc,
  488. pickOrderLineRequiredQty: line.requiredQty,
  489. pickOrderLineStatus: line.status,
  490. jobOrderId: data.pickOrder.jobOrder.id,
  491. jobOrderCode: data.pickOrder.jobOrder.code,
  492. // 添加 pickOrder 信息(如果需要)
  493. pickOrderId: data.pickOrder.id,
  494. pickOrderCode: data.pickOrder.code,
  495. pickOrderConsoCode: data.pickOrder.consoCode,
  496. pickOrderTargetDate: data.pickOrder.targetDate,
  497. pickOrderType: data.pickOrder.type,
  498. pickOrderStatus: data.pickOrder.status,
  499. pickOrderAssignTo: data.pickOrder.assignTo,
  500. });
  501. });
  502. }
  503. });
  504. return allLots;
  505. }, []);
  506. const allLotsFromData = useMemo(() => {
  507. return getAllLotsFromHierarchical(jobOrderData);
  508. }, [jobOrderData, getAllLotsFromHierarchical]);
  509. // 用于显示的 combinedLotData(支持搜索过滤)
  510. const combinedLotData = useMemo(() => {
  511. return filteredLotData.length > 0 ? filteredLotData : allLotsFromData;
  512. }, [filteredLotData, allLotsFromData]);
  513. // 用于搜索的原始数据
  514. const originalCombinedData = useMemo(() => {
  515. return allLotsFromData;
  516. }, [allLotsFromData]);
  517. // Enhanced lotDataIndexes with cached active lots for better performance
  518. const lotDataIndexes = useMemo(() => {
  519. const indexStartTime = performance.now();
  520. console.log(`⏱️ [PERF] lotDataIndexes calculation START, data length: ${combinedLotData.length}`);
  521. const byItemId = new Map<number, any[]>();
  522. const byItemCode = new Map<string, any[]>();
  523. const byLotId = new Map<number, any>();
  524. const byLotNo = new Map<string, any[]>();
  525. const byStockInLineId = new Map<number, any[]>();
  526. // Cache active lots separately to avoid filtering on every scan
  527. const activeLotsByItemId = new Map<number, any[]>();
  528. const rejectedStatuses = new Set(['rejected']);
  529. // ✅ Use for loop instead of forEach for better performance on tablets
  530. for (let i = 0; i < combinedLotData.length; i++) {
  531. const lot = combinedLotData[i];
  532. const isActive = !rejectedStatuses.has(lot.lotAvailability) &&
  533. !rejectedStatuses.has(lot.stockOutLineStatus) &&
  534. !rejectedStatuses.has(lot.processingStatus);
  535. if (lot.itemId) {
  536. if (!byItemId.has(lot.itemId)) {
  537. byItemId.set(lot.itemId, []);
  538. activeLotsByItemId.set(lot.itemId, []);
  539. }
  540. byItemId.get(lot.itemId)!.push(lot);
  541. if (isActive) {
  542. activeLotsByItemId.get(lot.itemId)!.push(lot);
  543. }
  544. }
  545. if (lot.itemCode) {
  546. if (!byItemCode.has(lot.itemCode)) {
  547. byItemCode.set(lot.itemCode, []);
  548. }
  549. byItemCode.get(lot.itemCode)!.push(lot);
  550. }
  551. if (lot.lotId) {
  552. byLotId.set(lot.lotId, lot);
  553. }
  554. if (lot.lotNo) {
  555. if (!byLotNo.has(lot.lotNo)) {
  556. byLotNo.set(lot.lotNo, []);
  557. }
  558. byLotNo.get(lot.lotNo)!.push(lot);
  559. }
  560. if (lot.stockInLineId) {
  561. if (!byStockInLineId.has(lot.stockInLineId)) {
  562. byStockInLineId.set(lot.stockInLineId, []);
  563. }
  564. byStockInLineId.get(lot.stockInLineId)!.push(lot);
  565. }
  566. }
  567. const indexTime = performance.now() - indexStartTime;
  568. if (indexTime > 10) {
  569. console.log(`⏱️ [PERF] lotDataIndexes calculation END: ${indexTime.toFixed(2)}ms (${(indexTime / 1000).toFixed(3)}s)`);
  570. }
  571. return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId };
  572. }, [combinedLotData.length, combinedLotData]);
  573. // Cached version of fetchStockInLineInfo to avoid redundant API calls
  574. const fetchStockInLineInfoCached = useCallback(async (stockInLineId: number): Promise<{ lotNo: string | null }> => {
  575. const now = Date.now();
  576. const cached = stockInLineInfoCache.current.get(stockInLineId);
  577. // Return cached value if still valid
  578. if (cached && (now - cached.timestamp) < CACHE_TTL) {
  579. console.log(`✅ [CACHE HIT] Using cached stockInLineInfo for ${stockInLineId}`);
  580. return { lotNo: cached.lotNo };
  581. }
  582. // Cancel previous request if exists
  583. if (abortControllerRef.current) {
  584. abortControllerRef.current.abort();
  585. }
  586. // Create new abort controller for this request
  587. const abortController = new AbortController();
  588. abortControllerRef.current = abortController;
  589. try {
  590. console.log(`⏱️ [CACHE MISS] Fetching stockInLineInfo for ${stockInLineId}`);
  591. const stockInLineInfo = await fetchStockInLineInfo(stockInLineId);
  592. // Store in cache
  593. stockInLineInfoCache.current.set(stockInLineId, {
  594. lotNo: stockInLineInfo.lotNo || null,
  595. timestamp: now
  596. });
  597. // Limit cache size to prevent memory leaks
  598. if (stockInLineInfoCache.current.size > 100) {
  599. const firstKey = stockInLineInfoCache.current.keys().next().value;
  600. if (firstKey !== undefined) {
  601. stockInLineInfoCache.current.delete(firstKey);
  602. }
  603. }
  604. return { lotNo: stockInLineInfo.lotNo || null };
  605. } catch (error: any) {
  606. if (error.name === 'AbortError') {
  607. console.log(`⏱️ [CACHE] Request aborted for ${stockInLineId}`);
  608. throw error;
  609. }
  610. console.error(`❌ [CACHE] Error fetching stockInLineInfo for ${stockInLineId}:`, error);
  611. throw error;
  612. }
  613. }, []);
  614. // 修改:加载未分配的 Job Order 订单
  615. const loadUnassignedOrders = useCallback(async () => {
  616. setIsLoadingUnassigned(true);
  617. try {
  618. const orders = await fetchUnassignedJobOrderPickOrders();
  619. setUnassignedOrders(orders);
  620. } catch (error) {
  621. console.error("Error loading unassigned orders:", error);
  622. } finally {
  623. setIsLoadingUnassigned(false);
  624. }
  625. }, []);
  626. // 修改:分配订单给当前用户
  627. const handleAssignOrder = useCallback(async (pickOrderId: number) => {
  628. if (!currentUserId) {
  629. console.error("Missing user id in session");
  630. return;
  631. }
  632. try {
  633. const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
  634. if (result.message === "Successfully assigned") {
  635. console.log(" Successfully assigned pick order");
  636. // 刷新数据
  637. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  638. // 重新加载未分配订单列表
  639. loadUnassignedOrders();
  640. } else {
  641. console.warn("⚠️ Assignment failed:", result.message);
  642. alert(`Assignment failed: ${result.message}`);
  643. }
  644. } catch (error) {
  645. console.error("❌ Error assigning order:", error);
  646. alert("Error occurred during assignment");
  647. }
  648. }, [currentUserId, loadUnassignedOrders]);
  649. const fetchFgPickOrdersData = useCallback(async () => {
  650. if (!currentUserId) return;
  651. setFgPickOrdersLoading(true);
  652. try {
  653. // Get all pick order IDs from combinedLotData
  654. const pickOrderIds = Array.from(new Set(combinedLotData.map(lot => lot.pickOrderId)));
  655. if (pickOrderIds.length === 0) {
  656. setFgPickOrders([]);
  657. return;
  658. }
  659. // Fetch FG pick orders for each pick order ID
  660. const fgPickOrdersPromises = pickOrderIds.map(pickOrderId =>
  661. fetchFGPickOrders(pickOrderId)
  662. );
  663. const fgPickOrdersResults = await Promise.all(fgPickOrdersPromises);
  664. // Flatten the results (each fetchFGPickOrders returns an array)
  665. const allFgPickOrders = fgPickOrdersResults.flat();
  666. setFgPickOrders(allFgPickOrders);
  667. console.log(" Fetched FG pick orders:", allFgPickOrders);
  668. } catch (error) {
  669. console.error("❌ Error fetching FG pick orders:", error);
  670. setFgPickOrders([]);
  671. } finally {
  672. setFgPickOrdersLoading(false);
  673. }
  674. }, [currentUserId, combinedLotData]);
  675. useEffect(() => {
  676. if (combinedLotData.length > 0) {
  677. fetchFgPickOrdersData();
  678. }
  679. }, [combinedLotData, fetchFgPickOrdersData]);
  680. // Handle QR code button click
  681. const handleQrCodeClick = (pickOrderId: number) => {
  682. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  683. // TODO: Implement QR code functionality
  684. };
  685. const getPickOrderId = useCallback(() => {
  686. return filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  687. }, [filterArgs?.pickOrderId]);
  688. // 修改:使用 Job Order API 获取数据
  689. const fetchJobOrderData = useCallback(async (userId?: number) => {
  690. setCombinedDataLoading(true);
  691. try {
  692. const userIdToUse = userId || currentUserId;
  693. if (!userIdToUse) {
  694. console.warn("⚠️ No userId available, skipping API call");
  695. setJobOrderData(null);
  696. return;
  697. }
  698. window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
  699. detail: {
  700. hasData: false,
  701. tabIndex: 0
  702. }
  703. }));
  704. // 直接使用类型化的响应
  705. const jobOrderData = await fetchJobOrderLotsHierarchical(userIdToUse);
  706. console.log(" Job Order data (hierarchical):", jobOrderData);
  707. setJobOrderData(jobOrderData);
  708. // 使用辅助函数获取所有 lots(用于计算完成状态等)
  709. const allLots = getAllLotsFromHierarchical(jobOrderData);
  710. setFilteredLotData(allLots);
  711. const hasData = allLots.length > 0;
  712. window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
  713. detail: {
  714. hasData: hasData,
  715. tabIndex: 0
  716. }
  717. }));
  718. // 计算完成状态
  719. const allCompleted = allLots.length > 0 && allLots.every((lot) =>
  720. lot.processingStatus === 'completed'
  721. );
  722. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  723. detail: {
  724. allLotsCompleted: allCompleted,
  725. tabIndex: 0
  726. }
  727. }));
  728. } catch (error) {
  729. console.error("❌ Error fetching job order data:", error);
  730. setJobOrderData(null);
  731. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  732. detail: {
  733. allLotsCompleted: false,
  734. tabIndex: 0
  735. }
  736. }));
  737. } finally {
  738. setCombinedDataLoading(false);
  739. }
  740. }, [currentUserId, getAllLotsFromHierarchical]);
  741. // 修改:初始化时加载数据
  742. useEffect(() => {
  743. if (session && currentUserId && !initializationRef.current) {
  744. console.log(" Session loaded, initializing job order...");
  745. initializationRef.current = true;
  746. // 加载 Job Order 数据
  747. fetchJobOrderData();
  748. // 加载未分配订单
  749. loadUnassignedOrders();
  750. }
  751. }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]);
  752. // Auto-start scanner only once on mount
  753. const scannerInitializedRef = useRef(false);
  754. // Separate effect for auto-starting scanner (only once, prevents multiple resets)
  755. useEffect(() => {
  756. if (session && currentUserId && !scannerInitializedRef.current) {
  757. scannerInitializedRef.current = true;
  758. // ✅ Auto-start scanner on mount for tablet use (background mode - no modal)
  759. console.log("✅ Auto-starting QR scanner in background mode");
  760. setIsManualScanning(true);
  761. startScan();
  762. }
  763. }, [session, currentUserId, startScan]);
  764. // Add event listener for manual assignment
  765. useEffect(() => {
  766. const handlePickOrderAssigned = () => {
  767. console.log("🔄 Pick order assigned event received, refreshing data...");
  768. fetchJobOrderData();
  769. };
  770. window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
  771. return () => {
  772. window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
  773. };
  774. }, [fetchJobOrderData]);
  775. // Handle QR code submission for matched lot (external scanning)
  776. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  777. console.log(` Processing QR Code for lot: ${lotNo}`);
  778. // Use current data without refreshing to avoid infinite loop
  779. const currentLotData = combinedLotData;
  780. console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
  781. const matchingLots = currentLotData.filter(lot =>
  782. lot.lotNo === lotNo ||
  783. lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
  784. );
  785. if (matchingLots.length === 0) {
  786. console.error(`❌ Lot not found: ${lotNo}`);
  787. setQrScanError(true);
  788. setQrScanSuccess(false);
  789. const availableLotNos = currentLotData.map(lot => lot.lotNo).join(', ');
  790. console.log(`❌ QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`);
  791. return;
  792. }
  793. console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
  794. setQrScanError(false);
  795. try {
  796. let successCount = 0;
  797. let errorCount = 0;
  798. for (const matchingLot of matchingLots) {
  799. console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
  800. if (matchingLot.stockOutLineId) {
  801. const stockOutLineUpdate = await updateStockOutLineStatus({
  802. id: matchingLot.stockOutLineId,
  803. status: 'checked',
  804. qty: 0
  805. });
  806. console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate);
  807. // Treat multiple backend shapes as success (type-safe via any)
  808. const r: any = stockOutLineUpdate as any;
  809. const updateOk =
  810. r?.code === 'SUCCESS' ||
  811. typeof r?.id === 'number' ||
  812. r?.type === 'checked' ||
  813. r?.status === 'checked' ||
  814. typeof r?.entity?.id === 'number' ||
  815. r?.entity?.status === 'checked';
  816. if (updateOk) {
  817. successCount++;
  818. } else {
  819. errorCount++;
  820. }
  821. } else {
  822. const createStockOutLineData = {
  823. consoCode: matchingLot.pickOrderConsoCode,
  824. pickOrderLineId: matchingLot.pickOrderLineId,
  825. inventoryLotLineId: matchingLot.lotId,
  826. qty: 0
  827. };
  828. const createResult = await createStockOutLine(createStockOutLineData);
  829. console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult);
  830. if (createResult && createResult.code === "SUCCESS") {
  831. // Immediately set status to checked for new line
  832. let newSolId: number | undefined;
  833. const anyRes: any = createResult as any;
  834. if (typeof anyRes?.id === 'number') {
  835. newSolId = anyRes.id;
  836. } else if (anyRes?.entity) {
  837. newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id;
  838. }
  839. if (newSolId) {
  840. const setChecked = await updateStockOutLineStatus({
  841. id: newSolId,
  842. status: 'checked',
  843. qty: 0
  844. });
  845. if (setChecked && setChecked.code === "SUCCESS") {
  846. successCount++;
  847. } else {
  848. errorCount++;
  849. }
  850. } else {
  851. console.warn("Created stock out line but no ID returned; cannot set to checked");
  852. errorCount++;
  853. }
  854. } else {
  855. errorCount++;
  856. }
  857. }
  858. }
  859. // FIXED: Set refresh flag before refreshing data
  860. setIsRefreshingData(true);
  861. console.log("🔄 Refreshing data after QR code processing...");
  862. await fetchJobOrderData();
  863. if (successCount > 0) {
  864. console.log(` QR Code processing completed: ${successCount} updated/created`);
  865. setQrScanSuccess(true);
  866. setQrScanError(false);
  867. setQrScanInput(''); // Clear input after successful processing
  868. } else {
  869. console.error(`❌ QR Code processing failed: ${errorCount} errors`);
  870. setQrScanError(true);
  871. setQrScanSuccess(false);
  872. }
  873. } catch (error) {
  874. console.error("❌ Error processing QR code:", error);
  875. setQrScanError(true);
  876. setQrScanSuccess(false);
  877. // Still refresh data even on error
  878. setIsRefreshingData(true);
  879. await fetchJobOrderData();
  880. } finally {
  881. // Clear refresh flag after a short delay
  882. setTimeout(() => {
  883. setIsRefreshingData(false);
  884. }, 1000);
  885. }
  886. }, [combinedLotData, fetchJobOrderData]);
  887. const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => {
  888. const mismatchStartTime = performance.now();
  889. console.log(`⏱️ [HANDLE LOT MISMATCH START]`);
  890. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  891. console.log("Lot mismatch detected:", { expectedLot, scannedLot });
  892. // ✅ Use setTimeout to avoid flushSync warning - schedule modal update in next tick
  893. const setTimeoutStartTime = performance.now();
  894. console.time('setLotConfirmationOpen');
  895. setTimeout(() => {
  896. const setStateStartTime = performance.now();
  897. setExpectedLotData(expectedLot);
  898. setScannedLotData({
  899. ...scannedLot,
  900. lotNo: scannedLot.lotNo || null,
  901. });
  902. setLotConfirmationOpen(true);
  903. const setStateTime = performance.now() - setStateStartTime;
  904. console.timeEnd('setLotConfirmationOpen');
  905. console.log(`⏱️ [HANDLE LOT MISMATCH] Modal state set to open (setState time: ${setStateTime.toFixed(2)}ms)`);
  906. console.log(`✅ [HANDLE LOT MISMATCH] Modal state set to open`);
  907. }, 0);
  908. const setTimeoutTime = performance.now() - setTimeoutStartTime;
  909. console.log(`⏱️ [PERF] setTimeout scheduling time: ${setTimeoutTime.toFixed(2)}ms`);
  910. // ✅ Fetch lotNo in background ONLY for display purposes (using cached version)
  911. if (!scannedLot.lotNo && scannedLot.stockInLineId) {
  912. const stockInLineId = scannedLot.stockInLineId;
  913. if (typeof stockInLineId !== 'number') {
  914. console.warn(`⏱️ [HANDLE LOT MISMATCH] Invalid stockInLineId: ${stockInLineId}`);
  915. return;
  916. }
  917. console.log(`⏱️ [HANDLE LOT MISMATCH] Fetching lotNo in background (stockInLineId: ${stockInLineId})`);
  918. const fetchStartTime = performance.now();
  919. fetchStockInLineInfoCached(stockInLineId)
  920. .then((stockInLineInfo) => {
  921. const fetchTime = performance.now() - fetchStartTime;
  922. console.log(`⏱️ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached time: ${fetchTime.toFixed(2)}ms (${(fetchTime / 1000).toFixed(3)}s)`);
  923. const updateStateStartTime = performance.now();
  924. startTransition(() => {
  925. setScannedLotData((prev: any) => ({
  926. ...prev,
  927. lotNo: stockInLineInfo.lotNo || null,
  928. }));
  929. });
  930. const updateStateTime = performance.now() - updateStateStartTime;
  931. console.log(`⏱️ [PERF] Update scanned lot data time: ${updateStateTime.toFixed(2)}ms`);
  932. const totalTime = performance.now() - mismatchStartTime;
  933. console.log(`⏱️ [HANDLE LOT MISMATCH] Background fetch completed: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  934. })
  935. .catch((error) => {
  936. if (error.name !== 'AbortError') {
  937. const fetchTime = performance.now() - fetchStartTime;
  938. console.error(`❌ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached failed after ${fetchTime.toFixed(2)}ms:`, error);
  939. }
  940. });
  941. } else {
  942. const totalTime = performance.now() - mismatchStartTime;
  943. console.log(`⏱️ [HANDLE LOT MISMATCH END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  944. }
  945. }, [fetchStockInLineInfoCached]);
  946. // Add handleLotConfirmation function
  947. const handleLotConfirmation = useCallback(async () => {
  948. if (!expectedLotData || !scannedLotData || !selectedLotForQr) return;
  949. setIsConfirmingLot(true);
  950. try {
  951. const newLotNo = scannedLotData?.lotNo;
  952. const newStockInLineId = scannedLotData?.stockInLineId;
  953. await confirmLotSubstitution({
  954. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  955. stockOutLineId: selectedLotForQr.stockOutLineId,
  956. originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId,
  957. newInventoryLotNo: "",
  958. newStockInLineId: newStockInLineId
  959. });
  960. setQrScanError(false);
  961. setQrScanSuccess(false);
  962. setQrScanInput('');
  963. // ✅ 修复:在确认后重置扫描状态,避免重复处理
  964. resetScan();
  965. setPickExecutionFormOpen(false);
  966. if(selectedLotForQr?.stockOutLineId){
  967. const stockOutLineUpdate = await updateStockOutLineStatus({
  968. id: selectedLotForQr.stockOutLineId,
  969. status: 'checked',
  970. qty: 0
  971. });
  972. }
  973. // ✅ 修复:先关闭 modal 和清空状态,再刷新数据
  974. setLotConfirmationOpen(false);
  975. setExpectedLotData(null);
  976. setScannedLotData(null);
  977. setSelectedLotForQr(null);
  978. // ✅ 修复:刷新数据前设置刷新标志,避免在刷新期间处理新的 QR code
  979. setIsRefreshingData(true);
  980. await fetchJobOrderData();
  981. setIsRefreshingData(false);
  982. } catch (error) {
  983. console.error("Error confirming lot substitution:", error);
  984. } finally {
  985. setIsConfirmingLot(false);
  986. }
  987. }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData, resetScan]);
  988. const handleManualLotConfirmation = useCallback(async (currentLotNo: string, newLotNo: string) => {
  989. console.log(` Manual lot confirmation: Current=${currentLotNo}, New=${newLotNo}`);
  990. // 使用第一个输入框的 lot number 查找当前数据
  991. const currentLot = combinedLotData.find(lot =>
  992. lot.lotNo && lot.lotNo === currentLotNo
  993. );
  994. if (!currentLot) {
  995. console.error(`❌ Current lot not found: ${currentLotNo}`);
  996. alert(t("Current lot number not found. Please verify and try again."));
  997. return;
  998. }
  999. if (!currentLot.stockOutLineId) {
  1000. console.error("❌ No stockOutLineId found for current lot");
  1001. alert(t("No stock out line found for current lot. Please contact administrator."));
  1002. return;
  1003. }
  1004. setIsConfirmingLot(true);
  1005. try {
  1006. // 调用 updateStockOutLineStatusByQRCodeAndLotNo API
  1007. // 第一个 lot 用于获取 pickOrderLineId, stockOutLineId, itemId
  1008. // 第二个 lot 作为 inventoryLotNo
  1009. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1010. pickOrderLineId: currentLot.pickOrderLineId,
  1011. inventoryLotNo: newLotNo, // 第二个输入框的值
  1012. stockOutLineId: currentLot.stockOutLineId,
  1013. itemId: currentLot.itemId,
  1014. status: "checked",
  1015. });
  1016. console.log("📥 updateStockOutLineStatusByQRCodeAndLotNo result:", res);
  1017. if (res.code === "checked" || res.code === "SUCCESS") {
  1018. // ✅ 更新本地状态
  1019. const entity = res.entity as any;
  1020. if (filteredLotData.length > 0) {
  1021. setFilteredLotData(prev => prev.map(lot => {
  1022. if (lot.stockOutLineId === currentLot.stockOutLineId &&
  1023. lot.pickOrderLineId === currentLot.pickOrderLineId) {
  1024. return {
  1025. ...lot,
  1026. stockOutLineStatus: 'checked',
  1027. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  1028. };
  1029. }
  1030. return lot;
  1031. }));
  1032. }
  1033. console.log("✅ Lot substitution completed successfully");
  1034. setQrScanSuccess(true);
  1035. setQrScanError(false);
  1036. // 关闭手动输入模态框
  1037. setManualLotConfirmationOpen(false);
  1038. // 刷新数据
  1039. await fetchJobOrderData();
  1040. } else if (res.code === "LOT_NUMBER_MISMATCH") {
  1041. console.warn("⚠️ Backend reported LOT_NUMBER_MISMATCH:", res.message);
  1042. // ✅ 打开 lot confirmation modal 而不是显示 alert
  1043. const expectedLotNo = currentLotNo; // 当前 lot 是期望的
  1044. // 查找新 lot 的信息(如果存在于 combinedLotData 中)
  1045. const newLot = combinedLotData.find(lot =>
  1046. lot.lotNo && lot.lotNo === newLotNo
  1047. );
  1048. // 设置 expected lot data
  1049. setExpectedLotData({
  1050. lotNo: expectedLotNo,
  1051. itemCode: currentLot.itemCode || '',
  1052. itemName: currentLot.itemName || ''
  1053. });
  1054. // 设置 scanned lot data
  1055. setScannedLotData({
  1056. lotNo: newLotNo,
  1057. itemCode: newLot?.itemCode || currentLot.itemCode || '',
  1058. itemName: newLot?.itemName || currentLot.itemName || '',
  1059. inventoryLotLineId: newLot?.lotId || null,
  1060. stockInLineId: null // 手动输入时可能没有 stockInLineId
  1061. });
  1062. // 设置 selectedLotForQr 为当前 lot
  1063. setSelectedLotForQr(currentLot);
  1064. // 关闭手动输入模态框
  1065. setManualLotConfirmationOpen(false);
  1066. // 打开 lot confirmation modal
  1067. setLotConfirmationOpen(true);
  1068. setQrScanError(false); // 不显示错误,因为会打开确认模态框
  1069. setQrScanSuccess(false);
  1070. } else if (res.code === "ITEM_MISMATCH") {
  1071. console.warn("⚠️ Backend reported ITEM_MISMATCH:", res.message);
  1072. alert(t("Item mismatch: {message}", { message: res.message || "" }));
  1073. setQrScanError(true);
  1074. setQrScanSuccess(false);
  1075. // 关闭手动输入模态框
  1076. setManualLotConfirmationOpen(false);
  1077. } else {
  1078. console.warn("⚠️ Unexpected response code:", res.code);
  1079. alert(t("Failed to update lot status. Response: {code}", { code: res.code }));
  1080. setQrScanError(true);
  1081. setQrScanSuccess(false);
  1082. // 关闭手动输入模态框
  1083. setManualLotConfirmationOpen(false);
  1084. }
  1085. } catch (error) {
  1086. console.error("❌ Error in manual lot confirmation:", error);
  1087. alert(t("Failed to confirm lot substitution. Please try again."));
  1088. setQrScanError(true);
  1089. setQrScanSuccess(false);
  1090. // 关闭手动输入模态框
  1091. setManualLotConfirmationOpen(false);
  1092. } finally {
  1093. setIsConfirmingLot(false);
  1094. }
  1095. }, [combinedLotData, filteredLotData, fetchJobOrderData, t]);
  1096. const handleFastQrScan = useCallback(async (lotNo: string) => {
  1097. const startTime = performance.now();
  1098. console.log(`⏱️ [FAST SCAN START] Lot: ${lotNo}`);
  1099. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  1100. // 从 combinedLotData 中找到对应的 lot
  1101. const findStartTime = performance.now();
  1102. const matchingLot = combinedLotData.find(lot =>
  1103. lot.lotNo && lot.lotNo === lotNo
  1104. );
  1105. const findTime = performance.now() - findStartTime;
  1106. console.log(`⏱️ Find lot time: ${findTime.toFixed(2)}ms`);
  1107. if (!matchingLot || !matchingLot.stockOutLineId) {
  1108. const totalTime = performance.now() - startTime;
  1109. console.warn(`⚠️ Fast scan: Lot ${lotNo} not found or no stockOutLineId`);
  1110. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`);
  1111. return;
  1112. }
  1113. try {
  1114. // ✅ 使用快速 API
  1115. const apiStartTime = performance.now();
  1116. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1117. pickOrderLineId: matchingLot.pickOrderLineId,
  1118. inventoryLotNo: lotNo,
  1119. stockOutLineId: matchingLot.stockOutLineId,
  1120. itemId: matchingLot.itemId,
  1121. status: "checked",
  1122. });
  1123. const apiTime = performance.now() - apiStartTime;
  1124. console.log(`⏱️ API call time: ${apiTime.toFixed(2)}ms`);
  1125. if (res.code === "checked" || res.code === "SUCCESS") {
  1126. // ✅ 只更新本地状态,不调用 fetchJobOrderData
  1127. const updateStartTime = performance.now();
  1128. const entity = res.entity as any;
  1129. if (filteredLotData.length > 0) {
  1130. setFilteredLotData(prev => prev.map((lot: any) => {
  1131. if (lot.stockOutLineId === matchingLot.stockOutLineId &&
  1132. lot.pickOrderLineId === matchingLot.pickOrderLineId) {
  1133. return {
  1134. ...lot,
  1135. stockOutLineStatus: 'checked',
  1136. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  1137. };
  1138. }
  1139. return lot;
  1140. }));
  1141. }
  1142. const updateTime = performance.now() - updateStartTime;
  1143. console.log(`⏱️ State update time: ${updateTime.toFixed(2)}ms`);
  1144. const totalTime = performance.now() - startTime;
  1145. console.log(`✅ [FAST SCAN END] Lot: ${lotNo}`);
  1146. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  1147. console.log(`⏰ End time: ${new Date().toISOString()}`);
  1148. } else {
  1149. const totalTime = performance.now() - startTime;
  1150. console.warn(`⚠️ Fast scan failed for ${lotNo}:`, res.code);
  1151. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`);
  1152. }
  1153. } catch (error) {
  1154. const totalTime = performance.now() - startTime;
  1155. console.error(`❌ Fast scan error for ${lotNo}:`, error);
  1156. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`);
  1157. }
  1158. }, [combinedLotData, filteredLotData]);
  1159. const processOutsideQrCode = useCallback(async (latestQr: string) => {
  1160. const totalStartTime = performance.now();
  1161. console.log(`⏱️ [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`);
  1162. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  1163. // ✅ Measure index access time
  1164. const indexAccessStart = performance.now();
  1165. const indexes = lotDataIndexes; // Access the memoized indexes
  1166. const indexAccessTime = performance.now() - indexAccessStart;
  1167. console.log(`⏱️ [PERF] Index access time: ${indexAccessTime.toFixed(2)}ms`);
  1168. // 1) Parse JSON safely (parse once, reuse)
  1169. const parseStartTime = performance.now();
  1170. let qrData: any = null;
  1171. let parseTime = 0;
  1172. try {
  1173. qrData = JSON.parse(latestQr);
  1174. parseTime = performance.now() - parseStartTime;
  1175. console.log(`⏱️ [PERF] JSON parse time: ${parseTime.toFixed(2)}ms`);
  1176. } catch {
  1177. console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches.");
  1178. startTransition(() => {
  1179. setQrScanError(true);
  1180. setQrScanSuccess(false);
  1181. });
  1182. return;
  1183. }
  1184. try {
  1185. const validationStartTime = performance.now();
  1186. if (!(qrData?.stockInLineId && qrData?.itemId)) {
  1187. console.log("QR JSON missing required fields (itemId, stockInLineId).");
  1188. startTransition(() => {
  1189. setQrScanError(true);
  1190. setQrScanSuccess(false);
  1191. });
  1192. return;
  1193. }
  1194. const validationTime = performance.now() - validationStartTime;
  1195. console.log(`⏱️ [PERF] Validation time: ${validationTime.toFixed(2)}ms`);
  1196. const scannedItemId = qrData.itemId;
  1197. const scannedStockInLineId = qrData.stockInLineId;
  1198. // ✅ Check if this combination was already processed
  1199. const duplicateCheckStartTime = performance.now();
  1200. const itemProcessedSet = processedQrCombinations.get(scannedItemId);
  1201. if (itemProcessedSet?.has(scannedStockInLineId)) {
  1202. const duplicateCheckTime = performance.now() - duplicateCheckStartTime;
  1203. console.log(`⏱️ [SKIP] Already processed combination: itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId} (check time: ${duplicateCheckTime.toFixed(2)}ms)`);
  1204. return;
  1205. }
  1206. const duplicateCheckTime = performance.now() - duplicateCheckStartTime;
  1207. console.log(`⏱️ [PERF] Duplicate check time: ${duplicateCheckTime.toFixed(2)}ms`);
  1208. // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed)
  1209. const lookupStartTime = performance.now();
  1210. const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || [];
  1211. const lookupTime = performance.now() - lookupStartTime;
  1212. console.log(`⏱️ [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${activeSuggestedLots.length} active lots`);
  1213. if (activeSuggestedLots.length === 0) {
  1214. console.error("No active suggested lots found for this item");
  1215. startTransition(() => {
  1216. setQrScanError(true);
  1217. setQrScanSuccess(false);
  1218. });
  1219. return;
  1220. }
  1221. // ✅ OPTIMIZATION: Direct Map lookup for stockInLineId match (O(1))
  1222. const matchStartTime = performance.now();
  1223. let exactMatch: any = null;
  1224. const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || [];
  1225. // Find exact match from stockInLineId index, then verify it's in active lots
  1226. for (let i = 0; i < stockInLineLots.length; i++) {
  1227. const lot = stockInLineLots[i];
  1228. if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) {
  1229. exactMatch = lot;
  1230. break;
  1231. }
  1232. }
  1233. const matchTime = performance.now() - matchStartTime;
  1234. console.log(`⏱️ [PERF] Find exact match time: ${matchTime.toFixed(2)}ms, found: ${exactMatch ? 'yes' : 'no'}`);
  1235. if (exactMatch) {
  1236. // ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认
  1237. console.log(`✅ Exact stockInLineId match found for lot: ${exactMatch.lotNo}`);
  1238. if (!exactMatch.stockOutLineId) {
  1239. console.warn("No stockOutLineId on exactMatch, cannot update status by QR.");
  1240. startTransition(() => {
  1241. setQrScanError(true);
  1242. setQrScanSuccess(false);
  1243. });
  1244. return;
  1245. }
  1246. try {
  1247. const apiStartTime = performance.now();
  1248. console.log(`⏱️ [API CALL START] Calling updateStockOutLineStatusByQRCodeAndLotNo`);
  1249. console.log(`⏰ [API CALL] API start time: ${new Date().toISOString()}`);
  1250. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1251. pickOrderLineId: exactMatch.pickOrderLineId,
  1252. inventoryLotNo: exactMatch.lotNo,
  1253. stockOutLineId: exactMatch.stockOutLineId,
  1254. itemId: exactMatch.itemId,
  1255. status: "checked",
  1256. });
  1257. const apiTime = performance.now() - apiStartTime;
  1258. console.log(`⏱️ [API CALL END] Total API time: ${apiTime.toFixed(2)}ms (${(apiTime / 1000).toFixed(3)}s)`);
  1259. console.log(`⏰ [API CALL] API end time: ${new Date().toISOString()}`);
  1260. if (res.code === "checked" || res.code === "SUCCESS") {
  1261. const entity = res.entity as any;
  1262. // ✅ Batch state updates using startTransition
  1263. const stateUpdateStartTime = performance.now();
  1264. startTransition(() => {
  1265. setQrScanError(false);
  1266. setQrScanSuccess(true);
  1267. // Update filteredLotData if it exists, otherwise update allLotsFromData via refresh
  1268. if (filteredLotData.length > 0) {
  1269. setFilteredLotData(prev => prev.map((lot: any) => {
  1270. if (lot.stockOutLineId === exactMatch.stockOutLineId &&
  1271. lot.pickOrderLineId === exactMatch.pickOrderLineId) {
  1272. return {
  1273. ...lot,
  1274. stockOutLineStatus: 'checked',
  1275. stockOutLineQty: entity?.qty ?? lot.stockOutLineQty,
  1276. };
  1277. }
  1278. return lot;
  1279. }));
  1280. }
  1281. });
  1282. const stateUpdateTime = performance.now() - stateUpdateStartTime;
  1283. console.log(`⏱️ [PERF] State update time: ${stateUpdateTime.toFixed(2)}ms`);
  1284. // Mark this combination as processed
  1285. const markProcessedStartTime = performance.now();
  1286. setProcessedQrCombinations(prev => {
  1287. const newMap = new Map(prev);
  1288. if (!newMap.has(scannedItemId)) {
  1289. newMap.set(scannedItemId, new Set());
  1290. }
  1291. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1292. return newMap;
  1293. });
  1294. const markProcessedTime = performance.now() - markProcessedStartTime;
  1295. console.log(`⏱️ [PERF] Mark processed time: ${markProcessedTime.toFixed(2)}ms`);
  1296. // Refresh data to ensure consistency
  1297. await fetchJobOrderData();
  1298. const totalTime = performance.now() - totalStartTime;
  1299. console.log(`✅ [PROCESS OUTSIDE QR END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  1300. console.log(`⏰ End time: ${new Date().toISOString()}`);
  1301. console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, api=${apiTime.toFixed(2)}ms, stateUpdate=${stateUpdateTime.toFixed(2)}ms, markProcessed=${markProcessedTime.toFixed(2)}ms`);
  1302. console.log("✅ Status updated, data refreshed");
  1303. } else {
  1304. console.warn("Unexpected response code from backend:", res.code);
  1305. startTransition(() => {
  1306. setQrScanError(true);
  1307. setQrScanSuccess(false);
  1308. });
  1309. }
  1310. } catch (e) {
  1311. const totalTime = performance.now() - totalStartTime;
  1312. console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`);
  1313. console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e);
  1314. startTransition(() => {
  1315. setQrScanError(true);
  1316. setQrScanSuccess(false);
  1317. });
  1318. }
  1319. return; // ✅ 直接返回,不需要确认表单
  1320. }
  1321. // ✅ Case 2: itemId 匹配但 stockInLineId 不匹配 - 显示确认表单
  1322. // Check if we should allow reopening (different stockInLineId)
  1323. const mismatchCheckStartTime = performance.now();
  1324. const itemProcessedSet2 = processedQrCombinations.get(scannedItemId);
  1325. if (itemProcessedSet2?.has(scannedStockInLineId)) {
  1326. const mismatchCheckTime = performance.now() - mismatchCheckStartTime;
  1327. console.log(`⏱️ [SKIP] Already processed this exact combination (check time: ${mismatchCheckTime.toFixed(2)}ms)`);
  1328. return;
  1329. }
  1330. const mismatchCheckTime = performance.now() - mismatchCheckStartTime;
  1331. console.log(`⏱️ [PERF] Mismatch check time: ${mismatchCheckTime.toFixed(2)}ms`);
  1332. // 取第一个活跃的 lot 作为期望的 lot
  1333. const expectedLotStartTime = performance.now();
  1334. const expectedLot = activeSuggestedLots[0];
  1335. if (!expectedLot) {
  1336. console.error("Could not determine expected lot for confirmation");
  1337. startTransition(() => {
  1338. setQrScanError(true);
  1339. setQrScanSuccess(false);
  1340. });
  1341. return;
  1342. }
  1343. const expectedLotTime = performance.now() - expectedLotStartTime;
  1344. console.log(`⏱️ [PERF] Get expected lot time: ${expectedLotTime.toFixed(2)}ms`);
  1345. // ✅ 立即打开确认模态框,不等待其他操作
  1346. console.log(`⚠️ Lot mismatch: Expected stockInLineId=${expectedLot.stockInLineId}, Scanned stockInLineId=${scannedStockInLineId}`);
  1347. // Set selected lot immediately (no transition delay)
  1348. const setSelectedLotStartTime = performance.now();
  1349. setSelectedLotForQr(expectedLot);
  1350. const setSelectedLotTime = performance.now() - setSelectedLotStartTime;
  1351. console.log(`⏱️ [PERF] Set selected lot time: ${setSelectedLotTime.toFixed(2)}ms`);
  1352. // ✅ 获取扫描的 lot 信息(从 QR 数据中提取,或使用默认值)
  1353. // Call handleLotMismatch immediately - it will open the modal
  1354. const handleMismatchStartTime = performance.now();
  1355. handleLotMismatch(
  1356. {
  1357. lotNo: expectedLot.lotNo,
  1358. itemCode: expectedLot.itemCode,
  1359. itemName: expectedLot.itemName
  1360. },
  1361. {
  1362. lotNo: null, // 扫描的 lotNo 未知,需要从后端获取或显示为未知
  1363. itemCode: expectedLot.itemCode,
  1364. itemName: expectedLot.itemName,
  1365. inventoryLotLineId: null,
  1366. stockInLineId: scannedStockInLineId // ✅ 传递 stockInLineId
  1367. }
  1368. );
  1369. const handleMismatchTime = performance.now() - handleMismatchStartTime;
  1370. console.log(`⏱️ [PERF] Handle mismatch call time: ${handleMismatchTime.toFixed(2)}ms`);
  1371. const totalTime = performance.now() - totalStartTime;
  1372. console.log(`⚠️ [PROCESS OUTSIDE QR MISMATCH] Total time before modal: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  1373. console.log(`⏰ End time: ${new Date().toISOString()}`);
  1374. console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, mismatchCheck=${mismatchCheckTime.toFixed(2)}ms, expectedLot=${expectedLotTime.toFixed(2)}ms, setSelectedLot=${setSelectedLotTime.toFixed(2)}ms, handleMismatch=${handleMismatchTime.toFixed(2)}ms`);
  1375. } catch (error) {
  1376. const totalTime = performance.now() - totalStartTime;
  1377. console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`);
  1378. console.error("Error during QR code processing:", error);
  1379. startTransition(() => {
  1380. setQrScanError(true);
  1381. setQrScanSuccess(false);
  1382. });
  1383. return;
  1384. }
  1385. }, [lotDataIndexes, handleLotMismatch, processedQrCombinations, filteredLotData, fetchJobOrderData]);
  1386. // Store processOutsideQrCode in ref for immediate access (update on every render)
  1387. processOutsideQrCodeRef.current = processOutsideQrCode;
  1388. // Store resetScan in ref for immediate access (update on every render)
  1389. resetScanRef.current = resetScan;
  1390. const handleManualInputSubmit = useCallback(() => {
  1391. if (qrScanInput.trim() !== '') {
  1392. handleQrCodeSubmit(qrScanInput.trim());
  1393. }
  1394. }, [qrScanInput, handleQrCodeSubmit]);
  1395. // Handle QR code submission from modal (internal scanning)
  1396. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  1397. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  1398. console.log(` QR Code verified for lot: ${lotNo}`);
  1399. const requiredQty = selectedLotForQr.requiredQty;
  1400. const lotId = selectedLotForQr.lotId;
  1401. // Create stock out line
  1402. const stockOutLineData: CreateStockOutLine = {
  1403. consoCode: selectedLotForQr.pickOrderConsoCode,
  1404. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  1405. inventoryLotLineId: selectedLotForQr.lotId,
  1406. qty: 0.0
  1407. };
  1408. try {
  1409. await createStockOutLine(stockOutLineData);
  1410. console.log("Stock out line created successfully!");
  1411. // Close modal
  1412. setQrModalOpen(false);
  1413. setSelectedLotForQr(null);
  1414. // Set pick quantity
  1415. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  1416. setTimeout(() => {
  1417. setPickQtyData(prev => ({
  1418. ...prev,
  1419. [lotKey]: requiredQty
  1420. }));
  1421. console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  1422. }, 500);
  1423. // Refresh data
  1424. await fetchJobOrderData();
  1425. } catch (error) {
  1426. console.error("Error creating stock out line:", error);
  1427. }
  1428. }
  1429. }, [selectedLotForQr, fetchJobOrderData]);
  1430. useEffect(() => {
  1431. // Skip if scanner is not active or no data available
  1432. if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) {
  1433. return;
  1434. }
  1435. const qrValuesChangeStartTime = performance.now();
  1436. console.log(`⏱️ [QR VALUES EFFECT] Triggered at: ${new Date().toISOString()}`);
  1437. console.log(`⏱️ [QR VALUES EFFECT] qrValues.length: ${qrValues.length}`);
  1438. console.log(`⏱️ [QR VALUES EFFECT] qrValues:`, qrValues);
  1439. const latestQr = qrValues[qrValues.length - 1];
  1440. console.log(`⏱️ [QR VALUES EFFECT] Latest QR: ${latestQr}`);
  1441. console.log(`⏰ [QR VALUES EFFECT] Latest QR detected at: ${new Date().toISOString()}`);
  1442. // ✅ FIXED: Handle test shortcut {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId
  1443. // Support both formats: {2fitest (2 t's) and {2fittest (3 t's)
  1444. if ((latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) && latestQr.endsWith("}")) {
  1445. // Extract content: remove "{2fitest" or "{2fittest" and "}"
  1446. let content = '';
  1447. if (latestQr.startsWith("{2fittest")) {
  1448. content = latestQr.substring(9, latestQr.length - 1); // Remove "{2fittest" and "}"
  1449. } else if (latestQr.startsWith("{2fitest")) {
  1450. content = latestQr.substring(8, latestQr.length - 1); // Remove "{2fitest" and "}"
  1451. }
  1452. const parts = content.split(',');
  1453. if (parts.length === 2) {
  1454. const itemId = parseInt(parts[0].trim(), 10);
  1455. const stockInLineId = parseInt(parts[1].trim(), 10);
  1456. if (!isNaN(itemId) && !isNaN(stockInLineId)) {
  1457. console.log(
  1458. `%c TEST QR: Detected ${latestQr.substring(0, 9)}... - Simulating QR input (itemId=${itemId}, stockInLineId=${stockInLineId})`,
  1459. "color: purple; font-weight: bold"
  1460. );
  1461. // ✅ Simulate QR code JSON format
  1462. const simulatedQr = JSON.stringify({
  1463. itemId: itemId,
  1464. stockInLineId: stockInLineId
  1465. });
  1466. console.log(`⏱️ [TEST QR] Simulated QR content: ${simulatedQr}`);
  1467. console.log(`⏱️ [TEST QR] Start time: ${new Date().toISOString()}`);
  1468. const testStartTime = performance.now();
  1469. // ✅ Mark as processed FIRST to avoid duplicate processing
  1470. lastProcessedQrRef.current = latestQr;
  1471. processedQrCodesRef.current.add(latestQr);
  1472. if (processedQrCodesRef.current.size > 100) {
  1473. const firstValue = processedQrCodesRef.current.values().next().value;
  1474. if (firstValue !== undefined) {
  1475. processedQrCodesRef.current.delete(firstValue);
  1476. }
  1477. }
  1478. setLastProcessedQr(latestQr);
  1479. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1480. // ✅ Process immediately (bypass QR scanner delay)
  1481. if (processOutsideQrCodeRef.current) {
  1482. processOutsideQrCodeRef.current(simulatedQr).then(() => {
  1483. const testTime = performance.now() - testStartTime;
  1484. console.log(`⏱️ [TEST QR] Total processing time: ${testTime.toFixed(2)}ms (${(testTime / 1000).toFixed(3)}s)`);
  1485. console.log(`⏱️ [TEST QR] End time: ${new Date().toISOString()}`);
  1486. }).catch((error) => {
  1487. const testTime = performance.now() - testStartTime;
  1488. console.error(`❌ [TEST QR] Error after ${testTime.toFixed(2)}ms:`, error);
  1489. });
  1490. }
  1491. // Reset scan
  1492. if (resetScanRef.current) {
  1493. resetScanRef.current();
  1494. }
  1495. const qrValuesChangeTime = performance.now() - qrValuesChangeStartTime;
  1496. console.log(`⏱️ [QR VALUES EFFECT] Test QR handling time: ${qrValuesChangeTime.toFixed(2)}ms`);
  1497. return; // ✅ IMPORTANT: Return early to prevent normal processing
  1498. } else {
  1499. console.warn(`⏱️ [TEST QR] Invalid itemId or stockInLineId: itemId=${parts[0]}, stockInLineId=${parts[1]}`);
  1500. }
  1501. } else {
  1502. console.warn(`⏱️ [TEST QR] Invalid format. Expected {2fitestx,y} or {2fittestx,y}, got: ${latestQr}`);
  1503. }
  1504. }
  1505. // Skip processing if confirmation modals are open
  1506. // BUT: Allow processing if modal was just closed (to allow reopening for different stockInLineId)
  1507. if (lotConfirmationOpen || manualLotConfirmationOpen) {
  1508. // Check if this is a different QR code than what triggered the modal
  1509. const modalTriggerQr = lastProcessedQrRef.current;
  1510. if (latestQr === modalTriggerQr) {
  1511. console.log(`⏱️ [QR PROCESS] Skipping - modal open for same QR: lotConfirmation=${lotConfirmationOpen}, manual=${manualLotConfirmationOpen}`);
  1512. return;
  1513. }
  1514. // If it's a different QR, allow processing (user might have canceled and scanned different lot)
  1515. console.log(`⏱️ [QR PROCESS] Different QR detected while modal open, allowing processing`);
  1516. }
  1517. const qrDetectionStartTime = performance.now();
  1518. console.log(`⏱️ [QR DETECTION] Latest QR detected: ${latestQr?.substring(0, 50)}...`);
  1519. console.log(`⏰ [QR DETECTION] Detection time: ${new Date().toISOString()}`);
  1520. console.log(`⏱️ [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`);
  1521. // Skip if already processed (use refs to avoid dependency issues and delays)
  1522. const checkProcessedStartTime = performance.now();
  1523. if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) {
  1524. const checkTime = performance.now() - checkProcessedStartTime;
  1525. console.log(`⏱️ [QR PROCESS] Already processed check time: ${checkTime.toFixed(2)}ms`);
  1526. return;
  1527. }
  1528. const checkTime = performance.now() - checkProcessedStartTime;
  1529. console.log(`⏱️ [QR PROCESS] Not processed check time: ${checkTime.toFixed(2)}ms`);
  1530. // Handle special shortcut
  1531. if (latestQr === "{2fic}") {
  1532. console.log(" Detected {2fic} shortcut - opening manual lot confirmation form");
  1533. setManualLotConfirmationOpen(true);
  1534. if (resetScanRef.current) {
  1535. resetScanRef.current();
  1536. }
  1537. lastProcessedQrRef.current = latestQr;
  1538. processedQrCodesRef.current.add(latestQr);
  1539. if (processedQrCodesRef.current.size > 100) {
  1540. const firstValue = processedQrCodesRef.current.values().next().value;
  1541. if (firstValue !== undefined) {
  1542. processedQrCodesRef.current.delete(firstValue);
  1543. }
  1544. }
  1545. setLastProcessedQr(latestQr);
  1546. setProcessedQrCodes(prev => {
  1547. const newSet = new Set(prev);
  1548. newSet.add(latestQr);
  1549. if (newSet.size > 100) {
  1550. const firstValue = newSet.values().next().value;
  1551. if (firstValue !== undefined) {
  1552. newSet.delete(firstValue);
  1553. }
  1554. }
  1555. return newSet;
  1556. });
  1557. return;
  1558. }
  1559. // Process new QR code immediately (background mode - no modal)
  1560. // Check against refs to avoid state update delays
  1561. if (latestQr && latestQr !== lastProcessedQrRef.current) {
  1562. const processingStartTime = performance.now();
  1563. console.log(`⏱️ [QR PROCESS] Starting processing at: ${new Date().toISOString()}`);
  1564. console.log(`⏱️ [QR PROCESS] Time since detection: ${(processingStartTime - qrDetectionStartTime).toFixed(2)}ms`);
  1565. // ✅ Process immediately for better responsiveness
  1566. // Clear any pending debounced processing
  1567. if (qrProcessingTimeoutRef.current) {
  1568. clearTimeout(qrProcessingTimeoutRef.current);
  1569. qrProcessingTimeoutRef.current = null;
  1570. }
  1571. // Log immediately (console.log is synchronous)
  1572. console.log(`⏱️ [QR PROCESS] Processing new QR code with enhanced validation: ${latestQr}`);
  1573. // Update refs immediately (no state update delay) - do this FIRST
  1574. const refUpdateStartTime = performance.now();
  1575. lastProcessedQrRef.current = latestQr;
  1576. processedQrCodesRef.current.add(latestQr);
  1577. if (processedQrCodesRef.current.size > 100) {
  1578. const firstValue = processedQrCodesRef.current.values().next().value;
  1579. if (firstValue !== undefined) {
  1580. processedQrCodesRef.current.delete(firstValue);
  1581. }
  1582. }
  1583. const refUpdateTime = performance.now() - refUpdateStartTime;
  1584. console.log(`⏱️ [QR PROCESS] Ref update time: ${refUpdateTime.toFixed(2)}ms`);
  1585. // Process immediately in background - no modal/form needed, no delays
  1586. // Use ref to avoid dependency issues
  1587. const processCallStartTime = performance.now();
  1588. if (processOutsideQrCodeRef.current) {
  1589. processOutsideQrCodeRef.current(latestQr).then(() => {
  1590. const processCallTime = performance.now() - processCallStartTime;
  1591. const totalProcessingTime = performance.now() - processingStartTime;
  1592. console.log(`⏱️ [QR PROCESS] processOutsideQrCode call time: ${processCallTime.toFixed(2)}ms`);
  1593. console.log(`⏱️ [QR PROCESS] Total processing time: ${totalProcessingTime.toFixed(2)}ms (${(totalProcessingTime / 1000).toFixed(3)}s)`);
  1594. }).catch((error) => {
  1595. const processCallTime = performance.now() - processCallStartTime;
  1596. const totalProcessingTime = performance.now() - processingStartTime;
  1597. console.error(`❌ [QR PROCESS] processOutsideQrCode error after ${processCallTime.toFixed(2)}ms:`, error);
  1598. console.error(`❌ [QR PROCESS] Total processing time before error: ${totalProcessingTime.toFixed(2)}ms`);
  1599. });
  1600. }
  1601. // Update state for UI (but don't block on it)
  1602. const stateUpdateStartTime = performance.now();
  1603. setLastProcessedQr(latestQr);
  1604. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1605. const stateUpdateTime = performance.now() - stateUpdateStartTime;
  1606. console.log(`⏱️ [QR PROCESS] State update time: ${stateUpdateTime.toFixed(2)}ms`);
  1607. const detectionTime = performance.now() - qrDetectionStartTime;
  1608. const totalEffectTime = performance.now() - qrValuesChangeStartTime;
  1609. console.log(`⏱️ [QR DETECTION] Total detection time: ${detectionTime.toFixed(2)}ms`);
  1610. console.log(`⏱️ [QR VALUES EFFECT] Total effect time: ${totalEffectTime.toFixed(2)}ms`);
  1611. }
  1612. return () => {
  1613. if (qrProcessingTimeoutRef.current) {
  1614. clearTimeout(qrProcessingTimeoutRef.current);
  1615. qrProcessingTimeoutRef.current = null;
  1616. }
  1617. };
  1618. }, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]);
  1619. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  1620. if (value === '' || value === null || value === undefined) {
  1621. setPickQtyData(prev => ({
  1622. ...prev,
  1623. [lotKey]: 0
  1624. }));
  1625. return;
  1626. }
  1627. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  1628. if (isNaN(numericValue)) {
  1629. setPickQtyData(prev => ({
  1630. ...prev,
  1631. [lotKey]: 0
  1632. }));
  1633. return;
  1634. }
  1635. setPickQtyData(prev => ({
  1636. ...prev,
  1637. [lotKey]: numericValue
  1638. }));
  1639. }, []);
  1640. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  1641. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  1642. const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
  1643. const checkAndAutoAssignNext = useCallback(async () => {
  1644. if (!currentUserId) return;
  1645. try {
  1646. const completionResponse = await checkPickOrderCompletion(currentUserId);
  1647. if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
  1648. console.log("Found completed pick orders, auto-assigning next...");
  1649. // 移除前端的自动分配逻辑,因为后端已经处理了
  1650. // await handleAutoAssignAndRelease(); // 删除这个函数
  1651. }
  1652. } catch (error) {
  1653. console.error("Error checking pick order completion:", error);
  1654. }
  1655. }, [currentUserId]);
  1656. // Handle submit pick quantity
  1657. const handleSubmitPickQty = useCallback(async (lot: any) => {
  1658. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  1659. const newQty = pickQtyData[lotKey] || 0;
  1660. if (!lot.stockOutLineId) {
  1661. console.error("No stock out line found for this lot");
  1662. return;
  1663. }
  1664. try {
  1665. const currentActualPickQty = lot.actualPickQty || 0;
  1666. const cumulativeQty = currentActualPickQty + newQty;
  1667. let newStatus = 'partially_completed';
  1668. if (cumulativeQty >= lot.requiredQty) {
  1669. newStatus = 'completed';
  1670. }
  1671. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  1672. console.log(`Lot: ${lot.lotNo}`);
  1673. console.log(`Required Qty: ${lot.requiredQty}`);
  1674. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  1675. console.log(`New Submitted Qty: ${newQty}`);
  1676. console.log(`Cumulative Qty: ${cumulativeQty}`);
  1677. console.log(`New Status: ${newStatus}`);
  1678. console.log(`=====================================`);
  1679. await updateStockOutLineStatus({
  1680. id: lot.stockOutLineId,
  1681. status: newStatus,
  1682. qty: cumulativeQty
  1683. });
  1684. if (newQty > 0) {
  1685. await updateInventoryLotLineQuantities({
  1686. inventoryLotLineId: lot.lotId,
  1687. qty: newQty,
  1688. status: 'available',
  1689. operation: 'pick'
  1690. });
  1691. }
  1692. // FIXED: Use the proper API function instead of direct fetch
  1693. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  1694. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1695. try {
  1696. // Use the imported API function instead of direct fetch
  1697. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1698. console.log(` Pick order completion check result:`, completionResponse);
  1699. if (completionResponse.code === "SUCCESS") {
  1700. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1701. } else if (completionResponse.message === "not completed") {
  1702. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1703. } else {
  1704. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1705. }
  1706. } catch (error) {
  1707. console.error("Error checking pick order completion:", error);
  1708. }
  1709. }
  1710. await fetchJobOrderData();
  1711. console.log("Pick quantity submitted successfully!");
  1712. setTimeout(() => {
  1713. checkAndAutoAssignNext();
  1714. }, 1000);
  1715. } catch (error) {
  1716. console.error("Error submitting pick quantity:", error);
  1717. }
  1718. }, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]);
  1719. const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
  1720. console.log('=== handleSubmitPickQtyWithQty called ===');
  1721. console.log('Lot:', lot);
  1722. console.log('submitQty:', submitQty);
  1723. console.log('stockOutLineId:', lot.stockOutLineId);
  1724. if (!lot.stockOutLineId) {
  1725. console.error("No stock out line found for this lot:", lot);
  1726. alert(`Error: No stock out line ID found for lot ${lot.lotNo}. Cannot update status.`);
  1727. return;
  1728. }
  1729. try {
  1730. // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0
  1731. if (submitQty === 0) {
  1732. console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
  1733. console.log(`Lot: ${lot.lotNo}`);
  1734. console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
  1735. console.log(`Setting status to 'completed' with qty: 0`);
  1736. const updateResult = await updateStockOutLineStatus({
  1737. id: lot.stockOutLineId,
  1738. status: 'completed',
  1739. qty: 0
  1740. });
  1741. console.log('Update result:', updateResult);
  1742. if (!updateResult || (updateResult as any).code !== 'SUCCESS') {
  1743. console.error('Failed to update stock out line status:', updateResult);
  1744. throw new Error('Failed to update stock out line status');
  1745. }
  1746. // Check if pick order is completed
  1747. if (lot.pickOrderConsoCode) {
  1748. console.log(` Lot ${lot.lotNo} completed (all zeros), checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1749. try {
  1750. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1751. console.log(` Pick order completion check result:`, completionResponse);
  1752. if (completionResponse.code === "SUCCESS") {
  1753. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1754. } else if (completionResponse.message === "not completed") {
  1755. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1756. } else {
  1757. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1758. }
  1759. } catch (error) {
  1760. console.error("Error checking pick order completion:", error);
  1761. }
  1762. }
  1763. await fetchJobOrderData();
  1764. console.log("All zeros submission completed successfully!");
  1765. setTimeout(() => {
  1766. checkAndAutoAssignNext();
  1767. }, 1000);
  1768. return;
  1769. }
  1770. // Normal case: Calculate cumulative quantity correctly
  1771. const currentActualPickQty = lot.actualPickQty || 0;
  1772. const cumulativeQty = currentActualPickQty + submitQty;
  1773. // Determine status based on cumulative quantity vs required quantity
  1774. let newStatus = 'partially_completed';
  1775. if (cumulativeQty >= lot.requiredQty) {
  1776. newStatus = 'completed';
  1777. } else if (cumulativeQty > 0) {
  1778. newStatus = 'partially_completed';
  1779. } else {
  1780. newStatus = 'checked'; // QR scanned but no quantity submitted yet
  1781. }
  1782. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  1783. console.log(`Lot: ${lot.lotNo}`);
  1784. console.log(`Required Qty: ${lot.requiredQty}`);
  1785. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  1786. console.log(`New Submitted Qty: ${submitQty}`);
  1787. console.log(`Cumulative Qty: ${cumulativeQty}`);
  1788. console.log(`New Status: ${newStatus}`);
  1789. console.log(`=====================================`);
  1790. await updateStockOutLineStatus({
  1791. id: lot.stockOutLineId,
  1792. status: newStatus,
  1793. qty: cumulativeQty
  1794. });
  1795. if (submitQty > 0) {
  1796. await updateInventoryLotLineQuantities({
  1797. inventoryLotLineId: lot.lotId,
  1798. qty: submitQty,
  1799. status: 'available',
  1800. operation: 'pick'
  1801. });
  1802. }
  1803. // Check if pick order is completed when lot status becomes 'completed'
  1804. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  1805. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1806. try {
  1807. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1808. console.log(` Pick order completion check result:`, completionResponse);
  1809. if (completionResponse.code === "SUCCESS") {
  1810. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1811. } else if (completionResponse.message === "not completed") {
  1812. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1813. } else {
  1814. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1815. }
  1816. } catch (error) {
  1817. console.error("Error checking pick order completion:", error);
  1818. }
  1819. }
  1820. await fetchJobOrderData();
  1821. console.log("Pick quantity submitted successfully!");
  1822. setTimeout(() => {
  1823. checkAndAutoAssignNext();
  1824. }, 1000);
  1825. } catch (error) {
  1826. console.error("Error submitting pick quantity:", error);
  1827. }
  1828. }, [fetchJobOrderData, checkAndAutoAssignNext]);
  1829. const handleSubmitAllScanned = useCallback(async () => {
  1830. const scannedLots = combinedLotData.filter(lot =>
  1831. lot.stockOutLineStatus === 'checked'
  1832. );
  1833. if (scannedLots.length === 0) {
  1834. console.log("No scanned items to submit");
  1835. return;
  1836. }
  1837. setIsSubmittingAll(true);
  1838. console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`);
  1839. try {
  1840. // 转换为 batchSubmitList 所需的格式
  1841. const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
  1842. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty || 0;
  1843. const currentActualPickQty = lot.actualPickQty || 0;
  1844. const cumulativeQty = currentActualPickQty + submitQty;
  1845. let newStatus = 'partially_completed';
  1846. if (cumulativeQty >= (lot.requiredQty || 0)) {
  1847. newStatus = 'completed';
  1848. }
  1849. return {
  1850. stockOutLineId: Number(lot.stockOutLineId) || 0,
  1851. pickOrderLineId: Number(lot.pickOrderLineId),
  1852. inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
  1853. requiredQty: Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0),
  1854. actualPickQty: Number(cumulativeQty),
  1855. stockOutLineStatus: newStatus,
  1856. pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
  1857. noLot: Boolean(false) // Job Order 通常都有 lot
  1858. };
  1859. });
  1860. const request: batchSubmitListRequest = {
  1861. userId: currentUserId || 0,
  1862. lines: lines
  1863. };
  1864. // 使用 batchSubmitList API
  1865. const result = await batchSubmitList(request);
  1866. console.log(`📥 Batch submit result:`, result);
  1867. // 刷新数据
  1868. await fetchJobOrderData(); // 或 pickOrderId,根据页面
  1869. if (result && result.code === "SUCCESS") {
  1870. setQrScanSuccess(true);
  1871. setTimeout(() => {
  1872. setQrScanSuccess(false);
  1873. checkAndAutoAssignNext();
  1874. }, 2000);
  1875. } else {
  1876. console.error("Batch submit failed:", result);
  1877. setQrScanError(true);
  1878. }
  1879. } catch (error) {
  1880. console.error("Error submitting all scanned items:", error);
  1881. setQrScanError(true);
  1882. } finally {
  1883. setIsSubmittingAll(false);
  1884. }
  1885. }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId]);
  1886. // Calculate scanned items count
  1887. const scannedItemsCount = useMemo(() => {
  1888. return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length;
  1889. }, [combinedLotData]);
  1890. // Handle reject lot
  1891. const handleRejectLot = useCallback(async (lot: any) => {
  1892. if (!lot.stockOutLineId) {
  1893. console.error("No stock out line found for this lot");
  1894. return;
  1895. }
  1896. try {
  1897. await updateStockOutLineStatus({
  1898. id: lot.stockOutLineId,
  1899. status: 'rejected',
  1900. qty: 0
  1901. });
  1902. await fetchJobOrderData();
  1903. console.log("Lot rejected successfully!");
  1904. setTimeout(() => {
  1905. checkAndAutoAssignNext();
  1906. }, 1000);
  1907. } catch (error) {
  1908. console.error("Error rejecting lot:", error);
  1909. }
  1910. }, [fetchJobOrderData, checkAndAutoAssignNext]);
  1911. // Handle pick execution form
  1912. const handlePickExecutionForm = useCallback((lot: any) => {
  1913. console.log("=== Pick Execution Form ===");
  1914. console.log("Lot data:", lot);
  1915. if (!lot) {
  1916. console.warn("No lot data provided for pick execution form");
  1917. return;
  1918. }
  1919. console.log("Opening pick execution form for lot:", lot.lotNo);
  1920. setSelectedLotForExecutionForm(lot);
  1921. setPickExecutionFormOpen(true);
  1922. console.log("Pick execution form opened for lot ID:", lot.lotId);
  1923. }, []);
  1924. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  1925. try {
  1926. console.log("Pick execution form submitted:", data);
  1927. const issueData = {
  1928. ...data,
  1929. type: "Jo", // Delivery Order Record 类型
  1930. };
  1931. const result = await recordPickExecutionIssue(issueData);
  1932. console.log("Pick execution issue recorded:", result);
  1933. if (result && result.code === "SUCCESS") {
  1934. console.log(" Pick execution issue recorded successfully");
  1935. } else {
  1936. console.error("❌ Failed to record pick execution issue:", result);
  1937. }
  1938. setPickExecutionFormOpen(false);
  1939. setSelectedLotForExecutionForm(null);
  1940. await fetchJobOrderData();
  1941. } catch (error) {
  1942. console.error("Error submitting pick execution form:", error);
  1943. }
  1944. }, [fetchJobOrderData]);
  1945. // Calculate remaining required quantity
  1946. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  1947. const requiredQty = lot.requiredQty || 0;
  1948. const stockOutLineQty = lot.stockOutLineQty || 0;
  1949. return Math.max(0, requiredQty - stockOutLineQty);
  1950. }, []);
  1951. // Search criteria
  1952. const searchCriteria: Criterion<any>[] = [
  1953. {
  1954. label: t("Pick Order Code"),
  1955. paramName: "pickOrderCode",
  1956. type: "text",
  1957. },
  1958. {
  1959. label: t("Item Code"),
  1960. paramName: "itemCode",
  1961. type: "text",
  1962. },
  1963. {
  1964. label: t("Item Name"),
  1965. paramName: "itemName",
  1966. type: "text",
  1967. },
  1968. {
  1969. label: t("Lot No"),
  1970. paramName: "lotNo",
  1971. type: "text",
  1972. },
  1973. ];
  1974. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  1975. setPaginationController(prev => ({
  1976. ...prev,
  1977. pageNum: newPage,
  1978. }));
  1979. }, []);
  1980. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  1981. const newPageSize = parseInt(event.target.value, 10);
  1982. setPaginationController({
  1983. pageNum: 0,
  1984. pageSize: newPageSize,
  1985. });
  1986. }, []);
  1987. // Pagination data with sorting by routerIndex
  1988. const paginatedData = useMemo(() => {
  1989. // Sort by routerIndex first, then by other criteria
  1990. const sortedData = [...combinedLotData].sort((a, b) => {
  1991. const aIndex = a.routerIndex || 0;
  1992. const bIndex = b.routerIndex || 0;
  1993. // Primary sort: by routerIndex
  1994. if (aIndex !== bIndex) {
  1995. return aIndex - bIndex;
  1996. }
  1997. // Secondary sort: by pickOrderCode if routerIndex is the same
  1998. if (a.pickOrderCode !== b.pickOrderCode) {
  1999. return a.pickOrderCode.localeCompare(b.pickOrderCode);
  2000. }
  2001. // Tertiary sort: by lotNo if everything else is the same
  2002. return (a.lotNo || '').localeCompare(b.lotNo || '');
  2003. });
  2004. const startIndex = paginationController.pageNum * paginationController.pageSize;
  2005. const endIndex = startIndex + paginationController.pageSize;
  2006. return sortedData.slice(startIndex, endIndex);
  2007. }, [combinedLotData, paginationController]);
  2008. // Add these functions for manual scanning
  2009. const handleStartScan = useCallback(() => {
  2010. console.log(" Starting manual QR scan...");
  2011. setIsManualScanning(true);
  2012. setProcessedQrCodes(new Set());
  2013. setLastProcessedQr('');
  2014. setQrScanError(false);
  2015. setQrScanSuccess(false);
  2016. startScan();
  2017. }, [startScan]);
  2018. const handleStopScan = useCallback(() => {
  2019. console.log("⏹️ Stopping manual QR scan...");
  2020. setIsManualScanning(false);
  2021. setQrScanError(false);
  2022. setQrScanSuccess(false);
  2023. stopScan();
  2024. resetScan();
  2025. }, [stopScan, resetScan]);
  2026. useEffect(() => {
  2027. return () => {
  2028. // Cleanup when component unmounts (e.g., when switching tabs)
  2029. if (isManualScanning) {
  2030. console.log("🧹 Component unmounting, stopping QR scanner...");
  2031. stopScan();
  2032. resetScan();
  2033. }
  2034. };
  2035. }, [isManualScanning, stopScan, resetScan]);
  2036. useEffect(() => {
  2037. if (isManualScanning && combinedLotData.length === 0) {
  2038. console.log("⏹️ No data available, auto-stopping QR scan...");
  2039. handleStopScan();
  2040. }
  2041. }, [combinedLotData.length, isManualScanning, handleStopScan]);
  2042. // Cleanup effect
  2043. useEffect(() => {
  2044. return () => {
  2045. // Cleanup when component unmounts (e.g., when switching tabs)
  2046. if (isManualScanning) {
  2047. console.log("🧹 Component unmounting, stopping QR scanner...");
  2048. stopScan();
  2049. resetScan();
  2050. }
  2051. };
  2052. }, [isManualScanning, stopScan, resetScan]);
  2053. const getStatusMessage = useCallback((lot: any) => {
  2054. switch (lot.stockOutLineStatus?.toLowerCase()) {
  2055. case 'pending':
  2056. return t("Please finish QR code scan and pick order.");
  2057. case 'checked':
  2058. return t("Please submit the pick order.");
  2059. case 'partially_completed':
  2060. return t("Partial quantity submitted. Please submit more or complete the order.");
  2061. case 'completed':
  2062. return t("Pick order completed successfully!");
  2063. case 'rejected':
  2064. return t("Lot has been rejected and marked as unavailable.");
  2065. case 'unavailable':
  2066. return t("This order is insufficient, please pick another lot.");
  2067. default:
  2068. return t("Please finish QR code scan and pick order.");
  2069. }
  2070. }, [t]);
  2071. return (
  2072. <TestQrCodeProvider
  2073. lotData={combinedLotData}
  2074. onScanLot={handleQrCodeSubmit}
  2075. filterActive={(lot) => (
  2076. lot.lotAvailability !== 'rejected' &&
  2077. lot.stockOutLineStatus !== 'rejected' &&
  2078. lot.stockOutLineStatus !== 'completed'
  2079. )}
  2080. >
  2081. <FormProvider {...formProps}>
  2082. <Stack spacing={2}>
  2083. {/* Job Order Header */}
  2084. {jobOrderData && (
  2085. <Paper sx={{ p: 2 }}>
  2086. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  2087. <Typography variant="subtitle1">
  2088. <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.code || '-'}
  2089. </Typography>
  2090. <Typography variant="subtitle1">
  2091. <strong>{t("Pick Order Code")}:</strong> {jobOrderData.pickOrder?.code || '-'}
  2092. </Typography>
  2093. <Typography variant="subtitle1">
  2094. <strong>{t("Target Date")}:</strong> {jobOrderData.pickOrder?.targetDate || '-'}
  2095. </Typography>
  2096. </Stack>
  2097. </Paper>
  2098. )}
  2099. {/* Combined Lot Table */}
  2100. <Box>
  2101. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
  2102. <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
  2103. {!isManualScanning ? (
  2104. <Button
  2105. variant="contained"
  2106. startIcon={<QrCodeIcon />}
  2107. onClick={handleStartScan}
  2108. color="primary"
  2109. sx={{ minWidth: '120px' }}
  2110. >
  2111. {t("Start QR Scan")}
  2112. </Button>
  2113. ) : (
  2114. <Button
  2115. variant="outlined"
  2116. startIcon={<QrCodeIcon />}
  2117. onClick={handleStopScan}
  2118. color="secondary"
  2119. sx={{ minWidth: '120px' }}
  2120. >
  2121. {t("Stop QR Scan")}
  2122. </Button>
  2123. )}
  2124. {/* ADD THIS: Submit All Scanned Button */}
  2125. <Button
  2126. variant="contained"
  2127. color="success"
  2128. onClick={handleSubmitAllScanned}
  2129. disabled={scannedItemsCount === 0 || isSubmittingAll}
  2130. sx={{ minWidth: '160px' }}
  2131. >
  2132. {isSubmittingAll ? (
  2133. <>
  2134. <CircularProgress size={16} sx={{ mr: 1 }} />
  2135. {t("Submitting...")}
  2136. </>
  2137. ) : (
  2138. `${t("Submit All Scanned")} (${scannedItemsCount})`
  2139. )}
  2140. </Button>
  2141. </Box>
  2142. </Box>
  2143. {qrScanError && !qrScanSuccess && (
  2144. <Alert severity="error" sx={{ mb: 2 }}>
  2145. {t("QR code does not match any item in current orders.")}
  2146. </Alert>
  2147. )}
  2148. {qrScanSuccess && (
  2149. <Alert severity="success" sx={{ mb: 2 }}>
  2150. {t("QR code verified.")}
  2151. </Alert>
  2152. )}
  2153. <TableContainer component={Paper}>
  2154. <Table>
  2155. <TableHead>
  2156. <TableRow>
  2157. <TableCell>{t("Index")}</TableCell>
  2158. <TableCell>{t("Route")}</TableCell>
  2159. <TableCell>{t("Item Code")}</TableCell>
  2160. <TableCell>{t("Item Name")}</TableCell>
  2161. <TableCell>{t("Lot No")}</TableCell>
  2162. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  2163. <TableCell align="center">{t("Scan Result")}</TableCell>
  2164. <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
  2165. </TableRow>
  2166. </TableHead>
  2167. <TableBody>
  2168. {paginatedData.length === 0 ? (
  2169. <TableRow>
  2170. <TableCell colSpan={8} align="center">
  2171. <Typography variant="body2" color="text.secondary">
  2172. {t("No data available")}
  2173. </Typography>
  2174. </TableCell>
  2175. </TableRow>
  2176. ) : (
  2177. paginatedData.map((lot, index) => (
  2178. <TableRow
  2179. key={`${lot.pickOrderLineId}-${lot.lotId}`}
  2180. sx={{
  2181. backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
  2182. opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
  2183. '& .MuiTableCell-root': {
  2184. color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
  2185. }
  2186. }}
  2187. >
  2188. <TableCell>
  2189. <Typography variant="body2" fontWeight="bold">
  2190. {index + 1}
  2191. </Typography>
  2192. </TableCell>
  2193. <TableCell>
  2194. <Typography variant="body2">
  2195. {lot.routerRoute || '-'}
  2196. </Typography>
  2197. </TableCell>
  2198. <TableCell>{lot.itemCode}</TableCell>
  2199. <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
  2200. <TableCell>
  2201. <Box>
  2202. <Typography
  2203. sx={{
  2204. color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
  2205. opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1
  2206. }}
  2207. >
  2208. {lot.lotNo}
  2209. </Typography>
  2210. </Box>
  2211. </TableCell>
  2212. <TableCell align="right">
  2213. {(() => {
  2214. const requiredQty = lot.requiredQty || 0;
  2215. return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
  2216. })()}
  2217. </TableCell>
  2218. <TableCell align="center">
  2219. {lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? (
  2220. <Box sx={{
  2221. display: 'flex',
  2222. justifyContent: 'center',
  2223. alignItems: 'center',
  2224. width: '100%',
  2225. height: '100%'
  2226. }}>
  2227. <Checkbox
  2228. checked={lot.stockOutLineStatus?.toLowerCase() !== 'pending'}
  2229. disabled={true}
  2230. readOnly={true}
  2231. size="large"
  2232. sx={{
  2233. color: lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? 'success.main' : 'grey.400',
  2234. '&.Mui-checked': {
  2235. color: 'success.main',
  2236. },
  2237. transform: 'scale(1.3)',
  2238. '& .MuiSvgIcon-root': {
  2239. fontSize: '1.5rem',
  2240. }
  2241. }}
  2242. />
  2243. </Box>
  2244. ) : null}
  2245. </TableCell>
  2246. <TableCell align="center">
  2247. <Box sx={{ display: 'flex', justifyContent: 'center' }}>
  2248. <Stack direction="row" spacing={1} alignItems="center">
  2249. <Button
  2250. variant="contained"
  2251. onClick={() => {
  2252. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  2253. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  2254. // Submit with default lot required pick qty
  2255. handlePickQtyChange(lotKey, submitQty);
  2256. handleSubmitPickQtyWithQty(lot, submitQty);
  2257. }}
  2258. disabled={
  2259. (lot.lotAvailability === 'expired' ||
  2260. lot.lotAvailability === 'status_unavailable' ||
  2261. lot.lotAvailability === 'rejected') ||
  2262. lot.stockOutLineStatus === 'completed' ||
  2263. lot.stockOutLineStatus === 'pending' // Disable when QR scan not passed
  2264. }
  2265. sx={{
  2266. fontSize: '0.75rem',
  2267. py: 0.5,
  2268. minHeight: '28px',
  2269. minWidth: '70px'
  2270. }}
  2271. >
  2272. {t("Submit")}
  2273. </Button>
  2274. <Button
  2275. variant="outlined"
  2276. size="small"
  2277. onClick={() => handlePickExecutionForm(lot)}
  2278. disabled={
  2279. // ✅ align with GoodPickExecutiondetail: Edit only disabled when completed
  2280. lot.stockOutLineStatus === 'completed'
  2281. }
  2282. sx={{
  2283. fontSize: '0.7rem',
  2284. py: 0.5,
  2285. minHeight: '28px',
  2286. minWidth: '60px',
  2287. borderColor: 'warning.main',
  2288. color: 'warning.main'
  2289. }}
  2290. title="Report missing or bad items"
  2291. >
  2292. {t("Edit")}
  2293. </Button>
  2294. <Button
  2295. variant="outlined"
  2296. size="small"
  2297. onClick={() => handleSubmitPickQtyWithQty(lot, lot.requiredQty || lot.pickOrderLineRequiredQty || 0)}
  2298. disabled={lot.stockOutLineStatus === 'completed'}
  2299. sx={{
  2300. fontSize: '0.7rem',
  2301. py: 0.5,
  2302. minHeight: '28px',
  2303. minWidth: '90px'
  2304. }}
  2305. >
  2306. {t("Just Complete")}
  2307. </Button>
  2308. </Stack>
  2309. </Box>
  2310. </TableCell>
  2311. </TableRow>
  2312. ))
  2313. )}
  2314. </TableBody>
  2315. </Table>
  2316. </TableContainer>
  2317. <TablePagination
  2318. component="div"
  2319. count={combinedLotData.length}
  2320. page={paginationController.pageNum}
  2321. rowsPerPage={paginationController.pageSize}
  2322. onPageChange={handlePageChange}
  2323. onRowsPerPageChange={handlePageSizeChange}
  2324. rowsPerPageOptions={[10, 25, 50]}
  2325. labelRowsPerPage={t("Rows per page")}
  2326. labelDisplayedRows={({ from, to, count }) =>
  2327. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  2328. }
  2329. />
  2330. </Box>
  2331. </Stack>
  2332. {/* QR Code Modal */}
  2333. {!lotConfirmationOpen && (
  2334. <QrCodeModal
  2335. open={qrModalOpen}
  2336. onClose={() => {
  2337. setQrModalOpen(false);
  2338. setSelectedLotForQr(null);
  2339. stopScan();
  2340. resetScan();
  2341. }}
  2342. lot={selectedLotForQr}
  2343. combinedLotData={combinedLotData}
  2344. onQrCodeSubmit={handleQrCodeSubmitFromModal}
  2345. />
  2346. )}
  2347. {/* Add Lot Confirmation Modal */}
  2348. {lotConfirmationOpen && expectedLotData && scannedLotData && (
  2349. <LotConfirmationModal
  2350. open={lotConfirmationOpen}
  2351. onClose={() => {
  2352. setLotConfirmationOpen(false);
  2353. setExpectedLotData(null);
  2354. setScannedLotData(null);
  2355. setSelectedLotForQr(null);
  2356. }}
  2357. onConfirm={handleLotConfirmation}
  2358. expectedLot={expectedLotData}
  2359. scannedLot={scannedLotData}
  2360. isLoading={isConfirmingLot}
  2361. />
  2362. )}
  2363. {/* Manual Lot Confirmation Modal */}
  2364. <ManualLotConfirmationModal
  2365. open={manualLotConfirmationOpen}
  2366. onClose={() => {
  2367. setManualLotConfirmationOpen(false);
  2368. }}
  2369. onConfirm={handleManualLotConfirmation}
  2370. expectedLot={expectedLotData}
  2371. scannedLot={scannedLotData}
  2372. isLoading={isConfirmingLot}
  2373. />
  2374. {/* Pick Execution Form Modal */}
  2375. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  2376. <GoodPickExecutionForm
  2377. open={pickExecutionFormOpen}
  2378. onClose={() => {
  2379. setPickExecutionFormOpen(false);
  2380. setSelectedLotForExecutionForm(null);
  2381. }}
  2382. onSubmit={handlePickExecutionFormSubmit}
  2383. selectedLot={selectedLotForExecutionForm}
  2384. selectedPickOrderLine={{
  2385. id: selectedLotForExecutionForm.pickOrderLineId,
  2386. itemId: selectedLotForExecutionForm.itemId,
  2387. itemCode: selectedLotForExecutionForm.itemCode,
  2388. itemName: selectedLotForExecutionForm.itemName,
  2389. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  2390. // Add missing required properties from GetPickOrderLineInfo interface
  2391. availableQty: selectedLotForExecutionForm.availableQty || 0,
  2392. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  2393. uomDesc: selectedLotForExecutionForm.uomDesc || '',
  2394. uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
  2395. pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
  2396. suggestedList: [],
  2397. noLotLines: []
  2398. }}
  2399. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  2400. pickOrderCreateDate={new Date()}
  2401. onNormalPickSubmit={async (lot, submitQty) => {
  2402. console.log('onNormalPickSubmit called in newJobPickExecution:', { lot, submitQty });
  2403. if (!lot) {
  2404. console.error('Lot is null or undefined');
  2405. return;
  2406. }
  2407. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  2408. handlePickQtyChange(lotKey, submitQty);
  2409. await handleSubmitPickQtyWithQty(lot, submitQty);
  2410. }}
  2411. />
  2412. )}
  2413. </FormProvider>
  2414. </TestQrCodeProvider>
  2415. );
  2416. };
  2417. export default JobPickExecution