This commit is contained in:
seb
2026-10-09 19:13:57 +02:00
parent e9190f1ea9
commit 645a9153a0
2 changed files with 169 additions and 38 deletions

View File

@@ -7,7 +7,7 @@ description: >-
# Dash API Routes Overview (All)
Overview of all 192 API endpoints across all modules.
Overview of all 205 API endpoints across all modules.
### Notation
@@ -17,7 +17,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
### Reusable types
- `T1` = `{ article: T2 }`
- `T2` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T32, brand: T28|null, categoryIds: int[], photos: T26[], eans: str[], prices: T36[], suppliers: T12[] }`
- `T2` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T31, brand: T28|null, categoryIds: int[], photos: T26[], eans: str[], prices: T35[], suppliers: T12[] }`
- `T3` = `{ delivery: T4 }`
- `T4` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T20[], files: T9[], externalNumber?: str|null, createdAt?: str, ledger?: T40 }`
- `T5` = `{ labeltemplate: T6 }`
@@ -45,13 +45,13 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- `T27` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }`
- `T28` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }`
- `T29` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str }`
- `T30` = `{ id: int, sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
- `T31` = `{ article: T34 }`
- `T32` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
- `T33` = `{ id: int, iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
- `T34` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }`
- `T35` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }`
- `T36` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }`
- `T30` = `{ article: T33 }`
- `T31` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
- `T32` = `{ id: int, iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
- `T33` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }`
- `T34` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }`
- `T35` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }`
- `T36` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
- `T37` = `{ id: int, entityId: int, contactType: str, contact: str }`
- `T38` = `{ id: int, attributeId: int, value: str, sort: int }`
- `T39` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
@@ -64,9 +64,18 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
### Routes
#### List accounts — `GET /account`
_none_
- Returns: `200 { accounts: { id: int, companyId: int, type: str, name: str }[] }`
#### Create account — `POST /account`
- Params: `body: { type: str, name: str }`
- Returns: `201 { account: { id: int, companyId: int, type: str, name: str } }`
#### Delete account — `DELETE /account/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Create article in category — `POST /article`
- Params: `body: { categoryId: int, name: str, desc?: str, seo?: str, track_stock?: bool }`
- Returns: `201 T31`
- Returns: `201 T30`
#### Export articles as ZIP — `GET /article/export.zip`
- Params: `query: categoryId?: int`
- Returns: `200 zip`
@@ -102,10 +111,10 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- Returns: `200 { children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }`
#### Create and attach a new child article to a parent article — `POST /article/{sku}/children`
- Params: `sku: str, body: { attributeValueId?: int, value?: str, name?: str }`
- Returns: `201 T31`
- Returns: `201 T30`
#### Attach an existing article as a child to a parent article — `PUT /article/{sku}/children`
- Params: `sku: str, body: { sku: str, attributeValueId?: int, value?: str, reparent?: bool }`
- Returns: `200 { article: T34, children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }`
- Returns: `200 { article: T33, children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }`
#### Remove a child article from a parent article — `DELETE /article/{sku}/children/{childSku}`
- Params: `sku: str, childSku: str, query: deleteArticle?: bool`
- Returns: `200 { success: bool, deleted: bool }`
@@ -132,7 +141,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- Returns: `200 { options: { id: int, articleId: int, sku: str, name: str, count: int, sort: int, mandated: bool }[] }`
#### Toggle parent mode for an article — `POST /article/{sku}/parent`
- Params: `sku: str, body: { isParent: bool, attributeNameId?: int }`
- Returns: `200 T31`
- Returns: `200 T30`
#### Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) — `PUT /article/{sku}/prices`
- Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }`
- Returns: `200 T1`
@@ -201,31 +210,34 @@ _none_
- Returns: `200 { attributevalue: T38 }`
#### Login with Google ID token — `POST /auth/login`
- Params: `body: { idToken: str }`
- Returns: `200 { message: str, user: T35 }`
- Returns: `200 { message: str, user: T34 }`
#### Logout — `POST /auth/logout`
_none_
- Returns: `200 { message: str }`
#### Current user — `GET /auth/me`
_none_
- Returns: `200 { user: T35, googleClientId: str }`
- Returns: `200 { user: T34, googleClientId: str }`
#### Set selected company — `PUT /auth/selected-company`
- Params: `body: { companyId: int }`
- Returns: `200 { user: T35 }`
- Returns: `200 { user: T34 }`
#### List brands — `GET /brand`
_none_
- Returns: `200 { brands: { id: int, name: str, pics: T30[], gsprpinfo?: str|null }[] }`
#### Upsert brand — `PUT /brand`
- Params: `query: offset?: int, limit?: int, includeSmallThumbs?: bool`
- Returns: `200 { brands: { id: int, name: str, pics: { fullHash: str, thumbW: int, thumbH: int, thumbHash: str, smallThumbW?: int|null, smallThumbH?: int|null, smallThumbBinary?: str|null }[], gsprpinfo?: str|null }[], total: int, offset: int, limit: int }`
#### Create brand — `POST /brand`
- Params: `body: { name: str, gsprpinfo?: str }`
- Returns: `200 { brand: { id: int, name: str, pics: T30[], gsprpinfo?: str|null } }`
- Returns: `201 { brand: { id: int, name: str, pics: T36[], gsprpinfo?: str|null } }`
#### Delete brand — `DELETE /brand/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Rename brand — `PATCH /brand/{id}`
- Params: `id: int, body: { name?: str, gsprpinfo?: str }`
- Returns: `200 { brand: { id: int, name: str, touchedArticleCount: int, gsprpinfo?: str|null } }`
#### Delete brand image — `DELETE /brand/{id}/image`
- Params: `id: int`
- Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T30[] } }`
- Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T36[] } }`
#### Add or replace brand image — `POST /brand/{id}/image`
- Params: `id: int, image/avif`
- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T30[] } }`
- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T36[] } }`
#### Create category — `POST /category`
- Params: `body: { parentId: int, name: str }`
- Returns: `201 { category: { id: int, parentId: int|null, sort: int, name: str } }`
@@ -270,10 +282,10 @@ _none_
- Returns: `200 { ok: bool }`
#### List companies — `GET /company`
_none_
- Returns: `200 { companies: T33[] }`
- Returns: `200 { companies: T32[] }`
#### Create company — `POST /company`
- Params: `body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
- Returns: `201 { company: T33, user: T35 }`
- Returns: `201 { company: T32, user: T34 }`
#### Get company history — `GET /company/history`
- Params: `query: limit?: int`
- Returns: `200 { history: { id: int, date: str, text: str, companyuserId?: int|null }[], truncated: bool }`
@@ -297,7 +309,7 @@ _none_
- Returns: `204 void`
#### Update company — `PUT /company/{id}`
- Params: `id: int, body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
- Returns: `200 { company: T33 }`
- Returns: `200 { company: T32 }`
#### List customer groups — `GET /customergroup`
_none_
- Returns: `200 { customergroups: { id: int, name: str, sort: int, editable: bool }[] }`
@@ -322,6 +334,15 @@ _none_
#### Parse document (SSE) — `POST /delivery/parse`
- Params: `form: { file: str }`
- Returns: `200 SSE`
- Events:
- `status`: `{ "message": "…" }`
- `llm_call`: `{ "model": "…", "promptSize": 0, "messages": 0 }`
- `tool_call`: `{ "name": "output_manifest" }`
- `usage`: `{ "input": 0, "cached": 0, "output": 0, "cost": 0 }`
- `progress`: `ManifestResult`
- `result`: `ManifestResult`
- `error`: `{ "message": "…" }`
#### Delete delivery — `DELETE /delivery/{id}`
- Params: `id: int`
- Returns: `204 void`
@@ -379,6 +400,22 @@ _none_
#### Subscribe to update signals (SSE) — `GET /events`
_none_
- Returns: `200 SSE`
- Events:
- `category`: `{}`
- `articles`: `{}`
- `article-<sku>`: `{}`
- `stock`: `{}`
- `ledger`: `{}`
- `attributename`: `{}`
- `propertyname`: `{}`
- `labeltemplate`: `{}`
- `printer`: `{}`
- `warehouse`: `{}`
- `vfs`: `{}`
- `modelconversation`: `{}`
- `modelconversation-<id>`: `{}`
- `brand`: `{ sender, brandId }`
#### Database health check — `GET /health`
_none_
- Returns: `200 { ok: bool }`
@@ -443,8 +480,8 @@ _none_
- Params: `id: int, body: { prefix?: str, padding?: int, next?: int }`
- Returns: `200 { number: { id: int, type: str, prefix: str, padding: int, next: int } }`
#### List orders — `GET /order`
_none_
- Returns: `200 { orders: { id: int, number: str, type: str, entityId: int|null, date: str, createdAt?: str }[] }`
- Params: `query: offset?: int, limit?: int`
- Returns: `200 { orders: T11[], total: int, offset: int, limit: int }`
#### Create order — `POST /order`
- Params: `body: { number: str, date: str, lines: T25[], entityId?: int|null }`
- Returns: `201 T7`
@@ -457,6 +494,24 @@ _none_
#### Update order — `PATCH /order/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T25[] }`
- Returns: `200 T7`
#### List payments of an order — `GET /order/{id}/payment`
- Params: `id: int`
- Returns: `200 { payments: { id: int, accountId: int, manifestId: int, type: str, amount: num }[] }`
#### Add payment to an order — `POST /order/{id}/payment`
- Params: `id: int, body: { accountId: int, type: str, amount: num }`
- Returns: `201 { payment: { id: int, accountId: int, manifestId: int, type: str, amount: num } }`
#### List shippings of an order — `GET /order/{id}/shipping`
- Params: `id: int`
- Returns: `200 { shippings: { id: int, manifestId: int, number: str, type: str, state: str }[] }`
#### Create shipping for an order — `POST /order/{id}/shipping`
- Params: `id: int, body: { type: str, state: str }`
- Returns: `201 { shipping: { id: int, manifestId: int, number: str, type: str, state: str } }`
#### Delete package — `DELETE /package/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Delete payment — `DELETE /payment/{id}`
- Params: `id: int`
- Returns: `204 void`
#### List POS terminals — `GET /posterminal`
_none_
- Returns: `200 { posterminals: T10[] }`
@@ -550,6 +605,21 @@ _none_
#### Search images via Serper.dev and stream WebP thumbnails (SSE) — `POST /serper/images`
- Params: `body: { q: str, gl?: str, hl?: str, num?: int, preferSource?: bool }`
- Returns: `200 SSE`
- Events:
- `images`: `{ "images": [ { "id": 0, "url": "…", "width": 1000, "height": 800 } ] }`
- `thumb`: `{ "id": 0, "dataUrl": "data:image/webp;base64,…" }`
- `done`: `{ "total": 100 }`
- `error`: `{ "error": "…" }`
#### Delete shipping — `DELETE /shipping/{id}`
- Params: `id: int`
- Returns: `204 void`
#### List packages of a shipping — `GET /shipping/{id}/package`
- Params: `id: int`
- Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }`
#### Create package for a shipping — `POST /shipping/{id}/package`
- Params: `id: int, body: { entityaddressId: int, type: str, state: str, details?: str }`
- Returns: `201 { package: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null } }`
#### Adjust stock manually — `POST /stock/adjust`
- Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }`
- Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T15[] }`
@@ -579,10 +649,10 @@ _none_
- Returns: `204 void`
#### List units — `GET /unit`
_none_
- Returns: `200 { units: T32[] }`
- Returns: `200 { units: T31[] }`
#### Upsert unit — `PUT /unit`
- Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }`
- Returns: `200 { unit: T32 }`
- Returns: `200 { unit: T31 }`
#### JTL-POS category synchronization — `GET /v1/category`
- Params: `query: lastChangedCategory?: str, limit?: int`
- Returns: `200 {}[]`

