This commit is contained in:
seb
2026-07-31 18:25:42 +02:00
parent 1a05a5b7d6
commit 6074aa7304
33 changed files with 3041 additions and 470 deletions

View File

@@ -0,0 +1,39 @@
# `POST /article`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Create article
Creates an article under an authorized category (`parentId`).
## Body (JSON)
```json
{
"parentId": 10,
"name": "New article"
}
```
## Response `201`
Created
```json
{
"article": { … }
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Validation error — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized for parent category — `{"error":"…"}` |
| 404 | Parent not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -1,121 +1,36 @@
# `GET /article/get`
# `GET /article/{sku}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Full article detail by **exact** match. Use after the user picks a row from [`GET /article/search`](search.md).
Get article by SKU
## Query parameters
Returns article detail by SKU if the user may access its category.
| Param | Required | Description |
|-------|----------|-------------|
| `target` | yes | Exact article number (`cArtNr`), packaging EAN (`tGebinde.cEAN`), or barcode (`cBarcode`). |
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `sku` | yes | string | Article SKU |
## Response `200`
Article detail
```json
{
"article": {
"kArtikel": 49,
"cArtNr": "42000044",
"cName": "BioBizz AllMix Peat Free 20L",
"cAktiv": "Y",
"fVKNEtto": 11.3445378151261,
"kSteuerklasse": 1,
"fBestand": null,
"cHersteller": null,
"cBarcode": null,
"KATs": [{ "kKategorie": 16 }, { "kKategorie": 39 }],
"lief": [],
"staffel": [],
"EANs": [],
"merkmale": [],
"attribute": []
}
"article": { … }
}
```
The object is enriched with related data in parallel. Fields below are always present on success; arrays may be empty.
### Core fields
| Field | Type | Notes |
|-------|------|-------|
| `kArtikel` | number | Internal ID |
| `cArtNr` | string | Article number |
| `cName` | string | Name |
| `cBeschreibung` | string | Long description (HTML) |
| `cKurzBeschreibung` | string | Short description |
| `cAktiv` | string | `"Y"` or `"N"` |
| `fVKNEtto` | number | Net sales price |
| `kSteuerklasse` | number | `1` / `2` / `3` |
| `fUVP` | number | RRP |
| `fBestand` | number \| null | Stock warehouse 1 |
| `fGewicht` | number | Weight |
| `cBarcode` | string \| null | |
| `cHersteller` | string \| null | Manufacturer name |
| `cAnmerkung` | string | Internal note |
| `kVersandklasse` | number | Shipping class ID |
| `tVersandklassecName` | string | Shipping class name |
| `nIstVater` | number | `1` if parent article with variants |
| `kVaterArtikel` | number | Parent article ID (variants) |
| `kStueckliste` | number | Bill-of-materials ID; `0` if none |
| `kEigenschaftKombi` | number | Variant combination |
| `cEigenschaftName` | string \| null | Variant property name |
| `cEigenschaftWertName` | string \| null | Variant value name |
| `cLiefArtNr96` | string \| null | Supplier 96 article number |
| `nVPE` | number | Sales unit flag |
| `kMassEinheit` | number \| null | Unit of measure |
| `fMassMenge` | number | Unit quantity |
| `shops` | number \| null | Shop bitmask sum |
| `garage` | number \| undefined | Extra storage slot 0 qty |
| `keller` | number \| undefined | Extra storage slot 1 qty |
| `cHerstellerGPSR` | string \| null | Manufacturer GPSR text |
| `herstellerBild` | string \| null | Manufacturer logo, base64 PNG |
### Related arrays / objects
| Field | Description |
|-------|-------------|
| `children` | Variant rows (when `nIstVater === 1`) |
| `vater` | Parent `{ cArtNr }` (when variant) |
| `liste` | Stückliste / BOM components |
| `fAndereLager` | Stock in other warehouses `[{ fSumme }]` |
| `EANs` | `[{ cEAN }]` |
| `KATs` | `[{ kKategorie }]` |
| `lief` | Suppliers; `cAnmerkung` may be parsed into `anmerkung` |
| `incoming` | Open supplier purchase orders |
| `orders` | Open incidents (`order_count`, `order_label`, `order_free`) |
| `merkmale` | Features `[{ kMerkmal, cName, cWert, … }]` |
| `attribute` | Custom attributes |
| `staffel` | Tier prices `[{ menge, fVKBrutto }]` — brutto pre-formatted string |
### Supplier row (`lief[]`)
| Field | Notes |
|-------|-------|
| `kLieferant`, `cFirma` | Supplier |
| `cLiefArtNr` | Supplier article number |
| `nStandard` | Preferred supplier flag |
| `fEKNetto`, `fMwSt` | Purchase price / VAT |
| `fLagerbestand` | Supplier stock |
| `cEmail`, `cAnmerkung` | Contact / note |
## Errors
| Status | Body |
|--------|------|
| 400 | `{ "error": "Query parameter \"target\" is required" }` |
| 401 | `{ "error": "Unauthorized" }` |
| 404 | `{ "error": "Article not found" }` |
| 500 | `{ "error": "..." }` |
| Status | Description |
|--------|-------------|
| 400 | Missing sku — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Article not found — `{"error":"…"}` |
## Example
---
```js
// User selected cArtNr from search results
const res = await fetch(
'http://localhost:4208/api/article/get?target=42000044',
{ credentials: 'include' }
);
const { article } = await res.json();
```
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -1,121 +0,0 @@
# `GET /article/search`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Fuzzy article search. Use this to build a search box / result list with category facets.
## Query parameters
| Param | Required | Description |
|-------|----------|-------------|
| `target` | yes | Search string. Split on spaces; each word must match (AND). Each word is matched with `LIKE %word%` against EAN, supplier article number, name, article number, or barcode. |
| `filter` | no | Comma-separated category IDs. Omit or leave empty to search all categories. See [Category filter](#category-filter). |
| `offset` | no | Pagination offset (default `0`). |
| `maxCount` | no | Page size (default `10`). |
## Search behaviour
- Multiple spaces are collapsed to one.
- Example: `target=bizz allmix` → both `bizz` **and** `allmix` must match somewhere on the article.
- By default only **active** articles (`cAktiv = 'Y'`) are returned.
- Without a category filter, at most **2000** rows are returned (before pagination).
- Results are ordered by product name ascending.
- `length` is the total match count (before `offset` / `maxCount`); `recordset` is the current page.
## Category filter
| `filter` value | Effect |
|----------------|--------|
| *(omitted or empty)* | All categories; `TOP 2000` |
| `751` | Include inactive articles (no `cAktiv = 'Y'` restriction). Still uses category branch logic if other IDs are present. |
| `16` or `16,39` | Restrict to articles in those categories (OR). No row limit. |
Pass category IDs as a comma-separated list: `filter=16,39`.
Whenever `filter` is present, **`categories` always includes every id from `filter` and its full ancestor chain** (parent → … → root), even when `length` is `0` or a filtered id has no hits. Those entries use master-data `name` / `parent` and may have `count: 0`. Other facet categories with hits are still returned as usual.
## Response `200`
```json
{
"length": 42,
"recordset": [
{
"categories": "16,39",
"available": 0,
"id": 49,
"stock": null,
"sku": "42000044",
"name": "BioBizz AllMix Peat Free 20L",
"price": 13.5,
"tax": 19
}
],
"categories": {
"16": { "name": "Dünger", "count": 42, "parent": 0 },
"39": { "name": "Erde", "count": 18, "parent": 16 }
}
}
```
Empty result with a nested filter still returns the leaf and its ancestors:
```json
{
"length": 0,
"recordset": [],
"categories": {
"16": { "name": "Dünger", "count": 0, "parent": 0 },
"39": { "name": "Erde", "count": 0, "parent": 16 }
}
}
```
### Top-level fields
| Field | Type | Notes |
|-------|------|-------|
| `length` | number | Total matching articles (unpaginated). |
| `recordset` | array | Current page of shaped article rows. |
| `categories` | object | Map keyed by category id (string). Facet counts for the current search (+ filters). |
### `recordset[]` fields
| Field | Type | Notes |
|-------|------|-------|
| `id` | number | Internal article ID (`kArtikel`) |
| `sku` | string | Article number — pass to [`GET /article/get`](get.md) |
| `name` | string | Display name |
| `stock` | number \| null | Stock in warehouse 1 |
| `available` | number \| null | Available stock |
| `price` | number \| null | Brutto VK, rounded to 1 decimal |
| `tax` | number \| null | Tax rate percent for the article’s Steuerklasse |
| `categories` | string \| null | Comma-separated category IDs on the article |
### `categories` map entries
| Field | Type | Notes |
|-------|------|-------|
| `name` | string \| null | Display label from category master data |
| `count` | number | Hit count under the current search (+ other filters); may be `0` |
| `parent` | number \| null | Parent category id, or `0` if root |
Counts include parent categories rolled up from each article’s category path. Use for category sidebar / facet UI. Resolve labels via `categories[id].name` (skip ids without a name).
## Errors
| Status | Body |
|--------|------|
| 400 | `{ "error": "Query parameter \"target\" is required" }` |
| 401 | `{ "error": "Unauthorized" }` |
| 500 | `{ "error": "..." }` |
## Example
```js
const res = await fetch(
'http://localhost:4208/api/article/search?target=bizz%20allmix&filter=16&maxCount=10',
{ credentials: 'include' }
);
const { categories, length, recordset } = await res.json();
```

View File

@@ -0,0 +1,44 @@
# `PATCH /article/{sku}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Update article by SKU
Updates an article name by SKU; returns the same detail shape as GET.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `sku` | yes | string | Article SKU |
## Body (JSON)
```json
{
"name": "Updated name"
}
```
## Response `200`
Updated article detail
```json
{
"article": { … }
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Validation error — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Article not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._