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.
 
 

1241 rivejä
44 KiB

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