import { PureComponent } from 'react';
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 { enqueueSnackbar } from 'notistack';
import QRCode from 'qrcode';
import BackendError from '../BackendError';
import companyAutomat from '../../automats/companyAutomat';
import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify';
function formatDateTime(val) {
if (!val) return null;
try {
const d = new Date(val);
if (isNaN(d.getTime())) return null;
return d.toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch {
return null;
}
}
class AgentQrCode extends PureComponent {
constructor(props) {
super(props);
this.state = { dataUrl: null };
}
componentDidMount() {
this.generateQr();
}
componentDidUpdate(prevProps) {
if (prevProps.value !== this.props.value || prevProps.size !== this.props.size) {
this.generateQr();
}
}
async generateQr() {
const { value, size = 110 } = this.props;
if (!value) {
this.setState({ dataUrl: null });
return;
}
try {
const dataUrl = await QRCode.toDataURL(value, {
width: size,
margin: 1,
color: {
dark: '#000000',
light: '#ffffff',
},
errorCorrectionLevel: 'M',
});
this.setState({ dataUrl });
} catch (err) {
console.error('Failed to generate QR code:', err);
}
}
render() {
const { size = 110 } = this.props;
const { dataUrl } = this.state;
if (!dataUrl) {
return (
);
}
return (
);
}
}
function renderPrinterDiagnostics(printer, live) {
if (!live) {
return (
);
}
const errors = live.errors || {};
const errorChips = [];
if (errors.headOpen) {
errorChips.push(
,
);
}
if (errors.pause) {
errorChips.push(
,
);
}
if (errors.unreachable) {
errorChips.push(
,
);
}
if (errors.other) {
errorChips.push(
,
);
}
if (errorChips.length > 0) {
return {errorChips};
}
return (
{live.labelLength != null ? (
) : null}
);
}
export default class PrinterAgentSettingsPanel extends PureComponent {
constructor(props) {
super(props);
this.state = {
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,
newAgentName: '',
creatingAgent: false,
deletingId: null,
patchingAgentId: null,
editingId: null,
editNameDraft: '',
sortDrafts: {},
};
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(() => {});
}
if (companyAutomat.state.agentprintersStatus === 'idle') {
void companyAutomat.actions.loadAgentPrinters().catch(() => {});
}
void companyAutomat.actions.loadLivePrinters().catch(() => {});
}
componentWillUnmount() {
this.unsub?.();
}
handleRetry() {
void companyAutomat.actions.loadPrinterAgents().catch(notifyError);
void companyAutomat.actions.loadAgentPrinters().catch(notifyError);
void companyAutomat.actions.loadLivePrinters().catch(() => {});
}
async handleCreateAgent() {
const { newAgentName, printeragents } = this.state;
const trimmed = newAgentName.trim();
if (!trimmed) return;
this.setState({ creatingAgent: true });
try {
const nextSort =
printeragents.length > 0
? Math.max(...printeragents.map((a) => Number(a.sort) || 0)) + 1
: 0;
await companyAutomat.actions.createPrinterAgent({
name: trimmed,
sort: nextSort,
});
this.setState({ newAgentName: '' });
enqueueSnackbar('Drucker-Agent hinzugefügt.', { variant: 'success' });
} catch (err) {
notifyError(err);
} finally {
this.setState({ creatingAgent: false });
}
}
async handleDeleteAgent(agent) {
if (!window.confirm(`Möchten Sie den Drucker-Agenten „${agent.name}“ wirklich löschen?`)) {
return;
}
this.setState({ deletingId: agent.id });
try {
await companyAutomat.actions.deletePrinterAgent(agent.id);
enqueueSnackbar('Drucker-Agent gelöscht.', { variant: 'success' });
} catch (err) {
notifyError(err);
} finally {
this.setState({ deletingId: null });
}
}
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 || '' });
}
handleCancelRename() {
this.setState({ editingId: null, editNameDraft: '' });
}
async handleSaveRename(agent) {
const { editNameDraft } = this.state;
const trimmed = editNameDraft.trim();
if (!trimmed || trimmed === agent.name) {
this.handleCancelRename();
return;
}
this.setState({ patchingAgentId: agent.id });
try {
await companyAutomat.actions.updatePrinterAgent(agent.id, { name: trimmed });
enqueueSnackbar('Name aktualisiert.', { variant: 'success' });
this.setState({ editingId: null, editNameDraft: '' });
} catch (err) {
notifyError(err);
} finally {
this.setState({ patchingAgentId: null });
}
}
handleSortDraftChange(agentId, value) {
this.setState((prev) => ({
sortDrafts: { ...prev.sortDrafts, [agentId]: value },
}));
}
async handleSortSubmit(agent) {
const draft = this.state.sortDrafts[agent.id];
if (draft === undefined) return;
const nextSort = Number(draft);
if (isNaN(nextSort) || nextSort === (agent.sort ?? 0)) {
this.setState((prev) => {
const next = { ...prev.sortDrafts };
delete next[agent.id];
return { sortDrafts: next };
});
return;
}
this.setState({ patchingAgentId: agent.id });
try {
await companyAutomat.actions.updatePrinterAgent(agent.id, { sort: nextSort });
this.setState((prev) => {
const next = { ...prev.sortDrafts };
delete next[agent.id];
return { sortDrafts: next };
});
enqueueSnackbar('Reihenfolge gespeichert.', { variant: 'success' });
} catch (err) {
notifyError(err);
} finally {
this.setState({ patchingAgentId: null });
}
}
async handleMoveAgent(agent, direction) {
const { printeragents } = this.state;
const currentIndex = printeragents.findIndex((a) => a.id === agent.id);
if (currentIndex === -1) return;
const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
if (targetIndex < 0 || targetIndex >= printeragents.length) return;
const targetAgent = printeragents[targetIndex];
const currentSort = Number(agent.sort) || 0;
const targetSort = Number(targetAgent.sort) || 0;
let newCurrentSort = targetSort;
let newTargetSort = currentSort;
if (newCurrentSort === newTargetSort) {
newCurrentSort = direction === 'up' ? targetSort - 1 : targetSort + 1;
}
this.setState({ patchingAgentId: agent.id });
try {
await Promise.all([
companyAutomat.actions.updatePrinterAgent(agent.id, { sort: newCurrentSort }),
companyAutomat.actions.updatePrinterAgent(targetAgent.id, { sort: newTargetSort }),
]);
} catch (err) {
notifyError(err);
} finally {
this.setState({ patchingAgentId: null });
}
}
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,
patchingAgentId,
editingId,
editNameDraft,
} = this.state;
if (error && printeragents.length === 0) {
return (
);
}
// Build lookup maps for fast access
const livePrintersBySerial = new Map();
for (const lp of livePrinters) {
if (lp.serial) livePrintersBySerial.set(lp.serial, lp);
}
return (
Drucker-Agenten
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.
{/* Add new agent row */}
this.setState({ newAgentName: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
void this.handleCreateAgent();
}
}}
disabled={creatingAgent}
autoComplete="off"
slotProps={{
htmlInput: {
autoComplete: 'off',
autoCorrect: 'off',
autoCapitalize: 'off',
spellCheck: false,
},
input: {
startAdornment: (
),
},
}}
sx={{ flex: 1 }}
/>
}
onClick={this.handleCreateAgent}
disabled={creatingAgent || !newAgentName.trim()}
>
Hinzufügen
{printeragents.length === 0 ? (
Keine Drucker-Agenten angelegt.
) : (
{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 (
{/* Header: Icon, Name (or Edit input), Status chips, Enabled switch, Delete */}
{isEditing ? (
this.setState({ editNameDraft: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
void this.handleSaveRename(agent);
} else if (e.key === 'Escape') {
this.handleCancelRename();
}
}}
autoFocus
disabled={isPatching}
autoComplete="off"
slotProps={{
htmlInput: {
autoComplete: 'off',
autoCorrect: 'off',
autoCapitalize: 'off',
spellCheck: false,
},
}}
sx={{ minWidth: 200 }}
/>
void this.handleSaveRename(agent)}
disabled={isPatching}
>
) : (
{agent.name}
this.handleStartRename(agent)}
disabled={isPatching || isDeleting}
sx={{ p: 0.5, color: 'text.secondary' }}
>
)}
{/* Pairing status badge */}
{isPaired ? (
}
label="Gekoppelt"
size="small"
variant="outlined"
color="success"
sx={{ fontWeight: 600, fontSize: '0.75rem' }}
/>
) : (
}
label="Nicht gekoppelt"
size="small"
variant="outlined"
color="warning"
sx={{ fontWeight: 600, fontSize: '0.75rem' }}
/>
)}
{/* Live connection state */}
{clientState ? (
) : null}
{/* Agent version */}
{agent.agentversion ? (
) : null}
{/* Enabled Switch and Delete action */}
void this.handleToggleAgentEnabled(agent, e.target.checked)}
color="success"
/>
}
label={
{isEnabled ? 'Aktiv' : 'Inaktiv'}
}
sx={{ m: 0 }}
/>
void this.handleDeleteAgent(agent)}
disabled={isDeleting || creatingAgent || isPatching}
color="error"
>
{/* Body: Pairing details with QR code or Discovered Printers */}
{!isPaired ? (
(() => {
const domain = typeof window !== 'undefined' ? window.location.host : '';
const qrPayload = JSON.stringify({
domain,
pin: String(agent.pairingpin || ''),
});
return (
Kopplungs-PIN:
}
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,
}}
/>
Domain:
{
navigator.clipboard?.writeText(qrPayload);
enqueueSnackbar('QR-Daten kopiert.', { variant: 'info' });
}}
sx={{ p: 0.5 }}
>
Scannen Sie den QR-Code mit dem Drucker-Agenten oder geben Sie Domain und PIN manuell ein.
);
})()
) : (
/* Discovered printers section */
Erkannte Drucker ({printersForAgent.length})
{printersForAgent.length === 0 ? (
Keine Drucker erkannt. Schließen Sie einen Drucker per USB an den Agenten an.
) : (
{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 (
{p.model || 'Drucker'}
{p.type ? (
) : null}
{p.serial ? (
({p.serial})
) : null}
{renderPrinterDiagnostics(p, live)}
{/* Enabled toggle switch */}
void this.handleTogglePrinterEnabled(p, e.target.checked)
}
/>
}
label={
{pEnabled ? 'Aktiv' : 'Inaktiv'}
}
sx={{ m: 0 }}
/>
{/* Reorder buttons */}
void this.handleMovePrinter(p, 'up')}
sx={{ p: 0.25 }}
>
void this.handleMovePrinter(p, 'down')}
sx={{ p: 0.25 }}
>
{/* Delete printer button */}
void this.handleDeletePrinter(p)}
sx={{ p: 0.5 }}
>
);
})}
)}
)}
{/* Agent sort order controls */}
Reihenfolge:
void this.handleMoveAgent(agent, 'up')}
sx={{ p: 0.25 }}
>
void this.handleMoveAgent(agent, 'down')}
sx={{ p: 0.25 }}
>
this.handleSortDraftChange(agent.id, e.target.value)}
onBlur={() => void this.handleSortSubmit(agent)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
e.target.blur();
}
}}
slotProps={{
htmlInput: {
step: 1,
style: { width: 44, padding: '3px 6px', textAlign: 'center', fontSize: '0.8125rem' },
},
}}
sx={{ width: 56 }}
/>
{/* Dates Row */}
{isPaired && pairedDateText ? (
Gekoppelt am: {pairedDateText}
) : null}
Zuletzt gesehen: {lastseenText}
Aktualisiert: {updatedText}
);
})}
)}
);
}
}