This commit is contained in:
seb
2026-10-30 17:30:10 +01:00
parent 72f168f188
commit 56d376c9bd
7 changed files with 775 additions and 17 deletions

View File

@@ -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 }` - `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 }` - `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[] }` - `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 }[] }` - `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, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T23[] }, thumb?: str|null }` - `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, 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 }` - `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 }` - `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 }` - `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 }` - `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` - Returns: `204 void`
#### Get article by SKU — `GET /article/{sku}` #### Get article by SKU — `GET /article/{sku}`
- Params: `sku: str` - 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}` #### Update article by SKU — `PATCH /article/{sku}`
- Params: `sku: str, body: { name?: str, desc?: str, seo?: str, unitamount?: num, unitSi?: str, track_stock?: bool }` - Params: `sku: str, body: { name?: str, desc?: str, seo?: str, unitamount?: num, unitSi?: str, track_stock?: bool }`
- Returns: `204 void` - Returns: `204 void`
@@ -295,10 +295,10 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### List article suppliers — `GET /article/{sku}/supplier` #### List article suppliers — `GET /article/{sku}/supplier`
- Params: `sku: str` - Params: `sku: str`
- Returns: `200 { suppliers: T8[] }` - Returns: `200 { suppliers: T9[] }`
#### Assign supplier to article — `POST /article/{sku}/supplier` #### 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 }` - 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` #### Reorder article suppliers — `PUT /article/{sku}/supplier/order`
- Params: `sku: str, body: { ids: int[] }` - Params: `sku: str, body: { ids: int[] }`
- Returns: `200 { ids: int[] }` - Returns: `200 { ids: int[] }`
@@ -307,7 +307,7 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Update article supplier assignment — `PATCH /article/{sku}/supplier/{id}` #### 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 }` - 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` #### Set article track_stock flag by SKU — `PUT /article/{sku}/track_stock`
- Params: `sku: str, body: { track_stock: bool }` - Params: `sku: str, body: { track_stock: bool }`
- Returns: `204 void` - 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 }[] }` - Returns: `200 { labeltemplates: { id: int, name: str, width: num, height: num, dpi: int, type: str, thumb?: str|null }[] }`
#### Create label template — `POST /labeltemplate` #### Create label template — `POST /labeltemplate`
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T23[] } }` - 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}` #### Delete label template — `DELETE /labeltemplate/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Get label template by ID — `GET /labeltemplate/{id}` #### Get label template by ID — `GET /labeltemplate/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { labeltemplate: T9 }` - Returns: `200 { labeltemplate: T10 }`
#### Update label template (partial) — `PATCH /labeltemplate/{id}` #### Update label template (partial) — `PATCH /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T23[] } }` - 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}` #### Update label template — `PUT /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T23[] } }` - 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 ### Section: /ledger
@@ -863,16 +863,16 @@ _none_
#### List printer agents — `GET /printeragent` #### List printer agents — `GET /printeragent`
_none_ _none_
- Returns: `200 { printeragents: T10[] }` - Returns: `200 { printeragents: T8[] }`
#### Create printer agent — `PUT /printeragent` #### Create printer agent — `PUT /printeragent`
- Params: `body: { name: str }` - Params: `body: { name: str, sort?: int }`
- Returns: `200 { printeragent: T10 }` - Returns: `200 { printeragent: T8 }`
#### Delete printer agent — `DELETE /printeragent/{id}` #### Delete printer agent — `DELETE /printeragent/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Rename printer agent — `PATCH /printeragent/{id}` #### Update printer agent — `PATCH /printeragent/{id}`
- Params: `id: int, body: { name: str }` - Params: `id: int, body: { name?: str, sort?: int }`
- Returns: `200 { printeragent: T10 }` - Returns: `200 { printeragent: T8 }`
### Section: /propertyname ### Section: /propertyname

View File

@@ -15,6 +15,7 @@ export { number } from './number.js';
export { order } from './order.js'; export { order } from './order.js';
export { posterminal } from './posterminal.js'; export { posterminal } from './posterminal.js';
export { printer } from './printer.js'; export { printer } from './printer.js';
export { printeragent } from './printeragent.js';
export { propertyname } from './propertyname.js'; export { propertyname } from './propertyname.js';
export { quote } from './quote.js'; export { quote } from './quote.js';
export { retoure } from './retoure.js'; export { retoure } from './retoure.js';

57
src/api/printeragent.js Normal file
View File

@@ -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',
});
},
};

View File

