u
This commit is contained in:
7
package-lock.json
generated
7
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"@shopify/draggable": "^1.2.1",
|
"@shopify/draggable": "^1.2.1",
|
||||||
"dashgui": "file:../dashgui",
|
"dashgui": "file:../dashgui",
|
||||||
"eslint": "^10.11.0",
|
"eslint": "^10.11.0",
|
||||||
|
"jsbarcode": "^3.12.3",
|
||||||
"notistack": "^3.0.2",
|
"notistack": "^3.0.2",
|
||||||
"pdfconvert-wasm": "^0.3.0",
|
"pdfconvert-wasm": "^0.3.0",
|
||||||
"react": "^19.3.0",
|
"react": "^19.3.0",
|
||||||
@@ -5415,6 +5416,12 @@
|
|||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jsbarcode": {
|
||||||
|
"version": "3.12.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.12.3.tgz",
|
||||||
|
"integrity": "sha512-CuHU9hC6dPsHF5oVFMo8NW76uQVjH4L22CsP4hW+dNnGywJHC/B0ThA1CTDVLnxKLrrpYdicBLnd2xsgTfRnvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/jsesc": {
|
"node_modules/jsesc": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"@shopify/draggable": "^1.2.1",
|
"@shopify/draggable": "^1.2.1",
|
||||||
"dashgui": "file:../dashgui",
|
"dashgui": "file:../dashgui",
|
||||||
"eslint": "^10.11.0",
|
"eslint": "^10.11.0",
|
||||||
|
"jsbarcode": "^3.12.3",
|
||||||
"notistack": "^3.0.2",
|
"notistack": "^3.0.2",
|
||||||
"pdfconvert-wasm": "^0.3.0",
|
"pdfconvert-wasm": "^0.3.0",
|
||||||
"react": "^19.3.0",
|
"react": "^19.3.0",
|
||||||
|
|||||||
@@ -442,6 +442,19 @@ export class ApiClient {
|
|||||||
update: (id, body) => this.#json('PATCH', `/api/number/${enc(id)}`, { body }),
|
update: (id, body) => this.#json('PATCH', `/api/number/${enc(id)}`, { body }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.printer = {
|
||||||
|
/** GET /api/printer → { printers: Printer[] } */
|
||||||
|
list: () => this.#json('GET', '/api/printer'),
|
||||||
|
/** POST /api/printer → 201 { printer } */
|
||||||
|
create: (body) => this.#json('POST', '/api/printer', { body }),
|
||||||
|
/** PATCH /api/printer/{id} → { printer } */
|
||||||
|
update: (id, body) => this.#json('PATCH', `/api/printer/${enc(id)}`, { body }),
|
||||||
|
/** DELETE /api/printer/{id} → 204 */
|
||||||
|
delete: (id) => this.#empty('DELETE', `/api/printer/${enc(id)}`),
|
||||||
|
/** PUT /api/printer/order body { printerIds } → { printerIds } */
|
||||||
|
order: (body) => this.#json('PUT', '/api/printer/order', { body }),
|
||||||
|
};
|
||||||
|
|
||||||
this.setting = {
|
this.setting = {
|
||||||
/** GET /api/company/setting?frontendOnly=1 → { settings: [{ key, value, frontend }] } */
|
/** GET /api/company/setting?frontendOnly=1 → { settings: [{ key, value, frontend }] } */
|
||||||
list: (query) => this.#json('GET', '/api/company/setting', { query }),
|
list: (query) => this.#json('GET', '/api/company/setting', { query }),
|
||||||
|
|||||||
@@ -581,6 +581,14 @@ export const brand = {
|
|||||||
upsert(body) {
|
upsert(body) {
|
||||||
return http('/api/brand', { method: 'PUT', body });
|
return http('/api/brand', { method: 'PUT', body });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/brand/:id
|
||||||
|
* → 204 No Content · 404 Brand not found
|
||||||
|
*/
|
||||||
|
delete(id) {
|
||||||
|
return http(`/api/brand/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const ArtikelUebersichtPage = lazy(() => import('./pages/artikel/index'));
|
|||||||
const ArtikelKategorienPage = lazy(() => import('./pages/artikel/kategorien'));
|
const ArtikelKategorienPage = lazy(() => import('./pages/artikel/kategorien'));
|
||||||
const RedirectArtikelKategorien = lazy(() => import('./pages/artikel/RedirectArtikelKategorien'));
|
const RedirectArtikelKategorien = lazy(() => import('./pages/artikel/RedirectArtikelKategorien'));
|
||||||
const ArtikelLagerPage = lazy(() => import('./pages/artikel/lager'));
|
const ArtikelLagerPage = lazy(() => import('./pages/artikel/lager'));
|
||||||
|
const ArtikelMarkenPage = lazy(() => import('./pages/artikel/marken'));
|
||||||
const ArtikelSkuPage = lazy(() => import('./pages/artikel/sku'));
|
const ArtikelSkuPage = lazy(() => import('./pages/artikel/sku'));
|
||||||
const LieferungListePage = lazy(() => import('./pages/lieferung/index'));
|
const LieferungListePage = lazy(() => import('./pages/lieferung/index'));
|
||||||
const LieferungNeuPage = lazy(() => import('./pages/lieferung/neu'));
|
const LieferungNeuPage = lazy(() => import('./pages/lieferung/neu'));
|
||||||
@@ -53,6 +54,7 @@ const routes = [
|
|||||||
{ index: true, element: <ArtikelUebersichtPage /> },
|
{ index: true, element: <ArtikelUebersichtPage /> },
|
||||||
{ path: 'kategorien/*', element: <RedirectArtikelKategorien /> },
|
{ path: 'kategorien/*', element: <RedirectArtikelKategorien /> },
|
||||||
{ path: 'lager', element: <ArtikelLagerPage /> },
|
{ path: 'lager', element: <ArtikelLagerPage /> },
|
||||||
|
{ path: 'marken', element: <ArtikelMarkenPage /> },
|
||||||
{ path: ':sku/:tab?', element: <ArtikelSkuPage /> },
|
{ path: ':sku/:tab?', element: <ArtikelSkuPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -54,6 +54,16 @@ const actions = {
|
|||||||
return row;
|
return row;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a brand by id.
|
||||||
|
* @param {number} id
|
||||||
|
*/
|
||||||
|
async remove(id) {
|
||||||
|
await brandApi.delete(id);
|
||||||
|
const brands = brandAutomat.state.brands.filter((b) => b.id !== id);
|
||||||
|
brandAutomat.setState({ brands });
|
||||||
|
},
|
||||||
|
|
||||||
/** Reset to idle state (called on logout / company switch). */
|
/** Reset to idle state (called on logout / company switch). */
|
||||||
reset() {
|
reset() {
|
||||||
brandAutomat.setState(initialState);
|
brandAutomat.setState(initialState);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { StorageManagerContext } from '../../../api/storageManagerContext';
|
|||||||
import { requireObject } from '../../../utils/apiShape';
|
import { requireObject } from '../../../utils/apiShape';
|
||||||
import { articleEans } from '../../../utils/articleDetail';
|
import { articleEans } from '../../../utils/articleDetail';
|
||||||
import { notifyError } from '../../../utils/notify';
|
import { notifyError } from '../../../utils/notify';
|
||||||
|
import JsBarcode from 'jsbarcode';
|
||||||
class PanelValue extends Component {
|
class PanelValue extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { label, children } = this.props;
|
const { label, children } = this.props;
|
||||||
@@ -41,20 +42,63 @@ function eansDraftValid(draft) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SkuBarcode extends PureComponent {
|
||||||
|
componentDidMount() {
|
||||||
|
this.renderBarcode();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.renderBarcode();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBarcode() {
|
||||||
|
if (!this._svg || !this.props.value) return;
|
||||||
|
try {
|
||||||
|
JsBarcode(this._svg, String(this.props.value).trim(), {
|
||||||
|
format: 'CODE128',
|
||||||
|
width: 1.5,
|
||||||
|
height: 38,
|
||||||
|
displayValue: false,
|
||||||
|
margin: 0,
|
||||||
|
background: 'transparent',
|
||||||
|
lineColor: '#1e293b',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Barcode render error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { value } = this.props;
|
||||||
|
if (!value) return null;
|
||||||
|
return (
|
||||||
|
<Box sx={{ my: 1, display: 'flex', alignItems: 'center' }}>
|
||||||
|
<svg
|
||||||
|
ref={(el) => {
|
||||||
|
this._svg = el;
|
||||||
|
}}
|
||||||
|
style={{ maxWidth: '100%', height: 'auto' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const SECOND_ROW_MIN_HEIGHT = 220;
|
const SECOND_ROW_MIN_HEIGHT = 220;
|
||||||
|
|
||||||
class EansDisplay extends Component {
|
class EansDisplay extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { sku, eans } = this.props;
|
const { sku, eans } = this.props;
|
||||||
return (
|
return (
|
||||||
<>
|
<Box sx={{ width: '100%' }}>
|
||||||
<PanelValue label="SKU">{sku}</PanelValue>
|
<PanelValue label="SKU">{sku}</PanelValue>
|
||||||
|
<SkuBarcode value={sku} />
|
||||||
{(eans ?? []).map((ean) => (
|
{(eans ?? []).map((ean) => (
|
||||||
<PanelValue key={ean} label="EAN">
|
<PanelValue key={ean} label="EAN">
|
||||||
{ean}
|
{ean}
|
||||||
</PanelValue>
|
</PanelValue>
|
||||||
))}
|
))}
|
||||||
</>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +109,8 @@ class EansEdit extends Component {
|
|||||||
return (
|
return (
|
||||||
<Box sx={{ width: '100%' }}>
|
<Box sx={{ width: '100%' }}>
|
||||||
<PanelValue label="SKU">{sku}</PanelValue>
|
<PanelValue label="SKU">{sku}</PanelValue>
|
||||||
<Box sx={{ mt: 3 }}>
|
<SkuBarcode value={sku} />
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
fullWidth
|
fullWidth
|
||||||
multiple
|
multiple
|
||||||
|
|||||||
482
src/components/settings/PrinterSettingsPanel.jsx
Normal file
482
src/components/settings/PrinterSettingsPanel.jsx
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
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 AddIcon from '@mui/icons-material/Add';
|
||||||
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
|
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||||
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
import BackendError from '../BackendError';
|
||||||
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||||
|
import { requireArray, requireObject } from '../../utils/apiShape';
|
||||||
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
|
import { notifyError } from '../../utils/notify';
|
||||||
|
|
||||||
|
const PRINTER_TYPES = ['esc/pos', 'zpl'];
|
||||||
|
|
||||||
|
const EMPTY_FORM = {
|
||||||
|
address: '',
|
||||||
|
type: 'esc/pos',
|
||||||
|
model: '',
|
||||||
|
serial: '',
|
||||||
|
desc: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
function formFromPrinter(p) {
|
||||||
|
return {
|
||||||
|
address: p.address ?? '',
|
||||||
|
type: p.type ?? 'esc/pos',
|
||||||
|
model: p.model ?? '',
|
||||||
|
serial: p.serial ?? '',
|
||||||
|
desc: p.desc ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateForm(form) {
|
||||||
|
if (!form.address.trim()) return 'Adresse ist erforderlich.';
|
||||||
|
if (!PRINTER_TYPES.includes(form.type)) return 'Ungültiger Druckertyp.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrinterFormDialog extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
form: props.initial ?? EMPTY_FORM,
|
||||||
|
busy: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (!prevProps.open && this.props.open) {
|
||||||
|
this.setState({ form: this.props.initial ?? EMPTY_FORM, busy: false, error: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(field, value) {
|
||||||
|
this.setState((prev) => ({ form: { ...prev.form, [field]: value }, error: null }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSubmit() {
|
||||||
|
const { form } = this.state;
|
||||||
|
const validationError = validateForm(form);
|
||||||
|
if (validationError) {
|
||||||
|
this.setState({ error: validationError });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ busy: true, error: null });
|
||||||
|
const body = {
|
||||||
|
address: form.address.trim(),
|
||||||
|
type: form.type,
|
||||||
|
model: form.model.trim() || null,
|
||||||
|
serial: form.serial.trim() || null,
|
||||||
|
desc: form.desc.trim() || null,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await this.props.onSubmit(body);
|
||||||
|
} catch (err) {
|
||||||
|
if (!this._unmounted) {
|
||||||
|
this.setState({ busy: false, error: backendErrorFrom(err) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._unmounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { open, onClose, title } = this.props;
|
||||||
|
const { form, busy, error } = this.state;
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={busy ? undefined : onClose} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Stack spacing={2} sx={{ mt: 1 }}>
|
||||||
|
<TextField
|
||||||
|
label="Adresse"
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
value={form.address}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => this.handleChange('address', e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="192.168.1.200"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Typ"
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
select
|
||||||
|
value={form.type}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => this.handleChange('type', e.target.value)}
|
||||||
|
>
|
||||||
|
{PRINTER_TYPES.map((t) => (
|
||||||
|
<MenuItem key={t} value={t}>{t}</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
label="Modell"
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
value={form.model}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => this.handleChange('model', e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="z. B. TM-T88VI"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Seriennummer"
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
value={form.serial}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => this.handleChange('serial', e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Beschreibung"
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
value={form.desc}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => this.handleChange('desc', e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="z. B. Bondrucker Bar"
|
||||||
|
/>
|
||||||
|
{error ? (
|
||||||
|
<Alert severity="error">
|
||||||
|
{typeof error === 'string' ? error : error.message}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClose} disabled={busy}>Abbrechen</Button>
|
||||||
|
<Button variant="contained" onClick={this.handleSubmit} disabled={busy}>
|
||||||
|
{busy ? <CircularProgress size={18} /> : 'Speichern'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PrinterSettingsPanel extends Component {
|
||||||
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
status: 'loading',
|
||||||
|
error: null,
|
||||||
|
printers: [],
|
||||||
|
draggingId: null,
|
||||||
|
dragOverId: null,
|
||||||
|
addOpen: false,
|
||||||
|
editPrinter: null,
|
||||||
|
deleteId: null,
|
||||||
|
deleteBusy: false,
|
||||||
|
};
|
||||||
|
this.handleRetry = this.handleRetry.bind(this);
|
||||||
|
this.handleAdd = this.handleAdd.bind(this);
|
||||||
|
this.handleAddSubmit = this.handleAddSubmit.bind(this);
|
||||||
|
this.handleEditSubmit = this.handleEditSubmit.bind(this);
|
||||||
|
this.handleCloseAdd = this.handleCloseAdd.bind(this);
|
||||||
|
this.handleCloseEdit = this.handleCloseEdit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
void this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.setState({ status: 'loading', error: null });
|
||||||
|
try {
|
||||||
|
const data = await this.context.printer.list();
|
||||||
|
requireObject(data, 'GET /api/printer');
|
||||||
|
const printers = requireArray(data.printers, 'printers');
|
||||||
|
this.setState({ status: 'ready', printers });
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ status: 'failed', error: backendErrorFrom(err) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRetry() {
|
||||||
|
void this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAdd() {
|
||||||
|
this.setState({ addOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseAdd() {
|
||||||
|
this.setState({ addOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseEdit() {
|
||||||
|
this.setState({ editPrinter: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleAddSubmit(body) {
|
||||||
|
const data = await this.context.printer.create(body);
|
||||||
|
requireObject(data, 'POST /api/printer');
|
||||||
|
const printer = data.printer;
|
||||||
|
this.setState((prev) => ({
|
||||||
|
printers: [...prev.printers, printer],
|
||||||
|
addOpen: false,
|
||||||
|
}));
|
||||||
|
enqueueSnackbar('Drucker hinzugefügt.', { variant: 'success' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleEditSubmit(body) {
|
||||||
|
const { editPrinter } = this.state;
|
||||||
|
const data = await this.context.printer.update(editPrinter.id, body);
|
||||||
|
requireObject(data, 'PATCH /api/printer/:id');
|
||||||
|
const updated = data.printer;
|
||||||
|
this.setState((prev) => ({
|
||||||
|
printers: prev.printers.map((p) => (p.id === updated.id ? updated : p)),
|
||||||
|
editPrinter: null,
|
||||||
|
}));
|
||||||
|
enqueueSnackbar('Drucker gespeichert.', { variant: 'success' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDelete(id) {
|
||||||
|
this.setState({ deleteId: id, deleteBusy: true });
|
||||||
|
try {
|
||||||
|
await this.context.printer.delete(id);
|
||||||
|
this.setState((prev) => ({
|
||||||
|
printers: prev.printers.filter((p) => p.id !== id),
|
||||||
|
deleteId: null,
|
||||||
|
deleteBusy: false,
|
||||||
|
}));
|
||||||
|
enqueueSnackbar('Drucker gelöscht.', { variant: 'success' });
|
||||||
|
} catch (err) {
|
||||||
|
if (this._unmounted) return;
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ deleteId: null, deleteBusy: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragStart(id) {
|
||||||
|
this.setState({ draggingId: id, dragOverId: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragOver(e, id) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (id !== this.state.dragOverId) {
|
||||||
|
this.setState({ dragOverId: id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragLeave() {
|
||||||
|
this.setState({ dragOverId: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDrop(targetId) {
|
||||||
|
const { draggingId, printers } = this.state;
|
||||||
|
this.setState({ draggingId: null, dragOverId: null });
|
||||||
|
if (!draggingId || draggingId === targetId) return;
|
||||||
|
|
||||||
|
const fromIdx = printers.findIndex((p) => p.id === draggingId);
|
||||||
|
const toIdx = printers.findIndex((p) => p.id === targetId);
|
||||||
|
if (fromIdx === -1 || toIdx === -1) return;
|
||||||
|
|
||||||
|
const reordered = [...printers];
|
||||||
|
const [item] = reordered.splice(fromIdx, 1);
|
||||||
|
reordered.splice(toIdx, 0, item);
|
||||||
|
const printerIds = reordered.map((p) => p.id);
|
||||||
|
|
||||||
|
this.setState({ printers: reordered });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.context.printer.order({ printerIds });
|
||||||
|
} catch (err) {
|
||||||
|
if (this._unmounted) return;
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ printers });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragEnd() {
|
||||||
|
this.setState({ draggingId: null, dragOverId: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._unmounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
status, error, printers,
|
||||||
|
draggingId, dragOverId,
|
||||||
|
addOpen, editPrinter,
|
||||||
|
deleteId, deleteBusy,
|
||||||
|
} = 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} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 1 }}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={this.handleAdd}
|
||||||
|
>
|
||||||
|
Drucker hinzufügen
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{printers.length === 0 ? (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ py: 2, textAlign: 'center' }}>
|
||||||
|
Noch keine Drucker konfiguriert.
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<TableContainer>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell sx={{ width: 32 }} />
|
||||||
|
<TableCell>Adresse</TableCell>
|
||||||
|
<TableCell>Typ</TableCell>
|
||||||
|
<TableCell>Modell</TableCell>
|
||||||
|
<TableCell>Beschreibung</TableCell>
|
||||||
|
<TableCell align="right" />
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{printers.map((printer) => {
|
||||||
|
const isDeleting = deleteId === printer.id && deleteBusy;
|
||||||
|
const isDragging = draggingId === printer.id;
|
||||||
|
const isDragOver = dragOverId === printer.id;
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={printer.id}
|
||||||
|
hover
|
||||||
|
draggable
|
||||||
|
onDragStart={() => this.handleDragStart(printer.id)}
|
||||||
|
onDragOver={(e) => this.handleDragOver(e, printer.id)}
|
||||||
|
onDragLeave={() => this.handleDragLeave()}
|
||||||
|
onDrop={() => this.handleDrop(printer.id)}
|
||||||
|
onDragEnd={() => this.handleDragEnd()}
|
||||||
|
sx={{
|
||||||
|
opacity: isDragging ? 0.4 : 1,
|
||||||
|
outline: isDragOver ? '2px solid' : 'none',
|
||||||
|
outlineColor: 'primary.main',
|
||||||
|
cursor: 'grab',
|
||||||
|
transition: 'opacity 0.15s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell sx={{ px: 0.5, color: 'text.disabled' }}>
|
||||||
|
<DragIndicatorIcon fontSize="small" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="body2">{printer.address}</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
|
||||||
|
{printer.type}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="body2" color={printer.model ? undefined : 'text.disabled'}>
|
||||||
|
{printer.model ?? '—'}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="body2" color={printer.desc ? undefined : 'text.disabled'}>
|
||||||
|
{printer.desc ?? '—'}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Stack direction="row" spacing={0} sx={{ justifyContent: 'flex-end' }}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
aria-label="Bearbeiten"
|
||||||
|
disabled={isDeleting}
|
||||||
|
onClick={() => this.setState({ editPrinter: printer })}
|
||||||
|
>
|
||||||
|
<EditOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
aria-label="Löschen"
|
||||||
|
color="error"
|
||||||
|
disabled={isDeleting}
|
||||||
|
onClick={() => void this.handleDelete(printer.id)}
|
||||||
|
>
|
||||||
|
{isDeleting ? (
|
||||||
|
<CircularProgress size={16} color="inherit" />
|
||||||
|
) : (
|
||||||
|
<DeleteOutlinedIcon fontSize="small" />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Reihenfolge durch Ziehen ändern. Unterstützte Typen: esc/pos, zpl.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<PrinterFormDialog
|
||||||
|
open={addOpen}
|
||||||
|
onClose={this.handleCloseAdd}
|
||||||
|
onSubmit={this.handleAddSubmit}
|
||||||
|
title="Drucker hinzufügen"
|
||||||
|
initial={EMPTY_FORM}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PrinterFormDialog
|
||||||
|
open={Boolean(editPrinter)}
|
||||||
|
onClose={this.handleCloseEdit}
|
||||||
|
onSubmit={this.handleEditSubmit}
|
||||||
|
title="Drucker bearbeiten"
|
||||||
|
initial={editPrinter ? formFromPrinter(editPrinter) : EMPTY_FORM}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,18 +6,22 @@ import DialogTitle from '@mui/material/DialogTitle';
|
|||||||
import Tab from '@mui/material/Tab';
|
import Tab from '@mui/material/Tab';
|
||||||
import Tabs from '@mui/material/Tabs';
|
import Tabs from '@mui/material/Tabs';
|
||||||
import NumbersIcon from '@mui/icons-material/Numbers';
|
import NumbersIcon from '@mui/icons-material/Numbers';
|
||||||
|
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
|
||||||
import TuneIcon from '@mui/icons-material/Tune';
|
import TuneIcon from '@mui/icons-material/Tune';
|
||||||
import NumberSettingsPanel from './NumberSettingsPanel';
|
import NumberSettingsPanel from './NumberSettingsPanel';
|
||||||
|
import PrinterSettingsPanel from './PrinterSettingsPanel';
|
||||||
import SettingsKeysPanel from './SettingsKeysPanel';
|
import SettingsKeysPanel from './SettingsKeysPanel';
|
||||||
|
|
||||||
const TAB_KEYS = {
|
const TAB_KEYS = {
|
||||||
keys: 'keys',
|
keys: 'keys',
|
||||||
numbers: 'numbers',
|
numbers: 'numbers',
|
||||||
|
printers: 'printers',
|
||||||
};
|
};
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: TAB_KEYS.keys, label: 'Einstellungen', icon: <TuneIcon />, Panel: SettingsKeysPanel },
|
{ id: TAB_KEYS.keys, label: 'Einstellungen', icon: <TuneIcon />, Panel: SettingsKeysPanel },
|
||||||
{ id: TAB_KEYS.numbers, label: 'Nummernkreise', icon: <NumbersIcon />, Panel: NumberSettingsPanel },
|
{ id: TAB_KEYS.numbers, label: 'Nummernkreise', icon: <NumbersIcon />, Panel: NumberSettingsPanel },
|
||||||
|
{ id: TAB_KEYS.printers, label: 'Drucker', icon: <PrintOutlinedIcon />, Panel: PrinterSettingsPanel },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default class SettingsDialog extends Component {
|
export default class SettingsDialog extends Component {
|
||||||
|
|||||||
@@ -27,16 +27,12 @@ const SETTABLE_KEYS = [
|
|||||||
'ui.theme',
|
'ui.theme',
|
||||||
'keys.tavily',
|
'keys.tavily',
|
||||||
'keys.openrouter',
|
'keys.openrouter',
|
||||||
'labelprinter.addr',
|
|
||||||
'labelprinter.type',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const KEY_LABELS = {
|
const KEY_LABELS = {
|
||||||
'ui.theme': 'UI-Theme',
|
'ui.theme': 'UI-Theme',
|
||||||
'keys.tavily': 'Tavily API-Key',
|
'keys.tavily': 'Tavily API-Key',
|
||||||
'keys.openrouter': 'OpenRouter API-Key',
|
'keys.openrouter': 'OpenRouter API-Key',
|
||||||
'labelprinter.addr': 'Labeldrucker-Adresse',
|
|
||||||
'labelprinter.type': 'Labeldrucker-Typ',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function keyLabel(key) {
|
function keyLabel(key) {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { default as SettingsDialog } from './SettingsDialog';
|
export { default as SettingsDialog } from './SettingsDialog';
|
||||||
export { default as SettingsKeysPanel } from './SettingsKeysPanel';
|
export { default as SettingsKeysPanel } from './SettingsKeysPanel';
|
||||||
export { default as NumberSettingsPanel } from './NumberSettingsPanel';
|
export { default as NumberSettingsPanel } from './NumberSettingsPanel';export { default as PrinterSettingsPanel } from './PrinterSettingsPanel';
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import InfoIcon from '@mui/icons-material/Info';
|
|||||||
import ViewListIcon from '@mui/icons-material/ViewList';
|
import ViewListIcon from '@mui/icons-material/ViewList';
|
||||||
import CategoryIcon from '@mui/icons-material/Category';
|
import CategoryIcon from '@mui/icons-material/Category';
|
||||||
import WarehouseIcon from '@mui/icons-material/Warehouse';
|
import WarehouseIcon from '@mui/icons-material/Warehouse';
|
||||||
|
import CopyrightIcon from '@mui/icons-material/Copyright';
|
||||||
import ListAltIcon from '@mui/icons-material/ListAlt';
|
import ListAltIcon from '@mui/icons-material/ListAlt';
|
||||||
import GroupsIcon from '@mui/icons-material/Groups';
|
import GroupsIcon from '@mui/icons-material/Groups';
|
||||||
import AddBoxIcon from '@mui/icons-material/AddBox';
|
import AddBoxIcon from '@mui/icons-material/AddBox';
|
||||||
@@ -103,6 +104,7 @@ export const SUB_NAV = {
|
|||||||
{ segment: '', label: 'Liste', title: 'Artikel', icon: ViewListIcon },
|
{ segment: '', label: 'Liste', title: 'Artikel', icon: ViewListIcon },
|
||||||
{ segment: 'kategorien', label: 'Kategorieen', title: 'Kategorien', icon: CategoryIcon },
|
{ segment: 'kategorien', label: 'Kategorieen', title: 'Kategorien', icon: CategoryIcon },
|
||||||
{ segment: 'lager', label: 'Lager', icon: WarehouseIcon },
|
{ segment: 'lager', label: 'Lager', icon: WarehouseIcon },
|
||||||
|
{ segment: 'marken', label: 'Marken', icon: CopyrightIcon },
|
||||||
],
|
],
|
||||||
'/lieferung': [
|
'/lieferung': [
|
||||||
{ segment: '', label: 'Liste', title: 'Lieferungen', icon: ListAltIcon },
|
{ segment: '', label: 'Liste', title: 'Lieferungen', icon: ListAltIcon },
|
||||||
|
|||||||
267
src/pages/artikel/marken.jsx
Normal file
267
src/pages/artikel/marken.jsx
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
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 Typography from '@mui/material/Typography';
|
||||||
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
|
import CopyrightIcon from '@mui/icons-material/Copyright';
|
||||||
|
import BackendError from '../../components/BackendError';
|
||||||
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
|
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||||
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
|
import { notifyError } from '../../utils/notify';
|
||||||
|
import { setLastArticleSku } from '../../automats';
|
||||||
|
import brandAutomat from '../../automats/brandAutomat';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
|
||||||
|
function draftsFrom(brands) {
|
||||||
|
return Object.fromEntries(brands.map((b) => [b.id, b.name]));
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtikelMarkenPage extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const initialBrands = brandAutomat.state.brands || [];
|
||||||
|
this.state = {
|
||||||
|
status: brandAutomat.state.status === 'succeeded' ? 'ready' : 'loading',
|
||||||
|
error: brandAutomat.state.error ? backendErrorFrom({ message: brandAutomat.state.error }) : null,
|
||||||
|
brands: initialBrands,
|
||||||
|
drafts: draftsFrom(initialBrands),
|
||||||
|
newName: '',
|
||||||
|
busyKey: null,
|
||||||
|
};
|
||||||
|
this._cancelled = false;
|
||||||
|
this.handleCreate = this.handleCreate.bind(this);
|
||||||
|
this.handleRetry = this.handleRetry.bind(this);
|
||||||
|
this.loadBrands = this.loadBrands.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
setLastArticleSku(null);
|
||||||
|
this._cancelled = false;
|
||||||
|
|
||||||
|
this.unsub = brandAutomat.subscribe((state) => {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
if (state.status === 'succeeded') {
|
||||||
|
this.applyBrands(state.brands);
|
||||||
|
} else if (state.status === 'failed') {
|
||||||
|
this.setState({ status: 'failed', error: backendErrorFrom({ message: state.error }) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (brandAutomat.state.status !== 'succeeded') {
|
||||||
|
void this.loadBrands();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._cancelled = true;
|
||||||
|
this.unsub?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyBrands(brands) {
|
||||||
|
this.setState((prev) => {
|
||||||
|
const currentDrafts = prev.drafts || {};
|
||||||
|
const nextDrafts = { ...draftsFrom(brands) };
|
||||||
|
for (const b of brands) {
|
||||||
|
if (prev.busyKey !== b.id && currentDrafts[b.id] !== undefined) {
|
||||||
|
nextDrafts[b.id] = currentDrafts[b.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: 'ready',
|
||||||
|
error: null,
|
||||||
|
brands,
|
||||||
|
drafts: nextDrafts,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadBrands() {
|
||||||
|
if (brandAutomat.state.status !== 'succeeded') {
|
||||||
|
this.setState({ status: 'loading', error: null });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const list = await brandAutomat.actions.load();
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.applyBrands(list);
|
||||||
|
} catch (err) {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.setState({ status: 'failed', error: backendErrorFrom(err) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRetry() {
|
||||||
|
void this.loadBrands();
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCreate() {
|
||||||
|
const name = this.state.newName.trim();
|
||||||
|
if (!name || this.state.busyKey != null) return;
|
||||||
|
|
||||||
|
this.setState({ busyKey: 'new' });
|
||||||
|
try {
|
||||||
|
const row = await brandAutomat.actions.upsert({ name });
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.setState({ newName: '', busyKey: null });
|
||||||
|
enqueueSnackbar(`Marke „${row.name || name}" angelegt.`, { variant: 'success' });
|
||||||
|
} catch (err) {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ busyKey: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRename(brand) {
|
||||||
|
const draft = String(this.state.drafts[brand.id] ?? '').trim();
|
||||||
|
if (!draft) {
|
||||||
|
this.setState((prev) => ({
|
||||||
|
drafts: { ...prev.drafts, [brand.id]: brand.name },
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (draft === brand.name || this.state.busyKey != null) return;
|
||||||
|
|
||||||
|
this.setState({ busyKey: brand.id });
|
||||||
|
try {
|
||||||
|
await brandAutomat.actions.upsert({ name: draft });
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.setState({ busyKey: null });
|
||||||
|
enqueueSnackbar('Marke gespeichert.', { variant: 'success' });
|
||||||
|
} catch (err) {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
notifyError(err);
|
||||||
|
this.setState((prev) => ({
|
||||||
|
busyKey: null,
|
||||||
|
drafts: { ...prev.drafts, [brand.id]: brand.name },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDelete(brand) {
|
||||||
|
if (this.state.busyKey != null) return;
|
||||||
|
if (!window.confirm(`Marke „${brand.name}" wirklich löschen?`)) return;
|
||||||
|
|
||||||
|
this.setState({ busyKey: brand.id });
|
||||||
|
try {
|
||||||
|
await brandAutomat.actions.remove(brand.id);
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.setState({ busyKey: null });
|
||||||
|
enqueueSnackbar('Marke gelöscht.', { variant: 'success' });
|
||||||
|
} catch (err) {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ busyKey: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { status, error, brands, drafts, newName, busyKey } = this.state;
|
||||||
|
const busy = busyKey != null;
|
||||||
|
|
||||||
|
if (status === 'loading') {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'failed') {
|
||||||
|
return <BackendError error={error} onRetry={this.handleRetry} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelTileGrid>
|
||||||
|
<PanelTile wide={2} minHeight={280} color="#ddd6fe">
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
Marken der ausgewählten Firma.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<List dense disablePadding>
|
||||||
|
{brands.length === 0 ? (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ py: 1 }}>
|
||||||
|
Noch keine Marken angelegt.
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
brands.map((b) => (
|
||||||
|
<ListItem
|
||||||
|
key={b.id}
|
||||||
|
disableGutters
|
||||||
|
sx={{ gap: 1, py: 0.75, alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
<CopyrightIcon sx={{ color: 'text.secondary', fontSize: 20, flexShrink: 0 }} />
|
||||||
|
<SingleLineTextEdit
|
||||||
|
size="small"
|
||||||
|
label=""
|
||||||
|
value={drafts[b.id] ?? ''}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(next) =>
|
||||||
|
this.setState((prev) => ({
|
||||||
|
drafts: { ...prev.drafts, [b.id]: next },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
onBlur={() => void this.handleRename(b)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.target.blur();
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.setState((prev) => ({
|
||||||
|
drafts: { ...prev.drafts, [b.id]: b.name },
|
||||||
|
}));
|
||||||
|
event.target.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
size="small"
|
||||||
|
aria-label={`„${b.name}" löschen`}
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => void this.handleDelete(b)}
|
||||||
|
>
|
||||||
|
<DeleteOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 2 }}>
|
||||||
|
<SingleLineTextEdit
|
||||||
|
size="small"
|
||||||
|
label="Neue Marke"
|
||||||
|
value={newName}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(next) => this.setState({ newName: next })}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
void this.handleCreate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
disabled={busy || !newName.trim()}
|
||||||
|
onClick={() => void this.handleCreate()}
|
||||||
|
>
|
||||||
|
{busyKey === 'new' ? '…' : 'Hinzufügen'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</PanelTile>
|
||||||
|
</PanelTileGrid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ArtikelMarkenPage;
|
||||||
Reference in New Issue
Block a user