|
|
|
@@ -87,7 +87,7 @@ const QrCodeModal: React.FC<{ |
|
|
|
const { t } = useTranslation("pickOrder"); |
|
|
|
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); |
|
|
|
const [manualInput, setManualInput] = useState<string>(''); |
|
|
|
|
|
|
|
const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({}); |
|
|
|
// ✅ Add state to track manual input submission |
|
|
|
const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false); |
|
|
|
const [manualInputError, setManualInputError] = useState<boolean>(false); |
|
|
|
@@ -260,7 +260,7 @@ const QrCodeModal: React.FC<{ |
|
|
|
setManualInputSubmitted(true); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// ✅ Add function to restart scanning after manual input error |
|
|
|
const handleRestartScan = () => { |
|
|
|
setQrScanFailed(false); |
|
|
|
@@ -397,7 +397,7 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
|
}, []); |
|
|
|
// ✅ 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); |
|
|
|
@@ -411,13 +411,10 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
|
|
|
|
|
// ✅ 添加状态消息生成函数 |
|
|
|
const getStatusMessage = useCallback((lot: LotPickData) => { |
|
|
|
if (!lot.stockOutLineId) { |
|
|
|
return t("Please finish QR code scanand pick order."); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
switch (lot.stockOutLineStatus?.toLowerCase()) { |
|
|
|
case 'pending': |
|
|
|
return t("Please finish pick order."); |
|
|
|
return t("Please finish QR code scanand pick order."); |
|
|
|
case 'checked': |
|
|
|
return t("Please submit the pick order."); |
|
|
|
case 'partially_completed': |
|
|
|
@@ -466,30 +463,58 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
|
if (!selectedRowId) return lot.availableQty; |
|
|
|
const lactualPickQty = lot.actualPickQty || 0; |
|
|
|
const actualPickQty = pickQtyData[selectedRowId]?.[lot.lotId] || 0; |
|
|
|
const remainingQty = lot.inQty - lot.outQty; |
|
|
|
|
|
|
|
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]); |
|
|
|
|
|
|
|
// ✅ Handle QR code submission |
|
|
|
const handleQrCodeSubmit = useCallback(async (lotNo: string) => { |
|
|
|
if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { |
|
|
|
console.log(`✅ QR Code verified for lot: ${lotNo}`); |
|
|
|
|
|
|
|
if (!selectedLotForQr.stockOutLineId) { |
|
|
|
console.error("No stock out line ID found for this lot"); |
|
|
|
alert("No stock out line found for this lot. Please contact administrator."); |
|
|
|
return; |
|
|
|
} |
|
|
|
// ✅ Store the required quantity before creating stock out line |
|
|
|
const requiredQty = selectedLotForQr.requiredQty; |
|
|
|
const lotId = selectedLotForQr.lotId; |
|
|
|
|
|
|
|
try { |
|
|
|
// ✅ Update stock out line status to 'checked' (QR scan completed) |
|
|
|
const stockOutLineUpdate = await updateStockOutLineStatus({ |
|
|
|
id: selectedLotForQr.stockOutLineId, |
|
|
|
status: 'checked', |
|
|
|
qty: selectedLotForQr.stockOutLineQty || 0 |
|
|
|
}); |
|
|
|
|
|
|
|
// ✅ Create stock out line and wait for it to complete |
|
|
|
await onCreateStockOutLine(selectedLotForQr.lotId); |
|
|
|
console.log("✅ Stock out line updated to 'checked':", stockOutLineUpdate); |
|
|
|
|
|
|
|
// ✅ Close modal |
|
|
|
setQrModalOpen(false); |
|
|
|
setSelectedLotForQr(null); |
|
|
|
|
|
|
|
// ✅ Set pick quantity AFTER stock out line creation and refresh is complete |
|
|
|
if (onLotDataRefresh) { |
|
|
|
await onLotDataRefresh(); |
|
|
|
} |
|
|
|
// ✅ Set pick quantity AFTER stock out line update is complete |
|
|
|
if (selectedRowId) { |
|
|
|
// Add a small delay to ensure the data refresh from onCreateStockOutLine is complete |
|
|
|
// Add a small delay to ensure the data refresh is complete |
|
|
|
setTimeout(() => { |
|
|
|
onPickQtyChange(selectedRowId, lotId, requiredQty); |
|
|
|
console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); |
|
|
|
@@ -497,9 +522,18 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
|
} |
|
|
|
|
|
|
|
// ✅ Show success message |
|
|
|
console.log("Stock out line created successfully!"); |
|
|
|
console.log("Stock out line updated successfully!"); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
console.error("❌ Error updating stock out line status:", error); |
|
|
|
alert("Failed to update lot status. Please try again."); |
|
|
|
} |
|
|
|
}, [selectedLotForQr, onCreateStockOutLine, selectedRowId, onPickQtyChange]); |
|
|
|
} else { |
|
|
|
// ✅ Handle case where lot numbers don't match |
|
|
|
console.error("QR scan mismatch:", { scanned: lotNo, expected: selectedLotForQr?.lotNo }); |
|
|
|
alert(`QR scan mismatch! Expected: ${selectedLotForQr?.lotNo}, Scanned: ${lotNo}`); |
|
|
|
} |
|
|
|
}, [selectedLotForQr, selectedRowId, onPickQtyChange]); |
|
|
|
|
|
|
|
// ✅ 添加 PickExecutionForm 相关的状态 |
|
|
|
const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); |
|
|
|
@@ -599,17 +633,17 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
|
}} |
|
|
|
> |
|
|
|
<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> |
|
|
|
<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 |
|
|
|
@@ -646,145 +680,121 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
|
})()} |
|
|
|
</TableCell> |
|
|
|
<TableCell align="center"> |
|
|
|
{/* Show QR Scan Button if not scanned, otherwise show TextField + Pick Form */} |
|
|
|
{!lot.stockOutLineId ? ( |
|
|
|
<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> |
|
|
|
) : ( |
|
|
|
// ✅ 当有 stockOutLineId 时,显示 TextField + Pick Form 按钮 |
|
|
|
<Stack direction="row" spacing={1} alignItems="center"> |
|
|
|
{/* ✅ 恢复 TextField 用于正常数量输入 */} |
|
|
|
<TextField |
|
|
|
type="number" |
|
|
|
size="small" |
|
|
|
value={pickQtyData[selectedRowId!]?.[lot.lotId] || ''} |
|
|
|
onChange={(e) => { |
|
|
|
if (selectedRowId) { |
|
|
|
onPickQtyChange(selectedRowId, lot.lotId, parseFloat(e.target.value) || 0); |
|
|
|
} |
|
|
|
}} |
|
|
|
disabled={ |
|
|
|
(lot.lotAvailability === 'expired' || |
|
|
|
lot.lotAvailability === 'status_unavailable' || |
|
|
|
lot.lotAvailability === 'rejected') || |
|
|
|
selectedLotRowId !== `row_${index}` || |
|
|
|
lot.stockOutLineStatus === 'completed' // ✅ 完成时禁用输入 |
|
|
|
} |
|
|
|
inputProps={{ |
|
|
|
min: 0, |
|
|
|
max: calculateRemainingRequiredQty(lot), |
|
|
|
step: 0.01 |
|
|
|
}} |
|
|
|
sx={{ |
|
|
|
width: '80px', |
|
|
|
'& .MuiInputBase-input': { |
|
|
|
fontSize: '0.75rem', |
|
|
|
textAlign: 'center', |
|
|
|
padding: '8px 4px' |
|
|
|
} |
|
|
|
}} |
|
|
|
placeholder="0" |
|
|
|
/> |
|
|
|
{/* 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> |
|
|
|
) : ( |
|
|
|
// ✅ 当有 stockOutLineId 时,显示 TextField + Pick Form 按钮 |
|
|
|
<Stack direction="row" spacing={1} alignItems="center"> |
|
|
|
{/* ✅ 恢复 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)); |
|
|
|
{/* |
|
|
|
// ✅ Validate input |
|
|
|
if (inputValue > maxAllowed) { |
|
|
|
// Set validation error for this lot |
|
|
|
setValidationErrors(prev => ({ ...prev, [`lot_${lot.lotId}`]: `${t('Input quantity cannot exceed')} ${maxAllowed}` })); |
|
|
|
return; |
|
|
|
} else { |
|
|
|
// Clear validation error if valid |
|
|
|
setValidationErrors(prev => { |
|
|
|
const newErrors = { ...prev }; |
|
|
|
delete newErrors[`lot_${lot.lotId}`]; |
|
|
|
return newErrors; |
|
|
|
}); |
|
|
|
*/} |
|
|
|
|
|
|
|
{/* ✅ 添加 Pick Form 按钮用于问题情况 */} |
|
|
|
<Button |
|
|
|
variant="outlined" |
|
|
|
size="small" |
|
|
|
onClick={() => handlePickExecutionForm(lot)} |
|
|
|
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> |
|
|
|
|
|
|
|
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}`]} // ✅ Show red border when error |
|
|
|
helperText={validationErrors[`lot_${lot.lotId}`]} // ✅ Show red error text below |
|
|
|
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)} |
|
|
|
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> |
|
|
|
|
|
|
|
<TableCell align="center"> |
|
|
|
{/* |
|
|
|
<Stack direction="column" spacing={1} alignItems="center"> |
|
|
|
<Button |
|
|
|
variant="outlined" |
|
|
|
size="small" |
|
|
|
onClick={async () => { |
|
|
|
if (selectedRowId && selectedRow && lot.stockOutLineId) { |
|
|
|
try { |
|
|
|
// ✅ Call updateStockOutLineStatus to reject the stock out line |
|
|
|
await updateStockOutLineStatus({ |
|
|
|
id: lot.stockOutLineId, |
|
|
|
status: 'rejected', |
|
|
|
qty: 0 |
|
|
|
}); |
|
|
|
|
|
|
|
// ✅ Refresh data after rejection |
|
|
|
if (onDataRefresh) { |
|
|
|
await onDataRefresh(); |
|
|
|
} |
|
|
|
if (onLotDataRefresh) { |
|
|
|
await onLotDataRefresh(); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error("Error rejecting lot:", error); |
|
|
|
} |
|
|
|
} |
|
|
|
}} |
|
|
|
// ✅ Only enable if stock out line exists |
|
|
|
disabled={!lot.stockOutLineId} |
|
|
|
sx={{ |
|
|
|
fontSize: '0.7rem', |
|
|
|
py: 0.5, |
|
|
|
minHeight: '28px', |
|
|
|
whiteSpace: 'nowrap', |
|
|
|
minWidth: '40px' |
|
|
|
}} |
|
|
|
> |
|
|
|
{t("Reject")} |
|
|
|
</Button> |
|
|
|
</Stack> |
|
|
|
*/} |
|
|
|
{/*} |
|
|
|
</TableCell> |
|
|
|
|
|
|
|
<TableCell align="center"> |
|
|
|
*/} |
|
|
|
|
|
|
|
<Stack direction="column" spacing={1} alignItems="center"> |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
|