Add editable article prices with gross input and staffel tiers.
Wire PUT /prices, de-DE formatting, tax/staffel pills, and keep the category list cache in sync after photo/price updates.
This commit is contained in:
185
src/components/article/ArticlePriceTiersDialog.jsx
Normal file
185
src/components/article/ArticlePriceTiersDialog.jsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { Component } from 'react';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { formatPriceAmount, parsePriceInput } from '../../utils/price';
|
||||
|
||||
function emptyTier() {
|
||||
return { minmultiplier: '', gross: '' };
|
||||
}
|
||||
|
||||
function parseMultiplier(value) {
|
||||
if (value === '' || value == null) return null;
|
||||
const n = Number(String(value).trim());
|
||||
return Number.isInteger(n) && n >= 2 ? n : null;
|
||||
}
|
||||
|
||||
function tiersValid(tiers) {
|
||||
const seen = new Set();
|
||||
for (const tier of tiers) {
|
||||
const mult = parseMultiplier(tier.minmultiplier);
|
||||
const gross = parsePriceInput(tier.gross);
|
||||
if (mult == null || gross == null) return false;
|
||||
if (seen.has(mult)) return false;
|
||||
seen.add(mult);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
class ArticlePriceTiersDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { tiers: [] };
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.open && !prevProps.open) {
|
||||
const tiers = this.props.tiers?.length
|
||||
? this.props.tiers.map((row) => ({
|
||||
minmultiplier: String(row.minmultiplier),
|
||||
gross: row.gross,
|
||||
}))
|
||||
: [];
|
||||
this.setState({ tiers });
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (index, field, value) => {
|
||||
this.setState((state) => {
|
||||
const tiers = state.tiers.map((row, i) =>
|
||||
i === index ? { ...row, [field]: value } : row,
|
||||
);
|
||||
return { tiers };
|
||||
});
|
||||
};
|
||||
|
||||
handleAdd = () => {
|
||||
this.setState((state) => ({ tiers: [...state.tiers, emptyTier()] }));
|
||||
};
|
||||
|
||||
handleRemove = (index) => {
|
||||
this.setState((state) => ({
|
||||
tiers: state.tiers.filter((_, i) => i !== index),
|
||||
}));
|
||||
};
|
||||
|
||||
handleApply = () => {
|
||||
const { tiers } = this.state;
|
||||
if (!tiersValid(tiers)) return;
|
||||
this.props.onApply(
|
||||
tiers.map((row) => ({
|
||||
minmultiplier: parseMultiplier(row.minmultiplier),
|
||||
gross: formatPriceAmount(parsePriceInput(row.gross)),
|
||||
})),
|
||||
);
|
||||
this.props.onClose();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { open, onClose } = this.props;
|
||||
const { tiers } = this.state;
|
||||
const canApply = tiersValid(tiers);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Staffelpreise</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{tiers.length === 0 ? (
|
||||
<Typography color="text.secondary" sx={{ py: 1 }}>
|
||||
Keine Staffeln. Basispreis bleibt der Einzelpreis (×1).
|
||||
</Typography>
|
||||
) : null}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
{tiers.map((tier, index) => {
|
||||
const mult = parseMultiplier(tier.minmultiplier);
|
||||
const gross = parsePriceInput(tier.gross);
|
||||
return (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Ab Menge"
|
||||
value={tier.minmultiplier}
|
||||
onChange={(event) =>
|
||||
this.handleChange(index, 'minmultiplier', event.target.value)
|
||||
}
|
||||
size="small"
|
||||
error={tier.minmultiplier !== '' && mult == null}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
htmlInput: { inputMode: 'numeric' },
|
||||
}}
|
||||
sx={{ width: 120 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Brutto"
|
||||
value={tier.gross}
|
||||
onChange={(event) =>
|
||||
this.handleChange(index, 'gross', event.target.value)
|
||||
}
|
||||
size="small"
|
||||
fullWidth
|
||||
error={tier.gross !== '' && gross == null}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">€</InputAdornment>
|
||||
),
|
||||
},
|
||||
htmlInput: { inputMode: 'decimal' },
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
type="button"
|
||||
aria-label="Staffel entfernen"
|
||||
onClick={() => this.handleRemove(index)}
|
||||
sx={{ mt: 0.5 }}
|
||||
>
|
||||
<DeleteOutlineOutlinedIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Button
|
||||
type="button"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={this.handleAdd}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Staffel hinzufügen
|
||||
</Button>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, py: 2 }}>
|
||||
<Button type="button" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="contained"
|
||||
onClick={this.handleApply}
|
||||
disabled={!canApply}
|
||||
>
|
||||
Übernehmen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ArticlePriceTiersDialog;
|
||||
Reference in New Issue
Block a user