CANCERYS\kw093 пре 3 месеци
родитељ
комит
b7755acd2e
3 измењених фајлова са 210 додато и 193 уклоњено
  1. +172
    -162
      src/components/PickOrderSearch/LotTable.tsx
  2. +24
    -30
      src/components/PickOrderSearch/PickExecutionForm.tsx
  3. +14
    -1
      src/i18n/zh/pickOrder.json

+ 172
- 162
src/components/PickOrderSearch/LotTable.tsx Прегледај датотеку

@@ -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"


+ 24
- 30
src/components/PickOrderSearch/PickExecutionForm.tsx Прегледај датотеку

@@ -168,7 +168,17 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
const newErrors: FormErrors = {};
if (formData.actualPickQty === undefined || formData.actualPickQty < 0) {
newErrors.actualPickQty = t('pickOrder.validation.actualPickQtyRequired');
newErrors.actualPickQty = t('Qty is required');
}
// ✅ ADD: Check if actual pick qty exceeds remaining available qty
if (formData.actualPickQty && formData.actualPickQty > remainingAvailableQty) {
newErrors.actualPickQty = t('Qty is not allowed to be greater than remaining available qty');
}
// ✅ ADD: Check if actual pick qty exceeds required qty
if (formData.actualPickQty && formData.actualPickQty > requiredQty) {
newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty');
}
// ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported)
@@ -176,24 +186,8 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0;
if (!hasMissQty && !hasBadItemQty) {
newErrors.missQty = t('pickOrder.validation.mustReportMissOrBadItems');
newErrors.badItemQty = t('pickOrder.validation.mustReportMissOrBadItems');
}
if (formData.missQty && formData.missQty < 0) {
newErrors.missQty = t('pickOrder.validation.missQtyInvalid');
}
if (formData.badItemQty && formData.badItemQty < 0) {
newErrors.badItemQty = t('pickOrder.validation.badItemQtyInvalid');
}
if (formData.badItemQty && formData.badItemQty > 0 && !formData.issueRemark) {
newErrors.issueRemark = t('pickOrder.validation.issueRemarkRequired');
}
if (formData.badItemQty && formData.badItemQty > 0 && !formData.handledBy) {
newErrors.handledBy = t('pickOrder.validation.handlerRequired');
newErrors.missQty = t('At least one issue must be reported');
newErrors.badItemQty = t('At least one issue must be reported');
}
setErrors(newErrors);
@@ -250,34 +244,34 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
<Grid item xs={6}>
<TextField
fullWidth
label={t('requiredQty')}
label={t('Required Qty')}
value={requiredQty || 0}
disabled
variant="outlined"
helperText={t('Still need to pick')}
//helperText={t('Still need to pick')}
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label={t('remainingAvailableQty')}
label={t('Remaining Available Qty')}
value={remainingAvailableQty}
disabled
variant="outlined"
helperText={t('Available in warehouse')}
//helperText={t('Available in warehouse')}
/>
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t('actualPickQty')}
label={t('Actual Pick Qty')}
type="number"
value={formData.actualPickQty || 0}
onChange={(e) => handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)}
error={!!errors.actualPickQty}
helperText={errors.actualPickQty || t('Enter the quantity actually picked')}
helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, requiredQty)}`}
variant="outlined"
/>
</Grid>
@@ -285,12 +279,12 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
<Grid item xs={12}>
<TextField
fullWidth
label={t('missQty')}
label={t('Missing item Qty')}
type="number"
value={formData.missQty || 0}
onChange={(e) => handleInputChange('missQty', parseFloat(e.target.value) || 0)}
error={!!errors.missQty}
helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')}
//helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')}
variant="outlined"
/>
</Grid>
@@ -298,12 +292,12 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
<Grid item xs={12}>
<TextField
fullWidth
label={t('badItemQty')}
label={t('Bad item Qty')}
type="number"
value={formData.badItemQty || 0}
onChange={(e) => handleInputChange('badItemQty', parseFloat(e.target.value) || 0)}
error={!!errors.badItemQty}
helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')}
//helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')}
variant="outlined"
/>
</Grid>
@@ -355,7 +349,7 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={loading}>
{t('cancel')}
{t('Cancel')}
</Button>
<Button
onClick={handleSubmit}


+ 14
- 1
src/i18n/zh/pickOrder.json Прегледај датотеку

@@ -221,5 +221,18 @@
"Please assgin/release the pickorders to picker": "請分派/放單提料單給提料員。",
"Assign To": "分派給",
"No Group": "沒有分組",
"Selected items will join above created group": "已選擇的貨品將加入以上建立的分組"
"Selected items will join above created group": "已選擇的貨品將加入以上建立的分組",
"Issue":"問題",
"Pick Execution Issue Form":"提料問題表單",
"This form is for reporting issues only. You must report either missing items or bad items.":"此表單僅用於報告問題。您必須報告缺少的貨品或不良貨品。",
"Bad item Qty":"不良貨品數量",
"Missing item Qty":"缺少貨品數量",
"Actual Pick Qty":"實際提料數量",
"Required Qty":"所需數量",
"Issue Remark":"問題描述",
"Handler":"處理者",
"Qty is required":"必需輸入數量",
"Qty is not allowed to be greater than remaining available qty":"輸入數量不能大於剩餘可用數量",
"Qty is not allowed to be greater than required qty":"輸入數量不能大於所需數量",
"At least one issue must be reported":"至少需要報告一個問題"
}

Loading…
Откажи
Сачувај