Compare commits

..

2 Commits

Author SHA1 Message Date
seb
20d83b00fc u 2026-10-17 05:11:07 +02:00
seb
54066fba7f u 2026-10-17 05:00:17 +02:00
12 changed files with 943 additions and 89 deletions

View File

@@ -7,7 +7,7 @@ description: >-
# Dash API Routes Overview (All) # Dash API Routes Overview (All)
Overview of all 255 API endpoints across all modules. Overview of all 261 API endpoints across all modules.
### Notation ### Notation
@@ -16,46 +16,47 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
### Reusable types ### Reusable types
- `T1` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T19[], externalNumber?: str|null, createdAt?: str, shipping?: T25|null, shippings?: T25[], payment?: T30|null, payments?: T30[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T3[], files?: T9[] }` - `T1` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, shipping?: T19|null, shippings?: T19[], payment?: T30|null, payments?: T30[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T3[], files?: T9[] }`
- `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: T27, brand: T26|null, categoryIds: int[], photos: T23[], eans: str[], prices: T34[], suppliers: T11[] }` - `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: T27, brand: T26|null, categoryIds: int[], photos: T24[], eans: str[], prices: T35[], suppliers: T11[] }`
- `T3` = `{ id: int, number: str, type: str, date: str, lines: T19[], shippings: T25[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T9[], state?: str }` - `T3` = `{ id: int, number: str, type: str, date: str, lines: T20[], shippings: T19[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T9[], state?: str }`
- `T4` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T19[], externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T9[] }` - `T4` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T9[] }`
- `T5` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T19[], files: T9[], externalNumber?: str|null, createdAt?: str, ledger?: T38 }` - `T5` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T20[], files: T9[], externalNumber?: str|null, createdAt?: str, ledger?: T39 }`
- `T6` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T7|null }` - `T6` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T7|null }`
- `T7` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T12, totalSizeBytes: int, durationMs: int }` - `T7` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T12, totalSizeBytes: int, durationMs: int }`
- `T8` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T20[] }, thumb?: str|null }` - `T8` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T21[] }, thumb?: str|null }`
- `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, 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 }`
- `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, 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[] }`
- `T11` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T40|null }` - `T11` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T41|null }`
- `T12` = `{ vfs: T39, storage: T39, database: T32 }` - `T12` = `{ vfs: T40, storage: T40, database: T33 }`
- `T13` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }` - `T13` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- `T14` = `{ 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 }` - `T14` = `{ 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 }`
- `T15` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }` - `T15` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }`
- `T16` = `{ id: int, name: str, sort: int, exclusive: bool, values: T28[], articleCount?: int }` - `T16` = `{ id: int, name: str, sort: int, exclusive: bool, values: T28[], articleCount?: int }`
- `T17` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str, started_by?: int|null, first_message?: str|null }` - `T17` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str, started_by?: int|null, first_message?: str|null }`
- `T18` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num }` - `T18` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num }`
- `T19` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }` - `T19` = `{ id: int, manifestId: int, number: str, carrierId: int, type: str, state: str, trackingCode?: str|null, carrier?: str|null }`
- `T20` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }` - `T20` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }`
- `T21` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }` - `T21` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
- `T22` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }` - `T22` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }`
- `T23` = `{ sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }` - `T23` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }`
- `T24` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }` - `T24` = `{ sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }`
- `T25` = `{ id: int, manifestId: int, number: str, type: str, state: str, trackingCode?: str|null, carrier?: str|null }` - `T25` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }`
- `T26` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }` - `T26` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }`
- `T27` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }` - `T27` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
- `T28` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }` - `T28` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }`
- `T29` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }` - `T29` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }`
- `T30` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }` - `T30` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }`
- `T31` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }` - `T31` = `{ id: int, name: str, sort: int, hasPackages: bool, isReverse: bool }`
- `T32` = `{ name: str|null, sizeBytes: int, backedUp: bool, reason?: str|null }` - `T32` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }`
- `T33` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }` - `T33` = `{ name: str|null, sizeBytes: int, backedUp: bool, reason?: str|null }`
- `T34` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }` - `T34` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }`
- `T35` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }` - `T35` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }`
- `T36` = `{ id: int, entityId: int, contactType: str, contact: str }` - `T36` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
- `T37` = `{ id: int, name: str, createdAt: str, updatedAt: str }` - `T37` = `{ id: int, entityId: int, contactType: str, contact: str }`
- `T38` = `{ totalPositions: int, completelyOnLedgerPositions: int }` - `T38` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
- `T39` = `{ name: str, sizeBytes: int, sourceExists: bool }` - `T39` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
- `T40` = `{ net: num, taxId: int, taxRate?: num|null }` - `T40` = `{ name: str, sizeBytes: int, sourceExists: bool }`
- `T41` = `{ net: num, taxId: int, taxRate?: num|null }`
### Routes ### Routes
@@ -85,7 +86,7 @@ _none_
#### Create article in category — `POST /article` #### Create article in category — `POST /article`
- Params: `body: { categoryId: int, name: str, desc?: str, seo?: str, track_stock?: bool }` - Params: `body: { categoryId: int, name: str, desc?: str, seo?: str, track_stock?: bool }`
- Returns: `201 { article: T31 }` - Returns: `201 { article: T32 }`
#### Export articles as ZIP — `GET /article/export.zip` #### Export articles as ZIP — `GET /article/export.zip`
- Params: `query: categoryId?: int` - Params: `query: categoryId?: int`
- Returns: `200 zip` - Returns: `200 zip`
@@ -121,10 +122,10 @@ _none_
- Returns: `200 { children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }` - 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` #### 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 }` - Params: `sku: str, body: { attributeValueId?: int, value?: str, name?: str }`
- Returns: `201 { article: T31 }` - Returns: `201 { article: T32 }`
#### Attach an existing article as a child to a parent article — `PUT /article/{sku}/children` #### 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 }` - Params: `sku: str, body: { sku: str, attributeValueId?: int, value?: str, reparent?: bool }`
- Returns: `200 { article: T31, children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }` - Returns: `200 { article: T32, 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}` #### Remove a child article from a parent article — `DELETE /article/{sku}/children/{childSku}`
- Params: `sku: str, childSku: str, query: deleteArticle?: bool` - Params: `sku: str, childSku: str, query: deleteArticle?: bool`
- Returns: `200 { success: bool, deleted: bool }` - Returns: `200 { success: bool, deleted: bool }`
@@ -151,7 +152,7 @@ _none_
- Returns: `200 { options: { id: int, articleId: int, sku: str, name: str, count: int, sort: int, mandated: bool }[] }` - 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` #### Toggle parent mode for an article — `POST /article/{sku}/parent`
- Params: `sku: str, body: { isParent: bool, attributeNameId?: int }` - Params: `sku: str, body: { isParent: bool, attributeNameId?: int }`
- Returns: `200 { article: T31 }` - Returns: `200 { article: T32 }`
#### Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) — `PUT /article/{sku}/prices` #### 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 }[] }` - Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }`
- Returns: `200 { article: T2 }` - Returns: `200 { article: T2 }`
@@ -226,25 +227,25 @@ _none_
- Returns: `200 { attributevalue: T28 }` - Returns: `200 { attributevalue: T28 }`
#### Delete attribute value image — `DELETE /attributevalue/{id}/image` #### Delete attribute value image — `DELETE /attributevalue/{id}/image`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T35[] } }` - Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T36[] } }`
#### Add or replace attribute value image — `POST /attributevalue/{id}/image` #### Add or replace attribute value image — `POST /attributevalue/{id}/image`
- Params: `id: int, image/avif` - Params: `id: int, image/avif`
- Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T35[] } }` - Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T36[] } }`
### Section: /auth ### Section: /auth
#### Login with Google ID token — `POST /auth/login` #### Login with Google ID token — `POST /auth/login`
- Params: `body: { idToken: str }` - Params: `body: { idToken: str }`
- Returns: `200 { message: str, user: T33 }` - Returns: `200 { message: str, user: T34 }`
#### Logout — `POST /auth/logout` #### Logout — `POST /auth/logout`
_none_ _none_
- Returns: `200 { message: str }` - Returns: `200 { message: str }`
#### Current user — `GET /auth/me` #### Current user — `GET /auth/me`
_none_ _none_
- Returns: `200 { user: T33, googleClientId: str }` - Returns: `200 { user: T34, googleClientId: str }`
#### Set selected company — `PUT /auth/selected-company` #### Set selected company — `PUT /auth/selected-company`
- Params: `body: { companyId: int }` - Params: `body: { companyId: int }`
- Returns: `200 { user: T33 }` - Returns: `200 { user: T34 }`
### Section: /backup ### Section: /backup
@@ -265,7 +266,7 @@ _none_
- Returns: `200 { brands: { id: int, name: str, articleCount: int, 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 }` - Returns: `200 { brands: { id: int, name: str, articleCount: int, 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` #### Create brand — `POST /brand`
- Params: `body: { name: str, gsprpinfo?: str }` - Params: `body: { name: str, gsprpinfo?: str }`
- Returns: `201 { brand: { id: int, name: str, pics: T35[], gsprpinfo?: str|null } }` - Returns: `201 { brand: { id: int, name: str, pics: T36[], gsprpinfo?: str|null } }`
#### Delete brand — `DELETE /brand/{id}` #### Delete brand — `DELETE /brand/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
@@ -274,10 +275,31 @@ _none_
- Returns: `200 { brand: { id: int, name: str, touchedArticleCount: int, gsprpinfo?: str|null } }` - Returns: `200 { brand: { id: int, name: str, touchedArticleCount: int, gsprpinfo?: str|null } }`
#### Delete brand image — `DELETE /brand/{id}/image` #### Delete brand image — `DELETE /brand/{id}/image`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T35[] } }` - 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` #### Add or replace brand image — `POST /brand/{id}/image`
- Params: `id: int, image/avif` - Params: `id: int, image/avif`
- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T35[] } }` - Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T36[] } }`
### Section: /carrier
#### List carriers — `GET /carrier`
_none_
- Returns: `200 { carriers: T31[] }`
#### Create carrier — `POST /carrier`
- Params: `body: { name: str, sort?: int, hasPackages?: bool, isReverse?: bool }`
- Returns: `201 { carrier: T31 }`
#### Reorder carriers — `PUT /carrier/order`
- Params: `body: { carrierIds: int[] }`
- Returns: `200 { carrierIds: int[] }`
#### Delete carrier — `DELETE /carrier/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Get carrier by ID — `GET /carrier/{id}`
- Params: `id: int`
- Returns: `200 { carrier: T31 }`
#### Update carrier — `PATCH /carrier/{id}`
- Params: `id: int, body: { name?: str, sort?: int, hasPackages?: bool, isReverse?: bool }`
- Returns: `200 { carrier: T31 }`
### Section: /category ### Section: /category
@@ -331,7 +353,7 @@ _none_
- Returns: `200 { companies: T18[] }` - Returns: `200 { companies: T18[] }`
#### Create company — `POST /company` #### Create company — `POST /company`
- Params: `body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }` - Params: `body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
- Returns: `201 { company: T18, user: T33 }` - Returns: `201 { company: T18, user: T34 }`
#### Export company catalog as zip archive (SSE) — `GET /company/export` #### Export company catalog as zip archive (SSE) — `GET /company/export`
- Params: `query: stream?: str` - Params: `query: stream?: str`
- Returns: `200 SSE` - Returns: `200 SSE`
@@ -385,9 +407,9 @@ _none_
#### List deliveries — `GET /delivery` #### List deliveries — `GET /delivery`
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int`
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T38 }[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T39 }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create delivery — `POST /delivery` #### Create delivery — `POST /delivery`
- Params: `body: { externalNumber: str, date: str, lines: T22[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Params: `body: { externalNumber: str, date: str, lines: T23[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `201 { delivery: T5 }` - Returns: `201 { delivery: T5 }`
#### Delete delivery — `DELETE /delivery/{id}` #### Delete delivery — `DELETE /delivery/{id}`
- Params: `id: int` - Params: `id: int`
@@ -396,7 +418,7 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 { delivery: T5 }` - Returns: `200 { delivery: T5 }`
#### Update delivery — `PATCH /delivery/{id}` #### Update delivery — `PATCH /delivery/{id}`
- Params: `id: int, body: { externalNumber?: str|null, date?: str, supplierId?: int|null, lines?: T22[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Params: `id: int, body: { externalNumber?: str|null, date?: str, supplierId?: int|null, lines?: T23[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `200 { delivery: T5 }` - Returns: `200 { delivery: T5 }`
#### Delete delivery file attachment — `DELETE /delivery/{id}/file/{fileId}` #### Delete delivery file attachment — `DELETE /delivery/{id}/file/{fileId}`
- Params: `id: int, fileId: int` - Params: `id: int, fileId: int`
@@ -412,10 +434,10 @@ _none_
#### List entities — `GET /entity` #### List entities — `GET /entity`
- Params: `query: customergroupId?: int, role?: str` - Params: `query: customergroupId?: int, role?: str`
- Returns: `200 { entities: T24[] }` - Returns: `200 { entities: T25[] }`
#### Create entity — `POST /entity` #### Create entity — `POST /entity`
- Params: `body: { customergroupId: int, name: str, firstName?: str|null, number?: str }` - Params: `body: { customergroupId: int, name: str, firstName?: str|null, number?: str }`
- Returns: `201 { entity: T24 }` - Returns: `201 { entity: T25 }`
#### Peek next customer number — `GET /entity/next-number` #### Peek next customer number — `GET /entity/next-number`
_none_ _none_
- Returns: `200 { number: str }` - Returns: `200 { number: str }`
@@ -433,25 +455,25 @@ _none_
- Returns: `200 { address: T13 }` - Returns: `200 { address: T13 }`
#### List entity contacts — `GET /entity/{entityId}/contact` #### List entity contacts — `GET /entity/{entityId}/contact`
- Params: `entityId: int` - Params: `entityId: int`
- Returns: `200 { contacts: T36[] }` - Returns: `200 { contacts: T37[] }`
#### Create entity contact — `POST /entity/{entityId}/contact` #### Create entity contact — `POST /entity/{entityId}/contact`
- Params: `entityId: int, body: { contactType: str, contact: str }` - Params: `entityId: int, body: { contactType: str, contact: str }`
- Returns: `201 { contact: T36 }` - Returns: `201 { contact: T37 }`
#### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}` #### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int` - Params: `entityId: int, id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update entity contact — `PATCH /entity/{entityId}/contact/{id}` #### Update entity contact — `PATCH /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }` - Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }`
- Returns: `200 { contact: T36 }` - Returns: `200 { contact: T37 }`
#### Delete entity — `DELETE /entity/{id}` #### Delete entity — `DELETE /entity/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Get entity — `GET /entity/{id}` #### Get entity — `GET /entity/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { entity: T24 }` - Returns: `200 { entity: T25 }`
#### Update entity — `PATCH /entity/{id}` #### Update entity — `PATCH /entity/{id}`
- Params: `id: int, body: { name?: str, firstName?: str|null, number?: str|null }` - Params: `id: int, body: { name?: str, firstName?: str|null, number?: str|null }`
- Returns: `200 { entity: T24 }` - Returns: `200 { entity: T25 }`
### Section: /events ### Section: /events
@@ -493,7 +515,7 @@ _none_
_none_ _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: T20[] } }` - Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T21[] } }`
- Returns: `201 { labeltemplate: T8 }` - Returns: `201 { labeltemplate: T8 }`
#### Delete label template — `DELETE /labeltemplate/{id}` #### Delete label template — `DELETE /labeltemplate/{id}`
- Params: `id: int` - Params: `id: int`
@@ -502,10 +524,10 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 { labeltemplate: T8 }` - Returns: `200 { labeltemplate: T8 }`
#### 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: T20[] } }` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T21[] } }`
- Returns: `200 { labeltemplate: T8 }` - Returns: `200 { labeltemplate: T8 }`
#### 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: T20[] } }` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T21[] } }`
- Returns: `200 { labeltemplate: T8 }` - Returns: `200 { labeltemplate: T8 }`
### Section: /ledger ### Section: /ledger
@@ -598,7 +620,7 @@ _none_
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int`
- Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create order — `POST /order` #### Create order — `POST /order`
- Params: `body: { number: str, date: str, lines: T22[], entityId?: int|null, externalNumber?: str|null }` - Params: `body: { number: str, date: str, lines: T23[], entityId?: int|null, externalNumber?: str|null }`
- Returns: `201 { order: T1 }` - Returns: `201 { order: T1 }`
#### Delete order — `DELETE /order/{id}` #### Delete order — `DELETE /order/{id}`
- Params: `id: int` - Params: `id: int`
@@ -607,7 +629,7 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 { order: T1 }` - Returns: `200 { order: T1 }`
#### Update order — `PATCH /order/{id}` #### Update order — `PATCH /order/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T22[] }` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T23[] }`
- Returns: `200 { order: T1 }` - Returns: `200 { order: T1 }`
#### Delete order file attachment — `DELETE /order/{id}/file/{fileId}` #### Delete order file attachment — `DELETE /order/{id}/file/{fileId}`
- Params: `id: int, fileId: int` - Params: `id: int, fileId: int`
@@ -632,10 +654,10 @@ _none_
- Returns: `201 { shipment: T3 }` - Returns: `201 { shipment: T3 }`
#### List shippings of an order — `GET /order/{id}/shipping` #### List shippings of an order — `GET /order/{id}/shipping`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { shippings: T25[] }` - Returns: `200 { shippings: T19[] }`
#### Create shipping for an order — `POST /order/{id}/shipping` #### Create shipping for an order — `POST /order/{id}/shipping`
- Params: `id: int, body: { type: str, state: str, warehouseId?: int }` - Params: `id: int, body: { carrierId: int, type: str, state: str, trackingCode?: str, warehouseId?: int }`
- Returns: `201 { shipping: T25 }` - Returns: `201 { shipping: T19 }`
### Section: /package ### Section: /package
@@ -680,10 +702,10 @@ _none_
#### List printers — `GET /printer` #### List printers — `GET /printer`
_none_ _none_
- Returns: `200 { printers: T21[] }` - Returns: `200 { printers: T22[] }`
#### Create printer — `POST /printer` #### Create printer — `POST /printer`
- Params: `body: { address: str, type: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }` - Params: `body: { address: str, type: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `201 { printer: T21 }` - Returns: `201 { printer: T22 }`
#### Reorder printers — `PUT /printer/order` #### Reorder printers — `PUT /printer/order`
- Params: `body: { printerIds: int[] }` - Params: `body: { printerIds: int[] }`
- Returns: `200 { printerIds: int[] }` - Returns: `200 { printerIds: int[] }`
@@ -692,13 +714,13 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Get printer by ID — `GET /printer/{id}` #### Get printer by ID — `GET /printer/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { printer: T21 }` - Returns: `200 { printer: T22 }`
#### Update printer (partial) — `PATCH /printer/{id}` #### Update printer (partial) — `PATCH /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }` - Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 { printer: T21 }` - Returns: `200 { printer: T22 }`
#### Update printer — `PUT /printer/{id}` #### Update printer — `PUT /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }` - Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 { printer: T21 }` - Returns: `200 { printer: T22 }`
### Section: /propertyname ### Section: /propertyname
@@ -728,7 +750,7 @@ _none_
- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, net: num|null, taxRate: num|null, gross: num|null, trackStock: bool, brand: { id: int|null, name: str }, fullHash: str|null, thumbW: int|null, thumbH: int|null, thumbHash: str|null }[] }` - Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, net: num|null, taxRate: num|null, gross: num|null, trackStock: bool, brand: { id: int|null, name: str }, fullHash: str|null, thumbW: int|null, thumbH: int|null, thumbHash: str|null }[] }`
#### Get public article detail — `GET /public/{companyId}/articles/{sku}` #### Get public article detail — `GET /public/{companyId}/articles/{sku}`
- Params: `companyId: int, sku: str` - Params: `companyId: int, sku: str`
- Returns: `200 { article: { id: int, sku: str, name: str, slug: str, description: str, unitamount: num|null, weight: num|null, trackStock: bool, unit: T27|null, brand: T26|null, price: { minmultiplier: num, net: num|null, taxRate: num|null, gross: num|null }, prices: { minmultiplier: num, net: num|null, taxRate: num|null, gross: num|null }[], categories: { id: int, name: str, slug: str }[], photos: T23[], eans: str[], isParent: bool, parentSku: str|null, variants: { id: int, sku: str, sort: int, name: str, slug: str, attributeValueId: int|null, attributeValue: str|null, unitamount: num|null, weight: num|null, trackStock: bool }[] } }` - Returns: `200 { article: { id: int, sku: str, name: str, slug: str, description: str, unitamount: num|null, weight: num|null, trackStock: bool, unit: T27|null, brand: T26|null, price: { minmultiplier: num, net: num|null, taxRate: num|null, gross: num|null }, prices: { minmultiplier: num, net: num|null, taxRate: num|null, gross: num|null }[], categories: { id: int, name: str, slug: str }[], photos: T24[], eans: str[], isParent: bool, parentSku: str|null, variants: { id: int, sku: str, sort: int, name: str, slug: str, attributeValueId: int|null, attributeValue: str|null, unitamount: num|null, weight: num|null, trackStock: bool }[] } }`
#### List public categories — `GET /public/{companyId}/categories` #### List public categories — `GET /public/{companyId}/categories`
- Params: `companyId: int` - Params: `companyId: int`
- Returns: `200 { categories: { id: int, parentId: int|null, sort: int, name: str, slug: str, fullHash: str|null, thumbW: int|null, thumbH: int|null, thumbHash: str|null }[] }` - Returns: `200 { categories: { id: int, parentId: int|null, sort: int, name: str, slug: str, fullHash: str|null, thumbW: int|null, thumbH: int|null, thumbHash: str|null }[] }`
@@ -739,7 +761,7 @@ _none_
- Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { quotes: T4[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { quotes: T4[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create quote — `POST /quote` #### Create quote — `POST /quote`
- Params: `body: { number: str, date: str, lines: T22[], entityId?: int|null, externalNumber?: str|null }` - Params: `body: { number: str, date: str, lines: T23[], entityId?: int|null, externalNumber?: str|null }`
- Returns: `201 { quote: T4 }` - Returns: `201 { quote: T4 }`
#### Delete quote — `DELETE /quote/{id}` #### Delete quote — `DELETE /quote/{id}`
- Params: `id: int` - Params: `id: int`
@@ -748,7 +770,7 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 { quote: T4 }` - Returns: `200 { quote: T4 }`
#### Update quote — `PATCH /quote/{id}` #### Update quote — `PATCH /quote/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T22[] }` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T23[] }`
- Returns: `200 { quote: T4 }` - Returns: `200 { quote: T4 }`
#### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}` #### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}`
- Params: `id: int, fileId: int` - Params: `id: int, fileId: int`
@@ -757,7 +779,7 @@ _none_
- Params: `id: int, fileId: int, query: download?: bool` - Params: `id: int, fileId: int, query: download?: bool`
- Returns: `200 str` - Returns: `200 str`
#### Create order from quote — `POST /quote/{id}/order` #### Create order from quote — `POST /quote/{id}/order`
- Params: `id: int, body?: { number?: str, date?: str, createdAt?: str, entityId?: int|null, externalNumber?: str|null, lines?: T22[] }` - Params: `id: int, body?: { number?: str, date?: str, createdAt?: str, entityId?: int|null, externalNumber?: str|null, lines?: T23[] }`
- 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`
@@ -803,10 +825,10 @@ _none_
- Returns: `201 { attachment: T9, fileUrl: str }` - Returns: `201 { attachment: T9, 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: T25[] }` - Returns: `200 { shippings: T19[] }`
#### Create package for a shipment — `POST /shipment/{id}/shipping` #### Create package for a shipment — `POST /shipment/{id}/shipping`
- Params: `id: int, body: { type: str, state: str, trackingCode?: str, carrier?: str, warehouseId?: int }` - Params: `id: int, body: { carrierId: int, type: str, state: str, trackingCode?: str, warehouseId?: int }`
- Returns: `201 { shipping: T25 }` - Returns: `201 { shipping: T19 }`
### Section: /shipping ### Section: /shipping
@@ -814,8 +836,8 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update shipping — `PATCH /shipping/{id}` #### Update shipping — `PATCH /shipping/{id}`
- Params: `id: int, body: { type?: str, state?: str, trackingCode?: str, carrier?: str, warehouseId?: int }` - Params: `id: int, body: { type?: str, state?: str, trackingCode?: str, carrierId?: int, warehouseId?: int }`
- Returns: `200 { shipping: T25 }` - Returns: `200 { shipping: T19 }`
#### List packages of a shipping — `GET /shipping/{id}/package` #### List packages of a shipping — `GET /shipping/{id}/package`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }` - Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }`
@@ -863,22 +885,22 @@ _none_
#### List suppliers — `GET /supplier` #### List suppliers — `GET /supplier`
_none_ _none_
- Returns: `200 { suppliers: T37[] }` - Returns: `200 { suppliers: T38[] }`
#### Create supplier — `POST /supplier` #### Create supplier — `POST /supplier`
- Params: `body: { name: str }` - Params: `body: { name: str }`
- Returns: `201 { supplier: T37 }` - Returns: `201 { supplier: T38 }`
#### Delete supplier — `DELETE /supplier/{id}` #### Delete supplier — `DELETE /supplier/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update supplier — `PATCH /supplier/{id}` #### Update supplier — `PATCH /supplier/{id}`
- Params: `id: int, body: { name: str }` - Params: `id: int, body: { name: str }`
- Returns: `200 { supplier: T37 }` - Returns: `200 { supplier: T38 }`
#### List supplier articles by SKUs — `GET /supplier/{id}/articles` #### List supplier articles by SKUs — `GET /supplier/{id}/articles`
- Params: `id: int, query: skus?: str` - Params: `id: int, query: skus?: str`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T40|null }[] }` - Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T41|null }[] }`
#### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles` #### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles`
- Params: `id: int, body?: { skus?: str[] }` - Params: `id: int, body?: { skus?: str[] }`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T40|null }[] }` - Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T41|null }[] }`
### Section: /thumb ### Section: /thumb