@@ -25,6 +25,7 @@ import {
company as companyApi, company as companyApi,
number as numberApi, number as numberApi,
posterminal as posterminalApi, posterminal as posterminalApi,
printeragent as printeragentApi,
supplier as supplierApi, supplier as supplierApi,
unit as unitApi, unit as unitApi,
warehouse as warehouseApi, 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) => const sortAccounts = (items) =>
[...items].sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de')); [...items].sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de'));
@@ -237,6 +247,12 @@ const initialState = {
terminalsStatus: 'idle', terminalsStatus: 'idle',
terminalsError: null, terminalsError: null,
// Printer agents
printeragents: [],
printeragentsCompanyId: null,
printeragentsStatus: 'idle',
printeragentsError: null,
// 4. Suppliers // 4. Suppliers
suppliers: [], suppliers: [],
articlesBySupplier: {}, articlesBySupplier: {},
@@ -303,6 +319,7 @@ let companyAutomat;
let inflightCompany = null; let inflightCompany = null;
let inflightNumbers = null; let inflightNumbers = null;
let inflightTerminals = null; let inflightTerminals = null;
let inflightPrinterAgents = null;
let inflightSuppliers = null; let inflightSuppliers = null;
let inflightUnits = null; let inflightUnits = null;
let inflightWarehouses = null; let inflightWarehouses = null;
@@ -496,6 +513,7 @@ const actions = {
actions.load(), actions.load(),
actions.loadNumbers(), actions.loadNumbers(),
actions.loadTerminals(), actions.loadTerminals(),
actions.loadPrinterAgents(),
actions.loadSuppliers(), actions.loadSuppliers(),
actions.loadUnits(), actions.loadUnits(),
actions.loadWarehouses(), actions.loadWarehouses(),
@@ -512,6 +530,7 @@ const actions = {
inflightCompany = null; inflightCompany = null;
inflightNumbers = null; inflightNumbers = null;
inflightTerminals = null; inflightTerminals = null;
inflightPrinterAgents = null;
inflightSuppliers = null; inflightSuppliers = null;
inflightUnits = null; inflightUnits = null;
inflightWarehouses = null; inflightWarehouses = null;
@@ -698,6 +717,72 @@ const actions = {
}, },
// #endregion // #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) // #region 4. Suppliers (supplier)
// MARK: calls supplier.list from ../api/index.js // MARK: calls supplier.list from ../api/index.js
loadSuppliers() { loadSuppliers() {
@@ -1553,6 +1638,9 @@ sseAutomat.actions.on('carrier', () => {
sseAutomat.actions.on('posterminal', () => { sseAutomat.actions.on('posterminal', () => {
void companyAutomat.actions.loadTerminals().catch(() => {}); void companyAutomat.actions.loadTerminals().catch(() => {});
}); });
sseAutomat.actions.on('printeragent', () => {
void companyAutomat.actions.loadPrinterAgents().catch(() => {});
});
sseAutomat.actions.on('customergroup', () => { sseAutomat.actions.on('customergroup', () => {
void companyAutomat.actions.reloadCustomergroups().catch(() => {}); void companyAutomat.actions.reloadCustomergroups().catch(() => {});
}); });
@@ -1593,6 +1681,11 @@ export const selectPosterminals = () => companyAutomat.state.terminals;
export const selectPosterminalStatus = () => companyAutomat.state.terminalsStatus; export const selectPosterminalStatus = () => companyAutomat.state.terminalsStatus;
export const selectHasPosterminals = () => companyAutomat.state.terminals.length > 0; 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 // Suppliers selectors
export const selectSuppliers = () => companyAutomat.state.suppliers; export const selectSuppliers = () => companyAutomat.state.suppliers;
export const selectSuppliersStatus = () => companyAutomat.state.suppliersStatus; export const selectSuppliersStatus = () => companyAutomat.state.suppliersStatus;

View File

@@ -151,6 +151,9 @@ export {
selectPosterminals, selectPosterminals,
selectPosterminalStatus, selectPosterminalStatus,
selectHasPosterminals, selectHasPosterminals,
selectPrinterAgents,
selectPrinterAgentStatus,
selectHasPrinterAgents,
selectSuppliers, selectSuppliers,
selectSuppliersStatus, selectSuppliersStatus,
selectUnits, selectUnits,

View File

@@ -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 (
<Box sx={{ p: 3 }}>
<BackendError error={error} onRetry={this.handleRetry} />
</Box>
);
}
return (
<Box sx={{ p: 3, maxWidth: 900, mx: 'auto' }}>
<Box sx={{ mb: 2 }}>
<Typography variant="h6" fontWeight={600} gutterBottom>
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.
</Typography>
</Box>
{/* Add new agent row */}
<Box sx={{ display: 'flex', gap: 1.5, mb: 3, alignItems: 'center' }}>
<TextField
size="small"
placeholder="Neuer Drucker-Agent Name (z.B. Bar Receipt Printer)"
value={newAgentName}
onChange={(e) => 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: <LocalPrintshopOutlinedIcon sx={{ mr: 1, color: 'text.secondary' }} />,
},
}}
sx={{ flex: 1 }}
/>
<Button
variant="contained"
size="small"
startIcon={<AddIcon />}
onClick={this.handleCreateAgent}
disabled={creatingAgent || !newAgentName.trim()}
>
Hinzufügen
</Button>
</Box>
<Divider sx={{ mb: 2 }} />
{printeragents.length === 0 ? (
<Box sx={{ textAlign: 'center', py: 6, color: 'text.secondary' }}>
<LocalPrintshopOutlinedIcon sx={{ fontSize: 48, mb: 1, opacity: 0.5 }} />
<Typography variant="body2">Keine Drucker-Agenten angelegt.</Typography>
</Box>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{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 (
<Box
key={agent.id}
sx={{
p: 2,
borderRadius: 2,
border: '1px solid',
borderColor: isPaired ? 'divider' : 'warning.light',
bgcolor: 'background.paper',
display: 'flex',
flexDirection: 'column',
gap: 1.5,
transition: 'border-color 0.2s ease',
}}
>
{/* Header: Icon, Name (or Edit input), Status chip, Actions */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 1,
flexWrap: 'wrap',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1, minWidth: 260 }}>
<LocalPrintshopOutlinedIcon
sx={{ color: isPaired ? 'primary.main' : 'warning.main', fontSize: 24 }}
/>
{isEditing ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1 }}>
<TextField
size="small"
value={editNameDraft}
onChange={(e) => 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 }}
/>
<IconButton
size="small"
color="primary"
onClick={() => void this.handleSaveRename(agent)}
disabled={isPatching}
>
<CheckIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
onClick={this.handleCancelRename}
disabled={isPatching}
>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
) : (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="subtitle1" fontWeight={600}>
{agent.name}
</Typography>
<Tooltip title="Name bearbeiten">
<IconButton
size="small"
onClick={() => this.handleStartRename(agent)}
disabled={isPatching || isDeleting}
sx={{ p: 0.5, color: 'text.secondary' }}
>
<EditOutlinedIcon sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
</Box>
)}
{isPaired ? (
<Chip
icon={<CheckCircleOutlinedIcon />}
label="Gekoppelt"
size="small"
variant="outlined"
color="success"
sx={{ fontWeight: 600, fontSize: '0.75rem' }}
/>
) : (
<Chip
icon={<HourglassEmptyIcon />}
label="Nicht gekoppelt"
size="small"
variant="outlined"
color="warning"
sx={{ fontWeight: 600, fontSize: '0.75rem' }}
/>
)}
</Box>
{/* Delete control */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton
size="small"
aria-label={`Drucker-Agent ${agent.name} löschen`}
onClick={() => void this.handleDeleteAgent(agent)}
disabled={isDeleting || creatingAgent || isPatching}
color="error"
>
<DeleteOutlinedIcon fontSize="small" />
</IconButton>
</Box>
</Box>
{/* Body: Pairing details or PIN info */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 2,
pt: 0.5,
}}
>
{!isPaired ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap' }}>
<Typography variant="body2" color="text.secondary">
Kopplungs-PIN:
</Typography>
<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>
<Typography variant="caption" color="text.secondary">
Geben Sie diesen PIN auf dem Drucker-Agent-Gerät ein.
</Typography>
</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}
</Box>
)}
{/* Sort Order Controls */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, ml: 'auto' }}>
<Typography variant="body2" color="text.secondary">
Reihenfolge:
</Typography>
<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"
disabled={isPatching || isDeleting}
value={this.state.sortDrafts[agent.id] ?? agent.sort ?? 0}
onChange={(e) => 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 }}
/>
</Box>
</Box>
{/* Dates Row */}
<Divider sx={{ my: 0.25 }} />
<Box
sx={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
gap: 2.5,
color: 'text.secondary',
}}
>
{isPaired && pairedDateText ? (
<Typography variant="caption">
Gekoppelt am: <strong>{pairedDateText}</strong>
</Typography>
) : null}
<Typography variant="caption">
Zuletzt gesehen: <strong>{lastseenText}</strong>
</Typography>
<Typography variant="caption">
Aktualisiert: <strong>{updatedText}</strong>
</Typography>
</Box>
</Box>
);
})}
</Box>
)}
</Box>
);
}
}

