Files
dashfront/src/components/article/ArticleUnitEditorDialog.jsx
2026-10-28 00:46:23 +01:00

308 lines
9.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 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 AppDialog from '../AppDialog';
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 TextInput from '../TextInput';
import Typography from '@mui/material/Typography';
import companyAutomat from '../../automats/companyAutomat';
import { unit as unitApi } from '../../api';
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 {
constructor(props) {
super(props);
this.state = {
busy: false,
units: companyAutomat.state.units,
unitsLoading: companyAutomat.state.unitsStatus === 'loading',
form: emptyForm(),
selectedSi: null,
};
}
componentDidMount() {
this.unsub = companyAutomat.subscribe(this, (state) => ({
units: state.units,
unitsLoading: state.unitsStatus === 'loading',
}));
}
componentWillUnmount() {
this.unsub?.();
}
componentDidUpdate(prevProps) {
if (this.props.open && !prevProps.open) {
this.setState({ form: emptyForm(), selectedSi: null });
}
}
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 payload = {
si: String(form.si ?? '').trim(),
basesi: String(form.basesi ?? '').trim(),
basesifactor: parseFactor(form.basesifactor),
symbol: String(form.symbol ?? '').trim(),
name: String(form.name ?? '').trim(),
};
const units = await companyAutomat.actions.upsertUnit(payload);
const saved = units.find((u) => u.si === payload.si) ?? payload;
this.setState({
busy: false,
units,
form: formFromUnit(saved),
selectedSi: saved.si,
});
this.props.onUnitUpserted?.(saved);
} 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 companyAutomat.actions.deleteUnit(selectedSi);
this.setState({
busy: false,
units: companyAutomat.state.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 (
<AppDialog 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 }}>
<TextInput
label="Name"
value={form.name}
onChange={(e) => this.handleField('name', e.target.value)}
fullWidth
disabled={busy}
/>
<TextInput
label="Symbol"
value={form.symbol}
onChange={(e) => this.handleField('symbol', e.target.value)}
fullWidth
disabled={busy}
/>
<TextInput
label="si"
value={form.si}
onChange={(e) => this.handleField('si', e.target.value)}
fullWidth
disabled={busy}
/>
<TextInput
label="basesi"
value={form.basesi}
onChange={(e) => this.handleField('basesi', e.target.value)}
fullWidth
disabled={busy}
/>
<TextInput
label="basesifactor"
value={form.basesifactor}
onChange={(e) => this.handleField('basesifactor', e.target.value)}
fullWidth
disabled={busy}
error={
form.basesifactor !== '' &&
(parseFactor(form.basesifactor) == null ||
parseFactor(form.basesifactor) <= 0)
}
slotProps={{
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>
</AppDialog>
);
}
}