Files
dashfront/src/components/article/ArticleUnitEditorDialog.jsx
2026-08-05 08:48:12 +02:00

317 lines
10 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 DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import CircularProgress from '@mui/material/CircularProgress';
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 List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { requireObject } from '../../utils/apiShape';
import { notifyError } from '../../utils/notify';
import { parsePriceInput } from '../../utils/price';
import { COMMON_UNITS, DEFAULT_UNIT_SI, unitLabel } from '../../utils/units';
function emptyForm() {
return {
si: '',
basesi: '',
basesifactor: '1',
symbol: '',
name: '',
};
}
function formFromUnit(row) {
// API may return null symbol/name (e.g. default pc).
return {
si: row.si ?? '',
basesi: row.basesi ?? '',
basesifactor: String(row.basesifactor ?? '1'),
symbol: row.symbol ?? '',
name: row.name ?? '',
};
}
function parseFactor(value) {
return parsePriceInput(value);
}
function formValid(form) {
const si = String(form.si ?? '').trim();
const basesi = String(form.basesi ?? '').trim();
const symbol = String(form.symbol ?? '').trim();
const name = String(form.name ?? '').trim();
const factor = parseFactor(form.basesifactor);
return (
si.length > 0 &&
basesi.length > 0 &&
symbol.length > 0 &&
name.length > 0 &&
factor != null &&
factor > 0
);
}
export default class ArticleUnitEditorDialog extends Component {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
busy: false,
units: [],
unitsLoading: false,
form: emptyForm(),
selectedSi: null,
};
}
componentDidUpdate(prevProps) {
if (this.props.open && !prevProps.open) {
this.setState({ form: emptyForm(), selectedSi: null });
void this.loadUnits();
}
}
async loadUnits() {
this.setState({ unitsLoading: true });
try {
const units = await this.context.unitPromise;
this.setState({ units, unitsLoading: false });
} catch (err) {
this.setState({ unitsLoading: false });
notifyError(err);
}
}
handleSelectUnit = (row) => {
this.setState({ form: formFromUnit(row), selectedSi: row.si });
};
handlePreset = (preset) => {
this.setState({ form: formFromUnit(preset), selectedSi: preset.si });
};
handleField = (field, value) => {
this.setState((state) => {
const form = { ...state.form, [field]: value };
if (field === 'si' && (!state.form.basesi || state.form.basesi === state.form.si)) {
form.basesi = value;
}
return { form };
});
};
handleSave = async () => {
const { form } = this.state;
if (!formValid(form)) return;
this.setState({ busy: true });
try {
const data = await this.context.unit.upsert({
si: String(form.si ?? '').trim(),
basesi: String(form.basesi ?? '').trim(),
basesifactor: parseFactor(form.basesifactor),
symbol: String(form.symbol ?? '').trim(),
name: String(form.name ?? '').trim(),
});
requireObject(data, 'PUT /api/unit');
requireObject(data.unit, 'unit');
const units = await this.context.updateUnit();
this.setState({
busy: false,
units,
form: formFromUnit(data.unit),
selectedSi: data.unit.si,
});
this.props.onUnitUpserted?.(data.unit);
} catch (err) {
this.setState({ busy: false });
notifyError(err);
}
};
handleDelete = async () => {
const { selectedSi } = this.state;
if (!selectedSi || selectedSi === DEFAULT_UNIT_SI) return;
this.setState({ busy: true });
try {
await this.context.unit.delete(selectedSi);
const units = await this.context.updateUnit();
this.setState({ busy: false, units, form: emptyForm(), selectedSi: null });
this.props.onUnitDeleted?.(selectedSi);
} catch (err) {
this.setState({ busy: false });
notifyError(err);
}
};
handleNew = () => {
this.setState({ form: emptyForm(), selectedSi: null });
};
render() {
const { open, onClose } = this.props;
const { busy, form, selectedSi, units, unitsLoading } = this.state;
const canSave = formValid(form) && !busy;
const loading = unitsLoading && units.length === 0;
return (
<Dialog open={open} onClose={busy ? undefined : onClose} maxWidth="md" fullWidth>
<DialogTitle>Einheiten</DialogTitle>
<DialogContent dividers>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>
Typische Einheiten
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75, mb: 2 }}>
{COMMON_UNITS.map((preset) => (
<Chip
key={preset.si}
size="small"
label={`${preset.symbol} (${preset.si})`}
onClick={() => this.handlePreset(preset)}
disabled={busy}
/>
))}
</Box>
<Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: '1fr 1.2fr' },
gap: 2,
minHeight: 280,
}}
>
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
Firma
</Typography>
{loading ? (
<CircularProgress size={28} sx={{ mt: 2 }} />
) : (
<List dense disablePadding sx={{ maxHeight: 320, overflow: 'auto' }}>
{units.map((row) => (
<ListItem
key={row.si}
disablePadding
secondaryAction={
selectedSi === row.si && row.si !== DEFAULT_UNIT_SI ? (
<IconButton
edge="end"
aria-label="Löschen"
disabled={busy}
onClick={() => void this.handleDelete()}
>
<DeleteOutlineOutlinedIcon />
</IconButton>
) : null
}
>
<ListItemButton
selected={selectedSi === row.si}
onClick={() => this.handleSelectUnit(row)}
>
<ListItemText
primary={unitLabel(row)}
secondary={`si ${row.si} · ${row.basesifactor} × ${row.basesi}`}
/>
</ListItemButton>
</ListItem>
))}
{units.length === 0 ? (
<Typography color="text.secondary" sx={{ py: 2 }}>
Noch keine Einheiten.
</Typography>
) : null}
</List>
)}
<Button type="button" onClick={this.handleNew} disabled={busy} sx={{ mt: 1 }}>
Neu
</Button>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<TextField
label="Name"
value={form.name}
onChange={(e) => this.handleField('name', e.target.value)}
size="small"
fullWidth
disabled={busy}
slotProps={{ inputLabel: { shrink: true } }}
/>
<TextField
label="Symbol"
value={form.symbol}
onChange={(e) => this.handleField('symbol', e.target.value)}
size="small"
fullWidth
disabled={busy}
slotProps={{ inputLabel: { shrink: true } }}
/>
<TextField
label="si"
value={form.si}
onChange={(e) => this.handleField('si', e.target.value)}
size="small"
fullWidth
disabled={busy}
slotProps={{ inputLabel: { shrink: true } }}
/>
<TextField
label="basesi"
value={form.basesi}
onChange={(e) => this.handleField('basesi', e.target.value)}
size="small"
fullWidth
disabled={busy}
slotProps={{ inputLabel: { shrink: true } }}
/>
<TextField
label="basesifactor"
value={form.basesifactor}
onChange={(e) => this.handleField('basesifactor', e.target.value)}
size="small"
fullWidth
disabled={busy}
error={
form.basesifactor !== '' &&
(parseFactor(form.basesifactor) == null ||
parseFactor(form.basesifactor) <= 0)
}
slotProps={{
inputLabel: { shrink: true },
htmlInput: { inputMode: 'decimal' },
}}
/>
<Button
type="button"
variant="contained"
onClick={() => void this.handleSave()}
disabled={!canSave}
>
{busy ? <CircularProgress size={22} color="inherit" /> : 'Speichern'}
</Button>
</Box>
</Box>
</DialogContent>
<DialogActions sx={{ px: 3, py: 2 }}>
<Button type="button" onClick={onClose} disabled={busy}>
Schließen
</Button>
</DialogActions>
</Dialog>
);
}
}