This commit is contained in:
seb
2026-10-31 20:37:21 +01:00
parent cdb031921b
commit 6719cf9d51
7 changed files with 733 additions and 89 deletions

View File

@@ -3,21 +3,26 @@ import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import FormControlLabel from '@mui/material/FormControlLabel';
import IconButton from '@mui/material/IconButton';
import Switch from '@mui/material/Switch';
import TextField from '@mui/material/TextField';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import AddIcon from '@mui/icons-material/Add';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import LocalPrintshopOutlinedIcon from '@mui/icons-material/LocalPrintshopOutlined';
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined';
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
import QRCode from 'qrcode';
import { enqueueSnackbar } from 'notistack';
import QRCode from 'qrcode';
import BackendError from '../BackendError';
import companyAutomat from '../../automats/companyAutomat';
import { backendErrorFrom } from '../../utils/backendError';
@@ -114,6 +119,91 @@ class AgentQrCode extends PureComponent {
}
}
function renderPrinterDiagnostics(printer, live) {
if (!live) {
return (
<Chip
label="Offline"
size="small"
variant="outlined"
color="default"
sx={{ fontSize: '0.7rem', height: 20 }}
/>
);
}
const errors = live.errors || {};
const errorChips = [];
if (errors.headOpen) {
errorChips.push(
<Chip
key="headOpen"
label="Druckkopf offen"
size="small"
color="error"
sx={{ fontSize: '0.7rem', height: 20, fontWeight: 600 }}
/>,
);
}
if (errors.pause) {
errorChips.push(
<Chip
key="pause"
label="Pausiert"
size="small"
color="warning"
sx={{ fontSize: '0.7rem', height: 20, fontWeight: 600 }}
/>,
);
}
if (errors.unreachable) {
errorChips.push(
<Chip
key="unreachable"
label="Nicht erreichbar"
size="small"
color="error"
sx={{ fontSize: '0.7rem', height: 20, fontWeight: 600 }}
/>,
);
}
if (errors.other) {
errorChips.push(
<Chip
key="other"
label="Fehler"
size="small"
color="error"
sx={{ fontSize: '0.7rem', height: 20, fontWeight: 600 }}
/>,
);
}
if (errorChips.length > 0) {
return <Box sx={{ display: 'inline-flex', gap: 0.5, flexWrap: 'wrap' }}>{errorChips}</Box>;
}
return (
<Box sx={{ display: 'inline-flex', gap: 0.5, alignItems: 'center', flexWrap: 'wrap' }}>
<Chip
label="Bereit"
size="small"
color="success"
variant="outlined"
sx={{ fontSize: '0.7rem', height: 20, fontWeight: 600 }}
/>
{live.labelLength != null ? (
<Chip
label={`Etikett: ${live.labelLength} px`}
size="small"
variant="outlined"
sx={{ fontSize: '0.7rem', height: 20 }}
/>
) : null}
</Box>
);
}
export default class PrinterAgentSettingsPanel extends PureComponent {
constructor(props) {
super(props);
@@ -121,6 +211,12 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
printeragents: Array.isArray(companyAutomat.state.printeragents)
? companyAutomat.state.printeragents
: [],
agentprinters: Array.isArray(companyAutomat.state.agentprinters)
? companyAutomat.state.agentprinters
: [],
livePrinters: Array.isArray(companyAutomat.state.livePrinters)
? companyAutomat.state.livePrinters
: [],
error: companyAutomat.state.printeragentsError
? backendErrorFrom(companyAutomat.state.printeragentsError)
: null,
@@ -135,22 +231,32 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
this.handleRetry = this.handleRetry.bind(this);
this.handleCreateAgent = this.handleCreateAgent.bind(this);
this.handleDeleteAgent = this.handleDeleteAgent.bind(this);
this.handleToggleAgentEnabled = this.handleToggleAgentEnabled.bind(this);
this.handleStartRename = this.handleStartRename.bind(this);
this.handleCancelRename = this.handleCancelRename.bind(this);
this.handleSaveRename = this.handleSaveRename.bind(this);
this.handleSortDraftChange = this.handleSortDraftChange.bind(this);
this.handleSortSubmit = this.handleSortSubmit.bind(this);
this.handleMoveAgent = this.handleMoveAgent.bind(this);
this.handleTogglePrinterEnabled = this.handleTogglePrinterEnabled.bind(this);
this.handleMovePrinter = this.handleMovePrinter.bind(this);
this.handleDeletePrinter = this.handleDeletePrinter.bind(this);
}
componentDidMount() {
this.unsub = companyAutomat.subscribe(this, (state) => ({
printeragents: Array.isArray(state.printeragents) ? state.printeragents : [],
agentprinters: Array.isArray(state.agentprinters) ? state.agentprinters : [],
livePrinters: Array.isArray(state.livePrinters) ? state.livePrinters : [],
error: state.printeragentsError ? backendErrorFrom(state.printeragentsError) : null,
}));
if (companyAutomat.state.printeragentsStatus === 'idle') {
void companyAutomat.actions.loadPrinterAgents().catch(() => { });
void companyAutomat.actions.loadPrinterAgents().catch(() => {});
}
if (companyAutomat.state.agentprintersStatus === 'idle') {
void companyAutomat.actions.loadAgentPrinters().catch(() => {});
}
void companyAutomat.actions.loadLivePrinters().catch(() => {});
}
componentWillUnmount() {
@@ -159,6 +265,8 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
handleRetry() {
void companyAutomat.actions.loadPrinterAgents().catch(notifyError);
void companyAutomat.actions.loadAgentPrinters().catch(notifyError);
void companyAutomat.actions.loadLivePrinters().catch(() => {});
}
async handleCreateAgent() {
@@ -200,6 +308,23 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
}
}
async handleToggleAgentEnabled(agent, nextValue) {
this.setState({ patchingAgentId: agent.id });
try {
await companyAutomat.actions.updatePrinterAgent(agent.id, {
enabled: Boolean(nextValue),
});
enqueueSnackbar(
nextValue ? 'Drucker-Agent aktiviert.' : 'Drucker-Agent deaktiviert.',
{ variant: 'success' },
);
} catch (err) {
notifyError(err);
} finally {
this.setState({ patchingAgentId: null });
}
}
handleStartRename(agent) {
this.setState({ editingId: agent.id, editNameDraft: agent.name || '' });
}
@@ -291,10 +416,63 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
}
}
async handleTogglePrinterEnabled(printer, nextValue) {
try {
await companyAutomat.actions.updateAgentPrinter(printer.id, {
enabled: Boolean(nextValue),
});
enqueueSnackbar(
nextValue ? 'Drucker aktiviert.' : 'Drucker deaktiviert.',
{ variant: 'success' },
);
} catch (err) {
notifyError(err);
}
}
async handleMovePrinter(printer, direction) {
const { agentprinters } = this.state;
const printersForAgent = agentprinters.filter(
(p) => p.printeragentId === printer.printeragentId,
);
const currentIndex = printersForAgent.findIndex((p) => p.id === printer.id);
if (currentIndex === -1) return;
const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
if (targetIndex < 0 || targetIndex >= printersForAgent.length) return;
const reordered = [...printersForAgent];
const [moved] = reordered.splice(currentIndex, 1);
reordered.splice(targetIndex, 0, moved);
try {
await companyAutomat.actions.reorderAgentPrinters({
printeragentId: printer.printeragentId,
agentprinterIds: reordered.map((p) => p.id),
});
} catch (err) {
notifyError(err);
}
}
async handleDeletePrinter(printer) {
const label = printer.model || printer.serial || `#${printer.id}`;
if (!window.confirm(`Möchten Sie den Drucker „${label}“ wirklich entfernen?`)) {
return;
}
try {
await companyAutomat.actions.deleteAgentPrinter(printer.id);
enqueueSnackbar('Drucker entfernt.', { variant: 'success' });
} catch (err) {
notifyError(err);
}
}
render() {
const {
error,
printeragents,
agentprinters,
livePrinters,
newAgentName,
creatingAgent,
deletingId,
@@ -311,6 +489,12 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
);
}
// Build lookup maps for fast access
const livePrintersBySerial = new Map();
for (const lp of livePrinters) {
if (lp.serial) livePrintersBySerial.set(lp.serial, lp);
}
return (
<Box sx={{ p: 3, maxWidth: 900, mx: 'auto' }}>
<Box sx={{ mb: 2 }}>
@@ -318,8 +502,9 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
Drucker-Agenten
</Typography>
<Typography variant="body2" color="text.secondary">
Verwalten Sie externe Drucker-Agenten für Druckaufträge. Nach Erstellung eines Agenten
geben Sie die 6-stellige PIN auf dem Agenten-Gerät ein, um die Kopplung abzuschließen.
Verwalten Sie externe Drucker-Agenten und die daran angeschlossenen Drucker. Nach
Erstellung eines Agenten geben Sie die 6-stellige PIN auf dem Gerät ein oder scannen
Sie den QR-Code, um die Kopplung abzuschließen.
</Typography>
</Box>
@@ -327,7 +512,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
<Box sx={{ display: 'flex', gap: 1.5, mb: 3, alignItems: 'center' }}>
<TextField
size="small"
placeholder="Neuer Drucker-Agent Name (z.B. Bar Receipt Printer)"
placeholder="Neuer Drucker-Agent Name (z.B. Bar, Küche)"
value={newAgentName}
onChange={(e) => this.setState({ newAgentName: e.target.value })}
onKeyDown={(e) => {
@@ -346,7 +531,9 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
spellCheck: false,
},
input: {
startAdornment: <LocalPrintshopOutlinedIcon sx={{ mr: 1, color: 'text.secondary' }} />,
startAdornment: (
<LocalPrintshopOutlinedIcon sx={{ mr: 1, color: 'text.secondary' }} />
),
},
}}
sx={{ flex: 1 }}
@@ -370,16 +557,23 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
<Typography variant="body2">Keine Drucker-Agenten angelegt.</Typography>
</Box>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
{printeragents.map((agent, index) => {
const isDeleting = deletingId === agent.id;
const isPatching = patchingAgentId === agent.id;
const isEditing = editingId === agent.id;
const isPaired = agent.pairedAt != null;
const isEnabled = agent.enabled !== false;
const clientState = agent.clientstate;
const isOnline = clientState === 'online';
const pairedDateText = formatDateTime(agent.pairedAt);
const lastseenText = formatDateTime(agent.lastseenAt) ?? 'Nie';
const updatedText = formatDateTime(agent.updatedAt) ?? '—';
const printersForAgent = agentprinters.filter(
(p) => p.printeragentId === agent.id,
);
return (
<Box
key={agent.id}
@@ -387,15 +581,20 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
p: 2,
borderRadius: 2,
border: '1px solid',
borderColor: isPaired ? 'divider' : 'warning.light',
borderColor: !isEnabled
? 'action.disabledBackground'
: isPaired
? 'divider'
: 'warning.light',
bgcolor: 'background.paper',
display: 'flex',
flexDirection: 'column',
gap: 1.5,
transition: 'border-color 0.2s ease',
opacity: isEnabled ? 1 : 0.72,
transition: 'opacity 0.2s ease, border-color 0.2s ease',
}}
>
{/* Header: Icon, Name (or Edit input), Status chip, Actions */}
{/* Header: Icon, Name (or Edit input), Status chips, Enabled switch, Delete */}
<Box
sx={{
display: 'flex',
@@ -407,7 +606,14 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1, minWidth: 260 }}>
<LocalPrintshopOutlinedIcon
sx={{ color: isPaired ? 'primary.main' : 'warning.main', fontSize: 24 }}
sx={{
color: !isEnabled
? 'text.disabled'
: isPaired
? 'primary.main'
: 'warning.main',
fontSize: 24,
}}
/>
{isEditing ? (
@@ -455,7 +661,11 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
</Box>
) : (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="subtitle1" fontWeight={600}>
<Typography
variant="subtitle1"
fontWeight={600}
sx={{ color: isEnabled ? 'text.primary' : 'text.disabled' }}
>
{agent.name}
</Typography>
<IconButton
@@ -469,6 +679,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
</Box>
)}
{/* Pairing status badge */}
{isPaired ? (
<Chip
icon={<CheckCircleOutlinedIcon />}
@@ -488,10 +699,59 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
sx={{ fontWeight: 600, fontSize: '0.75rem' }}
/>
)}
{/* Live connection state */}
{clientState ? (
<Chip
label={isOnline ? 'Online' : 'Offline'}
size="small"
variant="outlined"
color={isOnline ? 'success' : 'default'}
sx={{ fontSize: '0.75rem', fontWeight: 600 }}
/>
) : null}
{/* Agent version */}
{agent.agentversion ? (
<Tooltip title="Agent Software-Version">
<Chip
label={`v${agent.agentversion}`}
size="small"
variant="outlined"
color="info"
sx={{ fontFamily: 'monospace', fontWeight: 600, fontSize: '0.75rem' }}
/>
</Tooltip>
) : null}
</Box>
{/* Delete control */}
{/* Enabled Switch and Delete action */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<FormControlLabel
control={
<Switch
size="small"
checked={isEnabled}
disabled={isPatching || isDeleting}
onChange={(e) => void this.handleToggleAgentEnabled(agent, e.target.checked)}
color="success"
/>
}
label={
<Typography
variant="body2"
sx={{
fontSize: '0.8125rem',
fontWeight: 500,
color: isEnabled ? 'text.primary' : 'text.disabled',
}}
>
{isEnabled ? 'Aktiv' : 'Inaktiv'}
</Typography>
}
sx={{ m: 0 }}
/>
<IconButton
size="small"
aria-label={`Drucker-Agent ${agent.name} löschen`}
@@ -504,7 +764,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
</Box>
</Box>
{/* Body: Pairing details with QR code or Paired status info */}
{/* Body: Pairing details with QR code or Discovered Printers */}
<Box
sx={{
display: 'flex',
@@ -534,27 +794,39 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
borderColor: 'warning.light',
}}
>
<Box sx={{ display: 'inline-flex' }}>
<AgentQrCode value={qrPayload} size={110} />
</Box>
<Tooltip title="QR-Code zum Koppeln mit dem Drucker-Agenten">
<Box sx={{ display: 'inline-flex' }}>
<AgentQrCode value={qrPayload} size={110} />
</Box>
</Tooltip>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, flex: 1, minWidth: 220 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 500 }}>
Kopplungs-PIN:
</Typography>
<Chip
label={agent.pairingpin || '—'}
color="warning"
variant="filled"
sx={{
fontFamily: 'monospace',
fontWeight: 700,
fontSize: '0.9rem',
letterSpacing: '0.1em',
px: 0.5,
}}
/>
<Tooltip title="PIN in die Zwischenablage kopieren">
<Chip
icon={<ContentCopyIcon sx={{ fontSize: 14 }} />}
label={agent.pairingpin || '—'}
onClick={() => {
if (agent.pairingpin) {
navigator.clipboard?.writeText(agent.pairingpin);
enqueueSnackbar('PIN in Zwischenablage kopiert.', { variant: 'info' });
}
}}
clickable
color="warning"
variant="filled"
sx={{
fontFamily: 'monospace',
fontWeight: 700,
fontSize: '0.9rem',
letterSpacing: '0.1em',
px: 0.5,
}}
/>
</Tooltip>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
@@ -567,75 +839,205 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
variant="outlined"
sx={{ fontFamily: 'monospace', fontSize: '0.8rem' }}
/>
<Tooltip title="QR-Daten (JSON) kopieren">
<IconButton
size="small"
onClick={() => {
navigator.clipboard?.writeText(qrPayload);
enqueueSnackbar('QR-Daten kopiert.', { variant: 'info' });
}}
sx={{ p: 0.5 }}
>
<ContentCopyIcon sx={{ fontSize: 14 }} />
</IconButton>
</Tooltip>
</Box>
<Typography variant="caption" color="text.secondary">
Scannen Sie den QR-Code mit dem Drucker-Agenten oder geben Sie Domain und PIN manuell ein.
</Typography>
</Box>
</Box>
);
})()
) : (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap' }}>
{agent.clientserial ? (
<Chip
label={`S/N: ${agent.clientserial}`}
size="small"
variant="outlined"
sx={{ fontFamily: 'monospace', fontSize: '0.75rem' }}
/>
) : null}
{agent.clientmodel ? (
<Chip
label={`Modell: ${agent.clientmodel}`}
size="small"
variant="outlined"
sx={{ fontSize: '0.75rem' }}
/>
) : null}
{agent.agentversion ? (
<Chip
label={`v${agent.agentversion}`}
size="small"
variant="outlined"
color="info"
sx={{ fontFamily: 'monospace', fontWeight: 600, fontSize: '0.75rem' }}
/>
) : null}
{agent.clientstate ? (
<Chip
label={`Status: ${agent.clientstate}`}
size="small"
variant="outlined"
sx={{ fontSize: '0.75rem' }}
/>
) : null}
/* Discovered printers section */
<Box sx={{ p: 1.5, borderRadius: 1.5, bgcolor: 'action.hover' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" fontWeight={600} color="text.secondary">
Erkannte Drucker ({printersForAgent.length})
</Typography>
</Box>
{printersForAgent.length === 0 ? (
<Typography
variant="body2"
color="text.secondary"
sx={{ fontStyle: 'italic', py: 0.5 }}
>
Keine Drucker erkannt. Schließen Sie einen Drucker per USB an den Agenten an.
</Typography>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{printersForAgent.map((p, pIdx) => {
const pEnabled = p.enabled !== false;
const live =
(p.serial ? livePrintersBySerial.get(p.serial) : null) ??
livePrinters.find(
(lp) => lp.agentId === agent.id && lp.serial === p.serial,
);
return (
<Box
key={p.id}
sx={{
p: 1.25,
borderRadius: 1,
border: '1px solid',
borderColor: 'divider',
bgcolor: 'background.paper',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 1.5,
flexWrap: 'wrap',
opacity: pEnabled ? 1 : 0.65,
transition: 'opacity 0.2s ease',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<PrintOutlinedIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
<Typography variant="body2" fontWeight={600}>
{p.model || 'Drucker'}
</Typography>
{p.type ? (
<Chip
label={p.type.toUpperCase()}
size="small"
color="primary"
variant="outlined"
sx={{ fontWeight: 600, fontSize: '0.7rem', height: 20 }}
/>
) : null}
{p.serial ? (
<Typography
variant="caption"
color="text.secondary"
sx={{ fontFamily: 'monospace' }}
>
({p.serial})
</Typography>
) : null}
{renderPrinterDiagnostics(p, live)}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* Enabled toggle switch */}
<FormControlLabel
control={
<Switch
size="small"
checked={pEnabled}
onChange={(e) =>
void this.handleTogglePrinterEnabled(p, e.target.checked)
}
/>
}
label={
<Typography
variant="caption"
color={pEnabled ? 'text.primary' : 'text.disabled'}
>
{pEnabled ? 'Aktiv' : 'Inaktiv'}
</Typography>
}
sx={{ m: 0 }}
/>
{/* Reorder buttons */}
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Tooltip title="Nach oben verschieben">
<span>
<IconButton
size="small"
disabled={pIdx === 0}
onClick={() => void this.handleMovePrinter(p, 'up')}
sx={{ p: 0.25 }}
>
<ArrowUpwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Nach unten verschieben">
<span>
<IconButton
size="small"
disabled={pIdx === printersForAgent.length - 1}
onClick={() => void this.handleMovePrinter(p, 'down')}
sx={{ p: 0.25 }}
>
<ArrowDownwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
</Box>
{/* Delete printer button */}
<Tooltip title="Drucker entfernen">
<IconButton
size="small"
color="error"
onClick={() => void this.handleDeletePrinter(p)}
sx={{ p: 0.5 }}
>
<DeleteOutlinedIcon sx={{ fontSize: 18 }} />
</IconButton>
</Tooltip>
</Box>
</Box>
);
})}
</Box>
)}
</Box>
)}
{/* Row with Sort Order Controls */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 0.5, pt: 0.5 }}>
{/* Agent sort order controls */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
gap: 0.5,
pt: 0.5,
}}
>
<Typography variant="body2" color="text.secondary">
Reihenfolge:
</Typography>
<span>
<IconButton
size="small"
disabled={index === 0 || isPatching || isDeleting}
onClick={() => void this.handleMoveAgent(agent, 'up')}
sx={{ p: 0.25 }}
>
<ArrowUpwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
<span>
<IconButton
size="small"
disabled={index === printeragents.length - 1 || isPatching || isDeleting}
onClick={() => void this.handleMoveAgent(agent, 'down')}
sx={{ p: 0.25 }}
>
<ArrowDownwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
<Tooltip title="Nach oben verschieben">
<span>
<IconButton
size="small"
disabled={index === 0 || isPatching || isDeleting}
onClick={() => void this.handleMoveAgent(agent, 'up')}
sx={{ p: 0.25 }}
>
<ArrowUpwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Nach unten verschieben">
<span>
<IconButton
size="small"
disabled={index === printeragents.length - 1 || isPatching || isDeleting}
onClick={() => void this.handleMoveAgent(agent, 'down')}
sx={{ p: 0.25 }}
>
<ArrowDownwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
<TextField
type="number"
size="small"