From 56d376c9bd73f7a26a31292a57d441a6b80c1f0c Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 30 Oct 2026 17:30:10 +0100 Subject: [PATCH] u --- .agents/skills/backend/SKILL.md | 34 +- src/api/index.js | 1 + src/api/printeragent.js | 57 ++ src/automats/companyAutomat.js | 93 +++ src/automats/index.js | 3 + .../settings/PrinterAgentSettingsPanel.jsx | 600 ++++++++++++++++++ src/components/settings/SettingsDialog.jsx | 4 + 7 files changed, 775 insertions(+), 17 deletions(-) create mode 100644 src/api/printeragent.js create mode 100644 src/components/settings/PrinterAgentSettingsPanel.jsx 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 }} + /> + + + + + + {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 },