49
src/api/carrier.js Normal file
View File

@@ -0,0 +1,49 @@
import { http } from './http.js';
/**
* Company-scoped carriers / Lieferarten.
* Shape: Carrier = { id, name, sort, hasPackages, isReverse }
*/
export const carrier = {
/** GET /api/carrier → { carriers: Carrier[] } */
list() {
return http('/api/carrier');
},
/**
* POST /api/carrier
* Body: { name, sort?, hasPackages?, isReverse? }
* → 201 { carrier: Carrier }
*/
create(body) {
return http('/api/carrier', { method: 'POST', body });
},
/** GET /api/carrier/:id → 200 { carrier: Carrier } */
get(id) {
return http(`/api/carrier/${id}`);
},
/**
* PATCH /api/carrier/:id
* Body: { name?, sort?, hasPackages?, isReverse? }
* → 200 { carrier: Carrier }
*/
update(id, body) {
return http(`/api/carrier/${id}`, { method: 'PATCH', body });
},
/**
* PUT /api/carrier/order
* Body: { carrierIds: number[] }
* → 200 { carrierIds: number[] }
*/
order(body) {
return http('/api/carrier/order', { method: 'PUT', body });
},
/** DELETE /api/carrier/:id → 204 · 409 Conflict if in use */
delete(id) {
return http(`/api/carrier/${id}`, { method: 'DELETE' });
},
};

