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

2018 line
72 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 TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider';
  23. import { fetchLotDetail } from "@/app/api/inventory/actions";
  24. import { useCallback, useEffect, useState, useRef, useMemo } from "react";
  25. import { useTranslation } from "react-i18next";
  26. import { useRouter } from "next/navigation";
  27. import {
  28. fetchALLPickOrderLineLotDetails,
  29. updateStockOutLineStatus,
  30. createStockOutLine,
  31. updateStockOutLine,
  32. recordPickExecutionIssue,
  33. fetchFGPickOrders, // ✅ Add this import
  34. FGPickOrderResponse,
  35. checkPickOrderCompletion,
  36. fetchAllPickOrderLotsHierarchical,
  37. PickOrderCompletionResponse,
  38. checkAndCompletePickOrderByConsoCode,
  39. updateSuggestedLotLineId,
  40. confirmLotSubstitution,
  41. fetchDoPickOrderDetail, // ✅ 必须添加
  42. DoPickOrderDetail, // ✅ 必须添加
  43. fetchFGPickOrdersByUserId
  44. } from "@/app/api/pickOrder/actions";
  45. import FGPickOrderInfoCard from "./FGPickOrderInfoCard";
  46. import LotConfirmationModal from "./LotConfirmationModal";
  47. //import { fetchItem } from "@/app/api/settings/item";
  48. import { updateInventoryLotLineStatus, analyzeQrCode } from "@/app/api/inventory/actions";
  49. import { fetchNameList, NameList } from "@/app/api/user/actions";
  50. import {
  51. FormProvider,
  52. useForm,
  53. } from "react-hook-form";
  54. import SearchBox, { Criterion } from "../SearchBox";
  55. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  56. import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  57. import QrCodeIcon from '@mui/icons-material/QrCode';
  58. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  59. import { useSession } from "next-auth/react";
  60. import { SessionWithTokens } from "@/config/authConfig";
  61. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  62. import GoodPickExecutionForm from "./GoodPickExecutionForm";
  63. import FGPickOrderCard from "./FGPickOrderCard";
  64. interface Props {
  65. filterArgs: Record<string, any>;
  66. }
  67. // ✅ QR Code Modal Component (from LotTable)
  68. const QrCodeModal: React.FC<{
  69. open: boolean;
  70. onClose: () => void;
  71. lot: any | null;
  72. onQrCodeSubmit: (lotNo: string) => void;
  73. combinedLotData: any[]; // ✅ Add this prop
  74. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
  75. const { t } = useTranslation("pickOrder");
  76. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  77. const [manualInput, setManualInput] = useState<string>('');
  78. const [doPickOrderDetail, setDoPickOrderDetail] = useState<DoPickOrderDetail | null>(null);
  79. const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>(null);
  80. const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
  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. const fetchFgPickOrdersData = useCallback(async () => {
  346. if (!currentUserId) return;
  347. setFgPickOrdersLoading(true);
  348. try {
  349. const fgPickOrders = await fetchFGPickOrdersByUserId(currentUserId);
  350. console.log("🔍 DEBUG: Fetched FG pick orders:", fgPickOrders);
  351. console.log("🔍 DEBUG: First order numberOfPickOrders:", fgPickOrders[0]?.numberOfPickOrders);
  352. setFgPickOrders(fgPickOrders);
  353. // ✅ 移除:不需要再单独调用 fetchDoPickOrderDetail
  354. // all-lots-hierarchical API 已经包含了所有需要的数据
  355. } catch (error) {
  356. console.error("❌ Error fetching FG pick orders:", error);
  357. setFgPickOrders([]);
  358. } finally {
  359. setFgPickOrdersLoading(false);
  360. }
  361. }, [currentUserId]);
  362. useEffect(() => {
  363. if (combinedLotData.length > 0) {
  364. fetchFgPickOrdersData();
  365. }
  366. }, [combinedLotData, fetchFgPickOrdersData]);
  367. // ✅ Handle QR code button click
  368. const handleQrCodeClick = (pickOrderId: number) => {
  369. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  370. // TODO: Implement QR code functionality
  371. };
  372. const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => {
  373. console.log("Lot mismatch detected:", { expectedLot, scannedLot });
  374. setExpectedLotData(expectedLot);
  375. setScannedLotData(scannedLot);
  376. setLotConfirmationOpen(true);
  377. }, []);
  378. const checkAllLotsCompleted = useCallback((lotData: any[]) => {
  379. if (lotData.length === 0) {
  380. setAllLotsCompleted(false);
  381. return false;
  382. }
  383. // Filter out rejected lots
  384. const nonRejectedLots = lotData.filter(lot =>
  385. lot.lotAvailability !== 'rejected' &&
  386. lot.stockOutLineStatus !== 'rejected'
  387. );
  388. if (nonRejectedLots.length === 0) {
  389. setAllLotsCompleted(false);
  390. return false;
  391. }
  392. // Check if all non-rejected lots are completed
  393. const allCompleted = nonRejectedLots.every(lot =>
  394. lot.stockOutLineStatus === 'completed'
  395. );
  396. setAllLotsCompleted(allCompleted);
  397. return allCompleted;
  398. }, []);
  399. const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdOverride?: number) => {
  400. setCombinedDataLoading(true);
  401. try {
  402. const userIdToUse = userId || currentUserId;
  403. console.log("🔍 fetchAllCombinedLotData called with userId:", userIdToUse);
  404. if (!userIdToUse) {
  405. console.warn("⚠️ No userId available, skipping API call");
  406. setCombinedLotData([]);
  407. setOriginalCombinedData([]);
  408. setAllLotsCompleted(false);
  409. return;
  410. }
  411. // ✅ 获取新结构的层级数据
  412. const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse);
  413. console.log("✅ Hierarchical data (new structure):", hierarchicalData);
  414. // ✅ 检查数据结构
  415. if (!hierarchicalData.fgInfo || !hierarchicalData.pickOrders) {
  416. console.warn("⚠️ No FG info or pick orders found");
  417. setCombinedLotData([]);
  418. setOriginalCombinedData([]);
  419. setAllLotsCompleted(false);
  420. return;
  421. }
  422. // ✅ 设置 FG info 到 fgPickOrders(用于显示 FG 信息卡片)
  423. const fgOrder: FGPickOrderResponse = {
  424. doPickOrderId: hierarchicalData.fgInfo.doPickOrderId,
  425. ticketNo: hierarchicalData.fgInfo.ticketNo,
  426. storeId: hierarchicalData.fgInfo.storeId,
  427. shopCode: hierarchicalData.fgInfo.shopCode,
  428. shopName: hierarchicalData.fgInfo.shopName,
  429. truckLanceCode: hierarchicalData.fgInfo.truckLanceCode,
  430. DepartureTime: hierarchicalData.fgInfo.departureTime,
  431. shopAddress: "",
  432. // ✅ 从第一个 pick order 获取兼容字段
  433. pickOrderId: hierarchicalData.pickOrders[0]?.pickOrderId || 0,
  434. pickOrderCode: hierarchicalData.pickOrders[0]?.pickOrderCode || "",
  435. pickOrderConsoCode: hierarchicalData.pickOrders[0]?.consoCode || "",
  436. pickOrderTargetDate: hierarchicalData.pickOrders[0]?.targetDate || "",
  437. pickOrderStatus: hierarchicalData.pickOrders[0]?.status || "",
  438. deliveryOrderId: hierarchicalData.pickOrders[0]?.doOrderId || 0,
  439. deliveryNo: hierarchicalData.pickOrders[0]?.deliveryOrderCode || "",
  440. deliveryDate: "",
  441. shopId: 0,
  442. shopPoNo: "",
  443. numberOfCartons: 0,
  444. qrCodeData: hierarchicalData.fgInfo.doPickOrderId,
  445. // ✅ 新增:多个 pick orders 信息
  446. numberOfPickOrders: hierarchicalData.pickOrders.length,
  447. pickOrderIds: hierarchicalData.pickOrders.map((po: any) => po.pickOrderId),
  448. pickOrderCodes: hierarchicalData.pickOrders.map((po: any) => po.pickOrderCode).join(", "),
  449. deliveryOrderIds: hierarchicalData.pickOrders.map((po: any) => po.doOrderId),
  450. deliveryNos: hierarchicalData.pickOrders.map((po: any) => po.deliveryOrderCode).join(", ")
  451. };
  452. setFgPickOrders([fgOrder]);
  453. // ✅ 构建 doPickOrderDetail(用于 switcher)
  454. if (hierarchicalData.pickOrders.length > 1) {
  455. const detail: DoPickOrderDetail = {
  456. doPickOrder: {
  457. id: hierarchicalData.fgInfo.doPickOrderId,
  458. store_id: hierarchicalData.fgInfo.storeId,
  459. ticket_no: hierarchicalData.fgInfo.ticketNo,
  460. ticket_status: "",
  461. truck_id: 0,
  462. truck_departure_time: hierarchicalData.fgInfo.departureTime,
  463. shop_id: 0,
  464. handled_by: null,
  465. loading_sequence: 0,
  466. ticket_release_time: null,
  467. TruckLanceCode: hierarchicalData.fgInfo.truckLanceCode,
  468. ShopCode: hierarchicalData.fgInfo.shopCode,
  469. ShopName: hierarchicalData.fgInfo.shopName,
  470. RequiredDeliveryDate: ""
  471. },
  472. pickOrders: hierarchicalData.pickOrders.map((po: any) => ({
  473. pick_order_id: po.pickOrderId,
  474. pick_order_code: po.pickOrderCode,
  475. do_order_id: po.doOrderId,
  476. delivery_order_code: po.deliveryOrderCode,
  477. consoCode: po.consoCode,
  478. status: po.status,
  479. targetDate: po.targetDate
  480. })),
  481. selectedPickOrderId: pickOrderIdOverride || hierarchicalData.pickOrders[0]?.pickOrderId || 0,
  482. lotDetails: []
  483. };
  484. setDoPickOrderDetail(detail);
  485. // ✅ 设置默认选中的 pick order ID
  486. if (!selectedPickOrderId) {
  487. setSelectedPickOrderId(pickOrderIdOverride || hierarchicalData.pickOrders[0]?.pickOrderId);
  488. }
  489. }
  490. // ✅ 确定要显示的 pick order
  491. const targetPickOrderId = pickOrderIdOverride || selectedPickOrderId || hierarchicalData.pickOrders[0]?.pickOrderId;
  492. // ✅ 找到对应的 pick order 数据
  493. const targetPickOrder = hierarchicalData.pickOrders.find((po: any) =>
  494. po.pickOrderId === targetPickOrderId
  495. );
  496. if (!targetPickOrder) {
  497. console.warn("⚠️ Target pick order not found:", targetPickOrderId);
  498. setCombinedLotData([]);
  499. setOriginalCombinedData([]);
  500. setAllLotsCompleted(false);
  501. return;
  502. }
  503. console.log("🎯 Displaying pick order:", targetPickOrder.pickOrderCode);
  504. // ✅ 将层级数据转换为平铺格式(用于表格显示)
  505. const flatLotData: any[] = [];
  506. targetPickOrder.pickOrderLines.forEach((line: any) => {
  507. if (line.lots && line.lots.length > 0) {
  508. // ✅ 有 lots 的情况
  509. line.lots.forEach((lot: any) => {
  510. flatLotData.push({
  511. pickOrderId: targetPickOrder.pickOrderId,
  512. pickOrderCode: targetPickOrder.pickOrderCode,
  513. pickOrderConsoCode: targetPickOrder.consoCode,
  514. pickOrderTargetDate: targetPickOrder.targetDate,
  515. pickOrderStatus: targetPickOrder.status,
  516. pickOrderLineId: line.id,
  517. pickOrderLineRequiredQty: line.requiredQty,
  518. pickOrderLineStatus: line.status,
  519. itemId: line.item.id,
  520. itemCode: line.item.code,
  521. itemName: line.item.name,
  522. //uomCode: line.item.uomCode,
  523. uomDesc: line.item.uomDesc,
  524. uomShortDesc: line.item.uomShortDesc,
  525. lotId: lot.id,
  526. lotNo: lot.lotNo,
  527. expiryDate: lot.expiryDate,
  528. location: lot.location,
  529. stockUnit: lot.stockUnit,
  530. availableQty: lot.availableQty,
  531. requiredQty: lot.requiredQty,
  532. actualPickQty: lot.actualPickQty,
  533. inQty: lot.inQty,
  534. outQty: lot.outQty,
  535. holdQty: lot.holdQty,
  536. lotStatus: lot.lotStatus,
  537. lotAvailability: lot.lotAvailability,
  538. processingStatus: lot.processingStatus,
  539. suggestedPickLotId: lot.suggestedPickLotId,
  540. stockOutLineId: lot.stockOutLineId,
  541. stockOutLineStatus: lot.stockOutLineStatus,
  542. stockOutLineQty: lot.stockOutLineQty,
  543. routerId: lot.router?.id,
  544. routerIndex: lot.router?.index,
  545. routerRoute: lot.router?.route,
  546. routerArea: lot.router?.area,
  547. });
  548. });
  549. } else {
  550. // ✅ 没有 lots 的情况(null stock)- 也要显示
  551. flatLotData.push({
  552. pickOrderId: targetPickOrder.pickOrderId,
  553. pickOrderCode: targetPickOrder.pickOrderCode,
  554. pickOrderConsoCode: targetPickOrder.consoCode,
  555. pickOrderTargetDate: targetPickOrder.targetDate,
  556. pickOrderStatus: targetPickOrder.status,
  557. pickOrderLineId: line.id,
  558. pickOrderLineRequiredQty: line.requiredQty,
  559. pickOrderLineStatus: line.status,
  560. itemId: line.item.id,
  561. itemCode: line.item.code,
  562. itemName: line.item.name,
  563. //uomCode: line.item.uomCode,
  564. uomDesc: line.item.uomDesc,
  565. // ✅ Null stock 字段
  566. lotId: null,
  567. lotNo: null,
  568. expiryDate: null,
  569. location: null,
  570. stockUnit: line.item.uomDesc,
  571. availableQty: 0,
  572. requiredQty: line.requiredQty,
  573. actualPickQty: 0,
  574. inQty: 0,
  575. outQty: 0,
  576. holdQty: 0,
  577. lotStatus: 'unavailable',
  578. lotAvailability: 'insufficient_stock',
  579. processingStatus: 'pending',
  580. suggestedPickLotId: null,
  581. stockOutLineId: null,
  582. stockOutLineStatus: null,
  583. stockOutLineQty: 0,
  584. routerId: null,
  585. routerIndex: 999999, // ✅ 放到最后
  586. routerRoute: null,
  587. routerArea: null,
  588. uomShortDesc: line.item.uomShortDesc
  589. });
  590. }
  591. });
  592. console.log("✅ Transformed flat lot data:", flatLotData);
  593. console.log("🔍 Total items (including null stock):", flatLotData.length);
  594. setCombinedLotData(flatLotData);
  595. setOriginalCombinedData(flatLotData);
  596. checkAllLotsCompleted(flatLotData);
  597. } catch (error) {
  598. console.error("❌ Error fetching combined lot data:", error);
  599. setCombinedLotData([]);
  600. setOriginalCombinedData([]);
  601. setAllLotsCompleted(false);
  602. } finally {
  603. setCombinedDataLoading(false);
  604. }
  605. }, [currentUserId, selectedPickOrderId, checkAllLotsCompleted]);
  606. // ✅ Add effect to check completion when lot data changes
  607. useEffect(() => {
  608. if (combinedLotData.length > 0) {
  609. checkAllLotsCompleted(combinedLotData);
  610. }
  611. }, [combinedLotData, checkAllLotsCompleted]);
  612. // ✅ Add function to expose completion status to parent
  613. const getCompletionStatus = useCallback(() => {
  614. return allLotsCompleted;
  615. }, [allLotsCompleted]);
  616. // ✅ Expose completion status to parent component
  617. useEffect(() => {
  618. // Dispatch custom event with completion status
  619. const event = new CustomEvent('pickOrderCompletionStatus', {
  620. detail: {
  621. allLotsCompleted,
  622. tabIndex: 1 // ✅ 明确指定这是来自标签页 1 的事件
  623. }
  624. });
  625. window.dispatchEvent(event);
  626. }, [allLotsCompleted]);
  627. const handleLotConfirmation = useCallback(async () => {
  628. if (!expectedLotData || !scannedLotData || !selectedLotForQr) return;
  629. setIsConfirmingLot(true);
  630. try {
  631. let newLotLineId = scannedLotData?.inventoryLotLineId;
  632. if (!newLotLineId && scannedLotData?.stockInLineId) {
  633. const ld = await fetchLotDetail(scannedLotData.stockInLineId);
  634. newLotLineId = ld.inventoryLotLineId;
  635. }
  636. if (!newLotLineId) {
  637. console.error("No inventory lot line id for scanned lot");
  638. return;
  639. }
  640. await confirmLotSubstitution({
  641. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  642. stockOutLineId: selectedLotForQr.stockOutLineId,
  643. originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId,
  644. newInventoryLotLineId: newLotLineId
  645. });
  646. setQrScanError(false);
  647. setQrScanSuccess(false);
  648. setQrScanInput('');
  649. //setIsManualScanning(false);
  650. //stopScan();
  651. //resetScan();
  652. setProcessedQrCodes(new Set());
  653. setLastProcessedQr('');
  654. setQrModalOpen(false);
  655. setPickExecutionFormOpen(false);
  656. if(selectedLotForQr?.stockOutLineId){
  657. const stockOutLineUpdate = await updateStockOutLineStatus({
  658. id: selectedLotForQr.stockOutLineId,
  659. status: 'checked',
  660. qty: 0
  661. });
  662. }
  663. setLotConfirmationOpen(false);
  664. setExpectedLotData(null);
  665. setScannedLotData(null);
  666. setSelectedLotForQr(null);
  667. await fetchAllCombinedLotData();
  668. } catch (error) {
  669. console.error("Error confirming lot substitution:", error);
  670. } finally {
  671. setIsConfirmingLot(false);
  672. }
  673. }, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData]);
  674. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  675. console.log(`✅ Processing QR Code for lot: ${lotNo}`);
  676. // ✅ Use current data without refreshing to avoid infinite loop
  677. const currentLotData = combinedLotData;
  678. console.log(` Available lots:`, currentLotData.map(lot => lot.lotNo));
  679. const matchingLots = currentLotData.filter(lot =>
  680. lot.lotNo === lotNo ||
  681. lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
  682. );
  683. if (matchingLots.length === 0) {
  684. console.error(`❌ Lot not found: ${lotNo}`);
  685. setQrScanError(true);
  686. setQrScanSuccess(false);
  687. const availableLotNos = currentLotData.map(lot => lot.lotNo).join(', ');
  688. console.log(`❌ QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`);
  689. return;
  690. }
  691. console.log(`✅ Found ${matchingLots.length} matching lots:`, matchingLots);
  692. setQrScanError(false);
  693. try {
  694. let successCount = 0;
  695. let errorCount = 0;
  696. for (const matchingLot of matchingLots) {
  697. console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
  698. if (matchingLot.stockOutLineId) {
  699. const stockOutLineUpdate = await updateStockOutLineStatus({
  700. id: matchingLot.stockOutLineId,
  701. status: 'checked',
  702. qty: 0
  703. });
  704. console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate);
  705. // Treat multiple backend shapes as success (type-safe via any)
  706. const r: any = stockOutLineUpdate as any;
  707. const updateOk =
  708. r?.code === 'SUCCESS' ||
  709. typeof r?.id === 'number' ||
  710. r?.type === 'checked' ||
  711. r?.status === 'checked' ||
  712. typeof r?.entity?.id === 'number' ||
  713. r?.entity?.status === 'checked';
  714. if (updateOk) {
  715. successCount++;
  716. } else {
  717. errorCount++;
  718. }
  719. } else {
  720. const createStockOutLineData = {
  721. consoCode: matchingLot.pickOrderConsoCode,
  722. pickOrderLineId: matchingLot.pickOrderLineId,
  723. inventoryLotLineId: matchingLot.lotId,
  724. qty: 0
  725. };
  726. const createResult = await createStockOutLine(createStockOutLineData);
  727. console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult);
  728. if (createResult && createResult.code === "SUCCESS") {
  729. // Immediately set status to checked for new line
  730. let newSolId: number | undefined;
  731. const anyRes: any = createResult as any;
  732. if (typeof anyRes?.id === 'number') {
  733. newSolId = anyRes.id;
  734. } else if (anyRes?.entity) {
  735. newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id;
  736. }
  737. if (newSolId) {
  738. const setChecked = await updateStockOutLineStatus({
  739. id: newSolId,
  740. status: 'checked',
  741. qty: 0
  742. });
  743. if (setChecked && setChecked.code === "SUCCESS") {
  744. successCount++;
  745. } else {
  746. errorCount++;
  747. }
  748. } else {
  749. console.warn("Created stock out line but no ID returned; cannot set to checked");
  750. errorCount++;
  751. }
  752. } else {
  753. errorCount++;
  754. }
  755. }
  756. }
  757. // ✅ FIXED: Set refresh flag before refreshing data
  758. setIsRefreshingData(true);
  759. console.log("🔄 Refreshing data after QR code processing...");
  760. await fetchAllCombinedLotData();
  761. if (successCount > 0) {
  762. console.log(`✅ QR Code processing completed: ${successCount} updated/created`);
  763. setQrScanSuccess(true);
  764. setQrScanError(false);
  765. setQrScanInput(''); // Clear input after successful processing
  766. //setIsManualScanning(false);
  767. // stopScan();
  768. // resetScan();
  769. // ✅ Clear success state after a delay
  770. //setTimeout(() => {
  771. //setQrScanSuccess(false);
  772. //}, 2000);
  773. } else {
  774. console.error(`❌ QR Code processing failed: ${errorCount} errors`);
  775. setQrScanError(true);
  776. setQrScanSuccess(false);
  777. // ✅ Clear error state after a delay
  778. // setTimeout(() => {
  779. // setQrScanError(false);
  780. //}, 3000);
  781. }
  782. } catch (error) {
  783. console.error("❌ Error processing QR code:", error);
  784. setQrScanError(true);
  785. setQrScanSuccess(false);
  786. // ✅ Still refresh data even on error
  787. setIsRefreshingData(true);
  788. await fetchAllCombinedLotData();
  789. // ✅ Clear error state after a delay
  790. setTimeout(() => {
  791. setQrScanError(false);
  792. }, 3000);
  793. } finally {
  794. // ✅ Clear refresh flag after a short delay
  795. setTimeout(() => {
  796. setIsRefreshingData(false);
  797. }, 1000);
  798. }
  799. }, [combinedLotData, fetchAllCombinedLotData]);
  800. const processOutsideQrCode = useCallback(async (latestQr: string) => {
  801. // 1) Parse JSON safely
  802. let qrData: any = null;
  803. try {
  804. qrData = JSON.parse(latestQr);
  805. } catch {
  806. console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches.");
  807. setQrScanError(true);
  808. setQrScanSuccess(false);
  809. return;
  810. }
  811. try {
  812. // Only use the new API when we have JSON with stockInLineId + itemId
  813. if (!(qrData?.stockInLineId && qrData?.itemId)) {
  814. console.log("QR JSON missing required fields (itemId, stockInLineId).");
  815. setQrScanError(true);
  816. setQrScanSuccess(false);
  817. return;
  818. }
  819. // Call new analyze-qr-code API
  820. const analysis = await analyzeQrCode({
  821. itemId: qrData.itemId,
  822. stockInLineId: qrData.stockInLineId
  823. });
  824. if (!analysis) {
  825. console.error("analyzeQrCode returned no data");
  826. setQrScanError(true);
  827. setQrScanSuccess(false);
  828. return;
  829. }
  830. const {
  831. itemId: analyzedItemId,
  832. itemCode: analyzedItemCode,
  833. itemName: analyzedItemName,
  834. scanned,
  835. } = analysis || {};
  836. // 1) Find all lots for the same item from current expected list
  837. const sameItemLotsInExpected = combinedLotData.filter(l =>
  838. (l.itemId && analyzedItemId && l.itemId === analyzedItemId) ||
  839. (l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode)
  840. );
  841. if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) {
  842. // Case 3: No item code match
  843. console.error("No item match in expected lots for scanned code");
  844. setQrScanError(true);
  845. setQrScanSuccess(false);
  846. return;
  847. }
  848. // ✅ FIXED: Find the ACTIVE suggested lot (not rejected lots)
  849. const activeSuggestedLots = sameItemLotsInExpected.filter(lot =>
  850. lot.lotAvailability !== 'rejected' &&
  851. lot.stockOutLineStatus !== 'rejected' &&
  852. lot.processingStatus !== 'rejected'
  853. );
  854. if (activeSuggestedLots.length === 0) {
  855. console.error("No active suggested lots found for this item");
  856. setQrScanError(true);
  857. setQrScanSuccess(false);
  858. return;
  859. }
  860. // 2) Check if scanned lot is exactly in active suggested lots
  861. const exactLotMatch = activeSuggestedLots.find(l =>
  862. (scanned?.inventoryLotLineId && l.lotId === scanned.inventoryLotLineId) ||
  863. (scanned?.lotNo && l.lotNo === scanned.lotNo)
  864. );
  865. if (exactLotMatch && scanned?.lotNo) {
  866. // Case 1: Normal case - item matches AND lot matches -> proceed
  867. console.log(`Exact lot match found for ${scanned.lotNo}, submitting QR`);
  868. handleQrCodeSubmit(scanned.lotNo);
  869. return;
  870. }
  871. // Case 2: Item matches but lot number differs -> open confirmation modal
  872. // ✅ FIXED: Use the first ACTIVE suggested lot, not just any lot
  873. const expectedLot = activeSuggestedLots[0];
  874. if (!expectedLot) {
  875. console.error("Could not determine expected lot for confirmation");
  876. setQrScanError(true);
  877. setQrScanSuccess(false);
  878. return;
  879. }
  880. // ✅ Check if the expected lot is already the scanned lot (after substitution)
  881. if (expectedLot.lotNo === scanned?.lotNo) {
  882. console.log(`Lot already substituted, proceeding with ${scanned.lotNo}`);
  883. handleQrCodeSubmit(scanned.lotNo);
  884. return;
  885. }
  886. console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`);
  887. setSelectedLotForQr(expectedLot);
  888. handleLotMismatch(
  889. {
  890. lotNo: expectedLot.lotNo,
  891. itemCode: analyzedItemCode || expectedLot.itemCode,
  892. itemName: analyzedItemName || expectedLot.itemName
  893. },
  894. {
  895. lotNo: scanned?.lotNo || '',
  896. itemCode: analyzedItemCode || expectedLot.itemCode,
  897. itemName: analyzedItemName || expectedLot.itemName,
  898. inventoryLotLineId: scanned?.inventoryLotLineId,
  899. stockInLineId: qrData.stockInLineId
  900. }
  901. );
  902. } catch (error) {
  903. console.error("Error during analyzeQrCode flow:", error);
  904. setQrScanError(true);
  905. setQrScanSuccess(false);
  906. return;
  907. }
  908. }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch]);
  909. // ✅ Update the outside QR scanning effect to use enhanced processing
  910. // ✅ Update the outside QR scanning effect to use enhanced processing
  911. useEffect(() => {
  912. if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) {
  913. return;
  914. }
  915. const latestQr = qrValues[qrValues.length - 1];
  916. if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) {
  917. console.log("QR code already processed, skipping...");
  918. return;
  919. }
  920. if (latestQr && latestQr !== lastProcessedQr) {
  921. console.log(`🔍 Processing new QR code with enhanced validation: ${latestQr}`);
  922. setLastProcessedQr(latestQr);
  923. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  924. processOutsideQrCode(latestQr);
  925. }
  926. }, [qrValues, isManualScanning, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode, combinedLotData]);
  927. // ✅ Only fetch existing data when session is ready, no auto-assignment
  928. useEffect(() => {
  929. if (session && currentUserId && !initializationRef.current) {
  930. console.log("✅ Session loaded, initializing pick order...");
  931. initializationRef.current = true;
  932. // ✅ Only fetch existing data, no auto-assignment
  933. fetchAllCombinedLotData();
  934. }
  935. }, [session, currentUserId, fetchAllCombinedLotData]);
  936. // ✅ Add event listener for manual assignment
  937. useEffect(() => {
  938. const handlePickOrderAssigned = () => {
  939. console.log("🔄 Pick order assigned event received, refreshing data...");
  940. fetchAllCombinedLotData();
  941. };
  942. window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
  943. return () => {
  944. window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
  945. };
  946. }, [fetchAllCombinedLotData]);
  947. const handleManualInputSubmit = useCallback(() => {
  948. if (qrScanInput.trim() !== '') {
  949. handleQrCodeSubmit(qrScanInput.trim());
  950. }
  951. }, [qrScanInput, handleQrCodeSubmit]);
  952. // ✅ Handle QR code submission from modal (internal scanning)
  953. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  954. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  955. console.log(`✅ QR Code verified for lot: ${lotNo}`);
  956. const requiredQty = selectedLotForQr.requiredQty;
  957. const lotId = selectedLotForQr.lotId;
  958. // Create stock out line
  959. try {
  960. const stockOutLineUpdate = await updateStockOutLineStatus({
  961. id: selectedLotForQr.stockOutLineId,
  962. status: 'checked',
  963. qty: selectedLotForQr.stockOutLineQty || 0
  964. });
  965. console.log("Stock out line updated successfully!");
  966. setQrScanSuccess(true);
  967. setQrScanError(false);
  968. // Close modal
  969. setQrModalOpen(false);
  970. setSelectedLotForQr(null);
  971. // Set pick quantity
  972. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  973. setTimeout(() => {
  974. setPickQtyData(prev => ({
  975. ...prev,
  976. [lotKey]: requiredQty
  977. }));
  978. console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  979. }, 500);
  980. // Refresh data
  981. await fetchAllCombinedLotData();
  982. } catch (error) {
  983. console.error("Error creating stock out line:", error);
  984. }
  985. }
  986. }, [selectedLotForQr, fetchAllCombinedLotData]);
  987. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  988. if (value === '' || value === null || value === undefined) {
  989. setPickQtyData(prev => ({
  990. ...prev,
  991. [lotKey]: 0
  992. }));
  993. return;
  994. }
  995. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  996. if (isNaN(numericValue)) {
  997. setPickQtyData(prev => ({
  998. ...prev,
  999. [lotKey]: 0
  1000. }));
  1001. return;
  1002. }
  1003. setPickQtyData(prev => ({
  1004. ...prev,
  1005. [lotKey]: numericValue
  1006. }));
  1007. }, []);
  1008. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  1009. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  1010. const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
  1011. const checkAndAutoAssignNext = useCallback(async () => {
  1012. if (!currentUserId) return;
  1013. try {
  1014. const completionResponse = await checkPickOrderCompletion(currentUserId);
  1015. if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
  1016. console.log("Found completed pick orders, auto-assigning next...");
  1017. // ✅ 移除前端的自动分配逻辑,因为后端已经处理了
  1018. // await handleAutoAssignAndRelease(); // 删除这个函数
  1019. }
  1020. } catch (error) {
  1021. console.error("Error checking pick order completion:", error);
  1022. }
  1023. }, [currentUserId]);
  1024. // ✅ Handle submit pick quantity
  1025. const handleSubmitPickQty = useCallback(async (lot: any) => {
  1026. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  1027. const newQty = pickQtyData[lotKey] || 0;
  1028. if (!lot.stockOutLineId) {
  1029. console.error("No stock out line found for this lot");
  1030. return;
  1031. }
  1032. try {
  1033. // ✅ FIXED: Calculate cumulative quantity correctly
  1034. const currentActualPickQty = lot.actualPickQty || 0;
  1035. const cumulativeQty = currentActualPickQty + newQty;
  1036. // ✅ FIXED: Determine status based on cumulative quantity vs required quantity
  1037. let newStatus = 'partially_completed';
  1038. if (cumulativeQty >= lot.requiredQty) {
  1039. newStatus = 'completed';
  1040. } else if (cumulativeQty > 0) {
  1041. newStatus = 'partially_completed';
  1042. } else {
  1043. newStatus = 'checked'; // QR scanned but no quantity submitted yet
  1044. }
  1045. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  1046. console.log(`Lot: ${lot.lotNo}`);
  1047. console.log(`Required Qty: ${lot.requiredQty}`);
  1048. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  1049. console.log(`New Submitted Qty: ${newQty}`);
  1050. console.log(`Cumulative Qty: ${cumulativeQty}`);
  1051. console.log(`New Status: ${newStatus}`);
  1052. console.log(`=====================================`);
  1053. await updateStockOutLineStatus({
  1054. id: lot.stockOutLineId,
  1055. status: newStatus,
  1056. qty: cumulativeQty // ✅ Use cumulative quantity
  1057. });
  1058. if (newQty > 0) {
  1059. await updateInventoryLotLineQuantities({
  1060. inventoryLotLineId: lot.lotId,
  1061. qty: newQty,
  1062. status: 'available',
  1063. operation: 'pick'
  1064. });
  1065. }
  1066. // ✅ Check if pick order is completed when lot status becomes 'completed'
  1067. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  1068. console.log(`✅ Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1069. try {
  1070. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1071. console.log(`✅ Pick order completion check result:`, completionResponse);
  1072. if (completionResponse.code === "SUCCESS") {
  1073. console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1074. } else if (completionResponse.message === "not completed") {
  1075. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1076. } else {
  1077. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1078. }
  1079. } catch (error) {
  1080. console.error("Error checking pick order completion:", error);
  1081. }
  1082. }
  1083. await fetchAllCombinedLotData();
  1084. console.log("Pick quantity submitted successfully!");
  1085. setTimeout(() => {
  1086. checkAndAutoAssignNext();
  1087. }, 1000);
  1088. } catch (error) {
  1089. console.error("Error submitting pick quantity:", error);
  1090. }
  1091. }, [pickQtyData, fetchAllCombinedLotData, checkAndAutoAssignNext]);
  1092. // ✅ Handle reject lot
  1093. const handleRejectLot = useCallback(async (lot: any) => {
  1094. if (!lot.stockOutLineId) {
  1095. console.error("No stock out line found for this lot");
  1096. return;
  1097. }
  1098. try {
  1099. await updateStockOutLineStatus({
  1100. id: lot.stockOutLineId,
  1101. status: 'rejected',
  1102. qty: 0
  1103. });
  1104. await fetchAllCombinedLotData();
  1105. console.log("Lot rejected successfully!");
  1106. setTimeout(() => {
  1107. checkAndAutoAssignNext();
  1108. }, 1000);
  1109. } catch (error) {
  1110. console.error("Error rejecting lot:", error);
  1111. }
  1112. }, [fetchAllCombinedLotData, checkAndAutoAssignNext]);
  1113. // ✅ Handle pick execution form
  1114. const handlePickExecutionForm = useCallback((lot: any) => {
  1115. console.log("=== Pick Execution Form ===");
  1116. console.log("Lot data:", lot);
  1117. if (!lot) {
  1118. console.warn("No lot data provided for pick execution form");
  1119. return;
  1120. }
  1121. console.log("Opening pick execution form for lot:", lot.lotNo);
  1122. setSelectedLotForExecutionForm(lot);
  1123. setPickExecutionFormOpen(true);
  1124. console.log("Pick execution form opened for lot ID:", lot.lotId);
  1125. }, []);
  1126. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  1127. try {
  1128. console.log("Pick execution form submitted:", data);
  1129. const issueData = {
  1130. ...data,
  1131. type: "Do", // Delivery Order Record 类型
  1132. pickerName: session?.user?.name || '',
  1133. };
  1134. const result = await recordPickExecutionIssue(issueData);
  1135. console.log("Pick execution issue recorded:", result);
  1136. if (result && result.code === "SUCCESS") {
  1137. console.log("✅ Pick execution issue recorded successfully");
  1138. } else {
  1139. console.error("❌ Failed to record pick execution issue:", result);
  1140. }
  1141. setPickExecutionFormOpen(false);
  1142. setSelectedLotForExecutionForm(null);
  1143. setQrScanError(false);
  1144. setQrScanSuccess(false);
  1145. setQrScanInput('');
  1146. setIsManualScanning(false);
  1147. stopScan();
  1148. resetScan();
  1149. setProcessedQrCodes(new Set());
  1150. setLastProcessedQr('');
  1151. await fetchAllCombinedLotData();
  1152. } catch (error) {
  1153. console.error("Error submitting pick execution form:", error);
  1154. }
  1155. }, [fetchAllCombinedLotData]);
  1156. // ✅ Calculate remaining required quantity
  1157. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  1158. const requiredQty = lot.requiredQty || 0;
  1159. const stockOutLineQty = lot.stockOutLineQty || 0;
  1160. return Math.max(0, requiredQty - stockOutLineQty);
  1161. }, []);
  1162. // Search criteria
  1163. const searchCriteria: Criterion<any>[] = [
  1164. {
  1165. label: t("Pick Order Code"),
  1166. paramName: "pickOrderCode",
  1167. type: "text",
  1168. },
  1169. {
  1170. label: t("Item Code"),
  1171. paramName: "itemCode",
  1172. type: "text",
  1173. },
  1174. {
  1175. label: t("Item Name"),
  1176. paramName: "itemName",
  1177. type: "text",
  1178. },
  1179. {
  1180. label: t("Lot No"),
  1181. paramName: "lotNo",
  1182. type: "text",
  1183. },
  1184. ];
  1185. const handleSearch = useCallback((query: Record<string, any>) => {
  1186. setSearchQuery({ ...query });
  1187. console.log("Search query:", query);
  1188. if (!originalCombinedData) return;
  1189. const filtered = originalCombinedData.filter((lot: any) => {
  1190. const pickOrderCodeMatch = !query.pickOrderCode ||
  1191. lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  1192. const itemCodeMatch = !query.itemCode ||
  1193. lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
  1194. const itemNameMatch = !query.itemName ||
  1195. lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
  1196. const lotNoMatch = !query.lotNo ||
  1197. lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
  1198. return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
  1199. });
  1200. setCombinedLotData(filtered);
  1201. console.log("Filtered lots count:", filtered.length);
  1202. }, [originalCombinedData]);
  1203. const handleReset = useCallback(() => {
  1204. setSearchQuery({});
  1205. if (originalCombinedData) {
  1206. setCombinedLotData(originalCombinedData);
  1207. }
  1208. }, [originalCombinedData]);
  1209. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  1210. setPaginationController(prev => ({
  1211. ...prev,
  1212. pageNum: newPage,
  1213. }));
  1214. }, []);
  1215. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  1216. const newPageSize = parseInt(event.target.value, 10);
  1217. setPaginationController({
  1218. pageNum: 0,
  1219. pageSize: newPageSize,
  1220. });
  1221. }, []);
  1222. // Pagination data with sorting by routerIndex
  1223. // Remove the sorting logic and just do pagination
  1224. const paginatedData = useMemo(() => {
  1225. const startIndex = paginationController.pageNum * paginationController.pageSize;
  1226. const endIndex = startIndex + paginationController.pageSize;
  1227. return combinedLotData.slice(startIndex, endIndex); // ✅ No sorting needed
  1228. }, [combinedLotData, paginationController]);
  1229. const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
  1230. if (!lot.stockOutLineId) {
  1231. console.error("No stock out line found for this lot");
  1232. return;
  1233. }
  1234. try {
  1235. // ✅ FIXED: Calculate cumulative quantity correctly
  1236. const currentActualPickQty = lot.actualPickQty || 0;
  1237. const cumulativeQty = currentActualPickQty + submitQty;
  1238. // ✅ FIXED: Determine status based on cumulative quantity vs required quantity
  1239. let newStatus = 'partially_completed';
  1240. if (cumulativeQty >= lot.requiredQty) {
  1241. newStatus = 'completed';
  1242. } else if (cumulativeQty > 0) {
  1243. newStatus = 'partially_completed';
  1244. } else {
  1245. newStatus = 'checked'; // QR scanned but no quantity submitted yet
  1246. }
  1247. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  1248. console.log(`Lot: ${lot.lotNo}`);
  1249. console.log(`Required Qty: ${lot.requiredQty}`);
  1250. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  1251. console.log(`New Submitted Qty: ${submitQty}`);
  1252. console.log(`Cumulative Qty: ${cumulativeQty}`);
  1253. console.log(`New Status: ${newStatus}`);
  1254. console.log(`=====================================`);
  1255. await updateStockOutLineStatus({
  1256. id: lot.stockOutLineId,
  1257. status: newStatus,
  1258. qty: cumulativeQty // ✅ Use cumulative quantity
  1259. });
  1260. if (submitQty > 0) {
  1261. await updateInventoryLotLineQuantities({
  1262. inventoryLotLineId: lot.lotId,
  1263. qty: submitQty,
  1264. status: 'available',
  1265. operation: 'pick'
  1266. });
  1267. }
  1268. // ✅ Check if pick order is completed when lot status becomes 'completed'
  1269. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  1270. console.log(`✅ Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1271. try {
  1272. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1273. console.log(`✅ Pick order completion check result:`, completionResponse);
  1274. if (completionResponse.code === "SUCCESS") {
  1275. console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1276. } else if (completionResponse.message === "not completed") {
  1277. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1278. } else {
  1279. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1280. }
  1281. } catch (error) {
  1282. console.error("Error checking pick order completion:", error);
  1283. }
  1284. }
  1285. await fetchAllCombinedLotData();
  1286. console.log("Pick quantity submitted successfully!");
  1287. setTimeout(() => {
  1288. checkAndAutoAssignNext();
  1289. }, 1000);
  1290. } catch (error) {
  1291. console.error("Error submitting pick quantity:", error);
  1292. }
  1293. }, [fetchAllCombinedLotData, checkAndAutoAssignNext]);
  1294. // ✅ Add these functions after line 395
  1295. const handleStartScan = useCallback(() => {
  1296. console.log(" Starting manual QR scan...");
  1297. setIsManualScanning(true);
  1298. setProcessedQrCodes(new Set());
  1299. setLastProcessedQr('');
  1300. setQrScanError(false);
  1301. setQrScanSuccess(false);
  1302. startScan();
  1303. }, [startScan]);
  1304. const handlePickOrderSwitch = useCallback(async (pickOrderId: number) => {
  1305. if (pickOrderSwitching) return;
  1306. setPickOrderSwitching(true);
  1307. try {
  1308. console.log("🔍 Switching to pick order:", pickOrderId);
  1309. setSelectedPickOrderId(pickOrderId);
  1310. // ✅ 强制刷新数据,确保显示正确的 pick order 数据
  1311. await fetchAllCombinedLotData(currentUserId, pickOrderId);
  1312. } catch (error) {
  1313. console.error("Error switching pick order:", error);
  1314. } finally {
  1315. setPickOrderSwitching(false);
  1316. }
  1317. }, [pickOrderSwitching, currentUserId, fetchAllCombinedLotData]);
  1318. const handleStopScan = useCallback(() => {
  1319. console.log("⏹️ Stopping manual QR scan...");
  1320. setIsManualScanning(false);
  1321. setQrScanError(false);
  1322. setQrScanSuccess(false);
  1323. stopScan();
  1324. resetScan();
  1325. }, [stopScan, resetScan]);
  1326. const handleSubmitAllScanned = useCallback(async () => {
  1327. const scannedLots = combinedLotData.filter(lot =>
  1328. lot.stockOutLineStatus === 'checked' // Only submit items that are scanned but not yet submitted
  1329. );
  1330. if (scannedLots.length === 0) {
  1331. console.log("No scanned items to submit");
  1332. return;
  1333. }
  1334. setIsSubmittingAll(true);
  1335. console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`);
  1336. try {
  1337. // ✅ Submit all items in parallel using Promise.all
  1338. const submitPromises = scannedLots.map(async (lot) => {
  1339. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  1340. const currentActualPickQty = lot.actualPickQty || 0;
  1341. const cumulativeQty = currentActualPickQty + submitQty;
  1342. let newStatus = 'partially_completed';
  1343. if (cumulativeQty >= lot.requiredQty) {
  1344. newStatus = 'completed';
  1345. }
  1346. console.log(`Submitting lot ${lot.lotNo}: qty=${cumulativeQty}, status=${newStatus}`);
  1347. // Update stock out line
  1348. await updateStockOutLineStatus({
  1349. id: lot.stockOutLineId,
  1350. status: newStatus,
  1351. qty: cumulativeQty
  1352. });
  1353. // Update inventory
  1354. if (submitQty > 0) {
  1355. await updateInventoryLotLineQuantities({
  1356. inventoryLotLineId: lot.lotId,
  1357. qty: submitQty,
  1358. status: 'available',
  1359. operation: 'pick'
  1360. });
  1361. }
  1362. // Check if pick order is completed
  1363. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  1364. await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1365. }
  1366. return { success: true, lotNo: lot.lotNo };
  1367. });
  1368. // ✅ Wait for all submissions to complete
  1369. const results = await Promise.all(submitPromises);
  1370. const successCount = results.filter(r => r.success).length;
  1371. console.log(`✅ Batch submit completed: ${successCount}/${scannedLots.length} items submitted`);
  1372. // ✅ Refresh data once after all submissions
  1373. await fetchAllCombinedLotData();
  1374. if (successCount > 0) {
  1375. setQrScanSuccess(true);
  1376. setTimeout(() => {
  1377. setQrScanSuccess(false);
  1378. checkAndAutoAssignNext();
  1379. }, 2000);
  1380. }
  1381. } catch (error) {
  1382. console.error("Error submitting all scanned items:", error);
  1383. setQrScanError(true);
  1384. } finally {
  1385. setIsSubmittingAll(false);
  1386. }
  1387. }, [combinedLotData, fetchAllCombinedLotData, checkAndAutoAssignNext]);
  1388. // ✅ Calculate scanned items count
  1389. const scannedItemsCount = useMemo(() => {
  1390. return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length;
  1391. }, [combinedLotData]);
  1392. // ✅ ADD THIS: Auto-stop scan when no data available
  1393. useEffect(() => {
  1394. if (isManualScanning && combinedLotData.length === 0) {
  1395. console.log("⏹️ No data available, auto-stopping QR scan...");
  1396. handleStopScan();
  1397. }
  1398. }, [combinedLotData.length, isManualScanning, handleStopScan]);
  1399. // ✅ Cleanup effect
  1400. useEffect(() => {
  1401. return () => {
  1402. // Cleanup when component unmounts (e.g., when switching tabs)
  1403. if (isManualScanning) {
  1404. console.log("🧹 Pick execution component unmounting, stopping QR scanner...");
  1405. stopScan();
  1406. resetScan();
  1407. }
  1408. };
  1409. }, [isManualScanning, stopScan, resetScan]);
  1410. const getStatusMessage = useCallback((lot: any) => {
  1411. switch (lot.stockOutLineStatus?.toLowerCase()) {
  1412. case 'pending':
  1413. return t("Please finish QR code scan and pick order.");
  1414. case 'checked':
  1415. return t("Please submit the pick order.");
  1416. case 'partially_completed':
  1417. return t("Partial quantity submitted. Please submit more or complete the order.");
  1418. case 'completed':
  1419. return t("Pick order completed successfully!");
  1420. case 'rejected':
  1421. return t("Lot has been rejected and marked as unavailable.");
  1422. case 'unavailable':
  1423. return t("This order is insufficient, please pick another lot.");
  1424. default:
  1425. return t("Please finish QR code scan and pick order.");
  1426. }
  1427. }, [t]);
  1428. return (
  1429. <TestQrCodeProvider
  1430. lotData={combinedLotData}
  1431. onScanLot={handleQrCodeSubmit}
  1432. filterActive={(lot) => (
  1433. lot.lotAvailability !== 'rejected' &&
  1434. lot.stockOutLineStatus !== 'rejected' &&
  1435. lot.stockOutLineStatus !== 'completed'
  1436. )}
  1437. >
  1438. <FormProvider {...formProps}>
  1439. <Stack spacing={2}>
  1440. {/* DO Header */}
  1441. {fgPickOrdersLoading ? (
  1442. <Box sx={{ display: 'flex', justifyContent: 'center', p: 2 }}>
  1443. <CircularProgress size={20} />
  1444. </Box>
  1445. ) : (
  1446. fgPickOrders.length > 0 && (
  1447. <Paper sx={{ p: 2 }}>
  1448. <Stack spacing={2}>
  1449. {/* 基本信息 */}
  1450. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  1451. <Typography variant="subtitle1">
  1452. <strong>{t("Shop Name")}:</strong> {fgPickOrders[0].shopName || '-'}
  1453. </Typography>
  1454. <Typography variant="subtitle1">
  1455. <strong>{t("Store ID")}:</strong> {fgPickOrders[0].storeId || '-'}
  1456. </Typography>
  1457. <Typography variant="subtitle1">
  1458. <strong>{t("Ticket No.")}:</strong> {fgPickOrders[0].ticketNo || '-'}
  1459. </Typography>
  1460. <Typography variant="subtitle1">
  1461. <strong>{t("Departure Time")}:</strong> {fgPickOrders[0].DepartureTime || '-'}
  1462. </Typography>
  1463. </Stack>
  1464. </Stack>
  1465. </Paper>
  1466. )
  1467. )}
  1468. <Box>
  1469. {/* ✅ FG Info Card */}
  1470. {/* ✅ Pick Order Switcher - 放在 FG Info 下面,QR 按钮上面 */}
  1471. {doPickOrderDetail && doPickOrderDetail.pickOrders.length > 1 && (
  1472. <Box sx={{ mb: 2, mt: 1 }}>
  1473. <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
  1474. {t("Select Pick Order:")}
  1475. </Typography>
  1476. <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
  1477. {doPickOrderDetail.pickOrders.map((po: any) => (
  1478. <Chip
  1479. key={po.pick_order_id}
  1480. label={`${po.pick_order_code} (${po.delivery_order_code})`}
  1481. onClick={() => handlePickOrderSwitch(po.pick_order_id)}
  1482. color={selectedPickOrderId === po.pick_order_id ? "primary" : "default"}
  1483. variant={selectedPickOrderId === po.pick_order_id ? "filled" : "outlined"}
  1484. sx={{
  1485. cursor: 'pointer',
  1486. '&:hover': { backgroundColor: 'primary.light', color: 'white' }
  1487. }}
  1488. />
  1489. ))}
  1490. </Box>
  1491. </Box>
  1492. )}
  1493. </Box>
  1494. {/* ✅ 保留:Combined Lot Table - 包含所有 QR 扫描功能 */}
  1495. <Box>
  1496. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
  1497. <Typography variant="h6" gutterBottom sx={{ mb: 0 }}>
  1498. {t("All Pick Order Lots")}
  1499. </Typography>
  1500. <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
  1501. {!isManualScanning ? (
  1502. <Button
  1503. variant="contained"
  1504. startIcon={<QrCodeIcon />}
  1505. onClick={handleStartScan}
  1506. color="primary"
  1507. sx={{ minWidth: '120px' }}
  1508. >
  1509. {t("Start QR Scan")}
  1510. </Button>
  1511. ) : (
  1512. <Button
  1513. variant="outlined"
  1514. startIcon={<QrCodeIcon />}
  1515. onClick={handleStopScan}
  1516. color="secondary"
  1517. sx={{ minWidth: '120px' }}
  1518. >
  1519. {t("Stop QR Scan")}
  1520. </Button>
  1521. )}
  1522. {/* ✅ 保留:Submit All Scanned Button */}
  1523. <Button
  1524. variant="contained"
  1525. color="success"
  1526. onClick={handleSubmitAllScanned}
  1527. disabled={scannedItemsCount === 0 || isSubmittingAll}
  1528. sx={{ minWidth: '160px' }}
  1529. >
  1530. {isSubmittingAll ? (
  1531. <>
  1532. <CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
  1533. {t("Submitting...")}
  1534. </>
  1535. ) : (
  1536. `${t("Submit All Scanned")} (${scannedItemsCount})`
  1537. )}
  1538. </Button>
  1539. </Box>
  1540. </Box>
  1541. {qrScanError && !qrScanSuccess && (
  1542. <Alert severity="error" sx={{ mb: 2 }}>
  1543. {t("QR code does not match any item in current orders.")}
  1544. </Alert>
  1545. )}
  1546. {qrScanSuccess && (
  1547. <Alert severity="success" sx={{ mb: 2 }}>
  1548. {t("QR code verified.")}
  1549. </Alert>
  1550. )}
  1551. <TableContainer component={Paper}>
  1552. <Table>
  1553. <TableHead>
  1554. <TableRow>
  1555. <TableCell>{t("Index")}</TableCell>
  1556. <TableCell>{t("Route")}</TableCell>
  1557. <TableCell>{t("Item Code")}</TableCell>
  1558. <TableCell>{t("Item Name")}</TableCell>
  1559. <TableCell>{t("Lot#")}</TableCell>
  1560. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  1561. <TableCell align="center">{t("Scan Result")}</TableCell>
  1562. <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
  1563. </TableRow>
  1564. </TableHead>
  1565. <TableBody>
  1566. {paginatedData.length === 0 ? (
  1567. <TableRow>
  1568. <TableCell colSpan={11} align="center">
  1569. <Typography variant="body2" color="text.secondary">
  1570. {t("No data available")}
  1571. </Typography>
  1572. </TableCell>
  1573. </TableRow>
  1574. ) : (
  1575. // 在第 1797-1938 行之间,将整个 map 函数修改为:
  1576. paginatedData.map((lot, index) => {
  1577. // ✅ 检查是否是 issue lot
  1578. const isIssueLot = lot.stockOutLineStatus === 'rejected' || !lot.lotNo;
  1579. return (
  1580. <TableRow
  1581. key={`${lot.pickOrderLineId}-${lot.lotId || 'null'}`}
  1582. sx={{
  1583. //backgroundColor: isIssueLot ? '#fff3e0' : 'inherit',
  1584. // opacity: isIssueLot ? 0.6 : 1,
  1585. '& .MuiTableCell-root': {
  1586. //color: isIssueLot ? 'warning.main' : 'inherit'
  1587. }
  1588. }}
  1589. >
  1590. <TableCell>
  1591. <Typography variant="body2" fontWeight="bold">
  1592. {index + 1}
  1593. </Typography>
  1594. </TableCell>
  1595. <TableCell>
  1596. <Typography variant="body2">
  1597. {lot.routerRoute || '-'}
  1598. </Typography>
  1599. </TableCell>
  1600. <TableCell>{lot.itemCode}</TableCell>
  1601. <TableCell>{lot.itemName + '(' + lot.stockUnit + ')'}</TableCell>
  1602. <TableCell>
  1603. <Box>
  1604. <Typography
  1605. sx={{
  1606. // color: isIssueLot ? 'warning.main' : lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
  1607. }}
  1608. >
  1609. {lot.lotNo || t('⚠️ No Stock Available')}
  1610. </Typography>
  1611. </Box>
  1612. </TableCell>
  1613. <TableCell align="right">
  1614. {(() => {
  1615. const requiredQty = lot.requiredQty || 0;
  1616. return requiredQty.toLocaleString() + '(' + lot.uomShortDesc + ')';
  1617. })()}
  1618. </TableCell>
  1619. <TableCell align="center">
  1620. {/* ✅ Issue lot 不显示扫描状态 */}
  1621. {!isIssueLot && lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? (
  1622. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  1623. <Checkbox
  1624. checked={true}
  1625. disabled={true}
  1626. readOnly={true}
  1627. size="large"
  1628. sx={{
  1629. color: 'success.main',
  1630. '&.Mui-checked': { color: 'success.main' },
  1631. transform: 'scale(1.3)',
  1632. }}
  1633. />
  1634. </Box>
  1635. ) : isIssueLot ? (
  1636. null
  1637. ) : null}
  1638. </TableCell>
  1639. <TableCell align="center">
  1640. <Box sx={{ display: 'flex', justifyContent: 'center' }}>
  1641. {isIssueLot ? (
  1642. // ✅ Issue lot 只显示 Issue 按钮
  1643. <Button
  1644. variant="outlined"
  1645. size="small"
  1646. onClick={() => handlePickExecutionForm(lot)}
  1647. disabled={true}
  1648. sx={{
  1649. fontSize: '0.7rem',
  1650. py: 0.5,
  1651. minHeight: '28px',
  1652. minWidth: '60px',
  1653. borderColor: 'warning.main',
  1654. color: 'warning.main'
  1655. }}
  1656. title="Rejected lot - Issue only"
  1657. >
  1658. {t("Issue")}
  1659. </Button>
  1660. ) : (
  1661. // ✅ Normal lot 显示两个按钮
  1662. <Stack direction="row" spacing={1} alignItems="center">
  1663. <Button
  1664. variant="contained"
  1665. onClick={() => {
  1666. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  1667. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  1668. handlePickQtyChange(lotKey, submitQty);
  1669. handleSubmitPickQtyWithQty(lot, submitQty);
  1670. }}
  1671. disabled={
  1672. lot.lotAvailability === 'expired' ||
  1673. lot.lotAvailability === 'status_unavailable' ||
  1674. lot.lotAvailability === 'rejected' ||
  1675. lot.stockOutLineStatus === 'completed' ||
  1676. lot.stockOutLineStatus === 'pending'
  1677. }
  1678. sx={{ fontSize: '0.75rem', py: 0.5, minHeight: '28px', minWidth: '70px' }}
  1679. >
  1680. {t("Submit")}
  1681. </Button>
  1682. <Button
  1683. variant="outlined"
  1684. size="small"
  1685. onClick={() => handlePickExecutionForm(lot)}
  1686. disabled={
  1687. lot.lotAvailability === 'expired' ||
  1688. lot.lotAvailability === 'status_unavailable' ||
  1689. lot.lotAvailability === 'rejected' ||
  1690. lot.stockOutLineStatus === 'completed' ||
  1691. lot.stockOutLineStatus === 'pending'
  1692. }
  1693. sx={{
  1694. fontSize: '0.7rem',
  1695. py: 0.5,
  1696. minHeight: '28px',
  1697. minWidth: '60px',
  1698. borderColor: 'warning.main',
  1699. color: 'warning.main'
  1700. }}
  1701. title="Report missing or bad items"
  1702. >
  1703. {t("Issue")}
  1704. </Button>
  1705. </Stack>
  1706. )}
  1707. </Box>
  1708. </TableCell>
  1709. </TableRow>
  1710. );
  1711. })
  1712. )}
  1713. </TableBody>
  1714. </Table>
  1715. </TableContainer>
  1716. <TablePagination
  1717. component="div"
  1718. count={combinedLotData.length}
  1719. page={paginationController.pageNum}
  1720. rowsPerPage={paginationController.pageSize}
  1721. onPageChange={handlePageChange}
  1722. onRowsPerPageChange={handlePageSizeChange}
  1723. rowsPerPageOptions={[10, 25, 50]}
  1724. labelRowsPerPage={t("Rows per page")}
  1725. labelDisplayedRows={({ from, to, count }) =>
  1726. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  1727. }
  1728. />
  1729. </Box>
  1730. </Stack>
  1731. {/* ✅ 保留:QR Code Modal */}
  1732. <QrCodeModal
  1733. open={qrModalOpen}
  1734. onClose={() => {
  1735. setQrModalOpen(false);
  1736. setSelectedLotForQr(null);
  1737. stopScan();
  1738. resetScan();
  1739. }}
  1740. lot={selectedLotForQr}
  1741. combinedLotData={combinedLotData}
  1742. onQrCodeSubmit={handleQrCodeSubmitFromModal}
  1743. />
  1744. {/* ✅ 保留:Lot Confirmation Modal */}
  1745. {lotConfirmationOpen && expectedLotData && scannedLotData && (
  1746. <LotConfirmationModal
  1747. open={lotConfirmationOpen}
  1748. onClose={() => {
  1749. setLotConfirmationOpen(false);
  1750. setExpectedLotData(null);
  1751. setScannedLotData(null);
  1752. }}
  1753. onConfirm={handleLotConfirmation}
  1754. expectedLot={expectedLotData}
  1755. scannedLot={scannedLotData}
  1756. isLoading={isConfirmingLot}
  1757. />
  1758. )}
  1759. {/* ✅ 保留:Good Pick Execution Form Modal */}
  1760. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  1761. <GoodPickExecutionForm
  1762. open={pickExecutionFormOpen}
  1763. onClose={() => {
  1764. setPickExecutionFormOpen(false);
  1765. setSelectedLotForExecutionForm(null);
  1766. }}
  1767. onSubmit={handlePickExecutionFormSubmit}
  1768. selectedLot={selectedLotForExecutionForm}
  1769. selectedPickOrderLine={{
  1770. id: selectedLotForExecutionForm.pickOrderLineId,
  1771. itemId: selectedLotForExecutionForm.itemId,
  1772. itemCode: selectedLotForExecutionForm.itemCode,
  1773. itemName: selectedLotForExecutionForm.itemName,
  1774. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  1775. availableQty: selectedLotForExecutionForm.availableQty || 0,
  1776. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  1777. // uomCode: selectedLotForExecutionForm.uomCode || '',
  1778. uomDesc: selectedLotForExecutionForm.uomDesc || '',
  1779. pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
  1780. uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
  1781. suggestedList: []
  1782. }}
  1783. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  1784. pickOrderCreateDate={new Date()}
  1785. />
  1786. )}
  1787. </FormProvider>
  1788. </TestQrCodeProvider>
  1789. );
  1790. };
  1791. export default PickExecution;