From 87acdd929eb7a4fd80081a9ab0a4c77899c40f25 Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 8 Aug 2026 09:03:44 +0200 Subject: [PATCH] Add Kundennummer/Nummernkreise settings and settings dialog. Add number & setting API endpoints, StorageManager caches, and a Settings dialog (tabs for settings keys and number ranges) reachable from the user menu. Show Kundennummer and creation date in the Kunde list, and refactor Neuer Kunde to show an auto-generated number with group select. --- src/api/ApiClient.js | 19 ++ src/api/StorageManager.js | 12 + src/api/index.js | 25 ++ src/components/UserMenu.jsx | 22 +- .../settings/NumberSettingsPanel.jsx | 286 ++++++++++++++++++ src/components/settings/SettingsDialog.jsx | 76 +++++ src/components/settings/SettingsKeysPanel.jsx | 237 +++++++++++++++ src/components/settings/index.js | 3 + src/pages/kunde/index.jsx | 9 + src/pages/kunde/neu.jsx | 131 +++----- 10 files changed, 731 insertions(+), 89 deletions(-) create mode 100644 src/components/settings/NumberSettingsPanel.jsx create mode 100644 src/components/settings/SettingsDialog.jsx create mode 100644 src/components/settings/SettingsKeysPanel.jsx create mode 100644 src/components/settings/index.js diff --git a/src/api/ApiClient.js b/src/api/ApiClient.js index 8ab5273..acb8283 100644 --- a/src/api/ApiClient.js +++ b/src/api/ApiClient.js @@ -146,6 +146,8 @@ export class ApiClient { update: (id, body) => this.#json('PATCH', `/api/entity/${enc(id)}`, { body }), /** DELETE /api/entity/{id} → 204 */ delete: (id) => this.#empty('DELETE', `/api/entity/${enc(id)}`), + /** GET /api/entity/next-number → { number: string } */ + nextNumber: () => this.#json('GET', '/api/entity/next-number'), /** GET /api/entity/{entityId}/address → { addresses } */ listAddresses: (entityId) => this.#json('GET', `/api/entity/${enc(entityId)}/address`), @@ -287,6 +289,23 @@ export class ApiClient { setOffAll: (id) => this.#json('DELETE', `/api/posterminal/${enc(id)}/all`), }; + this.number = { + /** GET /api/number → { numbers: [{ id, type, prefix, padding, next }] } */ + list: () => this.#json('GET', '/api/number'), + /** PATCH /api/number/{id} — body: { prefix?, padding?, next? } (only present keys patched) */ + update: (id, body) => this.#json('PATCH', `/api/number/${enc(id)}`, { body }), + }; + + this.setting = { + /** GET /api/company/setting?frontendOnly=1 → { settings: [{ key, value, frontend }] } */ + list: (query) => this.#json('GET', '/api/company/setting', { query }), + /** GET /api/company/setting/{key} → { setting: { key, value, frontend } } */ + get: (key) => this.#json('GET', `/api/company/setting/${enc(key)}`), + /** PUT /api/company/setting/{key} body { value, frontend } → { setting: { key, value, frontend } } */ + upsert: (key, body) => + this.#json('PUT', `/api/company/setting/${enc(key)}`, { body }), + }; + this.image = { /** GET /api/image/{fullHash} → AVIF blob */ get: (fullHash) => this.#blob('GET', `/api/image/${enc(fullHash)}`), diff --git a/src/api/StorageManager.js b/src/api/StorageManager.js index 7ab71e8..29b9499 100644 --- a/src/api/StorageManager.js +++ b/src/api/StorageManager.js @@ -297,6 +297,7 @@ export class StorageManager extends ApiClient { this.#loadCustomergroups(), ); const entityCache = createCache('GET /entity', () => this.#loadEntities()); + const numberCache = createCache('GET /number', () => this.#loadNumbers()); const quoteCache = createCache('GET /quote', () => this.#loadQuotes()); const orderCache = createCache('GET /order', () => this.#loadOrders()); @@ -352,6 +353,11 @@ export class StorageManager extends ApiClient { /** Settled GET /entity list, or undefined. */ entities: { get: () => entityCache.value }, + numberPromise: { get: () => numberCache.promise }, + updateNumber: { value: numberCache.update }, + /** Settled GET /number list, or undefined. */ + numbers: { get: () => numberCache.value }, + quotePromise: { get: () => quoteCache.promise }, updateQuote: { value: quoteCache.update }, /** Settled GET /quote list, or undefined. */ @@ -506,6 +512,12 @@ export class StorageManager extends ApiClient { return requireArray(data.entities, 'entities'); } + async #loadNumbers() { + const data = await this.number.list(); + requireObject(data, 'GET /api/number'); + return requireArray(data.numbers, 'numbers'); + } + async #loadQuotes() { const data = await this.quote.list(); requireObject(data, 'GET /api/quote'); diff --git a/src/api/index.js b/src/api/index.js index 3facb18..69245c1 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -772,6 +772,11 @@ export const entity = { return http(`/api/entity/${id}`, { method: 'DELETE' }); }, + /** GET /api/entity/next-number → { number: string } */ + nextNumber() { + return http('/api/entity/next-number'); + }, + /** GET /api/entity/:entityId/address → { addresses: EntityAddress[] } */ listAddresses(entityId) { return http(`/api/entity/${entityId}/address`); @@ -936,6 +941,25 @@ export const posterminal = { }, }; +export const number = { + /** + * GET /api/number → { numbers: [{ id, type, prefix, padding, next }] } + * Lists the number sequences for the selected company. + */ + list() { + return http('/api/number'); + }, + + /** + * PATCH /api/number/:id + * Body: at least one of { prefix, padding, next } — only present keys are patched. + * → 200 { number: { id, type, prefix, padding, next } } + */ + update(id, body) { + return http(`/api/number/${id}`, { method: 'PATCH', body }); + }, +}; + export const auth = { async me() { try { @@ -997,6 +1021,7 @@ export default { customergroup, delivery, entity, + number, order, posterminal, quote, diff --git a/src/components/UserMenu.jsx b/src/components/UserMenu.jsx index 7afac24..96d8821 100644 --- a/src/components/UserMenu.jsx +++ b/src/components/UserMenu.jsx @@ -9,9 +9,11 @@ import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import LogoutIcon from '@mui/icons-material/Logout'; import BusinessIcon from '@mui/icons-material/Business'; +import SettingsIcon from '@mui/icons-material/Settings'; import { StorageManagerContext } from '../api/storageManagerContext'; import { logout } from '../store/authSlice'; import CompanySettingsDialog from './CompanySettingsDialog'; +import SettingsDialog from './settings/SettingsDialog'; import { notifyError } from '../utils/notify'; class UserMenu extends Component { @@ -22,12 +24,15 @@ class UserMenu extends Component { this.state = { anchorEl: null, settingsOpen: false, + numbersOpen: false, user: null, }; this.handleOpenMenu = this.handleOpenMenu.bind(this); this.handleCloseMenu = this.handleCloseMenu.bind(this); this.handleOpenSettings = this.handleOpenSettings.bind(this); this.handleCloseSettings = this.handleCloseSettings.bind(this); + this.handleOpenNumbers = this.handleOpenNumbers.bind(this); + this.handleCloseNumbers = this.handleCloseNumbers.bind(this); this.handleLogout = this.handleLogout.bind(this); } @@ -60,6 +65,14 @@ class UserMenu extends Component { this.setState({ settingsOpen: false }); } + handleOpenNumbers() { + this.setState({ anchorEl: null, numbersOpen: true }); + } + + handleCloseNumbers() { + this.setState({ numbersOpen: false }); + } + async handleLogout() { this.setState({ anchorEl: null }); try { @@ -71,7 +84,7 @@ class UserMenu extends Component { } render() { - const { user, anchorEl, settingsOpen } = this.state; + const { user, anchorEl, settingsOpen, numbersOpen } = this.state; const open = Boolean(anchorEl); return ( @@ -101,6 +114,12 @@ class UserMenu extends Component { Unternehmen + + + + + Einstellungen + @@ -109,6 +128,7 @@ class UserMenu extends Component { + ); } diff --git a/src/components/settings/NumberSettingsPanel.jsx b/src/components/settings/NumberSettingsPanel.jsx new file mode 100644 index 0000000..271833b --- /dev/null +++ b/src/components/settings/NumberSettingsPanel.jsx @@ -0,0 +1,286 @@ +import { Component } 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 TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined'; +import { enqueueSnackbar } from 'notistack'; +import BackendError from '../BackendError'; +import { StorageManagerContext } from '../../api/storageManagerContext'; +import { requireArray, 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; +} + +export default class NumberSettingsPanel extends Component { + static contextType = StorageManagerContext; + + constructor(props) { + super(props); + this.state = { + status: 'loading', + error: null, + numbers: [], + // id → { prefix, padding, next, busy, saveError } + drafts: {}, + }; + this.handleRetry = this.handleRetry.bind(this); + this.handleSave = this.handleSave.bind(this); + } + + componentDidMount() { + void this.load(); + } + + async load() { + this.setState({ status: 'loading', error: null }); + try { + const numbers = requireArray( + await (this.context.numbers ?? this.context.numberPromise), + 'numbers', + ); + const drafts = Object.fromEntries( + numbers.map((row) => [ + row.id, + { + prefix: row.prefix ?? '', + padding: row.padding != null ? String(row.padding) : '', + next: row.next != null ? String(row.next) : '', + busy: false, + saveError: null, + }, + ]), + ); + this.setState({ status: 'ready', numbers, drafts }); + } 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) { + 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 this.context.number.update(id, body); + requireObject(data, 'PATCH /api/number'); + requireObject(data.number, 'number'); + const updated = requireDefined(data.number, 'number'); + + const numbers = this.state.numbers.map((n) => (n.id === id ? { ...n, ...body } : n)); + await this.context.updateNumber(); + 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; + } + + render() { + const { status, error, numbers, drafts } = this.state; + + if (status === 'loading') { + return ( + + + + ); + } + if (status === 'failed') { + return ; + } + if (numbers.length === 0) { + return ( + + Keine Nummernkreise. + + ); + } + + 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)} + autoComplete="off" + /> + + + this.handleChange(row.id, 'padding', e.target.value)} + /> + + + this.handleChange(row.id, 'next', e.target.value)} + /> + + + + {draft.saveError ? ( + + + {typeof draft.saveError === 'string' + ? draft.saveError + : draft.saveError.message} + + + ) : null} + void this.handleSave(row.id)} + > + + + + + + ); + })} + +
+
+ ); + } +} \ No newline at end of file diff --git a/src/components/settings/SettingsDialog.jsx b/src/components/settings/SettingsDialog.jsx new file mode 100644 index 0000000..e298172 --- /dev/null +++ b/src/components/settings/SettingsDialog.jsx @@ -0,0 +1,76 @@ +import { Component } from 'react'; +import Box from '@mui/material/Box'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import NumbersIcon from '@mui/icons-material/Numbers'; +import TuneIcon from '@mui/icons-material/Tune'; +import NumberSettingsPanel from './NumberSettingsPanel'; +import SettingsKeysPanel from './SettingsKeysPanel'; + +const TAB_KEYS = { + keys: 'keys', + numbers: 'numbers', +}; + +const TABS = [ + { id: TAB_KEYS.keys, label: 'Einstellungen', icon: , Panel: SettingsKeysPanel }, + { id: TAB_KEYS.numbers, label: 'Nummernkreise', icon: , Panel: NumberSettingsPanel }, +]; + +export default class SettingsDialog extends Component { + constructor(props) { + super(props); + this.state = { tab: TAB_KEYS.keys }; + this.handleTabChange = this.handleTabChange.bind(this); + } + + componentDidUpdate(prevProps) { + const { open } = this.props; + if (!open && prevProps.open) { + this.setState({ tab: TAB_KEYS.keys }); + } + } + + handleTabChange(_event, value) { + this.setState({ tab: value }); + } + + render() { + const { open, onClose } = this.props; + const { tab } = this.state; + + return ( + + Einstellungen + + {TABS.map((t) => ( + + ))} + + + + {TABS.map((t) => ( + + + + ))} + + + + ); + } +} \ No newline at end of file diff --git a/src/components/settings/SettingsKeysPanel.jsx b/src/components/settings/SettingsKeysPanel.jsx new file mode 100644 index 0000000..7e8be2c --- /dev/null +++ b/src/components/settings/SettingsKeysPanel.jsx @@ -0,0 +1,237 @@ +import { Component } 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 TableRow from '@mui/material/TableRow'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined'; +import { enqueueSnackbar } from 'notistack'; +import BackendError from '../BackendError'; +import { StorageManagerContext } from '../../api/storageManagerContext'; +import { requireArray, requireDefined, requireObject } from '../../utils/apiShape'; +import { backendErrorFrom } from '../../utils/backendError'; +import { notifyError } from '../../utils/notify'; + +/** + * Settings that may be set via PUT /api/company/setting/{key}. + * Only these keys are accepted by the backend. + */ +const SETTABLE_KEYS = [ + 'ui.theme', + 'keys.tavily', + 'keys.openrouter', + 'labelprinter.addr', + 'labelprinter.type', +]; + +const KEY_LABELS = { + 'ui.theme': 'UI-Theme', + 'keys.tavily': 'Tavily API-Key', + 'keys.openrouter': 'OpenRouter API-Key', + 'labelprinter.addr': 'Labeldrucker-Adresse', + 'labelprinter.type': 'Labeldrucker-Typ', +}; + +function keyLabel(key) { + return KEY_LABELS[key] ?? key; +} + +export default class SettingsKeysPanel extends Component { + static contextType = StorageManagerContext; + + constructor(props) { + super(props); + this.state = { + status: 'loading', + error: null, + settings: [], + // key → { value, busy, saveError } + drafts: {}, + }; + this.handleRetry = this.handleRetry.bind(this); + this.handleSave = this.handleSave.bind(this); + } + + componentDidMount() { + void this.load(); + } + + async load() { + this.setState({ status: 'loading', error: null }); + try { + const data = await this.context.setting.list(); + requireObject(data, 'GET /api/company/setting'); + const settings = requireArray(data.settings, 'settings'); + const drafts = Object.fromEntries( + settings.map((s) => [ + s.key, + { + value: s.value ?? '', + busy: false, + saveError: null, + }, + ]), + ); + this.setState({ status: 'ready', settings, drafts }); + } catch (err) { + notifyError(err); + this.setState({ status: 'failed', error: backendErrorFrom(err) }); + } + } + + handleRetry() { + void this.load(); + } + + handleChange(key, field, value) { + this.setState((prev) => ({ + drafts: { + ...prev.drafts, + [key]: { ...prev.drafts[key], [field]: value, saveError: null }, + }, + })); + } + + async handleSave(key) { + const draft = this.state.drafts[key]; + if (!draft || draft.busy) return; + const row = this.state.settings.find((s) => s.key === key); + if (!row) return; + + const body = {}; + if (draft.value !== (row.value ?? '')) { + body.value = draft.value; + } + + if (Object.keys(body).length === 0) { + return; + } + + this.setState((prev) => ({ + drafts: { + ...prev.drafts, + [key]: { ...prev.drafts[key], busy: true, saveError: null }, + }, + })); + try { + const data = await this.context.setting.upsert(key, body); + requireObject(data, 'PUT /api/company/setting/:key'); + requireObject(data.setting, 'setting'); + const updated = requireDefined(data.setting, 'setting'); + + const settings = this.state.settings.map((s) => + s.key === key ? { ...s, ...body } : s, + ); + this.setState({ + settings, + drafts: { + ...this.state.drafts, + [key]: { + value: updated.value ?? '', + busy: false, + saveError: null, + }, + }, + }); + enqueueSnackbar('Einstellung gespeichert.', { variant: 'success' }); + } catch (err) { + if (this._unmounted) return; + this.setState((prev) => ({ + drafts: { + ...prev.drafts, + [key]: { ...prev.drafts[key], busy: false, saveError: backendErrorFrom(err) }, + }, + })); + } + } + + componentWillUnmount() { + this._unmounted = true; + } + + render() { + const { status, error, settings, drafts } = this.state; + + if (status === 'loading') { + return ( + + + + ); + } + if (status === 'failed') { + return ; + } + if (settings.length === 0) { + return ( + + Keine Einstellungen. + + ); + } + + return ( + + + + {SETTABLE_KEYS.map((key) => { + const row = settings.find((s) => s.key === key); + const draft = row ? drafts[row.key] : null; + if (!row || !draft) return null; + return ( + + + {keyLabel(row.key)} + + + this.handleChange(row.key, 'value', e.target.value)} + autoComplete="off" + /> + + + + {draft.saveError ? ( + + + {typeof draft.saveError === 'string' + ? draft.saveError + : draft.saveError.message} + + + ) : null} + void this.handleSave(row.key)} + > + + + + + + ); + })} + +
+
+ ); + } +} \ No newline at end of file diff --git a/src/components/settings/index.js b/src/components/settings/index.js new file mode 100644 index 0000000..df96d02 --- /dev/null +++ b/src/components/settings/index.js @@ -0,0 +1,3 @@ +export { default as SettingsDialog } from './SettingsDialog'; +export { default as SettingsKeysPanel } from './SettingsKeysPanel'; +export { default as NumberSettingsPanel } from './NumberSettingsPanel'; \ No newline at end of file diff --git a/src/pages/kunde/index.jsx b/src/pages/kunde/index.jsx index f50d230..592df04 100644 --- a/src/pages/kunde/index.jsx +++ b/src/pages/kunde/index.jsx @@ -29,6 +29,11 @@ function sortEntities(entities) { }); } +function formatDate(value) { + if (!value) return ''; + return new Intl.DateTimeFormat('de-DE', { dateStyle: 'medium' }).format(new Date(value)); +} + class KundeListePage extends PureComponent { static contextType = StorageManagerContext; @@ -147,9 +152,11 @@ class KundeListePage extends PureComponent { + Kundennummer Name Vorname Gruppe + Erstellt am @@ -161,9 +168,11 @@ class KundeListePage extends PureComponent { sx={{ cursor: 'pointer' }} onClick={() => this.handleEdit(row)} > + {row.number}{row.name}{row.firstName}{this.groupName(row.customergroupId)} + {formatDate(row.createdAt)} e.stopPropagation()}> { - 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; - }); -} - class KundeNeuPage extends PureComponent { static contextType = StorageManagerContext; - constructor(props) { - super(props); + constructor(props, context) { + super(props, context); + const groups = this.context.customergroups ?? []; this.state = { - status: 'loading', - error: null, - groups: [], - customergroupId: null, + customergroupId: groups.length > 0 ? groups[0].id : null, + number: '', name: '', firstName: '', saving: false, }; - this._cancelled = false; - this.handleRetry = this.handleRetry.bind(this); this.handleGroupChange = this.handleGroupChange.bind(this); this.handleSave = this.handleSave.bind(this); } componentDidMount() { - this._cancelled = false; - void this.loadGroups(); + void this.loadNumber(); } - componentWillUnmount() { - this._cancelled = true; - } - - async loadGroups() { - this.setState({ status: 'loading', error: null }); + async loadNumber() { try { - const list = - this.context.customergroups ?? - (await this.context.customergroupPromise); - if (this._cancelled) return; - this.setState({ - status: 'ready', - groups: sortGroups(list), - }); + const res = await this.context.entity.nextNumber(); + requireObject(res, 'GET /api/entity/next-number'); + requireDefined(res.number, 'number'); + this.setState({ number: res.number }); } catch (err) { - if (this._cancelled) return; - this.setState({ - status: 'failed', - error: backendErrorFrom(err), - }); + notifyError(err); } } - handleRetry() { - void this.loadGroups(); - } - - handleGroupChange(_event, option) { - this.setState({ customergroupId: option?.id ?? null }); + handleGroupChange(event) { + const value = event.target.value; + this.setState({ customergroupId: value === '' ? null : Number(value) }); } async handleSave() { @@ -109,33 +78,18 @@ class KundeNeuPage extends PureComponent { requireObject(data.entity, 'entity'); requireDefined(data.entity.id, 'entity.id'); await this.context.updateEntity(); - if (this._cancelled) return; enqueueSnackbar('Kunde angelegt.', { variant: 'success' }); void router.navigate(`/kunde/${data.entity.id}`); } catch (err) { - if (this._cancelled) return; notifyError(err); this.setState({ saving: false }); } } render() { - const { - status, - error, - groups, - customergroupId, - name, - firstName, - saving, - } = this.state; + const { customergroupId, number, name, firstName, saving } = this.state; + const groups = this.context.customergroups ?? []; - if (status === 'failed') { - return ; - } - - const selected = - groups.find((row) => row.id === customergroupId) ?? null; const canSave = customergroupId != null && name.trim() !== '' && !saving; @@ -143,31 +97,32 @@ class KundeNeuPage extends PureComponent { - String(option.name ?? '')} - isOptionEqualToValue={(option, value) => option.id === value.id} - onChange={this.handleGroupChange} - renderInput={(params) => ( - - )} + label="Kundennummer" + value={number} + disabled + slotProps={{ + htmlInput: { spellCheck: false, autoComplete: 'off' }, + }} /> +