FPSMS-frontend
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

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