FPSMS-frontend
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 

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