@@ -15,11 +15,13 @@ import { | |||||
TextField, | TextField, | ||||
Typography, | Typography, | ||||
TablePagination, | TablePagination, | ||||
Modal, | |||||
} from "@mui/material"; | } from "@mui/material"; | ||||
import { useCallback, useMemo, useState } from "react"; | |||||
import { useCallback, useMemo, useState, useEffect } from "react"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import QrCodeIcon from '@mui/icons-material/QrCode'; | import QrCodeIcon from '@mui/icons-material/QrCode'; | ||||
import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; | import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; | ||||
import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; | |||||
interface LotPickData { | interface LotPickData { | ||||
id: number; | id: number; | ||||
@@ -63,6 +65,164 @@ interface LotTableProps { | |||||
generateInputBody: () => any; | 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> = ({ | const LotTable: React.FC<LotTableProps> = ({ | ||||
lotData, | lotData, | ||||
selectedRowId, | selectedRowId, | ||||
@@ -83,6 +243,14 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
}) => { | }) => { | ||||
const { t } = useTranslation("pickOrder"); | 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({ | const [lotTablePagingController, setLotTablePagingController] = useState({ | ||||
pageNum: 0, | pageNum: 0, | ||||
@@ -95,10 +263,10 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
return "Please finish QR code scan, QC check and pick order."; | return "Please finish QR code scan, QC check and pick order."; | ||||
} | } | ||||
switch (lot.stockOutLineStatus?.toUpperCase()) { | |||||
case 'PENDING': | |||||
switch (lot.stockOutLineStatus?.toLowerCase()) { | |||||
case 'pending': | |||||
return "Please finish QC check and pick order."; | return "Please finish QC check and pick order."; | ||||
case 'COMPLETE': | |||||
case 'completed': | |||||
return "Please submit the pick order."; | return "Please submit the pick order."; | ||||
case 'unavailable': | case 'unavailable': | ||||
return "This order is insufficient, please pick another lot."; | return "This order is insufficient, please pick another lot."; | ||||
@@ -137,6 +305,23 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
}); | }); | ||||
}, []); | }, []); | ||||
// ✅ 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 ( | return ( | ||||
<> | <> | ||||
<TableContainer component={Paper}> | <TableContainer component={Paper}> | ||||
@@ -198,26 +383,47 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
{/* QR Code Scan Button */} | {/* QR Code Scan Button */} | ||||
<TableCell align="center"> | <TableCell align="center"> | ||||
<Button | |||||
variant="outlined" | |||||
size="small" | |||||
onClick={() => { | |||||
onCreateStockOutLine(lot.lotId); | |||||
onLotSelectForInput(lot); // Show input body when button is clicked | |||||
}} | |||||
// ✅ Allow creation for available AND insufficient_stock lots | |||||
disabled={(lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') || Boolean(lot.stockOutLineId)} | |||||
sx={{ | |||||
fontSize: '0.7rem', | |||||
py: 0.5, | |||||
minHeight: '28px', | |||||
whiteSpace: 'nowrap', | |||||
minWidth: '40px' | |||||
}} | |||||
startIcon={<QrCodeIcon />} // ✅ Add QR code icon | |||||
> | |||||
{lot.stockOutLineId ? t("Scanned") : t("Scan")} | |||||
</Button> | |||||
<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> | </TableCell> | ||||
{/* QC Check Button */} | {/* QC Check Button */} | ||||
@@ -320,6 +526,19 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | `${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} | |||||
/> | |||||
</> | </> | ||||
); | ); | ||||
}; | }; | ||||
@@ -490,20 +490,22 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
}, [selectedLotRowId]); | }, [selectedLotRowId]); | ||||
// ✅ Add function to handle row selection that resets lot selection | // ✅ Add function to handle row selection that resets lot selection | ||||
const handleRowSelect = useCallback(async (lineId: number) => { | |||||
const handleRowSelect = useCallback(async (lineId: number, preserveLotSelection: boolean = false) => { | |||||
setSelectedRowId(lineId); | setSelectedRowId(lineId); | ||||
// ✅ Reset lot selection when changing pick order line | |||||
setSelectedLotRowId(null); | |||||
setSelectedLotId(null); | |||||
// ✅ Only reset lot selection if not preserving | |||||
if (!preserveLotSelection) { | |||||
setSelectedLotRowId(null); | |||||
setSelectedLotId(null); | |||||
} | |||||
try { | try { | ||||
const lotDetails = await fetchPickOrderLineLotDetails(lineId); | const lotDetails = await fetchPickOrderLineLotDetails(lineId); | ||||
console.log("Lot details from API:", lotDetails); | console.log("Lot details from API:", lotDetails); | ||||
const realLotData: LotPickData[] = lotDetails.map((lot: any) => ({ | const realLotData: LotPickData[] = lotDetails.map((lot: any) => ({ | ||||
id: lot.lotId, // This is actually ill.id (inventory lot line ID) | |||||
lotId: lot.lotId, // This should be the unique inventory lot line ID | |||||
id: lot.id, // This should be the unique row ID for the table | |||||
lotId: lot.lotId, // This is the inventory lot line ID | |||||
lotNo: lot.lotNo, | lotNo: lot.lotNo, | ||||
expiryDate: lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A', | expiryDate: lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A', | ||||
location: lot.location, | location: lot.location, | ||||
@@ -513,7 +515,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
actualPickQty: lot.actualPickQty || 0, | actualPickQty: lot.actualPickQty || 0, | ||||
lotStatus: lot.lotStatus, | lotStatus: lot.lotStatus, | ||||
lotAvailability: lot.lotAvailability, | lotAvailability: lot.lotAvailability, | ||||
// ✅ Add StockOutLine fields | |||||
stockOutLineId: lot.stockOutLineId, | stockOutLineId: lot.stockOutLineId, | ||||
stockOutLineStatus: lot.stockOutLineStatus, | stockOutLineStatus: lot.stockOutLineStatus, | ||||
stockOutLineQty: lot.stockOutLineQty | stockOutLineQty: lot.stockOutLineQty | ||||
@@ -545,6 +546,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
pickOrderCode: pickOrder.code, | pickOrderCode: pickOrder.code, | ||||
targetDate: formattedTargetDate, // ✅ 使用 dayjs 格式化的日期 | targetDate: formattedTargetDate, // ✅ 使用 dayjs 格式化的日期 | ||||
balanceToPick: balanceToPick, | balanceToPick: balanceToPick, | ||||
pickedQty: line.pickedQty, | |||||
// 确保 availableQty 不为 null | // 确保 availableQty 不为 null | ||||
availableQty: availableQty, | availableQty: availableQty, | ||||
}; | }; | ||||
@@ -675,6 +677,10 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
} | } | ||||
try { | try { | ||||
// ✅ Store current lot selection before refresh | |||||
const currentSelectedLotRowId = selectedLotRowId; | |||||
const currentSelectedLotId = selectedLotId; | |||||
const stockOutLineData: CreateStockOutLine = { | const stockOutLineData: CreateStockOutLine = { | ||||
consoCode: pickOrderDetails.consoCode, | consoCode: pickOrderDetails.consoCode, | ||||
pickOrderLineId: selectedRowId, | pickOrderLineId: selectedRowId, | ||||
@@ -692,19 +698,57 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
if (result) { | if (result) { | ||||
console.log("Stock out line created successfully:", result); | console.log("Stock out line created successfully:", result); | ||||
//alert(`Stock out line created successfully! ID: ${result.id}`); | |||||
// ✅ Don't refresh immediately - let user see the result first | |||||
// ✅ Auto-refresh data after successful creation | |||||
console.log("🔄 Refreshing data after stock out line creation..."); | |||||
try { | |||||
// ✅ Refresh lot data for the selected row (maintains selection) | |||||
if (selectedRowId) { | |||||
await handleRowSelect(selectedRowId, true); // ✅ Preserve lot selection | |||||
} | |||||
// ✅ Refresh main pick order details | |||||
await handleFetchAllPickOrderDetails(); | |||||
console.log("✅ Data refresh completed - lot selection maintained!"); | |||||
} catch (refreshError) { | |||||
console.error("❌ Error refreshing data:", refreshError); | |||||
} | |||||
setShowInputBody(false); // Hide preview after successful creation | setShowInputBody(false); // Hide preview after successful creation | ||||
} else { | } else { | ||||
console.error("Failed to create stock out line: No response"); | console.error("Failed to create stock out line: No response"); | ||||
//alert("Failed to create stock out line: No response"); | |||||
} | } | ||||
} catch (error) { | } catch (error) { | ||||
console.error("Error creating stock out line:", error); | console.error("Error creating stock out line:", error); | ||||
//alert("Error creating stock out line. Please try again."); | |||||
} | } | ||||
}, [selectedRowId, pickOrderDetails?.consoCode]); | |||||
}, [selectedRowId, pickOrderDetails?.consoCode, handleRowSelect, handleFetchAllPickOrderDetails, selectedLotRowId, selectedLotId]); | |||||
// ✅ New function to refresh data while preserving lot selection | |||||
const handleRefreshDataPreserveSelection = useCallback(async () => { | |||||
if (!selectedRowId) return; | |||||
// ✅ Store current lot selection | |||||
const currentSelectedLotRowId = selectedLotRowId; | |||||
const currentSelectedLotId = selectedLotId; | |||||
try { | |||||
// ✅ Refresh lot data | |||||
await handleRowSelect(selectedRowId, true); // ✅ Preserve selection | |||||
// ✅ Refresh main pick order details | |||||
await handleFetchAllPickOrderDetails(); | |||||
// ✅ Restore lot selection | |||||
setSelectedLotRowId(currentSelectedLotRowId); | |||||
setSelectedLotId(currentSelectedLotId); | |||||
console.log("✅ Data refreshed with selection preserved"); | |||||
} catch (error) { | |||||
console.error("❌ Error refreshing data:", error); | |||||
} | |||||
}, [selectedRowId, selectedLotRowId, selectedLotId, handleRowSelect, handleFetchAllPickOrderDetails]); | |||||
// 自定义主表格组件 | // 自定义主表格组件 | ||||
const CustomMainTable = () => { | const CustomMainTable = () => { | ||||
@@ -740,7 +784,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
const availableQty = line.availableQty ?? 0; | const availableQty = line.availableQty ?? 0; | ||||
const balanceToPick = Math.max(0, availableQty - line.requiredQty); // 确保不为负数 | const balanceToPick = Math.max(0, availableQty - line.requiredQty); // 确保不为负数 | ||||
const totalPickedQty = getTotalPickedQty(line.id); | const totalPickedQty = getTotalPickedQty(line.id); | ||||
const actualPickedQty = line.pickedQty ?? 0; | |||||
return ( | return ( | ||||
<TableRow | <TableRow | ||||
key={line.id} | key={line.id} | ||||
@@ -777,7 +821,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
}}> | }}> | ||||
{availableQty.toLocaleString()} {/* 添加千位分隔符 */} | {availableQty.toLocaleString()} {/* 添加千位分隔符 */} | ||||
</TableCell> | </TableCell> | ||||
<TableCell align="right">{totalPickedQty}</TableCell> | |||||
<TableCell align="right">{actualPickedQty}</TableCell> | |||||
<TableCell align="right">{line.uomDesc}</TableCell> | <TableCell align="right">{line.uomDesc}</TableCell> | ||||
<TableCell align="right">{line.targetDate}</TableCell> | <TableCell align="right">{line.targetDate}</TableCell> | ||||
</TableRow> | </TableRow> | ||||
@@ -40,6 +40,8 @@ interface ExtendedQcItem extends QcItemWithChecks { | |||||
qcPassed?: boolean; | qcPassed?: boolean; | ||||
failQty?: number; | failQty?: number; | ||||
remarks?: string; | remarks?: string; | ||||
order?: number; // ✅ Add order property | |||||
stableId?: string; // ✅ Also add stableId for better row identification | |||||
} | } | ||||
const style = { | const style = { | ||||
@@ -195,15 +197,22 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
// ✅ 修改:在组件开始时自动设置失败数量 | // ✅ 修改:在组件开始时自动设置失败数量 | ||||
useEffect(() => { | useEffect(() => { | ||||
if (itemDetail && qcItems.length > 0) { | |||||
// ✅ 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty | |||||
const updatedQcItems = qcItems.map(item => ({ | |||||
...item, | |||||
failQty: itemDetail.requiredQty || 0 // 使用 Lot Required Pick Qty | |||||
})); | |||||
setQcItems(updatedQcItems); | |||||
if (itemDetail && qcItems.length > 0 && selectedLotId) { | |||||
// ✅ 获取选中的批次数据 | |||||
const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); | |||||
if (selectedLot) { | |||||
// ✅ 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty | |||||
const updatedQcItems = qcItems.map((item, index) => ({ | |||||
...item, | |||||
failQty: selectedLot.requiredQty || 0, // 使用 Lot Required Pick Qty | |||||
// ✅ Add stable order and ID fields | |||||
order: index, | |||||
stableId: `qc-${item.id}-${index}` | |||||
})); | |||||
setQcItems(updatedQcItems); | |||||
} | |||||
} | } | ||||
}, [itemDetail, qcItems.length]); | |||||
}, [itemDetail, qcItems.length, selectedLotId, lotData]); | |||||
// ✅ 修改:移除 alert 弹窗,改为控制台日志 | // ✅ 修改:移除 alert 弹窗,改为控制台日志 | ||||
const onSubmitQc = useCallback<SubmitHandler<any>>( | const onSubmitQc = useCallback<SubmitHandler<any>>( | ||||
@@ -215,7 +224,8 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
const acceptQty = Number(accQty) || null; | const acceptQty = Number(accQty) || null; | ||||
const validationErrors : string[] = []; | const validationErrors : string[] = []; | ||||
const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); | |||||
const itemsWithoutResult = qcItems.filter(item => item.qcPassed === undefined); | const itemsWithoutResult = qcItems.filter(item => item.qcPassed === undefined); | ||||
if (itemsWithoutResult.length > 0) { | if (itemsWithoutResult.length > 0) { | ||||
validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`); | validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`); | ||||
@@ -234,7 +244,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
qcItem: item.code, | qcItem: item.code, | ||||
qcDescription: item.description || "", | qcDescription: item.description || "", | ||||
isPassed: item.qcPassed, | isPassed: item.qcPassed, | ||||
failQty: item.qcPassed ? 0 : (itemDetail?.requiredQty || 0), | |||||
failQty: item.qcPassed ? 0 : (selectedLot?.requiredQty || 0), | |||||
remarks: item.remarks || "", | remarks: item.remarks || "", | ||||
})), | })), | ||||
}; | }; | ||||
@@ -248,7 +258,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
} | } | ||||
// ✅ Fix: Update stock out line status based on QC decision | // ✅ Fix: Update stock out line status based on QC decision | ||||
if (selectedLotId && qcData.qcAccept) { | |||||
if (selectedLotId) { // ✅ Remove qcData.qcAccept condition | |||||
try { | try { | ||||
const allPassed = qcData.qcItems.every(item => item.isPassed); | const allPassed = qcData.qcItems.every(item => item.isPassed); | ||||
@@ -261,17 +271,19 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
}); | }); | ||||
// ✅ Fix: 1. Update stock out line status with required qty field | // ✅ Fix: 1. Update stock out line status with required qty field | ||||
await updateStockOutLineStatus({ | |||||
id: selectedLotId, | |||||
status: newStockOutLineStatus, | |||||
qty: itemDetail?.requiredQty || 0 // ✅ Add required qty field | |||||
}); | |||||
if (selectedLot) { | |||||
await updateStockOutLineStatus({ | |||||
id: selectedLotId, | |||||
status: newStockOutLineStatus, | |||||
qty: selectedLot.requiredQty || 0 | |||||
}); | |||||
} else { | |||||
console.warn("Selected lot not found for stock out line status update"); | |||||
} | |||||
// ✅ Fix: 2. If QC failed, also update inventory lot line status | // ✅ Fix: 2. If QC failed, also update inventory lot line status | ||||
if (!allPassed) { | if (!allPassed) { | ||||
try { | try { | ||||
// ✅ Fix: Get the correct lot data | |||||
const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); | |||||
if (selectedLot) { | if (selectedLot) { | ||||
console.log("Updating inventory lot line status for failed QC:", { | console.log("Updating inventory lot line status for failed QC:", { | ||||
inventoryLotLineId: selectedLot.lotId, | inventoryLotLineId: selectedLot.lotId, | ||||
@@ -280,7 +292,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
await updateInventoryLotLineStatus({ | await updateInventoryLotLineStatus({ | ||||
inventoryLotLineId: selectedLot.lotId, | inventoryLotLineId: selectedLot.lotId, | ||||
status: 'unavailable' // ✅ Use correct backend enum value | |||||
status: 'unavailable' | |||||
}); | }); | ||||
console.log("Inventory lot line status updated to unavailable"); | console.log("Inventory lot line status updated to unavailable"); | ||||
} else { | } else { | ||||
@@ -288,7 +300,6 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
} | } | ||||
} catch (error) { | } catch (error) { | ||||
console.error("Failed to update inventory lot line status:", error); | console.error("Failed to update inventory lot line status:", error); | ||||
// ✅ Don't fail the entire operation, just log the error | |||||
} | } | ||||
} | } | ||||
@@ -300,12 +311,6 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
} | } | ||||
} catch (error) { | } catch (error) { | ||||
console.error("Error updating stock out line status after QC:", error); | console.error("Error updating stock out line status after QC:", error); | ||||
// ✅ Log detailed error information | |||||
if (error instanceof Error) { | |||||
console.error("Error details:", error.message); | |||||
console.error("Error stack:", error.stack); | |||||
} | |||||
// ✅ Don't fail the entire QC submission, just log the error | |||||
} | } | ||||
} | } | ||||
@@ -362,15 +367,20 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
value={current.qcPassed === undefined ? "" : (current.qcPassed ? "true" : "false")} | value={current.qcPassed === undefined ? "" : (current.qcPassed ? "true" : "false")} | ||||
onChange={(e) => { | onChange={(e) => { | ||||
const value = e.target.value === "true"; | const value = e.target.value === "true"; | ||||
setQcItems((prev) => | |||||
prev.map((r): ExtendedQcItem => (r.id === params.id ? { ...r, qcPassed: value } : r)) | |||||
// ✅ Simple state update | |||||
setQcItems(prev => | |||||
prev.map(item => | |||||
item.id === params.id | |||||
? { ...item, qcPassed: value } | |||||
: item | |||||
) | |||||
); | ); | ||||
}} | }} | ||||
name={`qcPassed-${params.id}`} | name={`qcPassed-${params.id}`} | ||||
> | > | ||||
<FormControlLabel | <FormControlLabel | ||||
value="true" | value="true" | ||||
control={<Radio size="small" />} | |||||
control={<Radio />} | |||||
label="合格" | label="合格" | ||||
sx={{ | sx={{ | ||||
color: current.qcPassed === true ? "green" : "inherit", | color: current.qcPassed === true ? "green" : "inherit", | ||||
@@ -379,7 +389,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
/> | /> | ||||
<FormControlLabel | <FormControlLabel | ||||
value="false" | value="false" | ||||
control={<Radio size="small" />} | |||||
control={<Radio />} | |||||
label="不合格" | label="不合格" | ||||
sx={{ | sx={{ | ||||
color: current.qcPassed === false ? "red" : "inherit", | color: current.qcPassed === false ? "red" : "inherit", | ||||
@@ -400,7 +410,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
type="number" | type="number" | ||||
size="small" | size="small" | ||||
// ✅ 修改:失败项目自动显示 Lot Required Pick Qty | // ✅ 修改:失败项目自动显示 Lot Required Pick Qty | ||||
value={!params.row.qcPassed ? (itemDetail?.requiredQty || 0) : 0} | |||||
value={!params.row.qcPassed ? (0) : 0} | |||||
disabled={params.row.qcPassed} | disabled={params.row.qcPassed} | ||||
// ✅ 移除 onChange,因为数量是固定的 | // ✅ 移除 onChange,因为数量是固定的 | ||||
// onChange={(e) => { | // onChange={(e) => { | ||||
@@ -444,6 +454,27 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
[t], | [t], | ||||
); | ); | ||||
// ✅ Add stable update function | |||||
const handleQcResultChange = useCallback((itemId: number, qcPassed: boolean) => { | |||||
setQcItems(prevItems => | |||||
prevItems.map(item => | |||||
item.id === itemId | |||||
? { ...item, qcPassed } | |||||
: item | |||||
) | |||||
); | |||||
}, []); | |||||
// ✅ Remove duplicate functions | |||||
const getRowId = useCallback((row: any) => { | |||||
return row.id; // Just use the original ID | |||||
}, []); | |||||
// ✅ Remove complex sorting logic | |||||
// const stableQcItems = useMemo(() => { ... }); // Remove | |||||
// const sortedQcItems = useMemo(() => { ... }); // Remove | |||||
// ✅ Use qcItems directly in DataGrid | |||||
return ( | return ( | ||||
<> | <> | ||||
<FormProvider {...formProps}> | <FormProvider {...formProps}> | ||||
@@ -481,8 +512,9 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
<StyledDataGrid | <StyledDataGrid | ||||
columns={qcColumns} | columns={qcColumns} | ||||
rows={qcItems} | |||||
rows={qcItems} // ✅ Use qcItems directly | |||||
autoHeight | autoHeight | ||||
getRowId={getRowId} // ✅ Simple row ID function | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
</> | </> | ||||
@@ -0,0 +1,511 @@ | |||||
"use client"; | |||||
import { | |||||
Autocomplete, | |||||
Box, | |||||
Button, | |||||
CircularProgress, | |||||
FormControl, | |||||
Grid, | |||||
Modal, | |||||
TextField, | |||||
Typography, | |||||
Table, | |||||
TableBody, | |||||
TableCell, | |||||
TableContainer, | |||||
TableHead, | |||||
TableRow, | |||||
Paper, | |||||
Checkbox, | |||||
TablePagination, | |||||
} from "@mui/material"; | |||||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import { | |||||
newassignPickOrder, | |||||
AssignPickOrderInputs, | |||||
releaseAssignedPickOrders, | |||||
fetchPickOrderWithStockClient, // Add this import | |||||
} from "@/app/api/pickOrder/actions"; | |||||
import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||||
import { | |||||
FormProvider, | |||||
useForm, | |||||
} from "react-hook-form"; | |||||
import { isEmpty, upperFirst, groupBy } from "lodash"; | |||||
import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil"; | |||||
import useUploadContext from "../UploadProvider/useUploadContext"; | |||||
import dayjs from "dayjs"; | |||||
import arraySupport from "dayjs/plugin/arraySupport"; | |||||
import SearchBox, { Criterion } from "../SearchBox"; | |||||
import { sortBy, uniqBy } from "lodash"; | |||||
import { createStockOutLine, CreateStockOutLine, fetchPickOrderDetails } from "@/app/api/pickOrder/actions"; | |||||
dayjs.extend(arraySupport); | |||||
interface Props { | |||||
filterArgs: Record<string, any>; | |||||
} | |||||
// Update the interface to match the new API response structure | |||||
interface PickOrderRow { | |||||
id: string; | |||||
code: string; | |||||
targetDate: string; | |||||
type: string; | |||||
status: string; | |||||
assignTo: number; | |||||
groupName: string; | |||||
consoCode?: string; | |||||
pickOrderLines: PickOrderLineRow[]; | |||||
} | |||||
interface PickOrderLineRow { | |||||
id: number; | |||||
itemId: number; | |||||
itemCode: string; | |||||
itemName: string; | |||||
availableQty: number | null; | |||||
requiredQty: number; | |||||
uomCode: string; | |||||
uomDesc: string; | |||||
suggestedList: any[]; | |||||
} | |||||
const style = { | |||||
position: "absolute", | |||||
top: "50%", | |||||
left: "50%", | |||||
transform: "translate(-50%, -50%)", | |||||
bgcolor: "background.paper", | |||||
pt: 5, | |||||
px: 5, | |||||
pb: 10, | |||||
width: { xs: "100%", sm: "100%", md: "100%" }, | |||||
}; | |||||
const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
const { t } = useTranslation("pickOrder"); | |||||
const { setIsUploading } = useUploadContext(); | |||||
const [isUploading, setIsUploadingLocal] = useState(false); | |||||
// Update state to use pick order data directly | |||||
const [selectedPickOrderIds, setSelectedPickOrderIds] = useState<string[]>([]); | |||||
const [filteredPickOrders, setFilteredPickOrders] = useState<PickOrderRow[]>([]); | |||||
const [isLoadingItems, setIsLoadingItems] = useState(false); | |||||
const [pagingController, setPagingController] = useState({ | |||||
pageNum: 1, | |||||
pageSize: 10, | |||||
}); | |||||
const [totalCountItems, setTotalCountItems] = useState<number>(); | |||||
const [modalOpen, setModalOpen] = useState(false); | |||||
const [usernameList, setUsernameList] = useState<NameList[]>([]); | |||||
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||||
const [originalPickOrderData, setOriginalPickOrderData] = useState<PickOrderRow[]>([]); | |||||
const formProps = useForm<AssignPickOrderInputs>(); | |||||
const errors = formProps.formState.errors; | |||||
// Update the handler functions to work with string IDs | |||||
const handlePickOrderSelect = useCallback((pickOrderId: string, checked: boolean) => { | |||||
if (checked) { | |||||
setSelectedPickOrderIds(prev => [...prev, pickOrderId]); | |||||
} else { | |||||
setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId)); | |||||
} | |||||
}, []); | |||||
const isPickOrderSelected = useCallback((pickOrderId: string) => { | |||||
return selectedPickOrderIds.includes(pickOrderId); | |||||
}, [selectedPickOrderIds]); | |||||
// Update the fetch function to use the correct endpoint | |||||
const fetchNewPageItems = useCallback( | |||||
async (pagingController: Record<string, number>, filterArgs: Record<string, any>) => { | |||||
setIsLoadingItems(true); | |||||
try { | |||||
const params = { | |||||
...pagingController, | |||||
...filterArgs, | |||||
pageNum: (pagingController.pageNum || 1) - 1, | |||||
pageSize: pagingController.pageSize || 10, | |||||
// Filter for assigned status only | |||||
status: "assigned" | |||||
}; | |||||
const res = await fetchPickOrderWithStockClient(params); | |||||
if (res && res.records) { | |||||
// Convert pick order data to the expected format | |||||
const pickOrderRows: PickOrderRow[] = res.records.map((pickOrder: any) => ({ | |||||
id: pickOrder.id, | |||||
code: pickOrder.code, | |||||
targetDate: pickOrder.targetDate, | |||||
type: pickOrder.type, | |||||
status: pickOrder.status, | |||||
assignTo: pickOrder.assignTo, | |||||
groupName: pickOrder.groupName || "No Group", | |||||
consoCode: pickOrder.consoCode, | |||||
pickOrderLines: pickOrder.pickOrderLines || [] | |||||
})); | |||||
setOriginalPickOrderData(pickOrderRows); | |||||
setFilteredPickOrders(pickOrderRows); | |||||
setTotalCountItems(res.total); | |||||
} else { | |||||
setFilteredPickOrders([]); | |||||
setTotalCountItems(0); | |||||
} | |||||
} catch (error) { | |||||
console.error("Error fetching pick orders:", error); | |||||
setFilteredPickOrders([]); | |||||
setTotalCountItems(0); | |||||
} finally { | |||||
setIsLoadingItems(false); | |||||
} | |||||
}, | |||||
[], | |||||
); | |||||
// Handle Release operation | |||||
// Handle Release operation | |||||
const handleRelease = useCallback(async () => { | |||||
if (selectedPickOrderIds.length === 0) return; | |||||
setIsUploading(true); | |||||
try { | |||||
// Get the assigned user from the selected pick orders | |||||
const selectedPickOrders = filteredPickOrders.filter(pickOrder => | |||||
selectedPickOrderIds.includes(pickOrder.id) | |||||
); | |||||
// Check if all selected pick orders have the same assigned user | |||||
const assignedUsers = selectedPickOrders.map(po => po.assignTo).filter(Boolean); | |||||
if (assignedUsers.length === 0) { | |||||
alert("Selected pick orders are not assigned to any user."); | |||||
return; | |||||
} | |||||
const assignToValue = assignedUsers[0]; | |||||
// Validate that all pick orders are assigned to the same user | |||||
const allSameUser = assignedUsers.every(userId => userId === assignToValue); | |||||
if (!allSameUser) { | |||||
alert("All selected pick orders must be assigned to the same user."); | |||||
return; | |||||
} | |||||
console.log("Using assigned user:", assignToValue); | |||||
console.log("selectedPickOrderIds:", selectedPickOrderIds); | |||||
const releaseRes = await releaseAssignedPickOrders({ | |||||
pickOrderIds: selectedPickOrderIds.map(id => parseInt(id)), | |||||
assignTo: assignToValue | |||||
}); | |||||
if (releaseRes.code === "SUCCESS") { | |||||
console.log("Pick orders released successfully"); | |||||
// Get the consoCode from the response | |||||
const consoCode = (releaseRes.entity as any)?.consoCode; | |||||
if (consoCode) { | |||||
// Create StockOutLine records for each pick order line | |||||
for (const pickOrder of selectedPickOrders) { | |||||
for (const line of pickOrder.pickOrderLines) { | |||||
try { | |||||
const stockOutLineData = { | |||||
consoCode: consoCode, | |||||
pickOrderLineId: line.id, | |||||
inventoryLotLineId: 0, // This will be set when user scans QR code | |||||
qty: line.requiredQty, | |||||
}; | |||||
console.log("Creating stock out line:", stockOutLineData); | |||||
await createStockOutLine(stockOutLineData); | |||||
} catch (error) { | |||||
console.error("Error creating stock out line for line", line.id, error); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
fetchNewPageItems(pagingController, filterArgs); | |||||
} else { | |||||
console.error("Release failed:", releaseRes.message); | |||||
} | |||||
} catch (error) { | |||||
console.error("Error releasing pick orders:", error); | |||||
} finally { | |||||
setIsUploading(false); | |||||
} | |||||
}, [selectedPickOrderIds, filteredPickOrders, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); | |||||
// Update search criteria to match the new data structure | |||||
const searchCriteria: Criterion<any>[] = useMemo( | |||||
() => [ | |||||
{ | |||||
label: t("Pick Order Code"), | |||||
paramName: "code", | |||||
type: "text", | |||||
}, | |||||
{ | |||||
label: t("Group Code"), | |||||
paramName: "groupName", | |||||
type: "text", | |||||
}, | |||||
{ | |||||
label: t("Target Date From"), | |||||
label2: t("Target Date To"), | |||||
paramName: "targetDate", | |||||
type: "dateRange", | |||||
}, | |||||
], | |||||
[t], | |||||
); | |||||
// Update search function to work with pick order data | |||||
const handleSearch = useCallback((query: Record<string, any>) => { | |||||
setSearchQuery({ ...query }); | |||||
const filtered = originalPickOrderData.filter((pickOrder) => { | |||||
const pickOrderTargetDateStr = arrayToDayjs(pickOrder.targetDate); | |||||
const codeMatch = !query.code || | |||||
pickOrder.code?.toLowerCase().includes((query.code || "").toLowerCase()); | |||||
const groupNameMatch = !query.groupName || | |||||
pickOrder.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase()); | |||||
// Date range search | |||||
let dateMatch = true; | |||||
if (query.targetDate || query.targetDateTo) { | |||||
try { | |||||
if (query.targetDate && !query.targetDateTo) { | |||||
const fromDate = dayjs(query.targetDate); | |||||
dateMatch = pickOrderTargetDateStr.isSame(fromDate, 'day') || | |||||
pickOrderTargetDateStr.isAfter(fromDate, 'day'); | |||||
} else if (!query.targetDate && query.targetDateTo) { | |||||
const toDate = dayjs(query.targetDateTo); | |||||
dateMatch = pickOrderTargetDateStr.isSame(toDate, 'day') || | |||||
pickOrderTargetDateStr.isBefore(toDate, 'day'); | |||||
} else if (query.targetDate && query.targetDateTo) { | |||||
const fromDate = dayjs(query.targetDate); | |||||
const toDate = dayjs(query.targetDateTo); | |||||
dateMatch = (pickOrderTargetDateStr.isSame(fromDate, 'day') || | |||||
pickOrderTargetDateStr.isAfter(fromDate, 'day')) && | |||||
(pickOrderTargetDateStr.isSame(toDate, 'day') || | |||||
pickOrderTargetDateStr.isBefore(toDate, 'day')); | |||||
} | |||||
} catch (error) { | |||||
console.error("Date parsing error:", error); | |||||
dateMatch = true; | |||||
} | |||||
} | |||||
return codeMatch && groupNameMatch && dateMatch; | |||||
}); | |||||
setFilteredPickOrders(filtered); | |||||
}, [originalPickOrderData]); | |||||
const handleReset = useCallback(() => { | |||||
setSearchQuery({}); | |||||
setFilteredPickOrders(originalPickOrderData); | |||||
setTimeout(() => { | |||||
setSearchQuery({}); | |||||
}, 0); | |||||
}, [originalPickOrderData]); | |||||
// Pagination handlers | |||||
const handlePageChange = useCallback((event: unknown, newPage: number) => { | |||||
const newPagingController = { | |||||
...pagingController, | |||||
pageNum: newPage + 1, | |||||
}; | |||||
setPagingController(newPagingController); | |||||
}, [pagingController]); | |||||
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | |||||
const newPageSize = parseInt(event.target.value, 10); | |||||
const newPagingController = { | |||||
pageNum: 1, | |||||
pageSize: newPageSize, | |||||
}; | |||||
setPagingController(newPagingController); | |||||
}, []); | |||||
// Component mount effect | |||||
useEffect(() => { | |||||
fetchNewPageItems(pagingController, filterArgs || {}); | |||||
}, []); | |||||
// Dependencies change effect | |||||
useEffect(() => { | |||||
if (pagingController && (filterArgs || {})) { | |||||
fetchNewPageItems(pagingController, filterArgs || {}); | |||||
} | |||||
}, [pagingController, filterArgs, fetchNewPageItems]); | |||||
useEffect(() => { | |||||
const loadUsernameList = async () => { | |||||
try { | |||||
const res = await fetchNameList(); | |||||
if (res) { | |||||
setUsernameList(res); | |||||
} | |||||
} catch (error) { | |||||
console.error("Error loading username list:", error); | |||||
} | |||||
}; | |||||
loadUsernameList(); | |||||
}, []); | |||||
// Update the table component to work with pick order data directly | |||||
const CustomPickOrderTable = () => { | |||||
// Helper function to get user name | |||||
const getUserName = useCallback((assignToId: number | null | undefined) => { | |||||
if (!assignToId) return '-'; | |||||
const user = usernameList.find(u => u.id === assignToId); | |||||
return user ? user.name : `User ${assignToId}`; | |||||
}, [usernameList]); | |||||
return ( | |||||
<> | |||||
<TableContainer component={Paper}> | |||||
<Table> | |||||
<TableHead> | |||||
<TableRow> | |||||
<TableCell>{t("Selected")}</TableCell> | |||||
<TableCell>{t("Pick Order Code")}</TableCell> | |||||
<TableCell>{t("Group Code")}</TableCell> | |||||
<TableCell>{t("Item Code")}</TableCell> | |||||
<TableCell>{t("Item Name")}</TableCell> | |||||
<TableCell align="right">{t("Order Quantity")}</TableCell> | |||||
<TableCell align="right">{t("Current Stock")}</TableCell> | |||||
<TableCell align="right">{t("Stock Unit")}</TableCell> | |||||
<TableCell>{t("Target Date")}</TableCell> | |||||
<TableCell>{t("Assigned To")}</TableCell> | |||||
</TableRow> | |||||
</TableHead> | |||||
<TableBody> | |||||
{filteredPickOrders.length === 0 ? ( | |||||
<TableRow> | |||||
<TableCell colSpan={10} align="center"> | |||||
<Typography variant="body2" color="text.secondary"> | |||||
{t("No data available")} | |||||
</Typography> | |||||
</TableCell> | |||||
</TableRow> | |||||
) : ( | |||||
filteredPickOrders.map((pickOrder) => ( | |||||
pickOrder.pickOrderLines.map((line: PickOrderLineRow, index: number) => ( | |||||
<TableRow key={`${pickOrder.id}-${line.id}`}> | |||||
{/* Checkbox - only show for first line of each pick order */} | |||||
<TableCell> | |||||
{index === 0 ? ( | |||||
<Checkbox | |||||
checked={isPickOrderSelected(pickOrder.id)} | |||||
onChange={(e) => handlePickOrderSelect(pickOrder.id, e.target.checked)} | |||||
disabled={!isEmpty(pickOrder.consoCode)} | |||||
/> | |||||
) : null} | |||||
</TableCell> | |||||
{/* Pick Order Code - only show for first line */} | |||||
<TableCell> | |||||
{index === 0 ? pickOrder.code : null} | |||||
</TableCell> | |||||
{/* Group Name - only show for first line */} | |||||
<TableCell> | |||||
{index === 0 ? pickOrder.groupName : null} | |||||
</TableCell> | |||||
{/* Item Code */} | |||||
<TableCell>{line.itemCode}</TableCell> | |||||
{/* Item Name */} | |||||
<TableCell>{line.itemName}</TableCell> | |||||
{/* Order Quantity */} | |||||
<TableCell align="right">{line.requiredQty}</TableCell> | |||||
{/* Current Stock */} | |||||
<TableCell align="right"> | |||||
<Typography | |||||
variant="body2" | |||||
color={line.availableQty && line.availableQty > 0 ? "success.main" : "error.main"} | |||||
sx={{ fontWeight: line.availableQty && line.availableQty > 0 ? 'bold' : 'normal' }} | |||||
> | |||||
{(line.availableQty || 0).toLocaleString()} | |||||
</Typography> | |||||
</TableCell> | |||||
{/* Unit */} | |||||
<TableCell align="right">{line.uomDesc}</TableCell> | |||||
{/* Target Date - only show for first line */} | |||||
<TableCell> | |||||
{index === 0 ? ( | |||||
arrayToDayjs(pickOrder.targetDate) | |||||
.add(-1, "month") | |||||
.format(OUTPUT_DATE_FORMAT) | |||||
) : null} | |||||
</TableCell> | |||||
{/* Assigned To - only show for first line */} | |||||
<TableCell> | |||||
{index === 0 ? ( | |||||
<Typography variant="body2"> | |||||
{getUserName(pickOrder.assignTo)} | |||||
</Typography> | |||||
) : null} | |||||
</TableCell> | |||||
</TableRow> | |||||
)) | |||||
)) | |||||
)} | |||||
</TableBody> | |||||
</Table> | |||||
</TableContainer> | |||||
<TablePagination | |||||
component="div" | |||||
count={totalCountItems || 0} | |||||
page={(pagingController.pageNum - 1)} | |||||
rowsPerPage={pagingController.pageSize} | |||||
onPageChange={handlePageChange} | |||||
onRowsPerPageChange={handlePageSizeChange} | |||||
rowsPerPageOptions={[10, 25, 50, 100]} | |||||
labelRowsPerPage={t("Rows per page")} | |||||
labelDisplayedRows={({ from, to, count }) => | |||||
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||||
} | |||||
/> | |||||
</> | |||||
); | |||||
}; | |||||
return ( | |||||
<> | |||||
<SearchBox criteria={searchCriteria} onSearch={handleSearch} onReset={handleReset} /> | |||||
<Grid container rowGap={1}> | |||||
<Grid item xs={12}> | |||||
{isLoadingItems ? ( | |||||
<CircularProgress size={40} /> | |||||
) : ( | |||||
<CustomPickOrderTable /> | |||||
)} | |||||
</Grid> | |||||
<Grid item xs={12}> | |||||
<Box sx={{ display: "flex", justifyContent: "flex-start", mt: 2 }}> | |||||
<Button | |||||
disabled={selectedPickOrderIds.length < 1} | |||||
variant="outlined" | |||||
onClick={handleRelease} | |||||
> | |||||
{t("Release")} | |||||
</Button> | |||||
</Box> | |||||
</Grid> | |||||
</Grid> | |||||
</> | |||||
); | |||||
}; | |||||
export default AssignTo; |