Wire PUT /prices, de-DE formatting, tax/staffel pills, and keep the category list cache in sync after photo/price updates.
186 lines
5.8 KiB
JavaScript
186 lines
5.8 KiB
JavaScript
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;
|