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.
 
 

1295 lines
45 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 { useCallback, useEffect, useState, useRef, useMemo } from "react";
  22. import { useTranslation } from "react-i18next";
  23. import { useRouter } from "next/navigation";
  24. // ✅ 修改:使用 Job Order API
  25. import {
  26. fetchCompletedJobOrderPickOrders,
  27. fetchUnassignedJobOrderPickOrders,
  28. assignJobOrderPickOrder,
  29. updateSecondQrScanStatus,
  30. submitSecondScanQuantity,
  31. recordSecondScanIssue
  32. } from "@/app/api/jo/actions";
  33. import { fetchNameList, NameList } from "@/app/api/user/actions";
  34. import {
  35. FormProvider,
  36. useForm,
  37. } from "react-hook-form";
  38. import SearchBox, { Criterion } from "../SearchBox";
  39. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  40. import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  41. import QrCodeIcon from '@mui/icons-material/QrCode';
  42. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  43. import { useSession } from "next-auth/react";
  44. import { SessionWithTokens } from "@/config/authConfig";
  45. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  46. import GoodPickExecutionForm from "./JobPickExecutionForm";
  47. import FGPickOrderCard from "./FGPickOrderCard";
  48. interface Props {
  49. filterArgs: Record<string, any>;
  50. }
  51. // ✅ QR Code Modal Component (from GoodPickExecution)
  52. const QrCodeModal: React.FC<{
  53. open: boolean;
  54. onClose: () => void;
  55. lot: any | null;
  56. onQrCodeSubmit: (lotNo: string) => void;
  57. combinedLotData: any[];
  58. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
  59. const { t } = useTranslation("jo");
  60. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  61. const [manualInput, setManualInput] = useState<string>('');
  62. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  63. const [manualInputError, setManualInputError] = useState<boolean>(false);
  64. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  65. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  66. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  67. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  68. const [scannedQrResult, setScannedQrResult] = useState<string>('');
  69. // Process scanned QR codes
  70. useEffect(() => {
  71. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  72. const latestQr = qrValues[qrValues.length - 1];
  73. if (processedQrCodes.has(latestQr)) {
  74. console.log("QR code already processed, skipping...");
  75. return;
  76. }
  77. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  78. try {
  79. const qrData = JSON.parse(latestQr);
  80. if (qrData.stockInLineId && qrData.itemId) {
  81. setIsProcessingQr(true);
  82. setQrScanFailed(false);
  83. fetchStockInLineInfo(qrData.stockInLineId)
  84. .then((stockInLineInfo) => {
  85. console.log("Stock in line info:", stockInLineInfo);
  86. setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
  87. if (stockInLineInfo.lotNo === lot.lotNo) {
  88. console.log(`✅ QR Code verified for lot: ${lot.lotNo}`);
  89. setQrScanSuccess(true);
  90. onQrCodeSubmit(lot.lotNo);
  91. onClose();
  92. resetScan();
  93. } else {
  94. console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
  95. setQrScanFailed(true);
  96. setManualInputError(true);
  97. setManualInputSubmitted(true);
  98. }
  99. })
  100. .catch((error) => {
  101. console.error("Error fetching stock in line info:", error);
  102. setScannedQrResult('Error fetching data');
  103. setQrScanFailed(true);
  104. setManualInputError(true);
  105. setManualInputSubmitted(true);
  106. })
  107. .finally(() => {
  108. setIsProcessingQr(false);
  109. });
  110. } else {
  111. const qrContent = latestQr.replace(/[{}]/g, '');
  112. setScannedQrResult(qrContent);
  113. if (qrContent === lot.lotNo) {
  114. setQrScanSuccess(true);
  115. onQrCodeSubmit(lot.lotNo);
  116. onClose();
  117. resetScan();
  118. } else {
  119. setQrScanFailed(true);
  120. setManualInputError(true);
  121. setManualInputSubmitted(true);
  122. }
  123. }
  124. } catch (error) {
  125. console.log("QR code is not JSON format, trying direct comparison");
  126. const qrContent = latestQr.replace(/[{}]/g, '');
  127. setScannedQrResult(qrContent);
  128. if (qrContent === lot.lotNo) {
  129. setQrScanSuccess(true);
  130. onQrCodeSubmit(lot.lotNo);
  131. onClose();
  132. resetScan();
  133. } else {
  134. setQrScanFailed(true);
  135. setManualInputError(true);
  136. setManualInputSubmitted(true);
  137. }
  138. }
  139. }
  140. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]);
  141. // Clear states when modal opens
  142. useEffect(() => {
  143. if (open) {
  144. setManualInput('');
  145. setManualInputSubmitted(false);
  146. setManualInputError(false);
  147. setIsProcessingQr(false);
  148. setQrScanFailed(false);
  149. setQrScanSuccess(false);
  150. setScannedQrResult('');
  151. setProcessedQrCodes(new Set());
  152. }
  153. }, [open]);
  154. useEffect(() => {
  155. if (lot) {
  156. setManualInput('');
  157. setManualInputSubmitted(false);
  158. setManualInputError(false);
  159. setIsProcessingQr(false);
  160. setQrScanFailed(false);
  161. setQrScanSuccess(false);
  162. setScannedQrResult('');
  163. setProcessedQrCodes(new Set());
  164. }
  165. }, [lot]);
  166. // Auto-submit manual input when it matches
  167. useEffect(() => {
  168. if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
  169. console.log(' Auto-submitting manual input:', manualInput.trim());
  170. const timer = setTimeout(() => {
  171. setQrScanSuccess(true);
  172. onQrCodeSubmit(lot.lotNo);
  173. onClose();
  174. setManualInput('');
  175. setManualInputError(false);
  176. setManualInputSubmitted(false);
  177. }, 200);
  178. return () => clearTimeout(timer);
  179. }
  180. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  181. const handleManualSubmit = () => {
  182. if (manualInput.trim() === lot?.lotNo) {
  183. setQrScanSuccess(true);
  184. onQrCodeSubmit(lot.lotNo);
  185. onClose();
  186. setManualInput('');
  187. } else {
  188. setQrScanFailed(true);
  189. setManualInputError(true);
  190. setManualInputSubmitted(true);
  191. }
  192. };
  193. useEffect(() => {
  194. if (open) {
  195. startScan();
  196. }
  197. }, [open, startScan]);
  198. return (
  199. <Modal open={open} onClose={onClose}>
  200. <Box sx={{
  201. position: 'absolute',
  202. top: '50%',
  203. left: '50%',
  204. transform: 'translate(-50%, -50%)',
  205. bgcolor: 'background.paper',
  206. p: 3,
  207. borderRadius: 2,
  208. minWidth: 400,
  209. }}>
  210. <Typography variant="h6" gutterBottom>
  211. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  212. </Typography>
  213. {isProcessingQr && (
  214. <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
  215. <Typography variant="body2" color="primary">
  216. {t("Processing QR code...")}
  217. </Typography>
  218. </Box>
  219. )}
  220. <Box sx={{ mb: 2 }}>
  221. <Typography variant="body2" gutterBottom>
  222. <strong>{t("Manual Input")}:</strong>
  223. </Typography>
  224. <TextField
  225. fullWidth
  226. size="small"
  227. value={manualInput}
  228. onChange={(e) => {
  229. setManualInput(e.target.value);
  230. if (qrScanFailed || manualInputError) {
  231. setQrScanFailed(false);
  232. setManualInputError(false);
  233. setManualInputSubmitted(false);
  234. }
  235. }}
  236. sx={{ mb: 1 }}
  237. error={manualInputSubmitted && manualInputError}
  238. helperText={
  239. manualInputSubmitted && manualInputError
  240. ? `${t("The input is not the same as the expected lot number.")}`
  241. : ''
  242. }
  243. />
  244. <Button
  245. variant="contained"
  246. onClick={handleManualSubmit}
  247. disabled={!manualInput.trim()}
  248. size="small"
  249. color="primary"
  250. >
  251. {t("Submit")}
  252. </Button>
  253. </Box>
  254. {qrValues.length > 0 && (
  255. <Box sx={{
  256. mb: 2,
  257. p: 2,
  258. backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
  259. borderRadius: 1
  260. }}>
  261. <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
  262. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  263. </Typography>
  264. {qrScanSuccess && (
  265. <Typography variant="caption" color="success" display="block">
  266. ✅ {t("Verified successfully!")}
  267. </Typography>
  268. )}
  269. </Box>
  270. )}
  271. <Box sx={{ mt: 2, textAlign: 'right' }}>
  272. <Button onClick={onClose} variant="outlined">
  273. {t("Cancel")}
  274. </Button>
  275. </Box>
  276. </Box>
  277. </Modal>
  278. );
  279. };
  280. const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
  281. const { t } = useTranslation("jo");
  282. const router = useRouter();
  283. const { data: session } = useSession() as { data: SessionWithTokens | null };
  284. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  285. // ✅ 修改:使用 Job Order 数据结构
  286. const [jobOrderData, setJobOrderData] = useState<any>(null);
  287. const [combinedLotData, setCombinedLotData] = useState<any[]>([]);
  288. const [combinedDataLoading, setCombinedDataLoading] = useState(false);
  289. const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
  290. // ✅ 添加未分配订单状态
  291. const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
  292. const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
  293. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  294. const [qrScanInput, setQrScanInput] = useState<string>('');
  295. const [qrScanError, setQrScanError] = useState<boolean>(false);
  296. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  297. const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
  298. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  299. const [paginationController, setPaginationController] = useState({
  300. pageNum: 0,
  301. pageSize: 10,
  302. });
  303. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  304. const initializationRef = useRef(false);
  305. const autoAssignRef = useRef(false);
  306. const formProps = useForm();
  307. const errors = formProps.formState.errors;
  308. // ✅ Add QR modal states
  309. const [qrModalOpen, setQrModalOpen] = useState(false);
  310. const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
  311. // ✅ Add GoodPickExecutionForm states
  312. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  313. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
  314. // ✅ Add these missing state variables
  315. const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
  316. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  317. const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
  318. const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
  319. // ✅ 修改:加载未分配的 Job Order 订单
  320. const loadUnassignedOrders = useCallback(async () => {
  321. setIsLoadingUnassigned(true);
  322. try {
  323. const orders = await fetchUnassignedJobOrderPickOrders();
  324. setUnassignedOrders(orders);
  325. } catch (error) {
  326. console.error("Error loading unassigned orders:", error);
  327. } finally {
  328. setIsLoadingUnassigned(false);
  329. }
  330. }, []);
  331. // ✅ 修改:分配订单给当前用户
  332. const handleAssignOrder = useCallback(async (pickOrderId: number) => {
  333. if (!currentUserId) {
  334. console.error("Missing user id in session");
  335. return;
  336. }
  337. try {
  338. const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
  339. if (result.message === "Successfully assigned") {
  340. console.log("✅ Successfully assigned pick order");
  341. // 刷新数据
  342. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  343. // 重新加载未分配订单列表
  344. loadUnassignedOrders();
  345. } else {
  346. console.warn("⚠️ Assignment failed:", result.message);
  347. alert(`Assignment failed: ${result.message}`);
  348. }
  349. } catch (error) {
  350. console.error("❌ Error assigning order:", error);
  351. alert("Error occurred during assignment");
  352. }
  353. }, [currentUserId, loadUnassignedOrders]);
  354. // ✅ Handle QR code button click
  355. const handleQrCodeClick = (pickOrderId: number) => {
  356. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  357. // TODO: Implement QR code functionality
  358. };
  359. // ✅ 修改:使用 Job Order API 获取数据
  360. const fetchJobOrderData = useCallback(async (userId?: number) => {
  361. setCombinedDataLoading(true);
  362. try {
  363. const userIdToUse = userId || currentUserId;
  364. console.log(" fetchJobOrderData called with userId:", userIdToUse);
  365. if (!userIdToUse) {
  366. console.warn("⚠️ No userId available, skipping API call");
  367. setJobOrderData(null);
  368. setCombinedLotData([]);
  369. setOriginalCombinedData([]);
  370. return;
  371. }
  372. // ✅ 使用 Job Order API
  373. const jobOrderData = await fetchCompletedJobOrderPickOrders(userIdToUse);
  374. console.log("✅ Job Order data:", jobOrderData);
  375. setJobOrderData(jobOrderData);
  376. // ✅ Transform hierarchical data to flat structure for the table
  377. const flatLotData: any[] = [];
  378. if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) {
  379. jobOrderData.pickOrderLines.forEach((line: any) => {
  380. if (line.lots && line.lots.length > 0) {
  381. line.lots.forEach((lot: any) => {
  382. flatLotData.push({
  383. // Pick order info
  384. pickOrderId: jobOrderData.pickOrder.id,
  385. pickOrderCode: jobOrderData.pickOrder.code,
  386. pickOrderConsoCode: jobOrderData.pickOrder.consoCode,
  387. pickOrderTargetDate: jobOrderData.pickOrder.targetDate,
  388. pickOrderType: jobOrderData.pickOrder.type,
  389. pickOrderStatus: jobOrderData.pickOrder.status,
  390. pickOrderAssignTo: jobOrderData.pickOrder.assignTo,
  391. // Pick order line info
  392. pickOrderLineId: line.id,
  393. pickOrderLineRequiredQty: line.requiredQty,
  394. pickOrderLineStatus: line.status,
  395. // Item info
  396. itemId: line.itemId,
  397. itemCode: line.itemCode,
  398. itemName: line.itemName,
  399. uomCode: line.uomCode,
  400. uomDesc: line.uomDesc,
  401. // Lot info
  402. lotId: lot.lotId,
  403. lotNo: lot.lotNo,
  404. expiryDate: lot.expiryDate,
  405. location: lot.location,
  406. availableQty: lot.availableQty,
  407. requiredQty: lot.requiredQty,
  408. actualPickQty: lot.actualPickQty,
  409. lotStatus: lot.lotStatus,
  410. lotAvailability: lot.lotAvailability,
  411. processingStatus: lot.processingStatus,
  412. stockOutLineId: lot.stockOutLineId,
  413. stockOutLineStatus: lot.stockOutLineStatus,
  414. stockOutLineQty: lot.stockOutLineQty,
  415. // Router info
  416. routerIndex: lot.routerIndex,
  417. secondQrScanStatus: lot.secondQrScanStatus,
  418. routerArea: lot.routerArea,
  419. routerRoute: lot.routerRoute,
  420. uomShortDesc: lot.uomShortDesc
  421. });
  422. });
  423. }
  424. });
  425. }
  426. console.log("✅ Transformed flat lot data:", flatLotData);
  427. setCombinedLotData(flatLotData);
  428. setOriginalCombinedData(flatLotData);
  429. // ✅ 计算完成状态并发送事件
  430. const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) =>
  431. lot.processingStatus === 'completed'
  432. );
  433. // ✅ 发送完成状态事件,包含标签页信息
  434. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  435. detail: {
  436. allLotsCompleted: allCompleted,
  437. tabIndex: 0 // ✅ 明确指定这是来自标签页 0 的事件
  438. }
  439. }));
  440. } catch (error) {
  441. console.error("❌ Error fetching job order data:", error);
  442. setJobOrderData(null);
  443. setCombinedLotData([]);
  444. setOriginalCombinedData([]);
  445. // ✅ 如果加载失败,禁用打印按钮
  446. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  447. detail: {
  448. allLotsCompleted: false,
  449. tabIndex: 0
  450. }
  451. }));
  452. } finally {
  453. setCombinedDataLoading(false);
  454. }
  455. }, [currentUserId]);
  456. // ✅ 修改:初始化时加载数据
  457. useEffect(() => {
  458. if (session && currentUserId && !initializationRef.current) {
  459. console.log("✅ Session loaded, initializing job order...");
  460. initializationRef.current = true;
  461. // 加载 Job Order 数据
  462. fetchJobOrderData();
  463. // 加载未分配订单
  464. loadUnassignedOrders();
  465. }
  466. }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]);
  467. // ✅ Add event listener for manual assignment
  468. useEffect(() => {
  469. const handlePickOrderAssigned = () => {
  470. console.log("🔄 Pick order assigned event received, refreshing data...");
  471. fetchJobOrderData();
  472. };
  473. window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
  474. return () => {
  475. window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
  476. };
  477. }, [fetchJobOrderData]);
  478. // ✅ Handle QR code submission for matched lot (external scanning)
  479. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  480. console.log(`✅ Processing Second QR Code for lot: ${lotNo}`);
  481. // ✅ Check if this lot was already processed recently
  482. const lotKey = `${lotNo}_${Date.now()}`;
  483. if (processedQrCodes.has(lotNo)) {
  484. console.log(`⏭️ Lot ${lotNo} already processed, skipping...`);
  485. return;
  486. }
  487. const currentLotData = combinedLotData;
  488. const matchingLots = currentLotData.filter(lot =>
  489. lot.lotNo === lotNo ||
  490. lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
  491. );
  492. if (matchingLots.length === 0) {
  493. console.error(`❌ Lot not found: ${lotNo}`);
  494. setQrScanError(true);
  495. setQrScanSuccess(false);
  496. return;
  497. }
  498. try {
  499. let successCount = 0;
  500. for (const matchingLot of matchingLots) {
  501. // ✅ Check if this specific item was already processed
  502. const itemKey = `${matchingLot.pickOrderId}_${matchingLot.itemId}`;
  503. if (processedQrCodes.has(itemKey)) {
  504. console.log(`⏭️ Item ${matchingLot.itemId} already processed, skipping...`);
  505. continue;
  506. }
  507. // ✅ Use the new second scan API
  508. const result = await updateSecondQrScanStatus(
  509. matchingLot.pickOrderId,
  510. matchingLot.itemId
  511. );
  512. if (result.code === "SUCCESS") {
  513. successCount++;
  514. // ✅ Mark this item as processed
  515. setProcessedQrCodes(prev => new Set(prev).add(itemKey));
  516. console.log(`✅ Second QR scan status updated for item ${matchingLot.itemId}`);
  517. } else {
  518. console.error(`❌ Failed to update second QR scan status: ${result.message}`);
  519. }
  520. }
  521. if (successCount > 0) {
  522. setQrScanSuccess(true);
  523. setQrScanError(false);
  524. await fetchJobOrderData(); // Refresh data
  525. } else {
  526. setQrScanError(true);
  527. setQrScanSuccess(false);
  528. }
  529. } catch (error) {
  530. console.error("❌ Error processing second QR code:", error);
  531. setQrScanError(true);
  532. setQrScanSuccess(false);
  533. }
  534. }, [combinedLotData, fetchJobOrderData, processedQrCodes]);
  535. useEffect(() => {
  536. if (qrValues.length > 0 && combinedLotData.length > 0) {
  537. const latestQr = qrValues[qrValues.length - 1];
  538. // ✅ Check if this QR was already processed recently
  539. if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) {
  540. console.log("⏭️ QR code already processed, skipping...");
  541. return;
  542. }
  543. // ✅ Mark as processed
  544. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  545. setLastProcessedQr(latestQr);
  546. // Extract lot number from QR code
  547. let lotNo = '';
  548. try {
  549. const qrData = JSON.parse(latestQr);
  550. if (qrData.stockInLineId && qrData.itemId) {
  551. // For JSON QR codes, we need to fetch the lot number
  552. fetchStockInLineInfo(qrData.stockInLineId)
  553. .then((stockInLineInfo) => {
  554. console.log("Outside QR scan - Stock in line info:", stockInLineInfo);
  555. const extractedLotNo = stockInLineInfo.lotNo;
  556. if (extractedLotNo) {
  557. console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`);
  558. handleQrCodeSubmit(extractedLotNo);
  559. }
  560. })
  561. .catch((error) => {
  562. console.error("Outside QR scan - Error fetching stock in line info:", error);
  563. });
  564. return; // Exit early for JSON QR codes
  565. }
  566. } catch (error) {
  567. // Not JSON format, treat as direct lot number
  568. lotNo = latestQr.replace(/[{}]/g, '');
  569. }
  570. // For direct lot number QR codes
  571. if (lotNo) {
  572. console.log(`Outside QR scan detected (direct): ${lotNo}`);
  573. handleQrCodeSubmit(lotNo);
  574. }
  575. }
  576. }, [qrValues, combinedLotData, handleQrCodeSubmit, processedQrCodes, lastProcessedQr]);
  577. const handleManualInputSubmit = useCallback(() => {
  578. if (qrScanInput.trim() !== '') {
  579. handleQrCodeSubmit(qrScanInput.trim());
  580. }
  581. }, [qrScanInput, handleQrCodeSubmit]);
  582. // ✅ Handle QR code submission from modal (internal scanning)
  583. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  584. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  585. console.log(`✅ QR Code verified for lot: ${lotNo}`);
  586. const requiredQty = selectedLotForQr.requiredQty;
  587. const lotId = selectedLotForQr.lotId;
  588. // Create stock out line
  589. const stockOutLineData: CreateStockOutLine = {
  590. consoCode: selectedLotForQr.pickOrderConsoCode,
  591. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  592. inventoryLotLineId: selectedLotForQr.lotId,
  593. qty: 0.0
  594. };
  595. try {
  596. // Close modal
  597. setQrModalOpen(false);
  598. setSelectedLotForQr(null);
  599. // Set pick quantity
  600. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  601. setTimeout(() => {
  602. setPickQtyData(prev => ({
  603. ...prev,
  604. [lotKey]: requiredQty
  605. }));
  606. console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  607. }, 500);
  608. // Refresh data
  609. await fetchJobOrderData();
  610. } catch (error) {
  611. console.error("Error creating stock out line:", error);
  612. }
  613. }
  614. }, [selectedLotForQr, fetchJobOrderData]);
  615. // ✅ Outside QR scanning - process QR codes from outside the page automatically
  616. useEffect(() => {
  617. if (qrValues.length > 0 && combinedLotData.length > 0) {
  618. const latestQr = qrValues[qrValues.length - 1];
  619. // Extract lot number from QR code
  620. let lotNo = '';
  621. try {
  622. const qrData = JSON.parse(latestQr);
  623. if (qrData.stockInLineId && qrData.itemId) {
  624. // For JSON QR codes, we need to fetch the lot number
  625. fetchStockInLineInfo(qrData.stockInLineId)
  626. .then((stockInLineInfo) => {
  627. console.log("Outside QR scan - Stock in line info:", stockInLineInfo);
  628. const extractedLotNo = stockInLineInfo.lotNo;
  629. if (extractedLotNo) {
  630. console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`);
  631. handleQrCodeSubmit(extractedLotNo);
  632. }
  633. })
  634. .catch((error) => {
  635. console.error("Outside QR scan - Error fetching stock in line info:", error);
  636. });
  637. return; // Exit early for JSON QR codes
  638. }
  639. } catch (error) {
  640. // Not JSON format, treat as direct lot number
  641. lotNo = latestQr.replace(/[{}]/g, '');
  642. }
  643. // For direct lot number QR codes
  644. if (lotNo) {
  645. console.log(`Outside QR scan detected (direct): ${lotNo}`);
  646. handleQrCodeSubmit(lotNo);
  647. }
  648. }
  649. }, [qrValues, combinedLotData, handleQrCodeSubmit]);
  650. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  651. if (value === '' || value === null || value === undefined) {
  652. setPickQtyData(prev => ({
  653. ...prev,
  654. [lotKey]: 0
  655. }));
  656. return;
  657. }
  658. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  659. if (isNaN(numericValue)) {
  660. setPickQtyData(prev => ({
  661. ...prev,
  662. [lotKey]: 0
  663. }));
  664. return;
  665. }
  666. setPickQtyData(prev => ({
  667. ...prev,
  668. [lotKey]: numericValue
  669. }));
  670. }, []);
  671. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  672. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  673. const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
  674. try {
  675. // ✅ Use the new second scan submit API
  676. const result = await submitSecondScanQuantity(
  677. lot.pickOrderId,
  678. lot.itemId,
  679. {
  680. qty: submitQty,
  681. isMissing: false,
  682. isBad: false,
  683. reason: undefined // ✅ Fix TypeScript error
  684. }
  685. );
  686. if (result.code === "SUCCESS") {
  687. console.log(`✅ Second scan quantity submitted: ${submitQty}`);
  688. await fetchJobOrderData(); // Refresh data
  689. } else {
  690. console.error(`❌ Failed to submit second scan quantity: ${result.message}`);
  691. }
  692. } catch (error) {
  693. console.error("Error submitting second scan quantity:", error);
  694. }
  695. }, [fetchJobOrderData]);
  696. // ✅ Handle reject lot
  697. // ✅ Handle pick execution form
  698. const handlePickExecutionForm = useCallback((lot: any) => {
  699. console.log("=== Pick Execution Form ===");
  700. console.log("Lot data:", lot);
  701. if (!lot) {
  702. console.warn("No lot data provided for pick execution form");
  703. return;
  704. }
  705. console.log("Opening pick execution form for lot:", lot.lotNo);
  706. setSelectedLotForExecutionForm(lot);
  707. setPickExecutionFormOpen(true);
  708. console.log("Pick execution form opened for lot ID:", lot.lotId);
  709. }, []);
  710. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  711. try {
  712. console.log("Pick execution form submitted:", data);
  713. if (!currentUserId) {
  714. console.error("❌ No current user ID available");
  715. return;
  716. }
  717. const result = await recordSecondScanIssue(
  718. selectedLotForExecutionForm.pickOrderId,
  719. selectedLotForExecutionForm.itemId,
  720. {
  721. qty: data.actualPickQty,
  722. isMissing: data.missQty > 0,
  723. isBad: data.badItemQty > 0,
  724. reason: data.issueRemark || '',
  725. createdBy: currentUserId
  726. }
  727. );
  728. console.log("Pick execution issue recorded:", result);
  729. if (result && result.code === "SUCCESS") {
  730. console.log("✅ Pick execution issue recorded successfully");
  731. } else {
  732. console.error("❌ Failed to record pick execution issue:", result);
  733. }
  734. setPickExecutionFormOpen(false);
  735. setSelectedLotForExecutionForm(null);
  736. await fetchJobOrderData();
  737. } catch (error) {
  738. console.error("Error submitting pick execution form:", error);
  739. }
  740. }, [currentUserId, selectedLotForExecutionForm, fetchJobOrderData,]);
  741. // ✅ Calculate remaining required quantity
  742. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  743. const requiredQty = lot.requiredQty || 0;
  744. const stockOutLineQty = lot.stockOutLineQty || 0;
  745. return Math.max(0, requiredQty - stockOutLineQty);
  746. }, []);
  747. // Search criteria
  748. const searchCriteria: Criterion<any>[] = [
  749. {
  750. label: t("Pick Order Code"),
  751. paramName: "pickOrderCode",
  752. type: "text",
  753. },
  754. {
  755. label: t("Item Code"),
  756. paramName: "itemCode",
  757. type: "text",
  758. },
  759. {
  760. label: t("Item Name"),
  761. paramName: "itemName",
  762. type: "text",
  763. },
  764. {
  765. label: t("Lot No"),
  766. paramName: "lotNo",
  767. type: "text",
  768. },
  769. ];
  770. const handleSearch = useCallback((query: Record<string, any>) => {
  771. setSearchQuery({ ...query });
  772. console.log("Search query:", query);
  773. if (!originalCombinedData) return;
  774. const filtered = originalCombinedData.filter((lot: any) => {
  775. const pickOrderCodeMatch = !query.pickOrderCode ||
  776. lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  777. const itemCodeMatch = !query.itemCode ||
  778. lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
  779. const itemNameMatch = !query.itemName ||
  780. lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
  781. const lotNoMatch = !query.lotNo ||
  782. lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
  783. return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
  784. });
  785. setCombinedLotData(filtered);
  786. console.log("Filtered lots count:", filtered.length);
  787. }, [originalCombinedData]);
  788. const handleReset = useCallback(() => {
  789. setSearchQuery({});
  790. if (originalCombinedData) {
  791. setCombinedLotData(originalCombinedData);
  792. }
  793. }, [originalCombinedData]);
  794. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  795. setPaginationController(prev => ({
  796. ...prev,
  797. pageNum: newPage,
  798. }));
  799. }, []);
  800. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  801. const newPageSize = parseInt(event.target.value, 10);
  802. setPaginationController({
  803. pageNum: 0,
  804. pageSize: newPageSize,
  805. });
  806. }, []);
  807. // Pagination data with sorting by routerIndex
  808. const paginatedData = useMemo(() => {
  809. // ✅ Sort by routerIndex first, then by other criteria
  810. const sortedData = [...combinedLotData].sort((a, b) => {
  811. const aIndex = a.routerIndex || 0;
  812. const bIndex = b.routerIndex || 0;
  813. // Primary sort: by routerIndex
  814. if (aIndex !== bIndex) {
  815. return aIndex - bIndex;
  816. }
  817. // Secondary sort: by pickOrderCode if routerIndex is the same
  818. if (a.pickOrderCode !== b.pickOrderCode) {
  819. return a.pickOrderCode.localeCompare(b.pickOrderCode);
  820. }
  821. // Tertiary sort: by lotNo if everything else is the same
  822. return (a.lotNo || '').localeCompare(b.lotNo || '');
  823. });
  824. const startIndex = paginationController.pageNum * paginationController.pageSize;
  825. const endIndex = startIndex + paginationController.pageSize;
  826. return sortedData.slice(startIndex, endIndex);
  827. }, [combinedLotData, paginationController]);
  828. // ✅ Add these functions for manual scanning
  829. const handleStartScan = useCallback(() => {
  830. console.log(" Starting manual QR scan...");
  831. setIsManualScanning(true);
  832. setProcessedQrCodes(new Set());
  833. setLastProcessedQr('');
  834. setQrScanError(false);
  835. setQrScanSuccess(false);
  836. startScan();
  837. }, [startScan]);
  838. const handleStopScan = useCallback(() => {
  839. console.log("⏹️ Stopping manual QR scan...");
  840. setIsManualScanning(false);
  841. setQrScanError(false);
  842. setQrScanSuccess(false);
  843. stopScan();
  844. resetScan();
  845. }, [stopScan, resetScan]);
  846. const getStatusMessage = useCallback((lot: any) => {
  847. switch (lot.stockOutLineStatus?.toLowerCase()) {
  848. case 'pending':
  849. return t("Please finish QR code scan and pick order.");
  850. case 'checked':
  851. return t("Please submit the pick order.");
  852. case 'partially_completed':
  853. return t("Partial quantity submitted. Please submit more or complete the order.");
  854. case 'completed':
  855. return t("Pick order completed successfully!");
  856. case 'rejected':
  857. return t("Lot has been rejected and marked as unavailable.");
  858. case 'unavailable':
  859. return t("This order is insufficient, please pick another lot.");
  860. default:
  861. return t("Please finish QR code scan and pick order.");
  862. }
  863. }, [t]);
  864. return (
  865. <FormProvider {...formProps}>
  866. <Stack spacing={2}>
  867. {/* Job Order Header */}
  868. {jobOrderData && (
  869. <Paper sx={{ p: 2 }}>
  870. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  871. <Typography variant="subtitle1">
  872. <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.name || '-'}
  873. </Typography>
  874. <Typography variant="subtitle1">
  875. <strong>{t("Pick Order Code")}:</strong> {jobOrderData.pickOrder?.code || '-'}
  876. </Typography>
  877. <Typography variant="subtitle1">
  878. <strong>{t("Target Date")}:</strong> {jobOrderData.pickOrder?.targetDate || '-'}
  879. </Typography>
  880. <Typography variant="subtitle1">
  881. <strong>{t("Status")}:</strong> {jobOrderData.pickOrder?.status || '-'}
  882. </Typography>
  883. </Stack>
  884. </Paper>
  885. )}
  886. {/* Combined Lot Table */}
  887. <Box>
  888. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
  889. <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
  890. {!isManualScanning ? (
  891. <Button
  892. variant="contained"
  893. startIcon={<QrCodeIcon />}
  894. onClick={handleStartScan}
  895. color="primary"
  896. sx={{ minWidth: '120px' }}
  897. >
  898. {t("Start QR Scan")}
  899. </Button>
  900. ) : (
  901. <Button
  902. variant="outlined"
  903. startIcon={<QrCodeIcon />}
  904. onClick={handleStopScan}
  905. color="secondary"
  906. sx={{ minWidth: '120px' }}
  907. >
  908. {t("Stop QR Scan")}
  909. </Button>
  910. )}
  911. {isManualScanning && (
  912. <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
  913. <CircularProgress size={16} />
  914. <Typography variant="caption" color="primary">
  915. {t("Scanning...")}
  916. </Typography>
  917. </Box>
  918. )}
  919. </Box>
  920. </Box>
  921. {qrScanError && !qrScanSuccess && (
  922. <Alert severity="error" sx={{ mb: 2 }}>
  923. {t("QR code does not match any item in current orders.")}
  924. </Alert>
  925. )}
  926. {qrScanSuccess && (
  927. <Alert severity="success" sx={{ mb: 2 }}>
  928. {t("QR code verified.")}
  929. </Alert>
  930. )}
  931. <TableContainer component={Paper}>
  932. <Table>
  933. <TableHead>
  934. <TableRow>
  935. <TableCell>{t("Index")}</TableCell>
  936. <TableCell>{t("Route")}</TableCell>
  937. <TableCell>{t("Item Code")}</TableCell>
  938. <TableCell>{t("Item Name")}</TableCell>
  939. <TableCell>{t("Lot No")}</TableCell>
  940. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  941. <TableCell align="center">{t("Scan Result")}</TableCell>
  942. <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
  943. </TableRow>
  944. </TableHead>
  945. <TableBody>
  946. {paginatedData.length === 0 ? (
  947. <TableRow>
  948. <TableCell colSpan={8} align="center">
  949. <Typography variant="body2" color="text.secondary">
  950. {t("No data available")}
  951. </Typography>
  952. </TableCell>
  953. </TableRow>
  954. ) : (
  955. paginatedData.map((lot, index) => (
  956. <TableRow
  957. key={`${lot.pickOrderLineId}-${lot.lotId}`}
  958. sx={{
  959. backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
  960. opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
  961. '& .MuiTableCell-root': {
  962. color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
  963. }
  964. }}
  965. >
  966. <TableCell>
  967. <Typography variant="body2" fontWeight="bold">
  968. {index + 1}
  969. </Typography>
  970. </TableCell>
  971. <TableCell>
  972. <Typography variant="body2">
  973. {lot.routerRoute || '-'}
  974. </Typography>
  975. </TableCell>
  976. <TableCell>{lot.itemCode}</TableCell>
  977. <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
  978. <TableCell>
  979. <Box>
  980. <Typography
  981. sx={{
  982. color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
  983. opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1
  984. }}
  985. >
  986. {lot.lotNo}
  987. </Typography>
  988. </Box>
  989. </TableCell>
  990. <TableCell align="right">
  991. {(() => {
  992. const requiredQty = lot.requiredQty || 0;
  993. return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
  994. })()}
  995. </TableCell>
  996. <TableCell align="center">
  997. {lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? (
  998. <Box sx={{
  999. display: 'flex',
  1000. justifyContent: 'center',
  1001. alignItems: 'center',
  1002. width: '100%',
  1003. height: '100%'
  1004. }}>
  1005. <Checkbox
  1006. checked={lot.secondQrScanStatus?.toLowerCase() !== 'pending'}
  1007. disabled={true}
  1008. readOnly={true}
  1009. size="large"
  1010. sx={{
  1011. color: lot.secondQrScanStatus?.toLowerCase() !== 'pending' ? 'success.main' : 'grey.400',
  1012. '&.Mui-checked': {
  1013. color: 'success.main',
  1014. },
  1015. transform: 'scale(1.3)',
  1016. '& .MuiSvgIcon-root': {
  1017. fontSize: '1.5rem',
  1018. }
  1019. }}
  1020. />
  1021. </Box>
  1022. ) : null}
  1023. </TableCell>
  1024. <TableCell align="center">
  1025. <Box sx={{ display: 'flex', justifyContent: 'center' }}>
  1026. <Stack direction="row" spacing={1} alignItems="center">
  1027. <Button
  1028. variant="contained"
  1029. onClick={() => {
  1030. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  1031. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  1032. // Submit with default lot required pick qty
  1033. handlePickQtyChange(lotKey, submitQty);
  1034. handleSubmitPickQtyWithQty(lot, submitQty);
  1035. }}
  1036. disabled={
  1037. (lot.secondQrScanStatus === 'expired' ||
  1038. lot.secondQrScanStatus === 'status_unavailable' ||
  1039. lot.secondQrScanStatus === 'rejected') ||
  1040. lot.secondQrScanStatus === 'completed' ||
  1041. lot.secondQrScanStatus === 'pending' // ✅ Disable when QR scan not passed
  1042. }
  1043. sx={{
  1044. fontSize: '0.75rem',
  1045. py: 0.5,
  1046. minHeight: '28px',
  1047. minWidth: '70px'
  1048. }}
  1049. >
  1050. {t("Submit")}
  1051. </Button>
  1052. <Button
  1053. variant="outlined"
  1054. size="small"
  1055. onClick={() => handlePickExecutionForm(lot)}
  1056. disabled={
  1057. (lot.lotAvailability === 'expired' ||
  1058. lot.lotAvailability === 'status_unavailable' ||
  1059. lot.lotAvailability === 'rejected') ||
  1060. lot.secondQrScanStatus === 'completed' || // ✅ Disable when finished
  1061. lot.secondQrScanStatus === 'pending' // ✅ Disable when QR scan not passed
  1062. }
  1063. sx={{
  1064. fontSize: '0.7rem',
  1065. py: 0.5,
  1066. minHeight: '28px',
  1067. minWidth: '60px',
  1068. borderColor: 'warning.main',
  1069. color: 'warning.main'
  1070. }}
  1071. title="Report missing or bad items"
  1072. >
  1073. {t("Issue")}
  1074. </Button>
  1075. </Stack>
  1076. </Box>
  1077. </TableCell>
  1078. </TableRow>
  1079. ))
  1080. )}
  1081. </TableBody>
  1082. </Table>
  1083. </TableContainer>
  1084. <TablePagination
  1085. component="div"
  1086. count={combinedLotData.length}
  1087. page={paginationController.pageNum}
  1088. rowsPerPage={paginationController.pageSize}
  1089. onPageChange={handlePageChange}
  1090. onRowsPerPageChange={handlePageSizeChange}
  1091. rowsPerPageOptions={[10, 25, 50]}
  1092. labelRowsPerPage={t("Rows per page")}
  1093. labelDisplayedRows={({ from, to, count }) =>
  1094. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  1095. }
  1096. />
  1097. </Box>
  1098. </Stack>
  1099. {/* ✅ QR Code Modal */}
  1100. <QrCodeModal
  1101. open={qrModalOpen}
  1102. onClose={() => {
  1103. setQrModalOpen(false);
  1104. setSelectedLotForQr(null);
  1105. stopScan();
  1106. resetScan();
  1107. }}
  1108. lot={selectedLotForQr}
  1109. combinedLotData={combinedLotData}
  1110. onQrCodeSubmit={handleQrCodeSubmitFromModal}
  1111. />
  1112. {/* ✅ Pick Execution Form Modal */}
  1113. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  1114. <GoodPickExecutionForm
  1115. open={pickExecutionFormOpen}
  1116. onClose={() => {
  1117. setPickExecutionFormOpen(false);
  1118. setSelectedLotForExecutionForm(null);
  1119. }}
  1120. onSubmit={handlePickExecutionFormSubmit}
  1121. selectedLot={selectedLotForExecutionForm}
  1122. selectedPickOrderLine={{
  1123. id: selectedLotForExecutionForm.pickOrderLineId,
  1124. itemId: selectedLotForExecutionForm.itemId,
  1125. itemCode: selectedLotForExecutionForm.itemCode,
  1126. itemName: selectedLotForExecutionForm.itemName,
  1127. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  1128. // ✅ Add missing required properties from GetPickOrderLineInfo interface
  1129. availableQty: selectedLotForExecutionForm.availableQty || 0,
  1130. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  1131. uomCode: selectedLotForExecutionForm.uomCode || '',
  1132. uomDesc: selectedLotForExecutionForm.uomDesc || '',
  1133. pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // ✅ Use pickedQty instead of actualPickQty
  1134. suggestedList: [] // ✅ Add required suggestedList property
  1135. }}
  1136. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  1137. pickOrderCreateDate={new Date()}
  1138. />
  1139. )}
  1140. </FormProvider>
  1141. );
  1142. };
  1143. export default JobPickExecution