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.
This commit is contained in:
@@ -146,6 +146,8 @@ export class ApiClient {
|
|||||||
update: (id, body) => this.#json('PATCH', `/api/entity/${enc(id)}`, { body }),
|
update: (id, body) => this.#json('PATCH', `/api/entity/${enc(id)}`, { body }),
|
||||||
/** DELETE /api/entity/{id} → 204 */
|
/** DELETE /api/entity/{id} → 204 */
|
||||||
delete: (id) => this.#empty('DELETE', `/api/entity/${enc(id)}`),
|
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 } */
|
/** GET /api/entity/{entityId}/address → { addresses } */
|
||||||
listAddresses: (entityId) =>
|
listAddresses: (entityId) =>
|
||||||
this.#json('GET', `/api/entity/${enc(entityId)}/address`),
|
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`),
|
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 = {
|
this.image = {
|
||||||
/** GET /api/image/{fullHash} → AVIF blob */
|
/** GET /api/image/{fullHash} → AVIF blob */
|
||||||
get: (fullHash) => this.#blob('GET', `/api/image/${enc(fullHash)}`),
|
get: (fullHash) => this.#blob('GET', `/api/image/${enc(fullHash)}`),
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ export class StorageManager extends ApiClient {
|
|||||||
this.#loadCustomergroups(),
|
this.#loadCustomergroups(),
|
||||||
);
|
);
|
||||||
const entityCache = createCache('GET /entity', () => this.#loadEntities());
|
const entityCache = createCache('GET /entity', () => this.#loadEntities());
|
||||||
|
const numberCache = createCache('GET /number', () => this.#loadNumbers());
|
||||||
const quoteCache = createCache('GET /quote', () => this.#loadQuotes());
|
const quoteCache = createCache('GET /quote', () => this.#loadQuotes());
|
||||||
const orderCache = createCache('GET /order', () => this.#loadOrders());
|
const orderCache = createCache('GET /order', () => this.#loadOrders());
|
||||||
|
|
||||||
@@ -352,6 +353,11 @@ export class StorageManager extends ApiClient {
|
|||||||
/** Settled GET /entity list, or undefined. */
|
/** Settled GET /entity list, or undefined. */
|
||||||
entities: { get: () => entityCache.value },
|
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 },
|
quotePromise: { get: () => quoteCache.promise },
|
||||||
updateQuote: { value: quoteCache.update },
|
updateQuote: { value: quoteCache.update },
|
||||||
/** Settled GET /quote list, or undefined. */
|
/** Settled GET /quote list, or undefined. */
|
||||||
@@ -506,6 +512,12 @@ export class StorageManager extends ApiClient {
|
|||||||
return requireArray(data.entities, 'entities');
|
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() {
|
async #loadQuotes() {
|
||||||
const data = await this.quote.list();
|
const data = await this.quote.list();
|
||||||
requireObject(data, 'GET /api/quote');
|
requireObject(data, 'GET /api/quote');
|
||||||
|
|||||||
@@ -772,6 +772,11 @@ export const entity = {
|
|||||||
return http(`/api/entity/${id}`, { method: 'DELETE' });
|
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[] } */
|
/** GET /api/entity/:entityId/address → { addresses: EntityAddress[] } */
|
||||||
listAddresses(entityId) {
|
listAddresses(entityId) {
|
||||||
return http(`/api/entity/${entityId}/address`);
|
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 = {
|
export const auth = {
|
||||||
async me() {
|
async me() {
|
||||||
try {
|
try {
|
||||||
@@ -997,6 +1021,7 @@ export default {
|
|||||||
customergroup,
|
customergroup,
|
||||||
delivery,
|
delivery,
|
||||||
entity,
|
entity,
|
||||||
|
number,
|
||||||
order,
|
order,
|
||||||
posterminal,
|
posterminal,
|
||||||
quote,
|
quote,
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import ListItemIcon from '@mui/material/ListItemIcon';
|
|||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import LogoutIcon from '@mui/icons-material/Logout';
|
import LogoutIcon from '@mui/icons-material/Logout';
|
||||||
import BusinessIcon from '@mui/icons-material/Business';
|
import BusinessIcon from '@mui/icons-material/Business';
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
import { StorageManagerContext } from '../api/storageManagerContext';
|
import { StorageManagerContext } from '../api/storageManagerContext';
|
||||||
import { logout } from '../store/authSlice';
|
import { logout } from '../store/authSlice';
|
||||||
import CompanySettingsDialog from './CompanySettingsDialog';
|
import CompanySettingsDialog from './CompanySettingsDialog';
|
||||||
|
import SettingsDialog from './settings/SettingsDialog';
|
||||||
import { notifyError } from '../utils/notify';
|
import { notifyError } from '../utils/notify';
|
||||||
|
|
||||||
class UserMenu extends Component {
|
class UserMenu extends Component {
|
||||||
@@ -22,12 +24,15 @@ class UserMenu extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
anchorEl: null,
|
anchorEl: null,
|
||||||
settingsOpen: false,
|
settingsOpen: false,
|
||||||
|
numbersOpen: false,
|
||||||
user: null,
|
user: null,
|
||||||
};
|
};
|
||||||
this.handleOpenMenu = this.handleOpenMenu.bind(this);
|
this.handleOpenMenu = this.handleOpenMenu.bind(this);
|
||||||
this.handleCloseMenu = this.handleCloseMenu.bind(this);
|
this.handleCloseMenu = this.handleCloseMenu.bind(this);
|
||||||
this.handleOpenSettings = this.handleOpenSettings.bind(this);
|
this.handleOpenSettings = this.handleOpenSettings.bind(this);
|
||||||
this.handleCloseSettings = this.handleCloseSettings.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);
|
this.handleLogout = this.handleLogout.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +65,14 @@ class UserMenu extends Component {
|
|||||||
this.setState({ settingsOpen: false });
|
this.setState({ settingsOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleOpenNumbers() {
|
||||||
|
this.setState({ anchorEl: null, numbersOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseNumbers() {
|
||||||
|
this.setState({ numbersOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
async handleLogout() {
|
async handleLogout() {
|
||||||
this.setState({ anchorEl: null });
|
this.setState({ anchorEl: null });
|
||||||
try {
|
try {
|
||||||
@@ -71,7 +84,7 @@ class UserMenu extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { user, anchorEl, settingsOpen } = this.state;
|
const { user, anchorEl, settingsOpen, numbersOpen } = this.state;
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -101,6 +114,12 @@ class UserMenu extends Component {
|
|||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Unternehmen</ListItemText>
|
<ListItemText>Unternehmen</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem disabled={!user?.selectedCompanyId} onClick={this.handleOpenNumbers}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<SettingsIcon fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Einstellungen</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem onClick={this.handleLogout}>
|
<MenuItem onClick={this.handleLogout}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LogoutIcon fontSize="small" />
|
<LogoutIcon fontSize="small" />
|
||||||
@@ -109,6 +128,7 @@ class UserMenu extends Component {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
<CompanySettingsDialog open={settingsOpen} onClose={this.handleCloseSettings} />
|
<CompanySettingsDialog open={settingsOpen} onClose={this.handleCloseSettings} />
|
||||||
|
<SettingsDialog open={numbersOpen} onClose={this.handleCloseNumbers} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
286
src/components/settings/NumberSettingsPanel.jsx
Normal file
286
src/components/settings/NumberSettingsPanel.jsx
Normal file
@@ -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 (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status === 'failed') {
|
||||||
|
return <BackendError error={error} onRetry={this.handleRetry} />;
|
||||||
|
}
|
||||||
|
if (numbers.length === 0) {
|
||||||
|
return (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Keine Nummernkreise.
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Typ</TableCell>
|
||||||
|
<TableCell>Präfix</TableCell>
|
||||||
|
<TableCell>Stellenzahl</TableCell>
|
||||||
|
<TableCell>Nächste Nummer</TableCell>
|
||||||
|
<TableCell align="right" />
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{numbers.map((row) => {
|
||||||
|
const draft = drafts[row.id];
|
||||||
|
if (!draft) return null;
|
||||||
|
return (
|
||||||
|
<TableRow key={row.id} hover>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="body2">{row.type}</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={draft.prefix}
|
||||||
|
disabled={draft.busy}
|
||||||
|
onChange={(e) => this.handleChange(row.id, 'prefix', e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
type="number"
|
||||||
|
value={draft.padding}
|
||||||
|
disabled={draft.busy}
|
||||||
|
slotProps={{ htmlInput: { min: 1, step: 1 } }}
|
||||||
|
onChange={(e) => this.handleChange(row.id, 'padding', e.target.value)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
type="number"
|
||||||
|
value={draft.next}
|
||||||
|
disabled={draft.busy}
|
||||||
|
slotProps={{ htmlInput: { min: 0, step: 1 } }}
|
||||||
|
onChange={(e) => this.handleChange(row.id, 'next', e.target.value)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={0}
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
{draft.saveError ? (
|
||||||
|
<Box sx={{ maxWidth: 220, mr: 1 }}>
|
||||||
|
<Alert severity="error" sx={{ py: 0 }}>
|
||||||
|
{typeof draft.saveError === 'string'
|
||||||
|
? draft.saveError
|
||||||
|
: draft.saveError.message}
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
aria-label="Speichern"
|
||||||
|
color="primary"
|
||||||
|
disabled={draft.busy}
|
||||||
|
onClick={() => void this.handleSave(row.id)}
|
||||||
|
>
|
||||||
|
<SaveOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/components/settings/SettingsDialog.jsx
Normal file
76
src/components/settings/SettingsDialog.jsx
Normal file
@@ -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: <TuneIcon />, Panel: SettingsKeysPanel },
|
||||||
|
{ id: TAB_KEYS.numbers, label: 'Nummernkreise', icon: <NumbersIcon />, 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 (
|
||||||
|
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||||
|
<DialogTitle>Einstellungen</DialogTitle>
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
onChange={this.handleTabChange}
|
||||||
|
variant="fullWidth"
|
||||||
|
sx={{ px: 3 }}
|
||||||
|
>
|
||||||
|
{TABS.map((t) => (
|
||||||
|
<Tab key={t.id} icon={t.icon} iconPosition="start" label={t.label} value={t.id} />
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Box sx={{ display: 'grid' }}>
|
||||||
|
{TABS.map((t) => (
|
||||||
|
<Box
|
||||||
|
key={t.id}
|
||||||
|
sx={{
|
||||||
|
gridArea: '1 / 1',
|
||||||
|
visibility: t.id === tab ? undefined : 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<t.Panel />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
237
src/components/settings/SettingsKeysPanel.jsx
Normal file
237
src/components/settings/SettingsKeysPanel.jsx
Normal file
@@ -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 (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status === 'failed') {
|
||||||
|
return <BackendError error={error} onRetry={this.handleRetry} />;
|
||||||
|
}
|
||||||
|
if (settings.length === 0) {
|
||||||
|
return (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Keine Einstellungen.
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table size="small">
|
||||||
|
<TableBody>
|
||||||
|
{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 (
|
||||||
|
<TableRow key={row.key} hover>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="body2">{keyLabel(row.key)}</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={draft.value}
|
||||||
|
disabled={draft.busy}
|
||||||
|
onChange={(e) => this.handleChange(row.key, 'value', e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={0}
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
{draft.saveError ? (
|
||||||
|
<Box sx={{ maxWidth: 220, mr: 1 }}>
|
||||||
|
<Alert severity="error" sx={{ py: 0 }}>
|
||||||
|
{typeof draft.saveError === 'string'
|
||||||
|
? draft.saveError
|
||||||
|
: draft.saveError.message}
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
aria-label="Speichern"
|
||||||
|
color="primary"
|
||||||
|
disabled={draft.busy}
|
||||||
|
onClick={() => void this.handleSave(row.key)}
|
||||||
|
>
|
||||||
|
<SaveOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/components/settings/index.js
Normal file
3
src/components/settings/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as SettingsDialog } from './SettingsDialog';
|
||||||
|
export { default as SettingsKeysPanel } from './SettingsKeysPanel';
|
||||||
|
export { default as NumberSettingsPanel } from './NumberSettingsPanel';
|
||||||
@@ -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 {
|
class KundeListePage extends PureComponent {
|
||||||
static contextType = StorageManagerContext;
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
@@ -147,9 +152,11 @@ class KundeListePage extends PureComponent {
|
|||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableCell>Kundennummer</TableCell>
|
||||||
<TableCell>Name</TableCell>
|
<TableCell>Name</TableCell>
|
||||||
<TableCell>Vorname</TableCell>
|
<TableCell>Vorname</TableCell>
|
||||||
<TableCell>Gruppe</TableCell>
|
<TableCell>Gruppe</TableCell>
|
||||||
|
<TableCell>Erstellt am</TableCell>
|
||||||
<TableCell padding="checkbox" />
|
<TableCell padding="checkbox" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -161,9 +168,11 @@ class KundeListePage extends PureComponent {
|
|||||||
sx={{ cursor: 'pointer' }}
|
sx={{ cursor: 'pointer' }}
|
||||||
onClick={() => this.handleEdit(row)}
|
onClick={() => this.handleEdit(row)}
|
||||||
>
|
>
|
||||||
|
<TableCell>{row.number}</TableCell>
|
||||||
<TableCell>{row.name}</TableCell>
|
<TableCell>{row.name}</TableCell>
|
||||||
<TableCell>{row.firstName}</TableCell>
|
<TableCell>{row.firstName}</TableCell>
|
||||||
<TableCell>{this.groupName(row.customergroupId)}</TableCell>
|
<TableCell>{this.groupName(row.customergroupId)}</TableCell>
|
||||||
|
<TableCell>{formatDate(row.createdAt)}</TableCell>
|
||||||
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -1,84 +1,53 @@
|
|||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
import Autocomplete from '@mui/material/Autocomplete';
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import BackendError from '../../components/BackendError';
|
|
||||||
import SingleLineTextEdit from '../../components/SingleLineTextEdit';
|
import SingleLineTextEdit from '../../components/SingleLineTextEdit';
|
||||||
import { PanelTile, PanelTileGrid } from '../../components/panels';
|
import { PanelTile, PanelTileGrid } from '../../components/panels';
|
||||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||||
import { router } from '../../appRouter';
|
import { router } from '../../appRouter';
|
||||||
import { requireDefined, requireObject } from '../../utils/apiShape';
|
import { requireDefined, requireObject } from '../../utils/apiShape';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
|
||||||
import { notifyError } from '../../utils/notify';
|
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class KundeNeuPage extends PureComponent {
|
class KundeNeuPage extends PureComponent {
|
||||||
static contextType = StorageManagerContext;
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props, context);
|
||||||
|
const groups = this.context.customergroups ?? [];
|
||||||
this.state = {
|
this.state = {
|
||||||
status: 'loading',
|
customergroupId: groups.length > 0 ? groups[0].id : null,
|
||||||
error: null,
|
number: '',
|
||||||
groups: [],
|
|
||||||
customergroupId: null,
|
|
||||||
name: '',
|
name: '',
|
||||||
firstName: '',
|
firstName: '',
|
||||||
saving: false,
|
saving: false,
|
||||||
};
|
};
|
||||||
this._cancelled = false;
|
|
||||||
this.handleRetry = this.handleRetry.bind(this);
|
|
||||||
this.handleGroupChange = this.handleGroupChange.bind(this);
|
this.handleGroupChange = this.handleGroupChange.bind(this);
|
||||||
this.handleSave = this.handleSave.bind(this);
|
this.handleSave = this.handleSave.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._cancelled = false;
|
void this.loadNumber();
|
||||||
void this.loadGroups();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
async loadNumber() {
|
||||||
this._cancelled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadGroups() {
|
|
||||||
this.setState({ status: 'loading', error: null });
|
|
||||||
try {
|
try {
|
||||||
const list =
|
const res = await this.context.entity.nextNumber();
|
||||||
this.context.customergroups ??
|
requireObject(res, 'GET /api/entity/next-number');
|
||||||
(await this.context.customergroupPromise);
|
requireDefined(res.number, 'number');
|
||||||
if (this._cancelled) return;
|
this.setState({ number: res.number });
|
||||||
this.setState({
|
|
||||||
status: 'ready',
|
|
||||||
groups: sortGroups(list),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (this._cancelled) return;
|
notifyError(err);
|
||||||
this.setState({
|
|
||||||
status: 'failed',
|
|
||||||
error: backendErrorFrom(err),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRetry() {
|
handleGroupChange(event) {
|
||||||
void this.loadGroups();
|
const value = event.target.value;
|
||||||
}
|
this.setState({ customergroupId: value === '' ? null : Number(value) });
|
||||||
|
|
||||||
handleGroupChange(_event, option) {
|
|
||||||
this.setState({ customergroupId: option?.id ?? null });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSave() {
|
async handleSave() {
|
||||||
@@ -109,33 +78,18 @@ class KundeNeuPage extends PureComponent {
|
|||||||
requireObject(data.entity, 'entity');
|
requireObject(data.entity, 'entity');
|
||||||
requireDefined(data.entity.id, 'entity.id');
|
requireDefined(data.entity.id, 'entity.id');
|
||||||
await this.context.updateEntity();
|
await this.context.updateEntity();
|
||||||
if (this._cancelled) return;
|
|
||||||
enqueueSnackbar('Kunde angelegt.', { variant: 'success' });
|
enqueueSnackbar('Kunde angelegt.', { variant: 'success' });
|
||||||
void router.navigate(`/kunde/${data.entity.id}`);
|
void router.navigate(`/kunde/${data.entity.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (this._cancelled) return;
|
|
||||||
notifyError(err);
|
notifyError(err);
|
||||||
this.setState({ saving: false });
|
this.setState({ saving: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { customergroupId, number, name, firstName, saving } = this.state;
|
||||||
status,
|
const groups = this.context.customergroups ?? [];
|
||||||
error,
|
|
||||||
groups,
|
|
||||||
customergroupId,
|
|
||||||
name,
|
|
||||||
firstName,
|
|
||||||
saving,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (status === 'failed') {
|
|
||||||
return <BackendError error={error} onRetry={this.handleRetry} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selected =
|
|
||||||
groups.find((row) => row.id === customergroupId) ?? null;
|
|
||||||
const canSave =
|
const canSave =
|
||||||
customergroupId != null && name.trim() !== '' && !saving;
|
customergroupId != null && name.trim() !== '' && !saving;
|
||||||
|
|
||||||
@@ -143,31 +97,32 @@ class KundeNeuPage extends PureComponent {
|
|||||||
<PanelTileGrid>
|
<PanelTileGrid>
|
||||||
<PanelTile wide={2} minHeight={220} color="#bfdbfe" title="Neuer Kunde">
|
<PanelTile wide={2} minHeight={220} color="#bfdbfe" title="Neuer Kunde">
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Autocomplete
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
options={groups}
|
label="Kundennummer"
|
||||||
value={selected}
|
value={number}
|
||||||
loading={status === 'loading'}
|
disabled
|
||||||
disabled={saving || status === 'loading'}
|
slotProps={{
|
||||||
getOptionLabel={(option) => String(option.name ?? '')}
|
htmlInput: { spellCheck: false, autoComplete: 'off' },
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
}}
|
||||||
onChange={this.handleGroupChange}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
label="Kundengruppe"
|
|
||||||
required
|
|
||||||
slotProps={{
|
|
||||||
...params.slotProps,
|
|
||||||
htmlInput: {
|
|
||||||
...params.slotProps?.htmlInput,
|
|
||||||
spellCheck: false,
|
|
||||||
autoComplete: 'off',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
value={customergroupId ?? ''}
|
||||||
|
disabled={saving}
|
||||||
|
onChange={this.handleGroupChange}
|
||||||
|
displayEmpty
|
||||||
|
inputProps={{
|
||||||
|
spellCheck: false,
|
||||||
|
autoComplete: 'off',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{groups.map((row) => (
|
||||||
|
<MenuItem key={row.id} value={row.id}>
|
||||||
|
{row.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
<SingleLineTextEdit
|
<SingleLineTextEdit
|
||||||
size="small"
|
size="small"
|
||||||
label="Name"
|
label="Name"
|
||||||
|
|||||||
Reference in New Issue
Block a user