| @@ -28,10 +28,10 @@ import { fetchStockInLineInfo } from "@/app/api/po/actions"; // Add this import | |||||
| import PickExecutionForm from "./PickExecutionForm"; | import PickExecutionForm from "./PickExecutionForm"; | ||||
| interface LotPickData { | interface LotPickData { | ||||
| id: number; | id: number; | ||||
| lotId: number | null; | |||||
| lotNo: string | null; | |||||
| lotId: number; | |||||
| lotNo: string; | |||||
| expiryDate: string; | expiryDate: string; | ||||
| location: string | null; | |||||
| location: string; | |||||
| stockUnit: string; | stockUnit: string; | ||||
| inQty: number; | inQty: number; | ||||
| availableQty: number; | availableQty: number; | ||||
| @@ -45,7 +45,6 @@ interface LotPickData { | |||||
| stockOutLineId?: number; | stockOutLineId?: number; | ||||
| stockOutLineStatus?: string; | stockOutLineStatus?: string; | ||||
| stockOutLineQty?: number; | stockOutLineQty?: number; | ||||
| noLot?: boolean; | |||||
| } | } | ||||
| interface PickQtyData { | interface PickQtyData { | ||||
| @@ -61,7 +60,7 @@ interface LotTableProps { | |||||
| pickQtyData: PickQtyData; | pickQtyData: PickQtyData; | ||||
| selectedLotRowId: string | null; | selectedLotRowId: string | null; | ||||
| selectedLotId: number | null; | selectedLotId: number | null; | ||||
| onLotSelection: (uniqueLotId: string, lotId: number | null) => void; | |||||
| onLotSelection: (uniqueLotId: string, lotId: number) => void; | |||||
| onPickQtyChange: (lineId: number, lotId: number, value: number) => void; | onPickQtyChange: (lineId: number, lotId: number, value: number) => void; | ||||
| onSubmitPickQty: (lineId: number, lotId: number) => void; | onSubmitPickQty: (lineId: number, lotId: number) => void; | ||||
| onCreateStockOutLine: (inventoryLotLineId: number) => void; | onCreateStockOutLine: (inventoryLotLineId: number) => void; | ||||
| @@ -76,7 +75,6 @@ interface LotTableProps { | |||||
| generateInputBody: () => any; | generateInputBody: () => any; | ||||
| onDataRefresh: () => Promise<void>; | onDataRefresh: () => Promise<void>; | ||||
| onLotDataRefresh: () => Promise<void>; | onLotDataRefresh: () => Promise<void>; | ||||
| onIssueNoLotStockOutLine: (stockOutLineId: number) => void; | |||||
| } | } | ||||
| // QR Code Modal Component | // QR Code Modal Component | ||||
| @@ -238,7 +236,7 @@ const QrCodeModal: React.FC<{ | |||||
| const timer = setTimeout(() => { | const timer = setTimeout(() => { | ||||
| setQrScanSuccess(true); | setQrScanSuccess(true); | ||||
| onQrCodeSubmit(lot.lotNo??''); | |||||
| onQrCodeSubmit(lot.lotNo); | |||||
| onClose(); | onClose(); | ||||
| setManualInput(''); | setManualInput(''); | ||||
| setManualInputError(false); | setManualInputError(false); | ||||
| @@ -303,7 +301,9 @@ const QrCodeModal: React.FC<{ | |||||
| </Box> | </Box> | ||||
| )} | )} | ||||
| {/* Manual Input with Submit-Triggered Helper Text */} | {/* Manual Input with Submit-Triggered Helper Text */} | ||||
| {false &&( | |||||
| <Box sx={{ mb: 2 }}> | <Box sx={{ mb: 2 }}> | ||||
| <Typography variant="body2" gutterBottom> | <Typography variant="body2" gutterBottom> | ||||
| <strong>{t("Manual Input")}:</strong> | <strong>{t("Manual Input")}:</strong> | ||||
| @@ -339,7 +339,8 @@ const QrCodeModal: React.FC<{ | |||||
| {t("Submit")} | {t("Submit")} | ||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| )} | |||||
| {/* Show QR Scan Status */} | {/* Show QR Scan Status */} | ||||
| {qrValues.length > 0 && ( | {qrValues.length > 0 && ( | ||||
| <Box sx={{ | <Box sx={{ | ||||
| @@ -390,28 +391,30 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| generateInputBody, | generateInputBody, | ||||
| onDataRefresh, | onDataRefresh, | ||||
| onLotDataRefresh, | onLotDataRefresh, | ||||
| onIssueNoLotStockOutLine, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| const calculateRemainingRequiredQty = useCallback((lot: LotPickData) => { | const calculateRemainingRequiredQty = useCallback((lot: LotPickData) => { | ||||
| const requiredQty = lot.requiredQty || 0; | const requiredQty = lot.requiredQty || 0; | ||||
| const stockOutLineQty = lot.stockOutLineQty || 0; | const stockOutLineQty = lot.stockOutLineQty || 0; | ||||
| return Math.max(0, requiredQty - stockOutLineQty); | return Math.max(0, requiredQty - stockOutLineQty); | ||||
| }, []); | }, []); | ||||
| // Add QR scanner context | |||||
| const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | ||||
| const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({}); | const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({}); | ||||
| // Add state for QR input modal | |||||
| const [qrModalOpen, setQrModalOpen] = useState(false); | const [qrModalOpen, setQrModalOpen] = useState(false); | ||||
| const [selectedLotForQr, setSelectedLotForQr] = useState<LotPickData | null>(null); | const [selectedLotForQr, setSelectedLotForQr] = useState<LotPickData | null>(null); | ||||
| const [manualQrInput, setManualQrInput] = useState<string>(''); | const [manualQrInput, setManualQrInput] = useState<string>(''); | ||||
| // 分页控制器 | |||||
| const [lotTablePagingController, setLotTablePagingController] = useState({ | const [lotTablePagingController, setLotTablePagingController] = useState({ | ||||
| pageNum: 0, | pageNum: 0, | ||||
| pageSize: 10, | pageSize: 10, | ||||
| }); | }); | ||||
| // 添加状态消息生成函数 | |||||
| const getStatusMessage = useCallback((lot: LotPickData) => { | const getStatusMessage = useCallback((lot: LotPickData) => { | ||||
| switch (lot.stockOutLineStatus?.toLowerCase()) { | switch (lot.stockOutLineStatus?.toLowerCase()) { | ||||
| case 'pending': | case 'pending': | ||||
| return t("Please finish QR code scanand pick order."); | return t("Please finish QR code scanand pick order."); | ||||
| @@ -428,35 +431,23 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| default: | default: | ||||
| return t("Please finish QR code scan and pick order."); | return t("Please finish QR code scan and pick order."); | ||||
| } | } | ||||
| }, [t]); | |||||
| const handleOpenQrModal = useCallback((lot: LotPickData) => { | |||||
| setSelectedLotForQr(lot); | |||||
| setManualQrInput(lot.lotNo ?? ""); | |||||
| setValidationErrors({}); | |||||
| setQrModalOpen(true); | |||||
| resetScan(); | |||||
| startScan(); | |||||
| }, [startScan, resetScan]); | |||||
| const handleCloseQrModal = useCallback(() => { | |||||
| setQrModalOpen(false); | |||||
| setSelectedLotForQr(null); | |||||
| stopScan(); | |||||
| resetScan(); | |||||
| }, [stopScan, resetScan]); | |||||
| }, []); | |||||
| const prepareLotTableData = useMemo(() => { | const prepareLotTableData = useMemo(() => { | ||||
| return lotData.map((lot) => ({ | return lotData.map((lot) => ({ | ||||
| ...lot, | ...lot, | ||||
| id: lot.lotId ?? lot.id, | |||||
| id: lot.lotId, | |||||
| })); | })); | ||||
| }, [lotData]); | }, [lotData]); | ||||
| // 分页数据 | |||||
| const paginatedLotTableData = useMemo(() => { | const paginatedLotTableData = useMemo(() => { | ||||
| const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize; | const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize; | ||||
| const endIndex = startIndex + lotTablePagingController.pageSize; | const endIndex = startIndex + lotTablePagingController.pageSize; | ||||
| return prepareLotTableData.slice(startIndex, endIndex); | return prepareLotTableData.slice(startIndex, endIndex); | ||||
| }, [prepareLotTableData, lotTablePagingController]); | }, [prepareLotTableData, lotTablePagingController]); | ||||
| // 分页处理函数 | |||||
| const handleLotTablePageChange = useCallback((event: unknown, newPage: number) => { | const handleLotTablePageChange = useCallback((event: unknown, newPage: number) => { | ||||
| setLotTablePagingController(prev => ({ | setLotTablePagingController(prev => ({ | ||||
| ...prev, | ...prev, | ||||
| @@ -471,25 +462,31 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| pageSize: newPageSize, | pageSize: newPageSize, | ||||
| }); | }); | ||||
| }, []); | }, []); | ||||
| const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { | const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { | ||||
| if (!selectedRowId || lot.noLot) return lot.availableQty; | |||||
| const actualPickQty = pickQtyData[selectedRowId]?.[lot.lotId ?? 0] || 0; | |||||
| const remainingQty = (lot.inQty || 0) - (lot.outQty || 0) - actualPickQty; | |||||
| if (!selectedRowId) return lot.availableQty; | |||||
| const lactualPickQty = lot.actualPickQty || 0; | |||||
| const actualPickQty = pickQtyData[selectedRowId]?.[lot.lotId] || 0; | |||||
| const remainingQty = lot.inQty - lot.outQty-actualPickQty; | |||||
| // Ensure it doesn't go below 0 | |||||
| return Math.max(0, remainingQty); | return Math.max(0, remainingQty); | ||||
| }, [selectedRowId, pickQtyData]); | }, [selectedRowId, pickQtyData]); | ||||
| const validatePickQty = useCallback((lot: LotPickData, inputValue: number) => { | const validatePickQty = useCallback((lot: LotPickData, inputValue: number) => { | ||||
| const maxAllowed = Math.min(calculateRemainingAvailableQty(lot), calculateRemainingRequiredQty(lot)); | |||||
| if (inputValue > maxAllowed) { | |||||
| return `${t('Input quantity cannot exceed')} ${maxAllowed}`; | |||||
| } | |||||
| if (inputValue < 0) { | |||||
| return t('Quantity cannot be negative'); | |||||
| } | |||||
| return null; | |||||
| }, [calculateRemainingAvailableQty, calculateRemainingRequiredQty, t]); | |||||
| const maxAllowed = Math.min(calculateRemainingAvailableQty(lot), calculateRemainingRequiredQty(lot)); | |||||
| if (inputValue > maxAllowed) { | |||||
| return `${t('Input quantity cannot exceed')} ${maxAllowed}`; | |||||
| } | |||||
| if (inputValue < 0) { | |||||
| return t('Quantity cannot be negative'); | |||||
| } | |||||
| return null; | |||||
| }, [calculateRemainingAvailableQty, calculateRemainingRequiredQty, t]); | |||||
| // Handle QR code submission | |||||
| const handleQrCodeSubmit = useCallback(async (lotNo: string) => { | const handleQrCodeSubmit = useCallback(async (lotNo: string) => { | ||||
| if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { | if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { | ||||
| console.log(` QR Code verified for lot: ${lotNo}`); | console.log(` QR Code verified for lot: ${lotNo}`); | ||||
| @@ -522,7 +519,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| if (selectedRowId) { | if (selectedRowId) { | ||||
| // Add a small delay to ensure the data refresh is complete | // Add a small delay to ensure the data refresh is complete | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| onPickQtyChange(selectedRowId, lotId ?? 0, requiredQty); | |||||
| onPickQtyChange(selectedRowId, lotId, requiredQty); | |||||
| console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); | console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); | ||||
| }, 500); // 500ms delay to ensure refresh is complete | }, 500); // 500ms delay to ensure refresh is complete | ||||
| } | } | ||||
| @@ -540,10 +537,12 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| alert(`QR scan mismatch! Expected: ${selectedLotForQr?.lotNo}, Scanned: ${lotNo}`); | alert(`QR scan mismatch! Expected: ${selectedLotForQr?.lotNo}, Scanned: ${lotNo}`); | ||||
| } | } | ||||
| }, [selectedLotForQr, selectedRowId, onPickQtyChange]); | }, [selectedLotForQr, selectedRowId, onPickQtyChange]); | ||||
| // PickExecutionForm 狀態與提交(保持原本邏輯) | |||||
| // 添加 PickExecutionForm 相关的状态 | |||||
| const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); | const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); | ||||
| const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<LotPickData | null>(null); | const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<LotPickData | null>(null); | ||||
| // 添加处理函数 | |||||
| const handlePickExecutionForm = useCallback((lot: LotPickData) => { | const handlePickExecutionForm = useCallback((lot: LotPickData) => { | ||||
| console.log("=== Pick Execution Form ==="); | console.log("=== Pick Execution Form ==="); | ||||
| console.log("Lot data:", lot); | console.log("Lot data:", lot); | ||||
| @@ -564,6 +563,8 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| const handlePickExecutionFormSubmit = useCallback(async (data: any) => { | const handlePickExecutionFormSubmit = useCallback(async (data: any) => { | ||||
| try { | try { | ||||
| console.log("Pick execution form submitted:", data); | console.log("Pick execution form submitted:", data); | ||||
| // 调用 API 提交数据 | |||||
| const result = await recordPickExecutionIssue(data); | const result = await recordPickExecutionIssue(data); | ||||
| console.log("Pick execution issue recorded:", result); | console.log("Pick execution issue recorded:", result); | ||||
| @@ -576,6 +577,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| setPickExecutionFormOpen(false); | setPickExecutionFormOpen(false); | ||||
| setSelectedLotForExecutionForm(null); | setSelectedLotForExecutionForm(null); | ||||
| // 刷新数据 | |||||
| if (onDataRefresh) { | if (onDataRefresh) { | ||||
| await onDataRefresh(); | await onDataRefresh(); | ||||
| } | } | ||||
| @@ -601,7 +603,14 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | ||||
| <TableCell align="right">{t("Original Available Qty")}</TableCell> | <TableCell align="right">{t("Original Available Qty")}</TableCell> | ||||
| <TableCell align="center">{t("Lot Actual Pick Qty")}</TableCell> | <TableCell align="center">{t("Lot Actual Pick Qty")}</TableCell> | ||||
| {/*<TableCell align="right">{t("Available Lot")}</TableCell>*/} | |||||
| <TableCell align="right">{t("Remaining Available Qty")}</TableCell> | <TableCell align="right">{t("Remaining Available Qty")}</TableCell> | ||||
| {/*<TableCell align="center">{t("QR Code Scan")}</TableCell>*/} | |||||
| {/*} | |||||
| <TableCell align="center">{t("Reject")}</TableCell> | |||||
| */} | |||||
| <TableCell align="center">{t("Action")}</TableCell> | <TableCell align="center">{t("Action")}</TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| @@ -617,7 +626,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| ) : ( | ) : ( | ||||
| paginatedLotTableData.map((lot, index) => ( | paginatedLotTableData.map((lot, index) => ( | ||||
| <TableRow | <TableRow | ||||
| key={lot.noLot ? `noLot_${lot.stockOutLineId}_${index}` : `lot_${lot.lotId}_${index}`} | |||||
| key={lot.id} | |||||
| sx={{ | sx={{ | ||||
| backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit', | backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit', | ||||
| opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1, | opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1, | ||||
| @@ -627,30 +636,36 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| }} | }} | ||||
| > | > | ||||
| <TableCell> | <TableCell> | ||||
| <Checkbox | |||||
| checked={selectedLotRowId === `row_${index}`} | |||||
| onChange={() => { | |||||
| if (!lot.noLot && lot.lotId != null) { | |||||
| onLotSelection(`row_${index}`, lot.lotId); | |||||
| } | |||||
| }} | |||||
| disabled={ | |||||
| lot.noLot || // 無批次行不支援勾選 | |||||
| lot.lotAvailability === 'expired' || | |||||
| lot.lotAvailability === 'status_unavailable' || | |||||
| lot.lotAvailability === 'rejected' | |||||
| } | |||||
| value={`row_${index}`} | |||||
| name="lot-selection" | |||||
| /> | |||||
| </TableCell> | |||||
| <Checkbox | |||||
| checked={selectedLotRowId === `row_${index}`} | |||||
| onChange={() => onLotSelection(`row_${index}`, lot.lotId)} | |||||
| // 禁用 rejected、expired 和 status_unavailable 的批次 | |||||
| disabled={lot.lotAvailability === 'expired' || | |||||
| lot.lotAvailability === 'status_unavailable' || | |||||
| lot.lotAvailability === 'rejected'} // 添加 rejected | |||||
| value={`row_${index}`} | |||||
| name="lot-selection" | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | <TableCell> | ||||
| <Box> | <Box> | ||||
| <Typography> | |||||
| {lot.noLot | |||||
| ? t('⚠️ No Stock Available') | |||||
| : lot.lotNo} | |||||
| <Typography | |||||
| sx={{ | |||||
| color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit', | |||||
| opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1 | |||||
| }} | |||||
| > | |||||
| {lot.lotNo} | |||||
| </Typography> | </Typography> | ||||
| {/* | |||||
| {lot.lotAvailability !== 'available' && ( | |||||
| <Typography variant="caption" color="error" display="block"> | |||||
| ({lot.lotAvailability === 'expired' ? 'Expired' : | |||||
| lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' : | |||||
| lot.lotAvailability === 'rejected' ? 'Rejected' : // 添加 rejected 显示 | |||||
| 'Unavailable'}) | |||||
| </Typography> | |||||
| )} */} | |||||
| </Box> | </Box> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell>{lot.expiryDate}</TableCell> | <TableCell>{lot.expiryDate}</TableCell> | ||||
| @@ -661,140 +676,154 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| {(() => { | {(() => { | ||||
| const inQty = lot.inQty || 0; | const inQty = lot.inQty || 0; | ||||
| const outQty = lot.outQty || 0; | const outQty = lot.outQty || 0; | ||||
| const result = inQty - outQty; | const result = inQty - outQty; | ||||
| return result.toLocaleString(); | return result.toLocaleString(); | ||||
| })()} | })()} | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell align="center"> | <TableCell align="center"> | ||||
| {lot.stockOutLineStatus?.toLowerCase() === 'pending' ? ( | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| startIcon={<QrCodeIcon />} | |||||
| onClick={() => handleOpenQrModal(lot)} | |||||
| disabled={ | |||||
| lot.lotAvailability === 'expired' || | |||||
| lot.lotAvailability === 'status_unavailable' || | |||||
| lot.lotAvailability === 'rejected' || | |||||
| selectedLotRowId !== `row_${index}` | |||||
| } | |||||
| sx={{ fontSize: '0.7rem', minHeight: 40, minWidth: 100 }} | |||||
| title={ | |||||
| selectedLotRowId !== `row_${index}` | |||||
| ? t("Please select this lot first to enable QR scanning") | |||||
| : t("Click to scan QR code") | |||||
| } | |||||
| > | |||||
| {t("Scan")} | |||||
| </Button> | |||||
| ) : ( | |||||
| <Stack direction="row" spacing={1} alignItems="center" justifyContent="center"> | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| value={ | |||||
| selectedRowId && lot.lotId != null | |||||
| ? pickQtyData[selectedRowId]?.[lot.lotId] ?? '' | |||||
| : '' | |||||
| } | |||||
| onChange={(e) => { | |||||
| if (selectedRowId && lot.lotId != null) { | |||||
| onPickQtyChange(selectedRowId, lot.lotId, Number(e.target.value) || 0); | |||||
| } | |||||
| }} | |||||
| disabled={ | |||||
| lot.lotAvailability === 'expired' || | |||||
| lot.lotAvailability === 'status_unavailable' || | |||||
| lot.lotAvailability === 'rejected' || | |||||
| selectedLotRowId !== `row_${index}` || | |||||
| lot.stockOutLineStatus === 'completed' | |||||
| } | |||||
| error={!!validationErrors[`lot_${lot.lotId}`]} | |||||
| helperText={validationErrors[`lot_${lot.lotId}`]} | |||||
| inputProps={{ min: 0, max: calculateRemainingRequiredQty(lot) }} | |||||
| sx={{ width: 70 }} | |||||
| placeholder="0" | |||||
| /> | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={() => handlePickExecutionForm(lot)} | |||||
| disabled={ | |||||
| lot.lotAvailability === 'expired' || | |||||
| lot.lotAvailability === 'status_unavailable' || | |||||
| lot.lotAvailability === 'rejected' || | |||||
| selectedLotRowId !== `row_${index}` | |||||
| } | |||||
| sx={{ fontSize: '0.7rem', minWidth: 70, borderColor: 'warning.main', color: 'warning.main' }} | |||||
| title={t("Report missing or bad items")} | |||||
| > | |||||
| {t("Issue")} | |||||
| </Button> | |||||
| </Stack> | |||||
| )} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {calculateRemainingAvailableQty(lot).toLocaleString()} | |||||
| </TableCell> | |||||
| {/* Show QR Scan Button if not scanned, otherwise show TextField + Pick Form */} | |||||
| {lot.stockOutLineStatus?.toLowerCase() === 'pending' ? ( | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={() => { | |||||
| setSelectedLotForQr(lot); | |||||
| setQrModalOpen(true); | |||||
| resetScan(); | |||||
| }} | |||||
| disabled={ | |||||
| (lot.lotAvailability === 'expired' || | |||||
| lot.lotAvailability === 'status_unavailable' || | |||||
| lot.lotAvailability === 'rejected') || | |||||
| selectedLotRowId !== `row_${index}` | |||||
| } | |||||
| sx={{ | |||||
| fontSize: '0.7rem', | |||||
| py: 0.5, | |||||
| minHeight: '40px', | |||||
| whiteSpace: 'nowrap', | |||||
| minWidth: '80px', | |||||
| opacity: selectedLotRowId === `row_${index}` ? 1 : 0.5 | |||||
| }} | |||||
| startIcon={<QrCodeIcon />} | |||||
| title={ | |||||
| selectedLotRowId !== `row_${index}` | |||||
| ? "Please select this lot first to enable QR scanning" | |||||
| : "Click to scan QR code" | |||||
| } | |||||
| > | |||||
| {t("Scan")} | |||||
| </Button> | |||||
| ) : ( | |||||
| <Stack | |||||
| direction="row" | |||||
| spacing={1} | |||||
| alignItems="center" | |||||
| justifyContent="center" // 添加水平居中 | |||||
| sx={{ | |||||
| width: '100%', // 确保占满整个单元格宽度 | |||||
| minHeight: '40px' // 设置最小高度确保垂直居中 | |||||
| }} | |||||
| > | |||||
| {/* 恢复 TextField 用于正常数量输入 */} | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| value={pickQtyData[selectedRowId!]?.[lot.lotId] || ''} | |||||
| onChange={(e) => { | |||||
| if (selectedRowId) { | |||||
| const inputValue = parseFloat(e.target.value) || 0; | |||||
| const maxAllowed = Math.min(calculateRemainingAvailableQty(lot), calculateRemainingRequiredQty(lot)); | |||||
| onPickQtyChange(selectedRowId, lot.lotId, inputValue); | |||||
| } | |||||
| }} | |||||
| disabled={ | |||||
| (lot.lotAvailability === 'expired' || | |||||
| lot.lotAvailability === 'status_unavailable' || | |||||
| lot.lotAvailability === 'rejected') || | |||||
| selectedLotRowId !== `row_${index}` || | |||||
| lot.stockOutLineStatus === 'completed' | |||||
| } | |||||
| error={!!validationErrors[`lot_${lot.lotId}`]} | |||||
| helperText={validationErrors[`lot_${lot.lotId}`]} | |||||
| inputProps={{ | |||||
| min: 0, | |||||
| max: calculateRemainingRequiredQty(lot), | |||||
| step: 0.01 | |||||
| }} | |||||
| sx={{ | |||||
| width: '60px', | |||||
| height: '28px', | |||||
| '& .MuiInputBase-input': { | |||||
| fontSize: '0.7rem', | |||||
| textAlign: 'center', | |||||
| padding: '6px 8px' | |||||
| } | |||||
| }} | |||||
| placeholder="0" | |||||
| /> | |||||
| {/* 添加 Pick Form 按钮用于问题情况 */} | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={() => handlePickExecutionForm(lot)} | |||||
| disabled={ | |||||
| (lot.lotAvailability === 'expired' || | |||||
| lot.lotAvailability === 'status_unavailable' || | |||||
| lot.lotAvailability === 'rejected') || | |||||
| selectedLotRowId !== `row_${index}` | |||||
| } | |||||
| sx={{ | |||||
| fontSize: '0.7rem', | |||||
| py: 0.5, | |||||
| minHeight: '28px', | |||||
| minWidth: '60px', | |||||
| borderColor: 'warning.main', | |||||
| color: 'warning.main' | |||||
| }} | |||||
| title="Report missing or bad items" | |||||
| > | |||||
| {t("Issue")} | |||||
| </Button> | |||||
| </Stack> | |||||
| )} | |||||
| </TableCell> | |||||
| {/*<TableCell align="right">{lot.availableQty.toLocaleString()}</TableCell>*/} | |||||
| <TableCell align="right">{calculateRemainingAvailableQty(lot).toLocaleString()}</TableCell> | |||||
| {/* ✅ Action 欄位:區分 noLot / 正常 lot */} | |||||
| <TableCell align="center"> | |||||
| <Stack direction="column" spacing={1} alignItems="center"> | |||||
| {lot.noLot ? ( | |||||
| // 沒有批次:只允許 Issue(報告 miss) | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={() => { | |||||
| if (lot.stockOutLineId) { | |||||
| onIssueNoLotStockOutLine(lot.stockOutLineId); | |||||
| } | |||||
| }} | |||||
| disabled={ | |||||
| lot.stockOutLineStatus === 'completed' || | |||||
| lot.lotAvailability === 'rejected' | |||||
| } | |||||
| sx={{ | |||||
| fontSize: '0.7rem', | |||||
| py: 0.5, | |||||
| minHeight: '28px', | |||||
| minWidth: '60px', | |||||
| borderColor: 'warning.main', | |||||
| color: 'warning.main' | |||||
| }} | |||||
| title="Report missing items (no lot available)" | |||||
| > | |||||
| {t("Issue")} | |||||
| </Button> | |||||
| ) : ( | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => { | |||||
| if (selectedRowId && lot.lotId != null) { | |||||
| onSubmitPickQty(selectedRowId, lot.lotId); | |||||
| } | |||||
| }} | |||||
| disabled={ | |||||
| lot.lotAvailability === 'expired' || | |||||
| lot.lotAvailability === 'status_unavailable' || | |||||
| lot.lotAvailability === 'rejected' || | |||||
| !selectedRowId || | |||||
| !pickQtyData[selectedRowId]?.[lot.lotId ?? 0] || | |||||
| !lot.stockOutLineStatus || | |||||
| !['pending','checked', 'partially_completed'].includes( | |||||
| lot.stockOutLineStatus.toLowerCase() | |||||
| ) | |||||
| } | |||||
| sx={{ | |||||
| fontSize: '0.75rem', | |||||
| py: 0.5, | |||||
| minHeight: '28px' | |||||
| }} | |||||
| > | |||||
| {t("Submit")} | |||||
| </Button> | |||||
| )} | |||||
| </Stack> | |||||
| <TableCell align="center"> | |||||
| <Stack direction="column" spacing={1} alignItems="center"> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => { | |||||
| if (selectedRowId) { | |||||
| onSubmitPickQty(selectedRowId, lot.lotId); | |||||
| } | |||||
| }} | |||||
| disabled={ | |||||
| (lot.lotAvailability === 'expired' || | |||||
| lot.lotAvailability === 'status_unavailable' || | |||||
| lot.lotAvailability === 'rejected') || // 添加 rejected | |||||
| !pickQtyData[selectedRowId!]?.[lot.lotId] || | |||||
| !lot.stockOutLineStatus || | |||||
| !['pending','checked', 'partially_completed'].includes(lot.stockOutLineStatus.toLowerCase()) | |||||
| } | |||||
| // Allow submission for available AND insufficient_stock lots | |||||
| sx={{ | |||||
| fontSize: '0.75rem', | |||||
| py: 0.5, | |||||
| minHeight: '28px' | |||||
| }} | |||||
| > | |||||
| {t("Submit")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| )) | )) | ||||
| @@ -802,14 +831,50 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| </TableContainer> | </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>{t("Lot")} {lot.lotNo}:</strong> {getStatusMessage(lot)} | |||||
| </Typography> | |||||
| </Box> | |||||
| ))} | |||||
| </Box> | |||||
| )} | |||||
| {/* Status message & pagination & modals 保持原有程式不變(略) */} | |||||
| <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 | <QrCodeModal | ||||
| open={qrModalOpen} | |||||
| onClose={handleCloseQrModal} | |||||
| lot={selectedLotForQr} | |||||
| onQrCodeSubmit={handleQrCodeSubmit} | |||||
| /> | |||||
| open={qrModalOpen} | |||||
| onClose={() => { | |||||
| setQrModalOpen(false); | |||||
| setSelectedLotForQr(null); | |||||
| stopScan(); | |||||
| resetScan(); | |||||
| }} | |||||
| lot={selectedLotForQr} | |||||
| onQrCodeSubmit={handleQrCodeSubmit} | |||||
| /> | |||||
| {/* Pick Execution Form Modal */} | |||||
| {pickExecutionFormOpen && selectedLotForExecutionForm && selectedRow && ( | {pickExecutionFormOpen && selectedLotForExecutionForm && selectedRow && ( | ||||
| <PickExecutionForm | <PickExecutionForm | ||||
| open={pickExecutionFormOpen} | open={pickExecutionFormOpen} | ||||