Files
dashfront/src/components/article/ArticlePriceTiersDialog.jsx
seb 335f808421 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.
2026-08-01 05:44:34 +02:00

186 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;