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

2925 строки
111 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. assignJobOrderPickOrder,
  45. fetchJobOrderLotsHierarchicalByPickOrderId,
  46. updateJoPickOrderHandledBy,
  47. JobOrderLotsHierarchicalResponse,
  48. } from "@/app/api/jo/actions";
  49. import { fetchNameList, NameList } from "@/app/api/user/actions";
  50. import {
  51. FormProvider,
  52. useForm,
  53. } from "react-hook-form";
  54. import SearchBox, { Criterion } from "../SearchBox";
  55. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  56. import { updateInventoryLotLineQuantities, analyzeQrCode, fetchLotDetail } from "@/app/api/inventory/actions";
  57. import QrCodeIcon from '@mui/icons-material/QrCode';
  58. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  59. import { useSession } from "next-auth/react";
  60. import { SessionWithTokens } from "@/config/authConfig";
  61. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  62. import GoodPickExecutionForm from "./JobPickExecutionForm";
  63. import FGPickOrderCard from "./FGPickOrderCard";
  64. import LotConfirmationModal from "./LotConfirmationModal";
  65. import LinearProgressWithLabel from "../common/LinearProgressWithLabel";
  66. import ScanStatusAlert from "../common/ScanStatusAlert";
  67. interface Props {
  68. filterArgs: Record<string, any>;
  69. //onSwitchToRecordTab: () => void;
  70. onBackToList?: () => void;
  71. }
  72. // Manual Lot Confirmation Modal (align with GoodPickExecutiondetail, opened by {2fic})
  73. const ManualLotConfirmationModal: React.FC<{
  74. open: boolean;
  75. onClose: () => void;
  76. onConfirm: (expectedLotNo: string, scannedLotNo: string) => void;
  77. expectedLot: { lotNo: string; itemCode: string; itemName: string } | null;
  78. scannedLot: { lotNo: string; itemCode: string; itemName: string } | null;
  79. isLoading?: boolean;
  80. }> = ({ open, onClose, onConfirm, expectedLot, scannedLot, isLoading = false }) => {
  81. const { t } = useTranslation("jo");
  82. const [expectedLotInput, setExpectedLotInput] = useState<string>('');
  83. const [scannedLotInput, setScannedLotInput] = useState<string>('');
  84. const [error, setError] = useState<string>('');
  85. useEffect(() => {
  86. if (open) {
  87. setExpectedLotInput(expectedLot?.lotNo || '');
  88. setScannedLotInput(scannedLot?.lotNo || '');
  89. setError('');
  90. }
  91. }, [open, expectedLot, scannedLot]);
  92. const handleConfirm = () => {
  93. if (!expectedLotInput.trim() || !scannedLotInput.trim()) {
  94. setError(t("Please enter both expected and scanned lot numbers."));
  95. return;
  96. }
  97. if (expectedLotInput.trim() === scannedLotInput.trim()) {
  98. setError(t("Expected and scanned lot numbers cannot be the same."));
  99. return;
  100. }
  101. onConfirm(expectedLotInput.trim(), scannedLotInput.trim());
  102. };
  103. return (
  104. <Modal open={open} onClose={onClose}>
  105. <Box sx={{
  106. position: 'absolute',
  107. top: '50%',
  108. left: '50%',
  109. transform: 'translate(-50%, -50%)',
  110. bgcolor: 'background.paper',
  111. p: 3,
  112. borderRadius: 2,
  113. minWidth: 500,
  114. }}>
  115. <Typography variant="h6" gutterBottom color="warning.main">
  116. {t("Manual Lot Confirmation")}
  117. </Typography>
  118. <Box sx={{ mb: 2 }}>
  119. <Typography variant="body2" gutterBottom>
  120. <strong>{t("Expected Lot Number")}:</strong>
  121. </Typography>
  122. <TextField
  123. fullWidth
  124. size="small"
  125. value={expectedLotInput}
  126. onChange={(e) => { setExpectedLotInput(e.target.value); setError(''); }}
  127. sx={{ mb: 2 }}
  128. error={!!error && !expectedLotInput.trim()}
  129. />
  130. </Box>
  131. <Box sx={{ mb: 2 }}>
  132. <Typography variant="body2" gutterBottom>
  133. <strong>{t("Scanned Lot Number")}:</strong>
  134. </Typography>
  135. <TextField
  136. fullWidth
  137. size="small"
  138. value={scannedLotInput}
  139. onChange={(e) => { setScannedLotInput(e.target.value); setError(''); }}
  140. sx={{ mb: 2 }}
  141. error={!!error && !scannedLotInput.trim()}
  142. />
  143. </Box>
  144. {error && (
  145. <Box sx={{ mb: 2, p: 1, backgroundColor: '#ffebee', borderRadius: 1 }}>
  146. <Typography variant="body2" color="error">
  147. {error}
  148. </Typography>
  149. </Box>
  150. )}
  151. <Box sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
  152. <Button onClick={onClose} variant="outlined" disabled={isLoading}>
  153. {t("Cancel")}
  154. </Button>
  155. <Button
  156. onClick={handleConfirm}
  157. variant="contained"
  158. color="warning"
  159. disabled={isLoading || !expectedLotInput.trim() || !scannedLotInput.trim()}
  160. >
  161. {isLoading ? t("Processing...") : t("Confirm")}
  162. </Button>
  163. </Box>
  164. </Box>
  165. </Modal>
  166. );
  167. };
  168. // QR Code Modal Component (from GoodPickExecution)
  169. const QrCodeModal: React.FC<{
  170. open: boolean;
  171. onClose: () => void;
  172. lot: any | null;
  173. onQrCodeSubmit: (lotNo: string) => void;
  174. combinedLotData: any[];
  175. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
  176. const { t } = useTranslation("jo");
  177. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  178. const [manualInput, setManualInput] = useState<string>('');
  179. const [selectedFloor, setSelectedFloor] = useState<string | null>(null);
  180. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  181. const [manualInputError, setManualInputError] = useState<boolean>(false);
  182. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  183. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  184. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  185. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  186. const [scannedQrResult, setScannedQrResult] = useState<string>('');
  187. // Process scanned QR codes
  188. useEffect(() => {
  189. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  190. const latestQr = qrValues[qrValues.length - 1];
  191. if (processedQrCodes.has(latestQr)) {
  192. console.log("QR code already processed, skipping...");
  193. return;
  194. }
  195. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  196. try {
  197. const qrData = JSON.parse(latestQr);
  198. if (qrData.stockInLineId && qrData.itemId) {
  199. setIsProcessingQr(true);
  200. setQrScanFailed(false);
  201. fetchStockInLineInfo(qrData.stockInLineId)
  202. .then((stockInLineInfo) => {
  203. console.log("Stock in line info:", stockInLineInfo);
  204. setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
  205. if (stockInLineInfo.lotNo === lot.lotNo) {
  206. console.log(` QR Code verified for lot: ${lot.lotNo}`);
  207. setQrScanSuccess(true);
  208. onQrCodeSubmit(lot.lotNo);
  209. onClose();
  210. resetScan();
  211. } else {
  212. console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
  213. setQrScanFailed(true);
  214. setManualInputError(true);
  215. setManualInputSubmitted(true);
  216. }
  217. })
  218. .catch((error) => {
  219. console.error("Error fetching stock in line info:", error);
  220. setScannedQrResult('Error fetching data');
  221. setQrScanFailed(true);
  222. setManualInputError(true);
  223. setManualInputSubmitted(true);
  224. })
  225. .finally(() => {
  226. setIsProcessingQr(false);
  227. });
  228. } else {
  229. const qrContent = latestQr.replace(/[{}]/g, '');
  230. setScannedQrResult(qrContent);
  231. if (qrContent === lot.lotNo) {
  232. setQrScanSuccess(true);
  233. onQrCodeSubmit(lot.lotNo);
  234. onClose();
  235. resetScan();
  236. } else {
  237. setQrScanFailed(true);
  238. setManualInputError(true);
  239. setManualInputSubmitted(true);
  240. }
  241. }
  242. } catch (error) {
  243. console.log("QR code is not JSON format, trying direct comparison");
  244. const qrContent = latestQr.replace(/[{}]/g, '');
  245. setScannedQrResult(qrContent);
  246. if (qrContent === lot.lotNo) {
  247. setQrScanSuccess(true);
  248. onQrCodeSubmit(lot.lotNo);
  249. onClose();
  250. resetScan();
  251. } else {
  252. setQrScanFailed(true);
  253. setManualInputError(true);
  254. setManualInputSubmitted(true);
  255. }
  256. }
  257. }
  258. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]);
  259. // Clear states when modal opens
  260. useEffect(() => {
  261. if (open) {
  262. setManualInput('');
  263. setManualInputSubmitted(false);
  264. setManualInputError(false);
  265. setIsProcessingQr(false);
  266. setQrScanFailed(false);
  267. setQrScanSuccess(false);
  268. setScannedQrResult('');
  269. setProcessedQrCodes(new Set());
  270. }
  271. }, [open]);
  272. useEffect(() => {
  273. if (lot) {
  274. setManualInput('');
  275. setManualInputSubmitted(false);
  276. setManualInputError(false);
  277. setIsProcessingQr(false);
  278. setQrScanFailed(false);
  279. setQrScanSuccess(false);
  280. setScannedQrResult('');
  281. setProcessedQrCodes(new Set());
  282. }
  283. }, [lot]);
  284. // Auto-submit manual input when it matches
  285. useEffect(() => {
  286. if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
  287. console.log(' Auto-submitting manual input:', manualInput.trim());
  288. const timer = setTimeout(() => {
  289. setQrScanSuccess(true);
  290. onQrCodeSubmit(lot.lotNo);
  291. onClose();
  292. setManualInput('');
  293. setManualInputError(false);
  294. setManualInputSubmitted(false);
  295. }, 200);
  296. return () => clearTimeout(timer);
  297. }
  298. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  299. const handleManualSubmit = () => {
  300. if (manualInput.trim() === lot?.lotNo) {
  301. setQrScanSuccess(true);
  302. onQrCodeSubmit(lot.lotNo);
  303. onClose();
  304. setManualInput('');
  305. } else {
  306. setQrScanFailed(true);
  307. setManualInputError(true);
  308. setManualInputSubmitted(true);
  309. }
  310. };
  311. useEffect(() => {
  312. if (open) {
  313. startScan();
  314. }
  315. }, [open, startScan]);
  316. return (
  317. <Modal open={open} onClose={onClose}>
  318. <Box sx={{
  319. position: 'absolute',
  320. top: '50%',
  321. left: '50%',
  322. transform: 'translate(-50%, -50%)',
  323. bgcolor: 'background.paper',
  324. p: 3,
  325. borderRadius: 2,
  326. minWidth: 400,
  327. }}>
  328. <Typography variant="h6" gutterBottom>
  329. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  330. </Typography>
  331. {isProcessingQr && (
  332. <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
  333. <Typography variant="body2" color="primary">
  334. {t("Processing QR code...")}
  335. </Typography>
  336. </Box>
  337. )}
  338. <Box sx={{ mb: 2 }}>
  339. <Typography variant="body2" gutterBottom>
  340. <strong>{t("Manual Input")}:</strong>
  341. </Typography>
  342. <TextField
  343. fullWidth
  344. size="small"
  345. value={manualInput}
  346. onChange={(e) => {
  347. setManualInput(e.target.value);
  348. if (qrScanFailed || manualInputError) {
  349. setQrScanFailed(false);
  350. setManualInputError(false);
  351. setManualInputSubmitted(false);
  352. }
  353. }}
  354. sx={{ mb: 1 }}
  355. error={manualInputSubmitted && manualInputError}
  356. helperText={
  357. manualInputSubmitted && manualInputError
  358. ? `${t("The input is not the same as the expected lot number.")}`
  359. : ''
  360. }
  361. />
  362. <Button
  363. variant="contained"
  364. onClick={handleManualSubmit}
  365. disabled={
  366. !manualInput.trim() ||
  367. lot?.noLot === true ||
  368. !lot?.lotId
  369. }
  370. size="small"
  371. color="primary"
  372. >
  373. {t("Submit")}
  374. </Button>
  375. </Box>
  376. {qrValues.length > 0 && (
  377. <Box sx={{
  378. mb: 2,
  379. p: 2,
  380. backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
  381. borderRadius: 1
  382. }}>
  383. <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
  384. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  385. </Typography>
  386. {qrScanSuccess && (
  387. <Typography variant="caption" color="success" display="block">
  388. {t("Verified successfully!")}
  389. </Typography>
  390. )}
  391. </Box>
  392. )}
  393. <Box sx={{ mt: 2, textAlign: 'right' }}>
  394. <Button onClick={onClose} variant="outlined">
  395. {t("Cancel")}
  396. </Button>
  397. </Box>
  398. </Box>
  399. </Modal>
  400. );
  401. };
  402. const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
  403. const { t } = useTranslation("jo");
  404. const router = useRouter();
  405. const { data: session } = useSession() as { data: SessionWithTokens | null };
  406. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  407. // 修改:使用 Job Order 数据结构
  408. const [combinedDataLoading, setCombinedDataLoading] = useState(false);
  409. // 添加未分配订单状态
  410. const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
  411. const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
  412. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  413. const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
  414. const [expectedLotData, setExpectedLotData] = useState<any>(null);
  415. const [scannedLotData, setScannedLotData] = useState<any>(null);
  416. const [isConfirmingLot, setIsConfirmingLot] = useState(false);
  417. const [qrScanInput, setQrScanInput] = useState<string>('');
  418. const [qrScanError, setQrScanError] = useState<boolean>(false);
  419. const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>('');
  420. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  421. const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null);
  422. const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
  423. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  424. // issue form 里填的 actualPickQty(用于 submit/batch submit 不补拣到 required)
  425. const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState<Record<number, number>>({});
  426. const [localSolStatusById, setLocalSolStatusById] = useState<Record<number, string>>({});
  427. // 防止同一行(以 stockOutLineId/solId 识别)被重复点击提交/完成
  428. const [actionBusyBySolId, setActionBusyBySolId] = useState<Record<number, boolean>>({});
  429. const [paginationController, setPaginationController] = useState({
  430. pageNum: 0,
  431. pageSize: 10,
  432. });
  433. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  434. const initializationRef = useRef(false);
  435. const autoAssignRef = useRef(false);
  436. const formProps = useForm();
  437. const errors = formProps.formState.errors;
  438. const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
  439. // Add QR modal states
  440. const [qrModalOpen, setQrModalOpen] = useState(false);
  441. const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
  442. const [selectedFloor, setSelectedFloor] = useState<string | null>(null);
  443. // Add GoodPickExecutionForm states
  444. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  445. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
  446. const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
  447. const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
  448. // Add these missing state variables
  449. const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
  450. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  451. const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
  452. const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
  453. // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling
  454. const [processedQrCombinations, setProcessedQrCombinations] = useState<Map<number, Set<number>>>(new Map());
  455. // Cache for fetchStockInLineInfo API calls to avoid redundant requests
  456. const stockInLineInfoCache = useRef<Map<number, { lotNo: string | null; timestamp: number }>>(new Map());
  457. const CACHE_TTL = 60000; // 60 seconds cache TTL
  458. const abortControllerRef = useRef<AbortController | null>(null);
  459. const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  460. // Use refs for processed QR tracking to avoid useEffect dependency issues and delays
  461. const processedQrCodesRef = useRef<Set<string>>(new Set());
  462. const lastProcessedQrRef = useRef<string>('');
  463. // Store callbacks in refs to avoid useEffect dependency issues
  464. const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | null>(null);
  465. const resetScanRef = useRef<(() => void) | null>(null);
  466. // Manual lot confirmation modal state (test shortcut {2fic})
  467. const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false);
  468. const getAllLotsFromHierarchical = useCallback((
  469. data: JobOrderLotsHierarchicalResponse | null
  470. ): any[] => {
  471. if (!data || !data.pickOrder || !data.pickOrderLines) {
  472. return [];
  473. }
  474. const allLots: any[] = [];
  475. data.pickOrderLines.forEach((line) => {
  476. // 用来记录这一行已经通过 lots 出现过的 lotId(避免 stockouts 再渲染一次)
  477. const lotIdSet = new Set<number>();
  478. // lots:按 lotId 去重并合并 requiredQty(对齐 GoodPickExecutiondetail)
  479. if (line.lots && line.lots.length > 0) {
  480. const lotMap = new Map<number, any>();
  481. line.lots.forEach((lot: any) => {
  482. const lotId = lot.lotId;
  483. if (lotId == null) return;
  484. if (lotMap.has(lotId)) {
  485. const existingLot = lotMap.get(lotId);
  486. existingLot.requiredQty =
  487. (existingLot.requiredQty || 0) + (lot.requiredQty || 0);
  488. } else {
  489. lotMap.set(lotId, { ...lot });
  490. }
  491. });
  492. lotMap.forEach((lot: any) => {
  493. if (lot.lotId != null) lotIdSet.add(lot.lotId);
  494. allLots.push({
  495. ...lot,
  496. pickOrderLineId: line.id,
  497. itemId: line.itemId,
  498. itemCode: line.itemCode,
  499. itemName: line.itemName,
  500. uomCode: line.uomCode,
  501. uomDesc: line.uomDesc,
  502. itemTotalAvailableQty: line.totalAvailableQty ?? null,
  503. pickOrderLineRequiredQty: line.requiredQty,
  504. pickOrderLineStatus: line.status,
  505. jobOrderId: data.pickOrder.jobOrder.id,
  506. jobOrderCode: data.pickOrder.jobOrder.code,
  507. pickOrderId: data.pickOrder.id,
  508. pickOrderCode: data.pickOrder.code,
  509. pickOrderConsoCode: data.pickOrder.consoCode,
  510. pickOrderTargetDate: data.pickOrder.targetDate,
  511. pickOrderType: data.pickOrder.type,
  512. pickOrderStatus: data.pickOrder.status,
  513. pickOrderAssignTo: data.pickOrder.assignTo,
  514. handler: line.handler,
  515. noLot: false,
  516. });
  517. });
  518. }
  519. // stockouts:用于“无 suggested lot / noLot”场景也显示并可 submit 0 闭环
  520. if (line.stockouts && line.stockouts.length > 0) {
  521. line.stockouts.forEach((stockout: any) => {
  522. const hasLot = stockout.lotId != null;
  523. const lotAlreadyInLots = hasLot && lotIdSet.has(stockout.lotId as number);
  524. // 有批次 & 已经通过 lots 渲染过 → 跳过,避免一条变两行
  525. if (!stockout.noLot && lotAlreadyInLots) {
  526. return;
  527. }
  528. allLots.push({
  529. pickOrderLineId: line.id,
  530. itemId: line.itemId,
  531. itemCode: line.itemCode,
  532. itemName: line.itemName,
  533. uomCode: line.uomCode,
  534. uomDesc: line.uomDesc,
  535. itemTotalAvailableQty: line.totalAvailableQty ?? null,
  536. pickOrderLineRequiredQty: line.requiredQty,
  537. pickOrderLineStatus: line.status,
  538. jobOrderId: data.pickOrder.jobOrder.id,
  539. jobOrderCode: data.pickOrder.jobOrder.code,
  540. pickOrderId: data.pickOrder.id,
  541. pickOrderCode: data.pickOrder.code,
  542. pickOrderConsoCode: data.pickOrder.consoCode,
  543. pickOrderTargetDate: data.pickOrder.targetDate,
  544. pickOrderType: data.pickOrder.type,
  545. pickOrderStatus: data.pickOrder.status,
  546. pickOrderAssignTo: data.pickOrder.assignTo,
  547. handler: line.handler,
  548. lotId: stockout.lotId || null,
  549. lotNo: stockout.lotNo || null,
  550. expiryDate: null,
  551. location: stockout.location || null,
  552. availableQty: stockout.availableQty ?? 0,
  553. requiredQty: line.requiredQty ?? 0,
  554. actualPickQty: stockout.qty ?? 0,
  555. processingStatus: stockout.status || "pending",
  556. lotAvailability: stockout.noLot ? "insufficient_stock" : "available",
  557. suggestedPickLotId: null,
  558. stockOutLineId: stockout.id || null,
  559. stockOutLineQty: stockout.qty ?? 0,
  560. stockOutLineStatus: stockout.status || null,
  561. stockInLineId: null,
  562. routerIndex: stockout.noLot ? 999999 : null,
  563. routerArea: null,
  564. routerRoute: null,
  565. noLot: !!stockout.noLot,
  566. });
  567. });
  568. }
  569. });
  570. return allLots;
  571. }, []);
  572. const extractFloor = (lot: any): string => {
  573. const raw = lot.routerRoute || lot.routerArea || lot.location || '';
  574. const match = raw.match(/^(\d+F?)/i) || raw.split('-')[0];
  575. return (match?.[1] || match || raw || '').toUpperCase().replace(/(\d)F?/i, '$1F');
  576. };
  577. // 楼层排序权重:4F > 3F > 2F(数字越大越靠前)
  578. const floorSortOrder = (floor: string): number => {
  579. const n = parseInt(floor.replace(/\D/g, ''), 10);
  580. return isNaN(n) ? 0 : n;
  581. };
  582. const combinedLotData = useMemo(() => {
  583. const lots = getAllLotsFromHierarchical(jobOrderData);
  584. // 前端覆盖:issue form/submit0 不会立刻改写后端 qty 时,用本地缓存让 UI 与 batch submit 计算一致
  585. return lots.map((lot: any) => {
  586. const solId = Number(lot.stockOutLineId) || 0;
  587. if (solId > 0) {
  588. const hasPickedOverride = Object.prototype.hasOwnProperty.call(issuePickedQtyBySolId, solId);
  589. const picked = Number(issuePickedQtyBySolId[solId] ?? lot.actualPickQty ?? 0);
  590. const statusRaw = localSolStatusById[solId] ?? lot.stockOutLineStatus ?? "";
  591. const status = String(statusRaw).toLowerCase();
  592. const isEnded = status === 'completed' || status === 'rejected';
  593. return {
  594. ...lot,
  595. actualPickQty: hasPickedOverride ? picked : lot.actualPickQty,
  596. stockOutLineQty: hasPickedOverride ? picked : lot.stockOutLineQty,
  597. stockOutLineStatus: isEnded ? statusRaw : (statusRaw || "checked"),
  598. };
  599. }
  600. return lot;
  601. });
  602. }, [jobOrderData, getAllLotsFromHierarchical, issuePickedQtyBySolId, localSolStatusById]);
  603. const originalCombinedData = useMemo(() => {
  604. return getAllLotsFromHierarchical(jobOrderData);
  605. }, [jobOrderData, getAllLotsFromHierarchical]);
  606. // Enhanced lotDataIndexes with cached active lots for better performance (align with GoodPickExecutiondetail)
  607. const lotDataIndexes = useMemo(() => {
  608. const byItemId = new Map<number, any[]>();
  609. const byItemCode = new Map<string, any[]>();
  610. const byLotId = new Map<number, any>();
  611. const byLotNo = new Map<string, any[]>();
  612. const byStockInLineId = new Map<number, any[]>();
  613. const activeLotsByItemId = new Map<number, any[]>();
  614. const rejectedStatuses = new Set(['rejected']);
  615. for (let i = 0; i < combinedLotData.length; i++) {
  616. const lot = combinedLotData[i];
  617. const isActive =
  618. !rejectedStatuses.has(lot.lotAvailability) &&
  619. !rejectedStatuses.has(lot.stockOutLineStatus) &&
  620. !rejectedStatuses.has(lot.processingStatus) &&
  621. lot.stockOutLineStatus !== 'completed';
  622. if (lot.itemId) {
  623. if (!byItemId.has(lot.itemId)) {
  624. byItemId.set(lot.itemId, []);
  625. activeLotsByItemId.set(lot.itemId, []);
  626. }
  627. byItemId.get(lot.itemId)!.push(lot);
  628. if (isActive) activeLotsByItemId.get(lot.itemId)!.push(lot);
  629. }
  630. if (lot.itemCode) {
  631. if (!byItemCode.has(lot.itemCode)) byItemCode.set(lot.itemCode, []);
  632. byItemCode.get(lot.itemCode)!.push(lot);
  633. }
  634. if (lot.lotId) byLotId.set(lot.lotId, lot);
  635. if (lot.lotNo) {
  636. if (!byLotNo.has(lot.lotNo)) byLotNo.set(lot.lotNo, []);
  637. byLotNo.get(lot.lotNo)!.push(lot);
  638. }
  639. if (lot.stockInLineId) {
  640. if (!byStockInLineId.has(lot.stockInLineId)) byStockInLineId.set(lot.stockInLineId, []);
  641. byStockInLineId.get(lot.stockInLineId)!.push(lot);
  642. }
  643. }
  644. return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId };
  645. }, [combinedLotData]);
  646. // Cached version of fetchStockInLineInfo to avoid redundant API calls
  647. const fetchStockInLineInfoCached = useCallback(async (stockInLineId: number): Promise<{ lotNo: string | null }> => {
  648. const now = Date.now();
  649. const cached = stockInLineInfoCache.current.get(stockInLineId);
  650. if (cached && (now - cached.timestamp) < CACHE_TTL) {
  651. return { lotNo: cached.lotNo };
  652. }
  653. if (abortControllerRef.current) abortControllerRef.current.abort();
  654. const abortController = new AbortController();
  655. abortControllerRef.current = abortController;
  656. const stockInLineInfo = await fetchStockInLineInfo(stockInLineId);
  657. stockInLineInfoCache.current.set(stockInLineId, {
  658. lotNo: stockInLineInfo.lotNo || null,
  659. timestamp: now
  660. });
  661. if (stockInLineInfoCache.current.size > 100) {
  662. const firstKey = stockInLineInfoCache.current.keys().next().value;
  663. if (firstKey !== undefined) stockInLineInfoCache.current.delete(firstKey);
  664. }
  665. return { lotNo: stockInLineInfo.lotNo || null };
  666. }, []);
  667. // 修改:加载未分配的 Job Order 订单
  668. const loadUnassignedOrders = useCallback(async () => {
  669. setIsLoadingUnassigned(true);
  670. try {
  671. //const orders = await fetchUnassignedJobOrderPickOrders();
  672. //setUnassignedOrders(orders);
  673. } catch (error) {
  674. console.error("Error loading unassigned orders:", error);
  675. } finally {
  676. setIsLoadingUnassigned(false);
  677. }
  678. }, []);
  679. // 修改:分配订单给当前用户
  680. const handleAssignOrder = useCallback(async (pickOrderId: number) => {
  681. if (!currentUserId) {
  682. console.error("Missing user id in session");
  683. return;
  684. }
  685. try {
  686. const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
  687. if (result.message === "Successfully assigned") {
  688. console.log(" Successfully assigned pick order");
  689. // 刷新数据
  690. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  691. // 重新加载未分配订单列表
  692. loadUnassignedOrders();
  693. } else {
  694. console.warn("⚠️ Assignment failed:", result.message);
  695. alert(`Assignment failed: ${result.message}`);
  696. }
  697. } catch (error) {
  698. console.error("❌ Error assigning order:", error);
  699. alert("Error occurred during assignment");
  700. }
  701. }, [currentUserId, loadUnassignedOrders]);
  702. const fetchFgPickOrdersData = useCallback(async () => {
  703. if (!currentUserId) return;
  704. setFgPickOrdersLoading(true);
  705. try {
  706. // Get all pick order IDs from combinedLotData
  707. const pickOrderIds = Array.from(new Set(combinedLotData.map(lot => lot.pickOrderId)));
  708. if (pickOrderIds.length === 0) {
  709. setFgPickOrders([]);
  710. return;
  711. }
  712. // Fetch FG pick orders for each pick order ID
  713. const fgPickOrdersPromises = pickOrderIds.map(pickOrderId =>
  714. fetchFGPickOrders(pickOrderId)
  715. );
  716. const fgPickOrdersResults = await Promise.all(fgPickOrdersPromises);
  717. // Flatten the results (each fetchFGPickOrders returns an array)
  718. const allFgPickOrders = fgPickOrdersResults.flat();
  719. setFgPickOrders(allFgPickOrders);
  720. console.log(" Fetched FG pick orders:", allFgPickOrders);
  721. } catch (error) {
  722. console.error("❌ Error fetching FG pick orders:", error);
  723. setFgPickOrders([]);
  724. } finally {
  725. setFgPickOrdersLoading(false);
  726. }
  727. }, [currentUserId, combinedLotData]);
  728. useEffect(() => {
  729. if (combinedLotData.length > 0) {
  730. fetchFgPickOrdersData();
  731. }
  732. }, [combinedLotData, fetchFgPickOrdersData]);
  733. // Handle QR code button click
  734. const handleQrCodeClick = (pickOrderId: number) => {
  735. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  736. // TODO: Implement QR code functionality
  737. };
  738. // 修改:使用 Job Order API 获取数据
  739. const fetchJobOrderData = useCallback(async (pickOrderId?: number) => {
  740. setCombinedDataLoading(true);
  741. try {
  742. if (!pickOrderId) {
  743. console.warn("⚠️ No pickOrderId provided, skipping API call");
  744. setJobOrderData(null);
  745. return;
  746. }
  747. // 直接使用类型化的响应
  748. const jobOrderData = await fetchJobOrderLotsHierarchicalByPickOrderId(pickOrderId);
  749. console.log("✅ Job Order data (hierarchical):", jobOrderData);
  750. setJobOrderData(jobOrderData);
  751. // 使用辅助函数获取所有 lots(不再扁平化)
  752. const allLots = getAllLotsFromHierarchical(jobOrderData);
  753. } catch (error) {
  754. console.error("❌ Error fetching job order data:", error);
  755. setJobOrderData(null);
  756. } finally {
  757. setCombinedDataLoading(false);
  758. }
  759. }, [getAllLotsFromHierarchical]);
  760. const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => {
  761. if (!currentUserId || !pickOrderId || !itemId) {
  762. return;
  763. }
  764. try {
  765. console.log(`Updating JoPickOrder.handledBy for pickOrderId: ${pickOrderId}, itemId: ${itemId}, userId: ${currentUserId}`);
  766. await updateJoPickOrderHandledBy({
  767. pickOrderId: pickOrderId,
  768. itemId: itemId,
  769. userId: currentUserId
  770. });
  771. console.log("✅ JoPickOrder.handledBy updated successfully");
  772. } catch (error) {
  773. console.error("❌ Error updating JoPickOrder.handledBy:", error);
  774. // Don't throw - this is not critical for the main flow
  775. }
  776. }, [currentUserId]);
  777. // 修改:初始化时加载数据
  778. useEffect(() => {
  779. if (session && currentUserId && !initializationRef.current) {
  780. console.log("✅ Session loaded, initializing job order...");
  781. initializationRef.current = true;
  782. // Get pickOrderId from filterArgs if available (when viewing from list)
  783. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  784. if (pickOrderId) {
  785. fetchJobOrderData(pickOrderId);
  786. }
  787. loadUnassignedOrders();
  788. }
  789. }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders, filterArgs?.pickOrderId]);
  790. // Add event listener for manual assignment
  791. useEffect(() => {
  792. const handlePickOrderAssigned = () => {
  793. console.log("🔄 Pick order assigned event received, refreshing data...");
  794. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  795. if (pickOrderId) {
  796. fetchJobOrderData(pickOrderId);
  797. }
  798. };
  799. window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
  800. return () => {
  801. window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
  802. };
  803. }, [fetchJobOrderData, filterArgs?.pickOrderId]);
  804. // Handle QR code submission for matched lot (external scanning)
  805. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  806. console.log(` Processing QR Code for lot: ${lotNo}`);
  807. // Use current data without refreshing to avoid infinite loop
  808. const currentLotData = combinedLotData;
  809. console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
  810. const matchingLots = currentLotData.filter(lot =>
  811. lot.lotNo === lotNo ||
  812. lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
  813. );
  814. if (matchingLots.length === 0) {
  815. console.error(`❌ Lot not found: ${lotNo}`);
  816. setQrScanError(true);
  817. setQrScanSuccess(false);
  818. const availableLotNos = currentLotData.map(lot => lot.lotNo).join(', ');
  819. console.log(`❌ QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`);
  820. return;
  821. }
  822. console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
  823. setQrScanError(false);
  824. try {
  825. let successCount = 0;
  826. let errorCount = 0;
  827. for (const matchingLot of matchingLots) {
  828. console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
  829. if (matchingLot.stockOutLineId) {
  830. const stockOutLineUpdate = await updateStockOutLineStatus({
  831. id: matchingLot.stockOutLineId,
  832. status: 'checked',
  833. qty: 0
  834. });
  835. console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate);
  836. // Treat multiple backend shapes as success (type-safe via any)
  837. const r: any = stockOutLineUpdate as any;
  838. const updateOk =
  839. r?.code === 'SUCCESS' ||
  840. typeof r?.id === 'number' ||
  841. r?.type === 'checked' ||
  842. r?.status === 'checked' ||
  843. typeof r?.entity?.id === 'number' ||
  844. r?.entity?.status === 'checked';
  845. if (updateOk) {
  846. successCount++;
  847. } else {
  848. errorCount++;
  849. }
  850. } else {
  851. const createStockOutLineData = {
  852. consoCode: matchingLot.pickOrderConsoCode,
  853. pickOrderLineId: matchingLot.pickOrderLineId,
  854. inventoryLotLineId: matchingLot.lotId,
  855. qty: 0
  856. };
  857. const createResult = await createStockOutLine(createStockOutLineData);
  858. console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult);
  859. if (createResult && createResult.code === "SUCCESS") {
  860. // Immediately set status to checked for new line
  861. let newSolId: number | undefined;
  862. const anyRes: any = createResult as any;
  863. if (typeof anyRes?.id === 'number') {
  864. newSolId = anyRes.id;
  865. } else if (anyRes?.entity) {
  866. newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id;
  867. }
  868. if (newSolId) {
  869. const setChecked = await updateStockOutLineStatus({
  870. id: newSolId,
  871. status: 'checked',
  872. qty: 0
  873. });
  874. if (setChecked && setChecked.code === "SUCCESS") {
  875. successCount++;
  876. } else {
  877. errorCount++;
  878. }
  879. } else {
  880. console.warn("Created stock out line but no ID returned; cannot set to checked");
  881. errorCount++;
  882. }
  883. } else {
  884. errorCount++;
  885. }
  886. }
  887. }
  888. // FIXED: Set refresh flag before refreshing data
  889. setIsRefreshingData(true);
  890. console.log("🔄 Refreshing data after QR code processing...");
  891. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  892. await fetchJobOrderData(pickOrderId);
  893. if (successCount > 0) {
  894. console.log(` QR Code processing completed: ${successCount} updated/created`);
  895. setQrScanSuccess(true);
  896. setQrScanError(false);
  897. setQrScanInput(''); // Clear input after successful processing
  898. } else {
  899. console.error(`❌ QR Code processing failed: ${errorCount} errors`);
  900. setQrScanError(true);
  901. setQrScanSuccess(false);
  902. }
  903. } catch (error) {
  904. console.error("❌ Error processing QR code:", error);
  905. setQrScanError(true);
  906. setQrScanSuccess(false);
  907. // Still refresh data even on error
  908. setIsRefreshingData(true);
  909. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  910. await fetchJobOrderData( pickOrderId);
  911. } finally {
  912. // Clear refresh flag after a short delay
  913. setTimeout(() => {
  914. setIsRefreshingData(false);
  915. }, 1000);
  916. }
  917. }, [combinedLotData, fetchJobOrderData]);
  918. const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => {
  919. console.log("⚠️ [LOT MISMATCH] Lot mismatch detected:", { expectedLot, scannedLot });
  920. console.log("⚠️ [LOT MISMATCH] Opening confirmation modal - NO lot will be marked as scanned until user confirms");
  921. // ✅ schedule modal open in next tick (avoid flushSync warnings on some builds)
  922. // ✅ IMPORTANT: This function ONLY opens the modal. It does NOT process any lot.
  923. setTimeout(() => {
  924. setExpectedLotData(expectedLot);
  925. setScannedLotData({
  926. ...scannedLot,
  927. lotNo: scannedLot.lotNo || null,
  928. });
  929. setLotConfirmationOpen(true);
  930. console.log("⚠️ [LOT MISMATCH] Modal opened - waiting for user confirmation");
  931. }, 0);
  932. // ✅ Fetch lotNo in background for display purposes (cached)
  933. // ✅ This is ONLY for display - it does NOT process any lot
  934. if (!scannedLot.lotNo && scannedLot.stockInLineId) {
  935. console.log(`⚠️ [LOT MISMATCH] Fetching lotNo for display (stockInLineId: ${scannedLot.stockInLineId})`);
  936. fetchStockInLineInfoCached(scannedLot.stockInLineId)
  937. .then((info) => {
  938. console.log(`⚠️ [LOT MISMATCH] Fetched lotNo for display: ${info.lotNo}`);
  939. startTransition(() => {
  940. setScannedLotData((prev: any) => ({
  941. ...prev,
  942. lotNo: info.lotNo || null,
  943. }));
  944. });
  945. })
  946. .catch((error) => {
  947. console.error(`❌ [LOT MISMATCH] Error fetching lotNo for display (stockInLineId may not exist):`, error);
  948. // ignore display fetch errors - this does NOT affect processing
  949. });
  950. }
  951. }, [fetchStockInLineInfoCached]);
  952. // Add handleLotConfirmation function
  953. const handleLotConfirmation = useCallback(async () => {
  954. if (!expectedLotData || !scannedLotData || !selectedLotForQr) {
  955. console.error("❌ [LOT CONFIRM] Missing required data for lot confirmation");
  956. return;
  957. }
  958. console.log("✅ [LOT CONFIRM] User confirmed lot substitution - processing now");
  959. console.log("✅ [LOT CONFIRM] Expected lot:", expectedLotData);
  960. console.log("✅ [LOT CONFIRM] Scanned lot:", scannedLotData);
  961. console.log("✅ [LOT CONFIRM] Selected lot for QR:", selectedLotForQr);
  962. setIsConfirmingLot(true);
  963. try {
  964. let newLotLineId = scannedLotData?.inventoryLotLineId;
  965. if (!newLotLineId && scannedLotData?.stockInLineId) {
  966. try {
  967. if (currentUserId && selectedLotForQr.pickOrderId && selectedLotForQr.itemId) {
  968. try {
  969. await updateHandledBy(selectedLotForQr.pickOrderId, selectedLotForQr.itemId);
  970. console.log(`✅ [LOT CONFIRM] Handler updated for itemId ${selectedLotForQr.itemId}`);
  971. } catch (error) {
  972. console.error(`❌ [LOT CONFIRM] Error updating handler (non-critical):`, error);
  973. }
  974. }
  975. console.log(`🔍 [LOT CONFIRM] Fetching lot detail for stockInLineId: ${scannedLotData.stockInLineId}`);
  976. const ld = await fetchLotDetail(scannedLotData.stockInLineId);
  977. newLotLineId = ld.inventoryLotLineId;
  978. console.log(`✅ [LOT CONFIRM] Fetched lot detail: inventoryLotLineId=${newLotLineId}`);
  979. } catch (error) {
  980. console.error("❌ [LOT CONFIRM] Error fetching lot detail (stockInLineId may not exist):", error);
  981. // If stockInLineId doesn't exist, we can still proceed with lotNo substitution
  982. // The backend confirmLotSubstitution should handle this case
  983. }
  984. }
  985. if (!newLotLineId) {
  986. console.warn("⚠️ [LOT CONFIRM] No inventory lot line id for scanned lot, proceeding with lotNo only");
  987. // Continue anyway - backend may handle lotNo substitution without inventoryLotLineId
  988. }
  989. console.log("=== [LOT CONFIRM] Lot Confirmation Debug ===");
  990. console.log("Selected Lot:", selectedLotForQr);
  991. console.log("Pick Order Line ID:", selectedLotForQr.pickOrderLineId);
  992. console.log("Stock Out Line ID:", selectedLotForQr.stockOutLineId);
  993. console.log("Suggested Pick Lot ID:", selectedLotForQr.suggestedPickLotId);
  994. console.log("Lot ID (fallback):", selectedLotForQr.lotId);
  995. console.log("New Inventory Lot Line ID:", newLotLineId);
  996. console.log("Scanned Lot No:", scannedLotData.lotNo);
  997. console.log("Scanned StockInLineId:", scannedLotData.stockInLineId);
  998. const originalSuggestedPickLotId =
  999. selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId;
  1000. // noLot / missing suggestedPickLotId 场景:没有 originalSuggestedPickLotId,改用 updateStockOutLineStatusByQRCodeAndLotNo
  1001. if (!originalSuggestedPickLotId) {
  1002. if (!selectedLotForQr?.stockOutLineId) {
  1003. throw new Error("Missing stockOutLineId for noLot line");
  1004. }
  1005. console.log("🔄 [LOT CONFIRM] No originalSuggestedPickLotId, using updateStockOutLineStatusByQRCodeAndLotNo...");
  1006. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1007. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  1008. inventoryLotNo: scannedLotData.lotNo || '',
  1009. stockInLineId: scannedLotData?.stockInLineId ?? null,
  1010. stockOutLineId: selectedLotForQr.stockOutLineId,
  1011. itemId: selectedLotForQr.itemId,
  1012. status: "checked",
  1013. });
  1014. console.log("✅ [LOT CONFIRM] updateStockOutLineStatusByQRCodeAndLotNo result:", res);
  1015. const ok = res?.code === "checked" || res?.code === "SUCCESS";
  1016. if (!ok) {
  1017. setQrScanError(true);
  1018. setQrScanSuccess(false);
  1019. setQrScanErrorMsg(res?.message || "换批失败:无法更新 stock out line");
  1020. return;
  1021. }
  1022. } else {
  1023. // Call confirmLotSubstitution to update the suggested lot
  1024. console.log("🔄 [LOT CONFIRM] Calling confirmLotSubstitution...");
  1025. const substitutionResult = await confirmLotSubstitution({
  1026. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  1027. stockOutLineId: selectedLotForQr.stockOutLineId,
  1028. originalSuggestedPickLotId,
  1029. newInventoryLotNo: scannedLotData.lotNo || '',
  1030. // ✅ required by LotSubstitutionConfirmRequest
  1031. newStockInLineId: scannedLotData?.stockInLineId ?? null,
  1032. });
  1033. console.log("✅ [LOT CONFIRM] Lot substitution result:", substitutionResult);
  1034. // ✅ CRITICAL: substitution failed => DO NOT mark original stockOutLine as checked.
  1035. // Keep modal open so user can cancel/rescan.
  1036. if (!substitutionResult || substitutionResult.code !== "SUCCESS") {
  1037. console.error("❌ [LOT CONFIRM] Lot substitution failed. Will NOT update stockOutLine status.");
  1038. setQrScanError(true);
  1039. setQrScanSuccess(false);
  1040. setQrScanErrorMsg(
  1041. substitutionResult?.message ||
  1042. `换批失败:stockInLineId ${scannedLotData?.stockInLineId ?? ""} 不存在或无法匹配`
  1043. );
  1044. return;
  1045. }
  1046. }
  1047. // Update stock out line status to 'checked' after substitution
  1048. if(selectedLotForQr?.stockOutLineId){
  1049. console.log(`🔄 [LOT CONFIRM] Updating stockOutLine ${selectedLotForQr.stockOutLineId} to 'checked'`);
  1050. await updateStockOutLineStatus({
  1051. id: selectedLotForQr.stockOutLineId,
  1052. status: 'checked',
  1053. qty: 0
  1054. });
  1055. console.log(`✅ [LOT CONFIRM] Stock out line ${selectedLotForQr.stockOutLineId} status updated to 'checked'`);
  1056. }
  1057. // Close modal and clean up state BEFORE refreshing
  1058. setLotConfirmationOpen(false);
  1059. setExpectedLotData(null);
  1060. setScannedLotData(null);
  1061. setSelectedLotForQr(null);
  1062. // Clear QR processing state but DON'T clear processedQrCodes yet
  1063. setQrScanError(false);
  1064. setQrScanSuccess(true);
  1065. setQrScanErrorMsg('');
  1066. setQrScanInput('');
  1067. // Set refreshing flag to prevent QR processing during refresh
  1068. setIsRefreshingData(true);
  1069. // Refresh data to show updated lot
  1070. console.log("🔄 Refreshing job order data...");
  1071. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1072. await fetchJobOrderData(pickOrderId);
  1073. console.log(" Lot substitution confirmed and data refreshed");
  1074. // Clear processed QR codes and flags immediately after refresh
  1075. // This allows new QR codes to be processed right away
  1076. setTimeout(() => {
  1077. console.log(" Clearing processed QR codes and resuming scan");
  1078. setProcessedQrCodes(new Set());
  1079. setLastProcessedQr('');
  1080. setQrScanSuccess(false);
  1081. setIsRefreshingData(false);
  1082. // ✅ Clear processedQrCombinations to allow reprocessing the same QR if needed
  1083. if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
  1084. setProcessedQrCombinations(prev => {
  1085. const newMap = new Map(prev);
  1086. const itemId = selectedLotForQr.itemId;
  1087. if (itemId && newMap.has(itemId)) {
  1088. newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
  1089. if (newMap.get(itemId)!.size === 0) {
  1090. newMap.delete(itemId);
  1091. }
  1092. }
  1093. return newMap;
  1094. });
  1095. }
  1096. }, 500); // Reduced from 3000ms to 500ms - just enough for UI update
  1097. } catch (error) {
  1098. console.error("Error confirming lot substitution:", error);
  1099. setQrScanError(true);
  1100. setQrScanSuccess(false);
  1101. setQrScanErrorMsg('换批发生异常,请重试或联系管理员');
  1102. // Clear refresh flag on error
  1103. setIsRefreshingData(false);
  1104. } finally {
  1105. setIsConfirmingLot(false);
  1106. }
  1107. }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData,currentUserId, updateHandledBy ]);
  1108. const processOutsideQrCode = useCallback(async (latestQr: string) => {
  1109. // ✅ Only JSON QR supported for outside scanner (avoid false positive with lotNo)
  1110. let qrData: any = null;
  1111. try {
  1112. qrData = JSON.parse(latestQr);
  1113. } catch {
  1114. startTransition(() => {
  1115. setQrScanError(true);
  1116. setQrScanSuccess(false);
  1117. });
  1118. return;
  1119. }
  1120. if (!(qrData?.stockInLineId && qrData?.itemId)) {
  1121. startTransition(() => {
  1122. setQrScanError(true);
  1123. setQrScanSuccess(false);
  1124. });
  1125. return;
  1126. }
  1127. const scannedItemId = Number(qrData.itemId);
  1128. const scannedStockInLineId = Number(qrData.stockInLineId);
  1129. // ✅ avoid duplicate processing by itemId+stockInLineId
  1130. const itemProcessedSet = processedQrCombinations.get(scannedItemId);
  1131. if (itemProcessedSet?.has(scannedStockInLineId)) return;
  1132. const indexes = lotDataIndexes;
  1133. const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || [];
  1134. // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected
  1135. const allLotsForItem = indexes.byItemId.get(scannedItemId) || [];
  1136. // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots
  1137. // This allows users to scan other lots even when all suggested lots are rejected
  1138. const scannedLot = allLotsForItem.find(
  1139. (lot: any) => lot.stockInLineId === scannedStockInLineId
  1140. );
  1141. if (scannedLot) {
  1142. const isRejected =
  1143. scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1144. scannedLot.lotAvailability === 'rejected' ||
  1145. scannedLot.lotAvailability === 'status_unavailable';
  1146. if (isRejected) {
  1147. console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`);
  1148. startTransition(() => {
  1149. setQrScanError(true);
  1150. setQrScanSuccess(false);
  1151. setQrScanErrorMsg(
  1152. `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
  1153. );
  1154. });
  1155. // Mark as processed to prevent re-processing
  1156. setProcessedQrCombinations(prev => {
  1157. const newMap = new Map(prev);
  1158. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1159. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1160. return newMap;
  1161. });
  1162. return;
  1163. }
  1164. }
  1165. // ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching
  1166. if (activeSuggestedLots.length === 0) {
  1167. // Check if there are any lots for this item (even if all are rejected)
  1168. if (allLotsForItem.length === 0) {
  1169. console.error("No lots found for this item");
  1170. startTransition(() => {
  1171. setQrScanError(true);
  1172. setQrScanSuccess(false);
  1173. setQrScanErrorMsg("当前订单中没有此物品的批次信息");
  1174. });
  1175. return;
  1176. }
  1177. // ✅ Allow lot switching: find a rejected lot as expected lot, or use first lot
  1178. // This allows users to switch to a new lot even when all suggested lots are rejected
  1179. console.log(`⚠️ [QR PROCESS] No active suggested lots, but allowing lot switching. Scanned lot is not rejected.`);
  1180. // Find a rejected lot as expected lot (the one that was rejected)
  1181. const rejectedLot = allLotsForItem.find((lot: any) =>
  1182. lot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1183. lot.lotAvailability === 'rejected' ||
  1184. lot.lotAvailability === 'status_unavailable'
  1185. );
  1186. const expectedLot = rejectedLot || allLotsForItem[0]; // Use rejected lot if exists, otherwise first lot
  1187. // ✅ Always open confirmation modal when no active lots (user needs to confirm switching)
  1188. // handleLotMismatch will fetch lotNo from backend using stockInLineId if needed
  1189. console.log(`⚠️ [QR PROCESS] Opening confirmation modal for lot switch (no active lots)`);
  1190. setSelectedLotForQr(expectedLot);
  1191. handleLotMismatch(
  1192. {
  1193. lotNo: expectedLot.lotNo,
  1194. itemCode: expectedLot.itemCode,
  1195. itemName: expectedLot.itemName
  1196. },
  1197. {
  1198. lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null
  1199. itemCode: expectedLot.itemCode,
  1200. itemName: expectedLot.itemName,
  1201. inventoryLotLineId: scannedLot?.lotId || null,
  1202. stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo
  1203. }
  1204. );
  1205. return;
  1206. }
  1207. // ✅ direct stockInLineId match (O(1))
  1208. const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || [];
  1209. let exactMatch: any = null;
  1210. for (let i = 0; i < stockInLineLots.length; i++) {
  1211. const lot = stockInLineLots[i];
  1212. if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) {
  1213. exactMatch = lot;
  1214. break;
  1215. }
  1216. }
  1217. console.log(`🔍 [QR PROCESS] Scanned stockInLineId: ${scannedStockInLineId}, itemId: ${scannedItemId}`);
  1218. console.log(`🔍 [QR PROCESS] Found ${stockInLineLots.length} lots with stockInLineId ${scannedStockInLineId}`);
  1219. console.log(`🔍 [QR PROCESS] Exact match found: ${exactMatch ? `YES (lotNo: ${exactMatch.lotNo}, stockOutLineId: ${exactMatch.stockOutLineId})` : 'NO'}`);
  1220. // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots
  1221. // This handles the case where Lot A is rejected and user scans Lot B
  1222. if (!exactMatch && scannedLot && !activeSuggestedLots.includes(scannedLot)) {
  1223. // Scanned lot is not in active suggested lots, open confirmation modal
  1224. const expectedLot = activeSuggestedLots[0] || allLotsForItem[0]; // Use first active lot or first lot as expected
  1225. if (expectedLot && scannedLot.stockInLineId !== expectedLot.stockInLineId) {
  1226. console.log(`⚠️ [QR PROCESS] Scanned lot ${scannedLot.lotNo} is not in active suggested lots, opening confirmation modal`);
  1227. setSelectedLotForQr(expectedLot);
  1228. handleLotMismatch(
  1229. {
  1230. lotNo: expectedLot.lotNo,
  1231. itemCode: expectedLot.itemCode,
  1232. itemName: expectedLot.itemName
  1233. },
  1234. {
  1235. lotNo: scannedLot.lotNo || null,
  1236. itemCode: expectedLot.itemCode,
  1237. itemName: expectedLot.itemName,
  1238. inventoryLotLineId: scannedLot.lotId || null,
  1239. stockInLineId: scannedStockInLineId
  1240. }
  1241. );
  1242. return;
  1243. }
  1244. }
  1245. if (exactMatch) {
  1246. if (!exactMatch.stockOutLineId) {
  1247. console.error(`❌ [QR PROCESS] Exact match found but no stockOutLineId`);
  1248. startTransition(() => {
  1249. setQrScanError(true);
  1250. setQrScanSuccess(false);
  1251. });
  1252. return;
  1253. }
  1254. console.log(`✅ [QR PROCESS] Processing exact match: lotNo=${exactMatch.lotNo}, stockOutLineId=${exactMatch.stockOutLineId}`);
  1255. try {
  1256. if (currentUserId && exactMatch.pickOrderId && exactMatch.itemId) {
  1257. try {
  1258. await updateHandledBy(exactMatch.pickOrderId, exactMatch.itemId);
  1259. console.log(`✅ [QR PROCESS] Handler updated for itemId ${exactMatch.itemId}`);
  1260. } catch (error) {
  1261. console.error(`❌ [QR PROCESS] Error updating handler (non-critical):`, error);
  1262. }
  1263. }
  1264. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1265. pickOrderLineId: exactMatch.pickOrderLineId,
  1266. inventoryLotNo: exactMatch.lotNo,
  1267. stockInLineId: exactMatch.stockInLineId ?? null,
  1268. stockOutLineId: exactMatch.stockOutLineId,
  1269. itemId: exactMatch.itemId,
  1270. status: "checked",
  1271. });
  1272. if (res.code === "checked" || res.code === "SUCCESS") {
  1273. console.log(`✅ [QR PROCESS] Successfully updated stockOutLine ${exactMatch.stockOutLineId} to checked`);
  1274. const entity = res.entity as any;
  1275. startTransition(() => {
  1276. setQrScanError(false);
  1277. setQrScanSuccess(true);
  1278. });
  1279. // mark combination processed
  1280. setProcessedQrCombinations(prev => {
  1281. const newMap = new Map(prev);
  1282. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1283. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1284. return newMap;
  1285. });
  1286. // refresh to keep consistency with server & handler updates
  1287. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1288. await fetchJobOrderData(pickOrderId);
  1289. } else {
  1290. console.error(`❌ [QR PROCESS] Update failed: ${res.code}`);
  1291. startTransition(() => {
  1292. setQrScanError(true);
  1293. setQrScanSuccess(false);
  1294. });
  1295. }
  1296. } catch (error) {
  1297. console.error(`❌ [QR PROCESS] Error updating stockOutLine:`, error);
  1298. startTransition(() => {
  1299. setQrScanError(true);
  1300. setQrScanSuccess(false);
  1301. });
  1302. }
  1303. return;
  1304. }
  1305. // ✅ mismatch: validate scanned stockInLineId exists before opening confirmation modal
  1306. console.log(`⚠️ [QR PROCESS] No exact match found. Validating scanned stockInLineId ${scannedStockInLineId} for itemId ${scannedItemId}`);
  1307. console.log(`⚠️ [QR PROCESS] Active suggested lots for itemId ${scannedItemId}:`, activeSuggestedLots.map(l => ({ lotNo: l.lotNo, stockInLineId: l.stockInLineId })));
  1308. if (activeSuggestedLots.length === 0) {
  1309. console.error(`❌ [QR PROCESS] No active suggested lots found for itemId ${scannedItemId}`);
  1310. startTransition(() => {
  1311. setQrScanError(true);
  1312. setQrScanSuccess(false);
  1313. setQrScanErrorMsg(`当前订单中没有 itemId ${scannedItemId} 的可用批次`);
  1314. });
  1315. return;
  1316. }
  1317. const expectedLot = activeSuggestedLots[0];
  1318. console.log(`⚠️ [QR PROCESS] Expected lot: ${expectedLot.lotNo} (stockInLineId: ${expectedLot.stockInLineId}), Scanned stockInLineId: ${scannedStockInLineId}`);
  1319. // ✅ Validate scanned stockInLineId exists before opening modal
  1320. // This ensures the backend can find the lot when user confirms
  1321. try {
  1322. console.log(`🔍 [QR PROCESS] Validating scanned stockInLineId ${scannedStockInLineId} exists...`);
  1323. const stockInLineInfo = await fetchStockInLineInfoCached(scannedStockInLineId);
  1324. console.log(`✅ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} exists, lotNo: ${stockInLineInfo.lotNo}`);
  1325. // ✅ 检查扫描的批次是否已被拒绝
  1326. const scannedLot = combinedLotData.find(
  1327. (lot: any) => lot.stockInLineId === scannedStockInLineId && lot.itemId === scannedItemId
  1328. );
  1329. if (scannedLot) {
  1330. const isRejected =
  1331. scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1332. scannedLot.lotAvailability === 'rejected' ||
  1333. scannedLot.lotAvailability === 'status_unavailable';
  1334. if (isRejected) {
  1335. console.warn(`⚠️ [QR PROCESS] Scanned lot ${stockInLineInfo.lotNo} (stockInLineId: ${scannedStockInLineId}) is rejected or unavailable`);
  1336. startTransition(() => {
  1337. setQrScanError(true);
  1338. setQrScanSuccess(false);
  1339. setQrScanErrorMsg(
  1340. `此批次(${stockInLineInfo.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
  1341. );
  1342. });
  1343. // Mark as processed to prevent re-processing
  1344. setProcessedQrCombinations(prev => {
  1345. const newMap = new Map(prev);
  1346. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1347. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1348. return newMap;
  1349. });
  1350. return;
  1351. }
  1352. }
  1353. // ✅ stockInLineId exists and is not rejected, open confirmation modal
  1354. console.log(`⚠️ [QR PROCESS] Opening confirmation modal - user must confirm before any lot is marked as scanned`);
  1355. setSelectedLotForQr(expectedLot);
  1356. handleLotMismatch(
  1357. {
  1358. lotNo: expectedLot.lotNo,
  1359. itemCode: expectedLot.itemCode,
  1360. itemName: expectedLot.itemName
  1361. },
  1362. {
  1363. lotNo: stockInLineInfo.lotNo || null, // Use fetched lotNo for display
  1364. itemCode: expectedLot.itemCode,
  1365. itemName: expectedLot.itemName,
  1366. inventoryLotLineId: null,
  1367. stockInLineId: scannedStockInLineId
  1368. }
  1369. );
  1370. } catch (error) {
  1371. // ✅ stockInLineId does NOT exist, show error immediately (don't open modal)
  1372. console.error(`❌ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} does NOT exist:`, error);
  1373. startTransition(() => {
  1374. setQrScanError(true);
  1375. setQrScanSuccess(false);
  1376. setQrScanErrorMsg(
  1377. `扫描的 stockInLineId ${scannedStockInLineId} 不存在。请检查 QR 码是否正确,或联系管理员。`
  1378. );
  1379. });
  1380. // Mark as processed to prevent re-processing
  1381. setProcessedQrCombinations(prev => {
  1382. const newMap = new Map(prev);
  1383. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1384. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1385. return newMap;
  1386. });
  1387. }
  1388. }, [filterArgs?.pickOrderId, fetchJobOrderData, handleLotMismatch, lotDataIndexes, processedQrCombinations, combinedLotData, fetchStockInLineInfoCached,currentUserId, updateHandledBy ]);
  1389. // Store in refs for immediate access in qrValues effect
  1390. processOutsideQrCodeRef.current = processOutsideQrCode;
  1391. resetScanRef.current = resetScan;
  1392. const handleManualInputSubmit = useCallback(() => {
  1393. if (qrScanInput.trim() !== '') {
  1394. handleQrCodeSubmit(qrScanInput.trim());
  1395. }
  1396. }, [qrScanInput, handleQrCodeSubmit]);
  1397. // Handle QR code submission from modal (internal scanning)
  1398. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  1399. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  1400. console.log(` QR Code verified for lot: ${lotNo}`);
  1401. const requiredQty = selectedLotForQr.requiredQty;
  1402. const lotId = selectedLotForQr.lotId;
  1403. // Create stock out line
  1404. const stockOutLineData: CreateStockOutLine = {
  1405. consoCode: selectedLotForQr.pickOrderConsoCode,
  1406. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  1407. inventoryLotLineId: selectedLotForQr.lotId,
  1408. qty: 0.0
  1409. };
  1410. try {
  1411. await createStockOutLine(stockOutLineData);
  1412. console.log("Stock out line created successfully!");
  1413. // Close modal
  1414. setQrModalOpen(false);
  1415. setSelectedLotForQr(null);
  1416. // Set pick quantity
  1417. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  1418. setTimeout(() => {
  1419. setPickQtyData(prev => ({
  1420. ...prev,
  1421. [lotKey]: requiredQty
  1422. }));
  1423. console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  1424. }, 500);
  1425. // Refresh data
  1426. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1427. await fetchJobOrderData(pickOrderId);
  1428. } catch (error) {
  1429. console.error("Error creating stock out line:", error);
  1430. }
  1431. }
  1432. }, [selectedLotForQr, fetchJobOrderData]);
  1433. useEffect(() => {
  1434. // Skip if scanner not active or no data or currently refreshing
  1435. if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) return;
  1436. const latestQr = qrValues[qrValues.length - 1];
  1437. // ✅ Test shortcut: {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId
  1438. if ((latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) && latestQr.endsWith("}")) {
  1439. let content = '';
  1440. if (latestQr.startsWith("{2fittest")) content = latestQr.substring(9, latestQr.length - 1);
  1441. else content = latestQr.substring(8, latestQr.length - 1);
  1442. const parts = content.split(',');
  1443. if (parts.length === 2) {
  1444. const itemId = parseInt(parts[0].trim(), 10);
  1445. const stockInLineId = parseInt(parts[1].trim(), 10);
  1446. if (!isNaN(itemId) && !isNaN(stockInLineId)) {
  1447. const simulatedQr = JSON.stringify({ itemId, stockInLineId });
  1448. lastProcessedQrRef.current = latestQr;
  1449. processedQrCodesRef.current.add(latestQr);
  1450. setLastProcessedQr(latestQr);
  1451. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1452. processOutsideQrCodeRef.current?.(simulatedQr);
  1453. resetScanRef.current?.();
  1454. return;
  1455. }
  1456. }
  1457. }
  1458. // ✅ Shortcut: {2fic} open manual lot confirmation modal
  1459. if (latestQr === "{2fic}") {
  1460. setManualLotConfirmationOpen(true);
  1461. resetScanRef.current?.();
  1462. lastProcessedQrRef.current = latestQr;
  1463. processedQrCodesRef.current.add(latestQr);
  1464. setLastProcessedQr(latestQr);
  1465. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1466. return;
  1467. }
  1468. // Skip processing if modal open for same QR
  1469. if (lotConfirmationOpen || manualLotConfirmationOpen) {
  1470. if (latestQr === lastProcessedQrRef.current) return;
  1471. }
  1472. // Skip if already processed (refs)
  1473. if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) return;
  1474. // Mark processed immediately
  1475. lastProcessedQrRef.current = latestQr;
  1476. processedQrCodesRef.current.add(latestQr);
  1477. if (processedQrCodesRef.current.size > 100) {
  1478. const firstValue = processedQrCodesRef.current.values().next().value;
  1479. if (firstValue !== undefined) processedQrCodesRef.current.delete(firstValue);
  1480. }
  1481. // Process immediately
  1482. if (qrProcessingTimeoutRef.current) {
  1483. clearTimeout(qrProcessingTimeoutRef.current);
  1484. qrProcessingTimeoutRef.current = null;
  1485. }
  1486. processOutsideQrCodeRef.current?.(latestQr);
  1487. // UI state updates (non-blocking)
  1488. startTransition(() => {
  1489. setLastProcessedQr(latestQr);
  1490. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1491. });
  1492. return () => {
  1493. if (qrProcessingTimeoutRef.current) {
  1494. clearTimeout(qrProcessingTimeoutRef.current);
  1495. qrProcessingTimeoutRef.current = null;
  1496. }
  1497. };
  1498. }, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]);
  1499. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  1500. if (value === '' || value === null || value === undefined) {
  1501. setPickQtyData(prev => ({
  1502. ...prev,
  1503. [lotKey]: 0
  1504. }));
  1505. return;
  1506. }
  1507. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  1508. if (isNaN(numericValue)) {
  1509. setPickQtyData(prev => ({
  1510. ...prev,
  1511. [lotKey]: 0
  1512. }));
  1513. return;
  1514. }
  1515. setPickQtyData(prev => ({
  1516. ...prev,
  1517. [lotKey]: numericValue
  1518. }));
  1519. }, []);
  1520. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  1521. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  1522. const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
  1523. const checkAndAutoAssignNext = useCallback(async () => {
  1524. if (!currentUserId) return;
  1525. try {
  1526. const completionResponse = await checkPickOrderCompletion(currentUserId);
  1527. if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
  1528. console.log("Found completed pick orders, auto-assigning next...");
  1529. // 移除前端的自动分配逻辑,因为后端已经处理了
  1530. // await handleAutoAssignAndRelease(); // 删除这个函数
  1531. }
  1532. } catch (error) {
  1533. console.error("Error checking pick order completion:", error);
  1534. }
  1535. }, [currentUserId]);
  1536. // Handle submit pick quantity
  1537. const handleSubmitPickQty = useCallback(async (lot: any) => {
  1538. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  1539. const newQty = pickQtyData[lotKey] || 0;
  1540. if (!lot.stockOutLineId) {
  1541. console.error("No stock out line found for this lot");
  1542. return;
  1543. }
  1544. try {
  1545. const currentActualPickQty = lot.actualPickQty || 0;
  1546. const cumulativeQty = currentActualPickQty + newQty;
  1547. let newStatus = 'partially_completed';
  1548. if (cumulativeQty >= lot.requiredQty) {
  1549. newStatus = 'completed';
  1550. }
  1551. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  1552. console.log(`Lot: ${lot.lotNo}`);
  1553. console.log(`Required Qty: ${lot.requiredQty}`);
  1554. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  1555. console.log(`New Submitted Qty: ${newQty}`);
  1556. console.log(`Cumulative Qty: ${cumulativeQty}`);
  1557. console.log(`New Status: ${newStatus}`);
  1558. console.log(`=====================================`);
  1559. await updateStockOutLineStatus({
  1560. id: lot.stockOutLineId,
  1561. status: newStatus,
  1562. qty: cumulativeQty
  1563. });
  1564. if (newQty > 0) {
  1565. await updateInventoryLotLineQuantities({
  1566. inventoryLotLineId: lot.lotId,
  1567. qty: newQty,
  1568. status: 'available',
  1569. operation: 'pick'
  1570. });
  1571. }
  1572. // FIXED: Use the proper API function instead of direct fetch
  1573. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  1574. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1575. try {
  1576. // Use the imported API function instead of direct fetch
  1577. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1578. console.log(` Pick order completion check result:`, completionResponse);
  1579. if (completionResponse.code === "SUCCESS") {
  1580. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1581. } else if (completionResponse.message === "not completed") {
  1582. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1583. } else {
  1584. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1585. }
  1586. } catch (error) {
  1587. console.error("Error checking pick order completion:", error);
  1588. }
  1589. }
  1590. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1591. await fetchJobOrderData(pickOrderId);
  1592. console.log("Pick quantity submitted successfully!");
  1593. setTimeout(() => {
  1594. checkAndAutoAssignNext();
  1595. }, 1000);
  1596. } catch (error) {
  1597. console.error("Error submitting pick quantity:", error);
  1598. }
  1599. }, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]);
  1600. const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
  1601. if (!lot.stockOutLineId) {
  1602. console.error("No stock out line found for this lot");
  1603. return;
  1604. }
  1605. const solId = Number(lot.stockOutLineId) || 0;
  1606. try {
  1607. if (currentUserId && lot.pickOrderId && lot.itemId) {
  1608. try {
  1609. await updateHandledBy(lot.pickOrderId, lot.itemId);
  1610. } catch (error) {
  1611. console.error("❌ Error updating handler (non-critical):", error);
  1612. // Continue even if handler update fails
  1613. }
  1614. }
  1615. // ✅ 两步完成(与 DO 对齐):
  1616. // 1) Skip/Submit0 只把 SOL 标记为 checked(不直接 completed)
  1617. // 2) 之后由 batch submit 把 SOL 推到 completed(允许 0)
  1618. if (submitQty === 0) {
  1619. console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
  1620. console.log(`Lot: ${lot.lotNo}`);
  1621. console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
  1622. console.log(`Setting status to 'checked' with qty: 0 (will complete in batch submit)`);
  1623. const updateResult = await updateStockOutLineStatus({
  1624. id: lot.stockOutLineId,
  1625. status: 'checked',
  1626. qty: 0
  1627. });
  1628. console.log('Update result:', updateResult);
  1629. const r: any = updateResult as any;
  1630. const updateOk =
  1631. r?.code === 'SUCCESS' ||
  1632. r?.type === 'completed' ||
  1633. typeof r?.id === 'number' ||
  1634. typeof r?.entity?.id === 'number' ||
  1635. (r?.message && r.message.includes('successfully'));
  1636. if (!updateResult || !updateOk) {
  1637. console.error('Failed to update stock out line status:', updateResult);
  1638. throw new Error('Failed to update stock out line status');
  1639. }
  1640. // 记录该 SOL 的“目标实际拣货量=0”,让 batch submit 走 onlyComplete(不补拣到 required)
  1641. if (solId > 0) {
  1642. setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: 0 }));
  1643. setLocalSolStatusById(prev => ({ ...prev, [solId]: 'checked' }));
  1644. }
  1645. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1646. void fetchJobOrderData(pickOrderId);
  1647. console.log("All zeros submission marked as checked successfully (waiting for batch submit).");
  1648. setTimeout(() => {
  1649. checkAndAutoAssignNext();
  1650. }, 1000);
  1651. return;
  1652. }
  1653. // Normal case: Calculate cumulative quantity correctly
  1654. const currentActualPickQty = lot.actualPickQty || 0;
  1655. const cumulativeQty = currentActualPickQty + submitQty;
  1656. // Determine status based on cumulative quantity vs required quantity
  1657. let newStatus = 'partially_completed';
  1658. if (cumulativeQty >= lot.requiredQty) {
  1659. newStatus = 'completed';
  1660. } else if (cumulativeQty > 0) {
  1661. newStatus = 'partially_completed';
  1662. } else {
  1663. newStatus = 'checked'; // QR scanned but no quantity submitted yet
  1664. }
  1665. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  1666. console.log(`Lot: ${lot.lotNo}`);
  1667. console.log(`Required Qty: ${lot.requiredQty}`);
  1668. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  1669. console.log(`New Submitted Qty: ${submitQty}`);
  1670. console.log(`Cumulative Qty: ${cumulativeQty}`);
  1671. console.log(`New Status: ${newStatus}`);
  1672. console.log(`=====================================`);
  1673. await updateStockOutLineStatus({
  1674. id: lot.stockOutLineId,
  1675. status: newStatus,
  1676. qty: cumulativeQty
  1677. });
  1678. if (solId > 0) {
  1679. setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: cumulativeQty }));
  1680. setLocalSolStatusById(prev => ({ ...prev, [solId]: newStatus }));
  1681. }
  1682. if (submitQty > 0) {
  1683. await updateInventoryLotLineQuantities({
  1684. inventoryLotLineId: lot.lotId,
  1685. qty: submitQty,
  1686. status: 'available',
  1687. operation: 'pick'
  1688. });
  1689. }
  1690. // Check if pick order is completed when lot status becomes 'completed'
  1691. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  1692. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1693. try {
  1694. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1695. console.log(` Pick order completion check result:`, completionResponse);
  1696. if (completionResponse.code === "SUCCESS") {
  1697. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1698. setTimeout(() => {
  1699. if (onBackToList) {
  1700. onBackToList();
  1701. }
  1702. }, 1500);
  1703. } else if (completionResponse.message === "not completed") {
  1704. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1705. } else {
  1706. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1707. }
  1708. } catch (error) {
  1709. console.error("Error checking pick order completion:", error);
  1710. }
  1711. }
  1712. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1713. void fetchJobOrderData(pickOrderId);
  1714. console.log("Pick quantity submitted successfully!");
  1715. setTimeout(() => {
  1716. checkAndAutoAssignNext();
  1717. }, 1000);
  1718. } catch (error) {
  1719. console.error("Error submitting pick quantity:", error);
  1720. }
  1721. }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]);
  1722. const handleSkip = useCallback(async (lot: any) => {
  1723. try {
  1724. console.log("Just Complete clicked, mark checked with 0 qty for lot:", lot.lotNo);
  1725. await handleSubmitPickQtyWithQty(lot, 0);
  1726. } catch (err) {
  1727. console.error("Error in Skip:", err);
  1728. }
  1729. }, [handleSubmitPickQtyWithQty]);
  1730. const hasPendingBatchSubmit = useMemo(() => {
  1731. return combinedLotData.some((lot) => {
  1732. const status = String(lot.stockOutLineStatus || "").toLowerCase();
  1733. return status === "checked" || status === "pending" || status === "partially_completed" || status === "partially_complete";
  1734. });
  1735. }, [combinedLotData]);
  1736. useEffect(() => {
  1737. if (!hasPendingBatchSubmit) return;
  1738. const handler = (event: BeforeUnloadEvent) => {
  1739. event.preventDefault();
  1740. event.returnValue = "";
  1741. };
  1742. window.addEventListener("beforeunload", handler);
  1743. return () => window.removeEventListener("beforeunload", handler);
  1744. }, [hasPendingBatchSubmit]);
  1745. const handleSubmitAllScanned = useCallback(async () => {
  1746. const scannedLots = combinedLotData.filter(lot => {
  1747. const status = lot.stockOutLineStatus;
  1748. const statusLower = String(status || "").toLowerCase();
  1749. if (statusLower === "completed" || statusLower === "complete") {
  1750. return false;
  1751. }
  1752. console.log("lot.noLot:", lot.noLot);
  1753. console.log("lot.status:", lot.stockOutLineStatus);
  1754. // ✅ no-lot:允許 pending / checked / partially_completed / PARTIALLY_COMPLETE
  1755. if (lot.noLot === true || !lot.lotId) {
  1756. return (
  1757. status === 'checked' ||
  1758. status === 'pending' ||
  1759. status === 'partially_completed' ||
  1760. status === 'PARTIALLY_COMPLETE'
  1761. );
  1762. }
  1763. // ✅ 有 lot:維持原本規則
  1764. return (
  1765. status === 'checked' ||
  1766. status === 'partially_completed' ||
  1767. status === 'PARTIALLY_COMPLETE'
  1768. );
  1769. });
  1770. if (scannedLots.length === 0) {
  1771. console.log("No scanned items to submit");
  1772. return;
  1773. }
  1774. setIsSubmittingAll(true);
  1775. console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`);
  1776. try {
  1777. // ✅ 批量更新所有相关行的 handler(在提交前)
  1778. if (currentUserId) {
  1779. const uniqueItemIds = new Set(scannedLots.map(lot => lot.itemId));
  1780. const updatePromises = Array.from(uniqueItemIds).map(itemId => {
  1781. const lot = scannedLots.find(l => l.itemId === itemId);
  1782. if (lot && lot.pickOrderId) {
  1783. return updateHandledBy(lot.pickOrderId, itemId).catch(err => {
  1784. console.error(`❌ Error updating handler for itemId ${itemId}:`, err);
  1785. });
  1786. }
  1787. return Promise.resolve();
  1788. });
  1789. await Promise.all(updatePromises);
  1790. console.log(`✅ Updated handlers for ${uniqueItemIds.size} unique items`);
  1791. }
  1792. // ✅ 转换为 batchSubmitList 所需的格式
  1793. const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
  1794. const requiredQty = Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0);
  1795. const solId = Number(lot.stockOutLineId) || 0;
  1796. const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined;
  1797. const currentActualPickQty = Number(issuePicked ?? lot.actualPickQty ?? 0);
  1798. const isNoLot = lot.noLot === true || !lot.lotId;
  1799. // ✅ 只改狀態模式:有 issuePicked 或 noLot
  1800. const onlyComplete =
  1801. lot.stockOutLineStatus === 'partially_completed' ||
  1802. issuePicked !== undefined ||
  1803. isNoLot;
  1804. let targetActual: number;
  1805. let newStatus: string;
  1806. if (onlyComplete) {
  1807. targetActual = currentActualPickQty; // no‑lot = 0,一律只改狀態
  1808. newStatus = 'completed';
  1809. } else {
  1810. const remainingQty = Math.max(0, requiredQty - currentActualPickQty);
  1811. targetActual = currentActualPickQty + remainingQty;
  1812. newStatus =
  1813. requiredQty > 0 && targetActual >= requiredQty
  1814. ? 'completed'
  1815. : 'partially_completed';
  1816. }
  1817. return {
  1818. stockOutLineId: Number(lot.stockOutLineId) || 0,
  1819. pickOrderLineId: Number(lot.pickOrderLineId),
  1820. inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
  1821. requiredQty,
  1822. actualPickQty: Number(targetActual),
  1823. stockOutLineStatus: newStatus,
  1824. pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
  1825. noLot: Boolean(lot.noLot === true)
  1826. };
  1827. });
  1828. const request: batchSubmitListRequest = {
  1829. userId: currentUserId || 0,
  1830. lines: lines
  1831. };
  1832. // ✅ 使用 batchSubmitList API
  1833. const result = await batchSubmitList(request);
  1834. console.log(`📥 Batch submit result:`, result);
  1835. // ✅ After batch submit, explicitly trigger completion check per consoCode.
  1836. // Otherwise pick_order/job_order may stay RELEASED even when all lines are completed.
  1837. try {
  1838. const consoCodes = Array.from(
  1839. new Set(
  1840. lines
  1841. .map((l) => (l.pickOrderConsoCode || "").trim())
  1842. .filter((c) => c.length > 0),
  1843. ),
  1844. );
  1845. if (consoCodes.length > 0) {
  1846. await Promise.all(
  1847. consoCodes.map(async (code) => {
  1848. try {
  1849. const completionResponse = await checkAndCompletePickOrderByConsoCode(code);
  1850. console.log(`✅ Pick order completion check (${code}):`, completionResponse);
  1851. } catch (e) {
  1852. console.error(`❌ Error checking completion for ${code}:`, e);
  1853. }
  1854. }),
  1855. );
  1856. }
  1857. } catch (e) {
  1858. console.error("❌ Error triggering completion checks after batch submit:", e);
  1859. }
  1860. // 刷新数据
  1861. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1862. await fetchJobOrderData(pickOrderId);
  1863. if (result && result.code === "SUCCESS") {
  1864. setQrScanSuccess(true);
  1865. setTimeout(() => {
  1866. setQrScanSuccess(false);
  1867. checkAndAutoAssignNext();
  1868. if (onBackToList) {
  1869. onBackToList();
  1870. }
  1871. }, 2000);
  1872. } else {
  1873. console.error("Batch submit failed:", result);
  1874. setQrScanError(true);
  1875. }
  1876. } catch (error) {
  1877. console.error("Error submitting all scanned items:", error);
  1878. setQrScanError(true);
  1879. } finally {
  1880. setIsSubmittingAll(false);
  1881. }
  1882. }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onBackToList, updateHandledBy, issuePickedQtyBySolId])
  1883. const scannedItemsCount = useMemo(() => {
  1884. return combinedLotData.filter(lot => {
  1885. const status = lot.stockOutLineStatus;
  1886. const statusLower = String(status || "").toLowerCase();
  1887. if (statusLower === "completed" || statusLower === "complete") {
  1888. return false;
  1889. }
  1890. const isNoLot = lot.noLot === true || !lot.lotId;
  1891. if (isNoLot) {
  1892. // no-lot:pending / checked / partially_completed 都算「已掃描」
  1893. return (
  1894. status === 'pending' ||
  1895. status === 'checked' ||
  1896. status === 'partially_completed' ||
  1897. status === 'PARTIALLY_COMPLETE'
  1898. );
  1899. }
  1900. // 有 lot:維持原規則
  1901. return (
  1902. status === 'checked' ||
  1903. status === 'partially_completed' ||
  1904. status === 'PARTIALLY_COMPLETE'
  1905. );
  1906. }).length;
  1907. }, [combinedLotData]);
  1908. // 先定义 filteredByFloor 和 availableFloors
  1909. const availableFloors = useMemo(() => {
  1910. const floors = new Set<string>();
  1911. combinedLotData.forEach(lot => {
  1912. const f = extractFloor(lot);
  1913. if (f) floors.add(f);
  1914. });
  1915. return Array.from(floors).sort((a, b) => floorSortOrder(b) - floorSortOrder(a));
  1916. }, [combinedLotData]);
  1917. const filteredByFloor = useMemo(() => {
  1918. if (!selectedFloor) return combinedLotData;
  1919. return combinedLotData.filter(lot => extractFloor(lot) === selectedFloor);
  1920. }, [combinedLotData, selectedFloor]);
  1921. // Progress bar data - 现在可以正确引用 filteredByFloor
  1922. const progress = useMemo(() => {
  1923. const data = selectedFloor ? filteredByFloor : combinedLotData;
  1924. if (data.length === 0) return { completed: 0, total: 0 };
  1925. const nonPendingCount = data.filter(lot =>
  1926. lot.stockOutLineStatus?.toLowerCase() !== 'pending'
  1927. ).length;
  1928. return { completed: nonPendingCount, total: data.length };
  1929. }, [selectedFloor, filteredByFloor, combinedLotData]);
  1930. // Handle reject lot
  1931. const handleRejectLot = useCallback(async (lot: any) => {
  1932. if (!lot.stockOutLineId) {
  1933. console.error("No stock out line found for this lot");
  1934. return;
  1935. }
  1936. try {
  1937. await updateStockOutLineStatus({
  1938. id: lot.stockOutLineId,
  1939. status: 'rejected',
  1940. qty: 0
  1941. });
  1942. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1943. await fetchJobOrderData(pickOrderId);
  1944. console.log("Lot rejected successfully!");
  1945. setTimeout(() => {
  1946. checkAndAutoAssignNext();
  1947. }, 1000);
  1948. } catch (error) {
  1949. console.error("Error rejecting lot:", error);
  1950. }
  1951. }, [fetchJobOrderData, checkAndAutoAssignNext]);
  1952. // Handle pick execution form
  1953. const handlePickExecutionForm = useCallback((lot: any) => {
  1954. console.log("=== Pick Execution Form ===");
  1955. console.log("Lot data:", lot);
  1956. if (!lot) {
  1957. console.warn("No lot data provided for pick execution form");
  1958. return;
  1959. }
  1960. console.log("Opening pick execution form for lot:", lot.lotNo);
  1961. setSelectedLotForExecutionForm(lot);
  1962. setPickExecutionFormOpen(true);
  1963. console.log("Pick execution form opened for lot ID:", lot.lotId);
  1964. }, []);
  1965. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  1966. try {
  1967. if (currentUserId && selectedLotForExecutionForm?.pickOrderId && selectedLotForExecutionForm?.itemId) {
  1968. try {
  1969. await updateHandledBy(selectedLotForExecutionForm.pickOrderId, selectedLotForExecutionForm.itemId);
  1970. console.log(`✅ [ISSUE FORM] Handler updated for itemId ${selectedLotForExecutionForm.itemId}`);
  1971. } catch (error) {
  1972. console.error(`❌ [ISSUE FORM] Error updating handler (non-critical):`, error);
  1973. }
  1974. }
  1975. console.log("Pick execution form submitted:", data);
  1976. const issueData = {
  1977. ...data,
  1978. type: "Jo", // Delivery Order Record 类型
  1979. pickerName: session?.user?.name || undefined,
  1980. handledBy: currentUserId || undefined,
  1981. };
  1982. const result = await recordPickExecutionIssue(issueData);
  1983. console.log("Pick execution issue recorded:", result);
  1984. if (result && result.code === "SUCCESS") {
  1985. console.log(" Pick execution issue recorded successfully");
  1986. const solId = Number(issueData.stockOutLineId || data?.stockOutLineId);
  1987. if (solId > 0) {
  1988. const picked = Number(issueData.actualPickQty || 0);
  1989. setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: picked }));
  1990. }
  1991. } else {
  1992. console.error("❌ Failed to record pick execution issue:", result);
  1993. }
  1994. setPickExecutionFormOpen(false);
  1995. setSelectedLotForExecutionForm(null);
  1996. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1997. await fetchJobOrderData(pickOrderId);
  1998. } catch (error) {
  1999. console.error("Error submitting pick execution form:", error);
  2000. }
  2001. }, [fetchJobOrderData, currentUserId, selectedLotForExecutionForm, updateHandledBy, filterArgs?.pickOrderId]);
  2002. // Calculate remaining required quantity
  2003. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  2004. const requiredQty = lot.requiredQty || 0;
  2005. const stockOutLineQty = lot.stockOutLineQty || 0;
  2006. return Math.max(0, requiredQty - stockOutLineQty);
  2007. }, []);
  2008. // Search criteria
  2009. const searchCriteria: Criterion<any>[] = [
  2010. {
  2011. label: t("Pick Order Code"),
  2012. paramName: "pickOrderCode",
  2013. type: "text",
  2014. },
  2015. {
  2016. label: t("Item Code"),
  2017. paramName: "itemCode",
  2018. type: "text",
  2019. },
  2020. {
  2021. label: t("Item Name"),
  2022. paramName: "itemName",
  2023. type: "text",
  2024. },
  2025. {
  2026. label: t("Lot No"),
  2027. paramName: "lotNo",
  2028. type: "text",
  2029. },
  2030. ];
  2031. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  2032. setPaginationController(prev => ({
  2033. ...prev,
  2034. pageNum: newPage,
  2035. }));
  2036. }, []);
  2037. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  2038. const newPageSize = parseInt(event.target.value, 10);
  2039. setPaginationController({
  2040. pageNum: 0,
  2041. pageSize: newPageSize,
  2042. });
  2043. }, []);
  2044. // Pagination data with sorting by routerIndex
  2045. const paginatedData = useMemo(() => {
  2046. const sourceData = selectedFloor ? filteredByFloor : combinedLotData;
  2047. const sortedData = [...sourceData].sort((a, b) => {
  2048. const floorA = extractFloor(a);
  2049. const floorB = extractFloor(b);
  2050. const orderA = floorSortOrder(floorA);
  2051. const orderB = floorSortOrder(floorB);
  2052. if (orderA !== orderB) return orderB - orderA; // 4F, 3F, 2F
  2053. // 同楼层再按 routerIndex、pickOrderCode、lotNo
  2054. const aIndex = a.routerIndex ?? 0;
  2055. const bIndex = b.routerIndex ?? 0;
  2056. if (aIndex !== bIndex) return aIndex - bIndex;
  2057. // Secondary sort: by pickOrderCode if routerIndex is the same
  2058. if (a.pickOrderCode !== b.pickOrderCode) {
  2059. return a.pickOrderCode.localeCompare(b.pickOrderCode);
  2060. }
  2061. // Tertiary sort: by lotNo if everything else is the same
  2062. return (a.lotNo || '').localeCompare(b.lotNo || '');
  2063. });
  2064. const startIndex = paginationController.pageNum * paginationController.pageSize;
  2065. const endIndex = startIndex + paginationController.pageSize;
  2066. return sortedData.slice(startIndex, endIndex);
  2067. }, [selectedFloor, filteredByFloor, combinedLotData, paginationController]);
  2068. // Add these functions for manual scanning
  2069. const handleStartScan = useCallback(() => {
  2070. console.log(" Starting manual QR scan...");
  2071. setIsManualScanning(true);
  2072. setProcessedQrCodes(new Set());
  2073. setLastProcessedQr('');
  2074. setQrScanError(false);
  2075. setQrScanSuccess(false);
  2076. startScan();
  2077. }, [startScan]);
  2078. const handleStopScan = useCallback(() => {
  2079. console.log(" Stopping manual QR scan...");
  2080. setIsManualScanning(false);
  2081. setQrScanError(false);
  2082. setQrScanSuccess(false);
  2083. stopScan();
  2084. resetScan();
  2085. }, [stopScan, resetScan]);
  2086. useEffect(() => {
  2087. return () => {
  2088. // Cleanup when component unmounts (e.g., when switching tabs)
  2089. if (isManualScanning) {
  2090. console.log("🧹 Component unmounting, stopping QR scanner...");
  2091. stopScan();
  2092. resetScan();
  2093. }
  2094. };
  2095. }, [isManualScanning, stopScan, resetScan]);
  2096. useEffect(() => {
  2097. if (isManualScanning && combinedLotData.length === 0) {
  2098. console.log(" No data available, auto-stopping QR scan...");
  2099. handleStopScan();
  2100. }
  2101. }, [combinedLotData.length, isManualScanning, handleStopScan]);
  2102. // Cleanup effect
  2103. useEffect(() => {
  2104. return () => {
  2105. // Cleanup when component unmounts (e.g., when switching tabs)
  2106. if (isManualScanning) {
  2107. console.log("🧹 Component unmounting, stopping QR scanner...");
  2108. stopScan();
  2109. resetScan();
  2110. }
  2111. };
  2112. }, [isManualScanning, stopScan, resetScan]);
  2113. const getStatusMessage = useCallback((lot: any) => {
  2114. if (lot?.noLot === true || lot?.lotAvailability === 'insufficient_stock') {
  2115. return t("This order is insufficient, please pick another lot.");
  2116. }
  2117. switch (lot.stockOutLineStatus?.toLowerCase()) {
  2118. case 'pending':
  2119. return t("Please finish QR code scan and pick order.");
  2120. case 'checked':
  2121. return t("Please submit the pick order.");
  2122. case 'partially_completed':
  2123. return t("Partial quantity submitted. Please submit more or complete the order.");
  2124. case 'completed':
  2125. return t("Pick order completed successfully!");
  2126. case 'rejected':
  2127. return t("Lot has been rejected and marked as unavailable.");
  2128. case 'unavailable':
  2129. return t("This order is insufficient, please pick another lot.");
  2130. default:
  2131. return t("Please finish QR code scan and pick order.");
  2132. }
  2133. }, [t]);
  2134. return (
  2135. <TestQrCodeProvider
  2136. lotData={combinedLotData}
  2137. onScanLot={handleQrCodeSubmit}
  2138. filterActive={(lot) => (
  2139. lot.lotAvailability !== 'rejected' &&
  2140. lot.stockOutLineStatus !== 'rejected' &&
  2141. lot.stockOutLineStatus !== 'completed'
  2142. )}
  2143. >
  2144. <FormProvider {...formProps}>
  2145. <Stack spacing={2}>
  2146. {/* Progress bar + scan status fixed at top */}
  2147. <Box
  2148. sx={{
  2149. position: 'fixed',
  2150. top: 0,
  2151. left: 0,
  2152. right: 0,
  2153. zIndex: 1100,
  2154. backgroundColor: 'background.paper',
  2155. pt: 2,
  2156. pb: 1,
  2157. px: 2,
  2158. borderBottom: '1px solid',
  2159. borderColor: 'divider',
  2160. boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
  2161. }}
  2162. >
  2163. <LinearProgressWithLabel
  2164. completed={progress.completed}
  2165. total={progress.total}
  2166. label={t("Progress")}
  2167. />
  2168. <ScanStatusAlert
  2169. error={qrScanError}
  2170. success={qrScanSuccess}
  2171. errorMessage={qrScanErrorMsg || t("QR code does not match any item in current orders.")}
  2172. successMessage={t("QR code verified.")}
  2173. />
  2174. </Box>
  2175. <Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
  2176. <Button
  2177. variant={selectedFloor === null ? 'contained' : 'outlined'}
  2178. size="small"
  2179. onClick={() => setSelectedFloor(null)}
  2180. >
  2181. {t("All")}
  2182. </Button>
  2183. {availableFloors.map(floor => (
  2184. <Button
  2185. key={floor}
  2186. variant={selectedFloor === floor ? 'contained' : 'outlined'}
  2187. size="small"
  2188. onClick={() => setSelectedFloor(floor)}
  2189. >
  2190. {floor}
  2191. </Button>
  2192. ))}
  2193. </Box>
  2194. {/* Job Order Header */}
  2195. {jobOrderData && (
  2196. <Paper sx={{ p: 2 }}>
  2197. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  2198. <Typography variant="subtitle1">
  2199. <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.code || '-'}
  2200. </Typography>
  2201. <Typography variant="subtitle1">
  2202. <strong>{t("Pick Order Code")}:</strong> {jobOrderData.pickOrder?.code || '-'}
  2203. </Typography>
  2204. <Typography variant="subtitle1">
  2205. <strong>{t("Target Date")}:</strong> {jobOrderData.pickOrder?.targetDate || '-'}
  2206. </Typography>
  2207. </Stack>
  2208. </Paper>
  2209. )}
  2210. {/* Combined Lot Table */}
  2211. <Box sx={{ mt: 10 }}>
  2212. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
  2213. <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
  2214. {!isManualScanning ? (
  2215. <Button
  2216. variant="contained"
  2217. startIcon={<QrCodeIcon />}
  2218. onClick={handleStartScan}
  2219. color="primary"
  2220. sx={{ minWidth: '120px' }}
  2221. >
  2222. {t("Start QR Scan")}
  2223. </Button>
  2224. ) : (
  2225. <Button
  2226. variant="outlined"
  2227. startIcon={<QrCodeIcon />}
  2228. onClick={handleStopScan}
  2229. color="secondary"
  2230. sx={{ minWidth: '120px' }}
  2231. >
  2232. {t("Stop QR Scan")}
  2233. </Button>
  2234. )}
  2235. {/* ADD THIS: Submit All Scanned Button */}
  2236. <Button
  2237. variant="contained"
  2238. color="success"
  2239. onClick={handleSubmitAllScanned}
  2240. disabled={scannedItemsCount === 0 || isSubmittingAll}
  2241. sx={{ minWidth: '160px' }}
  2242. >
  2243. {isSubmittingAll ? (
  2244. <>
  2245. <CircularProgress size={16} sx={{ mr: 1 }} />
  2246. {t("Submitting...")}
  2247. </>
  2248. ) : (
  2249. `${t("Submit All Scanned")} (${scannedItemsCount})`
  2250. )}
  2251. </Button>
  2252. </Box>
  2253. </Box>
  2254. <TableContainer component={Paper}>
  2255. <Table>
  2256. <TableHead>
  2257. <TableRow>
  2258. <TableCell>{t("Index")}</TableCell>
  2259. <TableCell>{t("Route")}</TableCell>
  2260. <TableCell>{t("Handler")}</TableCell>
  2261. <TableCell>{t("Item Code")}</TableCell>
  2262. <TableCell>{t("Item Name")}</TableCell>
  2263. <TableCell>{t("Lot No")}</TableCell>
  2264. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  2265. <TableCell align="right">{t("Available Qty")}</TableCell>
  2266. <TableCell align="center">{t("Scan Result")}</TableCell>
  2267. <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
  2268. </TableRow>
  2269. </TableHead>
  2270. <TableBody>
  2271. {paginatedData.length === 0 ? (
  2272. <TableRow>
  2273. <TableCell colSpan={9} align="center">
  2274. <Typography variant="body2" color="text.secondary">
  2275. {t("No data available")}
  2276. </Typography>
  2277. </TableCell>
  2278. </TableRow>
  2279. ) : (
  2280. paginatedData.map((lot, index) => (
  2281. <TableRow
  2282. key={`${lot.pickOrderLineId}-${lot.lotId}`}
  2283. sx={{
  2284. // backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
  2285. //opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
  2286. '& .MuiTableCell-root': {
  2287. // color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
  2288. }
  2289. }}
  2290. >
  2291. <TableCell>
  2292. <Typography variant="body2" fontWeight="bold">
  2293. {index + 1}
  2294. </Typography>
  2295. </TableCell>
  2296. <TableCell>
  2297. <Typography variant="body2">
  2298. {lot.routerRoute || '-'}
  2299. </Typography>
  2300. </TableCell>
  2301. <TableCell>{lot.handler || '-'}</TableCell>
  2302. <TableCell>{lot.itemCode}</TableCell>
  2303. <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
  2304. <TableCell>
  2305. {lot.noLot === true || !lot.lotId
  2306. ? t("Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.") // i18n key,下一步加文案
  2307. : (lot.lotNo || '-')}
  2308. </TableCell>
  2309. <TableCell align="right">
  2310. {(() => {
  2311. const requiredQty = lot.requiredQty || 0;
  2312. const unit = (lot.noLot === true || !lot.lotId)
  2313. ? (lot.uomDesc || "")
  2314. : ( lot.uomDesc || "");
  2315. return `${requiredQty.toLocaleString()}(${unit})`;
  2316. })()}
  2317. </TableCell>
  2318. <TableCell align="right">
  2319. {(() => {
  2320. const avail = lot.itemTotalAvailableQty;
  2321. if (avail == null) return "-";
  2322. const unit = lot.uomDesc || "";
  2323. return `${Number(avail).toLocaleString()}(${unit})`;
  2324. })()}
  2325. </TableCell>
  2326. <TableCell align="center">
  2327. {(() => {
  2328. const status = lot.stockOutLineStatus?.toLowerCase();
  2329. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  2330. const isNoLot = !lot.lotNo;
  2331. // ✅ rejected lot:显示红色勾选(已扫描但被拒绝)
  2332. if (isRejected && !isNoLot) {
  2333. return (
  2334. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2335. <Checkbox
  2336. checked={true}
  2337. disabled={true}
  2338. readOnly={true}
  2339. size="large"
  2340. sx={{
  2341. color: 'error.main',
  2342. '&.Mui-checked': { color: 'error.main' },
  2343. transform: 'scale(1.3)',
  2344. }}
  2345. />
  2346. </Box>
  2347. );
  2348. }
  2349. // ✅ 正常 lot:已扫描(checked/partially_completed/completed)
  2350. if (!isNoLot && status !== 'pending' && status !== 'rejected') {
  2351. return (
  2352. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2353. <Checkbox
  2354. checked={true}
  2355. disabled={true}
  2356. readOnly={true}
  2357. size="large"
  2358. sx={{
  2359. color: 'success.main',
  2360. '&.Mui-checked': { color: 'success.main' },
  2361. transform: 'scale(1.3)',
  2362. }}
  2363. />
  2364. </Box>
  2365. );
  2366. }
  2367. return null;
  2368. })()}
  2369. </TableCell>
  2370. <TableCell align="center">
  2371. <Box sx={{ display: 'flex', justifyContent: 'center' }}>
  2372. {(() => {
  2373. const status = lot.stockOutLineStatus?.toLowerCase();
  2374. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  2375. const isNoLot = !lot.lotNo;
  2376. // ✅ rejected lot:显示提示文本(换行显示)
  2377. if (isRejected && !isNoLot) {
  2378. return (
  2379. <Typography
  2380. variant="body2"
  2381. color="error.main"
  2382. sx={{
  2383. textAlign: 'center',
  2384. whiteSpace: 'normal',
  2385. wordBreak: 'break-word',
  2386. maxWidth: '200px',
  2387. lineHeight: 1.5
  2388. }}
  2389. >
  2390. {t("This lot is rejected, please scan another lot.")}
  2391. </Typography>
  2392. );
  2393. }
  2394. // 正常 lot:显示按钮
  2395. return (
  2396. <Stack direction="row" spacing={1} alignItems="center">
  2397. <Button
  2398. variant="contained"
  2399. onClick={async () => {
  2400. const solId = Number(lot.stockOutLineId) || 0;
  2401. if (solId > 0) {
  2402. setActionBusyBySolId(prev => ({ ...prev, [solId]: true }));
  2403. }
  2404. try {
  2405. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  2406. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  2407. handlePickQtyChange(lotKey, submitQty);
  2408. await handleSubmitPickQtyWithQty(lot, submitQty);
  2409. } finally {
  2410. if (solId > 0) {
  2411. setActionBusyBySolId(prev => ({ ...prev, [solId]: false }));
  2412. }
  2413. }
  2414. }}
  2415. disabled={
  2416. (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) ||
  2417. (lot.lotAvailability === 'expired' ||
  2418. lot.lotAvailability === 'status_unavailable' ||
  2419. lot.lotAvailability === 'rejected') ||
  2420. lot.stockOutLineStatus === 'completed' ||
  2421. lot.stockOutLineStatus === 'pending'
  2422. }
  2423. sx={{
  2424. fontSize: '0.75rem',
  2425. py: 0.5,
  2426. minHeight: '28px',
  2427. minWidth: '70px'
  2428. }}
  2429. >
  2430. {t("Submit")}
  2431. </Button>
  2432. <Button
  2433. variant="outlined"
  2434. size="small"
  2435. onClick={() => handlePickExecutionForm(lot)}
  2436. disabled={
  2437. lot.stockOutLineStatus === 'completed' || lot.noLot === true || !lot.lotId
  2438. }
  2439. sx={{
  2440. fontSize: '0.7rem',
  2441. py: 0.5,
  2442. minHeight: '28px',
  2443. minWidth: '60px',
  2444. borderColor: 'warning.main',
  2445. color: 'warning.main'
  2446. }}
  2447. title="Report missing or bad items"
  2448. >
  2449. {t("Edit")}
  2450. </Button>
  2451. <Button
  2452. variant="outlined"
  2453. size="small"
  2454. onClick={async () => {
  2455. const solId = Number(lot.stockOutLineId) || 0;
  2456. if (solId > 0) {
  2457. setActionBusyBySolId(prev => ({ ...prev, [solId]: true }));
  2458. }
  2459. try {
  2460. // ✅ 更新 handler 后再提交
  2461. if (currentUserId && lot.pickOrderId && lot.itemId) {
  2462. try {
  2463. await updateHandledBy(lot.pickOrderId, lot.itemId);
  2464. } catch (error) {
  2465. console.error("❌ Error updating handler (non-critical):", error);
  2466. }
  2467. }
  2468. await handleSubmitPickQtyWithQty(lot, 0);
  2469. } finally {
  2470. if (solId > 0) {
  2471. setActionBusyBySolId(prev => ({ ...prev, [solId]: false }));
  2472. }
  2473. }
  2474. }}
  2475. disabled={
  2476. (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) ||
  2477. lot.stockOutLineStatus === 'completed' ||
  2478. lot.noLot === true ||
  2479. !lot.lotId ||
  2480. (Number(lot.stockOutLineId) > 0 &&
  2481. Object.prototype.hasOwnProperty.call(
  2482. issuePickedQtyBySolId,
  2483. Number(lot.stockOutLineId)
  2484. ))
  2485. }
  2486. sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }}
  2487. >
  2488. {t("Just Complete")}
  2489. </Button>
  2490. </Stack>
  2491. );
  2492. })()}
  2493. </Box>
  2494. </TableCell>
  2495. </TableRow>
  2496. ))
  2497. )}
  2498. </TableBody>
  2499. </Table>
  2500. </TableContainer>
  2501. <TablePagination
  2502. component="div"
  2503. count={selectedFloor ? filteredByFloor.length : combinedLotData.length}
  2504. page={paginationController.pageNum}
  2505. rowsPerPage={paginationController.pageSize}
  2506. onPageChange={handlePageChange}
  2507. onRowsPerPageChange={handlePageSizeChange}
  2508. rowsPerPageOptions={[10, 25, 50]}
  2509. labelRowsPerPage={t("Rows per page")}
  2510. labelDisplayedRows={({ from, to, count }) =>
  2511. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  2512. }
  2513. />
  2514. </Box>
  2515. </Stack>
  2516. {/* QR Code Modal */}
  2517. {!lotConfirmationOpen && (
  2518. <QrCodeModal
  2519. open={qrModalOpen}
  2520. onClose={() => {
  2521. setQrModalOpen(false);
  2522. setSelectedLotForQr(null);
  2523. stopScan();
  2524. resetScan();
  2525. }}
  2526. lot={selectedLotForQr}
  2527. combinedLotData={combinedLotData}
  2528. onQrCodeSubmit={handleQrCodeSubmitFromModal}
  2529. />
  2530. )}
  2531. {/* Add Lot Confirmation Modal */}
  2532. {lotConfirmationOpen && expectedLotData && scannedLotData && (
  2533. <LotConfirmationModal
  2534. open={lotConfirmationOpen}
  2535. onClose={() => {
  2536. console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`);
  2537. setLotConfirmationOpen(false);
  2538. setExpectedLotData(null);
  2539. setScannedLotData(null);
  2540. setSelectedLotForQr(null);
  2541. // ✅ IMPORTANT: Clear refs and processedQrCombinations to allow reprocessing the same QR code
  2542. // This allows the modal to reopen if user cancels and scans the same QR again
  2543. setTimeout(() => {
  2544. lastProcessedQrRef.current = '';
  2545. processedQrCodesRef.current.clear();
  2546. // Clear processedQrCombinations for this itemId+stockInLineId combination
  2547. if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
  2548. setProcessedQrCombinations(prev => {
  2549. const newMap = new Map(prev);
  2550. const itemId = selectedLotForQr.itemId;
  2551. if (itemId && newMap.has(itemId)) {
  2552. newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
  2553. if (newMap.get(itemId)!.size === 0) {
  2554. newMap.delete(itemId);
  2555. }
  2556. }
  2557. return newMap;
  2558. });
  2559. }
  2560. console.log(`⏱️ [LOT CONFIRM MODAL] Cleared refs and processedQrCombinations to allow reprocessing`);
  2561. }, 100);
  2562. }}
  2563. onConfirm={handleLotConfirmation}
  2564. expectedLot={expectedLotData}
  2565. scannedLot={scannedLotData}
  2566. isLoading={isConfirmingLot}
  2567. />
  2568. )}
  2569. {/* Manual Lot Confirmation Modal (test shortcut {2fic}) */}
  2570. <ManualLotConfirmationModal
  2571. open={manualLotConfirmationOpen}
  2572. onClose={() => setManualLotConfirmationOpen(false)}
  2573. // Reuse existing handler: expectedLotInput=current lot, scannedLotInput=new lot
  2574. onConfirm={(currentLotNo, newLotNo) => {
  2575. // Use existing manual flow from handleManualLotConfirmation in other screens:
  2576. // Here we route through updateStockOutLineStatusByQRCodeAndLotNo via handleManualLotConfirmation-like inline logic.
  2577. // For now: open LotConfirmationModal path by setting expected/scanned and letting user confirm substitution.
  2578. setExpectedLotData({ lotNo: currentLotNo, itemCode: '', itemName: '' });
  2579. setScannedLotData({ lotNo: newLotNo, itemCode: '', itemName: '', inventoryLotLineId: null, stockInLineId: null });
  2580. setManualLotConfirmationOpen(false);
  2581. setLotConfirmationOpen(true);
  2582. }}
  2583. expectedLot={expectedLotData}
  2584. scannedLot={scannedLotData}
  2585. isLoading={isConfirmingLot}
  2586. />
  2587. {/* Pick Execution Form Modal */}
  2588. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  2589. <GoodPickExecutionForm
  2590. open={pickExecutionFormOpen}
  2591. onClose={() => {
  2592. setPickExecutionFormOpen(false);
  2593. setSelectedLotForExecutionForm(null);
  2594. }}
  2595. onSubmit={handlePickExecutionFormSubmit}
  2596. selectedLot={selectedLotForExecutionForm}
  2597. selectedPickOrderLine={{
  2598. id: selectedLotForExecutionForm.pickOrderLineId,
  2599. itemId: selectedLotForExecutionForm.itemId,
  2600. itemCode: selectedLotForExecutionForm.itemCode,
  2601. itemName: selectedLotForExecutionForm.itemName,
  2602. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  2603. // Add missing required properties from GetPickOrderLineInfo interface
  2604. availableQty: selectedLotForExecutionForm.availableQty || 0,
  2605. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  2606. uomDesc: selectedLotForExecutionForm.uomDesc || '',
  2607. uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
  2608. pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
  2609. suggestedList: [],
  2610. noLotLines: []
  2611. }}
  2612. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  2613. pickOrderCreateDate={new Date()}
  2614. onNormalPickSubmit={async (lot, submitQty) => {
  2615. console.log('onNormalPickSubmit called in newJobPickExecution:', { lot, submitQty });
  2616. if (!lot) {
  2617. console.error('Lot is null or undefined');
  2618. return;
  2619. }
  2620. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  2621. handlePickQtyChange(lotKey, submitQty);
  2622. await handleSubmitPickQtyWithQty(lot, submitQty);
  2623. }}
  2624. />
  2625. )}
  2626. </FormProvider>
  2627. </TestQrCodeProvider>
  2628. );
  2629. };
  2630. export default JobPickExecution