|
|
@@ -24,6 +24,7 @@ import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; |
|
|
import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; |
|
|
import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; |
|
|
import { updateInventoryLotLineStatus } from "@/app/api/inventory/actions"; |
|
|
import { updateInventoryLotLineStatus } from "@/app/api/inventory/actions"; |
|
|
import { updateStockOutLineStatus } from "@/app/api/pickOrder/actions"; |
|
|
import { updateStockOutLineStatus } from "@/app/api/pickOrder/actions"; |
|
|
|
|
|
import { fetchStockInLineInfo } from "@/app/api/po/actions"; // ✅ Add this import |
|
|
interface LotPickData { |
|
|
interface LotPickData { |
|
|
id: number; |
|
|
id: number; |
|
|
lotId: number; |
|
|
lotId: number; |
|
|
@@ -31,6 +32,7 @@ interface LotPickData { |
|
|
expiryDate: string; |
|
|
expiryDate: string; |
|
|
location: string; |
|
|
location: string; |
|
|
stockUnit: string; |
|
|
stockUnit: string; |
|
|
|
|
|
inQty: number; |
|
|
availableQty: number; |
|
|
availableQty: number; |
|
|
requiredQty: number; |
|
|
requiredQty: number; |
|
|
actualPickQty: number; |
|
|
actualPickQty: number; |
|
|
@@ -65,6 +67,7 @@ interface LotTableProps { |
|
|
selectedLotForInput: LotPickData | null; |
|
|
selectedLotForInput: LotPickData | null; |
|
|
generateInputBody: () => any; |
|
|
generateInputBody: () => any; |
|
|
onDataRefresh: () => Promise<void>; |
|
|
onDataRefresh: () => Promise<void>; |
|
|
|
|
|
onLotDataRefresh: () => Promise<void>; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// ✅ QR Code Modal Component |
|
|
// ✅ QR Code Modal Component |
|
|
@@ -81,24 +84,114 @@ const QrCodeModal: React.FC<{ |
|
|
// ✅ Add state to track manual input submission |
|
|
// ✅ Add state to track manual input submission |
|
|
const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false); |
|
|
const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false); |
|
|
const [manualInputError, setManualInputError] = useState<boolean>(false); |
|
|
const [manualInputError, setManualInputError] = useState<boolean>(false); |
|
|
|
|
|
const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false); |
|
|
|
|
|
const [qrScanFailed, setQrScanFailed] = useState<boolean>(false); |
|
|
|
|
|
const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false); |
|
|
|
|
|
|
|
|
|
|
|
// ✅ Add state to track processed QR codes to prevent re-processing |
|
|
|
|
|
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); |
|
|
|
|
|
|
|
|
|
|
|
// ✅ Add state to store the scanned QR result |
|
|
|
|
|
const [scannedQrResult, setScannedQrResult] = useState<string>(''); |
|
|
|
|
|
|
|
|
// ✅ Process scanned QR codes |
|
|
|
|
|
|
|
|
// ✅ Process scanned QR codes with new format |
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
if (qrValues.length > 0 && lot) { |
|
|
|
|
|
|
|
|
if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) { |
|
|
const latestQr = qrValues[qrValues.length - 1]; |
|
|
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); |
|
|
|
|
|
|
|
|
// ✅ Check if this QR code has already been processed |
|
|
|
|
|
if (processedQrCodes.has(latestQr)) { |
|
|
|
|
|
console.log("QR code already processed, skipping..."); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ✅ Add to processed set immediately to prevent re-processing |
|
|
|
|
|
setProcessedQrCodes(prev => new Set(prev).add(latestQr)); |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
// ✅ Parse QR code as JSON |
|
|
|
|
|
const qrData = JSON.parse(latestQr); |
|
|
|
|
|
|
|
|
|
|
|
// ✅ Check if it has the expected structure |
|
|
|
|
|
if (qrData.stockInLineId && qrData.itemId) { |
|
|
|
|
|
setIsProcessingQr(true); |
|
|
|
|
|
setQrScanFailed(false); |
|
|
|
|
|
|
|
|
|
|
|
// ✅ Fetch stock in line info to get lotNo |
|
|
|
|
|
fetchStockInLineInfo(qrData.stockInLineId) |
|
|
|
|
|
.then((stockInLineInfo) => { |
|
|
|
|
|
console.log("Stock in line info:", stockInLineInfo); |
|
|
|
|
|
|
|
|
|
|
|
// ✅ Store the scanned result for display |
|
|
|
|
|
setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number'); |
|
|
|
|
|
|
|
|
|
|
|
// ✅ Compare lotNo from API with expected lotNo |
|
|
|
|
|
if (stockInLineInfo.lotNo === lot.lotNo) { |
|
|
|
|
|
console.log(`✅ QR Code verified for lot: ${lot.lotNo}`); |
|
|
|
|
|
setQrScanSuccess(true); |
|
|
|
|
|
onQrCodeSubmit(lot.lotNo); |
|
|
|
|
|
onClose(); |
|
|
|
|
|
resetScan(); |
|
|
|
|
|
} else { |
|
|
|
|
|
console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`); |
|
|
|
|
|
setQrScanFailed(true); |
|
|
|
|
|
setManualInputError(true); |
|
|
|
|
|
setManualInputSubmitted(true); |
|
|
|
|
|
// ✅ DON'T stop scanning - allow new QR codes to be processed |
|
|
|
|
|
} |
|
|
|
|
|
}) |
|
|
|
|
|
.catch((error) => { |
|
|
|
|
|
console.error("Error fetching stock in line info:", error); |
|
|
|
|
|
setScannedQrResult('Error fetching data'); |
|
|
|
|
|
setQrScanFailed(true); |
|
|
|
|
|
setManualInputError(true); |
|
|
|
|
|
setManualInputSubmitted(true); |
|
|
|
|
|
// ✅ DON'T stop scanning - allow new QR codes to be processed |
|
|
|
|
|
}) |
|
|
|
|
|
.finally(() => { |
|
|
|
|
|
setIsProcessingQr(false); |
|
|
|
|
|
}); |
|
|
|
|
|
} else { |
|
|
|
|
|
// ✅ Fallback to old format (direct lotNo comparison) |
|
|
|
|
|
const qrContent = latestQr.replace(/[{}]/g, ''); |
|
|
|
|
|
|
|
|
|
|
|
// ✅ Store the scanned result for display |
|
|
|
|
|
setScannedQrResult(qrContent); |
|
|
|
|
|
|
|
|
|
|
|
if (qrContent === lot.lotNo) { |
|
|
|
|
|
setQrScanSuccess(true); |
|
|
|
|
|
onQrCodeSubmit(lot.lotNo); |
|
|
|
|
|
onClose(); |
|
|
|
|
|
resetScan(); |
|
|
|
|
|
} else { |
|
|
|
|
|
setQrScanFailed(true); |
|
|
|
|
|
setManualInputError(true); |
|
|
|
|
|
setManualInputSubmitted(true); |
|
|
|
|
|
// ✅ DON'T stop scanning - allow new QR codes to be processed |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
// ✅ If JSON parsing fails, fallback to old format |
|
|
|
|
|
console.log("QR code is not JSON format, trying direct comparison"); |
|
|
|
|
|
const qrContent = latestQr.replace(/[{}]/g, ''); |
|
|
|
|
|
|
|
|
|
|
|
// ✅ Store the scanned result for display |
|
|
|
|
|
setScannedQrResult(qrContent); |
|
|
|
|
|
|
|
|
|
|
|
if (qrContent === lot.lotNo) { |
|
|
|
|
|
setQrScanSuccess(true); |
|
|
|
|
|
onQrCodeSubmit(lot.lotNo); |
|
|
|
|
|
onClose(); |
|
|
|
|
|
resetScan(); |
|
|
|
|
|
} else { |
|
|
|
|
|
setQrScanFailed(true); |
|
|
|
|
|
setManualInputError(true); |
|
|
|
|
|
setManualInputSubmitted(true); |
|
|
|
|
|
// ✅ DON'T stop scanning - allow new QR codes to be processed |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}, [qrValues, lot, onQrCodeSubmit, onClose, resetScan]); |
|
|
|
|
|
|
|
|
}, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes, stopScan]); |
|
|
|
|
|
|
|
|
// ✅ Clear states when modal opens or lot changes |
|
|
// ✅ Clear states when modal opens or lot changes |
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
@@ -106,6 +199,12 @@ const QrCodeModal: React.FC<{ |
|
|
setManualInput(''); |
|
|
setManualInput(''); |
|
|
setManualInputSubmitted(false); |
|
|
setManualInputSubmitted(false); |
|
|
setManualInputError(false); |
|
|
setManualInputError(false); |
|
|
|
|
|
setIsProcessingQr(false); |
|
|
|
|
|
setQrScanFailed(false); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setScannedQrResult(''); // ✅ Clear scanned result |
|
|
|
|
|
// ✅ Clear processed QR codes when modal opens |
|
|
|
|
|
setProcessedQrCodes(new Set()); |
|
|
} |
|
|
} |
|
|
}, [open]); |
|
|
}, [open]); |
|
|
|
|
|
|
|
|
@@ -114,151 +213,62 @@ const QrCodeModal: React.FC<{ |
|
|
setManualInput(''); |
|
|
setManualInput(''); |
|
|
setManualInputSubmitted(false); |
|
|
setManualInputSubmitted(false); |
|
|
setManualInputError(false); |
|
|
setManualInputError(false); |
|
|
|
|
|
setIsProcessingQr(false); |
|
|
|
|
|
setQrScanFailed(false); |
|
|
|
|
|
setQrScanSuccess(false); |
|
|
|
|
|
setScannedQrResult(''); // ✅ Clear scanned result |
|
|
|
|
|
// ✅ Clear processed QR codes when lot changes |
|
|
|
|
|
setProcessedQrCodes(new Set()); |
|
|
} |
|
|
} |
|
|
}, [lot]); |
|
|
}, [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 |
|
|
|
|
|
|
|
|
// ✅ Auto-submit manual input when it matches (but only if QR scan hasn't failed) |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) { |
|
|
|
|
|
console.log('🔄 Auto-submitting manual input:', manualInput.trim()); |
|
|
|
|
|
|
|
|
|
|
|
const timer = setTimeout(() => { |
|
|
|
|
|
setQrScanSuccess(true); |
|
|
|
|
|
onQrCodeSubmit(lot.lotNo); |
|
|
|
|
|
onClose(); |
|
|
|
|
|
setManualInput(''); |
|
|
|
|
|
setManualInputError(false); |
|
|
|
|
|
setManualInputSubmitted(false); |
|
|
|
|
|
}, 200); |
|
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(timer); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
}, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]); |
|
|
|
|
|
|
|
|
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> |
|
|
|
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
|
|
|
{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> |
|
|
|
|
|
); |
|
|
|
|
|
}; |
|
|
|
|
|
*/} |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '') { |
|
|
|
|
|
// Auto-submit when manual input matches the expected lot number |
|
|
|
|
|
console.log('🔄 Auto-submitting manual input:', manualInput.trim()); |
|
|
|
|
|
|
|
|
|
|
|
// Add a small delay to ensure proper execution order |
|
|
|
|
|
const timer = setTimeout(() => { |
|
|
|
|
|
onQrCodeSubmit(lot.lotNo); |
|
|
|
|
|
onClose(); |
|
|
|
|
|
setManualInput(''); |
|
|
|
|
|
setManualInputError(false); |
|
|
|
|
|
setManualInputSubmitted(false); |
|
|
|
|
|
}, 200); // 200ms delay |
|
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(timer); |
|
|
|
|
|
} |
|
|
|
|
|
}, [manualInput, lot, onQrCodeSubmit, onClose]); |
|
|
|
|
|
|
|
|
// ✅ Add the missing handleManualSubmit function |
|
|
const handleManualSubmit = () => { |
|
|
const handleManualSubmit = () => { |
|
|
if (manualInput.trim() === lot?.lotNo) { |
|
|
if (manualInput.trim() === lot?.lotNo) { |
|
|
// ✅ Success - no error helper text needed |
|
|
|
|
|
|
|
|
setQrScanSuccess(true); |
|
|
onQrCodeSubmit(lot.lotNo); |
|
|
onQrCodeSubmit(lot.lotNo); |
|
|
onClose(); |
|
|
onClose(); |
|
|
setManualInput(''); |
|
|
setManualInput(''); |
|
|
} else { |
|
|
} else { |
|
|
// ✅ Show error helper text after submit |
|
|
|
|
|
|
|
|
setQrScanFailed(true); |
|
|
setManualInputError(true); |
|
|
setManualInputError(true); |
|
|
setManualInputSubmitted(true); |
|
|
setManualInputSubmitted(true); |
|
|
// Don't clear input - let user see what they typed |
|
|
|
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
useEffect(() => { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ Add function to restart scanning after manual input error |
|
|
|
|
|
const handleRestartScan = () => { |
|
|
|
|
|
setQrScanFailed(false); |
|
|
|
|
|
setManualInputError(false); |
|
|
|
|
|
setManualInputSubmitted(false); |
|
|
|
|
|
setProcessedQrCodes(new Set()); // Clear processed QR codes |
|
|
|
|
|
startScan(); // Restart scanning |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (open) { |
|
|
if (open) { |
|
|
startScan(); |
|
|
startScan(); |
|
|
} |
|
|
} |
|
|
}, [open, startScan]); |
|
|
}, [open, startScan]); |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<Modal open={open} onClose={onClose}> |
|
|
<Modal open={open} onClose={onClose}> |
|
|
<Box sx={{ |
|
|
<Box sx={{ |
|
|
@@ -272,24 +282,41 @@ useEffect(() => { |
|
|
minWidth: 400, |
|
|
minWidth: 400, |
|
|
}}> |
|
|
}}> |
|
|
<Typography variant="h6" gutterBottom> |
|
|
<Typography variant="h6" gutterBottom> |
|
|
QR Code Scan for Lot: {lot?.lotNo} |
|
|
|
|
|
|
|
|
{t("QR Code Scan for Lot")}: {lot?.lotNo} |
|
|
</Typography> |
|
|
</Typography> |
|
|
|
|
|
|
|
|
|
|
|
{/* ✅ Show processing status */} |
|
|
|
|
|
{isProcessingQr && ( |
|
|
|
|
|
<Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}> |
|
|
|
|
|
<Typography variant="body2" color="primary"> |
|
|
|
|
|
{t("Processing QR code...")} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
</Box> |
|
|
|
|
|
)} |
|
|
|
|
|
|
|
|
{/* Manual Input with Submit-Triggered Helper Text */} |
|
|
{/* Manual Input with Submit-Triggered Helper Text */} |
|
|
<Box sx={{ mb: 2 }}> |
|
|
<Box sx={{ mb: 2 }}> |
|
|
<Typography variant="body2" gutterBottom> |
|
|
<Typography variant="body2" gutterBottom> |
|
|
<strong>Manual Input:</strong> |
|
|
|
|
|
|
|
|
<strong>{t("Manual Input")}:</strong> |
|
|
</Typography> |
|
|
</Typography> |
|
|
<TextField |
|
|
<TextField |
|
|
fullWidth |
|
|
fullWidth |
|
|
size="small" |
|
|
size="small" |
|
|
value={manualInput} |
|
|
value={manualInput} |
|
|
onChange={(e) => setManualInput(e.target.value)} |
|
|
|
|
|
|
|
|
onChange={(e) => { |
|
|
|
|
|
setManualInput(e.target.value); |
|
|
|
|
|
// ✅ Reset error states when user starts typing |
|
|
|
|
|
if (qrScanFailed || manualInputError) { |
|
|
|
|
|
setQrScanFailed(false); |
|
|
|
|
|
setManualInputError(false); |
|
|
|
|
|
setManualInputSubmitted(false); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
sx={{ mb: 1 }} |
|
|
sx={{ mb: 1 }} |
|
|
error={manualInputSubmitted && manualInputError} |
|
|
error={manualInputSubmitted && manualInputError} |
|
|
helperText={ |
|
|
helperText={ |
|
|
manualInputSubmitted && manualInputError |
|
|
manualInputSubmitted && manualInputError |
|
|
? `The input is not the same as the expected lot number.` |
|
|
|
|
|
|
|
|
? `${t("The input is not the same as the expected lot number.")}` |
|
|
: '' |
|
|
: '' |
|
|
} |
|
|
} |
|
|
/> |
|
|
/> |
|
|
@@ -300,19 +327,25 @@ useEffect(() => { |
|
|
size="small" |
|
|
size="small" |
|
|
color="primary" |
|
|
color="primary" |
|
|
> |
|
|
> |
|
|
Submit Manual Input |
|
|
|
|
|
|
|
|
{t("Submit")} |
|
|
</Button> |
|
|
</Button> |
|
|
</Box> |
|
|
</Box> |
|
|
|
|
|
|
|
|
{/* Show QR Scan Status */} |
|
|
{/* Show QR Scan Status */} |
|
|
{qrValues.length > 0 && ( |
|
|
{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]} |
|
|
|
|
|
|
|
|
<Box sx={{ |
|
|
|
|
|
mb: 2, |
|
|
|
|
|
p: 2, |
|
|
|
|
|
backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5', |
|
|
|
|
|
borderRadius: 1 |
|
|
|
|
|
}}> |
|
|
|
|
|
<Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}> |
|
|
|
|
|
<strong>{t("QR Scan Result:")}</strong> {scannedQrResult} |
|
|
</Typography> |
|
|
</Typography> |
|
|
{manualInputError && ( |
|
|
|
|
|
<Typography variant="caption" color="error" display="block"> |
|
|
|
|
|
❌ Mismatch! Expected! |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{qrScanSuccess && ( |
|
|
|
|
|
<Typography variant="caption" color="success" display="block"> |
|
|
|
|
|
✅ {t("Verified successfully!")} |
|
|
</Typography> |
|
|
</Typography> |
|
|
)} |
|
|
)} |
|
|
</Box> |
|
|
</Box> |
|
|
@@ -320,7 +353,7 @@ useEffect(() => { |
|
|
|
|
|
|
|
|
<Box sx={{ mt: 2, textAlign: 'right' }}> |
|
|
<Box sx={{ mt: 2, textAlign: 'right' }}> |
|
|
<Button onClick={onClose} variant="outlined"> |
|
|
<Button onClick={onClose} variant="outlined"> |
|
|
Cancel |
|
|
|
|
|
|
|
|
{t("Cancel")} |
|
|
</Button> |
|
|
</Button> |
|
|
</Box> |
|
|
</Box> |
|
|
</Box> |
|
|
</Box> |
|
|
@@ -347,6 +380,7 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
selectedLotForInput, |
|
|
selectedLotForInput, |
|
|
generateInputBody, |
|
|
generateInputBody, |
|
|
onDataRefresh, |
|
|
onDataRefresh, |
|
|
|
|
|
onLotDataRefresh, |
|
|
}) => { |
|
|
}) => { |
|
|
const { t } = useTranslation("pickOrder"); |
|
|
const { t } = useTranslation("pickOrder"); |
|
|
|
|
|
|
|
|
@@ -367,12 +401,12 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
// ✅ 添加状态消息生成函数 |
|
|
// ✅ 添加状态消息生成函数 |
|
|
const getStatusMessage = useCallback((lot: LotPickData) => { |
|
|
const getStatusMessage = useCallback((lot: LotPickData) => { |
|
|
if (!lot.stockOutLineId) { |
|
|
if (!lot.stockOutLineId) { |
|
|
return t("Please finish QR code scan, QC check and pick order."); |
|
|
|
|
|
|
|
|
return t("Please finish QR code scanand pick order."); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
switch (lot.stockOutLineStatus?.toLowerCase()) { |
|
|
switch (lot.stockOutLineStatus?.toLowerCase()) { |
|
|
case 'pending': |
|
|
case 'pending': |
|
|
return t("Please finish QC check and pick order."); |
|
|
|
|
|
|
|
|
return t("Please finish pick order."); |
|
|
case 'checked': |
|
|
case 'checked': |
|
|
return t("Please submit the pick order."); |
|
|
return t("Please submit the pick order."); |
|
|
case 'partially_completed': |
|
|
case 'partially_completed': |
|
|
@@ -384,7 +418,7 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
case 'unavailable': |
|
|
case 'unavailable': |
|
|
return t("This order is insufficient, please pick another lot."); |
|
|
return t("This order is insufficient, please pick another lot."); |
|
|
default: |
|
|
default: |
|
|
return t("Please finish QR code scan, QC check and pick order."); |
|
|
|
|
|
|
|
|
return t("Please finish QR code scan and pick order."); |
|
|
} |
|
|
} |
|
|
}, []); |
|
|
}, []); |
|
|
|
|
|
|
|
|
@@ -417,7 +451,15 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
pageSize: newPageSize, |
|
|
pageSize: newPageSize, |
|
|
}); |
|
|
}); |
|
|
}, []); |
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { |
|
|
|
|
|
if (!selectedRowId) return lot.availableQty; |
|
|
|
|
|
const lactualPickQty = lot.actualPickQty || 0; |
|
|
|
|
|
const actualPickQty = pickQtyData[selectedRowId]?.[lot.lotId] || 0; |
|
|
|
|
|
const remainingQty = lot.inQty - actualPickQty - lactualPickQty; |
|
|
|
|
|
|
|
|
|
|
|
// Ensure it doesn't go below 0 |
|
|
|
|
|
return Math.max(0, remainingQty); |
|
|
|
|
|
}, [selectedRowId, pickQtyData]); |
|
|
// ✅ Handle QR code submission |
|
|
// ✅ 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) { |
|
|
@@ -458,13 +500,19 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
<TableCell>{t("Lot#")}</TableCell> |
|
|
<TableCell>{t("Lot#")}</TableCell> |
|
|
<TableCell>{t("Lot Expiry Date")}</TableCell> |
|
|
<TableCell>{t("Lot Expiry Date")}</TableCell> |
|
|
<TableCell>{t("Lot Location")}</TableCell> |
|
|
<TableCell>{t("Lot Location")}</TableCell> |
|
|
<TableCell align="right">{t("Available Lot")}</TableCell> |
|
|
|
|
|
<TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> |
|
|
|
|
|
<TableCell>{t("Stock Unit")}</TableCell> |
|
|
<TableCell>{t("Stock Unit")}</TableCell> |
|
|
<TableCell align="center">{t("QR Code Scan")}</TableCell> |
|
|
|
|
|
|
|
|
<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="center">{t("Lot Actual Pick Qty")}</TableCell> |
|
|
<TableCell align="right">{t("Reject")}</TableCell> |
|
|
|
|
|
<TableCell align="center">{t("Submit")}</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> |
|
|
</TableRow> |
|
|
</TableHead> |
|
|
</TableHead> |
|
|
<TableBody> |
|
|
<TableBody> |
|
|
@@ -503,11 +551,102 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
<TableCell>{lot.expiryDate}</TableCell> |
|
|
<TableCell>{lot.expiryDate}</TableCell> |
|
|
<TableCell>{lot.location}</TableCell> |
|
|
<TableCell>{lot.location}</TableCell> |
|
|
<TableCell align="right">{lot.availableQty.toLocaleString()}</TableCell> |
|
|
|
|
|
<TableCell align="right">{lot.requiredQty.toLocaleString()}</TableCell> |
|
|
|
|
|
<TableCell>{lot.stockUnit}</TableCell> |
|
|
<TableCell>{lot.stockUnit}</TableCell> |
|
|
|
|
|
<TableCell align="right">{lot.requiredQty.toLocaleString()}</TableCell> |
|
|
|
|
|
<TableCell align="right">{lot.inQty.toLocaleString()??'0'}</TableCell> |
|
|
|
|
|
<TableCell align="center"> |
|
|
|
|
|
{/* Show QR Scan Button if not scanned, otherwise show TextField */} |
|
|
|
|
|
{!lot.stockOutLineId ? ( |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
size="small" |
|
|
|
|
|
onClick={() => { |
|
|
|
|
|
setSelectedLotForQr(lot); |
|
|
|
|
|
setQrModalOpen(true); |
|
|
|
|
|
resetScan(); |
|
|
|
|
|
}} |
|
|
|
|
|
// ✅ Disable when: |
|
|
|
|
|
// 1. Lot is expired or unavailable |
|
|
|
|
|
// 2. Not selected (selectedLotRowId doesn't match) |
|
|
|
|
|
disabled={ |
|
|
|
|
|
(lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') || |
|
|
|
|
|
selectedLotRowId !== `row_${index}` |
|
|
|
|
|
} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
fontSize: '0.7rem', |
|
|
|
|
|
py: 0.5, |
|
|
|
|
|
minHeight: '40px', // ✅ Match TextField height |
|
|
|
|
|
whiteSpace: 'nowrap', |
|
|
|
|
|
minWidth: '80px', // ✅ Match TextField width |
|
|
|
|
|
// ✅ Visual feedback |
|
|
|
|
|
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> |
|
|
|
|
|
) : ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
type="number" |
|
|
|
|
|
value={selectedRowId ? (pickQtyData[selectedRowId]?.[lot.lotId] || '') : ''} |
|
|
|
|
|
onChange={(e) => { |
|
|
|
|
|
if (selectedRowId) { |
|
|
|
|
|
const inputValue = e.target.value; |
|
|
|
|
|
// ✅ Fixed: Handle empty string and prevent leading zeros |
|
|
|
|
|
if (inputValue === '') { |
|
|
|
|
|
// Allow empty input (user can backspace to clear) |
|
|
|
|
|
onPickQtyChange(selectedRowId, lot.lotId, 0); |
|
|
|
|
|
} else { |
|
|
|
|
|
// Parse the number and prevent leading zeros |
|
|
|
|
|
const numValue = parseInt(inputValue, 10); |
|
|
|
|
|
if (!isNaN(numValue)) { |
|
|
|
|
|
onPickQtyChange(selectedRowId, lot.lotId, numValue); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
onBlur={(e) => { |
|
|
|
|
|
// ✅ Fixed: When input loses focus, ensure we have a valid number |
|
|
|
|
|
if (selectedRowId) { |
|
|
|
|
|
const currentValue = pickQtyData[selectedRowId]?.[lot.lotId]; |
|
|
|
|
|
if (currentValue === undefined || currentValue === null) { |
|
|
|
|
|
// Set to 0 if no value |
|
|
|
|
|
onPickQtyChange(selectedRowId, lot.lotId, 0); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
inputProps={{ |
|
|
|
|
|
min: 0, |
|
|
|
|
|
max: lot.availableQty, |
|
|
|
|
|
step: 1 // Allow only whole numbers |
|
|
|
|
|
}} |
|
|
|
|
|
// ✅ Allow input for available AND insufficient_stock lots |
|
|
|
|
|
disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
width: '80px', |
|
|
|
|
|
'& .MuiInputBase-root': { |
|
|
|
|
|
height: '40px', // ✅ Match table cell height |
|
|
|
|
|
}, |
|
|
|
|
|
'& .MuiInputBase-input': { |
|
|
|
|
|
height: '40px', |
|
|
|
|
|
padding: '8px 12px', // ✅ Adjust padding to center text vertically |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
placeholder="0" // Show placeholder instead of default value |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
{/*<TableCell align="right">{lot.availableQty.toLocaleString()}</TableCell>*/} |
|
|
|
|
|
<TableCell align="right">{calculateRemainingAvailableQty(lot).toLocaleString()}</TableCell> |
|
|
|
|
|
{/* <TableCell>{lot.stockUnit}</TableCell> */} |
|
|
|
|
|
|
|
|
{/* QR Code Scan Button */} |
|
|
{/* QR Code Scan Button */} |
|
|
|
|
|
{/* |
|
|
<TableCell align="center"> |
|
|
<TableCell align="center"> |
|
|
<Box sx={{ textAlign: 'center' }}> |
|
|
<Box sx={{ textAlign: 'center' }}> |
|
|
<Button |
|
|
<Button |
|
|
@@ -547,11 +686,11 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
> |
|
|
> |
|
|
{lot.stockOutLineId ? t("Scanned") : t("Scan")} |
|
|
{lot.stockOutLineId ? t("Scanned") : t("Scan")} |
|
|
</Button> |
|
|
</Button> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</Box> |
|
|
</Box> |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
|
|
|
|
|
|
|
|
|
*/} |
|
|
{/* QC Check Button */} |
|
|
{/* QC Check Button */} |
|
|
{/* |
|
|
{/* |
|
|
<TableCell align="center"> |
|
|
<TableCell align="center"> |
|
|
@@ -579,48 +718,9 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Lot Actual Pick Qty */} |
|
|
{/* Lot Actual Pick Qty */} |
|
|
<TableCell align="right"> |
|
|
|
|
|
<TextField |
|
|
|
|
|
type="number" |
|
|
|
|
|
value={selectedRowId ? (pickQtyData[selectedRowId]?.[lot.lotId] || '') : ''} // ✅ Fixed: Use empty string instead of 0 |
|
|
|
|
|
onChange={(e) => { |
|
|
|
|
|
if (selectedRowId) { |
|
|
|
|
|
const inputValue = e.target.value; |
|
|
|
|
|
// ✅ Fixed: Handle empty string and prevent leading zeros |
|
|
|
|
|
if (inputValue === '') { |
|
|
|
|
|
// Allow empty input (user can backspace to clear) |
|
|
|
|
|
onPickQtyChange(selectedRowId, lot.lotId, 0); |
|
|
|
|
|
} else { |
|
|
|
|
|
// Parse the number and prevent leading zeros |
|
|
|
|
|
const numValue = parseInt(inputValue, 10); |
|
|
|
|
|
if (!isNaN(numValue)) { |
|
|
|
|
|
onPickQtyChange(selectedRowId, lot.lotId, numValue); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
onBlur={(e) => { |
|
|
|
|
|
// ✅ Fixed: When input loses focus, ensure we have a valid number |
|
|
|
|
|
if (selectedRowId) { |
|
|
|
|
|
const currentValue = pickQtyData[selectedRowId]?.[lot.lotId]; |
|
|
|
|
|
if (currentValue === undefined || currentValue === null) { |
|
|
|
|
|
// Set to 0 if no value |
|
|
|
|
|
onPickQtyChange(selectedRowId, lot.lotId, 0); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
inputProps={{ |
|
|
|
|
|
min: 0, |
|
|
|
|
|
max: lot.availableQty, |
|
|
|
|
|
step: 1 // Allow only whole numbers |
|
|
|
|
|
}} |
|
|
|
|
|
// ✅ Allow input for available AND insufficient_stock lots |
|
|
|
|
|
disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'} |
|
|
|
|
|
sx={{ width: '80px' }} |
|
|
|
|
|
placeholder="0" // Show placeholder instead of default value |
|
|
|
|
|
/> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
|
|
|
|
|
|
<TableCell align="center"> |
|
|
<TableCell align="center"> |
|
|
|
|
|
<Stack direction="column" spacing={1} alignItems="center"> |
|
|
<Button |
|
|
<Button |
|
|
variant="outlined" |
|
|
variant="outlined" |
|
|
size="small" |
|
|
size="small" |
|
|
@@ -638,6 +738,9 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
if (onDataRefresh) { |
|
|
if (onDataRefresh) { |
|
|
await onDataRefresh(); |
|
|
await onDataRefresh(); |
|
|
} |
|
|
} |
|
|
|
|
|
if (onLotDataRefresh) { |
|
|
|
|
|
await onLotDataRefresh(); |
|
|
|
|
|
} |
|
|
} catch (error) { |
|
|
} catch (error) { |
|
|
console.error("Error rejecting lot:", error); |
|
|
console.error("Error rejecting lot:", error); |
|
|
} |
|
|
} |
|
|
@@ -655,9 +758,13 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
> |
|
|
> |
|
|
{t("Reject")} |
|
|
{t("Reject")} |
|
|
</Button> |
|
|
</Button> |
|
|
|
|
|
</Stack> |
|
|
|
|
|
{/*} |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
{/* Submit Button */} |
|
|
|
|
|
|
|
|
|
|
|
<TableCell align="center"> |
|
|
<TableCell align="center"> |
|
|
|
|
|
*/} |
|
|
|
|
|
<Stack direction="column" spacing={1} alignItems="center"> |
|
|
<Button |
|
|
<Button |
|
|
variant="contained" |
|
|
variant="contained" |
|
|
onClick={() => { |
|
|
onClick={() => { |
|
|
@@ -681,6 +788,7 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
> |
|
|
> |
|
|
{t("Submit")} |
|
|
{t("Submit")} |
|
|
</Button> |
|
|
</Button> |
|
|
|
|
|
</Stack> |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
</TableRow> |
|
|
</TableRow> |
|
|
)) |
|
|
)) |
|
|
@@ -695,7 +803,7 @@ const LotTable: React.FC<LotTableProps> = ({ |
|
|
{paginatedLotTableData.map((lot, index) => ( |
|
|
{paginatedLotTableData.map((lot, index) => ( |
|
|
<Box key={lot.id} sx={{ mb: 1 }}> |
|
|
<Box key={lot.id} sx={{ mb: 1 }}> |
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
<strong>Lot {lot.lotNo}:</strong> {getStatusMessage(lot)} |
|
|
|
|
|
|
|
|
<strong>{t("Lot")} {lot.lotNo}:</strong> {getStatusMessage(lot)} |
|
|
</Typography> |
|
|
</Typography> |
|
|
</Box> |
|
|
</Box> |
|
|
))} |
|
|
))} |
|
|
|