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.
 
 

2615 lines
98 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. if (currentUserId && selectedLotForQr.pickOrderId && selectedLotForQr.itemId) {
  863. try {
  864. await updateHandledBy(selectedLotForQr.pickOrderId, selectedLotForQr.itemId);
  865. console.log(`✅ [LOT CONFIRM] Handler updated for itemId ${selectedLotForQr.itemId}`);
  866. } catch (error) {
  867. console.error(`❌ [LOT CONFIRM] Error updating handler (non-critical):`, error);
  868. }
  869. }
  870. console.log(`🔍 [LOT CONFIRM] Fetching lot detail for stockInLineId: ${scannedLotData.stockInLineId}`);
  871. const ld = await fetchLotDetail(scannedLotData.stockInLineId);
  872. newLotLineId = ld.inventoryLotLineId;
  873. console.log(`✅ [LOT CONFIRM] Fetched lot detail: inventoryLotLineId=${newLotLineId}`);
  874. } catch (error) {
  875. console.error("❌ [LOT CONFIRM] Error fetching lot detail (stockInLineId may not exist):", error);
  876. // If stockInLineId doesn't exist, we can still proceed with lotNo substitution
  877. // The backend confirmLotSubstitution should handle this case
  878. }
  879. }
  880. if (!newLotLineId) {
  881. console.warn("⚠️ [LOT CONFIRM] No inventory lot line id for scanned lot, proceeding with lotNo only");
  882. // Continue anyway - backend may handle lotNo substitution without inventoryLotLineId
  883. }
  884. console.log("=== [LOT CONFIRM] Lot Confirmation Debug ===");
  885. console.log("Selected Lot:", selectedLotForQr);
  886. console.log("Pick Order Line ID:", selectedLotForQr.pickOrderLineId);
  887. console.log("Stock Out Line ID:", selectedLotForQr.stockOutLineId);
  888. console.log("Suggested Pick Lot ID:", selectedLotForQr.suggestedPickLotId);
  889. console.log("Lot ID (fallback):", selectedLotForQr.lotId);
  890. console.log("New Inventory Lot Line ID:", newLotLineId);
  891. console.log("Scanned Lot No:", scannedLotData.lotNo);
  892. console.log("Scanned StockInLineId:", scannedLotData.stockInLineId);
  893. // Call confirmLotSubstitution to update the suggested lot
  894. console.log("🔄 [LOT CONFIRM] Calling confirmLotSubstitution...");
  895. const substitutionResult = await confirmLotSubstitution({
  896. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  897. stockOutLineId: selectedLotForQr.stockOutLineId,
  898. originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId,
  899. newInventoryLotNo: scannedLotData.lotNo || '',
  900. // ✅ required by LotSubstitutionConfirmRequest
  901. newStockInLineId: scannedLotData?.stockInLineId ?? null,
  902. });
  903. console.log("✅ [LOT CONFIRM] Lot substitution result:", substitutionResult);
  904. // ✅ CRITICAL: substitution failed => DO NOT mark original stockOutLine as checked.
  905. // Keep modal open so user can cancel/rescan.
  906. if (!substitutionResult || substitutionResult.code !== "SUCCESS") {
  907. console.error("❌ [LOT CONFIRM] Lot substitution failed. Will NOT update stockOutLine status.");
  908. setQrScanError(true);
  909. setQrScanSuccess(false);
  910. setQrScanErrorMsg(
  911. substitutionResult?.message ||
  912. `换批失败:stockInLineId ${scannedLotData?.stockInLineId ?? ""} 不存在或无法匹配`
  913. );
  914. return;
  915. }
  916. // Update stock out line status to 'checked' after substitution
  917. if(selectedLotForQr?.stockOutLineId){
  918. console.log(`🔄 [LOT CONFIRM] Updating stockOutLine ${selectedLotForQr.stockOutLineId} to 'checked'`);
  919. await updateStockOutLineStatus({
  920. id: selectedLotForQr.stockOutLineId,
  921. status: 'checked',
  922. qty: 0
  923. });
  924. console.log(`✅ [LOT CONFIRM] Stock out line ${selectedLotForQr.stockOutLineId} status updated to 'checked'`);
  925. }
  926. // Close modal and clean up state BEFORE refreshing
  927. setLotConfirmationOpen(false);
  928. setExpectedLotData(null);
  929. setScannedLotData(null);
  930. setSelectedLotForQr(null);
  931. // Clear QR processing state but DON'T clear processedQrCodes yet
  932. setQrScanError(false);
  933. setQrScanSuccess(true);
  934. setQrScanErrorMsg('');
  935. setQrScanInput('');
  936. // Set refreshing flag to prevent QR processing during refresh
  937. setIsRefreshingData(true);
  938. // Refresh data to show updated lot
  939. console.log("🔄 Refreshing job order data...");
  940. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  941. await fetchJobOrderData(pickOrderId);
  942. console.log(" Lot substitution confirmed and data refreshed");
  943. // Clear processed QR codes and flags immediately after refresh
  944. // This allows new QR codes to be processed right away
  945. setTimeout(() => {
  946. console.log(" Clearing processed QR codes and resuming scan");
  947. setProcessedQrCodes(new Set());
  948. setLastProcessedQr('');
  949. setQrScanSuccess(false);
  950. setIsRefreshingData(false);
  951. // ✅ Clear processedQrCombinations to allow reprocessing the same QR if needed
  952. if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
  953. setProcessedQrCombinations(prev => {
  954. const newMap = new Map(prev);
  955. const itemId = selectedLotForQr.itemId;
  956. if (itemId && newMap.has(itemId)) {
  957. newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
  958. if (newMap.get(itemId)!.size === 0) {
  959. newMap.delete(itemId);
  960. }
  961. }
  962. return newMap;
  963. });
  964. }
  965. }, 500); // Reduced from 3000ms to 500ms - just enough for UI update
  966. } catch (error) {
  967. console.error("Error confirming lot substitution:", error);
  968. setQrScanError(true);
  969. setQrScanSuccess(false);
  970. setQrScanErrorMsg('换批发生异常,请重试或联系管理员');
  971. // Clear refresh flag on error
  972. setIsRefreshingData(false);
  973. } finally {
  974. setIsConfirmingLot(false);
  975. }
  976. }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData,currentUserId, updateHandledBy ]);
  977. const processOutsideQrCode = useCallback(async (latestQr: string) => {
  978. // ✅ Only JSON QR supported for outside scanner (avoid false positive with lotNo)
  979. let qrData: any = null;
  980. try {
  981. qrData = JSON.parse(latestQr);
  982. } catch {
  983. startTransition(() => {
  984. setQrScanError(true);
  985. setQrScanSuccess(false);
  986. });
  987. return;
  988. }
  989. if (!(qrData?.stockInLineId && qrData?.itemId)) {
  990. startTransition(() => {
  991. setQrScanError(true);
  992. setQrScanSuccess(false);
  993. });
  994. return;
  995. }
  996. const scannedItemId = Number(qrData.itemId);
  997. const scannedStockInLineId = Number(qrData.stockInLineId);
  998. // ✅ avoid duplicate processing by itemId+stockInLineId
  999. const itemProcessedSet = processedQrCombinations.get(scannedItemId);
  1000. if (itemProcessedSet?.has(scannedStockInLineId)) return;
  1001. const indexes = lotDataIndexes;
  1002. const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || [];
  1003. // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected
  1004. const allLotsForItem = indexes.byItemId.get(scannedItemId) || [];
  1005. // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots
  1006. // This allows users to scan other lots even when all suggested lots are rejected
  1007. const scannedLot = allLotsForItem.find(
  1008. (lot: any) => lot.stockInLineId === scannedStockInLineId
  1009. );
  1010. if (scannedLot) {
  1011. const isRejected =
  1012. scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1013. scannedLot.lotAvailability === 'rejected' ||
  1014. scannedLot.lotAvailability === 'status_unavailable';
  1015. if (isRejected) {
  1016. console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`);
  1017. startTransition(() => {
  1018. setQrScanError(true);
  1019. setQrScanSuccess(false);
  1020. setQrScanErrorMsg(
  1021. `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
  1022. );
  1023. });
  1024. // Mark as processed to prevent re-processing
  1025. setProcessedQrCombinations(prev => {
  1026. const newMap = new Map(prev);
  1027. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1028. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1029. return newMap;
  1030. });
  1031. return;
  1032. }
  1033. }
  1034. // ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching
  1035. if (activeSuggestedLots.length === 0) {
  1036. // Check if there are any lots for this item (even if all are rejected)
  1037. if (allLotsForItem.length === 0) {
  1038. console.error("No lots found for this item");
  1039. startTransition(() => {
  1040. setQrScanError(true);
  1041. setQrScanSuccess(false);
  1042. setQrScanErrorMsg("当前订单中没有此物品的批次信息");
  1043. });
  1044. return;
  1045. }
  1046. // ✅ Allow lot switching: find a rejected lot as expected lot, or use first lot
  1047. // This allows users to switch to a new lot even when all suggested lots are rejected
  1048. console.log(`⚠️ [QR PROCESS] No active suggested lots, but allowing lot switching. Scanned lot is not rejected.`);
  1049. // Find a rejected lot as expected lot (the one that was rejected)
  1050. const rejectedLot = allLotsForItem.find((lot: any) =>
  1051. lot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1052. lot.lotAvailability === 'rejected' ||
  1053. lot.lotAvailability === 'status_unavailable'
  1054. );
  1055. const expectedLot = rejectedLot || allLotsForItem[0]; // Use rejected lot if exists, otherwise first lot
  1056. // ✅ Always open confirmation modal when no active lots (user needs to confirm switching)
  1057. // handleLotMismatch will fetch lotNo from backend using stockInLineId if needed
  1058. console.log(`⚠️ [QR PROCESS] Opening confirmation modal for lot switch (no active lots)`);
  1059. setSelectedLotForQr(expectedLot);
  1060. handleLotMismatch(
  1061. {
  1062. lotNo: expectedLot.lotNo,
  1063. itemCode: expectedLot.itemCode,
  1064. itemName: expectedLot.itemName
  1065. },
  1066. {
  1067. lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null
  1068. itemCode: expectedLot.itemCode,
  1069. itemName: expectedLot.itemName,
  1070. inventoryLotLineId: scannedLot?.lotId || null,
  1071. stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo
  1072. }
  1073. );
  1074. return;
  1075. }
  1076. // ✅ direct stockInLineId match (O(1))
  1077. const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || [];
  1078. let exactMatch: any = null;
  1079. for (let i = 0; i < stockInLineLots.length; i++) {
  1080. const lot = stockInLineLots[i];
  1081. if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) {
  1082. exactMatch = lot;
  1083. break;
  1084. }
  1085. }
  1086. console.log(`🔍 [QR PROCESS] Scanned stockInLineId: ${scannedStockInLineId}, itemId: ${scannedItemId}`);
  1087. console.log(`🔍 [QR PROCESS] Found ${stockInLineLots.length} lots with stockInLineId ${scannedStockInLineId}`);
  1088. console.log(`🔍 [QR PROCESS] Exact match found: ${exactMatch ? `YES (lotNo: ${exactMatch.lotNo}, stockOutLineId: ${exactMatch.stockOutLineId})` : 'NO'}`);
  1089. // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots
  1090. // This handles the case where Lot A is rejected and user scans Lot B
  1091. if (!exactMatch && scannedLot && !activeSuggestedLots.includes(scannedLot)) {
  1092. // Scanned lot is not in active suggested lots, open confirmation modal
  1093. const expectedLot = activeSuggestedLots[0] || allLotsForItem[0]; // Use first active lot or first lot as expected
  1094. if (expectedLot && scannedLot.stockInLineId !== expectedLot.stockInLineId) {
  1095. console.log(`⚠️ [QR PROCESS] Scanned lot ${scannedLot.lotNo} is not in active suggested lots, opening confirmation modal`);
  1096. setSelectedLotForQr(expectedLot);
  1097. handleLotMismatch(
  1098. {
  1099. lotNo: expectedLot.lotNo,
  1100. itemCode: expectedLot.itemCode,
  1101. itemName: expectedLot.itemName
  1102. },
  1103. {
  1104. lotNo: scannedLot.lotNo || null,
  1105. itemCode: expectedLot.itemCode,
  1106. itemName: expectedLot.itemName,
  1107. inventoryLotLineId: scannedLot.lotId || null,
  1108. stockInLineId: scannedStockInLineId
  1109. }
  1110. );
  1111. return;
  1112. }
  1113. }
  1114. if (exactMatch) {
  1115. if (!exactMatch.stockOutLineId) {
  1116. console.error(`❌ [QR PROCESS] Exact match found but no stockOutLineId`);
  1117. startTransition(() => {
  1118. setQrScanError(true);
  1119. setQrScanSuccess(false);
  1120. });
  1121. return;
  1122. }
  1123. console.log(`✅ [QR PROCESS] Processing exact match: lotNo=${exactMatch.lotNo}, stockOutLineId=${exactMatch.stockOutLineId}`);
  1124. try {
  1125. if (currentUserId && exactMatch.pickOrderId && exactMatch.itemId) {
  1126. try {
  1127. await updateHandledBy(exactMatch.pickOrderId, exactMatch.itemId);
  1128. console.log(`✅ [QR PROCESS] Handler updated for itemId ${exactMatch.itemId}`);
  1129. } catch (error) {
  1130. console.error(`❌ [QR PROCESS] Error updating handler (non-critical):`, error);
  1131. }
  1132. }
  1133. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1134. pickOrderLineId: exactMatch.pickOrderLineId,
  1135. inventoryLotNo: exactMatch.lotNo,
  1136. stockOutLineId: exactMatch.stockOutLineId,
  1137. itemId: exactMatch.itemId,
  1138. status: "checked",
  1139. });
  1140. if (res.code === "checked" || res.code === "SUCCESS") {
  1141. console.log(`✅ [QR PROCESS] Successfully updated stockOutLine ${exactMatch.stockOutLineId} to checked`);
  1142. const entity = res.entity as any;
  1143. startTransition(() => {
  1144. setQrScanError(false);
  1145. setQrScanSuccess(true);
  1146. });
  1147. // mark combination processed
  1148. setProcessedQrCombinations(prev => {
  1149. const newMap = new Map(prev);
  1150. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1151. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1152. return newMap;
  1153. });
  1154. // refresh to keep consistency with server & handler updates
  1155. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1156. await fetchJobOrderData(pickOrderId);
  1157. } else {
  1158. console.error(`❌ [QR PROCESS] Update failed: ${res.code}`);
  1159. startTransition(() => {
  1160. setQrScanError(true);
  1161. setQrScanSuccess(false);
  1162. });
  1163. }
  1164. } catch (error) {
  1165. console.error(`❌ [QR PROCESS] Error updating stockOutLine:`, error);
  1166. startTransition(() => {
  1167. setQrScanError(true);
  1168. setQrScanSuccess(false);
  1169. });
  1170. }
  1171. return;
  1172. }
  1173. // ✅ mismatch: validate scanned stockInLineId exists before opening confirmation modal
  1174. console.log(`⚠️ [QR PROCESS] No exact match found. Validating scanned stockInLineId ${scannedStockInLineId} for itemId ${scannedItemId}`);
  1175. console.log(`⚠️ [QR PROCESS] Active suggested lots for itemId ${scannedItemId}:`, activeSuggestedLots.map(l => ({ lotNo: l.lotNo, stockInLineId: l.stockInLineId })));
  1176. if (activeSuggestedLots.length === 0) {
  1177. console.error(`❌ [QR PROCESS] No active suggested lots found for itemId ${scannedItemId}`);
  1178. startTransition(() => {
  1179. setQrScanError(true);
  1180. setQrScanSuccess(false);
  1181. setQrScanErrorMsg(`当前订单中没有 itemId ${scannedItemId} 的可用批次`);
  1182. });
  1183. return;
  1184. }
  1185. const expectedLot = activeSuggestedLots[0];
  1186. console.log(`⚠️ [QR PROCESS] Expected lot: ${expectedLot.lotNo} (stockInLineId: ${expectedLot.stockInLineId}), Scanned stockInLineId: ${scannedStockInLineId}`);
  1187. // ✅ Validate scanned stockInLineId exists before opening modal
  1188. // This ensures the backend can find the lot when user confirms
  1189. try {
  1190. console.log(`🔍 [QR PROCESS] Validating scanned stockInLineId ${scannedStockInLineId} exists...`);
  1191. const stockInLineInfo = await fetchStockInLineInfoCached(scannedStockInLineId);
  1192. console.log(`✅ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} exists, lotNo: ${stockInLineInfo.lotNo}`);
  1193. // ✅ 检查扫描的批次是否已被拒绝
  1194. const scannedLot = combinedLotData.find(
  1195. (lot: any) => lot.stockInLineId === scannedStockInLineId && lot.itemId === scannedItemId
  1196. );
  1197. if (scannedLot) {
  1198. const isRejected =
  1199. scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1200. scannedLot.lotAvailability === 'rejected' ||
  1201. scannedLot.lotAvailability === 'status_unavailable';
  1202. if (isRejected) {
  1203. console.warn(`⚠️ [QR PROCESS] Scanned lot ${stockInLineInfo.lotNo} (stockInLineId: ${scannedStockInLineId}) is rejected or unavailable`);
  1204. startTransition(() => {
  1205. setQrScanError(true);
  1206. setQrScanSuccess(false);
  1207. setQrScanErrorMsg(
  1208. `此批次(${stockInLineInfo.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
  1209. );
  1210. });
  1211. // Mark as processed to prevent re-processing
  1212. setProcessedQrCombinations(prev => {
  1213. const newMap = new Map(prev);
  1214. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1215. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1216. return newMap;
  1217. });
  1218. return;
  1219. }
  1220. }
  1221. // ✅ stockInLineId exists and is not rejected, open confirmation modal
  1222. console.log(`⚠️ [QR PROCESS] Opening confirmation modal - user must confirm before any lot is marked as scanned`);
  1223. setSelectedLotForQr(expectedLot);
  1224. handleLotMismatch(
  1225. {
  1226. lotNo: expectedLot.lotNo,
  1227. itemCode: expectedLot.itemCode,
  1228. itemName: expectedLot.itemName
  1229. },
  1230. {
  1231. lotNo: stockInLineInfo.lotNo || null, // Use fetched lotNo for display
  1232. itemCode: expectedLot.itemCode,
  1233. itemName: expectedLot.itemName,
  1234. inventoryLotLineId: null,
  1235. stockInLineId: scannedStockInLineId
  1236. }
  1237. );
  1238. } catch (error) {
  1239. // ✅ stockInLineId does NOT exist, show error immediately (don't open modal)
  1240. console.error(`❌ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} does NOT exist:`, error);
  1241. startTransition(() => {
  1242. setQrScanError(true);
  1243. setQrScanSuccess(false);
  1244. setQrScanErrorMsg(
  1245. `扫描的 stockInLineId ${scannedStockInLineId} 不存在。请检查 QR 码是否正确,或联系管理员。`
  1246. );
  1247. });
  1248. // Mark as processed to prevent re-processing
  1249. setProcessedQrCombinations(prev => {
  1250. const newMap = new Map(prev);
  1251. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1252. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1253. return newMap;
  1254. });
  1255. }
  1256. }, [filterArgs?.pickOrderId, fetchJobOrderData, handleLotMismatch, lotDataIndexes, processedQrCombinations, combinedLotData, fetchStockInLineInfoCached,currentUserId, updateHandledBy ]);
  1257. // Store in refs for immediate access in qrValues effect
  1258. processOutsideQrCodeRef.current = processOutsideQrCode;
  1259. resetScanRef.current = resetScan;
  1260. const handleManualInputSubmit = useCallback(() => {
  1261. if (qrScanInput.trim() !== '') {
  1262. handleQrCodeSubmit(qrScanInput.trim());
  1263. }
  1264. }, [qrScanInput, handleQrCodeSubmit]);
  1265. // Handle QR code submission from modal (internal scanning)
  1266. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  1267. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  1268. console.log(` QR Code verified for lot: ${lotNo}`);
  1269. const requiredQty = selectedLotForQr.requiredQty;
  1270. const lotId = selectedLotForQr.lotId;
  1271. // Create stock out line
  1272. const stockOutLineData: CreateStockOutLine = {
  1273. consoCode: selectedLotForQr.pickOrderConsoCode,
  1274. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  1275. inventoryLotLineId: selectedLotForQr.lotId,
  1276. qty: 0.0
  1277. };
  1278. try {
  1279. await createStockOutLine(stockOutLineData);
  1280. console.log("Stock out line created successfully!");
  1281. // Close modal
  1282. setQrModalOpen(false);
  1283. setSelectedLotForQr(null);
  1284. // Set pick quantity
  1285. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  1286. setTimeout(() => {
  1287. setPickQtyData(prev => ({
  1288. ...prev,
  1289. [lotKey]: requiredQty
  1290. }));
  1291. console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  1292. }, 500);
  1293. // Refresh data
  1294. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1295. await fetchJobOrderData(pickOrderId);
  1296. } catch (error) {
  1297. console.error("Error creating stock out line:", error);
  1298. }
  1299. }
  1300. }, [selectedLotForQr, fetchJobOrderData]);
  1301. useEffect(() => {
  1302. // Skip if scanner not active or no data or currently refreshing
  1303. if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) return;
  1304. const latestQr = qrValues[qrValues.length - 1];
  1305. // ✅ Test shortcut: {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId
  1306. if ((latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) && latestQr.endsWith("}")) {
  1307. let content = '';
  1308. if (latestQr.startsWith("{2fittest")) content = latestQr.substring(9, latestQr.length - 1);
  1309. else content = latestQr.substring(8, latestQr.length - 1);
  1310. const parts = content.split(',');
  1311. if (parts.length === 2) {
  1312. const itemId = parseInt(parts[0].trim(), 10);
  1313. const stockInLineId = parseInt(parts[1].trim(), 10);
  1314. if (!isNaN(itemId) && !isNaN(stockInLineId)) {
  1315. const simulatedQr = JSON.stringify({ itemId, stockInLineId });
  1316. lastProcessedQrRef.current = latestQr;
  1317. processedQrCodesRef.current.add(latestQr);
  1318. setLastProcessedQr(latestQr);
  1319. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1320. processOutsideQrCodeRef.current?.(simulatedQr);
  1321. resetScanRef.current?.();
  1322. return;
  1323. }
  1324. }
  1325. }
  1326. // ✅ Shortcut: {2fic} open manual lot confirmation modal
  1327. if (latestQr === "{2fic}") {
  1328. setManualLotConfirmationOpen(true);
  1329. resetScanRef.current?.();
  1330. lastProcessedQrRef.current = latestQr;
  1331. processedQrCodesRef.current.add(latestQr);
  1332. setLastProcessedQr(latestQr);
  1333. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1334. return;
  1335. }
  1336. // Skip processing if modal open for same QR
  1337. if (lotConfirmationOpen || manualLotConfirmationOpen) {
  1338. if (latestQr === lastProcessedQrRef.current) return;
  1339. }
  1340. // Skip if already processed (refs)
  1341. if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) return;
  1342. // Mark processed immediately
  1343. lastProcessedQrRef.current = latestQr;
  1344. processedQrCodesRef.current.add(latestQr);
  1345. if (processedQrCodesRef.current.size > 100) {
  1346. const firstValue = processedQrCodesRef.current.values().next().value;
  1347. if (firstValue !== undefined) processedQrCodesRef.current.delete(firstValue);
  1348. }
  1349. // Process immediately
  1350. if (qrProcessingTimeoutRef.current) {
  1351. clearTimeout(qrProcessingTimeoutRef.current);
  1352. qrProcessingTimeoutRef.current = null;
  1353. }
  1354. processOutsideQrCodeRef.current?.(latestQr);
  1355. // UI state updates (non-blocking)
  1356. startTransition(() => {
  1357. setLastProcessedQr(latestQr);
  1358. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1359. });
  1360. return () => {
  1361. if (qrProcessingTimeoutRef.current) {
  1362. clearTimeout(qrProcessingTimeoutRef.current);
  1363. qrProcessingTimeoutRef.current = null;
  1364. }
  1365. };
  1366. }, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]);
  1367. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  1368. if (value === '' || value === null || value === undefined) {
  1369. setPickQtyData(prev => ({
  1370. ...prev,
  1371. [lotKey]: 0
  1372. }));
  1373. return;
  1374. }
  1375. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  1376. if (isNaN(numericValue)) {
  1377. setPickQtyData(prev => ({
  1378. ...prev,
  1379. [lotKey]: 0
  1380. }));
  1381. return;
  1382. }
  1383. setPickQtyData(prev => ({
  1384. ...prev,
  1385. [lotKey]: numericValue
  1386. }));
  1387. }, []);
  1388. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  1389. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  1390. const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
  1391. const checkAndAutoAssignNext = useCallback(async () => {
  1392. if (!currentUserId) return;
  1393. try {
  1394. const completionResponse = await checkPickOrderCompletion(currentUserId);
  1395. if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
  1396. console.log("Found completed pick orders, auto-assigning next...");
  1397. // 移除前端的自动分配逻辑,因为后端已经处理了
  1398. // await handleAutoAssignAndRelease(); // 删除这个函数
  1399. }
  1400. } catch (error) {
  1401. console.error("Error checking pick order completion:", error);
  1402. }
  1403. }, [currentUserId]);
  1404. // Handle submit pick quantity
  1405. const handleSubmitPickQty = useCallback(async (lot: any) => {
  1406. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  1407. const newQty = pickQtyData[lotKey] || 0;
  1408. if (!lot.stockOutLineId) {
  1409. console.error("No stock out line found for this lot");
  1410. return;
  1411. }
  1412. try {
  1413. const currentActualPickQty = lot.actualPickQty || 0;
  1414. const cumulativeQty = currentActualPickQty + newQty;
  1415. let newStatus = 'partially_completed';
  1416. if (cumulativeQty >= lot.requiredQty) {
  1417. newStatus = 'completed';
  1418. }
  1419. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  1420. console.log(`Lot: ${lot.lotNo}`);
  1421. console.log(`Required Qty: ${lot.requiredQty}`);
  1422. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  1423. console.log(`New Submitted Qty: ${newQty}`);
  1424. console.log(`Cumulative Qty: ${cumulativeQty}`);
  1425. console.log(`New Status: ${newStatus}`);
  1426. console.log(`=====================================`);
  1427. await updateStockOutLineStatus({
  1428. id: lot.stockOutLineId,
  1429. status: newStatus,
  1430. qty: cumulativeQty
  1431. });
  1432. if (newQty > 0) {
  1433. await updateInventoryLotLineQuantities({
  1434. inventoryLotLineId: lot.lotId,
  1435. qty: newQty,
  1436. status: 'available',
  1437. operation: 'pick'
  1438. });
  1439. }
  1440. // FIXED: Use the proper API function instead of direct fetch
  1441. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  1442. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1443. try {
  1444. // Use the imported API function instead of direct fetch
  1445. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1446. console.log(` Pick order completion check result:`, completionResponse);
  1447. if (completionResponse.code === "SUCCESS") {
  1448. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1449. } else if (completionResponse.message === "not completed") {
  1450. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1451. } else {
  1452. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1453. }
  1454. } catch (error) {
  1455. console.error("Error checking pick order completion:", error);
  1456. }
  1457. }
  1458. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1459. await fetchJobOrderData(pickOrderId);
  1460. console.log("Pick quantity submitted successfully!");
  1461. setTimeout(() => {
  1462. checkAndAutoAssignNext();
  1463. }, 1000);
  1464. } catch (error) {
  1465. console.error("Error submitting pick quantity:", error);
  1466. }
  1467. }, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]);
  1468. const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
  1469. if (!lot.stockOutLineId) {
  1470. console.error("No stock out line found for this lot");
  1471. return;
  1472. }
  1473. try {
  1474. if (currentUserId && lot.pickOrderId && lot.itemId) {
  1475. try {
  1476. await updateHandledBy(lot.pickOrderId, lot.itemId);
  1477. } catch (error) {
  1478. console.error("❌ Error updating handler (non-critical):", error);
  1479. // Continue even if handler update fails
  1480. }
  1481. }
  1482. // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0
  1483. if (submitQty === 0) {
  1484. console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
  1485. console.log(`Lot: ${lot.lotNo}`);
  1486. console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
  1487. console.log(`Setting status to 'completed' with qty: 0`);
  1488. const updateResult = await updateStockOutLineStatus({
  1489. id: lot.stockOutLineId,
  1490. status: 'completed',
  1491. qty: 0
  1492. });
  1493. console.log('Update result:', updateResult);
  1494. const r: any = updateResult as any;
  1495. const updateOk =
  1496. r?.code === 'SUCCESS' ||
  1497. r?.type === 'completed' ||
  1498. typeof r?.id === 'number' ||
  1499. typeof r?.entity?.id === 'number' ||
  1500. (r?.message && r.message.includes('successfully'));
  1501. if (!updateResult || !updateOk) {
  1502. console.error('Failed to update stock out line status:', updateResult);
  1503. throw new Error('Failed to update stock out line status');
  1504. }
  1505. // Check if pick order is completed
  1506. if (lot.pickOrderConsoCode) {
  1507. console.log(` Lot ${lot.lotNo} completed (all zeros), checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1508. try {
  1509. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1510. console.log(` Pick order completion check result:`, completionResponse);
  1511. if (completionResponse.code === "SUCCESS") {
  1512. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1513. setTimeout(() => {
  1514. if (onBackToList) {
  1515. onBackToList();
  1516. }
  1517. }, 1500);
  1518. } else if (completionResponse.message === "not completed") {
  1519. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1520. } else {
  1521. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1522. }
  1523. } catch (error) {
  1524. console.error("Error checking pick order completion:", error);
  1525. }
  1526. }
  1527. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1528. await fetchJobOrderData(pickOrderId);
  1529. console.log("All zeros submission completed successfully!");
  1530. setTimeout(() => {
  1531. checkAndAutoAssignNext();
  1532. }, 1000);
  1533. return;
  1534. }
  1535. // Normal case: Calculate cumulative quantity correctly
  1536. const currentActualPickQty = lot.actualPickQty || 0;
  1537. const cumulativeQty = currentActualPickQty + submitQty;
  1538. // Determine status based on cumulative quantity vs required quantity
  1539. let newStatus = 'partially_completed';
  1540. if (cumulativeQty >= lot.requiredQty) {
  1541. newStatus = 'completed';
  1542. } else if (cumulativeQty > 0) {
  1543. newStatus = 'partially_completed';
  1544. } else {
  1545. newStatus = 'checked'; // QR scanned but no quantity submitted yet
  1546. }
  1547. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  1548. console.log(`Lot: ${lot.lotNo}`);
  1549. console.log(`Required Qty: ${lot.requiredQty}`);
  1550. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  1551. console.log(`New Submitted Qty: ${submitQty}`);
  1552. console.log(`Cumulative Qty: ${cumulativeQty}`);
  1553. console.log(`New Status: ${newStatus}`);
  1554. console.log(`=====================================`);
  1555. await updateStockOutLineStatus({
  1556. id: lot.stockOutLineId,
  1557. status: newStatus,
  1558. qty: cumulativeQty
  1559. });
  1560. if (submitQty > 0) {
  1561. await updateInventoryLotLineQuantities({
  1562. inventoryLotLineId: lot.lotId,
  1563. qty: submitQty,
  1564. status: 'available',
  1565. operation: 'pick'
  1566. });
  1567. }
  1568. // Check if pick order is completed when lot status becomes 'completed'
  1569. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  1570. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1571. try {
  1572. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1573. console.log(` Pick order completion check result:`, completionResponse);
  1574. if (completionResponse.code === "SUCCESS") {
  1575. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1576. setTimeout(() => {
  1577. if (onBackToList) {
  1578. onBackToList();
  1579. }
  1580. }, 1500);
  1581. } else if (completionResponse.message === "not completed") {
  1582. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1583. } else {
  1584. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1585. }
  1586. } catch (error) {
  1587. console.error("Error checking pick order completion:", error);
  1588. }
  1589. }
  1590. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1591. await fetchJobOrderData(pickOrderId);
  1592. console.log("Pick quantity submitted successfully!");
  1593. setTimeout(() => {
  1594. checkAndAutoAssignNext();
  1595. }, 1000);
  1596. } catch (error) {
  1597. console.error("Error submitting pick quantity:", error);
  1598. }
  1599. }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]);
  1600. const handleSkip = useCallback(async (lot: any) => {
  1601. try {
  1602. console.log("Skip clicked, submit 0 qty for lot:", lot.lotNo);
  1603. await handleSubmitPickQtyWithQty(lot, 0);
  1604. } catch (err) {
  1605. console.error("Error in Skip:", err);
  1606. }
  1607. }, [handleSubmitPickQtyWithQty]);
  1608. const handleSubmitAllScanned = useCallback(async () => {
  1609. const scannedLots = combinedLotData.filter(lot =>
  1610. lot.stockOutLineStatus === 'checked'
  1611. );
  1612. if (scannedLots.length === 0) {
  1613. console.log("No scanned items to submit");
  1614. return;
  1615. }
  1616. setIsSubmittingAll(true);
  1617. console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`);
  1618. try {
  1619. // ✅ 批量更新所有相关行的 handler(在提交前)
  1620. if (currentUserId) {
  1621. const uniqueItemIds = new Set(scannedLots.map(lot => lot.itemId));
  1622. const updatePromises = Array.from(uniqueItemIds).map(itemId => {
  1623. const lot = scannedLots.find(l => l.itemId === itemId);
  1624. if (lot && lot.pickOrderId) {
  1625. return updateHandledBy(lot.pickOrderId, itemId).catch(err => {
  1626. console.error(`❌ Error updating handler for itemId ${itemId}:`, err);
  1627. });
  1628. }
  1629. return Promise.resolve();
  1630. });
  1631. await Promise.all(updatePromises);
  1632. console.log(`✅ Updated handlers for ${uniqueItemIds.size} unique items`);
  1633. }
  1634. // ✅ 转换为 batchSubmitList 所需的格式
  1635. const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
  1636. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty || 0;
  1637. const currentActualPickQty = lot.actualPickQty || 0;
  1638. const cumulativeQty = currentActualPickQty + submitQty;
  1639. let newStatus = 'partially_completed';
  1640. if (cumulativeQty >= (lot.requiredQty || 0)) {
  1641. newStatus = 'completed';
  1642. }
  1643. return {
  1644. stockOutLineId: Number(lot.stockOutLineId) || 0,
  1645. pickOrderLineId: Number(lot.pickOrderLineId),
  1646. inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
  1647. requiredQty: Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0),
  1648. actualPickQty: Number(cumulativeQty),
  1649. stockOutLineStatus: newStatus,
  1650. pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
  1651. noLot: Boolean(false) // Job Order 通常都有 lot
  1652. };
  1653. });
  1654. const request: batchSubmitListRequest = {
  1655. userId: currentUserId || 0,
  1656. lines: lines
  1657. };
  1658. // ✅ 使用 batchSubmitList API
  1659. const result = await batchSubmitList(request);
  1660. console.log(`📥 Batch submit result:`, result);
  1661. // 刷新数据
  1662. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1663. await fetchJobOrderData(pickOrderId);
  1664. if (result && result.code === "SUCCESS") {
  1665. setQrScanSuccess(true);
  1666. setTimeout(() => {
  1667. setQrScanSuccess(false);
  1668. checkAndAutoAssignNext();
  1669. if (onBackToList) {
  1670. onBackToList();
  1671. }
  1672. }, 2000);
  1673. } else {
  1674. console.error("Batch submit failed:", result);
  1675. setQrScanError(true);
  1676. }
  1677. } catch (error) {
  1678. console.error("Error submitting all scanned items:", error);
  1679. setQrScanError(true);
  1680. } finally {
  1681. setIsSubmittingAll(false);
  1682. }
  1683. }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onBackToList, updateHandledBy])
  1684. const scannedItemsCount = useMemo(() => {
  1685. return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length;
  1686. }, [combinedLotData]);
  1687. // Progress bar data (align with Finished Good execution detail)
  1688. const progress = useMemo(() => {
  1689. if (combinedLotData.length === 0) {
  1690. return { completed: 0, total: 0 };
  1691. }
  1692. const nonPendingCount = combinedLotData.filter((lot) => {
  1693. const status = lot.stockOutLineStatus?.toLowerCase();
  1694. return status !== 'pending';
  1695. }).length;
  1696. return {
  1697. completed: nonPendingCount,
  1698. total: combinedLotData.length,
  1699. };
  1700. }, [combinedLotData]);
  1701. // Handle reject lot
  1702. const handleRejectLot = useCallback(async (lot: any) => {
  1703. if (!lot.stockOutLineId) {
  1704. console.error("No stock out line found for this lot");
  1705. return;
  1706. }
  1707. try {
  1708. await updateStockOutLineStatus({
  1709. id: lot.stockOutLineId,
  1710. status: 'rejected',
  1711. qty: 0
  1712. });
  1713. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1714. await fetchJobOrderData(pickOrderId);
  1715. console.log("Lot rejected successfully!");
  1716. setTimeout(() => {
  1717. checkAndAutoAssignNext();
  1718. }, 1000);
  1719. } catch (error) {
  1720. console.error("Error rejecting lot:", error);
  1721. }
  1722. }, [fetchJobOrderData, checkAndAutoAssignNext]);
  1723. // Handle pick execution form
  1724. const handlePickExecutionForm = useCallback((lot: any) => {
  1725. console.log("=== Pick Execution Form ===");
  1726. console.log("Lot data:", lot);
  1727. if (!lot) {
  1728. console.warn("No lot data provided for pick execution form");
  1729. return;
  1730. }
  1731. console.log("Opening pick execution form for lot:", lot.lotNo);
  1732. setSelectedLotForExecutionForm(lot);
  1733. setPickExecutionFormOpen(true);
  1734. console.log("Pick execution form opened for lot ID:", lot.lotId);
  1735. }, []);
  1736. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  1737. try {
  1738. if (currentUserId && selectedLotForExecutionForm?.pickOrderId && selectedLotForExecutionForm?.itemId) {
  1739. try {
  1740. await updateHandledBy(selectedLotForExecutionForm.pickOrderId, selectedLotForExecutionForm.itemId);
  1741. console.log(`✅ [ISSUE FORM] Handler updated for itemId ${selectedLotForExecutionForm.itemId}`);
  1742. } catch (error) {
  1743. console.error(`❌ [ISSUE FORM] Error updating handler (non-critical):`, error);
  1744. }
  1745. }
  1746. console.log("Pick execution form submitted:", data);
  1747. const issueData = {
  1748. ...data,
  1749. type: "Jo", // Delivery Order Record 类型
  1750. };
  1751. const result = await recordPickExecutionIssue(issueData);
  1752. console.log("Pick execution issue recorded:", result);
  1753. if (result && result.code === "SUCCESS") {
  1754. console.log(" Pick execution issue recorded successfully");
  1755. } else {
  1756. console.error("❌ Failed to record pick execution issue:", result);
  1757. }
  1758. setPickExecutionFormOpen(false);
  1759. setSelectedLotForExecutionForm(null);
  1760. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1761. await fetchJobOrderData(pickOrderId);
  1762. } catch (error) {
  1763. console.error("Error submitting pick execution form:", error);
  1764. }
  1765. }, [fetchJobOrderData]);
  1766. // Calculate remaining required quantity
  1767. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  1768. const requiredQty = lot.requiredQty || 0;
  1769. const stockOutLineQty = lot.stockOutLineQty || 0;
  1770. return Math.max(0, requiredQty - stockOutLineQty);
  1771. }, []);
  1772. // Search criteria
  1773. const searchCriteria: Criterion<any>[] = [
  1774. {
  1775. label: t("Pick Order Code"),
  1776. paramName: "pickOrderCode",
  1777. type: "text",
  1778. },
  1779. {
  1780. label: t("Item Code"),
  1781. paramName: "itemCode",
  1782. type: "text",
  1783. },
  1784. {
  1785. label: t("Item Name"),
  1786. paramName: "itemName",
  1787. type: "text",
  1788. },
  1789. {
  1790. label: t("Lot No"),
  1791. paramName: "lotNo",
  1792. type: "text",
  1793. },
  1794. ];
  1795. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  1796. setPaginationController(prev => ({
  1797. ...prev,
  1798. pageNum: newPage,
  1799. }));
  1800. }, []);
  1801. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  1802. const newPageSize = parseInt(event.target.value, 10);
  1803. setPaginationController({
  1804. pageNum: 0,
  1805. pageSize: newPageSize,
  1806. });
  1807. }, []);
  1808. // Pagination data with sorting by routerIndex
  1809. const paginatedData = useMemo(() => {
  1810. // Sort by routerIndex first, then by other criteria
  1811. const sortedData = [...combinedLotData].sort((a, b) => {
  1812. const aIndex = a.routerIndex || 0;
  1813. const bIndex = b.routerIndex || 0;
  1814. // Primary sort: by routerIndex
  1815. if (aIndex !== bIndex) {
  1816. return aIndex - bIndex;
  1817. }
  1818. // Secondary sort: by pickOrderCode if routerIndex is the same
  1819. if (a.pickOrderCode !== b.pickOrderCode) {
  1820. return a.pickOrderCode.localeCompare(b.pickOrderCode);
  1821. }
  1822. // Tertiary sort: by lotNo if everything else is the same
  1823. return (a.lotNo || '').localeCompare(b.lotNo || '');
  1824. });
  1825. const startIndex = paginationController.pageNum * paginationController.pageSize;
  1826. const endIndex = startIndex + paginationController.pageSize;
  1827. return sortedData.slice(startIndex, endIndex);
  1828. }, [combinedLotData, paginationController]);
  1829. // Add these functions for manual scanning
  1830. const handleStartScan = useCallback(() => {
  1831. console.log(" Starting manual QR scan...");
  1832. setIsManualScanning(true);
  1833. setProcessedQrCodes(new Set());
  1834. setLastProcessedQr('');
  1835. setQrScanError(false);
  1836. setQrScanSuccess(false);
  1837. startScan();
  1838. }, [startScan]);
  1839. const handleStopScan = useCallback(() => {
  1840. console.log(" Stopping manual QR scan...");
  1841. setIsManualScanning(false);
  1842. setQrScanError(false);
  1843. setQrScanSuccess(false);
  1844. stopScan();
  1845. resetScan();
  1846. }, [stopScan, resetScan]);
  1847. useEffect(() => {
  1848. return () => {
  1849. // Cleanup when component unmounts (e.g., when switching tabs)
  1850. if (isManualScanning) {
  1851. console.log("🧹 Component unmounting, stopping QR scanner...");
  1852. stopScan();
  1853. resetScan();
  1854. }
  1855. };
  1856. }, [isManualScanning, stopScan, resetScan]);
  1857. useEffect(() => {
  1858. if (isManualScanning && combinedLotData.length === 0) {
  1859. console.log(" No data available, auto-stopping QR scan...");
  1860. handleStopScan();
  1861. }
  1862. }, [combinedLotData.length, isManualScanning, handleStopScan]);
  1863. // Cleanup effect
  1864. useEffect(() => {
  1865. return () => {
  1866. // Cleanup when component unmounts (e.g., when switching tabs)
  1867. if (isManualScanning) {
  1868. console.log("🧹 Component unmounting, stopping QR scanner...");
  1869. stopScan();
  1870. resetScan();
  1871. }
  1872. };
  1873. }, [isManualScanning, stopScan, resetScan]);
  1874. const getStatusMessage = useCallback((lot: any) => {
  1875. switch (lot.stockOutLineStatus?.toLowerCase()) {
  1876. case 'pending':
  1877. return t("Please finish QR code scan and pick order.");
  1878. case 'checked':
  1879. return t("Please submit the pick order.");
  1880. case 'partially_completed':
  1881. return t("Partial quantity submitted. Please submit more or complete the order.");
  1882. case 'completed':
  1883. return t("Pick order completed successfully!");
  1884. case 'rejected':
  1885. return t("Lot has been rejected and marked as unavailable.");
  1886. case 'unavailable':
  1887. return t("This order is insufficient, please pick another lot.");
  1888. default:
  1889. return t("Please finish QR code scan and pick order.");
  1890. }
  1891. }, [t]);
  1892. return (
  1893. <TestQrCodeProvider
  1894. lotData={combinedLotData}
  1895. onScanLot={handleQrCodeSubmit}
  1896. filterActive={(lot) => (
  1897. lot.lotAvailability !== 'rejected' &&
  1898. lot.stockOutLineStatus !== 'rejected' &&
  1899. lot.stockOutLineStatus !== 'completed'
  1900. )}
  1901. >
  1902. <FormProvider {...formProps}>
  1903. <Stack spacing={2}>
  1904. {/* Progress bar + scan status fixed at top */}
  1905. <Box
  1906. sx={{
  1907. position: 'fixed',
  1908. top: 0,
  1909. left: 0,
  1910. right: 0,
  1911. zIndex: 1100,
  1912. backgroundColor: 'background.paper',
  1913. pt: 2,
  1914. pb: 1,
  1915. px: 2,
  1916. borderBottom: '1px solid',
  1917. borderColor: 'divider',
  1918. boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
  1919. }}
  1920. >
  1921. <LinearProgressWithLabel
  1922. completed={progress.completed}
  1923. total={progress.total}
  1924. label={t("Progress")}
  1925. />
  1926. <ScanStatusAlert
  1927. error={qrScanError}
  1928. success={qrScanSuccess}
  1929. errorMessage={qrScanErrorMsg || t("QR code does not match any item in current orders.")}
  1930. successMessage={t("QR code verified.")}
  1931. />
  1932. </Box>
  1933. {/* Job Order Header */}
  1934. {jobOrderData && (
  1935. <Paper sx={{ p: 2 }}>
  1936. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  1937. <Typography variant="subtitle1">
  1938. <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.code || '-'}
  1939. </Typography>
  1940. <Typography variant="subtitle1">
  1941. <strong>{t("Pick Order Code")}:</strong> {jobOrderData.pickOrder?.code || '-'}
  1942. </Typography>
  1943. <Typography variant="subtitle1">
  1944. <strong>{t("Target Date")}:</strong> {jobOrderData.pickOrder?.targetDate || '-'}
  1945. </Typography>
  1946. </Stack>
  1947. </Paper>
  1948. )}
  1949. {/* Combined Lot Table */}
  1950. <Box sx={{ mt: 10 }}>
  1951. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
  1952. <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
  1953. {!isManualScanning ? (
  1954. <Button
  1955. variant="contained"
  1956. startIcon={<QrCodeIcon />}
  1957. onClick={handleStartScan}
  1958. color="primary"
  1959. sx={{ minWidth: '120px' }}
  1960. >
  1961. {t("Start QR Scan")}
  1962. </Button>
  1963. ) : (
  1964. <Button
  1965. variant="outlined"
  1966. startIcon={<QrCodeIcon />}
  1967. onClick={handleStopScan}
  1968. color="secondary"
  1969. sx={{ minWidth: '120px' }}
  1970. >
  1971. {t("Stop QR Scan")}
  1972. </Button>
  1973. )}
  1974. {/* ADD THIS: Submit All Scanned Button */}
  1975. <Button
  1976. variant="contained"
  1977. color="success"
  1978. onClick={handleSubmitAllScanned}
  1979. disabled={scannedItemsCount === 0 || isSubmittingAll}
  1980. sx={{ minWidth: '160px' }}
  1981. >
  1982. {isSubmittingAll ? (
  1983. <>
  1984. <CircularProgress size={16} sx={{ mr: 1 }} />
  1985. {t("Submitting...")}
  1986. </>
  1987. ) : (
  1988. `${t("Submit All Scanned")} (${scannedItemsCount})`
  1989. )}
  1990. </Button>
  1991. </Box>
  1992. </Box>
  1993. <TableContainer component={Paper}>
  1994. <Table>
  1995. <TableHead>
  1996. <TableRow>
  1997. <TableCell>{t("Index")}</TableCell>
  1998. <TableCell>{t("Route")}</TableCell>
  1999. <TableCell>{t("Handler")}</TableCell>
  2000. <TableCell>{t("Item Code")}</TableCell>
  2001. <TableCell>{t("Item Name")}</TableCell>
  2002. <TableCell>{t("Lot No")}</TableCell>
  2003. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  2004. <TableCell align="center">{t("Scan Result")}</TableCell>
  2005. <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
  2006. </TableRow>
  2007. </TableHead>
  2008. <TableBody>
  2009. {paginatedData.length === 0 ? (
  2010. <TableRow>
  2011. <TableCell colSpan={8} align="center">
  2012. <Typography variant="body2" color="text.secondary">
  2013. {t("No data available")}
  2014. </Typography>
  2015. </TableCell>
  2016. </TableRow>
  2017. ) : (
  2018. paginatedData.map((lot, index) => (
  2019. <TableRow
  2020. key={`${lot.pickOrderLineId}-${lot.lotId}`}
  2021. sx={{
  2022. // backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
  2023. //opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
  2024. '& .MuiTableCell-root': {
  2025. // color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
  2026. }
  2027. }}
  2028. >
  2029. <TableCell>
  2030. <Typography variant="body2" fontWeight="bold">
  2031. {index + 1}
  2032. </Typography>
  2033. </TableCell>
  2034. <TableCell>
  2035. <Typography variant="body2">
  2036. {lot.routerRoute || '-'}
  2037. </Typography>
  2038. </TableCell>
  2039. <TableCell>{lot.handler || '-'}</TableCell>
  2040. <TableCell>{lot.itemCode}</TableCell>
  2041. <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
  2042. <TableCell>
  2043. <Box>
  2044. <Typography
  2045. sx={{
  2046. // color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
  2047. //opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1
  2048. }}
  2049. >
  2050. {lot.lotNo}
  2051. </Typography>
  2052. </Box>
  2053. </TableCell>
  2054. <TableCell align="right">
  2055. {(() => {
  2056. const requiredQty = lot.requiredQty || 0;
  2057. return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
  2058. })()}
  2059. </TableCell>
  2060. <TableCell align="center">
  2061. {(() => {
  2062. const status = lot.stockOutLineStatus?.toLowerCase();
  2063. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  2064. const isNoLot = !lot.lotNo;
  2065. // ✅ rejected lot:显示红色勾选(已扫描但被拒绝)
  2066. if (isRejected && !isNoLot) {
  2067. return (
  2068. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2069. <Checkbox
  2070. checked={true}
  2071. disabled={true}
  2072. readOnly={true}
  2073. size="large"
  2074. sx={{
  2075. color: 'error.main',
  2076. '&.Mui-checked': { color: 'error.main' },
  2077. transform: 'scale(1.3)',
  2078. }}
  2079. />
  2080. </Box>
  2081. );
  2082. }
  2083. // ✅ 正常 lot:已扫描(checked/partially_completed/completed)
  2084. if (!isNoLot && status !== 'pending' && status !== 'rejected') {
  2085. return (
  2086. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2087. <Checkbox
  2088. checked={true}
  2089. disabled={true}
  2090. readOnly={true}
  2091. size="large"
  2092. sx={{
  2093. color: 'success.main',
  2094. '&.Mui-checked': { color: 'success.main' },
  2095. transform: 'scale(1.3)',
  2096. }}
  2097. />
  2098. </Box>
  2099. );
  2100. }
  2101. return null;
  2102. })()}
  2103. </TableCell>
  2104. <TableCell align="center">
  2105. <Box sx={{ display: 'flex', justifyContent: 'center' }}>
  2106. {(() => {
  2107. const status = lot.stockOutLineStatus?.toLowerCase();
  2108. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  2109. const isNoLot = !lot.lotNo;
  2110. // ✅ rejected lot:显示提示文本(换行显示)
  2111. if (isRejected && !isNoLot) {
  2112. return (
  2113. <Typography
  2114. variant="body2"
  2115. color="error.main"
  2116. sx={{
  2117. textAlign: 'center',
  2118. whiteSpace: 'normal',
  2119. wordBreak: 'break-word',
  2120. maxWidth: '200px',
  2121. lineHeight: 1.5
  2122. }}
  2123. >
  2124. {t("This lot is rejected, please scan another lot.")}
  2125. </Typography>
  2126. );
  2127. }
  2128. // 正常 lot:显示按钮
  2129. return (
  2130. <Stack direction="row" spacing={1} alignItems="center">
  2131. <Button
  2132. variant="contained"
  2133. onClick={async () => {
  2134. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  2135. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  2136. handlePickQtyChange(lotKey, submitQty);
  2137. await handleSubmitPickQtyWithQty(lot, submitQty);
  2138. }}
  2139. disabled={
  2140. (lot.lotAvailability === 'expired' ||
  2141. lot.lotAvailability === 'status_unavailable' ||
  2142. lot.lotAvailability === 'rejected') ||
  2143. lot.stockOutLineStatus === 'completed' ||
  2144. lot.stockOutLineStatus === 'pending'
  2145. }
  2146. sx={{
  2147. fontSize: '0.75rem',
  2148. py: 0.5,
  2149. minHeight: '28px',
  2150. minWidth: '70px'
  2151. }}
  2152. >
  2153. {t("Submit")}
  2154. </Button>
  2155. <Button
  2156. variant="outlined"
  2157. size="small"
  2158. onClick={() => handlePickExecutionForm(lot)}
  2159. disabled={
  2160. lot.stockOutLineStatus === 'completed'
  2161. }
  2162. sx={{
  2163. fontSize: '0.7rem',
  2164. py: 0.5,
  2165. minHeight: '28px',
  2166. minWidth: '60px',
  2167. borderColor: 'warning.main',
  2168. color: 'warning.main'
  2169. }}
  2170. title="Report missing or bad items"
  2171. >
  2172. {t("Edit")}
  2173. </Button>
  2174. <Button
  2175. variant="outlined"
  2176. size="small"
  2177. onClick={async () => {
  2178. // ✅ 更新 handler 后再提交
  2179. if (currentUserId && lot.pickOrderId && lot.itemId) {
  2180. try {
  2181. await updateHandledBy(lot.pickOrderId, lot.itemId);
  2182. } catch (error) {
  2183. console.error("❌ Error updating handler (non-critical):", error);
  2184. }
  2185. }
  2186. await handleSubmitPickQtyWithQty(lot, lot.requiredQty || lot.pickOrderLineRequiredQty || 0);
  2187. }}
  2188. disabled={lot.stockOutLineStatus === 'completed'}
  2189. sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }}
  2190. >
  2191. {t("Just Complete")}
  2192. </Button>
  2193. </Stack>
  2194. );
  2195. })()}
  2196. </Box>
  2197. </TableCell>
  2198. </TableRow>
  2199. ))
  2200. )}
  2201. </TableBody>
  2202. </Table>
  2203. </TableContainer>
  2204. <TablePagination
  2205. component="div"
  2206. count={combinedLotData.length}
  2207. page={paginationController.pageNum}
  2208. rowsPerPage={paginationController.pageSize}
  2209. onPageChange={handlePageChange}
  2210. onRowsPerPageChange={handlePageSizeChange}
  2211. rowsPerPageOptions={[10, 25, 50]}
  2212. labelRowsPerPage={t("Rows per page")}
  2213. labelDisplayedRows={({ from, to, count }) =>
  2214. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  2215. }
  2216. />
  2217. </Box>
  2218. </Stack>
  2219. {/* QR Code Modal */}
  2220. {!lotConfirmationOpen && (
  2221. <QrCodeModal
  2222. open={qrModalOpen}
  2223. onClose={() => {
  2224. setQrModalOpen(false);
  2225. setSelectedLotForQr(null);
  2226. stopScan();
  2227. resetScan();
  2228. }}
  2229. lot={selectedLotForQr}
  2230. combinedLotData={combinedLotData}
  2231. onQrCodeSubmit={handleQrCodeSubmitFromModal}
  2232. />
  2233. )}
  2234. {/* Add Lot Confirmation Modal */}
  2235. {lotConfirmationOpen && expectedLotData && scannedLotData && (
  2236. <LotConfirmationModal
  2237. open={lotConfirmationOpen}
  2238. onClose={() => {
  2239. console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`);
  2240. setLotConfirmationOpen(false);
  2241. setExpectedLotData(null);
  2242. setScannedLotData(null);
  2243. setSelectedLotForQr(null);
  2244. // ✅ IMPORTANT: Clear refs and processedQrCombinations to allow reprocessing the same QR code
  2245. // This allows the modal to reopen if user cancels and scans the same QR again
  2246. setTimeout(() => {
  2247. lastProcessedQrRef.current = '';
  2248. processedQrCodesRef.current.clear();
  2249. // Clear processedQrCombinations for this itemId+stockInLineId combination
  2250. if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
  2251. setProcessedQrCombinations(prev => {
  2252. const newMap = new Map(prev);
  2253. const itemId = selectedLotForQr.itemId;
  2254. if (itemId && newMap.has(itemId)) {
  2255. newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
  2256. if (newMap.get(itemId)!.size === 0) {
  2257. newMap.delete(itemId);
  2258. }
  2259. }
  2260. return newMap;
  2261. });
  2262. }
  2263. console.log(`⏱️ [LOT CONFIRM MODAL] Cleared refs and processedQrCombinations to allow reprocessing`);
  2264. }, 100);
  2265. }}
  2266. onConfirm={handleLotConfirmation}
  2267. expectedLot={expectedLotData}
  2268. scannedLot={scannedLotData}
  2269. isLoading={isConfirmingLot}
  2270. />
  2271. )}
  2272. {/* Manual Lot Confirmation Modal (test shortcut {2fic}) */}
  2273. <ManualLotConfirmationModal
  2274. open={manualLotConfirmationOpen}
  2275. onClose={() => setManualLotConfirmationOpen(false)}
  2276. // Reuse existing handler: expectedLotInput=current lot, scannedLotInput=new lot
  2277. onConfirm={(currentLotNo, newLotNo) => {
  2278. // Use existing manual flow from handleManualLotConfirmation in other screens:
  2279. // Here we route through updateStockOutLineStatusByQRCodeAndLotNo via handleManualLotConfirmation-like inline logic.
  2280. // For now: open LotConfirmationModal path by setting expected/scanned and letting user confirm substitution.
  2281. setExpectedLotData({ lotNo: currentLotNo, itemCode: '', itemName: '' });
  2282. setScannedLotData({ lotNo: newLotNo, itemCode: '', itemName: '', inventoryLotLineId: null, stockInLineId: null });
  2283. setManualLotConfirmationOpen(false);
  2284. setLotConfirmationOpen(true);
  2285. }}
  2286. expectedLot={expectedLotData}
  2287. scannedLot={scannedLotData}
  2288. isLoading={isConfirmingLot}
  2289. />
  2290. {/* Pick Execution Form Modal */}
  2291. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  2292. <GoodPickExecutionForm
  2293. open={pickExecutionFormOpen}
  2294. onClose={() => {
  2295. setPickExecutionFormOpen(false);
  2296. setSelectedLotForExecutionForm(null);
  2297. }}
  2298. onSubmit={handlePickExecutionFormSubmit}
  2299. selectedLot={selectedLotForExecutionForm}
  2300. selectedPickOrderLine={{
  2301. id: selectedLotForExecutionForm.pickOrderLineId,
  2302. itemId: selectedLotForExecutionForm.itemId,
  2303. itemCode: selectedLotForExecutionForm.itemCode,
  2304. itemName: selectedLotForExecutionForm.itemName,
  2305. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  2306. // Add missing required properties from GetPickOrderLineInfo interface
  2307. availableQty: selectedLotForExecutionForm.availableQty || 0,
  2308. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  2309. uomDesc: selectedLotForExecutionForm.uomDesc || '',
  2310. uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
  2311. pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
  2312. suggestedList: [],
  2313. noLotLines: []
  2314. }}
  2315. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  2316. pickOrderCreateDate={new Date()}
  2317. onNormalPickSubmit={async (lot, submitQty) => {
  2318. console.log('onNormalPickSubmit called in newJobPickExecution:', { lot, submitQty });
  2319. if (!lot) {
  2320. console.error('Lot is null or undefined');
  2321. return;
  2322. }
  2323. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  2324. handlePickQtyChange(lotKey, submitQty);
  2325. await handleSubmitPickQtyWithQty(lot, submitQty);
  2326. }}
  2327. />
  2328. )}
  2329. </FormProvider>
  2330. </TestQrCodeProvider>
  2331. );
  2332. };
  2333. export default JobPickExecution