FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 

1208 строки
41 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. } from "@/app/api/pickOrder/actions";
  38. import { fetchNameList, NameList } from "@/app/api/user/actions";
  39. import {
  40. FormProvider,
  41. useForm,
  42. } from "react-hook-form";
  43. import SearchBox, { Criterion } from "../SearchBox";
  44. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  45. import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  46. import QrCodeIcon from '@mui/icons-material/QrCode';
  47. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  48. import { useSession } from "next-auth/react";
  49. import { SessionWithTokens } from "@/config/authConfig";
  50. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  51. import GoodPickExecutionForm from "./GoodPickExecutionForm";
  52. import FGPickOrderCard from "./FGPickOrderCard";
  53. import FinishedGoodFloorLanePanel from "./FinishedGoodFloorLanePanel";
  54. import FGPickOrderInfoCard from "./FGPickOrderInfoCard";
  55. interface Props {
  56. filterArgs: Record<string, any>;
  57. onFgPickOrdersChange?: (fgPickOrders: FGPickOrderResponse[]) => void;
  58. onSwitchToDetailTab?: () => void;
  59. onFirstLoadDone?: () => void;
  60. }
  61. // QR Code Modal Component (from LotTable)
  62. const QrCodeModal: React.FC<{
  63. open: boolean;
  64. onClose: () => void;
  65. lot: any | null;
  66. onQrCodeSubmit: (lotNo: string) => void;
  67. combinedLotData: any[]; // Add this prop
  68. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
  69. const { t } = useTranslation("pickOrder");
  70. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  71. const [manualInput, setManualInput] = useState<string>('');
  72. const floorPanelTimerStartedRef = useRef(false);
  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, onFirstLoadDone }) => {
  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. const floorPanelTimerStartedRef = useRef(false);
  322. // Add GoodPickExecutionForm states
  323. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  324. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
  325. const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
  326. const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
  327. // 在 GoodPickExecutiondetail.tsx 中修改 fetchFgPickOrdersData
  328. // 修改 fetchFgPickOrdersData 函数:
  329. const fetchFgPickOrdersData = useCallback(async () => {
  330. if (!currentUserId) return;
  331. setFgPickOrdersLoading(true);
  332. try {
  333. const fgPickOrders = await fetchFGPickOrdersByUserId(currentUserId);
  334. console.log("🔍 DEBUG: Fetched FG pick orders:", fgPickOrders);
  335. console.log("🔍 DEBUG: First order numberOfPickOrders:", fgPickOrders[0]?.numberOfPickOrders);
  336. setFgPickOrders(fgPickOrders);
  337. if (onFgPickOrdersChange) {
  338. onFgPickOrdersChange(fgPickOrders);
  339. }
  340. } catch (error) {
  341. console.error("❌ Error fetching FG pick orders:", error);
  342. setFgPickOrders([]);
  343. if (onFgPickOrdersChange) {
  344. onFgPickOrdersChange([]);
  345. }
  346. } finally {
  347. setFgPickOrdersLoading(false);
  348. }
  349. }, [currentUserId, selectedPickOrderId]);
  350. // 简化:移除复杂的 useEffect 依赖
  351. useEffect(() => {
  352. if (currentUserId) {
  353. fetchFgPickOrdersData();
  354. }
  355. }, [currentUserId, fetchFgPickOrdersData]);
  356. // Handle QR code button click
  357. const handleQrCodeClick = (pickOrderId: number) => {
  358. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  359. // TODO: Implement QR code functionality
  360. };
  361. useEffect(() => {
  362. startScan();
  363. return () => {
  364. stopScan();
  365. resetScan();
  366. };
  367. }, [startScan, stopScan, resetScan]);
  368. const fetchAllCombinedLotData = useCallback(async (userId?: number) => {
  369. setCombinedDataLoading(true);
  370. try {
  371. const userIdToUse = userId || currentUserId;
  372. console.log(" fetchAllCombinedLotData called with userId:", userIdToUse);
  373. if (!userIdToUse) {
  374. console.warn("⚠️ No userId available, skipping API call");
  375. setCombinedLotData([]);
  376. setOriginalCombinedData([]);
  377. return;
  378. }
  379. // ✅ Fix: fetchAllPickOrderLotsHierarchical returns hierarchical data, not a flat array
  380. const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse);
  381. console.log(" Hierarchical data:", hierarchicalData);
  382. // ✅ Fix: Ensure we always set an array
  383. // If hierarchicalData is not in the expected format, default to empty array
  384. let allLotDetails: any[] = [];
  385. if (hierarchicalData && Array.isArray(hierarchicalData)) {
  386. // If it's already an array, use it directly
  387. allLotDetails = hierarchicalData;
  388. } else if (hierarchicalData?.pickOrders && Array.isArray(hierarchicalData.pickOrders)) {
  389. // Process hierarchical data into flat array (similar to GoodPickExecutiondetail.tsx)
  390. const mergedPickOrder = hierarchicalData.pickOrders[0];
  391. if (mergedPickOrder?.pickOrderLines) {
  392. mergedPickOrder.pickOrderLines.forEach((line: any) => {
  393. if (line.lots && line.lots.length > 0) {
  394. line.lots.forEach((lot: any) => {
  395. allLotDetails.push({
  396. pickOrderConsoCode: mergedPickOrder.consoCode,
  397. pickOrderTargetDate: mergedPickOrder.targetDate,
  398. pickOrderStatus: mergedPickOrder.status,
  399. pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0,
  400. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  401. pickOrderLineId: line.id,
  402. pickOrderLineRequiredQty: line.requiredQty,
  403. pickOrderLineStatus: line.status,
  404. itemId: line.item?.id,
  405. itemCode: line.item?.code,
  406. itemName: line.item?.name,
  407. uomDesc: line.item?.uomDesc,
  408. uomShortDesc: line.item?.uomShortDesc,
  409. lotId: lot.id,
  410. lotNo: lot.lotNo,
  411. expiryDate: lot.expiryDate,
  412. location: lot.location,
  413. stockUnit: lot.stockUnit,
  414. availableQty: lot.availableQty,
  415. requiredQty: lot.requiredQty,
  416. actualPickQty: lot.actualPickQty,
  417. lotStatus: lot.lotStatus,
  418. lotAvailability: lot.lotAvailability,
  419. processingStatus: lot.processingStatus,
  420. stockOutLineId: lot.stockOutLineId,
  421. stockOutLineStatus: lot.stockOutLineStatus,
  422. stockOutLineQty: lot.stockOutLineQty,
  423. routerId: lot.router?.id,
  424. routerIndex: lot.router?.index,
  425. routerRoute: lot.router?.route,
  426. routerArea: lot.router?.area,
  427. });
  428. });
  429. }
  430. });
  431. }
  432. }
  433. console.log(" All combined lot details:", allLotDetails);
  434. setCombinedLotData(allLotDetails);
  435. setOriginalCombinedData(allLotDetails);
  436. // ✅ Fix: Add safety check - ensure allLotDetails is an array before using .every()
  437. const allCompleted = Array.isArray(allLotDetails) && allLotDetails.length > 0 && allLotDetails.every((lot: any) =>
  438. lot.processingStatus === 'completed'
  439. );
  440. // 发送完成状态事件,包含标签页信息
  441. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  442. detail: {
  443. allLotsCompleted: allCompleted,
  444. tabIndex: 0 // 明确指定这是来自标签页 0 的事件
  445. }
  446. }));
  447. } catch (error) {
  448. console.error("❌ Error fetching combined lot data:", error);
  449. setCombinedLotData([]);
  450. setOriginalCombinedData([]);
  451. // 如果加载失败,禁用打印按钮
  452. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  453. detail: {
  454. allLotsCompleted: false,
  455. tabIndex: 0
  456. }
  457. }));
  458. } finally {
  459. setCombinedDataLoading(false);
  460. }
  461. }, [currentUserId, combinedLotData]);
  462. // Only fetch existing data when session is ready, no auto-assignment
  463. useEffect(() => {
  464. if (session && currentUserId && !initializationRef.current) {
  465. console.log(" Session loaded, initializing pick order...");
  466. initializationRef.current = true;
  467. (async () => {
  468. try {
  469. await fetchAllCombinedLotData(); // ✅ 等待数据加载完成
  470. } finally {
  471. if (onFirstLoadDone) {
  472. onFirstLoadDone(); // ✅ 这时候再结束 [FinishedGoodSearch] page ready
  473. }
  474. }
  475. })();
  476. }
  477. }, [session, currentUserId, fetchAllCombinedLotData, onFirstLoadDone]);
  478. // Add event listener for manual assignment
  479. useEffect(() => {
  480. const handlePickOrderAssigned = () => {
  481. console.log("🔄 Pick order assigned event received, refreshing data...");
  482. fetchAllCombinedLotData();
  483. };
  484. window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
  485. return () => {
  486. window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
  487. };
  488. }, [fetchAllCombinedLotData]);
  489. // Handle QR code submission for matched lot (external scanning)
  490. // Handle QR code submission for matched lot (external scanning)
  491. /*
  492. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  493. console.log(` Processing QR Code for lot: ${lotNo}`);
  494. // Use current data without refreshing to avoid infinite loop
  495. const currentLotData = combinedLotData;
  496. console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
  497. const matchingLots = currentLotData.filter(lot =>
  498. lot.lotNo === lotNo ||
  499. lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
  500. );
  501. if (matchingLots.length === 0) {
  502. console.error(`❌ Lot not found: ${lotNo}`);
  503. setQrScanError(true);
  504. setQrScanSuccess(false);
  505. return;
  506. }
  507. console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
  508. setQrScanError(false);
  509. try {
  510. let successCount = 0;
  511. let existsCount = 0;
  512. let errorCount = 0;
  513. for (const matchingLot of matchingLots) {
  514. console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
  515. if (matchingLot.stockOutLineId) {
  516. console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
  517. existsCount++;
  518. } else {
  519. const stockOutLineData: CreateStockOutLine = {
  520. consoCode: matchingLot.pickOrderConsoCode,
  521. pickOrderLineId: matchingLot.pickOrderLineId,
  522. inventoryLotLineId: matchingLot.lotId,
  523. qty: 0.0
  524. };
  525. console.log(`Creating stock out line for pick order line ${matchingLot.pickOrderLineId}:`, stockOutLineData);
  526. const result = await createStockOutLine(stockOutLineData);
  527. console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, result);
  528. if (result && result.code === "EXISTS") {
  529. console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
  530. existsCount++;
  531. } else if (result && result.code === "SUCCESS") {
  532. console.log(` Stock out line created successfully for line ${matchingLot.pickOrderLineId}`);
  533. successCount++;
  534. } else {
  535. console.error(`❌ Failed to create stock out line for line ${matchingLot.pickOrderLineId}:`, result);
  536. errorCount++;
  537. }
  538. }
  539. }
  540. // Always refresh data after processing (success or failure)
  541. console.log("🔄 Refreshing data after QR code processing...");
  542. await fetchAllCombinedLotData();
  543. if (successCount > 0 || existsCount > 0) {
  544. console.log(` QR Code processing completed: ${successCount} created, ${existsCount} already existed`);
  545. setQrScanSuccess(true);
  546. setQrScanInput(''); // Clear input after successful processing
  547. // Clear success state after a delay
  548. setTimeout(() => {
  549. setQrScanSuccess(false);
  550. }, 2000);
  551. } else {
  552. console.error(`❌ QR Code processing failed: ${errorCount} errors`);
  553. setQrScanError(true);
  554. setQrScanSuccess(false);
  555. // Clear error state after a delay
  556. setTimeout(() => {
  557. setQrScanError(false);
  558. }, 3000);
  559. }
  560. } catch (error) {
  561. console.error("❌ Error processing QR code:", error);
  562. setQrScanError(true);
  563. setQrScanSuccess(false);
  564. // Still refresh data even on error
  565. await fetchAllCombinedLotData();
  566. // Clear error state after a delay
  567. setTimeout(() => {
  568. setQrScanError(false);
  569. }, 3000);
  570. }
  571. }, [combinedLotData, fetchAllCombinedLotData]);
  572. */ const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  573. console.log(` Processing QR Code for lot: ${lotNo}`);
  574. // Use current data without refreshing to avoid infinite loop
  575. const currentLotData = combinedLotData;
  576. console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
  577. const matchingLots = currentLotData.filter(lot =>
  578. lot.lotNo === lotNo ||
  579. lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
  580. );
  581. if (matchingLots.length === 0) {
  582. console.error(`❌ Lot not found: ${lotNo}`);
  583. setQrScanError(true);
  584. setQrScanSuccess(false);
  585. return;
  586. }
  587. console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
  588. setQrScanError(false);
  589. try {
  590. let successCount = 0;
  591. let existsCount = 0;
  592. let errorCount = 0;
  593. for (const matchingLot of matchingLots) {
  594. console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
  595. if (matchingLot.stockOutLineId) {
  596. console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
  597. existsCount++;
  598. } else {
  599. const stockOutLineData: CreateStockOutLine = {
  600. consoCode: matchingLot.pickOrderConsoCode,
  601. pickOrderLineId: matchingLot.pickOrderLineId,
  602. inventoryLotLineId: matchingLot.lotId,
  603. qty: 0.0
  604. };
  605. console.log(`Creating stock out line for pick order line ${matchingLot.pickOrderLineId}:`, stockOutLineData);
  606. const result = await createStockOutLine(stockOutLineData);
  607. console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, result);
  608. if (result && result.code === "EXISTS") {
  609. console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
  610. existsCount++;
  611. } else if (result && result.code === "SUCCESS") {
  612. console.log(` Stock out line created successfully for line ${matchingLot.pickOrderLineId}`);
  613. successCount++;
  614. } else {
  615. console.error(`❌ Failed to create stock out line for line ${matchingLot.pickOrderLineId}:`, result);
  616. errorCount++;
  617. }
  618. }
  619. }
  620. // Always refresh data after processing (success or failure)
  621. console.log("🔄 Refreshing data after QR code processing...");
  622. await fetchAllCombinedLotData();
  623. if (successCount > 0 || existsCount > 0) {
  624. console.log(` QR Code processing completed: ${successCount} created, ${existsCount} already existed`);
  625. setQrScanSuccess(true);
  626. setQrScanInput(''); // Clear input after successful processing
  627. // Clear success state after a delay
  628. setTimeout(() => {
  629. setQrScanSuccess(false);
  630. }, 2000);
  631. } else {
  632. console.error(`❌ QR Code processing failed: ${errorCount} errors`);
  633. setQrScanError(true);
  634. setQrScanSuccess(false);
  635. // Clear error state after a delay
  636. setTimeout(() => {
  637. setQrScanError(false);
  638. }, 3000);
  639. }
  640. } catch (error) {
  641. console.error("❌ Error processing QR code:", error);
  642. setQrScanError(true);
  643. setQrScanSuccess(false);
  644. // Still refresh data even on error
  645. await fetchAllCombinedLotData();
  646. // Clear error state after a delay
  647. setTimeout(() => {
  648. setQrScanError(false);
  649. }, 3000);
  650. }
  651. }, [combinedLotData, fetchAllCombinedLotData]);
  652. const handleManualInputSubmit = useCallback(() => {
  653. if (qrScanInput.trim() !== '') {
  654. handleQrCodeSubmit(qrScanInput.trim());
  655. }
  656. }, [qrScanInput, handleQrCodeSubmit]);
  657. // Handle QR code submission from modal (internal scanning)
  658. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  659. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  660. console.log(` QR Code verified for lot: ${lotNo}`);
  661. const requiredQty = selectedLotForQr.requiredQty;
  662. const lotId = selectedLotForQr.lotId;
  663. // Create stock out line
  664. const stockOutLineData: CreateStockOutLine = {
  665. consoCode: selectedLotForQr.pickOrderConsoCode, // Use pickOrderConsoCode instead of pickOrderCode
  666. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  667. inventoryLotLineId: selectedLotForQr.lotId,
  668. qty: 0.0
  669. };
  670. try {
  671. await createStockOutLine(stockOutLineData);
  672. console.log("Stock out line created successfully!");
  673. // Close modal
  674. setQrModalOpen(false);
  675. setSelectedLotForQr(null);
  676. // Set pick quantity
  677. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  678. setTimeout(() => {
  679. setPickQtyData(prev => ({
  680. ...prev,
  681. [lotKey]: requiredQty
  682. }));
  683. console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  684. }, 500);
  685. // Refresh data
  686. await fetchAllCombinedLotData();
  687. } catch (error) {
  688. console.error("Error creating stock out line:", error);
  689. }
  690. }
  691. }, [selectedLotForQr, fetchAllCombinedLotData]);
  692. // Outside QR scanning - process QR codes from outside the page automatically
  693. useEffect(() => {
  694. if (qrValues.length > 0 && combinedLotData.length > 0) {
  695. const latestQr = qrValues[qrValues.length - 1];
  696. // Extract lot number from QR code
  697. let lotNo = '';
  698. try {
  699. const qrData = JSON.parse(latestQr);
  700. if (qrData.stockInLineId && qrData.itemId) {
  701. // For JSON QR codes, we need to fetch the lot number
  702. fetchStockInLineInfo(qrData.stockInLineId)
  703. .then((stockInLineInfo) => {
  704. console.log("Outside QR scan - Stock in line info:", stockInLineInfo);
  705. const extractedLotNo = stockInLineInfo.lotNo;
  706. if (extractedLotNo) {
  707. console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`);
  708. handleQrCodeSubmit(extractedLotNo);
  709. }
  710. })
  711. .catch((error) => {
  712. console.error("Outside QR scan - Error fetching stock in line info:", error);
  713. });
  714. return; // Exit early for JSON QR codes
  715. }
  716. } catch (error) {
  717. // Not JSON format, treat as direct lot number
  718. lotNo = latestQr.replace(/[{}]/g, '');
  719. }
  720. // For direct lot number QR codes
  721. if (lotNo) {
  722. console.log(`Outside QR scan detected (direct): ${lotNo}`);
  723. handleQrCodeSubmit(lotNo);
  724. }
  725. }
  726. }, [qrValues, combinedLotData, handleQrCodeSubmit]);
  727. useEffect(() => {
  728. if (typeof window === "undefined") return;
  729. const key = "__FG_FLOOR_PANEL_TIMER_STARTED__" as const;
  730. // 只有当「没有 FG 订单」时才会显示 FinishedGoodFloorLanePanel
  731. if (fgPickOrders.length === 0 && !fgPickOrdersLoading) {
  732. if (!(window as any)[key]) {
  733. (window as any)[key] = true;
  734. console.time("[FG] FloorLanePanel initial load");
  735. }
  736. }
  737. // 如果之后拿到 FG 订单,你可以选择在这里清掉标记(可选)
  738. if (fgPickOrders.length > 0 && (window as any)[key]) {
  739. // delete (window as any)[key];
  740. }
  741. }, [fgPickOrders.length, fgPickOrdersLoading]);
  742. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  743. if (value === '' || value === null || value === undefined) {
  744. setPickQtyData(prev => ({
  745. ...prev,
  746. [lotKey]: 0
  747. }));
  748. return;
  749. }
  750. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  751. if (isNaN(numericValue)) {
  752. setPickQtyData(prev => ({
  753. ...prev,
  754. [lotKey]: 0
  755. }));
  756. return;
  757. }
  758. setPickQtyData(prev => ({
  759. ...prev,
  760. [lotKey]: numericValue
  761. }));
  762. }, []);
  763. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  764. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  765. const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
  766. const checkAndAutoAssignNext = useCallback(async () => {
  767. if (!currentUserId) return;
  768. try {
  769. const completionResponse = await checkPickOrderCompletion(currentUserId);
  770. if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
  771. console.log("Found completed pick orders, auto-assigning next...");
  772. // 移除前端的自动分配逻辑,因为后端已经处理了
  773. // await handleAutoAssignAndRelease(); // 删除这个函数
  774. }
  775. } catch (error) {
  776. console.error("Error checking pick order completion:", error);
  777. }
  778. }, [currentUserId]);
  779. // Handle submit pick quantity
  780. const handleSubmitPickQty = useCallback(async (lot: any) => {
  781. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  782. const newQty = pickQtyData[lotKey] || 0;
  783. if (!lot.stockOutLineId) {
  784. console.error("No stock out line found for this lot");
  785. return;
  786. }
  787. try {
  788. const currentActualPickQty = lot.actualPickQty || 0;
  789. const cumulativeQty = currentActualPickQty + newQty;
  790. let newStatus = 'partially_completed';
  791. if (cumulativeQty >= lot.requiredQty) {
  792. newStatus = 'completed';
  793. }
  794. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  795. console.log(`Lot: ${lot.lotNo}`);
  796. console.log(`Required Qty: ${lot.requiredQty}`);
  797. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  798. console.log(`New Submitted Qty: ${newQty}`);
  799. console.log(`Cumulative Qty: ${cumulativeQty}`);
  800. console.log(`New Status: ${newStatus}`);
  801. console.log(`=====================================`);
  802. await updateStockOutLineStatus({
  803. id: lot.stockOutLineId,
  804. status: newStatus,
  805. qty: cumulativeQty
  806. });
  807. if (newQty > 0) {
  808. await updateInventoryLotLineQuantities({
  809. inventoryLotLineId: lot.lotId,
  810. qty: newQty,
  811. status: 'available',
  812. operation: 'pick'
  813. });
  814. }
  815. // FIXED: Use the proper API function instead of direct fetch
  816. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  817. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  818. try {
  819. // Use the imported API function instead of direct fetch
  820. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  821. console.log(` Pick order completion check result:`, completionResponse);
  822. if (completionResponse.code === "SUCCESS") {
  823. console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  824. } else if (completionResponse.message === "not completed") {
  825. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  826. } else {
  827. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  828. }
  829. } catch (error) {
  830. console.error("Error checking pick order completion:", error);
  831. }
  832. }
  833. await fetchAllCombinedLotData();
  834. console.log("Pick quantity submitted successfully!");
  835. setTimeout(() => {
  836. checkAndAutoAssignNext();
  837. }, 1000);
  838. } catch (error) {
  839. console.error("Error submitting pick quantity:", error);
  840. }
  841. }, [pickQtyData, fetchAllCombinedLotData, checkAndAutoAssignNext]);
  842. // Handle reject lot
  843. const handleRejectLot = useCallback(async (lot: any) => {
  844. if (!lot.stockOutLineId) {
  845. console.error("No stock out line found for this lot");
  846. return;
  847. }
  848. try {
  849. await updateStockOutLineStatus({
  850. id: lot.stockOutLineId,
  851. status: 'rejected',
  852. qty: 0
  853. });
  854. await fetchAllCombinedLotData();
  855. console.log("Lot rejected successfully!");
  856. setTimeout(() => {
  857. checkAndAutoAssignNext();
  858. }, 1000);
  859. } catch (error) {
  860. console.error("Error rejecting lot:", error);
  861. }
  862. }, [fetchAllCombinedLotData, checkAndAutoAssignNext]);
  863. // Handle pick execution form
  864. const handlePickExecutionForm = useCallback((lot: any) => {
  865. console.log("=== Pick Execution Form ===");
  866. console.log("Lot data:", lot);
  867. if (!lot) {
  868. console.warn("No lot data provided for pick execution form");
  869. return;
  870. }
  871. console.log("Opening pick execution form for lot:", lot.lotNo);
  872. setSelectedLotForExecutionForm(lot);
  873. setPickExecutionFormOpen(true);
  874. console.log("Pick execution form opened for lot ID:", lot.lotId);
  875. }, []);
  876. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  877. try {
  878. console.log("Pick execution form submitted:", data);
  879. const issueData = {
  880. ...data,
  881. type: "Do", // Delivery Order Record 类型
  882. };
  883. const result = await recordPickExecutionIssue(issueData);
  884. console.log("Pick execution issue recorded:", result);
  885. if (result && result.code === "SUCCESS") {
  886. console.log(" Pick execution issue recorded successfully");
  887. } else {
  888. console.error("❌ Failed to record pick execution issue:", result);
  889. }
  890. setPickExecutionFormOpen(false);
  891. setSelectedLotForExecutionForm(null);
  892. await fetchAllCombinedLotData();
  893. } catch (error) {
  894. console.error("Error submitting pick execution form:", error);
  895. }
  896. }, [fetchAllCombinedLotData]);
  897. // Calculate remaining required quantity
  898. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  899. const requiredQty = lot.requiredQty || 0;
  900. const stockOutLineQty = lot.stockOutLineQty || 0;
  901. return Math.max(0, requiredQty - stockOutLineQty);
  902. }, []);
  903. // Search criteria
  904. const searchCriteria: Criterion<any>[] = [
  905. {
  906. label: t("Pick Order Code"),
  907. paramName: "pickOrderCode",
  908. type: "text",
  909. },
  910. {
  911. label: t("Item Code"),
  912. paramName: "itemCode",
  913. type: "text",
  914. },
  915. {
  916. label: t("Item Name"),
  917. paramName: "itemName",
  918. type: "text",
  919. },
  920. {
  921. label: t("Lot No"),
  922. paramName: "lotNo",
  923. type: "text",
  924. },
  925. ];
  926. const handleSearch = useCallback((query: Record<string, any>) => {
  927. setSearchQuery({ ...query });
  928. console.log("Search query:", query);
  929. if (!originalCombinedData) return;
  930. const filtered = originalCombinedData.filter((lot: any) => {
  931. const pickOrderCodeMatch = !query.pickOrderCode ||
  932. lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  933. const itemCodeMatch = !query.itemCode ||
  934. lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
  935. const itemNameMatch = !query.itemName ||
  936. lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
  937. const lotNoMatch = !query.lotNo ||
  938. lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
  939. return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
  940. });
  941. setCombinedLotData(filtered);
  942. console.log("Filtered lots count:", filtered.length);
  943. }, [originalCombinedData]);
  944. const handleReset = useCallback(() => {
  945. setSearchQuery({});
  946. if (originalCombinedData) {
  947. setCombinedLotData(originalCombinedData);
  948. }
  949. }, [originalCombinedData]);
  950. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  951. setPaginationController(prev => ({
  952. ...prev,
  953. pageNum: newPage,
  954. }));
  955. }, []);
  956. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  957. const newPageSize = parseInt(event.target.value, 10);
  958. setPaginationController({
  959. pageNum: 0,
  960. pageSize: newPageSize,
  961. });
  962. }, []);
  963. const paginatedData = useMemo(() => {
  964. // ✅ Fix: Add safety check to ensure combinedLotData is an array
  965. if (!Array.isArray(combinedLotData)) {
  966. console.warn(" combinedLotData is not an array:", combinedLotData);
  967. return [];
  968. }
  969. // Sort by routerIndex first, then by other criteria
  970. const sortedData = [...combinedLotData].sort((a: any, b: any) => {
  971. const aIndex = a.routerIndex || 0;
  972. const bIndex = b.routerIndex || 0;
  973. // Primary sort: by routerIndex
  974. if (aIndex !== bIndex) {
  975. return aIndex - bIndex;
  976. }
  977. // Secondary sort: by pickOrderCode if routerIndex is the same
  978. if (a.pickOrderCode !== b.pickOrderCode) {
  979. return a.pickOrderCode.localeCompare(b.pickOrderCode);
  980. }
  981. // Tertiary sort: by lotNo if everything else is the same
  982. return (a.lotNo || '').localeCompare(b.lotNo || '');
  983. });
  984. const startIndex = paginationController.pageNum * paginationController.pageSize;
  985. const endIndex = startIndex + paginationController.pageSize;
  986. return sortedData.slice(startIndex, endIndex);
  987. }, [combinedLotData, paginationController]);
  988. return (
  989. <FormProvider {...formProps}>
  990. {/* 修复:改进条件渲染逻辑 */}
  991. {combinedDataLoading || fgPickOrdersLoading ? (
  992. // 数据加载中,显示加载指示器
  993. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  994. <CircularProgress />
  995. </Box>
  996. ) : fgPickOrders.length === 0 ? (
  997. // 没有活动订单,显示楼层选择面板
  998. <FinishedGoodFloorLanePanel
  999. onPickOrderAssigned={() => {
  1000. if (currentUserId) {
  1001. fetchAllCombinedLotData(currentUserId);
  1002. fetchFgPickOrdersData();
  1003. }
  1004. }}
  1005. onSwitchToDetailTab={onSwitchToDetailTab}
  1006. />
  1007. ) : (
  1008. // 有活动订单,显示 FG 订单信息
  1009. <Box>
  1010. {fgPickOrders.map((fgOrder) => (
  1011. <Box key={fgOrder.pickOrderId} sx={{ mb: 2 }}>
  1012. <FGPickOrderInfoCard
  1013. fgOrder={fgOrder}
  1014. />
  1015. </Box>
  1016. ))}
  1017. </Box>
  1018. )}
  1019. {/* Modals */}
  1020. <QrCodeModal
  1021. open={qrModalOpen}
  1022. onClose={() => setQrModalOpen(false)}
  1023. lot={selectedLotForQr}
  1024. onQrCodeSubmit={handleQrCodeSubmit}
  1025. combinedLotData={combinedLotData}
  1026. />
  1027. <GoodPickExecutionForm
  1028. open={pickExecutionFormOpen}
  1029. onClose={() => {
  1030. setPickExecutionFormOpen(false);
  1031. setSelectedLotForExecutionForm(null);
  1032. }}
  1033. onSubmit={handlePickExecutionFormSubmit}
  1034. selectedLot={selectedLotForExecutionForm}
  1035. selectedPickOrderLine={null}
  1036. pickOrderId={selectedLotForExecutionForm?.pickOrderId}
  1037. pickOrderCreateDate={null}
  1038. />
  1039. </FormProvider>
  1040. );
  1041. };
  1042. export default PickExecution;