Compare commits

..

2 Commits

Author SHA1 Message Date
seb
867d550814 u 2026-10-29 03:37:04 +01:00
seb
afb33cde38 u 2026-10-29 03:31:06 +01:00
4 changed files with 299 additions and 76 deletions

View File

@@ -140,16 +140,16 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
### Reusable types ### Reusable types
- `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T18[], externalNumber?: str|null, createdAt?: str, shipping?: T20|null, shippings?: T20[], payment?: T29|null, payments?: T29[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T2[], files?: T9[] }` - `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T18[], externalNumber?: str|null, createdAt?: str, shipping?: T20|null, shippings?: T20[], payment?: T29|null, payments?: T29[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T2[], files?: T10[] }`
- `T2` = `{ id: int, number: str, type: str, date: str, lines: T18[], shippings: T20[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T9[], state?: str }` - `T2` = `{ id: int, number: str, type: str, date: str, lines: T18[], shippings: T20[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T10[], state?: str }`
- `T3` = `{ id: int, number: str, type: str, date: str, lines: T18[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T9[] }` - `T3` = `{ id: int, number: str, type: str, date: str, lines: T18[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T10[] }`
- `T4` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T21[], files: T9[], externalNumber?: str|null, createdAt?: str, ledger?: T36 }` - `T4` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T21[], files: T10[], externalNumber?: str|null, createdAt?: str, ledger?: T36 }`
- `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: T11, totalSizeBytes: int, durationMs: int }` - `T6` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T11, totalSizeBytes: int, durationMs: int }`
- `T7` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T38|null, incoming?: { date: str, amount: num }[] }` - `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, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T22[] }, thumb?: str|null }` - `T8` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T38|null, incoming?: { date: str, amount: num }[] }`
- `T9` = `{ 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 }` - `T9` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T22[] }, thumb?: str|null }`
- `T10` = `{ id: int, name: str, pairingpin?: str|null, token?: str|null, pairedAt?: str|null, clientserial?: str|null, rootCategoryId?: int|null, syncRootArticles?: bool, lastseenAt?: str|null, updatedAt?: str|null, categoryIds?: int[] }` - `T10` = `{ 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` = `{ vfs: T37, storage: T37, database: T31 }` - `T11` = `{ vfs: T37, storage: T37, database: T31 }`
- `T12` = `{ 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 }` - `T12` = `{ 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, company_id: int, prompt: str, max_itr: int, cron: str, created_at: str, desc?: str|null, skills?: str|null, lastagentnote?: str|null, lastrun_at?: str|null }` - `T13` = `{ id: int, company_id: int, prompt: str, max_itr: int, cron: str, created_at: str, desc?: str|null, skills?: str|null, lastagentnote?: str|null, lastrun_at?: str|null }`
@@ -219,7 +219,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: T26, brand: T25|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: T7[], 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: T26, brand: T25|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 }[] } }`
#### 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`
@@ -294,10 +294,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: T7[] }` - Returns: `200 { suppliers: T8[] }`
#### 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: T7 }` - Returns: `201 { supplier: T8 }`
#### 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[] }`
@@ -306,7 +306,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: T7 }` - Returns: `200 { supplier: T8 }`
#### 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`
@@ -555,7 +555,7 @@ _none_
- Returns: `200 str` - Returns: `200 str`
#### Generate and attach delivery note PDF (goods receipt) — `POST /delivery/{id}/pdf` #### Generate and attach delivery note PDF (goods receipt) — `POST /delivery/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 { attachment: T9, fileUrl: str }` - Returns: `201 { attachment: T10, fileUrl: str }`
### Section: /entity ### Section: /entity
@@ -621,6 +621,7 @@ _none_
- `labeltemplate`: `{}` - `labeltemplate`: `{}`
- `printer`: `{}` - `printer`: `{}`
- `warehouse`: `{}` - `warehouse`: `{}`
- `supplier`: `{}`
- `vfs`: `{}` - `vfs`: `{}`
- `modelconversation`: `{}` - `modelconversation`: `{}`
- `modelconversation-<id>`: `{}` - `modelconversation-<id>`: `{}`
@@ -648,19 +649,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: T22[] } }` - Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T22[] } }`
- Returns: `201 { labeltemplate: T8 }` - Returns: `201 { labeltemplate: T9 }`
#### 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: T8 }` - Returns: `200 { labeltemplate: T9 }`
#### 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: T22[] } }` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T22[] } }`
- Returns: `200 { labeltemplate: T8 }` - Returns: `200 { labeltemplate: T9 }`
#### 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: T22[] } }` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T22[] } }`
- Returns: `200 { labeltemplate: T8 }` - Returns: `200 { labeltemplate: T9 }`
### Section: /ledger ### Section: /ledger
@@ -780,7 +781,7 @@ _none_
- Returns: `201 { payment: T29 }` - Returns: `201 { payment: T29 }`
#### Generate and attach order PDF — `POST /order/{id}/pdf` #### Generate and attach order PDF — `POST /order/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 { attachment: T9, fileUrl: str }` - Returns: `201 { attachment: T10, fileUrl: str }`
#### List shipments of an order — `GET /order/{id}/shipment` #### List shipments of an order — `GET /order/{id}/shipment`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { shipments: T2[] }` - Returns: `200 { shipments: T2[] }`
@@ -810,16 +811,16 @@ _none_
#### List POS terminals — `GET /posterminal` #### List POS terminals — `GET /posterminal`
_none_ _none_
- Returns: `200 { posterminals: T10[] }` - Returns: `200 { posterminals: T7[] }`
#### Create POS terminal — `PUT /posterminal` #### Create POS terminal — `PUT /posterminal`
- Params: `body: { name: str, rootCategoryId?: int|null, syncRootArticles?: bool }` - Params: `body: { name: str, rootCategoryId?: int|null, syncRootArticles?: bool, sort?: int, enabled?: bool }`
- Returns: `200 { posterminal: T10 }` - Returns: `200 { posterminal: T7 }`
#### Delete POS terminal — `DELETE /posterminal/{id}` #### Delete POS terminal — `DELETE /posterminal/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update POS terminal — `PATCH /posterminal/{id}` #### Update POS terminal — `PATCH /posterminal/{id}`
- Params: `id: int, body: { name?: str, rootCategoryId?: int|null, syncRootArticles?: bool, pairingpin?: str|null }` - Params: `id: int, body: { name?: str, rootCategoryId?: int|null, syncRootArticles?: bool, sort?: int, enabled?: bool, pairingpin?: str|null }`
- Returns: `200 { posterminal: T10 }` - Returns: `200 { posterminal: T7 }`
#### Disable all categories on terminal — `DELETE /posterminal/{id}/all` #### Disable all categories on terminal — `DELETE /posterminal/{id}/all`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { id: int, categoryIds: int[] }` - Returns: `200 { id: int, categoryIds: int[] }`
@@ -918,7 +919,7 @@ _none_
- Returns: `201 { order: T1 }` - Returns: `201 { order: T1 }`
#### Generate and attach quote PDF — `POST /quote/{id}/pdf` #### Generate and attach quote PDF — `POST /quote/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 { attachment: T9, fileUrl: str }` - Returns: `201 { attachment: T10, fileUrl: str }`
### Section: /retoure ### Section: /retoure
@@ -1005,7 +1006,7 @@ _none_
- Returns: `200 str` - Returns: `200 str`
#### Generate and attach shipment PDF (delivery note) — `POST /shipment/{id}/pdf` #### Generate and attach shipment PDF (delivery note) — `POST /shipment/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 { attachment: T9, fileUrl: str }` - Returns: `201 { attachment: T10, fileUrl: str }`
#### List packages of a shipment — `GET /shipment/{id}/shipping` #### List packages of a shipment — `GET /shipment/{id}/shipping`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { shippings: T20[] }` - Returns: `200 { shippings: T20[] }`
@@ -1112,7 +1113,7 @@ _none_
- Params: `query: path: str, size?: int, w?: int, h?: int, format?: str` - Params: `query: path: str, size?: int, w?: int, h?: int, format?: str`
- Returns: `200 jpeg` - Returns: `200 jpeg`
#### POS discovery and pairing — `GET /v1/client` #### POS discovery and pairing — `GET /v1/client`
- Params: `query: authCode: str, name?: str` - Params: `query: authCode: str, name?: str, clientversion?: str`
- Returns: `200 { authCode: str|null, authToken: str|null, certificateFingerprint: str, certificateSerialNumber: str, mandantId: str, mandantName: str|null, mandantDatabase: str|null, serverFingerprint: str|null, name: str|null, serverTimestamp: str }` - Returns: `200 { authCode: str|null, authToken: str|null, certificateFingerprint: str, certificateSerialNumber: str, mandantId: str, mandantName: str|null, mandantDatabase: str|null, serverFingerprint: str|null, name: str|null, serverTimestamp: str }`
#### JTL-POS customer catalog synchronization — `GET /v1/customer` #### JTL-POS customer catalog synchronization — `GET /v1/customer`
- Params: `query: lastChangedCustomer?: str, limit?: int` - Params: `query: lastChangedCustomer?: str, limit?: int`

