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

LotTable.tsx 34 KiB

5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
3ヶ月前
4ヶ月前
5ヶ月前
4日前
5ヶ月前
4日前
5ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
3ヶ月前
5ヶ月前
4日前
5ヶ月前
3ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
4ヶ月前
3ヶ月前
5ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
3ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
3ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
2ヶ月前
5ヶ月前
4ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
5ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
5ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
4ヶ月前
5ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934
  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;