FPSMS-frontend
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

2660 linhas
94 KiB

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