View File

@@ -32,3 +32,4 @@ export { payment } from './payment.js';
export { account } from './account.js'; export { account } from './account.js';
export { mcp } from './mcp.js'; export { mcp } from './mcp.js';
export { backup } from './backup.js'; export { backup } from './backup.js';
export { carrier } from './carrier.js';

View File

@@ -23,6 +23,7 @@ import AuftragBestellungNeuPage from './routes/auftrag/bestellung/neu';
import AuftragBestellungEditPage from './routes/auftrag/bestellung/_orderId'; import AuftragBestellungEditPage from './routes/auftrag/bestellung/_orderId';
import AuftragAusliefernPage from './routes/auftrag/ausliefern'; import AuftragAusliefernPage from './routes/auftrag/ausliefern';
import AuftragZahlungenPage from './routes/auftrag/zahlungen'; import AuftragZahlungenPage from './routes/auftrag/zahlungen';
import AuftragLieferartenPage from './routes/auftrag/lieferarten';
import KundeListePage from './routes/kunde'; import KundeListePage from './routes/kunde';
import KundeNeuPage from './routes/kunde/neu'; import KundeNeuPage from './routes/kunde/neu';
@@ -104,6 +105,7 @@ const routes = [
{ path: 'bestellung/:orderId', element: <AuftragBestellungEditPage /> }, { path: 'bestellung/:orderId', element: <AuftragBestellungEditPage /> },
{ path: 'ausliefern', element: <AuftragAusliefernPage /> }, { path: 'ausliefern', element: <AuftragAusliefernPage /> },
{ path: 'zahlungen', element: <AuftragZahlungenPage /> }, { path: 'zahlungen', element: <AuftragZahlungenPage /> },
{ path: 'lieferarten', element: <AuftragLieferartenPage /> },
{ path: ':quoteId', element: <AuftragAngebotEditPage /> }, { path: ':quoteId', element: <AuftragAngebotEditPage /> },
], ],

