FPSMS-frontend
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 

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