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.
 
 

934 lines
34 KiB

  1. "use client";
  2. import {
  3. Box,
  4. Button,
  5. Checkbox,
  6. Paper,
  7. Stack,
  8. Table,
  9. TableBody,
  10. TableCell,
  11. TableContainer,
  12. TableHead,
  13. TableRow,
  14. TextField,
  15. Typography,
  16. TablePagination,
  17. Modal,
  18. } from "@mui/material";
  19. import { useCallback, useMemo, useState, useEffect } from "react";
  20. import { useTranslation } from "react-i18next";
  21. import QrCodeIcon from '@mui/icons-material/QrCode';
  22. import { GetPickOrderLineInfo, recordPickExecutionIssue } from "@/app/api/pickOrder/actions";
  23. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  24. import { updateInventoryLotLineStatus } from "@/app/api/inventory/actions";
  25. import { updateStockOutLineStatus } from "@/app/api/pickOrder/actions";
  26. import { fetchStockInLineInfo } from "@/app/api/po/actions"; // Add this import
  27. import PickExecutionForm from "./PickExecutionForm";
  28. interface LotPickData {
  29. id: number;
  30. lotId: number ;
  31. lotNo: string ;
  32. expiryDate: string;
  33. location: string| null;
  34. stockUnit: string;
  35. inQty: number;
  36. availableQty: number;
  37. requiredQty: number;
  38. actualPickQty: number;
  39. lotStatus: string;
  40. outQty: number;
  41. holdQty: number;
  42. totalPickedByAllPickOrders: number;
  43. lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable' | 'rejected'; // 添加 'rejected'
  44. stockOutLineId?: number;
  45. stockOutLineStatus?: string;
  46. stockOutLineQty?: number;
  47. noLot?: boolean;
  48. }
  49. interface PickQtyData {
  50. [lineId: number]: {
  51. [lotId: number]: number;
  52. };
  53. }
  54. interface LotTableProps {
  55. lotData: LotPickData[];
  56. selectedRowId: number | null;
  57. selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string; pickOrderId: number }) | null; // 添加 pickOrderId
  58. pickQtyData: PickQtyData;
  59. selectedLotRowId: string | null;
  60. selectedLotId: number | null;
  61. onLotSelection: (uniqueLotId: string, lotId: number) => void;
  62. onPickQtyChange: (lineId: number, lotId: number, value: number) => void;
  63. onSubmitPickQty: (lineId: number, lotId: number) => void;
  64. onCreateStockOutLine: (inventoryLotLineId: number) => void;
  65. onQcCheck: (line: GetPickOrderLineInfo, pickOrderCode: string) => void;
  66. onLotSelectForInput: (lot: LotPickData) => void;
  67. showInputBody: boolean;
  68. totalPickedByAllPickOrders: number;
  69. outQty: number;
  70. holdQty: number;
  71. setShowInputBody: (show: boolean) => void;
  72. selectedLotForInput: LotPickData | null;
  73. generateInputBody: () => any;
  74. onDataRefresh: () => Promise<void>;
  75. onLotDataRefresh: () => Promise<void>;
  76. }
  77. // QR Code Modal Component
  78. const QrCodeModal: React.FC<{
  79. open: boolean;
  80. onClose: () => void;
  81. lot: LotPickData | null;
  82. onQrCodeSubmit: (lotNo: string) => void;
  83. }> = ({ open, onClose, lot, onQrCodeSubmit }) => {
  84. const { t } = useTranslation("pickOrder");
  85. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  86. const [manualInput, setManualInput] = useState<string>('');
  87. const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({});
  88. // Add state to track manual input submission
  89. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  90. const [manualInputError, setManualInputError] = useState<boolean>(false);
  91. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  92. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  93. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  94. // Add state to track processed QR codes to prevent re-processing
  95. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  96. // Add state to store the scanned QR result
  97. const [scannedQrResult, setScannedQrResult] = useState<string>('');
  98. // Process scanned QR codes with new format
  99. useEffect(() => {
  100. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  101. const latestQr = qrValues[qrValues.length - 1];
  102. // Check if this QR code has already been processed
  103. if (processedQrCodes.has(latestQr)) {
  104. console.log("QR code already processed, skipping...");
  105. return;
  106. }
  107. // Add to processed set immediately to prevent re-processing
  108. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  109. try {
  110. // Parse QR code as JSON
  111. const qrData = JSON.parse(latestQr);
  112. // Check if it has the expected structure
  113. if (qrData.stockInLineId && qrData.itemId) {
  114. setIsProcessingQr(true);
  115. setQrScanFailed(false);
  116. // Fetch stock in line info to get lotNo
  117. fetchStockInLineInfo(qrData.stockInLineId)
  118. .then((stockInLineInfo) => {
  119. console.log("Stock in line info:", stockInLineInfo);
  120. // Store the scanned result for display
  121. setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
  122. // Compare lotNo from API with expected lotNo
  123. if (stockInLineInfo.lotNo === lot.lotNo) {
  124. console.log(` QR Code verified for lot: ${lot.lotNo}`);
  125. setQrScanSuccess(true);
  126. onQrCodeSubmit(lot.lotNo);
  127. onClose();
  128. resetScan();
  129. } else {
  130. console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
  131. setQrScanFailed(true);
  132. setManualInputError(true);
  133. setManualInputSubmitted(true);
  134. // DON'T stop scanning - allow new QR codes to be processed
  135. }
  136. })
  137. .catch((error) => {
  138. console.error("Error fetching stock in line info:", error);
  139. setScannedQrResult('Error fetching data');
  140. setQrScanFailed(true);
  141. setManualInputError(true);
  142. setManualInputSubmitted(true);
  143. // DON'T stop scanning - allow new QR codes to be processed
  144. })
  145. .finally(() => {
  146. setIsProcessingQr(false);
  147. });
  148. } else {
  149. // Fallback to old format (direct lotNo comparison)
  150. const qrContent = latestQr.replace(/[{}]/g, '');
  151. // Store the scanned result for display
  152. setScannedQrResult(qrContent);
  153. if (qrContent === lot.lotNo) {
  154. setQrScanSuccess(true);
  155. onQrCodeSubmit(lot.lotNo);
  156. onClose();
  157. resetScan();
  158. } else {
  159. setQrScanFailed(true);
  160. setManualInputError(true);
  161. setManualInputSubmitted(true);
  162. // DON'T stop scanning - allow new QR codes to be processed
  163. }
  164. }
  165. } catch (error) {
  166. // If JSON parsing fails, fallback to old format
  167. console.log("QR code is not JSON format, trying direct comparison");
  168. const qrContent = latestQr.replace(/[{}]/g, '');
  169. // Store the scanned result for display
  170. setScannedQrResult(qrContent);
  171. if (qrContent === lot.lotNo) {
  172. setQrScanSuccess(true);
  173. onQrCodeSubmit(lot.lotNo);
  174. onClose();
  175. resetScan();
  176. } else {
  177. setQrScanFailed(true);
  178. setManualInputError(true);
  179. setManualInputSubmitted(true);
  180. // DON'T stop scanning - allow new QR codes to be processed
  181. }
  182. }
  183. }
  184. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes, stopScan]);
  185. // Clear states when modal opens or lot changes
  186. useEffect(() => {
  187. if (open) {
  188. setManualInput('');
  189. setManualInputSubmitted(false);
  190. setManualInputError(false);
  191. setIsProcessingQr(false);
  192. setQrScanFailed(false);
  193. setQrScanSuccess(false);
  194. setScannedQrResult(''); // Clear scanned result
  195. // Clear processed QR codes when modal opens
  196. setProcessedQrCodes(new Set());
  197. }
  198. }, [open]);
  199. useEffect(() => {
  200. if (lot) {
  201. setManualInput('');
  202. setManualInputSubmitted(false);
  203. setManualInputError(false);
  204. setIsProcessingQr(false);
  205. setQrScanFailed(false);
  206. setQrScanSuccess(false);
  207. setScannedQrResult(''); // Clear scanned result
  208. // Clear processed QR codes when lot changes
  209. setProcessedQrCodes(new Set());
  210. }
  211. }, [lot]);
  212. // Auto-submit manual input when it matches (but only if QR scan hasn't failed)
  213. useEffect(() => {
  214. if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
  215. console.log('🔄 Auto-submitting manual input:', manualInput.trim());
  216. const timer = setTimeout(() => {
  217. setQrScanSuccess(true);
  218. onQrCodeSubmit(lot.lotNo);
  219. onClose();
  220. setManualInput('');
  221. setManualInputError(false);
  222. setManualInputSubmitted(false);
  223. }, 200);
  224. return () => clearTimeout(timer);
  225. }
  226. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  227. // Add the missing handleManualSubmit function
  228. const handleManualSubmit = () => {
  229. if (manualInput.trim() === lot?.lotNo) {
  230. setQrScanSuccess(true);
  231. onQrCodeSubmit(lot.lotNo);
  232. onClose();
  233. setManualInput('');
  234. } else {
  235. setQrScanFailed(true);
  236. setManualInputError(true);
  237. setManualInputSubmitted(true);
  238. }
  239. };
  240. // Add function to restart scanning after manual input error
  241. const handleRestartScan = () => {
  242. setQrScanFailed(false);
  243. setManualInputError(false);
  244. setManualInputSubmitted(false);
  245. setProcessedQrCodes(new Set()); // Clear processed QR codes
  246. startScan(); // Restart scanning
  247. };
  248. useEffect(() => {
  249. if (open) {
  250. startScan();
  251. }
  252. }, [open, startScan]);
  253. return (
  254. <Modal open={open} onClose={onClose}>
  255. <Box sx={{
  256. position: 'absolute',
  257. top: '50%',
  258. left: '50%',
  259. transform: 'translate(-50%, -50%)',
  260. bgcolor: 'background.paper',
  261. p: 3,
  262. borderRadius: 2,
  263. minWidth: 400,
  264. }}>
  265. <Typography variant="h6" gutterBottom>
  266. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  267. </Typography>
  268. {/* Show processing status */}
  269. {isProcessingQr && (
  270. <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
  271. <Typography variant="body2" color="primary">
  272. {t("Processing QR code...")}
  273. </Typography>
  274. </Box>
  275. )}
  276. {/* Manual Input with Submit-Triggered Helper Text */}
  277. {true &&(
  278. <Box sx={{ mb: 2 }}>
  279. <Typography variant="body2" gutterBottom>
  280. <strong>{t("Manual Input")}:</strong>
  281. </Typography>
  282. <TextField
  283. fullWidth
  284. size="small"
  285. value={manualInput}
  286. onChange={(e) => {
  287. setManualInput(e.target.value);
  288. // Reset error states when user starts typing
  289. if (qrScanFailed || manualInputError) {
  290. setQrScanFailed(false);
  291. setManualInputError(false);
  292. setManualInputSubmitted(false);
  293. }
  294. }}
  295. sx={{ mb: 1 }}
  296. error={manualInputSubmitted && manualInputError}
  297. helperText={
  298. manualInputSubmitted && manualInputError
  299. ? `${t("The input is not the same as the expected lot number.")}`
  300. : ''
  301. }
  302. />
  303. <Button
  304. variant="contained"
  305. onClick={handleManualSubmit}
  306. disabled={!manualInput.trim()}
  307. size="small"
  308. color="primary"
  309. >
  310. {t("Submit")}
  311. </Button>
  312. </Box>
  313. )}
  314. {/* Show QR Scan Status */}
  315. {qrValues.length > 0 && (
  316. <Box sx={{
  317. mb: 2,
  318. p: 2,
  319. backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
  320. borderRadius: 1
  321. }}>
  322. <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
  323. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  324. </Typography>
  325. {qrScanSuccess && (
  326. <Typography variant="caption" color="success" display="block">
  327. {t("Verified successfully!")}
  328. </Typography>
  329. )}
  330. </Box>
  331. )}
  332. <Box sx={{ mt: 2, textAlign: 'right' }}>
  333. <Button onClick={onClose} variant="outlined">
  334. {t("Cancel")}
  335. </Button>
  336. </Box>
  337. </Box>
  338. </Modal>
  339. );
  340. };
  341. const LotTable: React.FC<LotTableProps> = ({
  342. lotData,
  343. selectedRowId,
  344. selectedRow,
  345. pickQtyData,
  346. selectedLotRowId,
  347. selectedLotId,
  348. onLotSelection,
  349. onPickQtyChange,
  350. onSubmitPickQty,
  351. onCreateStockOutLine,
  352. onQcCheck,
  353. onLotSelectForInput,
  354. showInputBody,
  355. setShowInputBody,
  356. selectedLotForInput,
  357. generateInputBody,
  358. onDataRefresh,
  359. onLotDataRefresh,
  360. }) => {
  361. const { t } = useTranslation("pickOrder");
  362. const calculateRemainingRequiredQty = useCallback((lot: LotPickData) => {
  363. const requiredQty = lot.requiredQty || 0;
  364. const stockOutLineQty = lot.stockOutLineQty || 0;
  365. return Math.max(0, requiredQty - stockOutLineQty);
  366. }, []);
  367. // Add QR scanner context
  368. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  369. const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({});
  370. // Add state for QR input modal
  371. const [qrModalOpen, setQrModalOpen] = useState(false);
  372. const [selectedLotForQr, setSelectedLotForQr] = useState<LotPickData | null>(null);
  373. const [manualQrInput, setManualQrInput] = useState<string>('');
  374. // 分页控制器
  375. const [lotTablePagingController, setLotTablePagingController] = useState({
  376. pageNum: 0,
  377. pageSize: 10,
  378. });
  379. // 添加状态消息生成函数
  380. const getStatusMessage = useCallback((lot: LotPickData) => {
  381. switch (lot.stockOutLineStatus?.toLowerCase()) {
  382. case 'pending':
  383. return t("Please finish QR code scanand pick order.");
  384. case 'checked':
  385. return t("Please submit the pick order.");
  386. case 'partially_completed':
  387. return t("Partial quantity submitted. Please submit more or complete the order.") ;
  388. case 'completed':
  389. return t("Pick order completed successfully!");
  390. case 'rejected':
  391. return t("Lot has been rejected and marked as unavailable.");
  392. case 'unavailable':
  393. return t("This order is insufficient, please pick another lot.");
  394. default:
  395. return t("Please finish QR code scan and pick order.");
  396. }
  397. }, []);
  398. const prepareLotTableData = useMemo(() => {
  399. return lotData.map((lot) => ({
  400. ...lot,
  401. id: lot.lotId,
  402. }));
  403. }, [lotData]);
  404. // 分页数据
  405. const paginatedLotTableData = useMemo(() => {
  406. const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize;
  407. const endIndex = startIndex + lotTablePagingController.pageSize;
  408. return prepareLotTableData.slice(startIndex, endIndex);
  409. }, [prepareLotTableData, lotTablePagingController]);
  410. // 分页处理函数
  411. const handleLotTablePageChange = useCallback((event: unknown, newPage: number) => {
  412. setLotTablePagingController(prev => ({
  413. ...prev,
  414. pageNum: newPage,
  415. }));
  416. }, []);
  417. const handleLotTablePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  418. const newPageSize = parseInt(event.target.value, 10);
  419. setLotTablePagingController({
  420. pageNum: 0,
  421. pageSize: newPageSize,
  422. });
  423. }, []);
  424. const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
  425. if (!selectedRowId) return lot.availableQty;
  426. const lactualPickQty = lot.actualPickQty || 0;
  427. const actualPickQty = pickQtyData[selectedRowId]?.[lot.lotId] || 0;
  428. const remainingQty = lot.inQty - lot.outQty-actualPickQty;
  429. // Ensure it doesn't go below 0
  430. return Math.max(0, remainingQty);
  431. }, [selectedRowId, pickQtyData]);
  432. const validatePickQty = useCallback((lot: LotPickData, inputValue: number) => {
  433. const maxAllowed = Math.min(calculateRemainingAvailableQty(lot), calculateRemainingRequiredQty(lot));
  434. if (inputValue > maxAllowed) {
  435. return `${t('Input quantity cannot exceed')} ${maxAllowed}`;
  436. }
  437. if (inputValue < 0) {
  438. return t('Quantity cannot be negative');
  439. }
  440. return null;
  441. }, [calculateRemainingAvailableQty, calculateRemainingRequiredQty, t]);
  442. // Handle QR code submission
  443. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  444. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  445. console.log(` QR Code verified for lot: ${lotNo}`);
  446. if (!selectedLotForQr.stockOutLineId) {
  447. console.error("No stock out line ID found for this lot");
  448. alert("No stock out line found for this lot. Please contact administrator.");
  449. return;
  450. }
  451. // Store the required quantity before creating stock out line
  452. const requiredQty = selectedLotForQr.requiredQty;
  453. const lotId = selectedLotForQr.lotId;
  454. try {
  455. // Update stock out line status to 'checked' (QR scan completed)
  456. const stockOutLineUpdate = await updateStockOutLineStatus({
  457. id: selectedLotForQr.stockOutLineId,
  458. status: 'checked',
  459. qty: selectedLotForQr.stockOutLineQty || 0
  460. });
  461. console.log(" Stock out line updated to 'checked':", stockOutLineUpdate);
  462. // Close modal
  463. setQrModalOpen(false);
  464. setSelectedLotForQr(null);
  465. if (onLotDataRefresh) {
  466. await onLotDataRefresh();
  467. }
  468. // Set pick quantity AFTER stock out line update is complete
  469. if (selectedRowId) {
  470. // Add a small delay to ensure the data refresh is complete
  471. setTimeout(() => {
  472. onPickQtyChange(selectedRowId, lotId, requiredQty);
  473. console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  474. }, 500); // 500ms delay to ensure refresh is complete
  475. }
  476. // Show success message
  477. console.log("Stock out line updated successfully!");
  478. } catch (error) {
  479. console.error("❌ Error updating stock out line status:", error);
  480. alert("Failed to update lot status. Please try again.");
  481. }
  482. } else {
  483. // Handle case where lot numbers don't match
  484. console.error("QR scan mismatch:", { scanned: lotNo, expected: selectedLotForQr?.lotNo });
  485. alert(`QR scan mismatch! Expected: ${selectedLotForQr?.lotNo}, Scanned: ${lotNo}`);
  486. }
  487. }, [selectedLotForQr, selectedRowId, onPickQtyChange]);
  488. // 添加 PickExecutionForm 相关的状态
  489. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  490. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<LotPickData | null>(null);
  491. // 添加处理函数
  492. const handlePickExecutionForm = useCallback((lot: LotPickData) => {
  493. console.log("=== Pick Execution Form ===");
  494. console.log("Lot data:", lot);
  495. if (!lot) {
  496. console.warn("No lot data provided for pick execution form");
  497. return;
  498. }
  499. console.log("Opening pick execution form for lot:", lot.lotNo);
  500. setSelectedLotForExecutionForm(lot);
  501. setPickExecutionFormOpen(true);
  502. console.log("Pick execution form opened for lot ID:", lot.lotId);
  503. }, []);
  504. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  505. try {
  506. console.log("Pick execution form submitted:", data);
  507. // 调用 API 提交数据
  508. const result = await recordPickExecutionIssue(data);
  509. console.log("Pick execution issue recorded:", result);
  510. if (result && result.code === "SUCCESS") {
  511. console.log(" Pick execution issue recorded successfully");
  512. } else {
  513. console.error("❌ Failed to record pick execution issue:", result);
  514. }
  515. setPickExecutionFormOpen(false);
  516. setSelectedLotForExecutionForm(null);
  517. // 刷新数据
  518. if (onDataRefresh) {
  519. await onDataRefresh();
  520. }
  521. if (onLotDataRefresh) {
  522. await onLotDataRefresh();
  523. }
  524. } catch (error) {
  525. console.error("Error submitting pick execution form:", error);
  526. }
  527. }, [onDataRefresh, onLotDataRefresh]);
  528. const allLotsUnavailable = useMemo(() => {
  529. if (!paginatedLotTableData || paginatedLotTableData.length === 0) return false;
  530. return paginatedLotTableData.every((lot) =>
  531. ['rejected', 'expired', 'insufficient_stock', 'status_unavailable']
  532. .includes(lot.lotAvailability)
  533. );
  534. }, [paginatedLotTableData]);
  535. // 完成当前行(无可用批次)的点击处理
  536. const handleCompleteWithoutLot = useCallback(async (lot: LotPickData) => {
  537. try {
  538. if (!lot.stockOutLineId) {
  539. alert("No stock out line for this lot. Please contact administrator.");
  540. return;
  541. }
  542. // 这里建议调用你自己在 actions 里封装的 API,例如:
  543. // await completeStockOutLineWithoutLot(lot.stockOutLineId);
  544. // 简单点可以复用 updateStockOutLineStatus,直接标记 COMPLETE、数量为 0:
  545. await updateStockOutLineStatus({
  546. id: lot.stockOutLineId,
  547. status: 'completed',
  548. qty: lot.stockOutLineQty || 0,
  549. });
  550. // 刷新数据
  551. if (onLotDataRefresh) {
  552. await onLotDataRefresh();
  553. }
  554. if (onDataRefresh) {
  555. await onDataRefresh();
  556. }
  557. } catch (e) {
  558. console.error("Error completing stock out line without lot", e);
  559. alert("Failed to complete this line. Please try again.");
  560. }
  561. }, [onDataRefresh, onLotDataRefresh]);
  562. return (
  563. <>
  564. <TableContainer component={Paper}>
  565. <Table>
  566. <TableHead>
  567. <TableRow>
  568. <TableCell>{t("Selected")}</TableCell>
  569. <TableCell>{t("Lot#")}</TableCell>
  570. <TableCell>{t("Lot Expiry Date")}</TableCell>
  571. <TableCell>{t("Lot Location")}</TableCell>
  572. <TableCell>{t("Stock Unit")}</TableCell>
  573. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  574. <TableCell align="right">{t("Original Available Qty")}</TableCell>
  575. <TableCell align="center">{t("Lot Actual Pick Qty")}</TableCell>
  576. {/*<TableCell align="right">{t("Available Lot")}</TableCell>*/}
  577. <TableCell align="right">{t("Remaining Available Qty")}</TableCell>
  578. {/*<TableCell align="center">{t("QR Code Scan")}</TableCell>*/}
  579. {/*}
  580. <TableCell align="center">{t("Reject")}</TableCell>
  581. */}
  582. <TableCell align="center">{t("Action")}</TableCell>
  583. </TableRow>
  584. </TableHead>
  585. <TableBody>
  586. {paginatedLotTableData.length === 0 ? (
  587. <TableRow>
  588. <TableCell colSpan={11} align="center">
  589. <Typography variant="body2" color="text.secondary">
  590. {t("No data available")}
  591. </Typography>
  592. </TableCell>
  593. </TableRow>
  594. ) : (
  595. paginatedLotTableData.map((lot, index) => (
  596. <TableRow
  597. key={lot.id}
  598. sx={{
  599. backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
  600. opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
  601. '& .MuiTableCell-root': {
  602. color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
  603. }
  604. }}
  605. >
  606. <TableCell>
  607. <Checkbox
  608. checked={selectedLotRowId === `row_${index}`}
  609. onChange={() => onLotSelection(`row_${index}`, lot.lotId)}
  610. // 禁用 rejected、expired 和 status_unavailable 的批次
  611. disabled={lot.lotAvailability === 'expired' ||
  612. lot.lotAvailability === 'status_unavailable' ||
  613. lot.lotAvailability === 'rejected'} // 添加 rejected
  614. value={`row_${index}`}
  615. name="lot-selection"
  616. />
  617. </TableCell>
  618. <TableCell>
  619. <Box>
  620. <Typography
  621. sx={{
  622. color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
  623. opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1
  624. }}
  625. >
  626. {lot.lotNo}
  627. </Typography>
  628. {/*
  629. {lot.lotAvailability !== 'available' && (
  630. <Typography variant="caption" color="error" display="block">
  631. ({lot.lotAvailability === 'expired' ? 'Expired' :
  632. lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' :
  633. lot.lotAvailability === 'rejected' ? 'Rejected' : // 添加 rejected 显示
  634. 'Unavailable'})
  635. </Typography>
  636. )} */}
  637. </Box>
  638. </TableCell>
  639. <TableCell>{lot.expiryDate}</TableCell>
  640. <TableCell>{lot.location}</TableCell>
  641. <TableCell>{lot.stockUnit}</TableCell>
  642. <TableCell align="right">{calculateRemainingRequiredQty(lot).toLocaleString()}</TableCell>
  643. <TableCell align="right">
  644. {(() => {
  645. const inQty = lot.inQty || 0;
  646. const outQty = lot.outQty || 0;
  647. const result = inQty - outQty;
  648. return result.toLocaleString();
  649. })()}
  650. </TableCell>
  651. <TableCell align="center">
  652. {/* Show QR Scan Button if not scanned, otherwise show TextField + Pick Form */}
  653. {lot.stockOutLineStatus?.toLowerCase() === 'pending' ? (
  654. <Button
  655. variant="outlined"
  656. size="small"
  657. onClick={() => {
  658. setSelectedLotForQr(lot);
  659. setQrModalOpen(true);
  660. resetScan();
  661. }}
  662. disabled={
  663. (lot.lotAvailability === 'expired' ||
  664. lot.lotAvailability === 'status_unavailable' ||
  665. lot.lotAvailability === 'rejected') ||
  666. selectedLotRowId !== `row_${index}`
  667. }
  668. sx={{
  669. fontSize: '0.7rem',
  670. py: 0.5,
  671. minHeight: '40px',
  672. whiteSpace: 'nowrap',
  673. minWidth: '80px',
  674. opacity: selectedLotRowId === `row_${index}` ? 1 : 0.5
  675. }}
  676. startIcon={<QrCodeIcon />}
  677. title={
  678. selectedLotRowId !== `row_${index}`
  679. ? "Please select this lot first to enable QR scanning"
  680. : "Click to scan QR code"
  681. }
  682. >
  683. {t("Scan")}
  684. </Button>
  685. ) : (
  686. <Stack
  687. direction="row"
  688. spacing={1}
  689. alignItems="center"
  690. justifyContent="center" // 添加水平居中
  691. sx={{
  692. width: '100%', // 确保占满整个单元格宽度
  693. minHeight: '40px' // 设置最小高度确保垂直居中
  694. }}
  695. >
  696. {/* 恢复 TextField 用于正常数量输入 */}
  697. <TextField
  698. type="number"
  699. size="small"
  700. value={pickQtyData[selectedRowId!]?.[lot.lotId] || ''}
  701. onChange={(e) => {
  702. if (selectedRowId) {
  703. const inputValue = parseFloat(e.target.value) || 0;
  704. const maxAllowed = Math.min(calculateRemainingAvailableQty(lot), calculateRemainingRequiredQty(lot));
  705. onPickQtyChange(selectedRowId, lot.lotId, inputValue);
  706. }
  707. }}
  708. disabled={
  709. (lot.lotAvailability === 'expired' ||
  710. lot.lotAvailability === 'status_unavailable' ||
  711. lot.lotAvailability === 'rejected') ||
  712. selectedLotRowId !== `row_${index}` ||
  713. lot.stockOutLineStatus === 'completed'
  714. }
  715. error={!!validationErrors[`lot_${lot.lotId}`]}
  716. helperText={validationErrors[`lot_${lot.lotId}`]}
  717. inputProps={{
  718. min: 0,
  719. max: calculateRemainingRequiredQty(lot),
  720. step: 0.01
  721. }}
  722. sx={{
  723. width: '60px',
  724. height: '28px',
  725. '& .MuiInputBase-input': {
  726. fontSize: '0.7rem',
  727. textAlign: 'center',
  728. padding: '6px 8px'
  729. }
  730. }}
  731. placeholder="0"
  732. />
  733. {/* 添加 Pick Form 按钮用于问题情况 */}
  734. <Button
  735. variant="outlined"
  736. size="small"
  737. onClick={() => handlePickExecutionForm(lot)}
  738. disabled={
  739. (lot.lotAvailability === 'expired' ||
  740. lot.lotAvailability === 'status_unavailable' ||
  741. lot.lotAvailability === 'rejected') ||
  742. selectedLotRowId !== `row_${index}`
  743. }
  744. sx={{
  745. fontSize: '0.7rem',
  746. py: 0.5,
  747. minHeight: '28px',
  748. minWidth: '60px',
  749. borderColor: 'warning.main',
  750. color: 'warning.main'
  751. }}
  752. title="Report missing or bad items"
  753. >
  754. {t("Issue")}
  755. </Button>
  756. </Stack>
  757. )}
  758. </TableCell>
  759. {/*<TableCell align="right">{lot.availableQty.toLocaleString()}</TableCell>*/}
  760. <TableCell align="right">{calculateRemainingAvailableQty(lot).toLocaleString()}</TableCell>
  761. <TableCell align="center">
  762. <Stack direction="column" spacing={1} alignItems="center">
  763. <Button
  764. variant="contained"
  765. onClick={() => {
  766. if (selectedRowId) {
  767. onSubmitPickQty(selectedRowId, lot.lotId);
  768. }
  769. }}
  770. disabled={
  771. (lot.lotAvailability === 'expired' ||
  772. lot.lotAvailability === 'status_unavailable' ||
  773. lot.lotAvailability === 'rejected') || // 添加 rejected
  774. !pickQtyData[selectedRowId!]?.[lot.lotId] ||
  775. !lot.stockOutLineStatus ||
  776. !['pending','checked', 'partially_completed'].includes(lot.stockOutLineStatus.toLowerCase())
  777. }
  778. // Allow submission for available AND insufficient_stock lots
  779. sx={{
  780. fontSize: '0.75rem',
  781. py: 0.5,
  782. minHeight: '28px'
  783. }}
  784. >
  785. {t("Submit")}
  786. </Button>
  787. </Stack>
  788. </TableCell>
  789. </TableRow>
  790. ))
  791. )}
  792. </TableBody>
  793. </Table>
  794. </TableContainer>
  795. {/* Status Messages Display */}
  796. {paginatedLotTableData.length > 0 && (
  797. <Box sx={{ mt: 2, p: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
  798. {paginatedLotTableData.map((lot, index) => (
  799. <Box key={lot.id} sx={{ mb: 1 }}>
  800. <Typography variant="body2" color="text.secondary">
  801. <strong>{t("Lot")} {lot.lotNo}:</strong> {getStatusMessage(lot)}
  802. </Typography>
  803. </Box>
  804. ))}
  805. </Box>
  806. )}
  807. <TablePagination
  808. component="div"
  809. count={prepareLotTableData.length}
  810. page={lotTablePagingController.pageNum}
  811. rowsPerPage={lotTablePagingController.pageSize}
  812. onPageChange={handleLotTablePageChange}
  813. onRowsPerPageChange={handleLotTablePageSizeChange}
  814. rowsPerPageOptions={[10, 25, 50]}
  815. labelRowsPerPage={t("Rows per page")}
  816. labelDisplayedRows={({ from, to, count }) =>
  817. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  818. }
  819. />
  820. {/* QR Code Modal */}
  821. <QrCodeModal
  822. open={qrModalOpen}
  823. onClose={() => {
  824. setQrModalOpen(false);
  825. setSelectedLotForQr(null);
  826. stopScan();
  827. resetScan();
  828. }}
  829. lot={selectedLotForQr}
  830. onQrCodeSubmit={handleQrCodeSubmit}
  831. />
  832. {/* Pick Execution Form Modal */}
  833. {pickExecutionFormOpen && selectedLotForExecutionForm && selectedRow && (
  834. <PickExecutionForm
  835. open={pickExecutionFormOpen}
  836. onClose={() => {
  837. setPickExecutionFormOpen(false);
  838. setSelectedLotForExecutionForm(null);
  839. }}
  840. onSubmit={handlePickExecutionFormSubmit}
  841. selectedLot={selectedLotForExecutionForm}
  842. selectedPickOrderLine={selectedRow}
  843. pickOrderId={selectedRow.pickOrderId}
  844. pickOrderCreateDate={new Date()}
  845. />
  846. )}
  847. </>
  848. );
  849. };
  850. export default LotTable;