import { PureComponent } from 'react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; import IconButton from '@mui/material/IconButton'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import GroupsIcon from '@mui/icons-material/Groups'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import { enqueueSnackbar } from 'notistack'; import BackendError from '../../components/BackendError'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; import { PanelTile, PanelTileGrid } from 'dashgui/panels'; import { StorageManagerContext } from '../../api/storageManagerContext'; import { requireArray, requireDefined, requireObject } from '../../utils/apiShape'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; function sortGroups(groups) { return [...groups].sort((a, b) => { const bySort = a.sort - b.sort; if (bySort !== 0) return bySort; const byName = String(a.name).localeCompare(String(b.name), 'de'); if (byName !== 0) return byName; return a.id - b.id; }); } function draftsFrom(groups) { return Object.fromEntries(groups.map((row) => [row.id, row.name])); } function requireGroupListItem(row, path) { requireObject(row, path); requireDefined(row.id, `${path}.id`); requireDefined(row.name, `${path}.name`); requireDefined(row.sort, `${path}.sort`); requireDefined(row.editable, `${path}.editable`); } class KundeGruppenPage extends PureComponent { static contextType = StorageManagerContext; constructor(props) { super(props); this.state = { status: 'loading', error: null, groups: [], drafts: {}, newName: '', busyKey: null, }; this._cancelled = false; this.handleCreate = this.handleCreate.bind(this); this.handleRetry = this.handleRetry.bind(this); } componentDidMount() { this._cancelled = false; void this.load(); } componentWillUnmount() { this._cancelled = true; } applyGroups(groups) { groups.forEach((row, index) => { requireGroupListItem(row, `customergroups[${index}]`); }); const sorted = sortGroups(groups); this.setState({ status: 'ready', error: null, groups: sorted, drafts: draftsFrom(sorted), }); } async load() { this.setState({ status: 'loading', error: null }); try { const list = this.context.customergroups ?? (await this.context.customergroupPromise); if (this._cancelled) return; this.applyGroups(list); } catch (err) { if (this._cancelled) return; this.setState({ status: 'failed', error: backendErrorFrom(err) }); } } handleRetry() { void this.load(); } async refreshFromServer() { const list = await this.context.updateCustomergroup(); if (this._cancelled) return; this.applyGroups(list); } async handleCreate() { const name = this.state.newName.trim(); if (!name || this.state.busyKey != null) return; this.setState({ busyKey: 'new' }); try { const data = await this.context.customergroup.create({ name }); requireObject(data, 'POST /api/customergroup'); requireObject(data.customergroup, 'customergroup'); requireDefined(data.customergroup.id, 'customergroup.id'); requireDefined(data.customergroup.sort, 'customergroup.sort'); await this.refreshFromServer(); if (this._cancelled) return; this.setState({ newName: '', busyKey: null }); enqueueSnackbar('Kundengruppe angelegt.', { variant: 'success' }); } catch (err) { if (this._cancelled) return; notifyError(err); this.setState({ busyKey: null }); } } async handleRename(group) { if (!group.editable) return; const draft = String(this.state.drafts[group.id] ?? '').trim(); if (!draft) { this.setState((prev) => ({ drafts: { ...prev.drafts, [group.id]: group.name }, })); return; } if (draft === group.name || this.state.busyKey != null) return; this.setState({ busyKey: group.id }); try { const data = await this.context.customergroup.update(group.id, { name: draft, }); requireObject(data, 'PATCH /api/customergroup/:id'); requireObject(data.customergroup, 'customergroup'); requireDefined(data.customergroup.id, 'customergroup.id'); requireDefined(data.customergroup.sort, 'customergroup.sort'); await this.refreshFromServer(); if (this._cancelled) return; this.setState({ busyKey: null }); enqueueSnackbar('Kundengruppe gespeichert.', { variant: 'success' }); } catch (err) { if (this._cancelled) return; notifyError(err); this.setState((prev) => ({ busyKey: null, drafts: { ...prev.drafts, [group.id]: group.name }, })); } } async handleMove(group, delta) { const { groups, busyKey } = this.state; if (busyKey != null) return; const index = groups.findIndex((row) => row.id === group.id); const targetIndex = index + delta; if (index < 0 || targetIndex < 0 || targetIndex >= groups.length) return; const next = [...groups]; const [row] = next.splice(index, 1); next.splice(targetIndex, 0, row); const customergroupIds = next.map((item) => item.id); this.setState({ busyKey: group.id }); try { const data = await this.context.customergroup.order({ customergroupIds }); requireObject(data, 'PUT /api/customergroup/order'); requireArray(data.customergroupIds, 'customergroupIds'); await this.refreshFromServer(); if (this._cancelled) return; this.setState({ busyKey: null }); } catch (err) { if (this._cancelled) return; notifyError(err); try { await this.refreshFromServer(); } catch (refreshErr) { notifyError(refreshErr); } if (this._cancelled) return; this.setState({ busyKey: null }); } } async handleDelete(group) { if (!group.editable || this.state.busyKey != null) return; if (!window.confirm(`Kundengruppe „${group.name}“ wirklich löschen?`)) { return; } this.setState({ busyKey: group.id }); try { await this.context.customergroup.delete(group.id); await this.refreshFromServer(); if (this._cancelled) return; this.setState({ busyKey: null }); enqueueSnackbar('Kundengruppe gelöscht.', { variant: 'success' }); } catch (err) { if (this._cancelled) return; notifyError(err); this.setState({ busyKey: null }); } } render() { const { status, error, groups, drafts, newName, busyKey } = this.state; const busy = busyKey != null; if (status === 'loading') { return ( ); } if (status === 'failed') { return ; } return ( {groups.length === 0 ? ( Noch keine Kundengruppen. ) : ( groups.map((group, index) => { const editable = group.editable === true; return ( this.setState((prev) => ({ drafts: { ...prev.drafts, [group.id]: next }, })) } onBlur={() => void this.handleRename(group)} onKeyDown={(event) => { if (!editable) return; if (event.key === 'Enter') { event.preventDefault(); event.target.blur(); } if (event.key === 'Escape') { event.preventDefault(); this.setState((prev) => ({ drafts: { ...prev.drafts, [group.id]: group.name, }, })); event.target.blur(); } }} sx={{ flex: 1 }} /> void this.handleMove(group, -1)} > = groups.length - 1} onClick={() => void this.handleMove(group, 1)} > void this.handleDelete(group)} > ); }) )} this.setState({ newName: next })} onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault(); void this.handleCreate(); } }} sx={{ flex: 1 }} /> ); } } export default KundeGruppenPage;