u
This commit is contained in:
@@ -26,18 +26,28 @@ Sessions use an HTTP-only `token` cookie (`Secure`, `SameSite=Strict`, 24h). The
|
||||
|
||||
## Routes
|
||||
|
||||
Mirrors [`src/api/routes/`](../../src/api/routes/).
|
||||
Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/routes/). Source of truth is the code; regenerate with `npm run docs:api`.
|
||||
|
||||
| File | Endpoint |
|
||||
|------|----------|
|
||||
| [`routes/article/create.md`](routes/article/create.md) | `POST /article` |
|
||||
| [`routes/article/get.md`](routes/article/get.md) | `GET /article/{sku}` |
|
||||
| [`routes/article/update.md`](routes/article/update.md) | `PATCH /article/{sku}` |
|
||||
| [`routes/auth/login.md`](routes/auth/login.md) | `POST /auth/login` |
|
||||
| [`routes/auth/logout.md`](routes/auth/logout.md) | `POST /auth/logout` |
|
||||
| [`routes/auth/me.md`](routes/auth/me.md) | `GET /auth/me` |
|
||||
| [`routes/auth/selected-company.md`](routes/auth/selected-company.md) | `PUT /auth/selected-company` |
|
||||
| [`routes/category/create.md`](routes/category/create.md) | `POST /category` |
|
||||
| [`routes/category/tree.md`](routes/category/tree.md) | `GET /category/tree` |
|
||||
| [`routes/category/delete.md`](routes/category/delete.md) | `DELETE /category/{id}` |
|
||||
| [`routes/category/articles.md`](routes/category/articles.md) | `GET /category/{id}/articles` |
|
||||
| [`routes/company/list.md`](routes/company/list.md) | `GET /company` |
|
||||
| [`routes/company/create.md`](routes/company/create.md) | `POST /company` |
|
||||
| [`routes/company/delete.md`](routes/company/delete.md) | `DELETE /company/{id}` |
|
||||
| [`routes/company/update.md`](routes/company/update.md) | `PUT /company/{id}` |
|
||||
| [`routes/health.md`](routes/health.md) | `GET /health` |
|
||||
| [`routes/article/search.md`](routes/article/search.md) | `GET /article/search` |
|
||||
| [`routes/article/get.md`](routes/article/get.md) | `GET /article/get` |
|
||||
| [`routes/misc/getSteuer.md`](routes/misc/getSteuer.md) | `GET /misc/getSteuer` |
|
||||
| [`routes/misc/getCategories.md`](routes/misc/getCategories.md) | `GET /misc/getCategories` |
|
||||
|
||||
Machine-readable spec: [`openapi.json`](openapi.json).
|
||||
|
||||
## DESIGN
|
||||
|
||||
@@ -45,4 +55,5 @@ Mirrors [`src/api/routes/`](../../src/api/routes/).
|
||||
- that should loosly bound to services in /src/services
|
||||
- for every service we have a CLI tester in /scripts/
|
||||
- all DB SQL code shall be in /src/db
|
||||
- with mssql staements in /src/db/mssql/
|
||||
- with mssql staements in /src/db/mssql/
|
||||
- API docs: `@openapi` JSDoc on handlers → `npm run docs:api` → `docs/api/`
|
||||
|
||||
1384
docs/api/openapi.json
Normal file
1384
docs/api/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
39
docs/api/routes/article/create.md
Normal file
39
docs/api/routes/article/create.md
Normal 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`._
|
||||
@@ -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`._
|
||||
|
||||
@@ -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();
|
||||
```
|
||||
44
docs/api/routes/article/update.md
Normal file
44
docs/api/routes/article/update.md
Normal 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`._
|
||||
@@ -2,25 +2,38 @@
|
||||
|
||||
Public. No cookie required.
|
||||
|
||||
**Body (JSON)**
|
||||
Login with Google ID token
|
||||
|
||||
Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true`), and sets an HTTP-only `token` cookie.
|
||||
|
||||
## Body (JSON)
|
||||
|
||||
```json
|
||||
{ "idToken": "<google-id-token>" }
|
||||
{
|
||||
"idToken": "<google-id-token>"
|
||||
}
|
||||
```
|
||||
|
||||
**200**
|
||||
## Response `200`
|
||||
|
||||
Login successful; `token` cookie set
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Login successful",
|
||||
"user": { "id": 1, "email": "you@example.com", "picture": "https://lh3.googleusercontent.com/..." }
|
||||
"user": { … }
|
||||
}
|
||||
```
|
||||
|
||||
**Errors**
|
||||
## Errors
|
||||
|
||||
| Status | Body |
|
||||
|--------|------|
|
||||
| 400 | `{ "error": "idToken is required" }` or `{ "error": "GOOGLE_CLIENT_ID is not configured" }` |
|
||||
| 401 | `{ "error": "Invalid Google token" }` or `{ "error": "Invalid Google account" }` |
|
||||
| 403 | `{ "error": "Email is not authorized" }` |
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 400 | Missing token or Google client not configured — `{"error":"…"}` |
|
||||
| 401 | Invalid Google token or account — `{"error":"…"}` |
|
||||
| 403 | Email not on allowlist — `{"error":"…"}` |
|
||||
| 500 | Server error — `{"error":"…"}` |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
# `POST /auth/logout`
|
||||
|
||||
Public. Clears the session cookie.
|
||||
Public. No cookie required.
|
||||
|
||||
**200**
|
||||
Logout
|
||||
|
||||
Clears the HTTP-only `token` cookie set at login.
|
||||
|
||||
## Response `200`
|
||||
|
||||
Logged out
|
||||
|
||||
```json
|
||||
{ "message": "Logged out successfully" }
|
||||
{
|
||||
"message": "Logged out successfully"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
# `GET /auth/me`
|
||||
|
||||
Protected. Requires a valid session cookie. Every response includes `googleClientId`.
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
**200**
|
||||
Current user
|
||||
|
||||
Returns the authenticated user from the session cookie and the Google client ID. Unauthenticated calls still receive `googleClientId` with 401.
|
||||
|
||||
## Response `200`
|
||||
|
||||
Authenticated
|
||||
|
||||
```json
|
||||
{
|
||||
"user": { "id": 1, "email": "you@example.com", "picture": "https://lh3.googleusercontent.com/..." },
|
||||
"googleClientId": "your-client-id.apps.googleusercontent.com"
|
||||
"user": { … },
|
||||
"googleClientId": "…"
|
||||
}
|
||||
```
|
||||
|
||||
**Errors**
|
||||
## Errors
|
||||
|
||||
| Status | Body |
|
||||
|--------|------|
|
||||
| 401 | `{ "error": "Unauthorized", "googleClientId": "..." }` or `{ "error": "Invalid token", "googleClientId": "..." }` |
|
||||
| 403 | `{ "error": "User is no longer authorized", "googleClientId": "..." }` |
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 401 | Unauthorized — still includes `googleClientId` for the frontend |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
|
||||
37
docs/api/routes/auth/selected-company.md
Normal file
37
docs/api/routes/auth/selected-company.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# `PUT /auth/selected-company`
|
||||
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
Set selected company
|
||||
|
||||
Sets the authenticated user's selected company.
|
||||
|
||||
## Body (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"companyId": 1
|
||||
}
|
||||
```
|
||||
|
||||
## Response `200`
|
||||
|
||||
Updated user
|
||||
|
||||
```json
|
||||
{
|
||||
"user": { … }
|
||||
}
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 400 | Invalid companyId — `{"error":"…"}` |
|
||||
| 401 | Unauthorized — `{"error":"…"}` |
|
||||
| 500 | Server error — `{"error":"…"}` |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
35
docs/api/routes/category/articles.md
Normal file
35
docs/api/routes/category/articles.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# `GET /category/{id}/articles`
|
||||
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
Articles in category
|
||||
|
||||
Returns articles in a category the user is authorized for.
|
||||
|
||||
## Path parameters
|
||||
|
||||
| Param | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| `id` | yes | integer | Category id |
|
||||
|
||||
## Response `200`
|
||||
|
||||
Articles list
|
||||
|
||||
```json
|
||||
{
|
||||
"articles": []
|
||||
}
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 400 | Invalid id — `{"error":"…"}` |
|
||||
| 401 | Unauthorized — `{"error":"…"}` |
|
||||
| 403 | Not authorized — `{"error":"…"}` |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
39
docs/api/routes/category/create.md
Normal file
39
docs/api/routes/category/create.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# `POST /category`
|
||||
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
Create category
|
||||
|
||||
Creates a category under an authorized parent.
|
||||
|
||||
## Body (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"parentId": 1,
|
||||
"name": "New category"
|
||||
}
|
||||
```
|
||||
|
||||
## Response `201`
|
||||
|
||||
Created
|
||||
|
||||
```json
|
||||
{
|
||||
"category": { … }
|
||||
}
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 400 | Validation error — `{"error":"…"}` |
|
||||
| 401 | Unauthorized — `{"error":"…"}` |
|
||||
| 403 | Not authorized — `{"error":"…"}` |
|
||||
| 404 | Parent not found — `{"error":"…"}` |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
31
docs/api/routes/category/delete.md
Normal file
31
docs/api/routes/category/delete.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# `DELETE /category/{id}`
|
||||
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
Delete category
|
||||
|
||||
Deletes a category the user is authorized for (not the company root).
|
||||
|
||||
## Path parameters
|
||||
|
||||
| Param | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| `id` | yes | integer | Category id |
|
||||
|
||||
## Response `204`
|
||||
|
||||
Deleted
|
||||
|
||||
_No content_
|
||||
|
||||
## Errors
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 400 | Invalid id or cannot delete root — `{"error":"…"}` |
|
||||
| 401 | Unauthorized — `{"error":"…"}` |
|
||||
| 403 | Not authorized — `{"error":"…"}` |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
32
docs/api/routes/category/tree.md
Normal file
32
docs/api/routes/category/tree.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# `GET /category/tree`
|
||||
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
Category tree
|
||||
|
||||
Returns the category tree for the authenticated user's selected company.
|
||||
|
||||
## Response `200`
|
||||
|
||||
Tree and articles for the selected company
|
||||
|
||||
```json
|
||||
{
|
||||
"languageId": 1,
|
||||
"tree": [],
|
||||
"articles": []
|
||||
}
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 400 | No company selected — `{"error":"…"}` |
|
||||
| 401 | Unauthorized — `{"error":"…"}` |
|
||||
| 403 | Not authorized — `{"error":"…"}` |
|
||||
| 404 | Company not found — `{"error":"…"}` |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
41
docs/api/routes/company/create.md
Normal file
41
docs/api/routes/company/create.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# `POST /company`
|
||||
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
Create company
|
||||
|
||||
Creates a company and selects it for the current user.
|
||||
|
||||
## Body (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"iso639": "de",
|
||||
"name": "Acme GmbH",
|
||||
"taxrate": 19,
|
||||
"reducedtaxrate": 7
|
||||
}
|
||||
```
|
||||
|
||||
## Response `201`
|
||||
|
||||
Created; selected company updated
|
||||
|
||||
```json
|
||||
{
|
||||
"company": { … },
|
||||
"user": { … }
|
||||
}
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 400 | Validation error — `{"error":"…"}` |
|
||||
| 401 | Unauthorized — `{"error":"…"}` |
|
||||
| 409 | Company name not available — `{"error":"…"}` |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
31
docs/api/routes/company/delete.md
Normal file
31
docs/api/routes/company/delete.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# `DELETE /company/{id}`
|
||||
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
Delete company
|
||||
|
||||
Deletes a company.
|
||||
|
||||
## Path parameters
|
||||
|
||||
| Param | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| `id` | yes | integer | |
|
||||
|
||||
## Response `204`
|
||||
|
||||
Deleted
|
||||
|
||||
_No content_
|
||||
|
||||
## Errors
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 400 | Invalid id — `{"error":"…"}` |
|
||||
| 401 | Unauthorized — `{"error":"…"}` |
|
||||
| 500 | Server error — `{"error":"…"}` |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
28
docs/api/routes/company/list.md
Normal file
28
docs/api/routes/company/list.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# `GET /company`
|
||||
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
List companies
|
||||
|
||||
Lists companies for the authenticated user.
|
||||
|
||||
## Response `200`
|
||||
|
||||
Company list
|
||||
|
||||
```json
|
||||
{
|
||||
"companies": []
|
||||
}
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 401 | Unauthorized — `{"error":"…"}` |
|
||||
| 500 | Server error — `{"error":"…"}` |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
47
docs/api/routes/company/update.md
Normal file
47
docs/api/routes/company/update.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# `PUT /company/{id}`
|
||||
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
Update company
|
||||
|
||||
Updates a company.
|
||||
|
||||
## Path parameters
|
||||
|
||||
| Param | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| `id` | yes | integer | |
|
||||
|
||||
## Body (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"iso639": "…",
|
||||
"name": "…",
|
||||
"taxrate": 1,
|
||||
"reducedtaxrate": 1
|
||||
}
|
||||
```
|
||||
|
||||
## Response `200`
|
||||
|
||||
Updated
|
||||
|
||||
```json
|
||||
{
|
||||
"company": { … }
|
||||
}
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 400 | Validation error — `{"error":"…"}` |
|
||||
| 401 | Unauthorized — `{"error":"…"}` |
|
||||
| 404 | Company not found — `{"error":"…"}` |
|
||||
| 409 | Company name not available — `{"error":"…"}` |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
@@ -2,14 +2,26 @@
|
||||
|
||||
Public. No cookie required.
|
||||
|
||||
**200**
|
||||
Database health check
|
||||
|
||||
Verifies the database client can establish a connection.
|
||||
|
||||
## Response `200`
|
||||
|
||||
Database reachable
|
||||
|
||||
```json
|
||||
{ "ok": true }
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
**Errors**
|
||||
## Errors
|
||||
|
||||
| Status | Body |
|
||||
|--------|------|
|
||||
| 503 | `{ "ok": false, "error": "..." }` |
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 503 | Database unreachable |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
# `GET /misc/getCategories`
|
||||
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
Returns the full JTL category list and a nested tree. Same data source as the legacy `getKat` socket handler in dashApp.
|
||||
|
||||
## Response `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"kKategorie": 16,
|
||||
"kOberKategorie": 0,
|
||||
"nSort": 10,
|
||||
"cName": "Dünger",
|
||||
"count": 142
|
||||
}
|
||||
],
|
||||
"tree": [
|
||||
{
|
||||
"kKategorie": 16,
|
||||
"kOberKategorie": 0,
|
||||
"nSort": 10,
|
||||
"cName": "Dünger",
|
||||
"count": 142,
|
||||
"children": [
|
||||
{
|
||||
"kKategorie": 39,
|
||||
"kOberKategorie": 16,
|
||||
"nSort": 0,
|
||||
"cName": "Bio",
|
||||
"count": 47,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `categories[]`
|
||||
|
||||
Flat list — useful for lookups by ID (`katNames`-style maps).
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `kKategorie` | number | Category ID |
|
||||
| `kOberKategorie` | number | Parent category ID (`0` = root) |
|
||||
| `nSort` | number | Sort order among siblings |
|
||||
| `cName` | string \| null | German display name (Sprache 1, Plattform 1, Shop 0) |
|
||||
| `count` | number | Active articles directly in this category |
|
||||
|
||||
### `tree[]`
|
||||
|
||||
Nested hierarchy rooted at `kOberKategorie === 0` (or orphaned nodes). Each node has the same fields plus `children[]`, sorted by `nSort` then `cName`.
|
||||
|
||||
Use `tree` for sidebar navigation; use `categories` for ID → name maps or [`GET /article/search`](../article/search.md) `filter` param.
|
||||
|
||||
## Errors
|
||||
|
||||
| Status | Body |
|
||||
|--------|------|
|
||||
| 401 | `{ "error": "Unauthorized" }` |
|
||||
| 500 | `{ "error": "..." }` |
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
const res = await fetch('http://localhost:4208/api/misc/getCategories', {
|
||||
credentials: 'include',
|
||||
});
|
||||
const { categories, tree } = await res.json();
|
||||
|
||||
const katNames = Object.fromEntries(
|
||||
categories.map((kat) => [kat.kKategorie, kat.cName])
|
||||
);
|
||||
```
|
||||
@@ -1,63 +0,0 @@
|
||||
# `GET /misc/getSteuer`
|
||||
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
Returns tax classes and their rates from JTL-Wawi (`tSteuerklasse` + `tSteuersatz`, domestic zone `kSteuerzone = 1`).
|
||||
|
||||
## Response `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"steuerklassen": [
|
||||
{
|
||||
"kSteuerklasse": 1,
|
||||
"cName": "Normaler Steuersatz",
|
||||
"nStandard": 1,
|
||||
"fSteuersatz": 19
|
||||
},
|
||||
{
|
||||
"kSteuerklasse": 2,
|
||||
"cName": "Ermäßigter Steuersatz",
|
||||
"nStandard": 0,
|
||||
"fSteuersatz": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `kSteuerklasse` | Tax class ID (used on articles as `kSteuerklasse`) |
|
||||
| `cName` | Display name |
|
||||
| `nStandard` | `1` if this is the default tax class |
|
||||
| `fSteuersatz` | VAT rate in percent |
|
||||
|
||||
### Brutto calculation
|
||||
|
||||
Look up the article's `kSteuerklasse` in `steuerklassen`, then:
|
||||
|
||||
```js
|
||||
function getBrutto(netto, kSteuerklasse, steuerklassen) {
|
||||
const row = steuerklassen.find((s) => s.kSteuerklasse === kSteuerklasse);
|
||||
if (!row) return null;
|
||||
return netto * (1 + row.fSteuersatz / 100);
|
||||
}
|
||||
```
|
||||
|
||||
Load once on app init and reuse for search results and article detail.
|
||||
|
||||
## Errors
|
||||
|
||||
| Status | Body |
|
||||
|--------|------|
|
||||
| 401 | `{ "error": "Unauthorized" }` |
|
||||
| 500 | `{ "error": "..." }` |
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
const res = await fetch('http://localhost:4208/api/misc/getSteuer', {
|
||||
credentials: 'include',
|
||||
});
|
||||
const { steuerklassen } = await res.json();
|
||||
```
|
||||
Reference in New Issue
Block a user