FPSMS-frontend
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 

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