View File

@@ -0,0 +1,251 @@
/**
* @file carrierAutomat.js
* @description Manages cached shipping carriers / Lieferarten (`GET /api/carrier`) for the active company.
*
* Upstream Triggers:
* - `authAutomat`: Subscribes to auth state changes (`syncWithAuth`) — triggers
* `actions.load()` on `'authed'` with a `selectedCompanyId`, and `actions.clear()` on `'anon'`.
* - `sseAutomat`: Calls `actions.reload()` upon receiving a `'carrier'` SSE event.
* - UI / Routes: Calls to `actions.load()`, `actions.reload()`, or mutation actions.
*/
import { Automat } from 'react-automat';
import { carrier as carrierApi } from '../api';
import authAutomat from './authAutomat';
/**
* @typedef {Object} Carrier
* @property {number} id Carrier identifier.
* @property {string} name Carrier name.
* @property {number} sort Sort order.
* @property {boolean} hasPackages Whether carrier uses package tracking.
* @property {boolean} isReverse Whether carrier is used for returns.
*/
/**
* @typedef {Object} CarrierState
* @property {Array<Carrier>} carriers Company-scoped carriers.
* @property {Array<Carrier>} filteredCarriers Filtered carriers according to active filter query.
* @property {string} filter Active search/filter string.
* @property {'idle'|'loading'|'ready'|'failed'} status Load lifecycle state.
* @property {Error|null} error Last load error.
*/
/** @type {CarrierState} */
const initialState = {
carriers: [],
filteredCarriers: [],
filter: '',
status: 'idle',
error: null,
};
let carrierAutomat;
const applyFilter = (carriers, filter) => {
const query = String(filter ?? '').trim().toLowerCase();
return query ? carriers.filter((c) => String(c.name ?? '').toLowerCase().includes(query)) : carriers;
};
const updateStateWithCarriers = (carriers, extra = {}) => {
const list = Array.isArray(carriers) ? carriers : [];
const filteredCarriers = applyFilter(list, carrierAutomat.state.filter);
carrierAutomat.setState({ carriers: list, filteredCarriers, ...extra });
return list;
};
/**
* Fetches the company-scoped carrier list.
*
* @returns {Promise<Array<Carrier>>} Carrier list.
*/
async function fetchCarriers() {
const data = await carrierApi.list();
if (Array.isArray(data)) return data;
if (data && Array.isArray(data.carriers)) return data.carriers;
return [];
}
/** In-flight `load()` promise so concurrent callers share one request. */
let inflightLoad = null;
/**
* Loads the carrier list once, deduplicating concurrent callers.
*
* @returns {Promise<Array<Carrier>>} Loaded carriers.
*/
function loadCarriers() {
if (inflightLoad) return inflightLoad;
inflightLoad = (async () => {
carrierAutomat.setState({ status: 'loading', error: null });
try {
const carriers = await fetchCarriers();
updateStateWithCarriers(carriers, { status: 'ready', error: null });
return carriers;
} catch (error) {
carrierAutomat.setState({ status: 'failed', error });
throw error;
} finally {
inflightLoad = null;
}
})();
return inflightLoad;
}
const actions = {
/** Sets the active carrier search filter and updates filteredCarriers. */
filter(query) {
const filter = String(query ?? '');
carrierAutomat.setState({
filter,
filteredCarriers: applyFilter(carrierAutomat.state.carriers, filter),
});
},
/**
* Loads the carrier list, exposing a loading/failed lifecycle.
*
* @returns {Promise<Array<Carrier>>} Loaded carriers.
*/
load() {
return loadCarriers();
},
/**
* Silently re-fetches the carrier list while keeping the current state.
*
* @returns {Promise<Array<Carrier>>} Fresh carriers.
*/
async reload() {
try {
const carriers = await fetchCarriers();
updateStateWithCarriers(carriers, { status: 'ready', error: null });
return carriers;
} catch (error) {
carrierAutomat.setState({ error });
throw error;
}
},
/**
* Creates a carrier and refreshes the cached list.
*
* @param {{name: string, sort?: number, hasPackages?: boolean, isReverse?: boolean}} body Creation payload.
* @returns {Promise<Carrier>} Created carrier.
*/
async create(body) {
const data = await carrierApi.create(body);
const created = data?.carrier || data;
await carrierAutomat.actions.reload();
return created;
},
/**
* Updates a carrier and refreshes the cached list.
*
* @param {string|number} id Carrier identifier.
* @param {{name?: string, sort?: number, hasPackages?: boolean, isReverse?: boolean}} body Update payload.
* @returns {Promise<Carrier>} Updated carrier.
*/
async update(id, body) {
const data = await carrierApi.update(id, body);
const updated = data?.carrier || data;
await carrierAutomat.actions.reload();
return updated;
},
/**
* Deletes a carrier and refreshes the cached list.
*
* @param {string|number} id Carrier identifier.
* @returns {Promise<void>}
*/
async remove(id) {
await carrierApi.delete(id);
await carrierAutomat.actions.reload();
},
/**
* Persists a new sort order for all carriers.
*
* @param {Array<string|number>} carrierIds IDs in the desired order.
* @returns {Promise<void>}
*/
async reorder(carrierIds) {
const byId = new Map(carrierAutomat.state.carriers.map((c) => [c.id, c]));
const reordered = carrierIds.map((id) => byId.get(id)).filter(Boolean);
if (reordered.length === carrierAutomat.state.carriers.length) {
updateStateWithCarriers(reordered);
}
await carrierApi.order({ carrierIds });
await carrierAutomat.actions.reload();
},
/**
* Replaces the cached list without a round-trip.
*
* @param {Array<Carrier>} carriers Carriers to cache.
* @returns {void}
*/
setCarriers(carriers) {
updateStateWithCarriers(carriers, {
status: 'ready',
error: null,
});
},
/**
* Forgets the cached list and returns to the initial state.
*
* @returns {void}
*/
clear() {
carrierAutomat.setState({ ...initialState });
},
};
/**
* Company-scoped carrier cache. Loads automatically once an authenticated
* session with a selected company exists and is cleared when auth is gone.
*
* @type {Automat<CarrierState>}
*/
carrierAutomat = new Automat(initialState, actions, { name: 'carrier' });
/**
* Derives the cache key from the auth state: the selected company while
* authenticated, otherwise `null`.
*
* @param {import('./authAutomat').AuthState} authState Auth automat state.
* @returns {string|null} Cache key.
*/
function authKey(authState) {
if (authState?.status !== 'authed' || !authState.user) return null;
const companyId = authState.user.selectedCompanyId;
return companyId == null ? null : String(companyId);
}
let lastAuthKey;
/**
* Loads the carrier list when auth/company becomes available and forgets it
* when the session ends.
*
* @param {import('./authAutomat').AuthState} authState Auth automat state.
* @returns {void}
*/
function syncWithAuth(authState) {
const key = authKey(authState);
if (key === lastAuthKey) return;
lastAuthKey = key;
if (key == null) {
carrierAutomat.actions.clear();
} else {
void carrierAutomat.actions.load().catch(() => {});
}
}
authAutomat.subscribe(syncWithAuth);
syncWithAuth(authAutomat.state);
export default carrierAutomat;

