import { PureComponent } from 'react'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; import CircularProgress from '@mui/material/CircularProgress'; import IconButton from '@mui/material/IconButton'; import Stack from '@mui/material/Stack'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import TextInput from '../TextInput'; import Typography from '@mui/material/Typography'; import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined'; import { enqueueSnackbar } from 'notistack'; import BackendError from '../BackendError'; import { number as numberApi } from '../../api'; import companyAutomat from '../../automats/companyAutomat'; import { requireDefined, requireObject } from '../../utils/apiShape'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; function parsePadding(value) { const n = Number(value); return Number.isInteger(n) && n > 0 ? n : null; } function parseNext(value) { const n = Number(value); return Number.isInteger(n) && n >= 0 ? n : null; } function buildDrafts(numbers, prevDrafts = {}) { return Object.fromEntries( (Array.isArray(numbers) ? numbers : []).map((row) => { const existing = prevDrafts[row.id]; return [ row.id, existing?.busy ? existing : { prefix: row.prefix ?? '', padding: row.padding != null ? String(row.padding) : '', next: row.next != null ? String(row.next) : '', busy: false, saveError: null, }, ]; }), ); } export default class NumberSettingsPanel extends PureComponent { constructor(props) { super(props); const numbers = Array.isArray(companyAutomat.state.numbers) ? companyAutomat.state.numbers : []; const numbersStatus = companyAutomat.state.numbersStatus; this.state = { isAdmin: companyAutomat.state.current?.role === 'admin', status: numbersStatus === 'ready' ? 'ready' : numbersStatus === 'failed' ? 'failed' : 'loading', error: companyAutomat.state.numbersError ? backendErrorFrom(companyAutomat.state.numbersError) : null, numbers, // id → { prefix, padding, next, busy, saveError } drafts: buildDrafts(numbers), }; this.handleRetry = this.handleRetry.bind(this); this.handleSave = this.handleSave.bind(this); } componentDidMount() { this._unmounted = false; this.unsub = companyAutomat.subscribe(this, (state) => { const numbers = Array.isArray(state.numbers) ? state.numbers : []; return { isAdmin: state.current?.role === 'admin', status: state.numbersStatus === 'ready' ? 'ready' : state.numbersStatus === 'failed' ? 'failed' : 'loading', error: state.numbersError ? backendErrorFrom(state.numbersError) : null, numbers, drafts: numbers !== this.state.numbers ? buildDrafts(numbers, this.state.drafts) : this.state.drafts, }; }); } async load() { this.setState({ status: 'loading', error: null }); try { await companyAutomat.actions.reloadNumbers(); } catch (err) { notifyError(err); this.setState({ status: 'failed', error: backendErrorFrom(err) }); } } handleRetry() { void this.load(); } handleChange(id, field, value) { this.setState((prev) => ({ drafts: { ...prev.drafts, [id]: { ...prev.drafts[id], [field]: value, saveError: null }, }, })); } async handleSave(id) { if (!this.state.isAdmin) return; const draft = this.state.drafts[id]; if (!draft || draft.busy) return; const row = this.state.numbers.find((r) => r.id === id); if (!row) return; const body = {}; let invalid = false; let errorMessage = null; if (draft.prefix !== (row.prefix ?? '')) { body.prefix = draft.prefix; } if (draft.padding !== (row.padding != null ? String(row.padding) : '')) { const padding = parsePadding(draft.padding); if (padding == null) { invalid = true; errorMessage = 'Stellenzahl muss eine positive ganze Zahl sein.'; } else { body.padding = padding; } } if (draft.next !== (row.next != null ? String(row.next) : '')) { const next = parseNext(draft.next); if (next == null) { invalid = true; errorMessage = 'Nächste Nummer muss eine nicht-negative ganze Zahl sein.'; } else { body.next = next; } } if (Object.keys(body).length === 0) { return; } if (invalid) { this.setState((prev) => ({ drafts: { ...prev.drafts, [id]: { ...prev.drafts[id], saveError: errorMessage }, }, })); return; } this.setState((prev) => ({ drafts: { ...prev.drafts, [id]: { ...prev.drafts[id], busy: true, saveError: null }, }, })); try { const data = await numberApi.update(id, body); requireObject(data, 'PATCH /api/number'); requireObject(data.number, 'number'); const updated = requireDefined(data.number, 'number'); const numbers = companyAutomat.actions.replaceNumbers( this.state.numbers.map((n) => (n.id === id ? { ...n, ...updated } : n)), ); this.setState({ numbers, drafts: { ...this.state.drafts, [id]: { prefix: updated.prefix ?? '', padding: updated.padding != null ? String(updated.padding) : '', next: updated.next != null ? String(updated.next) : '', busy: false, saveError: null, }, }, }); enqueueSnackbar('Nummernkreis gespeichert.', { variant: 'success' }); } catch (err) { if (this._unmounted) return; this.setState((prev) => ({ drafts: { ...prev.drafts, [id]: { ...prev.drafts[id], busy: false, saveError: backendErrorFrom(err) }, }, })); } } componentWillUnmount() { this._unmounted = true; this.unsub?.(); } render() { const { isAdmin, status, error, numbers, drafts } = this.state; if (status === 'loading') { return ( ); } if (status === 'failed') { return ; } return ( Typ Präfix Stellenzahl Nächste Nummer {numbers.map((row) => { const draft = drafts[row.id]; if (!draft) return null; return ( {row.type} this.handleChange(row.id, 'prefix', e.target.value)} sx={{ input: { fontFamily: 'monospace' } }} /> this.handleChange(row.id, 'padding', e.target.value)} sx={{ input: { fontFamily: 'monospace' } }} /> this.handleChange(row.id, 'next', e.target.value)} sx={{ input: { fontFamily: 'monospace' } }} /> {draft.saveError ? ( {typeof draft.saveError === 'string' ? draft.saveError : draft.saveError.message} ) : null} void this.handleSave(row.id)} > ); })}
); } }