View File

@@ -18,6 +18,7 @@ import authAutomat from '../../automats/authAutomat';
import CompanySettingsPanel from './CompanySettingsPanel'; import CompanySettingsPanel from './CompanySettingsPanel';
import NumberSettingsPanel from './NumberSettingsPanel'; import NumberSettingsPanel from './NumberSettingsPanel';
import PrinterSettingsPanel from './PrinterSettingsPanel'; import PrinterSettingsPanel from './PrinterSettingsPanel';
import PrinterAgentSettingsPanel from './PrinterAgentSettingsPanel';
import SettingsKeysPanel from './SettingsKeysPanel'; import SettingsKeysPanel from './SettingsKeysPanel';
import ImportExportSettingsPanel from './ImportExportSettingsPanel'; import ImportExportSettingsPanel from './ImportExportSettingsPanel';
import PosTerminalSettingsPanel from './PosTerminalSettingsPanel'; import PosTerminalSettingsPanel from './PosTerminalSettingsPanel';
@@ -27,6 +28,7 @@ import HealthSettingsPanel from './HealthSettingsPanel';
import MonitorHeartOutlinedIcon from '@mui/icons-material/MonitorHeartOutlined'; import MonitorHeartOutlinedIcon from '@mui/icons-material/MonitorHeartOutlined';
import SecurityIcon from '@mui/icons-material/Security'; import SecurityIcon from '@mui/icons-material/Security';
import BackupTableOutlinedIcon from '@mui/icons-material/BackupTableOutlined'; import BackupTableOutlinedIcon from '@mui/icons-material/BackupTableOutlined';
import LocalPrintshopOutlinedIcon from '@mui/icons-material/LocalPrintshopOutlined';
import { SURFACE_BASE } from '../../colors'; import { SURFACE_BASE } from '../../colors';
const TAB_KEYS = { const TAB_KEYS = {
@@ -35,6 +37,7 @@ const TAB_KEYS = {
keys: 'keys', keys: 'keys',
numbers: 'numbers', numbers: 'numbers',
printers: 'printers', printers: 'printers',
printeragent: 'printeragent',
importExport: 'importExport', importExport: 'importExport',
backup: 'backup', backup: 'backup',
posterminal: 'posterminal', posterminal: 'posterminal',
@@ -47,6 +50,7 @@ const TABS = [
{ id: TAB_KEYS.keys, label: 'Einstellungen', icon: <TuneIcon />, Panel: SettingsKeysPanel }, { id: TAB_KEYS.keys, label: 'Einstellungen', icon: <TuneIcon />, Panel: SettingsKeysPanel },
{ id: TAB_KEYS.numbers, label: 'Nummernkreise', icon: <NumbersIcon />, Panel: NumberSettingsPanel }, { id: TAB_KEYS.numbers, label: 'Nummernkreise', icon: <NumbersIcon />, Panel: NumberSettingsPanel },
{ id: TAB_KEYS.printers, label: 'Drucker', icon: <PrintOutlinedIcon />, Panel: PrinterSettingsPanel }, { id: TAB_KEYS.printers, label: 'Drucker', icon: <PrintOutlinedIcon />, Panel: PrinterSettingsPanel },
{ id: TAB_KEYS.printeragent, label: 'Drucker-Agent', icon: <LocalPrintshopOutlinedIcon />, Panel: PrinterAgentSettingsPanel },
{ id: TAB_KEYS.importExport, label: 'Import/Export', icon: <ImportExportIcon />, Panel: ImportExportSettingsPanel }, { id: TAB_KEYS.importExport, label: 'Import/Export', icon: <ImportExportIcon />, Panel: ImportExportSettingsPanel },
{ id: TAB_KEYS.backup, label: 'Backups', icon: <BackupTableOutlinedIcon />, Panel: BackupSettingsPanel }, { id: TAB_KEYS.backup, label: 'Backups', icon: <BackupTableOutlinedIcon />, Panel: BackupSettingsPanel },
{ id: TAB_KEYS.posterminal, label: 'POS Terminal', icon: <PointOfSaleIcon />, Panel: PosTerminalSettingsPanel }, { id: TAB_KEYS.posterminal, label: 'POS Terminal', icon: <PointOfSaleIcon />, Panel: PosTerminalSettingsPanel },