FPSMS-frontend
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 

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