View File

@@ -2,8 +2,8 @@ import { http } from './http.js';
export const posterminal = { export const posterminal = {
/** /**
* GET /api/posterminal → { posterminals: [{ id, name, token, pairingpin }] } * GET /api/posterminal → { posterminals: [{ id, name, token, pairingpin, sort, enabled, clientversion, ... }] }
* List all terminals for the selected company. * List all terminals for the selected company, sorted by sort ASC, id ASC.
*/ */
// #region GET /api/posterminal — list // #region GET /api/posterminal — list
list() { list() {
@@ -13,24 +13,29 @@ export const posterminal = {
/** /**
* PUT /api/posterminal * PUT /api/posterminal
* Body: { name: string } * Body: { name: string, sort?: number, enabled?: boolean, rootCategoryId?: number|null, syncRootArticles?: boolean }
* Creates a new terminal (auto-generates 6-digit pin). * Creates a new terminal (auto-generates 6-digit pin).
* → 200 { posterminal: { id, name, token, pairingpin } } * → 200 { posterminal: { id, name, token, pairingpin, sort, enabled, clientversion, ... } }
*/ */
// #region PUT /api/posterminal — create // #region PUT /api/posterminal — create
create({ name }) { create({ name, sort, enabled, rootCategoryId, syncRootArticles } = {}) {
const body = { name };
if (sort !== undefined) body.sort = Number(sort);
if (enabled !== undefined) body.enabled = Boolean(enabled);
if (rootCategoryId !== undefined) body.rootCategoryId = rootCategoryId != null ? Number(rootCategoryId) : null;
if (syncRootArticles !== undefined) body.syncRootArticles = Boolean(syncRootArticles);
return http('/api/posterminal', { return http('/api/posterminal', {
method: 'PUT', method: 'PUT',
body: { name }, body,
}); });
}, },
// #endregion // #endregion
/** /**
* PATCH /api/posterminal/:id * PATCH /api/posterminal/:id
* Body: { name?: string, rootCategoryId?: number|null, syncRootArticles?: boolean, pairingpin?: string } * Body: { name?: string, rootCategoryId?: number|null, syncRootArticles?: boolean, pairingpin?: string, sort?: number, enabled?: boolean }
* Update posterminal configuration. * Update posterminal configuration.
* → 200 { posterminal } * → 200 { posterminal: { id, name, token, pairingpin, sort, enabled, clientversion, ... } }
*/ */
// #region PATCH /api/posterminal/:id — update // #region PATCH /api/posterminal/:id — update
update(terminalId, patch) { update(terminalId, patch) {

View File

@@ -159,6 +159,15 @@ const sortBrands = (brands) =>
const sortGroups = (items) => const sortGroups = (items) =>
[...items].sort((a, b) => (Number(a.sort) || 0) - (Number(b.sort) || 0)); [...items].sort((a, b) => (Number(a.sort) || 0) - (Number(b.sort) || 0));
const sortTerminals = (terminals) => {
if (!Array.isArray(terminals)) return [];
return [...terminals].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'));
@@ -578,7 +587,7 @@ const actions = {
inflightTerminals = (async () => { inflightTerminals = (async () => {
try { try {
const res = await posterminalApi.list(); const res = await posterminalApi.list();
const terminals = Array.isArray(res.posterminals) ? res.posterminals : []; const terminals = sortTerminals(Array.isArray(res.posterminals) ? res.posterminals : []);
companyAutomat.setState({ companyAutomat.setState({
terminals, terminals,
terminalsCompanyId: companyId, terminalsCompanyId: companyId,
@@ -603,11 +612,15 @@ const actions = {
return actions.loadTerminals(); return actions.loadTerminals();
}, },
async createTerminal({ name }) { reloadTerminals() {
const res = await posterminalApi.create({ name }); return actions.loadTerminals();
},
async createTerminal(payload) {
const res = await posterminalApi.create(payload);
const terminal = res.posterminal; const terminal = res.posterminal;
companyAutomat.setState({ companyAutomat.setState({
terminals: [...companyAutomat.state.terminals, terminal], terminals: sortTerminals([...companyAutomat.state.terminals, terminal]),
}); });
return terminal; return terminal;
}, },
@@ -620,8 +633,10 @@ const actions = {
const res = await posterminalApi.update(terminalId, patch); const res = await posterminalApi.update(terminalId, patch);
const updated = res.posterminal; const updated = res.posterminal;
companyAutomat.setState({ companyAutomat.setState({
terminals: companyAutomat.state.terminals.map((t) => terminals: sortTerminals(
t.id === terminalId ? { ...t, ...updated } : t, companyAutomat.state.terminals.map((t) =>
t.id === terminalId ? { ...t, ...updated } : t,
),
), ),
}); });
return updated; return updated;

View File

@@ -11,6 +11,8 @@ import TextField from '@mui/material/TextField';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import FolderIcon from '@mui/icons-material/Folder'; import FolderIcon from '@mui/icons-material/Folder';
import PointOfSaleIcon from '@mui/icons-material/PointOfSale'; import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
@@ -56,6 +58,7 @@ export default class PosTerminalSettingsPanel extends Component {
deletingId: null, deletingId: null,
pickerTerminal: null, pickerTerminal: null,
patchingTerminalId: null, patchingTerminalId: null,
sortDrafts: {},
}; };
this.handleRetry = this.handleRetry.bind(this); this.handleRetry = this.handleRetry.bind(this);
this.loadTerminals = this.loadTerminals.bind(this); this.loadTerminals = this.loadTerminals.bind(this);
@@ -63,6 +66,10 @@ export default class PosTerminalSettingsPanel extends Component {
this.handleDeleteTerminal = this.handleDeleteTerminal.bind(this); this.handleDeleteTerminal = this.handleDeleteTerminal.bind(this);
this.handleSetRootCategory = this.handleSetRootCategory.bind(this); this.handleSetRootCategory = this.handleSetRootCategory.bind(this);
this.handleToggleSyncRootArticles = this.handleToggleSyncRootArticles.bind(this); this.handleToggleSyncRootArticles = this.handleToggleSyncRootArticles.bind(this);
this.handleToggleEnabled = this.handleToggleEnabled.bind(this);
this.handleSortDraftChange = this.handleSortDraftChange.bind(this);
this.handleSortSubmit = this.handleSortSubmit.bind(this);
this.handleMoveTerminal = this.handleMoveTerminal.bind(this);
this.getCategoryLabel = this.getCategoryLabel.bind(this); this.getCategoryLabel = this.getCategoryLabel.bind(this);
} }
@@ -104,13 +111,20 @@ export default class PosTerminalSettingsPanel extends Component {
} }
async handleCreateTerminal() { async handleCreateTerminal() {
const { newTerminalName, enableAllForNew } = this.state; const { newTerminalName, enableAllForNew, terminals } = this.state;
const trimmed = newTerminalName.trim(); const trimmed = newTerminalName.trim();
if (!trimmed) return; if (!trimmed) return;
this.setState({ creatingTerminal: true }); this.setState({ creatingTerminal: true });
try { try {
const created = await companyAutomat.actions.createTerminal({ name: trimmed }); const nextSort = terminals.length > 0
? Math.max(...terminals.map((t) => Number(t.sort) || 0)) + 1
: 0;
const created = await companyAutomat.actions.createTerminal({
name: trimmed,
sort: nextSort,
enabled: true,
});
if (enableAllForNew && created?.id != null) { if (enableAllForNew && created?.id != null) {
await companyAutomat.actions.enableAllTerminalCategories({ terminalId: created.id }); await companyAutomat.actions.enableAllTerminalCategories({ terminalId: created.id });
@@ -126,6 +140,84 @@ export default class PosTerminalSettingsPanel extends Component {
} }
} }
async handleToggleEnabled(terminal, nextValue) {
this.setState({ patchingTerminalId: terminal.id });
try {
await companyAutomat.actions.updateTerminal(terminal.id, {
enabled: Boolean(nextValue),
});
enqueueSnackbar(nextValue ? 'Terminal aktiviert.' : 'Terminal deaktiviert.', {
variant: 'success',
});
} catch (err) {
notifyError(err);
} finally {
this.setState({ patchingTerminalId: null });
}
}
handleSortDraftChange(terminalId, value) {
this.setState((prev) => ({
sortDrafts: { ...prev.sortDrafts, [terminalId]: value },
}));
}
async handleSortSubmit(terminal) {
const draft = this.state.sortDrafts[terminal.id];
if (draft === undefined) return;
const nextSort = Number(draft);
if (isNaN(nextSort) || nextSort === (terminal.sort ?? 0)) {
this.setState((prev) => {
const next = { ...prev.sortDrafts };
delete next[terminal.id];
return { sortDrafts: next };
});
return;
}
this.setState({ patchingTerminalId: terminal.id });
try {
await companyAutomat.actions.updateTerminal(terminal.id, { sort: nextSort });
enqueueSnackbar('Reihenfolge aktualisiert.', { variant: 'success' });
} catch (err) {
notifyError(err);
} finally {
this.setState((prev) => {
const next = { ...prev.sortDrafts };
delete next[terminal.id];
return { sortDrafts: next, patchingTerminalId: null };
});
}
}
async handleMoveTerminal(terminal, direction) {
const { terminals } = this.state;
const currentIndex = terminals.findIndex((t) => t.id === terminal.id);
if (currentIndex === -1) return;
const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
if (targetIndex < 0 || targetIndex >= terminals.length) return;
const other = terminals[targetIndex];
const currentSort = terminal.sort ?? 0;
const otherSort = other.sort ?? 0;
let newSort;
if (direction === 'up') {
newSort = otherSort <= currentSort ? otherSort - 1 : otherSort;
} else {
newSort = otherSort >= currentSort ? otherSort + 1 : otherSort;
}
this.setState({ patchingTerminalId: terminal.id });
try {
await companyAutomat.actions.updateTerminal(terminal.id, { sort: newSort });
enqueueSnackbar('Reihenfolge aktualisiert.', { variant: 'success' });
} catch (err) {
notifyError(err);
} finally {
this.setState({ patchingTerminalId: null });
}
}
async handleDeleteTerminal(terminal) { async handleDeleteTerminal(terminal) {
this.setState({ deletingId: terminal.id }); this.setState({ deletingId: terminal.id });
try { try {
@@ -261,9 +353,10 @@ export default class PosTerminalSettingsPanel extends Component {
</Box> </Box>
) : ( ) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{terminals.map((terminal) => { {terminals.map((terminal, index) => {
const isDeleting = deletingId === terminal.id; const isDeleting = deletingId === terminal.id;
const isPatching = patchingTerminalId === terminal.id; const isPatching = patchingTerminalId === terminal.id;
const isEnabled = terminal.enabled !== false && terminal.enabled !== 0 && terminal.enabled != null;
const pairedDateText = formatDateTime(terminal.pairedAt); const pairedDateText = formatDateTime(terminal.pairedAt);
const lastseenText = formatDateTime(terminal.lastseenAt) ?? 'Nie'; const lastseenText = formatDateTime(terminal.lastseenAt) ?? 'Nie';
const updatedText = formatDateTime(terminal.updatedAt) ?? '—'; const updatedText = formatDateTime(terminal.updatedAt) ?? '—';
@@ -275,18 +368,24 @@ export default class PosTerminalSettingsPanel extends Component {
p: 2, p: 2,
borderRadius: 2, borderRadius: 2,
border: '1px solid', border: '1px solid',
borderColor: 'divider', borderColor: isEnabled ? 'divider' : 'action.disabledBackground',
bgcolor: 'background.paper', bgcolor: 'background.paper',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: 1.5, gap: 1.5,
opacity: isEnabled ? 1 : 0.72,
transition: 'opacity 0.2s ease, border-color 0.2s ease',
}} }}
> >
{/* Header: Icon, Name, PIN, Delete action */} {/* Header: Icon, Name, PIN, Version, Status Switch, Delete action */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap' }}>
<PointOfSaleIcon sx={{ color: 'primary.main', fontSize: 24 }} /> <PointOfSaleIcon sx={{ color: isEnabled ? 'primary.main' : 'text.disabled', fontSize: 24 }} />
<Typography variant="subtitle1" fontWeight={600}> <Typography
variant="subtitle1"
fontWeight={600}
sx={{ color: isEnabled ? 'text.primary' : 'text.secondary' }}
>
{terminal.name} {terminal.name}
</Typography> </Typography>
{terminal.pairingpin ? ( {terminal.pairingpin ? (
@@ -310,23 +409,68 @@ export default class PosTerminalSettingsPanel extends Component {
({terminal.clientserial}) ({terminal.clientserial})
</Typography> </Typography>
) : null} ) : null}
{terminal.clientversion ? (
<Tooltip title="POS Client Software-Version">
<Chip
label={`v${terminal.clientversion}`}
size="small"
variant="outlined"
color="info"
sx={{ fontFamily: 'monospace', fontWeight: 600, fontSize: '0.75rem' }}
/>
</Tooltip>
) : null}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Tooltip
title={
isEnabled
? 'Terminal aktiv (kann synchronisieren)'
: 'Terminal deaktiviert (POS-Zugriff blockiert)'
}
>
<FormControlLabel
control={
<Switch
size="small"
checked={isEnabled}
disabled={isPatching || isDeleting}
onChange={(e) => void this.handleToggleEnabled(terminal, e.target.checked)}
color="success"
/>
}
label={
<Typography
variant="body2"
sx={{
fontSize: '0.8125rem',
fontWeight: 500,
color: isEnabled ? 'text.primary' : 'text.disabled',
}}
>
{isEnabled ? 'Aktiv' : 'Inaktiv'}
</Typography>
}
sx={{ m: 0 }}
/>
</Tooltip>
<IconButton
size="small"
aria-label={`Terminal ${terminal.name} löschen`}
onClick={() => void this.handleDeleteTerminal(terminal)}
disabled={isDeleting || creatingTerminal || isPatching}
color="error"
>
{isDeleting ? (
<CircularProgress size={16} color="inherit" />
) : (
<DeleteOutlinedIcon fontSize="small" />
)}
</IconButton>
</Box> </Box>
<IconButton
size="small"
aria-label={`Terminal ${terminal.name} löschen`}
onClick={() => void this.handleDeleteTerminal(terminal)}
disabled={isDeleting || creatingTerminal || isPatching}
color="error"
>
{isDeleting ? (
<CircularProgress size={16} color="inherit" />
) : (
<DeleteOutlinedIcon fontSize="small" />
)}
</IconButton>
</Box> </Box>
{/* Settings row: Root category picker and syncRootArticles toggle */} {/* Settings row: Root category picker, sort order controls, and syncRootArticles toggle */}
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -365,22 +509,75 @@ export default class PosTerminalSettingsPanel extends Component {
</Button> </Button>
</Box> </Box>
<FormControlLabel <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
control={ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Switch <Typography variant="body2" color="text.secondary">
size="small" Reihenfolge:
checked={Boolean(terminal.syncRootArticles)}
disabled={isPatching || isDeleting}
onChange={(e) => void this.handleToggleSyncRootArticles(terminal, e.target.checked)}
/>
}
label={
<Typography variant="body2">
Root-Artikel synchronisieren
</Typography> </Typography>
} <Tooltip title="Nach oben verschieben">
sx={{ m: 0 }} <span>
/> <IconButton
size="small"
disabled={index === 0 || isPatching || isDeleting}
onClick={() => void this.handleMoveTerminal(terminal, 'up')}
sx={{ p: 0.25 }}
>
<ArrowUpwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Nach unten verschieben">
<span>
<IconButton
size="small"
disabled={index === terminals.length - 1 || isPatching || isDeleting}
onClick={() => void this.handleMoveTerminal(terminal, 'down')}
sx={{ p: 0.25 }}
>
<ArrowDownwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
<TextField
type="number"
size="small"
disabled={isPatching || isDeleting}
value={this.state.sortDrafts[terminal.id] ?? terminal.sort ?? 0}
onChange={(e) => this.handleSortDraftChange(terminal.id, e.target.value)}
onBlur={() => void this.handleSortSubmit(terminal)}
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>
<FormControlLabel
control={
<Switch
size="small"
checked={Boolean(terminal.syncRootArticles)}
disabled={isPatching || isDeleting}
onChange={(e) => void this.handleToggleSyncRootArticles(terminal, e.target.checked)}
/>
}
label={
<Typography variant="body2">
Root-Artikel synchronisieren
</Typography>
}
sx={{ m: 0 }}
/>
</Box>
</Box> </Box>
{/* Dates row */} {/* Dates row */}
@@ -402,6 +599,11 @@ export default class PosTerminalSettingsPanel extends Component {
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
<strong>Zuletzt online:</strong> {lastseenText} <strong>Zuletzt online:</strong> {lastseenText}
</Typography> </Typography>
{terminal.clientversion && (
<Typography variant="caption" color="text.secondary">
<strong>POS-Software:</strong> {terminal.clientversion}
</Typography>
)}
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
<strong>Aktualisiert:</strong> {updatedText} <strong>Aktualisiert:</strong> {updatedText}
</Typography> </Typography>