FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

LotTable.tsx 33 KiB

3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
2週間前
3週間前
2週間前
3週間前
2週間前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
1ヶ月前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
2週間前
3週間前
2週間前
3ヶ月前
3ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  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;