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.
 
 

1186 lines
40 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. TablePagination,
  18. Modal,
  19. } from "@mui/material";
  20. import { useCallback, useEffect, useState, useRef, useMemo } from "react";
  21. import { useTranslation } from "react-i18next";
  22. import { useRouter } from "next/navigation";
  23. import {
  24. fetchAllPickOrderLotsHierarchical,
  25. updateStockOutLineStatus,
  26. createStockOutLine,
  27. recordPickExecutionIssue,
  28. fetchFGPickOrdersByUserId, // Add this import
  29. FGPickOrderResponse,
  30. autoAssignAndReleasePickOrder,
  31. AutoAssignReleaseResponse,
  32. checkPickOrderCompletion,
  33. PickOrderCompletionResponse,
  34. checkAndCompletePickOrderByConsoCode,
  35. fetchDoPickOrderDetail,
  36. DoPickOrderDetail,
  37. batchQrSubmit,
  38. } from "@/app/api/pickOrder/actions";
  39. import { fetchNameList, NameList } from "@/app/api/user/actions";
  40. import {
  41. FormProvider,
  42. useForm,
  43. } from "react-hook-form";
  44. import SearchBox, { Criterion } from "../SearchBox";
  45. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  46. import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  47. import QrCodeIcon from '@mui/icons-material/QrCode';
  48. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  49. import { useSession } from "next-auth/react";
  50. import { SessionWithTokens } from "@/config/authConfig";
  51. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  52. import GoodPickExecutionForm from "./GoodPickExecutionForm";
  53. import FGPickOrderCard from "./FGPickOrderCard";
  54. import FinishedGoodFloorLanePanel from "./FinishedGoodFloorLanePanel";
  55. import FGPickOrderInfoCard from "./FGPickOrderInfoCard";
  56. import GoodPickExecutiondetail from "./GoodPickExecutiondetail";
  57. interface Props {
  58. filterArgs: Record<string, any>;
  59. onFgPickOrdersChange?: (fgPickOrders: FGPickOrderResponse[]) => void;
  60. onSwitchToDetailTab?: () => void;
  61. }
  62. // QR Code Modal Component (from LotTable)
  63. const QrCodeModal: React.FC<{
  64. open: boolean;
  65. onClose: () => void;
  66. lot: any | null;
  67. onQrCodeSubmit: (lotNo: string) => void;
  68. combinedLotData: any[]; // Add this prop
  69. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
  70. const { t } = useTranslation("pickOrder");
  71. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  72. const [manualInput, setManualInput] = useState<string>('');
  73. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  74. const [manualInputError, setManualInputError] = useState<boolean>(false);
  75. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  76. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  77. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  78. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  79. const [scannedQrResult, setScannedQrResult] = useState<string>('');
  80. const [fgPickOrder, setFgPickOrder] = useState<FGPickOrderResponse | null>(null);
  81. // Process scanned QR codes
  82. useEffect(() => {
  83. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  84. const latestQr = qrValues[qrValues.length - 1];
  85. if (processedQrCodes.has(latestQr)) {
  86. console.log("QR code already processed, skipping...");
  87. return;
  88. }
  89. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  90. try {
  91. const qrData = JSON.parse(latestQr);
  92. if (qrData.stockInLineId && qrData.itemId) {
  93. setIsProcessingQr(true);
  94. setQrScanFailed(false);
  95. fetchStockInLineInfo(qrData.stockInLineId)
  96. .then((stockInLineInfo) => {
  97. console.log("Stock in line info:", stockInLineInfo);
  98. setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
  99. if (stockInLineInfo.lotNo === lot.lotNo) {
  100. console.log(` QR Code verified for lot: ${lot.lotNo}`);
  101. setQrScanSuccess(true);
  102. onQrCodeSubmit(lot.lotNo);
  103. onClose();
  104. resetScan();
  105. } else {
  106. console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
  107. setQrScanFailed(true);
  108. setManualInputError(true);
  109. setManualInputSubmitted(true);
  110. }
  111. })
  112. .catch((error) => {
  113. console.error("Error fetching stock in line info:", error);
  114. setScannedQrResult('Error fetching data');
  115. setQrScanFailed(true);
  116. setManualInputError(true);
  117. setManualInputSubmitted(true);
  118. })
  119. .finally(() => {
  120. setIsProcessingQr(false);
  121. });
  122. } else {
  123. const qrContent = latestQr.replace(/[{}]/g, '');
  124. setScannedQrResult(qrContent);
  125. if (qrContent === lot.lotNo) {
  126. setQrScanSuccess(true);
  127. onQrCodeSubmit(lot.lotNo);
  128. onClose();
  129. resetScan();
  130. } else {
  131. setQrScanFailed(true);
  132. setManualInputError(true);
  133. setManualInputSubmitted(true);
  134. }
  135. }
  136. } catch (error) {
  137. console.log("QR code is not JSON format, trying direct comparison");
  138. const qrContent = latestQr.replace(/[{}]/g, '');
  139. setScannedQrResult(qrContent);
  140. if (qrContent === lot.lotNo) {
  141. setQrScanSuccess(true);
  142. onQrCodeSubmit(lot.lotNo);
  143. onClose();
  144. resetScan();
  145. } else {
  146. setQrScanFailed(true);
  147. setManualInputError(true);
  148. setManualInputSubmitted(true);
  149. }
  150. }
  151. }
  152. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]);
  153. // Clear states when modal opens
  154. useEffect(() => {
  155. if (open) {
  156. setManualInput('');
  157. setManualInputSubmitted(false);
  158. setManualInputError(false);
  159. setIsProcessingQr(false);
  160. setQrScanFailed(false);
  161. setQrScanSuccess(false);
  162. setScannedQrResult('');
  163. setProcessedQrCodes(new Set());
  164. }
  165. }, [open]);
  166. useEffect(() => {
  167. if (lot) {
  168. setManualInput('');
  169. setManualInputSubmitted(false);
  170. setManualInputError(false);
  171. setIsProcessingQr(false);
  172. setQrScanFailed(false);
  173. setQrScanSuccess(false);
  174. setScannedQrResult('');
  175. setProcessedQrCodes(new Set());
  176. }
  177. }, [lot]);
  178. // Auto-submit manual input when it matches
  179. useEffect(() => {
  180. if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
  181. console.log(' Auto-submitting manual input:', manualInput.trim());
  182. const timer = setTimeout(() => {
  183. setQrScanSuccess(true);
  184. onQrCodeSubmit(lot.lotNo);
  185. onClose();
  186. setManualInput('');
  187. setManualInputError(false);
  188. setManualInputSubmitted(false);
  189. }, 200);
  190. return () => clearTimeout(timer);
  191. }
  192. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  193. const handleManualSubmit = () => {
  194. if (manualInput.trim() === lot?.lotNo) {
  195. setQrScanSuccess(true);
  196. onQrCodeSubmit(lot.lotNo);
  197. onClose();
  198. setManualInput('');
  199. } else {
  200. setQrScanFailed(true);
  201. setManualInputError(true);
  202. setManualInputSubmitted(true);
  203. }
  204. };
  205. useEffect(() => {
  206. if (open) {
  207. startScan();
  208. }
  209. }, [open, startScan]);
  210. return (
  211. <Modal open={open} onClose={onClose}>
  212. <Box sx={{
  213. position: 'absolute',
  214. top: '50%',
  215. left: '50%',
  216. transform: 'translate(-50%, -50%)',
  217. bgcolor: 'background.paper',
  218. p: 3,
  219. borderRadius: 2,
  220. minWidth: 400,
  221. }}>
  222. <Typography variant="h6" gutterBottom>
  223. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  224. </Typography>
  225. {isProcessingQr && (
  226. <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
  227. <Typography variant="body2" color="primary">
  228. {t("Processing QR code...")}
  229. </Typography>
  230. </Box>
  231. )}
  232. <Box sx={{ mb: 2 }}>
  233. <Typography variant="body2" gutterBottom>
  234. <strong>{t("Manual Input")}:</strong>
  235. </Typography>
  236. <TextField
  237. fullWidth
  238. size="small"
  239. value={manualInput}
  240. onChange={(e) => {
  241. setManualInput(e.target.value);
  242. if (qrScanFailed || manualInputError) {
  243. setQrScanFailed(false);
  244. setManualInputError(false);
  245. setManualInputSubmitted(false);
  246. }
  247. }}
  248. sx={{ mb: 1 }}
  249. error={manualInputSubmitted && manualInputError}
  250. helperText={
  251. manualInputSubmitted && manualInputError
  252. ? `${t("The input is not the same as the expected lot number.")}`
  253. : ''
  254. }
  255. />
  256. <Button
  257. variant="contained"
  258. onClick={handleManualSubmit}
  259. disabled={!manualInput.trim()}
  260. size="small"
  261. color="primary"
  262. >
  263. {t("Submit")}
  264. </Button>
  265. </Box>
  266. {qrValues.length > 0 && (
  267. <Box sx={{
  268. mb: 2,
  269. p: 2,
  270. backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
  271. borderRadius: 1
  272. }}>
  273. <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
  274. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  275. </Typography>
  276. {qrScanSuccess && (
  277. <Typography variant="caption" color="success" display="block">
  278. {t("Verified successfully!")}
  279. </Typography>
  280. )}
  281. </Box>
  282. )}
  283. <Box sx={{ mt: 2, textAlign: 'right' }}>
  284. <Button onClick={onClose} variant="outlined">
  285. {t("Cancel")}
  286. </Button>
  287. </Box>
  288. </Box>
  289. </Modal>
  290. );
  291. };
  292. const PickExecution: React.FC<Props> = ({ filterArgs, onFgPickOrdersChange, onSwitchToDetailTab }) => {
  293. const { t } = useTranslation("pickOrder");
  294. const router = useRouter();
  295. const { data: session } = useSession() as { data: SessionWithTokens | null };
  296. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  297. const [combinedLotData, setCombinedLotData] = useState<any[]>([]);
  298. const [combinedDataLoading, setCombinedDataLoading] = useState(false);
  299. const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
  300. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  301. const [doPickOrderDetail, setDoPickOrderDetail] = useState<DoPickOrderDetail | null>(null);
  302. const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>(null);
  303. const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
  304. const [qrScanInput, setQrScanInput] = useState<string>('');
  305. const [qrScanError, setQrScanError] = useState<boolean>(false);
  306. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  307. const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
  308. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  309. const [paginationController, setPaginationController] = useState({
  310. pageNum: 0,
  311. pageSize: 10,
  312. });
  313. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  314. const initializationRef = useRef(false);
  315. const autoAssignRef = useRef(false);
  316. const formProps = useForm();
  317. const errors = formProps.formState.errors;
  318. // Add QR modal states
  319. const [qrModalOpen, setQrModalOpen] = useState(false);
  320. const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
  321. // Add GoodPickExecutionForm states
  322. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  323. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
  324. const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
  325. const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
  326. // 在 GoodPickExecutiondetail.tsx 中修改 fetchFgPickOrdersData
  327. // 修改 fetchFgPickOrdersData 函数:
  328. const fetchFgPickOrdersData = useCallback(async () => {
  329. if (!currentUserId) return;
  330. setFgPickOrdersLoading(true);
  331. try {
  332. const fgPickOrders = await fetchFGPickOrdersByUserId(currentUserId);
  333. console.log("🔍 DEBUG: Fetched FG pick orders:", fgPickOrders);
  334. console.log("🔍 DEBUG: First order numberOfPickOrders:", fgPickOrders[0]?.numberOfPickOrders);
  335. setFgPickOrders(fgPickOrders);
  336. if (onFgPickOrdersChange) {
  337. onFgPickOrdersChange(fgPickOrders);
  338. }
  339. } catch (error) {
  340. console.error("❌ Error fetching FG pick orders:", error);
  341. setFgPickOrders([]);
  342. if (onFgPickOrdersChange) {
  343. onFgPickOrdersChange([]);
  344. }
  345. } finally {
  346. setFgPickOrdersLoading(false);
  347. }
  348. }, [currentUserId, selectedPickOrderId]);
  349. // 简化:移除复杂的 useEffect 依赖
  350. useEffect(() => {
  351. if (currentUserId) {
  352. fetchFgPickOrdersData();
  353. }
  354. }, [currentUserId, fetchFgPickOrdersData]);
  355. // Handle QR code button click
  356. const handleQrCodeClick = (pickOrderId: number) => {
  357. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  358. // TODO: Implement QR code functionality
  359. };
  360. useEffect(() => {
  361. startScan();
  362. return () => {
  363. stopScan();
  364. resetScan();
  365. };
  366. }, [startScan, stopScan, resetScan]);
  367. const fetchAllCombinedLotData = useCallback(async (userId?: number) => {
  368. setCombinedDataLoading(true);
  369. try {
  370. const userIdToUse = userId || currentUserId;
  371. console.log(" fetchAllCombinedLotData called with userId:", userIdToUse);
  372. if (!userIdToUse) {
  373. console.warn("⚠️ No userId available, skipping API call");
  374. setCombinedLotData([]);
  375. setOriginalCombinedData([]);
  376. return;
  377. }
  378. // ✅ Fix: fetchAllPickOrderLotsHierarchical returns hierarchical data, not a flat array
  379. const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse);
  380. console.log(" Hierarchical data:", hierarchicalData);
  381. // ✅ Fix: Ensure we always set an array
  382. // If hierarchicalData is not in the expected format, default to empty array
  383. let allLotDetails: any[] = [];
  384. if (hierarchicalData && Array.isArray(hierarchicalData)) {
  385. // If it's already an array, use it directly
  386. allLotDetails = hierarchicalData;
  387. } else if (hierarchicalData?.pickOrders && Array.isArray(hierarchicalData.pickOrders)) {
  388. // Process hierarchical data into flat array (similar to GoodPickExecutiondetail.tsx)
  389. const mergedPickOrder = hierarchicalData.pickOrders[0];
  390. if (mergedPickOrder?.pickOrderLines) {
  391. mergedPickOrder.pickOrderLines.forEach((line: any) => {
  392. if (line.lots && line.lots.length > 0) {
  393. line.lots.forEach((lot: any) => {
  394. allLotDetails.push({
  395. pickOrderConsoCode: mergedPickOrder.consoCode,
  396. pickOrderTargetDate: mergedPickOrder.targetDate,
  397. pickOrderStatus: mergedPickOrder.status,
  398. pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0,
  399. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  400. pickOrderLineId: line.id,
  401. pickOrderLineRequiredQty: line.requiredQty,
  402. pickOrderLineStatus: line.status,
  403. itemId: line.item?.id,
  404. itemCode: line.item?.code,
  405. itemName: line.item?.name,
  406. uomDesc: line.item?.uomDesc,
  407. uomShortDesc: line.item?.uomShortDesc,
  408. lotId: lot.id,
  409. lotNo: lot.lotNo,
  410. expiryDate: lot.expiryDate,
  411. location: lot.location,
  412. stockUnit: lot.stockUnit,
  413. availableQty: lot.availableQty,
  414. requiredQty: lot.requiredQty,
  415. actualPickQty: lot.actualPickQty,
  416. lotStatus: lot.lotStatus,
  417. lotAvailability: lot.lotAvailability,
  418. processingStatus: lot.processingStatus,
  419. stockOutLineId: lot.stockOutLineId,
  420. stockOutLineStatus: lot.stockOutLineStatus,
  421. stockOutLineQty: lot.stockOutLineQty,
  422. routerId: lot.router?.id,
  423. routerIndex: lot.router?.index,
  424. routerRoute: lot.router?.route,
  425. routerArea: lot.router?.area,
  426. });
  427. });
  428. }
  429. });
  430. }
  431. }
  432. console.log(" All combined lot details:", allLotDetails);
  433. setCombinedLotData(allLotDetails);
  434. setOriginalCombinedData(allLotDetails);
  435. // ✅ Fix: Add safety check - ensure allLotDetails is an array before using .every()
  436. const allCompleted = Array.isArray(allLotDetails) && allLotDetails.length > 0 && allLotDetails.every((lot: any) =>
  437. lot.processingStatus === 'completed'
  438. );
  439. // 发送完成状态事件,包含标签页信息
  440. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  441. detail: {
  442. allLotsCompleted: allCompleted,
  443. tabIndex: 0 // 明确指定这是来自标签页 0 的事件
  444. }
  445. }));
  446. } catch (error) {
  447. console.error("❌ Error fetching combined lot data:", error);
  448. setCombinedLotData([]);
  449. setOriginalCombinedData([]);
  450. // 如果加载失败,禁用打印按钮
  451. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  452. detail: {
  453. allLotsCompleted: false,
  454. tabIndex: 0
  455. }
  456. }));
  457. } finally {
  458. setCombinedDataLoading(false);
  459. }
  460. }, [currentUserId, combinedLotData]);
  461. // Only fetch existing data when session is ready, no auto-assignment
  462. useEffect(() => {
  463. if (session && currentUserId && !initializationRef.current) {
  464. console.log(" Session loaded, initializing pick order...");
  465. initializationRef.current = true;
  466. // Only fetch existing data, no auto-assignment
  467. fetchAllCombinedLotData();
  468. }
  469. }, [session, currentUserId, fetchAllCombinedLotData]);
  470. // Add event listener for manual assignment
  471. useEffect(() => {
  472. const handlePickOrderAssigned = () => {
  473. console.log("🔄 Pick order assigned event received, refreshing data...");
  474. fetchAllCombinedLotData();
  475. };
  476. window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
  477. return () => {
  478. window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
  479. };
  480. }, [fetchAllCombinedLotData]);
  481. // Handle QR code submission for matched lot (external scanning)
  482. // Handle QR code submission for matched lot (external scanning)
  483. /*
  484. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  485. console.log(` Processing QR Code for lot: ${lotNo}`);
  486. // Use current data without refreshing to avoid infinite loop
  487. const currentLotData = combinedLotData;
  488. console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
  489. const matchingLots = currentLotData.filter(lot =>
  490. lot.lotNo === lotNo ||
  491. lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
  492. );
  493. if (matchingLots.length === 0) {
  494. console.error(`❌ Lot not found: ${lotNo}`);
  495. setQrScanError(true);
  496. setQrScanSuccess(false);
  497. return;
  498. }
  499. console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
  500. setQrScanError(false);
  501. try {
  502. let successCount = 0;
  503. let existsCount = 0;
  504. let errorCount = 0;
  505. for (const matchingLot of matchingLots) {
  506. console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
  507. if (matchingLot.stockOutLineId) {
  508. console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
  509. existsCount++;
  510. } else {
  511. const stockOutLineData: CreateStockOutLine = {
  512. consoCode: matchingLot.pickOrderConsoCode,
  513. pickOrderLineId: matchingLot.pickOrderLineId,
  514. inventoryLotLineId: matchingLot.lotId,
  515. qty: 0.0
  516. };
  517. console.log(`Creating stock out line for pick order line ${matchingLot.pickOrderLineId}:`, stockOutLineData);
  518. const result = await createStockOutLine(stockOutLineData);
  519. console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, result);
  520. if (result && result.code === "EXISTS") {
  521. console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
  522. existsCount++;
  523. } else if (result && result.code === "SUCCESS") {
  524. console.log(` Stock out line created successfully for line ${matchingLot.pickOrderLineId}`);
  525. successCount++;
  526. } else {
  527. console.error(`❌ Failed to create stock out line for line ${matchingLot.pickOrderLineId}:`, result);
  528. errorCount++;
  529. }
  530. }
  531. }
  532. // Always refresh data after processing (success or failure)
  533. console.log("🔄 Refreshing data after QR code processing...");
  534. await fetchAllCombinedLotData();
  535. if (successCount > 0 || existsCount > 0) {
  536. console.log(` QR Code processing completed: ${successCount} created, ${existsCount} already existed`);
  537. setQrScanSuccess(true);
  538. setQrScanInput(''); // Clear input after successful processing
  539. // Clear success state after a delay
  540. setTimeout(() => {
  541. setQrScanSuccess(false);
  542. }, 2000);
  543. } else {
  544. console.error(`❌ QR Code processing failed: ${errorCount} errors`);
  545. setQrScanError(true);
  546. setQrScanSuccess(false);
  547. // Clear error state after a delay
  548. setTimeout(() => {
  549. setQrScanError(false);
  550. }, 3000);
  551. }
  552. } catch (error) {
  553. console.error("❌ Error processing QR code:", error);
  554. setQrScanError(true);
  555. setQrScanSuccess(false);
  556. // Still refresh data even on error
  557. await fetchAllCombinedLotData();
  558. // Clear error state after a delay
  559. setTimeout(() => {
  560. setQrScanError(false);
  561. }, 3000);
  562. }
  563. }, [combinedLotData, fetchAllCombinedLotData]);
  564. */ const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  565. console.log(` Processing QR Code for lot: ${lotNo}`);
  566. // Use current data without refreshing to avoid infinite loop
  567. const currentLotData = combinedLotData;
  568. console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
  569. const matchingLots = currentLotData.filter(lot =>
  570. lot.lotNo === lotNo ||
  571. lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
  572. );
  573. if (matchingLots.length === 0) {
  574. console.error(`❌ Lot not found: ${lotNo}`);
  575. setQrScanError(true);
  576. setQrScanSuccess(false);
  577. return;
  578. }
  579. console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
  580. setQrScanError(false);
  581. try {
  582. let successCount = 0;
  583. let existsCount = 0;
  584. let errorCount = 0;
  585. for (const matchingLot of matchingLots) {
  586. console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
  587. if (matchingLot.stockOutLineId) {
  588. console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
  589. existsCount++;
  590. } else {
  591. const stockOutLineData: CreateStockOutLine = {
  592. consoCode: matchingLot.pickOrderConsoCode,
  593. pickOrderLineId: matchingLot.pickOrderLineId,
  594. inventoryLotLineId: matchingLot.lotId,
  595. qty: 0.0
  596. };
  597. console.log(`Creating stock out line for pick order line ${matchingLot.pickOrderLineId}:`, stockOutLineData);
  598. const result = await createStockOutLine(stockOutLineData);
  599. console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, result);
  600. if (result && result.code === "EXISTS") {
  601. console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
  602. existsCount++;
  603. } else if (result && result.code === "SUCCESS") {
  604. console.log(` Stock out line created successfully for line ${matchingLot.pickOrderLineId}`);
  605. successCount++;
  606. } else {
  607. console.error(`❌ Failed to create stock out line for line ${matchingLot.pickOrderLineId}:`, result);
  608. errorCount++;
  609. }
  610. }
  611. }
  612. // Always refresh data after processing (success or failure)
  613. console.log("🔄 Refreshing data after QR code processing...");
  614. await fetchAllCombinedLotData();
  615. if (successCount > 0 || existsCount > 0) {
  616. console.log(` QR Code processing completed: ${successCount} created, ${existsCount} already existed`);
  617. setQrScanSuccess(true);
  618. setQrScanInput(''); // Clear input after successful processing
  619. // Clear success state after a delay
  620. setTimeout(() => {
  621. setQrScanSuccess(false);
  622. }, 2000);
  623. } else {
  624. console.error(`❌ QR Code processing failed: ${errorCount} errors`);
  625. setQrScanError(true);
  626. setQrScanSuccess(false);
  627. // Clear error state after a delay
  628. setTimeout(() => {
  629. setQrScanError(false);
  630. }, 3000);
  631. }
  632. } catch (error) {
  633. console.error("❌ Error processing QR code:", error);
  634. setQrScanError(true);
  635. setQrScanSuccess(false);
  636. // Still refresh data even on error
  637. await fetchAllCombinedLotData();
  638. // Clear error state after a delay
  639. setTimeout(() => {
  640. setQrScanError(false);
  641. }, 3000);
  642. }
  643. }, [combinedLotData, fetchAllCombinedLotData]);
  644. const handleManualInputSubmit = useCallback(() => {
  645. if (qrScanInput.trim() !== '') {
  646. handleQrCodeSubmit(qrScanInput.trim());
  647. }
  648. }, [qrScanInput, handleQrCodeSubmit]);
  649. // Handle QR code submission from modal (internal scanning)
  650. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  651. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  652. console.log(` QR Code verified for lot: ${lotNo}`);
  653. const requiredQty = selectedLotForQr.requiredQty;
  654. const lotId = selectedLotForQr.lotId;
  655. // Create stock out line
  656. const stockOutLineData: CreateStockOutLine = {
  657. consoCode: selectedLotForQr.pickOrderConsoCode, // Use pickOrderConsoCode instead of pickOrderCode
  658. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  659. inventoryLotLineId: selectedLotForQr.lotId,
  660. qty: 0.0
  661. };
  662. try {
  663. await createStockOutLine(stockOutLineData);
  664. console.log("Stock out line created successfully!");
  665. // Close modal
  666. setQrModalOpen(false);
  667. setSelectedLotForQr(null);
  668. // Set pick quantity
  669. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  670. setTimeout(() => {
  671. setPickQtyData(prev => ({
  672. ...prev,
  673. [lotKey]: requiredQty
  674. }));
  675. console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  676. }, 500);
  677. // Refresh data
  678. await fetchAllCombinedLotData();
  679. } catch (error) {
  680. console.error("Error creating stock out line:", error);
  681. }
  682. }
  683. }, [selectedLotForQr, fetchAllCombinedLotData]);
  684. // Outside QR scanning - process QR codes from outside the page automatically
  685. useEffect(() => {
  686. if (qrValues.length > 0 && combinedLotData.length > 0) {
  687. const latestQr = qrValues[qrValues.length - 1];
  688. // Extract lot number from QR code
  689. let lotNo = '';
  690. try {
  691. const qrData = JSON.parse(latestQr);
  692. if (qrData.stockInLineId && qrData.itemId) {
  693. // For JSON QR codes, we need to fetch the lot number
  694. fetchStockInLineInfo(qrData.stockInLineId)
  695. .then((stockInLineInfo) => {
  696. console.log("Outside QR scan - Stock in line info:", stockInLineInfo);
  697. const extractedLotNo = stockInLineInfo.lotNo;
  698. if (extractedLotNo) {
  699. console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`);
  700. handleQrCodeSubmit(extractedLotNo);
  701. }
  702. })
  703. .catch((error) => {
  704. console.error("Outside QR scan - Error fetching stock in line info:", error);
  705. });
  706. return; // Exit early for JSON QR codes
  707. }
  708. } catch (error) {
  709. // Not JSON format, treat as direct lot number
  710. lotNo = latestQr.replace(/[{}]/g, '');
  711. }
  712. // For direct lot number QR codes
  713. if (lotNo) {
  714. console.log(`Outside QR scan detected (direct): ${lotNo}`);
  715. handleQrCodeSubmit(lotNo);
  716. }
  717. }
  718. }, [qrValues, combinedLotData, handleQrCodeSubmit]);
  719. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  720. if (value === '' || value === null || value === undefined) {
  721. setPickQtyData(prev => ({
  722. ...prev,
  723. [lotKey]: 0
  724. }));
  725. return;
  726. }
  727. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  728. if (isNaN(numericValue)) {
  729. setPickQtyData(prev => ({
  730. ...prev,
  731. [lotKey]: 0
  732. }));
  733. return;
  734. }
  735. setPickQtyData(prev => ({
  736. ...prev,
  737. [lotKey]: numericValue
  738. }));
  739. }, []);
  740. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  741. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  742. const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
  743. const checkAndAutoAssignNext = useCallback(async () => {
  744. if (!currentUserId) return;
  745. try {
  746. const completionResponse = await checkPickOrderCompletion(currentUserId);
  747. if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
  748. console.log("Found completed pick orders, auto-assigning next...");
  749. // 移除前端的自动分配逻辑,因为后端已经处理了
  750. // await handleAutoAssignAndRelease(); // 删除这个函数
  751. }
  752. } catch (error) {
  753. console.error("Error checking pick order completion:", error);
  754. }
  755. }, [currentUserId]);
  756. // Handle submit pick quantity
  757. const handleSubmitPickQty = useCallback(async (lot: any) => {
  758. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  759. const newQty = pickQtyData[lotKey] || 0;
  760. if (!lot.stockOutLineId) {
  761. console.error("No stock out line found for this lot");
  762. return;
  763. }
  764. try {
  765. const currentActualPickQty = lot.actualPickQty || 0;
  766. const cumulativeQty = currentActualPickQty + newQty;
  767. let newStatus = 'partially_completed';
  768. if (cumulativeQty >= lot.requiredQty) {
  769. newStatus = 'completed';
  770. }
  771. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  772. console.log(`Lot: ${lot.lotNo}`);
  773. console.log(`Required Qty: ${lot.requiredQty}`);
  774. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  775. console.log(`New Submitted Qty: ${newQty}`);
  776. console.log(`Cumulative Qty: ${cumulativeQty}`);
  777. console.log(`New Status: ${newStatus}`);
  778. console.log(`=====================================`);
  779. await updateStockOutLineStatus({
  780. id: lot.stockOutLineId,
  781. status: newStatus,
  782. qty: cumulativeQty
  783. });
  784. if (newQty > 0) {
  785. await updateInventoryLotLineQuantities({
  786. inventoryLotLineId: lot.lotId,
  787. qty: newQty,
  788. status: 'available',
  789. operation: 'pick'
  790. });
  791. }
  792. // FIXED: Use the proper API function instead of direct fetch
  793. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  794. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  795. try {
  796. // Use the imported API function instead of direct fetch
  797. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  798. console.log(` Pick order completion check result:`, completionResponse);
  799. if (completionResponse.code === "SUCCESS") {
  800. console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  801. } else if (completionResponse.message === "not completed") {
  802. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  803. } else {
  804. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  805. }
  806. } catch (error) {
  807. console.error("Error checking pick order completion:", error);
  808. }
  809. }
  810. await fetchAllCombinedLotData();
  811. console.log("Pick quantity submitted successfully!");
  812. setTimeout(() => {
  813. checkAndAutoAssignNext();
  814. }, 1000);
  815. } catch (error) {
  816. console.error("Error submitting pick quantity:", error);
  817. }
  818. }, [pickQtyData, fetchAllCombinedLotData, checkAndAutoAssignNext]);
  819. // Handle reject lot
  820. const handleRejectLot = useCallback(async (lot: any) => {
  821. if (!lot.stockOutLineId) {
  822. console.error("No stock out line found for this lot");
  823. return;
  824. }
  825. try {
  826. await updateStockOutLineStatus({
  827. id: lot.stockOutLineId,
  828. status: 'rejected',
  829. qty: 0
  830. });
  831. await fetchAllCombinedLotData();
  832. console.log("Lot rejected successfully!");
  833. setTimeout(() => {
  834. checkAndAutoAssignNext();
  835. }, 1000);
  836. } catch (error) {
  837. console.error("Error rejecting lot:", error);
  838. }
  839. }, [fetchAllCombinedLotData, checkAndAutoAssignNext]);
  840. // Handle pick execution form
  841. const handlePickExecutionForm = useCallback((lot: any) => {
  842. console.log("=== Pick Execution Form ===");
  843. console.log("Lot data:", lot);
  844. if (!lot) {
  845. console.warn("No lot data provided for pick execution form");
  846. return;
  847. }
  848. console.log("Opening pick execution form for lot:", lot.lotNo);
  849. setSelectedLotForExecutionForm(lot);
  850. setPickExecutionFormOpen(true);
  851. console.log("Pick execution form opened for lot ID:", lot.lotId);
  852. }, []);
  853. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  854. try {
  855. console.log("Pick execution form submitted:", data);
  856. const issueData = {
  857. ...data,
  858. type: "Do", // Delivery Order Record 类型
  859. };
  860. const result = await recordPickExecutionIssue(issueData);
  861. console.log("Pick execution issue recorded:", result);
  862. if (result && result.code === "SUCCESS") {
  863. console.log(" Pick execution issue recorded successfully");
  864. } else {
  865. console.error("❌ Failed to record pick execution issue:", result);
  866. }
  867. setPickExecutionFormOpen(false);
  868. setSelectedLotForExecutionForm(null);
  869. await fetchAllCombinedLotData();
  870. } catch (error) {
  871. console.error("Error submitting pick execution form:", error);
  872. }
  873. }, [fetchAllCombinedLotData]);
  874. // Calculate remaining required quantity
  875. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  876. const requiredQty = lot.requiredQty || 0;
  877. const stockOutLineQty = lot.stockOutLineQty || 0;
  878. return Math.max(0, requiredQty - stockOutLineQty);
  879. }, []);
  880. // Search criteria
  881. const searchCriteria: Criterion<any>[] = [
  882. {
  883. label: t("Pick Order Code"),
  884. paramName: "pickOrderCode",
  885. type: "text",
  886. },
  887. {
  888. label: t("Item Code"),
  889. paramName: "itemCode",
  890. type: "text",
  891. },
  892. {
  893. label: t("Item Name"),
  894. paramName: "itemName",
  895. type: "text",
  896. },
  897. {
  898. label: t("Lot No"),
  899. paramName: "lotNo",
  900. type: "text",
  901. },
  902. ];
  903. const handleSearch = useCallback((query: Record<string, any>) => {
  904. setSearchQuery({ ...query });
  905. console.log("Search query:", query);
  906. if (!originalCombinedData) return;
  907. const filtered = originalCombinedData.filter((lot: any) => {
  908. const pickOrderCodeMatch = !query.pickOrderCode ||
  909. lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  910. const itemCodeMatch = !query.itemCode ||
  911. lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
  912. const itemNameMatch = !query.itemName ||
  913. lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
  914. const lotNoMatch = !query.lotNo ||
  915. lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
  916. return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
  917. });
  918. setCombinedLotData(filtered);
  919. console.log("Filtered lots count:", filtered.length);
  920. }, [originalCombinedData]);
  921. const handleReset = useCallback(() => {
  922. setSearchQuery({});
  923. if (originalCombinedData) {
  924. setCombinedLotData(originalCombinedData);
  925. }
  926. }, [originalCombinedData]);
  927. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  928. setPaginationController(prev => ({
  929. ...prev,
  930. pageNum: newPage,
  931. }));
  932. }, []);
  933. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  934. const newPageSize = parseInt(event.target.value, 10);
  935. setPaginationController({
  936. pageNum: 0,
  937. pageSize: newPageSize,
  938. });
  939. }, []);
  940. const paginatedData = useMemo(() => {
  941. // ✅ Fix: Add safety check to ensure combinedLotData is an array
  942. if (!Array.isArray(combinedLotData)) {
  943. console.warn("⚠️ combinedLotData is not an array:", combinedLotData);
  944. return [];
  945. }
  946. // Sort by routerIndex first, then by other criteria
  947. const sortedData = [...combinedLotData].sort((a: any, b: any) => {
  948. const aIndex = a.routerIndex || 0;
  949. const bIndex = b.routerIndex || 0;
  950. // Primary sort: by routerIndex
  951. if (aIndex !== bIndex) {
  952. return aIndex - bIndex;
  953. }
  954. // Secondary sort: by pickOrderCode if routerIndex is the same
  955. if (a.pickOrderCode !== b.pickOrderCode) {
  956. return a.pickOrderCode.localeCompare(b.pickOrderCode);
  957. }
  958. // Tertiary sort: by lotNo if everything else is the same
  959. return (a.lotNo || '').localeCompare(b.lotNo || '');
  960. });
  961. const startIndex = paginationController.pageNum * paginationController.pageSize;
  962. const endIndex = startIndex + paginationController.pageSize;
  963. return sortedData.slice(startIndex, endIndex);
  964. }, [combinedLotData, paginationController]);
  965. return (
  966. <FormProvider {...formProps}>
  967. {/* 修复:改进条件渲染逻辑 */}
  968. {combinedDataLoading || fgPickOrdersLoading ? (
  969. // 数据加载中,显示加载指示器
  970. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  971. <CircularProgress />
  972. </Box>
  973. ) : fgPickOrders.length === 0 ? (
  974. // 没有活动订单,显示楼层选择面板
  975. <FinishedGoodFloorLanePanel
  976. onPickOrderAssigned={() => {
  977. if (currentUserId) {
  978. fetchAllCombinedLotData(currentUserId);
  979. fetchFgPickOrdersData();
  980. }
  981. }}
  982. onSwitchToDetailTab={onSwitchToDetailTab}
  983. />
  984. ) : (
  985. // 有活动订单,显示 FG 订单信息
  986. <Box>
  987. {fgPickOrders.map((fgOrder) => (
  988. <Box key={fgOrder.pickOrderId} sx={{ mb: 2 }}>
  989. <FGPickOrderInfoCard
  990. fgOrder={fgOrder}
  991. />
  992. </Box>
  993. ))}
  994. </Box>
  995. )}
  996. {/* Modals */}
  997. <QrCodeModal
  998. open={qrModalOpen}
  999. onClose={() => setQrModalOpen(false)}
  1000. lot={selectedLotForQr}
  1001. onQrCodeSubmit={handleQrCodeSubmit}
  1002. combinedLotData={combinedLotData}
  1003. />
  1004. <GoodPickExecutionForm
  1005. open={pickExecutionFormOpen}
  1006. onClose={() => {
  1007. setPickExecutionFormOpen(false);
  1008. setSelectedLotForExecutionForm(null);
  1009. }}
  1010. onSubmit={handlePickExecutionFormSubmit}
  1011. selectedLot={selectedLotForExecutionForm}
  1012. selectedPickOrderLine={null}
  1013. pickOrderId={selectedLotForExecutionForm?.pickOrderId}
  1014. pickOrderCreateDate={null}
  1015. />
  1016. </FormProvider>
  1017. );
  1018. };
  1019. export default PickExecution;