diff --git a/.agents/skills/backend/SKILL.md b/.agents/skills/backend/SKILL.md
index 7cc1b67..fde5a42 100644
--- a/.agents/skills/backend/SKILL.md
+++ b/.agents/skills/backend/SKILL.md
@@ -147,9 +147,9 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- `T5` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T6|null }`
- `T6` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T12, totalSizeBytes: int, durationMs: int }`
- `T7` = `{ id: int, name: str, pairingpin?: str|null, token?: str|null, pairedAt?: str|null, clientserial?: str|null, rootCategoryId?: int|null, syncRootArticles?: bool, sort?: int, enabled?: bool, clientversion?: str|null, lastseenAt?: str|null, updatedAt?: str|null, categoryIds?: int[] }`
-- `T8` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T39|null, incoming?: { date: str, amount: num }[] }`
-- `T9` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T23[] }, thumb?: str|null }`
-- `T10` = `{ id: int, name: str, companyId?: int, token?: str|null, pairingpin?: str|null, pairedAt?: str|null, clientserial?: str|null, clientmodel?: str|null, clientstate?: str|null, agentversion?: str|null, lastseenAt?: str|null, updatedAt?: str }`
+- `T8` = `{ id: int, name: str, companyId?: int, token?: str|null, pairingpin?: str|null, pairedAt?: str|null, clientserial?: str|null, clientmodel?: str|null, clientstate?: str|null, agentversion?: str|null, lastseenAt?: str|null, updatedAt?: str, sort?: int }`
+- `T9` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T39|null, incoming?: { date: str, amount: num }[] }`
+- `T10` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T23[] }, thumb?: str|null }`
- `T11` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }`
- `T12` = `{ vfs: T38, storage: T38, database: T32 }`
- `T13` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
@@ -220,7 +220,7 @@ _none_
- Returns: `204 void`
#### Get article by SKU — `GET /article/{sku}`
- Params: `sku: str`
-- Returns: `200 { article: { id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T27, brand: T26|null, categoryIds: int[], photos: { sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[], prices: { minmultiplier: num, net: num, taxRate: num, customergroupId: int }[], suppliers: T8[], undeliveredOrders: { orderId: int, orderNumber: str, date: str, amount: num, customerName?: str|null }[] } }`
+- Returns: `200 { article: { id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T27, brand: T26|null, categoryIds: int[], photos: { sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[], prices: { minmultiplier: num, net: num, taxRate: num, customergroupId: int }[], suppliers: T9[], undeliveredOrders: { orderId: int, orderNumber: str, date: str, amount: num, customerName?: str|null }[] } }`
#### Update article by SKU — `PATCH /article/{sku}`
- Params: `sku: str, body: { name?: str, desc?: str, seo?: str, unitamount?: num, unitSi?: str, track_stock?: bool }`
- Returns: `204 void`
@@ -295,10 +295,10 @@ _none_
- Returns: `200 { ok: bool }`
#### List article suppliers — `GET /article/{sku}/supplier`
- Params: `sku: str`
-- Returns: `200 { suppliers: T8[] }`
+- Returns: `200 { suppliers: T9[] }`
#### Assign supplier to article — `POST /article/{sku}/supplier`
- Params: `sku: str, body: { entityId: int, sku?: str|null, pu?: num, stock?: num|null, sort?: int|null, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
-- Returns: `201 { supplier: T8 }`
+- Returns: `201 { supplier: T9 }`
#### Reorder article suppliers — `PUT /article/{sku}/supplier/order`
- Params: `sku: str, body: { ids: int[] }`
- Returns: `200 { ids: int[] }`
@@ -307,7 +307,7 @@ _none_
- Returns: `204 void`
#### Update article supplier assignment — `PATCH /article/{sku}/supplier/{id}`
- Params: `sku: str, id: int, body: { sku?: str|null, pu?: num, stock?: num|null, sort?: int, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
-- Returns: `200 { supplier: T8 }`
+- Returns: `200 { supplier: T9 }`
#### Set article track_stock flag by SKU — `PUT /article/{sku}/track_stock`
- Params: `sku: str, body: { track_stock: bool }`
- Returns: `204 void`
@@ -650,19 +650,19 @@ _none_
- Returns: `200 { labeltemplates: { id: int, name: str, width: num, height: num, dpi: int, type: str, thumb?: str|null }[] }`
#### Create label template — `POST /labeltemplate`
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T23[] } }`
-- Returns: `201 { labeltemplate: T9 }`
+- Returns: `201 { labeltemplate: T10 }`
#### Delete label template — `DELETE /labeltemplate/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Get label template by ID — `GET /labeltemplate/{id}`
- Params: `id: int`
-- Returns: `200 { labeltemplate: T9 }`
+- Returns: `200 { labeltemplate: T10 }`
#### Update label template (partial) — `PATCH /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T23[] } }`
-- Returns: `200 { labeltemplate: T9 }`
+- Returns: `200 { labeltemplate: T10 }`
#### Update label template — `PUT /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T23[] } }`
-- Returns: `200 { labeltemplate: T9 }`
+- Returns: `200 { labeltemplate: T10 }`
### Section: /ledger
@@ -863,16 +863,16 @@ _none_
#### List printer agents — `GET /printeragent`
_none_
-- Returns: `200 { printeragents: T10[] }`
+- Returns: `200 { printeragents: T8[] }`
#### Create printer agent — `PUT /printeragent`
-- Params: `body: { name: str }`
-- Returns: `200 { printeragent: T10 }`
+- Params: `body: { name: str, sort?: int }`
+- Returns: `200 { printeragent: T8 }`
#### Delete printer agent — `DELETE /printeragent/{id}`
- Params: `id: int`
- Returns: `204 void`
-#### Rename printer agent — `PATCH /printeragent/{id}`
-- Params: `id: int, body: { name: str }`
-- Returns: `200 { printeragent: T10 }`
+#### Update printer agent — `PATCH /printeragent/{id}`
+- Params: `id: int, body: { name?: str, sort?: int }`
+- Returns: `200 { printeragent: T8 }`
### Section: /propertyname
diff --git a/src/api/index.js b/src/api/index.js
index f09659c..a55eacf 100644
--- a/src/api/index.js
+++ b/src/api/index.js
@@ -15,6 +15,7 @@ export { number } from './number.js';
export { order } from './order.js';
export { posterminal } from './posterminal.js';
export { printer } from './printer.js';
+export { printeragent } from './printeragent.js';
export { propertyname } from './propertyname.js';
export { quote } from './quote.js';
export { retoure } from './retoure.js';
diff --git a/src/api/printeragent.js b/src/api/printeragent.js
new file mode 100644
index 0000000..d696dad
--- /dev/null
+++ b/src/api/printeragent.js
@@ -0,0 +1,57 @@
+import { http } from './http.js';
+
+export const printeragent = {
+ /**
+ * GET /api/printeragent → { printeragents: Printeragent[] }
+ * List all printer agents for the selected company, sorted by sort ASC, id ASC.
+ */
+ list() {
+ return http('/api/printeragent');
+ },
+
+ /**
+ * PUT /api/printeragent
+ * Body: { name: string, sort?: number }
+ * Creates a new printer agent (auto-generates 6-digit pairingpin).
+ * → 200 { printeragent: Printeragent }
+ */
+ create({ name, sort } = {}) {
+ const body = { name };
+ if (sort !== undefined && sort !== null && sort !== '') {
+ body.sort = Number(sort);
+ }
+ return http('/api/printeragent', {
+ method: 'PUT',
+ body,
+ });
+ },
+
+ /**
+ * PATCH /api/printeragent/:id
+ * Body: { name?: string, sort?: number }
+ * Updates name, sort, or both.
+ * → 200 { printeragent: Printeragent }
+ */
+ update(id, patch = {}) {
+ const body = {};
+ if (patch.name !== undefined) body.name = patch.name;
+ if (patch.sort !== undefined && patch.sort !== null && patch.sort !== '') {
+ body.sort = Number(patch.sort);
+ }
+ return http(`/api/printeragent/${id}`, {
+ method: 'PATCH',
+ body,
+ });
+ },
+
+ /**
+ * DELETE /api/printeragent/:id → 204 No Content
+ * Permanently deletes agent.
+ */
+ delete(id) {
+ return http(`/api/printeragent/${id}`, {
+ method: 'DELETE',
+ });
+ },
+};
+
diff --git a/src/automats/companyAutomat.js b/src/automats/companyAutomat.js
index eb2b243..a371473 100644
--- a/src/automats/companyAutomat.js
+++ b/src/automats/companyAutomat.js
@@ -25,6 +25,7 @@ import {
company as companyApi,
number as numberApi,
posterminal as posterminalApi,
+ printeragent as printeragentApi,
supplier as supplierApi,
unit as unitApi,
warehouse as warehouseApi,
@@ -168,6 +169,15 @@ const sortTerminals = (terminals) => {
});
};
+const sortPrinterAgents = (agents) => {
+ if (!Array.isArray(agents)) return [];
+ return [...agents].sort((a, b) => {
+ const sDiff = (Number(a.sort) || 0) - (Number(b.sort) || 0);
+ if (sDiff !== 0) return sDiff;
+ return (Number(a.id) || 0) - (Number(b.id) || 0);
+ });
+};
+
const sortAccounts = (items) =>
[...items].sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de'));
@@ -237,6 +247,12 @@ const initialState = {
terminalsStatus: 'idle',
terminalsError: null,
+ // Printer agents
+ printeragents: [],
+ printeragentsCompanyId: null,
+ printeragentsStatus: 'idle',
+ printeragentsError: null,
+
// 4. Suppliers
suppliers: [],
articlesBySupplier: {},
@@ -303,6 +319,7 @@ let companyAutomat;
let inflightCompany = null;
let inflightNumbers = null;
let inflightTerminals = null;
+let inflightPrinterAgents = null;
let inflightSuppliers = null;
let inflightUnits = null;
let inflightWarehouses = null;
@@ -496,6 +513,7 @@ const actions = {
actions.load(),
actions.loadNumbers(),
actions.loadTerminals(),
+ actions.loadPrinterAgents(),
actions.loadSuppliers(),
actions.loadUnits(),
actions.loadWarehouses(),
@@ -512,6 +530,7 @@ const actions = {
inflightCompany = null;
inflightNumbers = null;
inflightTerminals = null;
+ inflightPrinterAgents = null;
inflightSuppliers = null;
inflightUnits = null;
inflightWarehouses = null;
@@ -698,6 +717,72 @@ const actions = {
},
// #endregion
+ // #region Printer agents (printeragent)
+ // MARK: calls printeragent.list from ../api/index.js
+ async loadPrinterAgents() {
+ const companyId =
+ authAutomat.state.user?.selectedCompanyId ?? companyAutomat.state.current?.id;
+ if (!companyId) return [];
+ if (inflightPrinterAgents) return inflightPrinterAgents;
+ companyAutomat.setState({ printeragentsStatus: 'loading', printeragentsError: null });
+ inflightPrinterAgents = (async () => {
+ try {
+ const res = await printeragentApi.list();
+ const printeragents = sortPrinterAgents(Array.isArray(res.printeragents) ? res.printeragents : []);
+ companyAutomat.setState({
+ printeragents,
+ printeragentsCompanyId: companyId,
+ printeragentsStatus: 'succeeded',
+ printeragentsError: null,
+ });
+ return printeragents;
+ } catch (err) {
+ companyAutomat.setState({
+ printeragentsStatus: 'failed',
+ printeragentsError: err?.message ?? String(err),
+ });
+ throw err;
+ } finally {
+ inflightPrinterAgents = null;
+ }
+ })();
+ return inflightPrinterAgents;
+ },
+
+ reloadPrinterAgents() {
+ return actions.loadPrinterAgents();
+ },
+
+ async createPrinterAgent(payload) {
+ const res = await printeragentApi.create(payload);
+ const agent = res.printeragent;
+ companyAutomat.setState({
+ printeragents: sortPrinterAgents([...companyAutomat.state.printeragents, agent]),
+ });
+ return agent;
+ },
+
+ async updatePrinterAgent(agentId, patch) {
+ const res = await printeragentApi.update(agentId, patch);
+ const updated = res.printeragent;
+ companyAutomat.setState({
+ printeragents: sortPrinterAgents(
+ companyAutomat.state.printeragents.map((a) =>
+ a.id === agentId ? { ...a, ...updated } : a,
+ ),
+ ),
+ });
+ return updated;
+ },
+
+ async deletePrinterAgent(agentId) {
+ await printeragentApi.delete(agentId);
+ companyAutomat.setState({
+ printeragents: companyAutomat.state.printeragents.filter((a) => a.id !== agentId),
+ });
+ },
+ // #endregion
+
// #region 4. Suppliers (supplier)
// MARK: calls supplier.list from ../api/index.js
loadSuppliers() {
@@ -1553,6 +1638,9 @@ sseAutomat.actions.on('carrier', () => {
sseAutomat.actions.on('posterminal', () => {
void companyAutomat.actions.loadTerminals().catch(() => {});
});
+sseAutomat.actions.on('printeragent', () => {
+ void companyAutomat.actions.loadPrinterAgents().catch(() => {});
+});
sseAutomat.actions.on('customergroup', () => {
void companyAutomat.actions.reloadCustomergroups().catch(() => {});
});
@@ -1593,6 +1681,11 @@ export const selectPosterminals = () => companyAutomat.state.terminals;
export const selectPosterminalStatus = () => companyAutomat.state.terminalsStatus;
export const selectHasPosterminals = () => companyAutomat.state.terminals.length > 0;
+// Printer agents selectors
+export const selectPrinterAgents = () => companyAutomat.state.printeragents;
+export const selectPrinterAgentStatus = () => companyAutomat.state.printeragentsStatus;
+export const selectHasPrinterAgents = () => companyAutomat.state.printeragents.length > 0;
+
// Suppliers selectors
export const selectSuppliers = () => companyAutomat.state.suppliers;
export const selectSuppliersStatus = () => companyAutomat.state.suppliersStatus;
diff --git a/src/automats/index.js b/src/automats/index.js
index c454244..ce805af 100644
--- a/src/automats/index.js
+++ b/src/automats/index.js
@@ -151,6 +151,9 @@ export {
selectPosterminals,
selectPosterminalStatus,
selectHasPosterminals,
+ selectPrinterAgents,
+ selectPrinterAgentStatus,
+ selectHasPrinterAgents,
selectSuppliers,
selectSuppliersStatus,
selectUnits,
diff --git a/src/components/settings/PrinterAgentSettingsPanel.jsx b/src/components/settings/PrinterAgentSettingsPanel.jsx
new file mode 100644
index 0000000..ec97dbe
--- /dev/null
+++ b/src/components/settings/PrinterAgentSettingsPanel.jsx
@@ -0,0 +1,600 @@
+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 IconButton from '@mui/material/IconButton';
+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 CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined';
+import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
+import { enqueueSnackbar } from 'notistack';
+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;
+ }
+}
+
+export default class PrinterAgentSettingsPanel extends PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ printeragents: Array.isArray(companyAutomat.state.printeragents)
+ ? companyAutomat.state.printeragents
+ : [],
+ 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.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);
+ }
+
+ componentDidMount() {
+ this.unsub = companyAutomat.subscribe(this, (state) => ({
+ printeragents: Array.isArray(state.printeragents) ? state.printeragents : [],
+ error: state.printeragentsError ? backendErrorFrom(state.printeragentsError) : null,
+ }));
+ if (companyAutomat.state.printeragentsStatus === 'idle') {
+ void companyAutomat.actions.loadPrinterAgents().catch(() => {});
+ }
+ }
+
+ componentWillUnmount() {
+ this.unsub?.();
+ }
+
+ handleRetry() {
+ void companyAutomat.actions.loadPrinterAgents().catch(notifyError);
+ }
+
+ 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 });
+ }
+ }
+
+ 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 });
+ }
+ }
+
+ render() {
+ const {
+ error,
+ printeragents,
+ newAgentName,
+ creatingAgent,
+ deletingId,
+ patchingAgentId,
+ editingId,
+ editNameDraft,
+ } = this.state;
+
+ if (error && printeragents.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Drucker-Agenten
+
+
+ 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.
+
+
+
+ {/* 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 pairedDateText = formatDateTime(agent.pairedAt);
+ const lastseenText = formatDateTime(agent.lastseenAt) ?? 'Nie';
+ const updatedText = formatDateTime(agent.updatedAt) ?? '—';
+
+ return (
+
+ {/* Header: Icon, Name (or Edit input), Status chip, Actions */}
+
+
+
+
+ {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' }}
+ >
+
+
+
+
+ )}
+
+ {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' }}
+ />
+ )}
+
+
+ {/* Delete control */}
+
+ void this.handleDeleteAgent(agent)}
+ disabled={isDeleting || creatingAgent || isPatching}
+ color="error"
+ >
+
+
+
+
+
+ {/* Body: Pairing details or PIN info */}
+
+ {!isPaired ? (
+
+
+ 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,
+ }}
+ />
+
+
+ Geben Sie diesen PIN auf dem Drucker-Agent-Gerät ein.
+
+
+ ) : (
+
+ {agent.clientserial ? (
+
+ ) : null}
+ {agent.clientmodel ? (
+
+ ) : null}
+ {agent.agentversion ? (
+
+ ) : null}
+ {agent.clientstate ? (
+
+ ) : null}
+
+ )}
+
+ {/* 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}
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+ }
+}
diff --git a/src/components/settings/SettingsDialog.jsx b/src/components/settings/SettingsDialog.jsx
index 2ac7986..1446644 100644
--- a/src/components/settings/SettingsDialog.jsx
+++ b/src/components/settings/SettingsDialog.jsx
@@ -18,6 +18,7 @@ import authAutomat from '../../automats/authAutomat';
import CompanySettingsPanel from './CompanySettingsPanel';
import NumberSettingsPanel from './NumberSettingsPanel';
import PrinterSettingsPanel from './PrinterSettingsPanel';
+import PrinterAgentSettingsPanel from './PrinterAgentSettingsPanel';
import SettingsKeysPanel from './SettingsKeysPanel';
import ImportExportSettingsPanel from './ImportExportSettingsPanel';
import PosTerminalSettingsPanel from './PosTerminalSettingsPanel';
@@ -27,6 +28,7 @@ import HealthSettingsPanel from './HealthSettingsPanel';
import MonitorHeartOutlinedIcon from '@mui/icons-material/MonitorHeartOutlined';
import SecurityIcon from '@mui/icons-material/Security';
import BackupTableOutlinedIcon from '@mui/icons-material/BackupTableOutlined';
+import LocalPrintshopOutlinedIcon from '@mui/icons-material/LocalPrintshopOutlined';
import { SURFACE_BASE } from '../../colors';
const TAB_KEYS = {
@@ -35,6 +37,7 @@ const TAB_KEYS = {
keys: 'keys',
numbers: 'numbers',
printers: 'printers',
+ printeragent: 'printeragent',
importExport: 'importExport',
backup: 'backup',
posterminal: 'posterminal',
@@ -47,6 +50,7 @@ const TABS = [
{ id: TAB_KEYS.keys, label: 'Einstellungen', icon: , Panel: SettingsKeysPanel },
{ id: TAB_KEYS.numbers, label: 'Nummernkreise', icon: , Panel: NumberSettingsPanel },
{ id: TAB_KEYS.printers, label: 'Drucker', icon: , Panel: PrinterSettingsPanel },
+ { id: TAB_KEYS.printeragent, label: 'Drucker-Agent', icon: , Panel: PrinterAgentSettingsPanel },
{ id: TAB_KEYS.importExport, label: 'Import/Export', icon: , Panel: ImportExportSettingsPanel },
{ id: TAB_KEYS.backup, label: 'Backups', icon: , Panel: BackupSettingsPanel },
{ id: TAB_KEYS.posterminal, label: 'POS Terminal', icon: , Panel: PosTerminalSettingsPanel },