FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

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