|
- "use client";
-
- import {
- Box,
- Button,
- Checkbox,
- Paper,
- Stack,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- TextField,
- Typography,
- TablePagination,
- Modal,
- } from "@mui/material";
- import { useCallback, useMemo, useState, useEffect } from "react";
- import { useTranslation } from "react-i18next";
- import QrCodeIcon from '@mui/icons-material/QrCode';
- import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions";
- import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
-
- interface LotPickData {
- id: number;
- lotId: number;
- lotNo: string;
- expiryDate: string;
- location: string;
- stockUnit: string;
- availableQty: number;
- requiredQty: number;
- actualPickQty: number;
- lotStatus: string;
- lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable';
- stockOutLineId?: number;
- stockOutLineStatus?: string;
- stockOutLineQty?: number;
- }
-
- interface PickQtyData {
- [lineId: number]: {
- [lotId: number]: number;
- };
- }
-
- interface LotTableProps {
- lotData: LotPickData[];
- selectedRowId: number | null;
- selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
- pickQtyData: PickQtyData;
- selectedLotRowId: string | null;
- selectedLotId: number | null;
- onLotSelection: (uniqueLotId: string, lotId: number) => void;
- onPickQtyChange: (lineId: number, lotId: number, value: number) => void;
- onSubmitPickQty: (lineId: number, lotId: number) => void;
- onCreateStockOutLine: (inventoryLotLineId: number) => void;
- onQcCheck: (line: GetPickOrderLineInfo, pickOrderCode: string) => void;
- onLotSelectForInput: (lot: LotPickData) => void;
- showInputBody: boolean;
- setShowInputBody: (show: boolean) => void;
- selectedLotForInput: LotPickData | null;
- generateInputBody: () => any;
- }
-
- // ✅ QR Code Modal Component
- const QrCodeModal: React.FC<{
- open: boolean;
- onClose: () => void;
- lot: LotPickData | null;
- onQrCodeSubmit: (lotNo: string) => void;
- }> = ({ open, onClose, lot, onQrCodeSubmit }) => {
- const { t } = useTranslation("pickOrder");
- const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
- const [manualInput, setManualInput] = useState<string>('');
-
- // ✅ Add state to track manual input submission
- const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
- const [manualInputError, setManualInputError] = useState<boolean>(false);
-
- // ✅ Process scanned QR codes
- useEffect(() => {
- if (qrValues.length > 0 && lot) {
- const latestQr = qrValues[qrValues.length - 1];
- const qrContent = latestQr.replace(/[{}]/g, '');
-
- if (qrContent === lot.lotNo) {
- onQrCodeSubmit(lot.lotNo);
- onClose();
- resetScan();
- } else {
- // ✅ Set error state for helper text
- setManualInputError(true);
- setManualInputSubmitted(true);
- }
- }
- }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan]);
-
- // ✅ Clear states when modal opens or lot changes
- useEffect(() => {
- if (open) {
- setManualInput('');
- setManualInputSubmitted(false);
- setManualInputError(false);
- }
- }, [open]);
-
- useEffect(() => {
- if (lot) {
- setManualInput('');
- setManualInputSubmitted(false);
- setManualInputError(false);
- }
- }, [lot]);
-
- const handleManualSubmit = () => {
- if (manualInput.trim() === lot?.lotNo) {
- // ✅ Success - no error helper text needed
- onQrCodeSubmit(lot.lotNo);
- onClose();
- setManualInput('');
- } else {
- // ✅ Show error helper text after submit
- setManualInputError(true);
- setManualInputSubmitted(true);
- // Don't clear input - let user see what they typed
- }
- };
-
- return (
- <Modal open={open} onClose={onClose}>
- <Box sx={{
- position: 'absolute',
- top: '50%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- bgcolor: 'background.paper',
- p: 3,
- borderRadius: 2,
- minWidth: 400,
- }}>
- <Typography variant="h6" gutterBottom>
- {t("QR Code Scan for Lot")}: {lot?.lotNo}
- </Typography>
-
- {/* QR Scanner Status */}
- <Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}>
- <Typography variant="body2" gutterBottom>
- <strong>Scanner Status:</strong> {isScanning ? 'Scanning...' : 'Ready'}
- </Typography>
- <Stack direction="row" spacing={1}>
- <Button
- variant="contained"
- onClick={isScanning ? stopScan : startScan}
- size="small"
- >
- {isScanning ? 'Stop Scan' : 'Start Scan'}
- </Button>
- <Button
- variant="outlined"
- onClick={resetScan}
- size="small"
- >
- Reset
- </Button>
- </Stack>
- </Box>
-
- {/* Manual Input with Submit-Triggered Helper Text */}
- <Box sx={{ mb: 2 }}>
- <Typography variant="body2" gutterBottom>
- <strong>Manual Input:</strong>
- </Typography>
- <TextField
- fullWidth
- size="small"
- value={manualInput}
- onChange={(e) => setManualInput(e.target.value)}
- sx={{ mb: 1 }}
- // ✅ Only show error after submit button is clicked
- error={manualInputSubmitted && manualInputError}
- helperText={
- // ✅ Show helper text only after submit with error
- manualInputSubmitted && manualInputError
- ? `The input is not the same as the expected lot number. Expected: ${lot?.lotNo}`
- : ''
- }
- />
- <Button
- variant="contained"
- onClick={handleManualSubmit}
- disabled={!manualInput.trim()}
- size="small"
- color="primary"
- >
- Submit Manual Input
- </Button>
- </Box>
-
- {/* ✅ Show QR Scan Status */}
- {qrValues.length > 0 && (
- <Box sx={{ mb: 2, p: 2, backgroundColor: manualInputError ? '#ffebee' : '#e8f5e8', borderRadius: 1 }}>
- <Typography variant="body2" color={manualInputError ? 'error' : 'success'}>
- <strong>QR Scan Result:</strong> {qrValues[qrValues.length - 1]}
- </Typography>
- {manualInputError && (
- <Typography variant="caption" color="error" display="block">
- ❌ Mismatch! Expected: {lot?.lotNo}
- </Typography>
- )}
- </Box>
- )}
-
- <Box sx={{ mt: 2, textAlign: 'right' }}>
- <Button onClick={onClose} variant="outlined">
- Cancel
- </Button>
- </Box>
- </Box>
- </Modal>
- );
- };
-
- const LotTable: React.FC<LotTableProps> = ({
- lotData,
- selectedRowId,
- selectedRow,
- pickQtyData,
- selectedLotRowId,
- selectedLotId,
- onLotSelection,
- onPickQtyChange,
- onSubmitPickQty,
- onCreateStockOutLine,
- onQcCheck,
- onLotSelectForInput,
- showInputBody,
- setShowInputBody,
- selectedLotForInput,
- generateInputBody,
- }) => {
- const { t } = useTranslation("pickOrder");
-
- // ✅ Add QR scanner context
- const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
-
- // ✅ Add state for QR input modal
- const [qrModalOpen, setQrModalOpen] = useState(false);
- const [selectedLotForQr, setSelectedLotForQr] = useState<LotPickData | null>(null);
- const [manualQrInput, setManualQrInput] = useState<string>('');
-
- // 分页控制器
- const [lotTablePagingController, setLotTablePagingController] = useState({
- pageNum: 0,
- pageSize: 10,
- });
-
- // ✅ 添加状态消息生成函数
- const getStatusMessage = useCallback((lot: LotPickData) => {
- if (!lot.stockOutLineId) {
- return "Please finish QR code scan, QC check and pick order.";
- }
-
- switch (lot.stockOutLineStatus?.toLowerCase()) {
- case 'pending':
- return "Please finish QC check and pick order.";
- case 'completed':
- return "Please submit the pick order.";
- case 'unavailable':
- return "This order is insufficient, please pick another lot.";
- default:
- return "Please finish QR code scan, QC check and pick order.";
- }
- }, []);
-
- const prepareLotTableData = useMemo(() => {
- return lotData.map((lot) => ({
- ...lot,
- id: lot.lotId,
- }));
- }, [lotData]);
-
- // 分页数据
- const paginatedLotTableData = useMemo(() => {
- const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize;
- const endIndex = startIndex + lotTablePagingController.pageSize;
- return prepareLotTableData.slice(startIndex, endIndex);
- }, [prepareLotTableData, lotTablePagingController]);
-
- // 分页处理函数
- const handleLotTablePageChange = useCallback((event: unknown, newPage: number) => {
- setLotTablePagingController(prev => ({
- ...prev,
- pageNum: newPage,
- }));
- }, []);
-
- const handleLotTablePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
- const newPageSize = parseInt(event.target.value, 10);
- setLotTablePagingController({
- pageNum: 0,
- pageSize: newPageSize,
- });
- }, []);
-
- // ✅ Handle QR code submission
- const handleQrCodeSubmit = useCallback((lotNo: string) => {
- if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
- console.log(`✅ QR Code verified for lot: ${lotNo}`);
-
- // ✅ Create stock out line
- onCreateStockOutLine(selectedLotForQr.lotId);
-
- // ✅ Show success message
- console.log("Stock out line created successfully!");
-
- // ✅ Close modal
- setQrModalOpen(false);
- setSelectedLotForQr(null);
- }
- }, [selectedLotForQr, onCreateStockOutLine]);
-
- return (
- <>
- <TableContainer component={Paper}>
- <Table>
- <TableHead>
- <TableRow>
- <TableCell>{t("Selected")}</TableCell>
- <TableCell>{t("Lot#")}</TableCell>
- <TableCell>{t("Lot Expiry Date")}</TableCell>
- <TableCell>{t("Lot Location")}</TableCell>
- <TableCell align="right">{t("Available Lot")}</TableCell>
- <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
- <TableCell>{t("Stock Unit")}</TableCell>
- <TableCell align="center">{t("QR Code Scan")}</TableCell>
- <TableCell align="center">{t("QC Check")}</TableCell>
- <TableCell align="right">{t("Lot Actual Pick Qty")}</TableCell>
- <TableCell align="center">{t("Submit")}</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {paginatedLotTableData.length === 0 ? (
- <TableRow>
- <TableCell colSpan={11} align="center">
- <Typography variant="body2" color="text.secondary">
- {t("No data available")}
- </Typography>
- </TableCell>
- </TableRow>
- ) : (
- paginatedLotTableData.map((lot, index) => (
- <TableRow key={lot.id}>
- <TableCell>
- <Checkbox
- checked={selectedLotRowId === `row_${index}`}
- onChange={() => onLotSelection(`row_${index}`, lot.lotId)}
- // ✅ Allow selection of available AND insufficient_stock lots
- disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'}
- value={`row_${index}`}
- name="lot-selection"
- />
- </TableCell>
- <TableCell>
- <Box>
- <Typography>{lot.lotNo}</Typography>
- {lot.lotAvailability !== 'available' && (
- <Typography variant="caption" color="error" display="block">
- ({lot.lotAvailability === 'expired' ? 'Expired' :
- lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' :
- 'Unavailable'})
- </Typography>
- )}
- </Box>
- </TableCell>
- <TableCell>{lot.expiryDate}</TableCell>
- <TableCell>{lot.location}</TableCell>
- <TableCell align="right">{lot.availableQty.toLocaleString()}</TableCell>
- <TableCell align="right">{lot.requiredQty.toLocaleString()}</TableCell>
- <TableCell>{lot.stockUnit}</TableCell>
-
- {/* QR Code Scan Button */}
- <TableCell align="center">
- <Box sx={{ textAlign: 'center' }}>
- <Button
- variant="outlined"
- size="small"
- onClick={() => {
- setSelectedLotForQr(lot);
- setQrModalOpen(true);
- resetScan();
- }}
- // ✅ Disable when:
- // 1. Lot is expired or unavailable
- // 2. Already scanned (has stockOutLineId)
- // 3. Not selected (selectedLotRowId doesn't match)
- disabled={
- (lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') ||
- Boolean(lot.stockOutLineId) ||
- selectedLotRowId !== `row_${index}`
- }
- sx={{
- fontSize: '0.7rem',
- py: 0.5,
- minHeight: '28px',
- whiteSpace: 'nowrap',
- minWidth: '40px',
- // ✅ Visual feedback
- opacity: selectedLotRowId === `row_${index}` ? 1 : 0.5
- }}
- startIcon={<QrCodeIcon />}
- title={
- selectedLotRowId !== `row_${index}`
- ? "Please select this lot first to enable QR scanning"
- : lot.stockOutLineId
- ? "Already scanned"
- : "Click to scan QR code"
- }
- >
- {lot.stockOutLineId ? t("Scanned") : t("Scan")}
- </Button>
-
-
- </Box>
- </TableCell>
-
- {/* QC Check Button */}
- <TableCell align="center">
- <Button
- variant="outlined"
- size="small"
- onClick={() => {
- if (selectedRowId && selectedRow) {
- onQcCheck(selectedRow, selectedRow.pickOrderCode);
- }
- }}
- // ✅ Enable QC check only when stock out line exists
- disabled={!lot.stockOutLineId || selectedLotRowId !== `row_${index}`}
- sx={{
- fontSize: '0.7rem',
- py: 0.5,
- minHeight: '28px',
- whiteSpace: 'nowrap',
- minWidth: '40px'
- }}
- >
- {t("QC")}
- </Button>
- </TableCell>
-
- {/* Lot Actual Pick Qty */}
- <TableCell align="right">
- <TextField
- type="number"
- value={selectedRowId ? (pickQtyData[selectedRowId]?.[lot.lotId] || 0) : 0}
- onChange={(e) => {
- if (selectedRowId) {
- onPickQtyChange(
- selectedRowId,
- lot.lotId, // This should be unique (ill.id)
- parseInt(e.target.value) || 0
- );
- }
- }}
- inputProps={{ min: 0, max: lot.availableQty }}
- // ✅ Allow input for available AND insufficient_stock lots
- disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'}
- sx={{ width: '80px' }}
- />
- </TableCell>
-
- {/* Submit Button */}
- <TableCell align="center">
- <Button
- variant="contained"
- onClick={() => {
- if (selectedRowId) {
- onSubmitPickQty(selectedRowId, lot.lotId);
- }
- }}
- // ✅ Allow submission for available AND insufficient_stock lots
- disabled={(lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') || !pickQtyData[selectedRowId!]?.[lot.lotId]}
- sx={{
- fontSize: '0.75rem',
- py: 0.5,
- minHeight: '28px'
- }}
- >
- {t("Submit")}
- </Button>
- </TableCell>
- </TableRow>
- ))
- )}
- </TableBody>
- </Table>
- </TableContainer>
-
- {/* ✅ Status Messages Display */}
- {paginatedLotTableData.length > 0 && (
- <Box sx={{ mt: 2, p: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
- {paginatedLotTableData.map((lot, index) => (
- <Box key={lot.id} sx={{ mb: 1 }}>
- <Typography variant="body2" color="text.secondary">
- <strong>Lot {lot.lotNo}:</strong> {getStatusMessage(lot)}
- </Typography>
- </Box>
- ))}
- </Box>
- )}
-
-
-
- <TablePagination
- component="div"
- count={prepareLotTableData.length}
- page={lotTablePagingController.pageNum}
- rowsPerPage={lotTablePagingController.pageSize}
- onPageChange={handleLotTablePageChange}
- onRowsPerPageChange={handleLotTablePageSizeChange}
- rowsPerPageOptions={[10, 25, 50]}
- labelRowsPerPage={t("Rows per page")}
- labelDisplayedRows={({ from, to, count }) =>
- `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
- }
- />
-
- {/* ✅ QR Code Modal */}
- <QrCodeModal
- open={qrModalOpen}
- onClose={() => {
- setQrModalOpen(false);
- setSelectedLotForQr(null);
- stopScan();
- resetScan();
- }}
- lot={selectedLotForQr}
- onQrCodeSubmit={handleQrCodeSubmit}
- />
- </>
- );
- };
-
- export default LotTable;
|