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 }} /> {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} ); })} )} ); } }