FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

546 line
19 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 } from "@/app/api/pickOrder/actions";
  23. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  24. interface LotPickData {
  25. id: number;
  26. lotId: number;
  27. lotNo: string;
  28. expiryDate: string;
  29. location: string;
  30. stockUnit: string;
  31. availableQty: number;
  32. requiredQty: number;
  33. actualPickQty: number;
  34. lotStatus: string;
  35. lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable';
  36. stockOutLineId?: number;
  37. stockOutLineStatus?: string;
  38. stockOutLineQty?: number;
  39. }
  40. interface PickQtyData {
  41. [lineId: number]: {
  42. [lotId: number]: number;
  43. };
  44. }
  45. interface LotTableProps {
  46. lotData: LotPickData[];
  47. selectedRowId: number | null;
  48. selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
  49. pickQtyData: PickQtyData;
  50. selectedLotRowId: string | null;
  51. selectedLotId: number | null;
  52. onLotSelection: (uniqueLotId: string, lotId: number) => void;
  53. onPickQtyChange: (lineId: number, lotId: number, value: number) => void;
  54. onSubmitPickQty: (lineId: number, lotId: number) => void;
  55. onCreateStockOutLine: (inventoryLotLineId: number) => void;
  56. onQcCheck: (line: GetPickOrderLineInfo, pickOrderCode: string) => void;
  57. onLotSelectForInput: (lot: LotPickData) => void;
  58. showInputBody: boolean;
  59. setShowInputBody: (show: boolean) => void;
  60. selectedLotForInput: LotPickData | null;
  61. generateInputBody: () => any;
  62. }
  63. // ✅ QR Code Modal Component
  64. const QrCodeModal: React.FC<{
  65. open: boolean;
  66. onClose: () => void;
  67. lot: LotPickData | null;
  68. onQrCodeSubmit: (lotNo: string) => void;
  69. }> = ({ open, onClose, lot, onQrCodeSubmit }) => {
  70. const { t } = useTranslation("pickOrder");
  71. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  72. const [manualInput, setManualInput] = useState<string>('');
  73. // ✅ Add state to track manual input submission
  74. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  75. const [manualInputError, setManualInputError] = useState<boolean>(false);
  76. // ✅ Process scanned QR codes
  77. useEffect(() => {
  78. if (qrValues.length > 0 && lot) {
  79. const latestQr = qrValues[qrValues.length - 1];
  80. const qrContent = latestQr.replace(/[{}]/g, '');
  81. if (qrContent === lot.lotNo) {
  82. onQrCodeSubmit(lot.lotNo);
  83. onClose();
  84. resetScan();
  85. } else {
  86. // ✅ Set error state for helper text
  87. setManualInputError(true);
  88. setManualInputSubmitted(true);
  89. }
  90. }
  91. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan]);
  92. // ✅ Clear states when modal opens or lot changes
  93. useEffect(() => {
  94. if (open) {
  95. setManualInput('');
  96. setManualInputSubmitted(false);
  97. setManualInputError(false);
  98. }
  99. }, [open]);
  100. useEffect(() => {
  101. if (lot) {
  102. setManualInput('');
  103. setManualInputSubmitted(false);
  104. setManualInputError(false);
  105. }
  106. }, [lot]);
  107. const handleManualSubmit = () => {
  108. if (manualInput.trim() === lot?.lotNo) {
  109. // ✅ Success - no error helper text needed
  110. onQrCodeSubmit(lot.lotNo);
  111. onClose();
  112. setManualInput('');
  113. } else {
  114. // ✅ Show error helper text after submit
  115. setManualInputError(true);
  116. setManualInputSubmitted(true);
  117. // Don't clear input - let user see what they typed
  118. }
  119. };
  120. return (
  121. <Modal open={open} onClose={onClose}>
  122. <Box sx={{
  123. position: 'absolute',
  124. top: '50%',
  125. left: '50%',
  126. transform: 'translate(-50%, -50%)',
  127. bgcolor: 'background.paper',
  128. p: 3,
  129. borderRadius: 2,
  130. minWidth: 400,
  131. }}>
  132. <Typography variant="h6" gutterBottom>
  133. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  134. </Typography>
  135. {/* QR Scanner Status */}
  136. <Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}>
  137. <Typography variant="body2" gutterBottom>
  138. <strong>Scanner Status:</strong> {isScanning ? 'Scanning...' : 'Ready'}
  139. </Typography>
  140. <Stack direction="row" spacing={1}>
  141. <Button
  142. variant="contained"
  143. onClick={isScanning ? stopScan : startScan}
  144. size="small"
  145. >
  146. {isScanning ? 'Stop Scan' : 'Start Scan'}
  147. </Button>
  148. <Button
  149. variant="outlined"
  150. onClick={resetScan}
  151. size="small"
  152. >
  153. Reset
  154. </Button>
  155. </Stack>
  156. </Box>
  157. {/* Manual Input with Submit-Triggered Helper Text */}
  158. <Box sx={{ mb: 2 }}>
  159. <Typography variant="body2" gutterBottom>
  160. <strong>Manual Input:</strong>
  161. </Typography>
  162. <TextField
  163. fullWidth
  164. size="small"
  165. value={manualInput}
  166. onChange={(e) => setManualInput(e.target.value)}
  167. sx={{ mb: 1 }}
  168. // ✅ Only show error after submit button is clicked
  169. error={manualInputSubmitted && manualInputError}
  170. helperText={
  171. // ✅ Show helper text only after submit with error
  172. manualInputSubmitted && manualInputError
  173. ? `The input is not the same as the expected lot number. Expected: ${lot?.lotNo}`
  174. : ''
  175. }
  176. />
  177. <Button
  178. variant="contained"
  179. onClick={handleManualSubmit}
  180. disabled={!manualInput.trim()}
  181. size="small"
  182. color="primary"
  183. >
  184. Submit Manual Input
  185. </Button>
  186. </Box>
  187. {/* ✅ Show QR Scan Status */}
  188. {qrValues.length > 0 && (
  189. <Box sx={{ mb: 2, p: 2, backgroundColor: manualInputError ? '#ffebee' : '#e8f5e8', borderRadius: 1 }}>
  190. <Typography variant="body2" color={manualInputError ? 'error' : 'success'}>
  191. <strong>QR Scan Result:</strong> {qrValues[qrValues.length - 1]}
  192. </Typography>
  193. {manualInputError && (
  194. <Typography variant="caption" color="error" display="block">
  195. ❌ Mismatch! Expected: {lot?.lotNo}
  196. </Typography>
  197. )}
  198. </Box>
  199. )}
  200. <Box sx={{ mt: 2, textAlign: 'right' }}>
  201. <Button onClick={onClose} variant="outlined">
  202. Cancel
  203. </Button>
  204. </Box>
  205. </Box>
  206. </Modal>
  207. );
  208. };
  209. const LotTable: React.FC<LotTableProps> = ({
  210. lotData,
  211. selectedRowId,
  212. selectedRow,
  213. pickQtyData,
  214. selectedLotRowId,
  215. selectedLotId,
  216. onLotSelection,
  217. onPickQtyChange,
  218. onSubmitPickQty,
  219. onCreateStockOutLine,
  220. onQcCheck,
  221. onLotSelectForInput,
  222. showInputBody,
  223. setShowInputBody,
  224. selectedLotForInput,
  225. generateInputBody,
  226. }) => {
  227. const { t } = useTranslation("pickOrder");
  228. // ✅ Add QR scanner context
  229. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  230. // ✅ Add state for QR input modal
  231. const [qrModalOpen, setQrModalOpen] = useState(false);
  232. const [selectedLotForQr, setSelectedLotForQr] = useState<LotPickData | null>(null);
  233. const [manualQrInput, setManualQrInput] = useState<string>('');
  234. // 分页控制器
  235. const [lotTablePagingController, setLotTablePagingController] = useState({
  236. pageNum: 0,
  237. pageSize: 10,
  238. });
  239. // ✅ 添加状态消息生成函数
  240. const getStatusMessage = useCallback((lot: LotPickData) => {
  241. if (!lot.stockOutLineId) {
  242. return "Please finish QR code scan, QC check and pick order.";
  243. }
  244. switch (lot.stockOutLineStatus?.toLowerCase()) {
  245. case 'pending':
  246. return "Please finish QC check and pick order.";
  247. case 'completed':
  248. return "Please submit the pick order.";
  249. case 'unavailable':
  250. return "This order is insufficient, please pick another lot.";
  251. default:
  252. return "Please finish QR code scan, QC check and pick order.";
  253. }
  254. }, []);
  255. const prepareLotTableData = useMemo(() => {
  256. return lotData.map((lot) => ({
  257. ...lot,
  258. id: lot.lotId,
  259. }));
  260. }, [lotData]);
  261. // 分页数据
  262. const paginatedLotTableData = useMemo(() => {
  263. const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize;
  264. const endIndex = startIndex + lotTablePagingController.pageSize;
  265. return prepareLotTableData.slice(startIndex, endIndex);
  266. }, [prepareLotTableData, lotTablePagingController]);
  267. // 分页处理函数
  268. const handleLotTablePageChange = useCallback((event: unknown, newPage: number) => {
  269. setLotTablePagingController(prev => ({
  270. ...prev,
  271. pageNum: newPage,
  272. }));
  273. }, []);
  274. const handleLotTablePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  275. const newPageSize = parseInt(event.target.value, 10);
  276. setLotTablePagingController({
  277. pageNum: 0,
  278. pageSize: newPageSize,
  279. });
  280. }, []);
  281. // ✅ Handle QR code submission
  282. const handleQrCodeSubmit = useCallback((lotNo: string) => {
  283. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  284. console.log(`✅ QR Code verified for lot: ${lotNo}`);
  285. // ✅ Create stock out line
  286. onCreateStockOutLine(selectedLotForQr.lotId);
  287. // ✅ Show success message
  288. console.log("Stock out line created successfully!");
  289. // ✅ Close modal
  290. setQrModalOpen(false);
  291. setSelectedLotForQr(null);
  292. }
  293. }, [selectedLotForQr, onCreateStockOutLine]);
  294. return (
  295. <>
  296. <TableContainer component={Paper}>
  297. <Table>
  298. <TableHead>
  299. <TableRow>
  300. <TableCell>{t("Selected")}</TableCell>
  301. <TableCell>{t("Lot#")}</TableCell>
  302. <TableCell>{t("Lot Expiry Date")}</TableCell>
  303. <TableCell>{t("Lot Location")}</TableCell>
  304. <TableCell align="right">{t("Available Lot")}</TableCell>
  305. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  306. <TableCell>{t("Stock Unit")}</TableCell>
  307. <TableCell align="center">{t("QR Code Scan")}</TableCell>
  308. <TableCell align="center">{t("QC Check")}</TableCell>
  309. <TableCell align="right">{t("Lot Actual Pick Qty")}</TableCell>
  310. <TableCell align="center">{t("Submit")}</TableCell>
  311. </TableRow>
  312. </TableHead>
  313. <TableBody>
  314. {paginatedLotTableData.length === 0 ? (
  315. <TableRow>
  316. <TableCell colSpan={11} align="center">
  317. <Typography variant="body2" color="text.secondary">
  318. {t("No data available")}
  319. </Typography>
  320. </TableCell>
  321. </TableRow>
  322. ) : (
  323. paginatedLotTableData.map((lot, index) => (
  324. <TableRow key={lot.id}>
  325. <TableCell>
  326. <Checkbox
  327. checked={selectedLotRowId === `row_${index}`}
  328. onChange={() => onLotSelection(`row_${index}`, lot.lotId)}
  329. // ✅ Allow selection of available AND insufficient_stock lots
  330. disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'}
  331. value={`row_${index}`}
  332. name="lot-selection"
  333. />
  334. </TableCell>
  335. <TableCell>
  336. <Box>
  337. <Typography>{lot.lotNo}</Typography>
  338. {lot.lotAvailability !== 'available' && (
  339. <Typography variant="caption" color="error" display="block">
  340. ({lot.lotAvailability === 'expired' ? 'Expired' :
  341. lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' :
  342. 'Unavailable'})
  343. </Typography>
  344. )}
  345. </Box>
  346. </TableCell>
  347. <TableCell>{lot.expiryDate}</TableCell>
  348. <TableCell>{lot.location}</TableCell>
  349. <TableCell align="right">{lot.availableQty.toLocaleString()}</TableCell>
  350. <TableCell align="right">{lot.requiredQty.toLocaleString()}</TableCell>
  351. <TableCell>{lot.stockUnit}</TableCell>
  352. {/* QR Code Scan Button */}
  353. <TableCell align="center">
  354. <Box sx={{ textAlign: 'center' }}>
  355. <Button
  356. variant="outlined"
  357. size="small"
  358. onClick={() => {
  359. setSelectedLotForQr(lot);
  360. setQrModalOpen(true);
  361. resetScan();
  362. }}
  363. // ✅ Disable when:
  364. // 1. Lot is expired or unavailable
  365. // 2. Already scanned (has stockOutLineId)
  366. // 3. Not selected (selectedLotRowId doesn't match)
  367. disabled={
  368. (lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') ||
  369. Boolean(lot.stockOutLineId) ||
  370. selectedLotRowId !== `row_${index}`
  371. }
  372. sx={{
  373. fontSize: '0.7rem',
  374. py: 0.5,
  375. minHeight: '28px',
  376. whiteSpace: 'nowrap',
  377. minWidth: '40px',
  378. // ✅ Visual feedback
  379. opacity: selectedLotRowId === `row_${index}` ? 1 : 0.5
  380. }}
  381. startIcon={<QrCodeIcon />}
  382. title={
  383. selectedLotRowId !== `row_${index}`
  384. ? "Please select this lot first to enable QR scanning"
  385. : lot.stockOutLineId
  386. ? "Already scanned"
  387. : "Click to scan QR code"
  388. }
  389. >
  390. {lot.stockOutLineId ? t("Scanned") : t("Scan")}
  391. </Button>
  392. </Box>
  393. </TableCell>
  394. {/* QC Check Button */}
  395. <TableCell align="center">
  396. <Button
  397. variant="outlined"
  398. size="small"
  399. onClick={() => {
  400. if (selectedRowId && selectedRow) {
  401. onQcCheck(selectedRow, selectedRow.pickOrderCode);
  402. }
  403. }}
  404. // ✅ Enable QC check only when stock out line exists
  405. disabled={!lot.stockOutLineId || selectedLotRowId !== `row_${index}`}
  406. sx={{
  407. fontSize: '0.7rem',
  408. py: 0.5,
  409. minHeight: '28px',
  410. whiteSpace: 'nowrap',
  411. minWidth: '40px'
  412. }}
  413. >
  414. {t("QC")}
  415. </Button>
  416. </TableCell>
  417. {/* Lot Actual Pick Qty */}
  418. <TableCell align="right">
  419. <TextField
  420. type="number"
  421. value={selectedRowId ? (pickQtyData[selectedRowId]?.[lot.lotId] || 0) : 0}
  422. onChange={(e) => {
  423. if (selectedRowId) {
  424. onPickQtyChange(
  425. selectedRowId,
  426. lot.lotId, // This should be unique (ill.id)
  427. parseInt(e.target.value) || 0
  428. );
  429. }
  430. }}
  431. inputProps={{ min: 0, max: lot.availableQty }}
  432. // ✅ Allow input for available AND insufficient_stock lots
  433. disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'}
  434. sx={{ width: '80px' }}
  435. />
  436. </TableCell>
  437. {/* Submit Button */}
  438. <TableCell align="center">
  439. <Button
  440. variant="contained"
  441. onClick={() => {
  442. if (selectedRowId) {
  443. onSubmitPickQty(selectedRowId, lot.lotId);
  444. }
  445. }}
  446. // ✅ Allow submission for available AND insufficient_stock lots
  447. disabled={(lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') || !pickQtyData[selectedRowId!]?.[lot.lotId]}
  448. sx={{
  449. fontSize: '0.75rem',
  450. py: 0.5,
  451. minHeight: '28px'
  452. }}
  453. >
  454. {t("Submit")}
  455. </Button>
  456. </TableCell>
  457. </TableRow>
  458. ))
  459. )}
  460. </TableBody>
  461. </Table>
  462. </TableContainer>
  463. {/* ✅ Status Messages Display */}
  464. {paginatedLotTableData.length > 0 && (
  465. <Box sx={{ mt: 2, p: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
  466. {paginatedLotTableData.map((lot, index) => (
  467. <Box key={lot.id} sx={{ mb: 1 }}>
  468. <Typography variant="body2" color="text.secondary">
  469. <strong>Lot {lot.lotNo}:</strong> {getStatusMessage(lot)}
  470. </Typography>
  471. </Box>
  472. ))}
  473. </Box>
  474. )}
  475. <TablePagination
  476. component="div"
  477. count={prepareLotTableData.length}
  478. page={lotTablePagingController.pageNum}
  479. rowsPerPage={lotTablePagingController.pageSize}
  480. onPageChange={handleLotTablePageChange}
  481. onRowsPerPageChange={handleLotTablePageSizeChange}
  482. rowsPerPageOptions={[10, 25, 50]}
  483. labelRowsPerPage={t("Rows per page")}
  484. labelDisplayedRows={({ from, to, count }) =>
  485. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  486. }
  487. />
  488. {/* ✅ QR Code Modal */}
  489. <QrCodeModal
  490. open={qrModalOpen}
  491. onClose={() => {
  492. setQrModalOpen(false);
  493. setSelectedLotForQr(null);
  494. stopScan();
  495. resetScan();
  496. }}
  497. lot={selectedLotForQr}
  498. onQrCodeSubmit={handleQrCodeSubmit}
  499. />
  500. </>
  501. );
  502. };
  503. export default LotTable;