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', 'ipp']; 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 ( {title} this.handleChange('address', e.target.value)} autoComplete="off" placeholder="192.168.1.200" /> this.handleChange('type', e.target.value)} > {PRINTER_TYPES.map((t) => ( {t} ))} this.handleChange('model', e.target.value)} autoComplete="off" placeholder="z. B. TM-T88VI" /> this.handleChange('serial', e.target.value)} autoComplete="off" /> this.handleChange('desc', e.target.value)} autoComplete="off" placeholder="z. B. Bondrucker Bar" /> {error ? ( {typeof error === 'string' ? error : error.message} ) : null} ); } } 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 ( ); } if (status === 'failed') { return ; } return ( <> {printers.length === 0 ? ( Noch keine Drucker konfiguriert. ) : ( Adresse Typ Modell Beschreibung {printers.map((printer) => { const isDeleting = deleteId === printer.id && deleteBusy; const isDragging = draggingId === printer.id; const isDragOver = dragOverId === printer.id; return ( 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', }} > {printer.address} {printer.type} {printer.model ?? '—'} {printer.desc ?? '—'} this.setState({ editPrinter: printer })} > void this.handleDelete(printer.id)} > {isDeleting ? ( ) : ( )} ); })}
)} Reihenfolge durch Ziehen ändern. Unterstützte Typen: esc/pos, zpl, ipp. ); } }