Browse Source

quick fix

master
kelvin.yau 2 weeks ago
parent
commit
0d809c933b
1 changed files with 266 additions and 201 deletions
  1. +266
    -201
      src/components/PickOrderSearch/LotTable.tsx

+ 266
- 201
src/components/PickOrderSearch/LotTable.tsx View File

@@ -28,10 +28,10 @@ import { fetchStockInLineInfo } from "@/app/api/po/actions"; // Add this import
import PickExecutionForm from "./PickExecutionForm";
interface LotPickData {
id: number;
lotId: number | null;
lotNo: string | null;
lotId: number;
lotNo: string;
expiryDate: string;
location: string | null;
location: string;
stockUnit: string;
inQty: number;
availableQty: number;
@@ -45,7 +45,6 @@ interface LotPickData {
stockOutLineId?: number;
stockOutLineStatus?: string;
stockOutLineQty?: number;
noLot?: boolean;
}

interface PickQtyData {
@@ -61,7 +60,7 @@ interface LotTableProps {
pickQtyData: PickQtyData;
selectedLotRowId: string | 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;
onSubmitPickQty: (lineId: number, lotId: number) => void;
onCreateStockOutLine: (inventoryLotLineId: number) => void;
@@ -76,7 +75,6 @@ interface LotTableProps {
generateInputBody: () => any;
onDataRefresh: () => Promise<void>;
onLotDataRefresh: () => Promise<void>;
onIssueNoLotStockOutLine: (stockOutLineId: number) => void;
}

// QR Code Modal Component
@@ -238,7 +236,7 @@ const QrCodeModal: React.FC<{
const timer = setTimeout(() => {
setQrScanSuccess(true);
onQrCodeSubmit(lot.lotNo??'');
onQrCodeSubmit(lot.lotNo);
onClose();
setManualInput('');
setManualInputError(false);
@@ -303,7 +301,9 @@ const QrCodeModal: React.FC<{
</Box>
)}
{/* Manual Input with Submit-Triggered Helper Text */}
{false &&(
<Box sx={{ mb: 2 }}>
<Typography variant="body2" gutterBottom>
<strong>{t("Manual Input")}:</strong>
@@ -339,7 +339,8 @@ const QrCodeModal: React.FC<{
{t("Submit")}
</Button>
</Box>

)}
{/* Show QR Scan Status */}
{qrValues.length > 0 && (
<Box sx={{
@@ -390,28 +391,30 @@ const LotTable: React.FC<LotTableProps> = ({
generateInputBody,
onDataRefresh,
onLotDataRefresh,
onIssueNoLotStockOutLine,
}) => {
const { t } = useTranslation("pickOrder");

const calculateRemainingRequiredQty = useCallback((lot: LotPickData) => {
const requiredQty = lot.requiredQty || 0;
const stockOutLineQty = lot.stockOutLineQty || 0;
return Math.max(0, requiredQty - stockOutLineQty);
}, []);
// Add QR scanner context
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({});
// 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) => {

switch (lot.stockOutLineStatus?.toLowerCase()) {
case 'pending':
return t("Please finish QR code scanand pick order.");
@@ -428,35 +431,23 @@ const LotTable: React.FC<LotTableProps> = ({
default:
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(() => {
return lotData.map((lot) => ({
...lot,
id: lot.lotId ?? lot.id,
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,
@@ -471,25 +462,31 @@ const LotTable: React.FC<LotTableProps> = ({
pageSize: newPageSize,
});
}, []);

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);
}, [selectedRowId, pickQtyData]);

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) => {
if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
console.log(` QR Code verified for lot: ${lotNo}`);
@@ -522,7 +519,7 @@ const LotTable: React.FC<LotTableProps> = ({
if (selectedRowId) {
// Add a small delay to ensure the data refresh is complete
setTimeout(() => {
onPickQtyChange(selectedRowId, lotId ?? 0, requiredQty);
onPickQtyChange(selectedRowId, lotId, requiredQty);
console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
}, 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}`);
}
}, [selectedLotForQr, selectedRowId, onPickQtyChange]);
// PickExecutionForm 狀態與提交(保持原本邏輯)

// 添加 PickExecutionForm 相关的状态
const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<LotPickData | null>(null);

// 添加处理函数
const handlePickExecutionForm = useCallback((lot: LotPickData) => {
console.log("=== Pick Execution Form ===");
console.log("Lot data:", lot);
@@ -564,6 +563,8 @@ const LotTable: React.FC<LotTableProps> = ({
const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
try {
console.log("Pick execution form submitted:", data);
// 调用 API 提交数据
const result = await recordPickExecutionIssue(data);
console.log("Pick execution issue recorded:", result);
@@ -576,6 +577,7 @@ const LotTable: React.FC<LotTableProps> = ({
setPickExecutionFormOpen(false);
setSelectedLotForExecutionForm(null);
// 刷新数据
if (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("Original Available 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="center">{t("QR Code Scan")}</TableCell>*/}
{/*}
<TableCell align="center">{t("Reject")}</TableCell>
*/}

<TableCell align="center">{t("Action")}</TableCell>
</TableRow>
</TableHead>
@@ -617,7 +626,7 @@ const LotTable: React.FC<LotTableProps> = ({
) : (
paginatedLotTableData.map((lot, index) => (
<TableRow
key={lot.noLot ? `noLot_${lot.stockOutLineId}_${index}` : `lot_${lot.lotId}_${index}`}
key={lot.id}
sx={{
backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
@@ -627,30 +636,36 @@ const LotTable: React.FC<LotTableProps> = ({
}}
>
<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>
<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>
{/*
{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>
</TableCell>
<TableCell>{lot.expiryDate}</TableCell>
@@ -661,140 +676,154 @@ const LotTable: React.FC<LotTableProps> = ({
{(() => {
const inQty = lot.inQty || 0;
const outQty = lot.outQty || 0;
const result = inQty - outQty;
return result.toLocaleString();
})()}
</TableCell>
<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>
</TableRow>
))
@@ -802,14 +831,50 @@ const LotTable: React.FC<LotTableProps> = ({
</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>{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
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 && (
<PickExecutionForm
open={pickExecutionFormOpen}


Loading…
Cancel
Save