u
This commit is contained in:
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user