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.
 
 

2559 rivejä
96 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. Checkbox,
  18. TablePagination,
  19. Modal,
  20. } from "@mui/material";
  21. import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider';
  22. import { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react";
  23. import { useTranslation } from "react-i18next";
  24. import { useRouter } from "next/navigation";
  25. import {
  26. updateStockOutLineStatus,
  27. createStockOutLine,
  28. recordPickExecutionIssue,
  29. fetchFGPickOrders,
  30. FGPickOrderResponse,
  31. autoAssignAndReleasePickOrder,
  32. AutoAssignReleaseResponse,
  33. checkPickOrderCompletion,
  34. PickOrderCompletionResponse,
  35. checkAndCompletePickOrderByConsoCode,
  36. confirmLotSubstitution,
  37. updateStockOutLineStatusByQRCodeAndLotNo, // ✅ 添加
  38. batchSubmitList, // ✅ 添加
  39. batchSubmitListRequest, // ✅ 添加
  40. batchSubmitListLineRequest,
  41. } from "@/app/api/pickOrder/actions";
  42. // 修改:使用 Job Order API
  43. import {
  44. assignJobOrderPickOrder,
  45. fetchJobOrderLotsHierarchicalByPickOrderId,
  46. updateJoPickOrderHandledBy,
  47. JobOrderLotsHierarchicalResponse,
  48. } from "@/app/api/jo/actions";
  49. import { fetchNameList, NameList } from "@/app/api/user/actions";
  50. import {
  51. FormProvider,
  52. useForm,
  53. } from "react-hook-form";
  54. import SearchBox, { Criterion } from "../SearchBox";
  55. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  56. import { updateInventoryLotLineQuantities, analyzeQrCode, fetchLotDetail } from "@/app/api/inventory/actions";
  57. import QrCodeIcon from '@mui/icons-material/QrCode';
  58. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  59. import { useSession } from "next-auth/react";
  60. import { SessionWithTokens } from "@/config/authConfig";
  61. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  62. import GoodPickExecutionForm from "./JobPickExecutionForm";
  63. import FGPickOrderCard from "./FGPickOrderCard";
  64. import LotConfirmationModal from "./LotConfirmationModal";
  65. import LinearProgressWithLabel from "../common/LinearProgressWithLabel";
  66. import ScanStatusAlert from "../common/ScanStatusAlert";
  67. interface Props {
  68. filterArgs: Record<string, any>;
  69. //onSwitchToRecordTab: () => void;
  70. onBackToList?: () => void;
  71. }
  72. // Manual Lot Confirmation Modal (align with GoodPickExecutiondetail, opened by {2fic})
  73. const ManualLotConfirmationModal: React.FC<{
  74. open: boolean;
  75. onClose: () => void;
  76. onConfirm: (expectedLotNo: string, scannedLotNo: string) => void;
  77. expectedLot: { lotNo: string; itemCode: string; itemName: string } | null;
  78. scannedLot: { lotNo: string; itemCode: string; itemName: string } | null;
  79. isLoading?: boolean;
  80. }> = ({ open, onClose, onConfirm, expectedLot, scannedLot, isLoading = false }) => {
  81. const { t } = useTranslation("jo");
  82. const [expectedLotInput, setExpectedLotInput] = useState<string>('');
  83. const [scannedLotInput, setScannedLotInput] = useState<string>('');
  84. const [error, setError] = useState<string>('');
  85. useEffect(() => {
  86. if (open) {
  87. setExpectedLotInput(expectedLot?.lotNo || '');
  88. setScannedLotInput(scannedLot?.lotNo || '');
  89. setError('');
  90. }
  91. }, [open, expectedLot, scannedLot]);
  92. const handleConfirm = () => {
  93. if (!expectedLotInput.trim() || !scannedLotInput.trim()) {
  94. setError(t("Please enter both expected and scanned lot numbers."));
  95. return;
  96. }
  97. if (expectedLotInput.trim() === scannedLotInput.trim()) {
  98. setError(t("Expected and scanned lot numbers cannot be the same."));
  99. return;
  100. }
  101. onConfirm(expectedLotInput.trim(), scannedLotInput.trim());
  102. };
  103. return (
  104. <Modal open={open} onClose={onClose}>
  105. <Box sx={{
  106. position: 'absolute',
  107. top: '50%',
  108. left: '50%',
  109. transform: 'translate(-50%, -50%)',
  110. bgcolor: 'background.paper',
  111. p: 3,
  112. borderRadius: 2,
  113. minWidth: 500,
  114. }}>
  115. <Typography variant="h6" gutterBottom color="warning.main">
  116. {t("Manual Lot Confirmation")}
  117. </Typography>
  118. <Box sx={{ mb: 2 }}>
  119. <Typography variant="body2" gutterBottom>
  120. <strong>{t("Expected Lot Number")}:</strong>
  121. </Typography>
  122. <TextField
  123. fullWidth
  124. size="small"
  125. value={expectedLotInput}
  126. onChange={(e) => { setExpectedLotInput(e.target.value); setError(''); }}
  127. sx={{ mb: 2 }}
  128. error={!!error && !expectedLotInput.trim()}
  129. />
  130. </Box>
  131. <Box sx={{ mb: 2 }}>
  132. <Typography variant="body2" gutterBottom>
  133. <strong>{t("Scanned Lot Number")}:</strong>
  134. </Typography>
  135. <TextField
  136. fullWidth
  137. size="small"
  138. value={scannedLotInput}
  139. onChange={(e) => { setScannedLotInput(e.target.value); setError(''); }}
  140. sx={{ mb: 2 }}
  141. error={!!error && !scannedLotInput.trim()}
  142. />
  143. </Box>
  144. {error && (
  145. <Box sx={{ mb: 2, p: 1, backgroundColor: '#ffebee', borderRadius: 1 }}>
  146. <Typography variant="body2" color="error">
  147. {error}
  148. </Typography>
  149. </Box>
  150. )}
  151. <Box sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
  152. <Button onClick={onClose} variant="outlined" disabled={isLoading}>
  153. {t("Cancel")}
  154. </Button>
  155. <Button
  156. onClick={handleConfirm}
  157. variant="contained"
  158. color="warning"
  159. disabled={isLoading || !expectedLotInput.trim() || !scannedLotInput.trim()}
  160. >
  161. {isLoading ? t("Processing...") : t("Confirm")}
  162. </Button>
  163. </Box>
  164. </Box>
  165. </Modal>
  166. );
  167. };
  168. // QR Code Modal Component (from GoodPickExecution)
  169. const QrCodeModal: React.FC<{
  170. open: boolean;
  171. onClose: () => void;
  172. lot: any | null;
  173. onQrCodeSubmit: (lotNo: string) => void;
  174. combinedLotData: any[];
  175. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
  176. const { t } = useTranslation("jo");
  177. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  178. const [manualInput, setManualInput] = useState<string>('');
  179. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  180. const [manualInputError, setManualInputError] = useState<boolean>(false);
  181. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  182. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  183. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  184. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  185. const [scannedQrResult, setScannedQrResult] = useState<string>('');
  186. // Process scanned QR codes
  187. useEffect(() => {
  188. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  189. const latestQr = qrValues[qrValues.length - 1];
  190. if (processedQrCodes.has(latestQr)) {
  191. console.log("QR code already processed, skipping...");
  192. return;
  193. }
  194. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  195. try {
  196. const qrData = JSON.parse(latestQr);
  197. if (qrData.stockInLineId && qrData.itemId) {
  198. setIsProcessingQr(true);
  199. setQrScanFailed(false);
  200. fetchStockInLineInfo(qrData.stockInLineId)
  201. .then((stockInLineInfo) => {
  202. console.log("Stock in line info:", stockInLineInfo);
  203. setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
  204. if (stockInLineInfo.lotNo === lot.lotNo) {
  205. console.log(` QR Code verified for lot: ${lot.lotNo}`);
  206. setQrScanSuccess(true);
  207. onQrCodeSubmit(lot.lotNo);
  208. onClose();
  209. resetScan();
  210. } else {
  211. console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
  212. setQrScanFailed(true);
  213. setManualInputError(true);
  214. setManualInputSubmitted(true);
  215. }
  216. })
  217. .catch((error) => {
  218. console.error("Error fetching stock in line info:", error);
  219. setScannedQrResult('Error fetching data');
  220. setQrScanFailed(true);
  221. setManualInputError(true);
  222. setManualInputSubmitted(true);
  223. })
  224. .finally(() => {
  225. setIsProcessingQr(false);
  226. });
  227. } else {
  228. const qrContent = latestQr.replace(/[{}]/g, '');
  229. setScannedQrResult(qrContent);
  230. if (qrContent === lot.lotNo) {
  231. setQrScanSuccess(true);
  232. onQrCodeSubmit(lot.lotNo);
  233. onClose();
  234. resetScan();
  235. } else {
  236. setQrScanFailed(true);
  237. setManualInputError(true);
  238. setManualInputSubmitted(true);
  239. }
  240. }
  241. } catch (error) {
  242. console.log("QR code is not JSON format, trying direct comparison");
  243. const qrContent = latestQr.replace(/[{}]/g, '');
  244. setScannedQrResult(qrContent);
  245. if (qrContent === lot.lotNo) {
  246. setQrScanSuccess(true);
  247. onQrCodeSubmit(lot.lotNo);
  248. onClose();
  249. resetScan();
  250. } else {
  251. setQrScanFailed(true);
  252. setManualInputError(true);
  253. setManualInputSubmitted(true);
  254. }
  255. }
  256. }
  257. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]);
  258. // Clear states when modal opens
  259. useEffect(() => {
  260. if (open) {
  261. setManualInput('');
  262. setManualInputSubmitted(false);
  263. setManualInputError(false);
  264. setIsProcessingQr(false);
  265. setQrScanFailed(false);
  266. setQrScanSuccess(false);
  267. setScannedQrResult('');
  268. setProcessedQrCodes(new Set());
  269. }
  270. }, [open]);
  271. useEffect(() => {
  272. if (lot) {
  273. setManualInput('');
  274. setManualInputSubmitted(false);
  275. setManualInputError(false);
  276. setIsProcessingQr(false);
  277. setQrScanFailed(false);
  278. setQrScanSuccess(false);
  279. setScannedQrResult('');
  280. setProcessedQrCodes(new Set());
  281. }
  282. }, [lot]);
  283. // Auto-submit manual input when it matches
  284. useEffect(() => {
  285. if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
  286. console.log(' Auto-submitting manual input:', manualInput.trim());
  287. const timer = setTimeout(() => {
  288. setQrScanSuccess(true);
  289. onQrCodeSubmit(lot.lotNo);
  290. onClose();
  291. setManualInput('');
  292. setManualInputError(false);
  293. setManualInputSubmitted(false);
  294. }, 200);
  295. return () => clearTimeout(timer);
  296. }
  297. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  298. const handleManualSubmit = () => {
  299. if (manualInput.trim() === lot?.lotNo) {
  300. setQrScanSuccess(true);
  301. onQrCodeSubmit(lot.lotNo);
  302. onClose();
  303. setManualInput('');
  304. } else {
  305. setQrScanFailed(true);
  306. setManualInputError(true);
  307. setManualInputSubmitted(true);
  308. }
  309. };
  310. useEffect(() => {
  311. if (open) {
  312. startScan();
  313. }
  314. }, [open, startScan]);
  315. return (
  316. <Modal open={open} onClose={onClose}>
  317. <Box sx={{
  318. position: 'absolute',
  319. top: '50%',
  320. left: '50%',
  321. transform: 'translate(-50%, -50%)',
  322. bgcolor: 'background.paper',
  323. p: 3,
  324. borderRadius: 2,
  325. minWidth: 400,
  326. }}>
  327. <Typography variant="h6" gutterBottom>
  328. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  329. </Typography>
  330. {isProcessingQr && (
  331. <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
  332. <Typography variant="body2" color="primary">
  333. {t("Processing QR code...")}
  334. </Typography>
  335. </Box>
  336. )}
  337. <Box sx={{ mb: 2 }}>
  338. <Typography variant="body2" gutterBottom>
  339. <strong>{t("Manual Input")}:</strong>
  340. </Typography>
  341. <TextField
  342. fullWidth
  343. size="small"
  344. value={manualInput}
  345. onChange={(e) => {
  346. setManualInput(e.target.value);
  347. if (qrScanFailed || manualInputError) {
  348. setQrScanFailed(false);
  349. setManualInputError(false);
  350. setManualInputSubmitted(false);
  351. }
  352. }}
  353. sx={{ mb: 1 }}
  354. error={manualInputSubmitted && manualInputError}
  355. helperText={
  356. manualInputSubmitted && manualInputError
  357. ? `${t("The input is not the same as the expected lot number.")}`
  358. : ''
  359. }
  360. />
  361. <Button
  362. variant="contained"
  363. onClick={handleManualSubmit}
  364. disabled={!manualInput.trim()}
  365. size="small"
  366. color="primary"
  367. >
  368. {t("Submit")}
  369. </Button>
  370. </Box>
  371. {qrValues.length > 0 && (
  372. <Box sx={{
  373. mb: 2,
  374. p: 2,
  375. backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
  376. borderRadius: 1
  377. }}>
  378. <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
  379. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  380. </Typography>
  381. {qrScanSuccess && (
  382. <Typography variant="caption" color="success" display="block">
  383. {t("Verified successfully!")}
  384. </Typography>
  385. )}
  386. </Box>
  387. )}
  388. <Box sx={{ mt: 2, textAlign: 'right' }}>
  389. <Button onClick={onClose} variant="outlined">
  390. {t("Cancel")}
  391. </Button>
  392. </Box>
  393. </Box>
  394. </Modal>
  395. );
  396. };
  397. const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
  398. const { t } = useTranslation("jo");
  399. const router = useRouter();
  400. const { data: session } = useSession() as { data: SessionWithTokens | null };
  401. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  402. // 修改:使用 Job Order 数据结构
  403. const [combinedDataLoading, setCombinedDataLoading] = useState(false);
  404. // 添加未分配订单状态
  405. const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
  406. const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
  407. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  408. const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
  409. const [expectedLotData, setExpectedLotData] = useState<any>(null);
  410. const [scannedLotData, setScannedLotData] = useState<any>(null);
  411. const [isConfirmingLot, setIsConfirmingLot] = useState(false);
  412. const [qrScanInput, setQrScanInput] = useState<string>('');
  413. const [qrScanError, setQrScanError] = useState<boolean>(false);
  414. const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>('');
  415. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  416. const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null);
  417. const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
  418. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  419. const [paginationController, setPaginationController] = useState({
  420. pageNum: 0,
  421. pageSize: 10,
  422. });
  423. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  424. const initializationRef = useRef(false);
  425. const autoAssignRef = useRef(false);
  426. const formProps = useForm();
  427. const errors = formProps.formState.errors;
  428. const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
  429. // Add QR modal states
  430. const [qrModalOpen, setQrModalOpen] = useState(false);
  431. const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
  432. // Add GoodPickExecutionForm states
  433. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  434. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
  435. const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
  436. const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
  437. // Add these missing state variables
  438. const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
  439. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  440. const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
  441. const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
  442. // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling
  443. const [processedQrCombinations, setProcessedQrCombinations] = useState<Map<number, Set<number>>>(new Map());
  444. // Cache for fetchStockInLineInfo API calls to avoid redundant requests
  445. const stockInLineInfoCache = useRef<Map<number, { lotNo: string | null; timestamp: number }>>(new Map());
  446. const CACHE_TTL = 60000; // 60 seconds cache TTL
  447. const abortControllerRef = useRef<AbortController | null>(null);
  448. const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  449. // Use refs for processed QR tracking to avoid useEffect dependency issues and delays
  450. const processedQrCodesRef = useRef<Set<string>>(new Set());
  451. const lastProcessedQrRef = useRef<string>('');
  452. // Store callbacks in refs to avoid useEffect dependency issues
  453. const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | null>(null);
  454. const resetScanRef = useRef<(() => void) | null>(null);
  455. // Manual lot confirmation modal state (test shortcut {2fic})
  456. const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false);
  457. const getAllLotsFromHierarchical = useCallback((
  458. data: JobOrderLotsHierarchicalResponse | null
  459. ): any[] => {
  460. if (!data || !data.pickOrder || !data.pickOrderLines) {
  461. return [];
  462. }
  463. const allLots: any[] = [];
  464. data.pickOrderLines.forEach((line) => {
  465. if (line.lots && line.lots.length > 0) {
  466. line.lots.forEach((lot) => {
  467. allLots.push({
  468. ...lot,
  469. pickOrderLineId: line.id,
  470. itemId: line.itemId,
  471. itemCode: line.itemCode,
  472. itemName: line.itemName,
  473. uomCode: line.uomCode,
  474. uomDesc: line.uomDesc,
  475. pickOrderLineRequiredQty: line.requiredQty,
  476. pickOrderLineStatus: line.status,
  477. jobOrderId: data.pickOrder.jobOrder.id,
  478. jobOrderCode: data.pickOrder.jobOrder.code,
  479. // 添加 pickOrder 信息(如果需要)
  480. pickOrderId: data.pickOrder.id,
  481. pickOrderCode: data.pickOrder.code,
  482. pickOrderConsoCode: data.pickOrder.consoCode,
  483. pickOrderTargetDate: data.pickOrder.targetDate,
  484. pickOrderType: data.pickOrder.type,
  485. pickOrderStatus: data.pickOrder.status,
  486. pickOrderAssignTo: data.pickOrder.assignTo,
  487. handler: line.handler,
  488. });
  489. });
  490. }
  491. });
  492. return allLots;
  493. }, []);
  494. const combinedLotData = useMemo(() => {
  495. return getAllLotsFromHierarchical(jobOrderData);
  496. }, [jobOrderData, getAllLotsFromHierarchical]);
  497. const originalCombinedData = useMemo(() => {
  498. return getAllLotsFromHierarchical(jobOrderData);
  499. }, [jobOrderData, getAllLotsFromHierarchical]);
  500. // Enhanced lotDataIndexes with cached active lots for better performance (align with GoodPickExecutiondetail)
  501. const lotDataIndexes = useMemo(() => {
  502. const byItemId = new Map<number, any[]>();
  503. const byItemCode = new Map<string, any[]>();
  504. const byLotId = new Map<number, any>();
  505. const byLotNo = new Map<string, any[]>();
  506. const byStockInLineId = new Map<number, any[]>();
  507. const activeLotsByItemId = new Map<number, any[]>();
  508. const rejectedStatuses = new Set(['rejected']);
  509. for (let i = 0; i < combinedLotData.length; i++) {
  510. const lot = combinedLotData[i];
  511. const isActive =
  512. !rejectedStatuses.has(lot.lotAvailability) &&
  513. !rejectedStatuses.has(lot.stockOutLineStatus) &&
  514. !rejectedStatuses.has(lot.processingStatus) &&
  515. lot.stockOutLineStatus !== 'completed';
  516. if (lot.itemId) {
  517. if (!byItemId.has(lot.itemId)) {
  518. byItemId.set(lot.itemId, []);
  519. activeLotsByItemId.set(lot.itemId, []);
  520. }
  521. byItemId.get(lot.itemId)!.push(lot);
  522. if (isActive) activeLotsByItemId.get(lot.itemId)!.push(lot);
  523. }
  524. if (lot.itemCode) {
  525. if (!byItemCode.has(lot.itemCode)) byItemCode.set(lot.itemCode, []);
  526. byItemCode.get(lot.itemCode)!.push(lot);
  527. }
  528. if (lot.lotId) byLotId.set(lot.lotId, lot);
  529. if (lot.lotNo) {
  530. if (!byLotNo.has(lot.lotNo)) byLotNo.set(lot.lotNo, []);
  531. byLotNo.get(lot.lotNo)!.push(lot);
  532. }
  533. if (lot.stockInLineId) {
  534. if (!byStockInLineId.has(lot.stockInLineId)) byStockInLineId.set(lot.stockInLineId, []);
  535. byStockInLineId.get(lot.stockInLineId)!.push(lot);
  536. }
  537. }
  538. return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId };
  539. }, [combinedLotData]);
  540. // Cached version of fetchStockInLineInfo to avoid redundant API calls
  541. const fetchStockInLineInfoCached = useCallback(async (stockInLineId: number): Promise<{ lotNo: string | null }> => {
  542. const now = Date.now();
  543. const cached = stockInLineInfoCache.current.get(stockInLineId);
  544. if (cached && (now - cached.timestamp) < CACHE_TTL) {
  545. return { lotNo: cached.lotNo };
  546. }
  547. if (abortControllerRef.current) abortControllerRef.current.abort();
  548. const abortController = new AbortController();
  549. abortControllerRef.current = abortController;
  550. const stockInLineInfo = await fetchStockInLineInfo(stockInLineId);
  551. stockInLineInfoCache.current.set(stockInLineId, {
  552. lotNo: stockInLineInfo.lotNo || null,
  553. timestamp: now
  554. });
  555. if (stockInLineInfoCache.current.size > 100) {
  556. const firstKey = stockInLineInfoCache.current.keys().next().value;
  557. if (firstKey !== undefined) stockInLineInfoCache.current.delete(firstKey);
  558. }
  559. return { lotNo: stockInLineInfo.lotNo || null };
  560. }, []);
  561. // 修改:加载未分配的 Job Order 订单
  562. const loadUnassignedOrders = useCallback(async () => {
  563. setIsLoadingUnassigned(true);
  564. try {
  565. //const orders = await fetchUnassignedJobOrderPickOrders();
  566. //setUnassignedOrders(orders);
  567. } catch (error) {
  568. console.error("Error loading unassigned orders:", error);
  569. } finally {
  570. setIsLoadingUnassigned(false);
  571. }
  572. }, []);
  573. // 修改:分配订单给当前用户
  574. const handleAssignOrder = useCallback(async (pickOrderId: number) => {
  575. if (!currentUserId) {
  576. console.error("Missing user id in session");
  577. return;
  578. }
  579. try {
  580. const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
  581. if (result.message === "Successfully assigned") {
  582. console.log(" Successfully assigned pick order");
  583. // 刷新数据
  584. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  585. // 重新加载未分配订单列表
  586. loadUnassignedOrders();
  587. } else {
  588. console.warn("⚠️ Assignment failed:", result.message);
  589. alert(`Assignment failed: ${result.message}`);
  590. }
  591. } catch (error) {
  592. console.error("❌ Error assigning order:", error);
  593. alert("Error occurred during assignment");
  594. }
  595. }, [currentUserId, loadUnassignedOrders]);
  596. const fetchFgPickOrdersData = useCallback(async () => {
  597. if (!currentUserId) return;
  598. setFgPickOrdersLoading(true);
  599. try {
  600. // Get all pick order IDs from combinedLotData
  601. const pickOrderIds = Array.from(new Set(combinedLotData.map(lot => lot.pickOrderId)));
  602. if (pickOrderIds.length === 0) {
  603. setFgPickOrders([]);
  604. return;
  605. }
  606. // Fetch FG pick orders for each pick order ID
  607. const fgPickOrdersPromises = pickOrderIds.map(pickOrderId =>
  608. fetchFGPickOrders(pickOrderId)
  609. );
  610. const fgPickOrdersResults = await Promise.all(fgPickOrdersPromises);
  611. // Flatten the results (each fetchFGPickOrders returns an array)
  612. const allFgPickOrders = fgPickOrdersResults.flat();
  613. setFgPickOrders(allFgPickOrders);
  614. console.log(" Fetched FG pick orders:", allFgPickOrders);
  615. } catch (error) {
  616. console.error("❌ Error fetching FG pick orders:", error);
  617. setFgPickOrders([]);
  618. } finally {
  619. setFgPickOrdersLoading(false);
  620. }
  621. }, [currentUserId, combinedLotData]);
  622. useEffect(() => {
  623. if (combinedLotData.length > 0) {
  624. fetchFgPickOrdersData();
  625. }
  626. }, [combinedLotData, fetchFgPickOrdersData]);
  627. // Handle QR code button click
  628. const handleQrCodeClick = (pickOrderId: number) => {
  629. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  630. // TODO: Implement QR code functionality
  631. };
  632. // 修改:使用 Job Order API 获取数据
  633. const fetchJobOrderData = useCallback(async (pickOrderId?: number) => {
  634. setCombinedDataLoading(true);
  635. try {
  636. if (!pickOrderId) {
  637. console.warn("⚠️ No pickOrderId provided, skipping API call");
  638. setJobOrderData(null);
  639. return;
  640. }
  641. // 直接使用类型化的响应
  642. const jobOrderData = await fetchJobOrderLotsHierarchicalByPickOrderId(pickOrderId);
  643. console.log("✅ Job Order data (hierarchical):", jobOrderData);
  644. setJobOrderData(jobOrderData);
  645. // 使用辅助函数获取所有 lots(不再扁平化)
  646. const allLots = getAllLotsFromHierarchical(jobOrderData);
  647. // ... 其他逻辑保持不变 ...
  648. } catch (error) {
  649. console.error("❌ Error fetching job order data:", error);
  650. setJobOrderData(null);
  651. } finally {
  652. setCombinedDataLoading(false);
  653. }
  654. }, [getAllLotsFromHierarchical]);
  655. const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => {
  656. if (!currentUserId || !pickOrderId || !itemId) {
  657. return;
  658. }
  659. try {
  660. console.log(`Updating JoPickOrder.handledBy for pickOrderId: ${pickOrderId}, itemId: ${itemId}, userId: ${currentUserId}`);
  661. await updateJoPickOrderHandledBy({
  662. pickOrderId: pickOrderId,
  663. itemId: itemId,
  664. userId: currentUserId
  665. });
  666. console.log("✅ JoPickOrder.handledBy updated successfully");
  667. } catch (error) {
  668. console.error("❌ Error updating JoPickOrder.handledBy:", error);
  669. // Don't throw - this is not critical for the main flow
  670. }
  671. }, [currentUserId]);
  672. // 修改:初始化时加载数据
  673. useEffect(() => {
  674. if (session && currentUserId && !initializationRef.current) {
  675. console.log("✅ Session loaded, initializing job order...");
  676. initializationRef.current = true;
  677. // Get pickOrderId from filterArgs if available (when viewing from list)
  678. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  679. if (pickOrderId) {
  680. fetchJobOrderData(pickOrderId);
  681. }
  682. loadUnassignedOrders();
  683. }
  684. }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders, filterArgs?.pickOrderId]);
  685. // Add event listener for manual assignment
  686. useEffect(() => {
  687. const handlePickOrderAssigned = () => {
  688. console.log("🔄 Pick order assigned event received, refreshing data...");
  689. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  690. if (pickOrderId) {
  691. fetchJobOrderData(pickOrderId);
  692. }
  693. };
  694. window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
  695. return () => {
  696. window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
  697. };
  698. }, [fetchJobOrderData, filterArgs?.pickOrderId]);
  699. // Handle QR code submission for matched lot (external scanning)
  700. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  701. console.log(` Processing QR Code for lot: ${lotNo}`);
  702. // Use current data without refreshing to avoid infinite loop
  703. const currentLotData = combinedLotData;
  704. console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
  705. const matchingLots = currentLotData.filter(lot =>
  706. lot.lotNo === lotNo ||
  707. lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
  708. );
  709. if (matchingLots.length === 0) {
  710. console.error(`❌ Lot not found: ${lotNo}`);
  711. setQrScanError(true);
  712. setQrScanSuccess(false);
  713. const availableLotNos = currentLotData.map(lot => lot.lotNo).join(', ');
  714. console.log(`❌ QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`);
  715. return;
  716. }
  717. console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
  718. setQrScanError(false);
  719. try {
  720. let successCount = 0;
  721. let errorCount = 0;
  722. for (const matchingLot of matchingLots) {
  723. console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
  724. if (matchingLot.stockOutLineId) {
  725. const stockOutLineUpdate = await updateStockOutLineStatus({
  726. id: matchingLot.stockOutLineId,
  727. status: 'checked',
  728. qty: 0
  729. });
  730. console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate);
  731. // Treat multiple backend shapes as success (type-safe via any)
  732. const r: any = stockOutLineUpdate as any;
  733. const updateOk =
  734. r?.code === 'SUCCESS' ||
  735. typeof r?.id === 'number' ||
  736. r?.type === 'checked' ||
  737. r?.status === 'checked' ||
  738. typeof r?.entity?.id === 'number' ||
  739. r?.entity?.status === 'checked';
  740. if (updateOk) {
  741. successCount++;
  742. } else {
  743. errorCount++;
  744. }
  745. } else {
  746. const createStockOutLineData = {
  747. consoCode: matchingLot.pickOrderConsoCode,
  748. pickOrderLineId: matchingLot.pickOrderLineId,
  749. inventoryLotLineId: matchingLot.lotId,
  750. qty: 0
  751. };
  752. const createResult = await createStockOutLine(createStockOutLineData);
  753. console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult);
  754. if (createResult && createResult.code === "SUCCESS") {
  755. // Immediately set status to checked for new line
  756. let newSolId: number | undefined;
  757. const anyRes: any = createResult as any;
  758. if (typeof anyRes?.id === 'number') {
  759. newSolId = anyRes.id;
  760. } else if (anyRes?.entity) {
  761. newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id;
  762. }
  763. if (newSolId) {
  764. const setChecked = await updateStockOutLineStatus({
  765. id: newSolId,
  766. status: 'checked',
  767. qty: 0
  768. });
  769. if (setChecked && setChecked.code === "SUCCESS") {
  770. successCount++;
  771. } else {
  772. errorCount++;
  773. }
  774. } else {
  775. console.warn("Created stock out line but no ID returned; cannot set to checked");
  776. errorCount++;
  777. }
  778. } else {
  779. errorCount++;
  780. }
  781. }
  782. }
  783. // FIXED: Set refresh flag before refreshing data
  784. setIsRefreshingData(true);
  785. console.log("🔄 Refreshing data after QR code processing...");
  786. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  787. await fetchJobOrderData(pickOrderId);
  788. if (successCount > 0) {
  789. console.log(` QR Code processing completed: ${successCount} updated/created`);
  790. setQrScanSuccess(true);
  791. setQrScanError(false);
  792. setQrScanInput(''); // Clear input after successful processing
  793. } else {
  794. console.error(`❌ QR Code processing failed: ${errorCount} errors`);
  795. setQrScanError(true);
  796. setQrScanSuccess(false);
  797. }
  798. } catch (error) {
  799. console.error("❌ Error processing QR code:", error);
  800. setQrScanError(true);
  801. setQrScanSuccess(false);
  802. // Still refresh data even on error
  803. setIsRefreshingData(true);
  804. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  805. await fetchJobOrderData( pickOrderId);
  806. } finally {
  807. // Clear refresh flag after a short delay
  808. setTimeout(() => {
  809. setIsRefreshingData(false);
  810. }, 1000);
  811. }
  812. }, [combinedLotData, fetchJobOrderData]);
  813. const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => {
  814. console.log("⚠️ [LOT MISMATCH] Lot mismatch detected:", { expectedLot, scannedLot });
  815. console.log("⚠️ [LOT MISMATCH] Opening confirmation modal - NO lot will be marked as scanned until user confirms");
  816. // ✅ schedule modal open in next tick (avoid flushSync warnings on some builds)
  817. // ✅ IMPORTANT: This function ONLY opens the modal. It does NOT process any lot.
  818. setTimeout(() => {
  819. setExpectedLotData(expectedLot);
  820. setScannedLotData({
  821. ...scannedLot,
  822. lotNo: scannedLot.lotNo || null,
  823. });
  824. setLotConfirmationOpen(true);
  825. console.log("⚠️ [LOT MISMATCH] Modal opened - waiting for user confirmation");
  826. }, 0);
  827. // ✅ Fetch lotNo in background for display purposes (cached)
  828. // ✅ This is ONLY for display - it does NOT process any lot
  829. if (!scannedLot.lotNo && scannedLot.stockInLineId) {
  830. console.log(`⚠️ [LOT MISMATCH] Fetching lotNo for display (stockInLineId: ${scannedLot.stockInLineId})`);
  831. fetchStockInLineInfoCached(scannedLot.stockInLineId)
  832. .then((info) => {
  833. console.log(`⚠️ [LOT MISMATCH] Fetched lotNo for display: ${info.lotNo}`);
  834. startTransition(() => {
  835. setScannedLotData((prev: any) => ({
  836. ...prev,
  837. lotNo: info.lotNo || null,
  838. }));
  839. });
  840. })
  841. .catch((error) => {
  842. console.error(`❌ [LOT MISMATCH] Error fetching lotNo for display (stockInLineId may not exist):`, error);
  843. // ignore display fetch errors - this does NOT affect processing
  844. });
  845. }
  846. }, [fetchStockInLineInfoCached]);
  847. // Add handleLotConfirmation function
  848. const handleLotConfirmation = useCallback(async () => {
  849. if (!expectedLotData || !scannedLotData || !selectedLotForQr) {
  850. console.error("❌ [LOT CONFIRM] Missing required data for lot confirmation");
  851. return;
  852. }
  853. console.log("✅ [LOT CONFIRM] User confirmed lot substitution - processing now");
  854. console.log("✅ [LOT CONFIRM] Expected lot:", expectedLotData);
  855. console.log("✅ [LOT CONFIRM] Scanned lot:", scannedLotData);
  856. console.log("✅ [LOT CONFIRM] Selected lot for QR:", selectedLotForQr);
  857. setIsConfirmingLot(true);
  858. try {
  859. let newLotLineId = scannedLotData?.inventoryLotLineId;
  860. if (!newLotLineId && scannedLotData?.stockInLineId) {
  861. try {
  862. console.log(`🔍 [LOT CONFIRM] Fetching lot detail for stockInLineId: ${scannedLotData.stockInLineId}`);
  863. const ld = await fetchLotDetail(scannedLotData.stockInLineId);
  864. newLotLineId = ld.inventoryLotLineId;
  865. console.log(`✅ [LOT CONFIRM] Fetched lot detail: inventoryLotLineId=${newLotLineId}`);
  866. } catch (error) {
  867. console.error("❌ [LOT CONFIRM] Error fetching lot detail (stockInLineId may not exist):", error);
  868. // If stockInLineId doesn't exist, we can still proceed with lotNo substitution
  869. // The backend confirmLotSubstitution should handle this case
  870. }
  871. }
  872. if (!newLotLineId) {
  873. console.warn("⚠️ [LOT CONFIRM] No inventory lot line id for scanned lot, proceeding with lotNo only");
  874. // Continue anyway - backend may handle lotNo substitution without inventoryLotLineId
  875. }
  876. console.log("=== [LOT CONFIRM] Lot Confirmation Debug ===");
  877. console.log("Selected Lot:", selectedLotForQr);
  878. console.log("Pick Order Line ID:", selectedLotForQr.pickOrderLineId);
  879. console.log("Stock Out Line ID:", selectedLotForQr.stockOutLineId);
  880. console.log("Suggested Pick Lot ID:", selectedLotForQr.suggestedPickLotId);
  881. console.log("Lot ID (fallback):", selectedLotForQr.lotId);
  882. console.log("New Inventory Lot Line ID:", newLotLineId);
  883. console.log("Scanned Lot No:", scannedLotData.lotNo);
  884. console.log("Scanned StockInLineId:", scannedLotData.stockInLineId);
  885. // Call confirmLotSubstitution to update the suggested lot
  886. console.log("🔄 [LOT CONFIRM] Calling confirmLotSubstitution...");
  887. const substitutionResult = await confirmLotSubstitution({
  888. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  889. stockOutLineId: selectedLotForQr.stockOutLineId,
  890. originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId,
  891. newInventoryLotNo: scannedLotData.lotNo || '',
  892. // ✅ required by LotSubstitutionConfirmRequest
  893. newStockInLineId: scannedLotData?.stockInLineId ?? null,
  894. });
  895. console.log("✅ [LOT CONFIRM] Lot substitution result:", substitutionResult);
  896. // ✅ CRITICAL: substitution failed => DO NOT mark original stockOutLine as checked.
  897. // Keep modal open so user can cancel/rescan.
  898. if (!substitutionResult || substitutionResult.code !== "SUCCESS") {
  899. console.error("❌ [LOT CONFIRM] Lot substitution failed. Will NOT update stockOutLine status.");
  900. setQrScanError(true);
  901. setQrScanSuccess(false);
  902. setQrScanErrorMsg(
  903. substitutionResult?.message ||
  904. `换批失败:stockInLineId ${scannedLotData?.stockInLineId ?? ""} 不存在或无法匹配`
  905. );
  906. return;
  907. }
  908. // Update stock out line status to 'checked' after substitution
  909. if(selectedLotForQr?.stockOutLineId){
  910. console.log(`🔄 [LOT CONFIRM] Updating stockOutLine ${selectedLotForQr.stockOutLineId} to 'checked'`);
  911. await updateStockOutLineStatus({
  912. id: selectedLotForQr.stockOutLineId,
  913. status: 'checked',
  914. qty: 0
  915. });
  916. console.log(`✅ [LOT CONFIRM] Stock out line ${selectedLotForQr.stockOutLineId} status updated to 'checked'`);
  917. }
  918. // Close modal and clean up state BEFORE refreshing
  919. setLotConfirmationOpen(false);
  920. setExpectedLotData(null);
  921. setScannedLotData(null);
  922. setSelectedLotForQr(null);
  923. // Clear QR processing state but DON'T clear processedQrCodes yet
  924. setQrScanError(false);
  925. setQrScanSuccess(true);
  926. setQrScanErrorMsg('');
  927. setQrScanInput('');
  928. // Set refreshing flag to prevent QR processing during refresh
  929. setIsRefreshingData(true);
  930. // Refresh data to show updated lot
  931. console.log("🔄 Refreshing job order data...");
  932. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  933. await fetchJobOrderData(pickOrderId);
  934. console.log(" Lot substitution confirmed and data refreshed");
  935. // Clear processed QR codes and flags immediately after refresh
  936. // This allows new QR codes to be processed right away
  937. setTimeout(() => {
  938. console.log(" Clearing processed QR codes and resuming scan");
  939. setProcessedQrCodes(new Set());
  940. setLastProcessedQr('');
  941. setQrScanSuccess(false);
  942. setIsRefreshingData(false);
  943. // ✅ Clear processedQrCombinations to allow reprocessing the same QR if needed
  944. if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
  945. setProcessedQrCombinations(prev => {
  946. const newMap = new Map(prev);
  947. const itemId = selectedLotForQr.itemId;
  948. if (itemId && newMap.has(itemId)) {
  949. newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
  950. if (newMap.get(itemId)!.size === 0) {
  951. newMap.delete(itemId);
  952. }
  953. }
  954. return newMap;
  955. });
  956. }
  957. }, 500); // Reduced from 3000ms to 500ms - just enough for UI update
  958. } catch (error) {
  959. console.error("Error confirming lot substitution:", error);
  960. setQrScanError(true);
  961. setQrScanSuccess(false);
  962. setQrScanErrorMsg('换批发生异常,请重试或联系管理员');
  963. // Clear refresh flag on error
  964. setIsRefreshingData(false);
  965. } finally {
  966. setIsConfirmingLot(false);
  967. }
  968. }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData]);
  969. const processOutsideQrCode = useCallback(async (latestQr: string) => {
  970. // ✅ Only JSON QR supported for outside scanner (avoid false positive with lotNo)
  971. let qrData: any = null;
  972. try {
  973. qrData = JSON.parse(latestQr);
  974. } catch {
  975. startTransition(() => {
  976. setQrScanError(true);
  977. setQrScanSuccess(false);
  978. });
  979. return;
  980. }
  981. if (!(qrData?.stockInLineId && qrData?.itemId)) {
  982. startTransition(() => {
  983. setQrScanError(true);
  984. setQrScanSuccess(false);
  985. });
  986. return;
  987. }
  988. const scannedItemId = Number(qrData.itemId);
  989. const scannedStockInLineId = Number(qrData.stockInLineId);
  990. // ✅ avoid duplicate processing by itemId+stockInLineId
  991. const itemProcessedSet = processedQrCombinations.get(scannedItemId);
  992. if (itemProcessedSet?.has(scannedStockInLineId)) return;
  993. const indexes = lotDataIndexes;
  994. const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || [];
  995. // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected
  996. const allLotsForItem = indexes.byItemId.get(scannedItemId) || [];
  997. // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots
  998. // This allows users to scan other lots even when all suggested lots are rejected
  999. const scannedLot = allLotsForItem.find(
  1000. (lot: any) => lot.stockInLineId === scannedStockInLineId
  1001. );
  1002. if (scannedLot) {
  1003. const isRejected =
  1004. scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1005. scannedLot.lotAvailability === 'rejected' ||
  1006. scannedLot.lotAvailability === 'status_unavailable';
  1007. if (isRejected) {
  1008. console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`);
  1009. startTransition(() => {
  1010. setQrScanError(true);
  1011. setQrScanSuccess(false);
  1012. setQrScanErrorMsg(
  1013. `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
  1014. );
  1015. });
  1016. // Mark as processed to prevent re-processing
  1017. setProcessedQrCombinations(prev => {
  1018. const newMap = new Map(prev);
  1019. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1020. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1021. return newMap;
  1022. });
  1023. return;
  1024. }
  1025. }
  1026. // ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching
  1027. if (activeSuggestedLots.length === 0) {
  1028. // Check if there are any lots for this item (even if all are rejected)
  1029. if (allLotsForItem.length === 0) {
  1030. console.error("No lots found for this item");
  1031. startTransition(() => {
  1032. setQrScanError(true);
  1033. setQrScanSuccess(false);
  1034. setQrScanErrorMsg("当前订单中没有此物品的批次信息");
  1035. });
  1036. return;
  1037. }
  1038. // ✅ Allow lot switching: find a rejected lot as expected lot, or use first lot
  1039. // This allows users to switch to a new lot even when all suggested lots are rejected
  1040. console.log(`⚠️ [QR PROCESS] No active suggested lots, but allowing lot switching. Scanned lot is not rejected.`);
  1041. // Find a rejected lot as expected lot (the one that was rejected)
  1042. const rejectedLot = allLotsForItem.find((lot: any) =>
  1043. lot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1044. lot.lotAvailability === 'rejected' ||
  1045. lot.lotAvailability === 'status_unavailable'
  1046. );
  1047. const expectedLot = rejectedLot || allLotsForItem[0]; // Use rejected lot if exists, otherwise first lot
  1048. // ✅ Always open confirmation modal when no active lots (user needs to confirm switching)
  1049. // handleLotMismatch will fetch lotNo from backend using stockInLineId if needed
  1050. console.log(`⚠️ [QR PROCESS] Opening confirmation modal for lot switch (no active lots)`);
  1051. setSelectedLotForQr(expectedLot);
  1052. handleLotMismatch(
  1053. {
  1054. lotNo: expectedLot.lotNo,
  1055. itemCode: expectedLot.itemCode,
  1056. itemName: expectedLot.itemName
  1057. },
  1058. {
  1059. lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null
  1060. itemCode: expectedLot.itemCode,
  1061. itemName: expectedLot.itemName,
  1062. inventoryLotLineId: scannedLot?.lotId || null,
  1063. stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo
  1064. }
  1065. );
  1066. return;
  1067. }
  1068. // ✅ direct stockInLineId match (O(1))
  1069. const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || [];
  1070. let exactMatch: any = null;
  1071. for (let i = 0; i < stockInLineLots.length; i++) {
  1072. const lot = stockInLineLots[i];
  1073. if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) {
  1074. exactMatch = lot;
  1075. break;
  1076. }
  1077. }
  1078. console.log(`🔍 [QR PROCESS] Scanned stockInLineId: ${scannedStockInLineId}, itemId: ${scannedItemId}`);
  1079. console.log(`🔍 [QR PROCESS] Found ${stockInLineLots.length} lots with stockInLineId ${scannedStockInLineId}`);
  1080. console.log(`🔍 [QR PROCESS] Exact match found: ${exactMatch ? `YES (lotNo: ${exactMatch.lotNo}, stockOutLineId: ${exactMatch.stockOutLineId})` : 'NO'}`);
  1081. // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots
  1082. // This handles the case where Lot A is rejected and user scans Lot B
  1083. if (!exactMatch && scannedLot && !activeSuggestedLots.includes(scannedLot)) {
  1084. // Scanned lot is not in active suggested lots, open confirmation modal
  1085. const expectedLot = activeSuggestedLots[0] || allLotsForItem[0]; // Use first active lot or first lot as expected
  1086. if (expectedLot && scannedLot.stockInLineId !== expectedLot.stockInLineId) {
  1087. console.log(`⚠️ [QR PROCESS] Scanned lot ${scannedLot.lotNo} is not in active suggested lots, opening confirmation modal`);
  1088. setSelectedLotForQr(expectedLot);
  1089. handleLotMismatch(
  1090. {
  1091. lotNo: expectedLot.lotNo,
  1092. itemCode: expectedLot.itemCode,
  1093. itemName: expectedLot.itemName
  1094. },
  1095. {
  1096. lotNo: scannedLot.lotNo || null,
  1097. itemCode: expectedLot.itemCode,
  1098. itemName: expectedLot.itemName,
  1099. inventoryLotLineId: scannedLot.lotId || null,
  1100. stockInLineId: scannedStockInLineId
  1101. }
  1102. );
  1103. return;
  1104. }
  1105. }
  1106. if (exactMatch) {
  1107. if (!exactMatch.stockOutLineId) {
  1108. console.error(`❌ [QR PROCESS] Exact match found but no stockOutLineId`);
  1109. startTransition(() => {
  1110. setQrScanError(true);
  1111. setQrScanSuccess(false);
  1112. });
  1113. return;
  1114. }
  1115. console.log(`✅ [QR PROCESS] Processing exact match: lotNo=${exactMatch.lotNo}, stockOutLineId=${exactMatch.stockOutLineId}`);
  1116. try {
  1117. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1118. pickOrderLineId: exactMatch.pickOrderLineId,
  1119. inventoryLotNo: exactMatch.lotNo,
  1120. stockOutLineId: exactMatch.stockOutLineId,
  1121. itemId: exactMatch.itemId,
  1122. status: "checked",
  1123. });
  1124. if (res.code === "checked" || res.code === "SUCCESS") {
  1125. console.log(`✅ [QR PROCESS] Successfully updated stockOutLine ${exactMatch.stockOutLineId} to checked`);
  1126. const entity = res.entity as any;
  1127. startTransition(() => {
  1128. setQrScanError(false);
  1129. setQrScanSuccess(true);
  1130. });
  1131. // mark combination processed
  1132. setProcessedQrCombinations(prev => {
  1133. const newMap = new Map(prev);
  1134. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1135. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1136. return newMap;
  1137. });
  1138. // refresh to keep consistency with server & handler updates
  1139. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1140. await fetchJobOrderData(pickOrderId);
  1141. } else {
  1142. console.error(`❌ [QR PROCESS] Update failed: ${res.code}`);
  1143. startTransition(() => {
  1144. setQrScanError(true);
  1145. setQrScanSuccess(false);
  1146. });
  1147. }
  1148. } catch (error) {
  1149. console.error(`❌ [QR PROCESS] Error updating stockOutLine:`, error);
  1150. startTransition(() => {
  1151. setQrScanError(true);
  1152. setQrScanSuccess(false);
  1153. });
  1154. }
  1155. return;
  1156. }
  1157. // ✅ mismatch: validate scanned stockInLineId exists before opening confirmation modal
  1158. console.log(`⚠️ [QR PROCESS] No exact match found. Validating scanned stockInLineId ${scannedStockInLineId} for itemId ${scannedItemId}`);
  1159. console.log(`⚠️ [QR PROCESS] Active suggested lots for itemId ${scannedItemId}:`, activeSuggestedLots.map(l => ({ lotNo: l.lotNo, stockInLineId: l.stockInLineId })));
  1160. if (activeSuggestedLots.length === 0) {
  1161. console.error(`❌ [QR PROCESS] No active suggested lots found for itemId ${scannedItemId}`);
  1162. startTransition(() => {
  1163. setQrScanError(true);
  1164. setQrScanSuccess(false);
  1165. setQrScanErrorMsg(`当前订单中没有 itemId ${scannedItemId} 的可用批次`);
  1166. });
  1167. return;
  1168. }
  1169. const expectedLot = activeSuggestedLots[0];
  1170. console.log(`⚠️ [QR PROCESS] Expected lot: ${expectedLot.lotNo} (stockInLineId: ${expectedLot.stockInLineId}), Scanned stockInLineId: ${scannedStockInLineId}`);
  1171. // ✅ Validate scanned stockInLineId exists before opening modal
  1172. // This ensures the backend can find the lot when user confirms
  1173. try {
  1174. console.log(`🔍 [QR PROCESS] Validating scanned stockInLineId ${scannedStockInLineId} exists...`);
  1175. const stockInLineInfo = await fetchStockInLineInfoCached(scannedStockInLineId);
  1176. console.log(`✅ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} exists, lotNo: ${stockInLineInfo.lotNo}`);
  1177. // ✅ 检查扫描的批次是否已被拒绝
  1178. const scannedLot = combinedLotData.find(
  1179. (lot: any) => lot.stockInLineId === scannedStockInLineId && lot.itemId === scannedItemId
  1180. );
  1181. if (scannedLot) {
  1182. const isRejected =
  1183. scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1184. scannedLot.lotAvailability === 'rejected' ||
  1185. scannedLot.lotAvailability === 'status_unavailable';
  1186. if (isRejected) {
  1187. console.warn(`⚠️ [QR PROCESS] Scanned lot ${stockInLineInfo.lotNo} (stockInLineId: ${scannedStockInLineId}) is rejected or unavailable`);
  1188. startTransition(() => {
  1189. setQrScanError(true);
  1190. setQrScanSuccess(false);
  1191. setQrScanErrorMsg(
  1192. `此批次(${stockInLineInfo.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
  1193. );
  1194. });
  1195. // Mark as processed to prevent re-processing
  1196. setProcessedQrCombinations(prev => {
  1197. const newMap = new Map(prev);
  1198. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1199. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1200. return newMap;
  1201. });
  1202. return;
  1203. }
  1204. }
  1205. // ✅ stockInLineId exists and is not rejected, open confirmation modal
  1206. console.log(`⚠️ [QR PROCESS] Opening confirmation modal - user must confirm before any lot is marked as scanned`);
  1207. setSelectedLotForQr(expectedLot);
  1208. handleLotMismatch(
  1209. {
  1210. lotNo: expectedLot.lotNo,
  1211. itemCode: expectedLot.itemCode,
  1212. itemName: expectedLot.itemName
  1213. },
  1214. {
  1215. lotNo: stockInLineInfo.lotNo || null, // Use fetched lotNo for display
  1216. itemCode: expectedLot.itemCode,
  1217. itemName: expectedLot.itemName,
  1218. inventoryLotLineId: null,
  1219. stockInLineId: scannedStockInLineId
  1220. }
  1221. );
  1222. } catch (error) {
  1223. // ✅ stockInLineId does NOT exist, show error immediately (don't open modal)
  1224. console.error(`❌ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} does NOT exist:`, error);
  1225. startTransition(() => {
  1226. setQrScanError(true);
  1227. setQrScanSuccess(false);
  1228. setQrScanErrorMsg(
  1229. `扫描的 stockInLineId ${scannedStockInLineId} 不存在。请检查 QR 码是否正确,或联系管理员。`
  1230. );
  1231. });
  1232. // Mark as processed to prevent re-processing
  1233. setProcessedQrCombinations(prev => {
  1234. const newMap = new Map(prev);
  1235. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1236. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1237. return newMap;
  1238. });
  1239. }
  1240. }, [filterArgs?.pickOrderId, fetchJobOrderData, handleLotMismatch, lotDataIndexes, processedQrCombinations, combinedLotData, fetchStockInLineInfoCached]);
  1241. // Store in refs for immediate access in qrValues effect
  1242. processOutsideQrCodeRef.current = processOutsideQrCode;
  1243. resetScanRef.current = resetScan;
  1244. const handleManualInputSubmit = useCallback(() => {
  1245. if (qrScanInput.trim() !== '') {
  1246. handleQrCodeSubmit(qrScanInput.trim());
  1247. }
  1248. }, [qrScanInput, handleQrCodeSubmit]);
  1249. // Handle QR code submission from modal (internal scanning)
  1250. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  1251. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  1252. console.log(` QR Code verified for lot: ${lotNo}`);
  1253. const requiredQty = selectedLotForQr.requiredQty;
  1254. const lotId = selectedLotForQr.lotId;
  1255. // Create stock out line
  1256. const stockOutLineData: CreateStockOutLine = {
  1257. consoCode: selectedLotForQr.pickOrderConsoCode,
  1258. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  1259. inventoryLotLineId: selectedLotForQr.lotId,
  1260. qty: 0.0
  1261. };
  1262. try {
  1263. await createStockOutLine(stockOutLineData);
  1264. console.log("Stock out line created successfully!");
  1265. // Close modal
  1266. setQrModalOpen(false);
  1267. setSelectedLotForQr(null);
  1268. // Set pick quantity
  1269. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  1270. setTimeout(() => {
  1271. setPickQtyData(prev => ({
  1272. ...prev,
  1273. [lotKey]: requiredQty
  1274. }));
  1275. console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  1276. }, 500);
  1277. // Refresh data
  1278. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1279. await fetchJobOrderData(pickOrderId);
  1280. } catch (error) {
  1281. console.error("Error creating stock out line:", error);
  1282. }
  1283. }
  1284. }, [selectedLotForQr, fetchJobOrderData]);
  1285. useEffect(() => {
  1286. // Skip if scanner not active or no data or currently refreshing
  1287. if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) return;
  1288. const latestQr = qrValues[qrValues.length - 1];
  1289. // ✅ Test shortcut: {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId
  1290. if ((latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) && latestQr.endsWith("}")) {
  1291. let content = '';
  1292. if (latestQr.startsWith("{2fittest")) content = latestQr.substring(9, latestQr.length - 1);
  1293. else content = latestQr.substring(8, latestQr.length - 1);
  1294. const parts = content.split(',');
  1295. if (parts.length === 2) {
  1296. const itemId = parseInt(parts[0].trim(), 10);
  1297. const stockInLineId = parseInt(parts[1].trim(), 10);
  1298. if (!isNaN(itemId) && !isNaN(stockInLineId)) {
  1299. const simulatedQr = JSON.stringify({ itemId, stockInLineId });
  1300. lastProcessedQrRef.current = latestQr;
  1301. processedQrCodesRef.current.add(latestQr);
  1302. setLastProcessedQr(latestQr);
  1303. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1304. processOutsideQrCodeRef.current?.(simulatedQr);
  1305. resetScanRef.current?.();
  1306. return;
  1307. }
  1308. }
  1309. }
  1310. // ✅ Shortcut: {2fic} open manual lot confirmation modal
  1311. if (latestQr === "{2fic}") {
  1312. setManualLotConfirmationOpen(true);
  1313. resetScanRef.current?.();
  1314. lastProcessedQrRef.current = latestQr;
  1315. processedQrCodesRef.current.add(latestQr);
  1316. setLastProcessedQr(latestQr);
  1317. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1318. return;
  1319. }
  1320. // Skip processing if modal open for same QR
  1321. if (lotConfirmationOpen || manualLotConfirmationOpen) {
  1322. if (latestQr === lastProcessedQrRef.current) return;
  1323. }
  1324. // Skip if already processed (refs)
  1325. if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) return;
  1326. // Mark processed immediately
  1327. lastProcessedQrRef.current = latestQr;
  1328. processedQrCodesRef.current.add(latestQr);
  1329. if (processedQrCodesRef.current.size > 100) {
  1330. const firstValue = processedQrCodesRef.current.values().next().value;
  1331. if (firstValue !== undefined) processedQrCodesRef.current.delete(firstValue);
  1332. }
  1333. // Process immediately
  1334. if (qrProcessingTimeoutRef.current) {
  1335. clearTimeout(qrProcessingTimeoutRef.current);
  1336. qrProcessingTimeoutRef.current = null;
  1337. }
  1338. processOutsideQrCodeRef.current?.(latestQr);
  1339. // UI state updates (non-blocking)
  1340. startTransition(() => {
  1341. setLastProcessedQr(latestQr);
  1342. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1343. });
  1344. return () => {
  1345. if (qrProcessingTimeoutRef.current) {
  1346. clearTimeout(qrProcessingTimeoutRef.current);
  1347. qrProcessingTimeoutRef.current = null;
  1348. }
  1349. };
  1350. }, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]);
  1351. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  1352. if (value === '' || value === null || value === undefined) {
  1353. setPickQtyData(prev => ({
  1354. ...prev,
  1355. [lotKey]: 0
  1356. }));
  1357. return;
  1358. }
  1359. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  1360. if (isNaN(numericValue)) {
  1361. setPickQtyData(prev => ({
  1362. ...prev,
  1363. [lotKey]: 0
  1364. }));
  1365. return;
  1366. }
  1367. setPickQtyData(prev => ({
  1368. ...prev,
  1369. [lotKey]: numericValue
  1370. }));
  1371. }, []);
  1372. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  1373. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  1374. const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
  1375. const checkAndAutoAssignNext = useCallback(async () => {
  1376. if (!currentUserId) return;
  1377. try {
  1378. const completionResponse = await checkPickOrderCompletion(currentUserId);
  1379. if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
  1380. console.log("Found completed pick orders, auto-assigning next...");
  1381. // 移除前端的自动分配逻辑,因为后端已经处理了
  1382. // await handleAutoAssignAndRelease(); // 删除这个函数
  1383. }
  1384. } catch (error) {
  1385. console.error("Error checking pick order completion:", error);
  1386. }
  1387. }, [currentUserId]);
  1388. // Handle submit pick quantity
  1389. const handleSubmitPickQty = useCallback(async (lot: any) => {
  1390. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  1391. const newQty = pickQtyData[lotKey] || 0;
  1392. if (!lot.stockOutLineId) {
  1393. console.error("No stock out line found for this lot");
  1394. return;
  1395. }
  1396. try {
  1397. const currentActualPickQty = lot.actualPickQty || 0;
  1398. const cumulativeQty = currentActualPickQty + newQty;
  1399. let newStatus = 'partially_completed';
  1400. if (cumulativeQty >= lot.requiredQty) {
  1401. newStatus = 'completed';
  1402. }
  1403. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  1404. console.log(`Lot: ${lot.lotNo}`);
  1405. console.log(`Required Qty: ${lot.requiredQty}`);
  1406. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  1407. console.log(`New Submitted Qty: ${newQty}`);
  1408. console.log(`Cumulative Qty: ${cumulativeQty}`);
  1409. console.log(`New Status: ${newStatus}`);
  1410. console.log(`=====================================`);
  1411. await updateStockOutLineStatus({
  1412. id: lot.stockOutLineId,
  1413. status: newStatus,
  1414. qty: cumulativeQty
  1415. });
  1416. if (newQty > 0) {
  1417. await updateInventoryLotLineQuantities({
  1418. inventoryLotLineId: lot.lotId,
  1419. qty: newQty,
  1420. status: 'available',
  1421. operation: 'pick'
  1422. });
  1423. }
  1424. // FIXED: Use the proper API function instead of direct fetch
  1425. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  1426. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1427. try {
  1428. // Use the imported API function instead of direct fetch
  1429. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1430. console.log(` Pick order completion check result:`, completionResponse);
  1431. if (completionResponse.code === "SUCCESS") {
  1432. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1433. } else if (completionResponse.message === "not completed") {
  1434. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1435. } else {
  1436. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1437. }
  1438. } catch (error) {
  1439. console.error("Error checking pick order completion:", error);
  1440. }
  1441. }
  1442. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1443. await fetchJobOrderData(pickOrderId);
  1444. console.log("Pick quantity submitted successfully!");
  1445. setTimeout(() => {
  1446. checkAndAutoAssignNext();
  1447. }, 1000);
  1448. } catch (error) {
  1449. console.error("Error submitting pick quantity:", error);
  1450. }
  1451. }, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]);
  1452. const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
  1453. if (!lot.stockOutLineId) {
  1454. console.error("No stock out line found for this lot");
  1455. return;
  1456. }
  1457. try {
  1458. // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0
  1459. if (submitQty === 0) {
  1460. console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
  1461. console.log(`Lot: ${lot.lotNo}`);
  1462. console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
  1463. console.log(`Setting status to 'completed' with qty: 0`);
  1464. const updateResult = await updateStockOutLineStatus({
  1465. id: lot.stockOutLineId,
  1466. status: 'completed',
  1467. qty: 0
  1468. });
  1469. console.log('Update result:', updateResult);
  1470. const r: any = updateResult as any;
  1471. const updateOk =
  1472. r?.code === 'SUCCESS' ||
  1473. r?.type === 'completed' ||
  1474. typeof r?.id === 'number' ||
  1475. typeof r?.entity?.id === 'number' ||
  1476. (r?.message && r.message.includes('successfully'));
  1477. if (!updateResult || !updateOk) {
  1478. console.error('Failed to update stock out line status:', updateResult);
  1479. throw new Error('Failed to update stock out line status');
  1480. }
  1481. // Check if pick order is completed
  1482. if (lot.pickOrderConsoCode) {
  1483. console.log(` Lot ${lot.lotNo} completed (all zeros), checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1484. try {
  1485. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1486. console.log(` Pick order completion check result:`, completionResponse);
  1487. if (completionResponse.code === "SUCCESS") {
  1488. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1489. setTimeout(() => {
  1490. if (onBackToList) {
  1491. onBackToList();
  1492. }
  1493. }, 1500);
  1494. } else if (completionResponse.message === "not completed") {
  1495. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1496. } else {
  1497. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1498. }
  1499. } catch (error) {
  1500. console.error("Error checking pick order completion:", error);
  1501. }
  1502. }
  1503. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1504. await fetchJobOrderData(pickOrderId);
  1505. console.log("All zeros submission completed successfully!");
  1506. setTimeout(() => {
  1507. checkAndAutoAssignNext();
  1508. }, 1000);
  1509. return;
  1510. }
  1511. // Normal case: Calculate cumulative quantity correctly
  1512. const currentActualPickQty = lot.actualPickQty || 0;
  1513. const cumulativeQty = currentActualPickQty + submitQty;
  1514. // Determine status based on cumulative quantity vs required quantity
  1515. let newStatus = 'partially_completed';
  1516. if (cumulativeQty >= lot.requiredQty) {
  1517. newStatus = 'completed';
  1518. } else if (cumulativeQty > 0) {
  1519. newStatus = 'partially_completed';
  1520. } else {
  1521. newStatus = 'checked'; // QR scanned but no quantity submitted yet
  1522. }
  1523. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  1524. console.log(`Lot: ${lot.lotNo}`);
  1525. console.log(`Required Qty: ${lot.requiredQty}`);
  1526. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  1527. console.log(`New Submitted Qty: ${submitQty}`);
  1528. console.log(`Cumulative Qty: ${cumulativeQty}`);
  1529. console.log(`New Status: ${newStatus}`);
  1530. console.log(`=====================================`);
  1531. await updateStockOutLineStatus({
  1532. id: lot.stockOutLineId,
  1533. status: newStatus,
  1534. qty: cumulativeQty
  1535. });
  1536. if (submitQty > 0) {
  1537. await updateInventoryLotLineQuantities({
  1538. inventoryLotLineId: lot.lotId,
  1539. qty: submitQty,
  1540. status: 'available',
  1541. operation: 'pick'
  1542. });
  1543. }
  1544. // Check if pick order is completed when lot status becomes 'completed'
  1545. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  1546. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1547. try {
  1548. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1549. console.log(` Pick order completion check result:`, completionResponse);
  1550. if (completionResponse.code === "SUCCESS") {
  1551. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1552. setTimeout(() => {
  1553. if (onBackToList) {
  1554. onBackToList();
  1555. }
  1556. }, 1500);
  1557. } else if (completionResponse.message === "not completed") {
  1558. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1559. } else {
  1560. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1561. }
  1562. } catch (error) {
  1563. console.error("Error checking pick order completion:", error);
  1564. }
  1565. }
  1566. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1567. await fetchJobOrderData(pickOrderId);
  1568. console.log("Pick quantity submitted successfully!");
  1569. setTimeout(() => {
  1570. checkAndAutoAssignNext();
  1571. }, 1000);
  1572. } catch (error) {
  1573. console.error("Error submitting pick quantity:", error);
  1574. }
  1575. }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]);
  1576. const handleSkip = useCallback(async (lot: any) => {
  1577. try {
  1578. console.log("Skip clicked, submit 0 qty for lot:", lot.lotNo);
  1579. await handleSubmitPickQtyWithQty(lot, 0);
  1580. } catch (err) {
  1581. console.error("Error in Skip:", err);
  1582. }
  1583. }, [handleSubmitPickQtyWithQty]);
  1584. const handleSubmitAllScanned = useCallback(async () => {
  1585. const scannedLots = combinedLotData.filter(lot =>
  1586. lot.stockOutLineStatus === 'checked'
  1587. );
  1588. if (scannedLots.length === 0) {
  1589. console.log("No scanned items to submit");
  1590. return;
  1591. }
  1592. setIsSubmittingAll(true);
  1593. console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`);
  1594. try {
  1595. // ✅ 转换为 batchSubmitList 所需的格式
  1596. const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
  1597. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty || 0;
  1598. const currentActualPickQty = lot.actualPickQty || 0;
  1599. const cumulativeQty = currentActualPickQty + submitQty;
  1600. let newStatus = 'partially_completed';
  1601. if (cumulativeQty >= (lot.requiredQty || 0)) {
  1602. newStatus = 'completed';
  1603. }
  1604. return {
  1605. stockOutLineId: Number(lot.stockOutLineId) || 0,
  1606. pickOrderLineId: Number(lot.pickOrderLineId),
  1607. inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
  1608. requiredQty: Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0),
  1609. actualPickQty: Number(cumulativeQty),
  1610. stockOutLineStatus: newStatus,
  1611. pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
  1612. noLot: Boolean(false) // Job Order 通常都有 lot
  1613. };
  1614. });
  1615. const request: batchSubmitListRequest = {
  1616. userId: currentUserId || 0,
  1617. lines: lines
  1618. };
  1619. // ✅ 使用 batchSubmitList API
  1620. const result = await batchSubmitList(request);
  1621. console.log(`📥 Batch submit result:`, result);
  1622. // 刷新数据
  1623. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1624. await fetchJobOrderData(pickOrderId);
  1625. if (result && result.code === "SUCCESS") {
  1626. setQrScanSuccess(true);
  1627. setTimeout(() => {
  1628. setQrScanSuccess(false);
  1629. checkAndAutoAssignNext();
  1630. if (onBackToList) {
  1631. onBackToList();
  1632. }
  1633. }, 2000);
  1634. } else {
  1635. console.error("Batch submit failed:", result);
  1636. setQrScanError(true);
  1637. }
  1638. } catch (error) {
  1639. console.error("Error submitting all scanned items:", error);
  1640. setQrScanError(true);
  1641. } finally {
  1642. setIsSubmittingAll(false);
  1643. }
  1644. }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onBackToList])
  1645. // Calculate scanned items count
  1646. const scannedItemsCount = useMemo(() => {
  1647. return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length;
  1648. }, [combinedLotData]);
  1649. // Progress bar data (align with Finished Good execution detail)
  1650. const progress = useMemo(() => {
  1651. if (combinedLotData.length === 0) {
  1652. return { completed: 0, total: 0 };
  1653. }
  1654. const nonPendingCount = combinedLotData.filter((lot) => {
  1655. const status = lot.stockOutLineStatus?.toLowerCase();
  1656. return status !== 'pending';
  1657. }).length;
  1658. return {
  1659. completed: nonPendingCount,
  1660. total: combinedLotData.length,
  1661. };
  1662. }, [combinedLotData]);
  1663. // Handle reject lot
  1664. const handleRejectLot = useCallback(async (lot: any) => {
  1665. if (!lot.stockOutLineId) {
  1666. console.error("No stock out line found for this lot");
  1667. return;
  1668. }
  1669. try {
  1670. await updateStockOutLineStatus({
  1671. id: lot.stockOutLineId,
  1672. status: 'rejected',
  1673. qty: 0
  1674. });
  1675. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1676. await fetchJobOrderData(pickOrderId);
  1677. console.log("Lot rejected successfully!");
  1678. setTimeout(() => {
  1679. checkAndAutoAssignNext();
  1680. }, 1000);
  1681. } catch (error) {
  1682. console.error("Error rejecting lot:", error);
  1683. }
  1684. }, [fetchJobOrderData, checkAndAutoAssignNext]);
  1685. // Handle pick execution form
  1686. const handlePickExecutionForm = useCallback((lot: any) => {
  1687. console.log("=== Pick Execution Form ===");
  1688. console.log("Lot data:", lot);
  1689. if (!lot) {
  1690. console.warn("No lot data provided for pick execution form");
  1691. return;
  1692. }
  1693. console.log("Opening pick execution form for lot:", lot.lotNo);
  1694. setSelectedLotForExecutionForm(lot);
  1695. setPickExecutionFormOpen(true);
  1696. console.log("Pick execution form opened for lot ID:", lot.lotId);
  1697. }, []);
  1698. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  1699. try {
  1700. console.log("Pick execution form submitted:", data);
  1701. const issueData = {
  1702. ...data,
  1703. type: "Jo", // Delivery Order Record 类型
  1704. };
  1705. const result = await recordPickExecutionIssue(issueData);
  1706. console.log("Pick execution issue recorded:", result);
  1707. if (result && result.code === "SUCCESS") {
  1708. console.log(" Pick execution issue recorded successfully");
  1709. } else {
  1710. console.error("❌ Failed to record pick execution issue:", result);
  1711. }
  1712. setPickExecutionFormOpen(false);
  1713. setSelectedLotForExecutionForm(null);
  1714. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1715. await fetchJobOrderData(pickOrderId);
  1716. } catch (error) {
  1717. console.error("Error submitting pick execution form:", error);
  1718. }
  1719. }, [fetchJobOrderData]);
  1720. // Calculate remaining required quantity
  1721. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  1722. const requiredQty = lot.requiredQty || 0;
  1723. const stockOutLineQty = lot.stockOutLineQty || 0;
  1724. return Math.max(0, requiredQty - stockOutLineQty);
  1725. }, []);
  1726. // Search criteria
  1727. const searchCriteria: Criterion<any>[] = [
  1728. {
  1729. label: t("Pick Order Code"),
  1730. paramName: "pickOrderCode",
  1731. type: "text",
  1732. },
  1733. {
  1734. label: t("Item Code"),
  1735. paramName: "itemCode",
  1736. type: "text",
  1737. },
  1738. {
  1739. label: t("Item Name"),
  1740. paramName: "itemName",
  1741. type: "text",
  1742. },
  1743. {
  1744. label: t("Lot No"),
  1745. paramName: "lotNo",
  1746. type: "text",
  1747. },
  1748. ];
  1749. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  1750. setPaginationController(prev => ({
  1751. ...prev,
  1752. pageNum: newPage,
  1753. }));
  1754. }, []);
  1755. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  1756. const newPageSize = parseInt(event.target.value, 10);
  1757. setPaginationController({
  1758. pageNum: 0,
  1759. pageSize: newPageSize,
  1760. });
  1761. }, []);
  1762. // Pagination data with sorting by routerIndex
  1763. const paginatedData = useMemo(() => {
  1764. // Sort by routerIndex first, then by other criteria
  1765. const sortedData = [...combinedLotData].sort((a, b) => {
  1766. const aIndex = a.routerIndex || 0;
  1767. const bIndex = b.routerIndex || 0;
  1768. // Primary sort: by routerIndex
  1769. if (aIndex !== bIndex) {
  1770. return aIndex - bIndex;
  1771. }
  1772. // Secondary sort: by pickOrderCode if routerIndex is the same
  1773. if (a.pickOrderCode !== b.pickOrderCode) {
  1774. return a.pickOrderCode.localeCompare(b.pickOrderCode);
  1775. }
  1776. // Tertiary sort: by lotNo if everything else is the same
  1777. return (a.lotNo || '').localeCompare(b.lotNo || '');
  1778. });
  1779. const startIndex = paginationController.pageNum * paginationController.pageSize;
  1780. const endIndex = startIndex + paginationController.pageSize;
  1781. return sortedData.slice(startIndex, endIndex);
  1782. }, [combinedLotData, paginationController]);
  1783. // Add these functions for manual scanning
  1784. const handleStartScan = useCallback(() => {
  1785. console.log(" Starting manual QR scan...");
  1786. setIsManualScanning(true);
  1787. setProcessedQrCodes(new Set());
  1788. setLastProcessedQr('');
  1789. setQrScanError(false);
  1790. setQrScanSuccess(false);
  1791. startScan();
  1792. }, [startScan]);
  1793. const handleStopScan = useCallback(() => {
  1794. console.log(" Stopping manual QR scan...");
  1795. setIsManualScanning(false);
  1796. setQrScanError(false);
  1797. setQrScanSuccess(false);
  1798. stopScan();
  1799. resetScan();
  1800. }, [stopScan, resetScan]);
  1801. useEffect(() => {
  1802. return () => {
  1803. // Cleanup when component unmounts (e.g., when switching tabs)
  1804. if (isManualScanning) {
  1805. console.log("🧹 Component unmounting, stopping QR scanner...");
  1806. stopScan();
  1807. resetScan();
  1808. }
  1809. };
  1810. }, [isManualScanning, stopScan, resetScan]);
  1811. useEffect(() => {
  1812. if (isManualScanning && combinedLotData.length === 0) {
  1813. console.log(" No data available, auto-stopping QR scan...");
  1814. handleStopScan();
  1815. }
  1816. }, [combinedLotData.length, isManualScanning, handleStopScan]);
  1817. // Cleanup effect
  1818. useEffect(() => {
  1819. return () => {
  1820. // Cleanup when component unmounts (e.g., when switching tabs)
  1821. if (isManualScanning) {
  1822. console.log("🧹 Component unmounting, stopping QR scanner...");
  1823. stopScan();
  1824. resetScan();
  1825. }
  1826. };
  1827. }, [isManualScanning, stopScan, resetScan]);
  1828. const getStatusMessage = useCallback((lot: any) => {
  1829. switch (lot.stockOutLineStatus?.toLowerCase()) {
  1830. case 'pending':
  1831. return t("Please finish QR code scan and pick order.");
  1832. case 'checked':
  1833. return t("Please submit the pick order.");
  1834. case 'partially_completed':
  1835. return t("Partial quantity submitted. Please submit more or complete the order.");
  1836. case 'completed':
  1837. return t("Pick order completed successfully!");
  1838. case 'rejected':
  1839. return t("Lot has been rejected and marked as unavailable.");
  1840. case 'unavailable':
  1841. return t("This order is insufficient, please pick another lot.");
  1842. default:
  1843. return t("Please finish QR code scan and pick order.");
  1844. }
  1845. }, [t]);
  1846. return (
  1847. <TestQrCodeProvider
  1848. lotData={combinedLotData}
  1849. onScanLot={handleQrCodeSubmit}
  1850. filterActive={(lot) => (
  1851. lot.lotAvailability !== 'rejected' &&
  1852. lot.stockOutLineStatus !== 'rejected' &&
  1853. lot.stockOutLineStatus !== 'completed'
  1854. )}
  1855. >
  1856. <FormProvider {...formProps}>
  1857. <Stack spacing={2}>
  1858. {/* Progress bar + scan status fixed at top */}
  1859. <Box
  1860. sx={{
  1861. position: 'fixed',
  1862. top: 0,
  1863. left: 0,
  1864. right: 0,
  1865. zIndex: 1100,
  1866. backgroundColor: 'background.paper',
  1867. pt: 2,
  1868. pb: 1,
  1869. px: 2,
  1870. borderBottom: '1px solid',
  1871. borderColor: 'divider',
  1872. boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
  1873. }}
  1874. >
  1875. <LinearProgressWithLabel
  1876. completed={progress.completed}
  1877. total={progress.total}
  1878. label={t("Progress")}
  1879. />
  1880. <ScanStatusAlert
  1881. error={qrScanError}
  1882. success={qrScanSuccess}
  1883. errorMessage={qrScanErrorMsg || t("QR code does not match any item in current orders.")}
  1884. successMessage={t("QR code verified.")}
  1885. />
  1886. </Box>
  1887. {/* Job Order Header */}
  1888. {jobOrderData && (
  1889. <Paper sx={{ p: 2 }}>
  1890. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  1891. <Typography variant="subtitle1">
  1892. <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.code || '-'}
  1893. </Typography>
  1894. <Typography variant="subtitle1">
  1895. <strong>{t("Pick Order Code")}:</strong> {jobOrderData.pickOrder?.code || '-'}
  1896. </Typography>
  1897. <Typography variant="subtitle1">
  1898. <strong>{t("Target Date")}:</strong> {jobOrderData.pickOrder?.targetDate || '-'}
  1899. </Typography>
  1900. </Stack>
  1901. </Paper>
  1902. )}
  1903. {/* Combined Lot Table */}
  1904. <Box sx={{ mt: 10 }}>
  1905. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
  1906. <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
  1907. {!isManualScanning ? (
  1908. <Button
  1909. variant="contained"
  1910. startIcon={<QrCodeIcon />}
  1911. onClick={handleStartScan}
  1912. color="primary"
  1913. sx={{ minWidth: '120px' }}
  1914. >
  1915. {t("Start QR Scan")}
  1916. </Button>
  1917. ) : (
  1918. <Button
  1919. variant="outlined"
  1920. startIcon={<QrCodeIcon />}
  1921. onClick={handleStopScan}
  1922. color="secondary"
  1923. sx={{ minWidth: '120px' }}
  1924. >
  1925. {t("Stop QR Scan")}
  1926. </Button>
  1927. )}
  1928. {/* ADD THIS: Submit All Scanned Button */}
  1929. <Button
  1930. variant="contained"
  1931. color="success"
  1932. onClick={handleSubmitAllScanned}
  1933. disabled={scannedItemsCount === 0 || isSubmittingAll}
  1934. sx={{ minWidth: '160px' }}
  1935. >
  1936. {isSubmittingAll ? (
  1937. <>
  1938. <CircularProgress size={16} sx={{ mr: 1 }} />
  1939. {t("Submitting...")}
  1940. </>
  1941. ) : (
  1942. `${t("Submit All Scanned")} (${scannedItemsCount})`
  1943. )}
  1944. </Button>
  1945. </Box>
  1946. </Box>
  1947. <TableContainer component={Paper}>
  1948. <Table>
  1949. <TableHead>
  1950. <TableRow>
  1951. <TableCell>{t("Index")}</TableCell>
  1952. <TableCell>{t("Route")}</TableCell>
  1953. <TableCell>{t("Handler")}</TableCell>
  1954. <TableCell>{t("Item Code")}</TableCell>
  1955. <TableCell>{t("Item Name")}</TableCell>
  1956. <TableCell>{t("Lot No")}</TableCell>
  1957. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  1958. <TableCell align="center">{t("Scan Result")}</TableCell>
  1959. <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
  1960. </TableRow>
  1961. </TableHead>
  1962. <TableBody>
  1963. {paginatedData.length === 0 ? (
  1964. <TableRow>
  1965. <TableCell colSpan={8} align="center">
  1966. <Typography variant="body2" color="text.secondary">
  1967. {t("No data available")}
  1968. </Typography>
  1969. </TableCell>
  1970. </TableRow>
  1971. ) : (
  1972. paginatedData.map((lot, index) => (
  1973. <TableRow
  1974. key={`${lot.pickOrderLineId}-${lot.lotId}`}
  1975. sx={{
  1976. // backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
  1977. //opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
  1978. '& .MuiTableCell-root': {
  1979. // color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
  1980. }
  1981. }}
  1982. >
  1983. <TableCell>
  1984. <Typography variant="body2" fontWeight="bold">
  1985. {index + 1}
  1986. </Typography>
  1987. </TableCell>
  1988. <TableCell>
  1989. <Typography variant="body2">
  1990. {lot.routerRoute || '-'}
  1991. </Typography>
  1992. </TableCell>
  1993. <TableCell>{lot.handler || '-'}</TableCell>
  1994. <TableCell>{lot.itemCode}</TableCell>
  1995. <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
  1996. <TableCell>
  1997. <Box>
  1998. <Typography
  1999. sx={{
  2000. // color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
  2001. //opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1
  2002. }}
  2003. >
  2004. {lot.lotNo}
  2005. </Typography>
  2006. </Box>
  2007. </TableCell>
  2008. <TableCell align="right">
  2009. {(() => {
  2010. const requiredQty = lot.requiredQty || 0;
  2011. return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
  2012. })()}
  2013. </TableCell>
  2014. <TableCell align="center">
  2015. {(() => {
  2016. const status = lot.stockOutLineStatus?.toLowerCase();
  2017. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  2018. const isNoLot = !lot.lotNo;
  2019. // ✅ rejected lot:显示红色勾选(已扫描但被拒绝)
  2020. if (isRejected && !isNoLot) {
  2021. return (
  2022. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2023. <Checkbox
  2024. checked={true}
  2025. disabled={true}
  2026. readOnly={true}
  2027. size="large"
  2028. sx={{
  2029. color: 'error.main',
  2030. '&.Mui-checked': { color: 'error.main' },
  2031. transform: 'scale(1.3)',
  2032. }}
  2033. />
  2034. </Box>
  2035. );
  2036. }
  2037. // ✅ 正常 lot:已扫描(checked/partially_completed/completed)
  2038. if (!isNoLot && status !== 'pending' && status !== 'rejected') {
  2039. return (
  2040. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2041. <Checkbox
  2042. checked={true}
  2043. disabled={true}
  2044. readOnly={true}
  2045. size="large"
  2046. sx={{
  2047. color: 'success.main',
  2048. '&.Mui-checked': { color: 'success.main' },
  2049. transform: 'scale(1.3)',
  2050. }}
  2051. />
  2052. </Box>
  2053. );
  2054. }
  2055. return null;
  2056. })()}
  2057. </TableCell>
  2058. <TableCell align="center">
  2059. <Box sx={{ display: 'flex', justifyContent: 'center' }}>
  2060. {(() => {
  2061. const status = lot.stockOutLineStatus?.toLowerCase();
  2062. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  2063. const isNoLot = !lot.lotNo;
  2064. // ✅ rejected lot:显示提示文本(换行显示)
  2065. if (isRejected && !isNoLot) {
  2066. return (
  2067. <Typography
  2068. variant="body2"
  2069. color="error.main"
  2070. sx={{
  2071. textAlign: 'center',
  2072. whiteSpace: 'normal',
  2073. wordBreak: 'break-word',
  2074. maxWidth: '200px',
  2075. lineHeight: 1.5
  2076. }}
  2077. >
  2078. {t("This lot is rejected, please scan another lot.")}
  2079. </Typography>
  2080. );
  2081. }
  2082. // 正常 lot:显示按钮
  2083. return (
  2084. <Stack direction="row" spacing={1} alignItems="center">
  2085. <Button
  2086. variant="contained"
  2087. onClick={() => {
  2088. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  2089. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  2090. handlePickQtyChange(lotKey, submitQty);
  2091. handleSubmitPickQtyWithQty(lot, submitQty);
  2092. updateHandledBy(lot.pickOrderId, lot.itemId);
  2093. }}
  2094. disabled={
  2095. (lot.lotAvailability === 'expired' ||
  2096. lot.lotAvailability === 'status_unavailable' ||
  2097. lot.lotAvailability === 'rejected') ||
  2098. lot.stockOutLineStatus === 'completed' ||
  2099. lot.stockOutLineStatus === 'pending'
  2100. }
  2101. sx={{
  2102. fontSize: '0.75rem',
  2103. py: 0.5,
  2104. minHeight: '28px',
  2105. minWidth: '70px'
  2106. }}
  2107. >
  2108. {t("Submit")}
  2109. </Button>
  2110. <Button
  2111. variant="outlined"
  2112. size="small"
  2113. onClick={() => handlePickExecutionForm(lot)}
  2114. disabled={
  2115. lot.stockOutLineStatus === 'completed'
  2116. }
  2117. sx={{
  2118. fontSize: '0.7rem',
  2119. py: 0.5,
  2120. minHeight: '28px',
  2121. minWidth: '60px',
  2122. borderColor: 'warning.main',
  2123. color: 'warning.main'
  2124. }}
  2125. title="Report missing or bad items"
  2126. >
  2127. {t("Edit")}
  2128. </Button>
  2129. <Button
  2130. variant="outlined"
  2131. size="small"
  2132. onClick={() => handleSubmitPickQtyWithQty(lot, lot.requiredQty || lot.pickOrderLineRequiredQty || 0)}
  2133. disabled={lot.stockOutLineStatus === 'completed'}
  2134. sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }}
  2135. >
  2136. {t("Just Complete")}
  2137. </Button>
  2138. </Stack>
  2139. );
  2140. })()}
  2141. </Box>
  2142. </TableCell>
  2143. </TableRow>
  2144. ))
  2145. )}
  2146. </TableBody>
  2147. </Table>
  2148. </TableContainer>
  2149. <TablePagination
  2150. component="div"
  2151. count={combinedLotData.length}
  2152. page={paginationController.pageNum}
  2153. rowsPerPage={paginationController.pageSize}
  2154. onPageChange={handlePageChange}
  2155. onRowsPerPageChange={handlePageSizeChange}
  2156. rowsPerPageOptions={[10, 25, 50]}
  2157. labelRowsPerPage={t("Rows per page")}
  2158. labelDisplayedRows={({ from, to, count }) =>
  2159. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  2160. }
  2161. />
  2162. </Box>
  2163. </Stack>
  2164. {/* QR Code Modal */}
  2165. {!lotConfirmationOpen && (
  2166. <QrCodeModal
  2167. open={qrModalOpen}
  2168. onClose={() => {
  2169. setQrModalOpen(false);
  2170. setSelectedLotForQr(null);
  2171. stopScan();
  2172. resetScan();
  2173. }}
  2174. lot={selectedLotForQr}
  2175. combinedLotData={combinedLotData}
  2176. onQrCodeSubmit={handleQrCodeSubmitFromModal}
  2177. />
  2178. )}
  2179. {/* Add Lot Confirmation Modal */}
  2180. {lotConfirmationOpen && expectedLotData && scannedLotData && (
  2181. <LotConfirmationModal
  2182. open={lotConfirmationOpen}
  2183. onClose={() => {
  2184. console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`);
  2185. setLotConfirmationOpen(false);
  2186. setExpectedLotData(null);
  2187. setScannedLotData(null);
  2188. setSelectedLotForQr(null);
  2189. // ✅ IMPORTANT: Clear refs and processedQrCombinations to allow reprocessing the same QR code
  2190. // This allows the modal to reopen if user cancels and scans the same QR again
  2191. setTimeout(() => {
  2192. lastProcessedQrRef.current = '';
  2193. processedQrCodesRef.current.clear();
  2194. // Clear processedQrCombinations for this itemId+stockInLineId combination
  2195. if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
  2196. setProcessedQrCombinations(prev => {
  2197. const newMap = new Map(prev);
  2198. const itemId = selectedLotForQr.itemId;
  2199. if (itemId && newMap.has(itemId)) {
  2200. newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
  2201. if (newMap.get(itemId)!.size === 0) {
  2202. newMap.delete(itemId);
  2203. }
  2204. }
  2205. return newMap;
  2206. });
  2207. }
  2208. console.log(`⏱️ [LOT CONFIRM MODAL] Cleared refs and processedQrCombinations to allow reprocessing`);
  2209. }, 100);
  2210. }}
  2211. onConfirm={handleLotConfirmation}
  2212. expectedLot={expectedLotData}
  2213. scannedLot={scannedLotData}
  2214. isLoading={isConfirmingLot}
  2215. />
  2216. )}
  2217. {/* Manual Lot Confirmation Modal (test shortcut {2fic}) */}
  2218. <ManualLotConfirmationModal
  2219. open={manualLotConfirmationOpen}
  2220. onClose={() => setManualLotConfirmationOpen(false)}
  2221. // Reuse existing handler: expectedLotInput=current lot, scannedLotInput=new lot
  2222. onConfirm={(currentLotNo, newLotNo) => {
  2223. // Use existing manual flow from handleManualLotConfirmation in other screens:
  2224. // Here we route through updateStockOutLineStatusByQRCodeAndLotNo via handleManualLotConfirmation-like inline logic.
  2225. // For now: open LotConfirmationModal path by setting expected/scanned and letting user confirm substitution.
  2226. setExpectedLotData({ lotNo: currentLotNo, itemCode: '', itemName: '' });
  2227. setScannedLotData({ lotNo: newLotNo, itemCode: '', itemName: '', inventoryLotLineId: null, stockInLineId: null });
  2228. setManualLotConfirmationOpen(false);
  2229. setLotConfirmationOpen(true);
  2230. }}
  2231. expectedLot={expectedLotData}
  2232. scannedLot={scannedLotData}
  2233. isLoading={isConfirmingLot}
  2234. />
  2235. {/* Pick Execution Form Modal */}
  2236. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  2237. <GoodPickExecutionForm
  2238. open={pickExecutionFormOpen}
  2239. onClose={() => {
  2240. setPickExecutionFormOpen(false);
  2241. setSelectedLotForExecutionForm(null);
  2242. }}
  2243. onSubmit={handlePickExecutionFormSubmit}
  2244. selectedLot={selectedLotForExecutionForm}
  2245. selectedPickOrderLine={{
  2246. id: selectedLotForExecutionForm.pickOrderLineId,
  2247. itemId: selectedLotForExecutionForm.itemId,
  2248. itemCode: selectedLotForExecutionForm.itemCode,
  2249. itemName: selectedLotForExecutionForm.itemName,
  2250. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  2251. // Add missing required properties from GetPickOrderLineInfo interface
  2252. availableQty: selectedLotForExecutionForm.availableQty || 0,
  2253. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  2254. uomDesc: selectedLotForExecutionForm.uomDesc || '',
  2255. uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
  2256. pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
  2257. suggestedList: [],
  2258. noLotLines: []
  2259. }}
  2260. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  2261. pickOrderCreateDate={new Date()}
  2262. onNormalPickSubmit={async (lot, submitQty) => {
  2263. console.log('onNormalPickSubmit called in newJobPickExecution:', { lot, submitQty });
  2264. if (!lot) {
  2265. console.error('Lot is null or undefined');
  2266. return;
  2267. }
  2268. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  2269. handlePickQtyChange(lotKey, submitQty);
  2270. await handleSubmitPickQtyWithQty(lot, submitQty);
  2271. }}
  2272. />
  2273. )}
  2274. </FormProvider>
  2275. </TestQrCodeProvider>
  2276. );
  2277. };
  2278. export default JobPickExecution