View File

@@ -46,6 +46,7 @@ export {
export { default as supplierAutomat } from './supplierAutomat'; export { default as supplierAutomat } from './supplierAutomat';
export { default as warehouseAutomat } from './warehouseAutomat'; export { default as warehouseAutomat } from './warehouseAutomat';
export { default as carrierAutomat } from './carrierAutomat';
/** Article search over the derived index (replaces StorageManager.doSearch). */ /** Article search over the derived index (replaces StorageManager.doSearch). */
export { export {

View File

@@ -36,6 +36,7 @@ import deliveryAutomat from './deliveryAutomat';
import orderAutomat from './orderAutomat'; import orderAutomat from './orderAutomat';
import quoteAutomat from './quoteAutomat'; import quoteAutomat from './quoteAutomat';
import shipmentAutomat from './shipmentAutomat'; import shipmentAutomat from './shipmentAutomat';
import carrierAutomat from './carrierAutomat';
/** /**
@@ -223,6 +224,12 @@ function connect() {
}); });
}); });
addTrackedListener(es, 'carrier', () => {
void carrierAutomat.actions.reload().catch((err) => {
console.error('[sse] carrier refresh failed', err);
});
});
addTrackedListener(es, 'attributename', () => { addTrackedListener(es, 'attributename', () => {
void attributeAutomat.actions.load().catch((err) => { void attributeAutomat.actions.load().catch((err) => {
console.error('[sse] attributename refresh failed', err); console.error('[sse] attributename refresh failed', err);

View File

@@ -0,0 +1,213 @@
import { PureComponent } from 'react';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import ListItem from '@mui/material/ListItem';
import Switch from '@mui/material/Switch';
import FormControlLabel from '@mui/material/FormControlLabel';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import { enqueueSnackbar } from 'notistack';
import carrierAutomat from '../../automats/carrierAutomat';
import { notifyError } from '../../utils/notify';
class CarrierItem extends PureComponent {
constructor(props) {
super(props);
const item = props.carrier ?? props.item;
this.state = {
draft: item?.name ?? '',
};
}
componentDidUpdate(prevProps) {
const prevItem = prevProps.carrier ?? prevProps.item;
const item = this.props.carrier ?? this.props.item;
if (prevItem?.name !== item?.name) {
this.setState({ draft: item?.name ?? '' });
} else if (prevProps.disabled && !this.props.disabled) {
if (this.state.draft !== item?.name) {
this.setState({ draft: item?.name ?? '' });
}
}
}
handleChange = (next) => {
this.setState({ draft: next });
const item = this.props.carrier ?? this.props.item;
this.props.onChange?.(next, item);
};
handleBlur = () => {
const item = this.props.carrier ?? this.props.item;
if (!item) return;
const draft = this.state.draft.trim();
if (!draft) {
this.setState({ draft: item.name ?? '' });
return;
}
if (draft !== item.name) {
void this.updateCarrier(item, { name: draft });
}
};
async updateCarrier(item, patch) {
try {
await carrierAutomat.actions.update(item.id, patch);
enqueueSnackbar('Lieferart gespeichert.', { variant: 'success' });
} catch (err) {
this.setState({ draft: item.name ?? '' });
notifyError(err);
}
}
handleToggleHasPackages = (e) => {
const item = this.props.carrier ?? this.props.item;
if (!item) return;
const nextVal = e.target.checked;
void this.updateCarrier(item, { hasPackages: nextVal });
};
handleToggleIsReverse = (e) => {
const item = this.props.carrier ?? this.props.item;
if (!item) return;
const nextVal = e.target.checked;
void this.updateCarrier(item, { isReverse: nextVal });
};
handleKeyDown = (event) => {
const item = this.props.carrier ?? this.props.item;
if (event.key === 'Enter') {
event.preventDefault();
event.target.blur();
}
if (event.key === 'Escape') {
event.preventDefault();
this.setState({ draft: item?.name ?? '' });
event.target.blur();
}
};
handleDelete = () => {
const item = this.props.carrier ?? this.props.item;
if (!item) return;
if (!window.confirm(`Lieferart „${item.name}“ wirklich löschen?`)) return;
void (async () => {
try {
await carrierAutomat.actions.remove(item.id);
enqueueSnackbar('Lieferart gelöscht.', { variant: 'success' });
} catch (err) {
notifyError(err);
}
})();
};
render() {
const { disabled, sx } = this.props;
const item = this.props.carrier ?? this.props.item;
const { draft } = this.state;
const name = item?.name ?? '';
const hasPackages = Boolean(item?.hasPackages);
const isReverse = Boolean(item?.isReverse);
return (
<ListItem
disableGutters
data-id={item?.id}
sx={{
gap: 1,
py: 0.75,
alignItems: 'center',
width: '100%',
minWidth: 0,
flexWrap: { xs: 'wrap', sm: 'nowrap' },
...sx,
}}
>
<Box
className="drag-handle"
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 40,
height: 40,
flexShrink: 0,
color: 'text.disabled',
cursor: disabled ? 'default' : 'grab',
userSelect: 'none',
touchAction: 'none',
'&:hover': disabled ? undefined : { color: 'text.secondary' },
transition: 'color 0.15s',
}}
>
<DragIndicatorIcon sx={{ fontSize: 24, pointerEvents: 'none' }} />
</Box>
<SingleLineTextEdit
size="small"
label=""
value={draft}
disabled={disabled || !item}
onChange={this.handleChange}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
sx={{ flex: 1, minWidth: 140 }}
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexShrink: 0 }}>
<Tooltip title="Paketverfolgung / Tracking für Sendungen aktivieren">
<FormControlLabel
control={
<Switch
size="small"
checked={hasPackages}
disabled={disabled || !item}
onChange={this.handleToggleHasPackages}
/>
}
label={
<Typography variant="caption" sx={{ userSelect: 'none', fontWeight: 500 }}>
Pakete
</Typography>
}
sx={{ m: 0, gap: 0.5 }}
/>
</Tooltip>
<Tooltip title="Für Retouren / Rücksendungen verwenden">
<FormControlLabel
control={
<Switch
size="small"
checked={isReverse}
disabled={disabled || !item}
onChange={this.handleToggleIsReverse}
/>
}
label={
<Typography variant="caption" sx={{ userSelect: 'none', fontWeight: 500 }}>
Retoure
</Typography>
}
sx={{ m: 0, gap: 0.5 }}
/>
</Tooltip>
</Box>
<IconButton
aria-label={`„${name}“ löschen`}
disabled={disabled || !item}
onClick={this.handleDelete}
sx={{ width: 40, height: 40, flexShrink: 0 }}
>
<DeleteOutlinedIcon sx={{ fontSize: 24 }} />
</IconButton>
</ListItem>
);
}
}
export default CarrierItem;

View File

@@ -0,0 +1 @@
export { default as CarrierItem } from './CarrierItem';

View File

@@ -31,16 +31,17 @@ import { notifyError } from '../../utils/notify';
import { formatDate } from './manifestConfig'; import { formatDate } from './manifestConfig';
import orderAutomat from '../../automats/orderAutomat'; import orderAutomat from '../../automats/orderAutomat';
import shipmentAutomat from '../../automats/shipmentAutomat'; import shipmentAutomat from '../../automats/shipmentAutomat';
import carrierAutomat from '../../automats/carrierAutomat';
import { SURFACE_RAISED, WHITE } from '../../colors'; import { SURFACE_RAISED, WHITE } from '../../colors';
const CARRIERS = ['DHL', 'UPS', 'DPD', 'GLS', 'Hermes', 'Spedition', 'Selbstabholung'];
// MARK: <AddPackageDialog/> // MARK: <AddPackageDialog/>
class AddPackageDialog extends PureComponent { class AddPackageDialog extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
const carriers = carrierAutomat.state.carriers || [];
this.state = { this.state = {
carrier: 'DHL', carriers,
carrierId: carriers[0]?.id ?? '',
trackingCode: '', trackingCode: '',
type: 'parcel', type: 'parcel',
state: 'draft', state: 'draft',
@@ -49,15 +50,34 @@ class AddPackageDialog extends PureComponent {
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
} }
componentDidMount() {
this.unsub = carrierAutomat.subscribe(this, (state) => {
const carriers = state.carriers || [];
const currentId = this.state.carrierId;
const valid = carriers.some((c) => c.id === currentId);
return {
carriers,
carrierId: valid ? currentId : (carriers[0]?.id ?? ''),
};
});
if (carrierAutomat.state.status !== 'ready') {
void carrierAutomat.actions.load().catch(() => {});
}
}
componentWillUnmount() {
this.unsub?.();
}
async handleSubmit() { async handleSubmit() {
const { shipment, onClose, onSuccess } = this.props; const { shipment, onClose, onSuccess } = this.props;
const { carrier, trackingCode, type, state } = this.state; const { carrierId, trackingCode, type, state } = this.state;
if (!shipment?.id) return; if (!shipment?.id || !carrierId) return;
this.setState({ saving: true }); this.setState({ saving: true });
try { try {
await shippingApi.create(shipment.id, { await shippingApi.create(shipment.id, {
carrier: carrier.trim() || undefined, carrierId: Number(carrierId),
trackingCode: trackingCode.trim() || undefined, trackingCode: trackingCode.trim() || undefined,
type, type,
state, state,
@@ -77,7 +97,8 @@ class AddPackageDialog extends PureComponent {
render() { render() {
const { open, onClose, shipment } = this.props; const { open, onClose, shipment } = this.props;
const { carrier, trackingCode, type, state, saving } = this.state; const { carriers, carrierId, trackingCode, type, state, saving } = this.state;
const canSubmit = Boolean(carrierId) && !saving;
return ( return (
<AppDialog open={open} onClose={onClose} maxWidth="xs" fullWidth> <AppDialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
@@ -97,13 +118,21 @@ class AddPackageDialog extends PureComponent {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}>
<TextField <TextField
select select
label="Versanddienstleister (Carrier)" label="Lieferart (Carrier)"
size="small" size="small"
value={carrier} value={carrierId}
onChange={(e) => this.setState({ carrier: e.target.value })} onChange={(e) => this.setState({ carrierId: e.target.value })}
error={carriers.length === 0}
helperText={
carriers.length === 0
? 'Keine Lieferarten eingerichtet (unter Auftrag → Lieferarten anlegen)'
: undefined
}
> >
{CARRIERS.map((c) => ( {carriers.map((c) => (
<MenuItem key={c} value={c}>{c}</MenuItem> <MenuItem key={c.id} value={c.id}>
{c.name}
</MenuItem>
))} ))}
</TextField> </TextField>
@@ -145,7 +174,7 @@ class AddPackageDialog extends PureComponent {
<Button variant="outlined" onClick={onClose} disabled={saving}> <Button variant="outlined" onClick={onClose} disabled={saving}>
Abbrechen Abbrechen
</Button> </Button>
<Button variant="contained" onClick={this.handleSubmit} disabled={saving}> <Button variant="contained" onClick={this.handleSubmit} disabled={!canSubmit}>
{saving ? 'Wird gespeichert…' : 'Paket anlegen'} {saving ? 'Wird gespeichert…' : 'Paket anlegen'}
</Button> </Button>
</Box> </Box>

View File

@@ -17,6 +17,7 @@ import StorefrontIcon from '@mui/icons-material/Storefront';
import RequestQuoteIcon from '@mui/icons-material/RequestQuote'; import RequestQuoteIcon from '@mui/icons-material/RequestQuote';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import PaymentsIcon from '@mui/icons-material/Payments'; import PaymentsIcon from '@mui/icons-material/Payments';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import LabelIcon from '@mui/icons-material/Label'; import LabelIcon from '@mui/icons-material/Label';
import HistoryIcon from '@mui/icons-material/History'; import HistoryIcon from '@mui/icons-material/History';
@@ -226,6 +227,13 @@ export const SUB_NAV = {
icon: PaymentsIcon, icon: PaymentsIcon,
help: 'Zahlungseingänge erfassen und Auftragszahlungen überwachen', help: 'Zahlungseingänge erfassen und Auftragszahlungen überwachen',
}, },
{
segment: 'lieferarten',
label: 'Lieferarten',
title: 'Lieferarten',
icon: LocalShippingOutlinedIcon,
help: 'Versanddienstleister und Lieferarten (Carriers) verwalten',
},
], ],
'/kunde': [ '/kunde': [
{ {

View File

@@ -0,0 +1,270 @@
import { PureComponent, createRef } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import List from '@mui/material/List';
import Typography from '@mui/material/Typography';
import Sortable from 'sortablejs';
import { enqueueSnackbar } from 'notistack';
import BackendError from '../../components/BackendError';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
import withNavigate from '../../components/withNavigate';
import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify';
import carrierAutomat from '../../automats/carrierAutomat';
import { CarrierItem } from '../../components/carrier';
import { GREEN_PANEL } from '../../colors';
class LieferartenSearchField extends PureComponent {
constructor(props) {
super(props);
const initial = carrierAutomat.state.filter || '';
this.state = {
input: initial,
submitting: false,
};
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
this.unsub = carrierAutomat.subscribe(this, (state) => ({
input: state.filter,
}));
}
componentWillUnmount() {
this.unsub?.();
}
onInputChange(input) {
this.setState({ input });
carrierAutomat.actions.filter(input);
}
async handleSubmit() {
const name = this.state.input.trim();
if (!name || this.state.submitting) return;
this.setState({ submitting: true });
try {
const row = await carrierAutomat.actions.create({
name,
hasPackages: true,
isReverse: false,
});
enqueueSnackbar(`Lieferart „${row.name || name}“ angelegt.`, { variant: 'success' });
this.setState({ input: '', submitting: false });
carrierAutomat.actions.filter('');
} catch (err) {
this.setState({ submitting: false });
notifyError(err);
}
}
render() {
const { inputRef } = this.props;
const { input, submitting } = this.state;
const canSubmit = Boolean(input.trim()) && !submitting;
return (
<PanelTileGrid>
<Box
ref={inputRef}
sx={{
gridColumn: {
xs: '1 / -1',
sm: 'span 2',
md: 'span 2',
},
display: 'flex',
flexDirection: 'column',
gap: 1,
pb: 2,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<SingleLineTextEdit
sx={{ flex: 1 }}
size="small"
search
clear
placeholder="Lieferart suchen oder neu anlegen..."
value={input}
disabled={submitting}
onChange={(next) => this.onInputChange(next)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
void this.handleSubmit();
}
}}
/>
<Button
variant="contained"
disabled={!canSubmit}
onClick={() => void this.handleSubmit()}
>
Hinzufügen
</Button>
</Box>
</Box>
</PanelTileGrid>
);
}
}
class AuftragLieferartenPage extends PureComponent {
constructor(props) {
super(props);
const initialCarriers =
carrierAutomat.state.filteredCarriers ?? carrierAutomat.state.carriers ?? [];
this.state = {
status: carrierAutomat.state.status === 'ready' ? 'ready' : 'loading',
error: carrierAutomat.state.error ? backendErrorFrom(carrierAutomat.state.error) : null,
carriers: initialCarriers,
filter: carrierAutomat.state.filter ?? '',
};
this.searchRef = createRef();
this.listRef = createRef();
this._sortable = null;
this._cancelled = false;
this.handleRetry = this.handleRetry.bind(this);
this._initSortable = this._initSortable.bind(this);
this._destroySortable = this._destroySortable.bind(this);
}
componentDidMount() {
this._cancelled = false;
this.unsub = carrierAutomat.subscribe(this, (state) => ({
carriers: state.filteredCarriers ?? state.carriers,
filter: state.filter ?? '',
status: state.status,
error: state.error ? backendErrorFrom(state.error) : null,
}));
if (carrierAutomat.state.status !== 'ready') {
void carrierAutomat.actions.load().catch((err) => notifyError(err));
} else {
this._initSortable();
}
}
componentDidUpdate(prevProps, prevState) {
if (
prevState.status !== this.state.status ||
prevState.carriers !== this.state.carriers ||
prevState.filter !== this.state.filter
) {
if (this.state.status === 'ready') {
this._initSortable();
}
}
}
componentWillUnmount() {
this._cancelled = true;
this.unsub?.();
this._destroySortable();
}
_initSortable() {
this._destroySortable();
const el = this.listRef.current;
if (!el) return;
const isFiltered = Boolean(this.state.filter?.trim());
this._sortable = Sortable.create(el, {
handle: '.drag-handle',
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
disabled: isFiltered,
onEnd: (evt) => {
const { oldIndex, newIndex } = evt;
if (oldIndex === newIndex) return;
this.setState(
(prev) => {
const next = [...prev.carriers];
const [moved] = next.splice(oldIndex, 1);
next.splice(newIndex, 0, moved);
return { carriers: next };
},
() => {
const ids = this.state.carriers.map((c) => c.id);
void this._persistOrder(ids);
}
);
},
});
}
_destroySortable() {
this._sortable?.destroy();
this._sortable = null;
}
async _persistOrder(carrierIds) {
try {
await carrierAutomat.actions.reorder(carrierIds);
} catch (err) {
if (this._cancelled) return;
notifyError(err);
void carrierAutomat.actions.reload();
}
}
handleRetry() {
void carrierAutomat.actions.load().catch((err) => notifyError(err));
}
render() {
const { status, error, carriers, filter } = this.state;
if (status === 'loading' || status === 'idle') {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
<CircularProgress />
</Box>
);
}
if (status === 'failed') {
return <BackendError error={error} onRetry={this.handleRetry} />;
}
const trimmedFilter = filter?.trim() ?? '';
return (
<>
<LieferartenSearchField inputRef={this.searchRef} />
<PanelTileGrid>
<PanelTile wide={2} minHeight={0} color={GREEN_PANEL}>
<List dense disablePadding ref={this.listRef} sx={{ width: '100%' }}>
{carriers.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 1 }}>
{trimmedFilter
? `Keine Treffer für „${trimmedFilter}“.`
: 'Noch keine Lieferarten angelegt.'}
</Typography>
) : (
carriers.map((c) => (
<CarrierItem
key={c.id}
carrier={c}
/>
))
)}
</List>
</PanelTile>
</PanelTileGrid>
</>
);
}
}
export { CarrierItem, LieferartenSearchField };
export default withNavigate(AuftragLieferartenPage);