View File

@@ -5,12 +5,19 @@
* Upstream Triggers:
* - `authAutomat`: Subscribes to auth state changes — calls `actions.load()` on
* `'authed'` with a `selectedCompanyId`, and `actions.reset()` on `'anon'`.
* - `sseAutomat`: Subscribes to the single `'brand'` event carrying
* `{ sender, brandId }` — any create/rename/delete/picture change triggers
* `actions.reload()`, which silently re-fetches the list. Subscribed views
* (e.g. `ArtikelMarkenPage` via `brandAutomat.subscribe(...)`) then
* re-render from the updated state. This is the reload pattern. Events sent
* by this tab (matching `sender`) are ignored.
* - UI / Settings: Mutating actions (`create`, `rename`, `remove`, `addImage`, `deleteImage`).
*/
import { Automat } from 'react-automat';
import { brand as brandApi } from '../api';
import authAutomat from './authAutomat';
import sseAutomat, { getSseClientId } from './sseAutomat';
/**
* @typedef {Object} BrandState
@@ -34,19 +41,56 @@ function sortBrands(brands) {
let brandAutomat;
/** In-flight `load()`/`reload()` promise so concurrent callers share one request. */
let inflightLoad = null;
async function fetchBrands() {
const res = await brandApi.list();
return sortBrands(Array.isArray(res.brands) ? res.brands : []);
}
const actions = {
/** Load all brands for the selected company. */
async load() {
if (inflightLoad) return inflightLoad;
brandAutomat.setState({ status: 'loading', error: null });
try {
const res = await brandApi.list();
const brands = sortBrands(Array.isArray(res.brands) ? res.brands : []);
brandAutomat.setState({ brands, status: 'succeeded' });
return brands;
} catch (err) {
brandAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
throw err;
}
inflightLoad = (async () => {
try {
const brands = await fetchBrands();
brandAutomat.setState({ brands, status: 'succeeded' });
return brands;
} catch (err) {
brandAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
throw err;
} finally {
inflightLoad = null;
}
})();
return inflightLoad;
},
/**
* Silently re-fetches the brand list while keeping the current state
* visible (no loading flash). Subscribed views re-render from the updated
* state — this is the reload pattern for SSE-driven refreshes.
*
* @returns {Promise<Array<object>>} Fresh brands.
*/
async reload() {
if (inflightLoad) return inflightLoad;
inflightLoad = (async () => {
try {
const brands = await fetchBrands();
brandAutomat.setState({ brands, status: 'succeeded', error: null });
return brands;
} catch (err) {
brandAutomat.setState({ error: err?.message ?? String(err) });
throw err;
} finally {
inflightLoad = null;
}
})();
return inflightLoad;
},
/**
@@ -134,4 +178,21 @@ authAutomat.subscribe((auth) => {
}
});
// SSE: single `brand` event with `{ sender, brandId }` payload for every
// create/rename/delete/picture change. Silently reloads the list; subscribed
// views re-render from state. Own events are skipped.
sseAutomat.actions.on('brand', (event) => {
try {
const payload = JSON.parse(event?.data ?? '{}');
if (payload && typeof payload.sender === 'string' && payload.sender) {
try {
if (payload.sender === getSseClientId()) return;
} catch {}
}
} catch {
// Unparseable payload: fall through to reload.
}
void brandAutomat.actions.reload().catch(() => {});
});
export default brandAutomat;