u
This commit is contained in:
@@ -7,7 +7,7 @@ description: >-
|
|||||||
|
|
||||||
# Dash API Routes Overview (All)
|
# Dash API Routes Overview (All)
|
||||||
|
|
||||||
Overview of all 247 API endpoints across all modules.
|
Overview of all 249 API endpoints across all modules.
|
||||||
|
|
||||||
### Notation
|
### Notation
|
||||||
|
|
||||||
@@ -17,61 +17,62 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
|
|||||||
### Reusable types
|
### Reusable types
|
||||||
|
|
||||||
- `T1` = `{ order: T2 }`
|
- `T1` = `{ order: T2 }`
|
||||||
- `T2` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T29[], externalNumber?: str|null, createdAt?: str, shipping?: T37|null, shippings?: T37[], payment?: T43|null, payments?: T43[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T5[], files?: T15[] }`
|
- `T2` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T30[], externalNumber?: str|null, createdAt?: str, shipping?: T38|null, shippings?: T38[], payment?: T44|null, payments?: T44[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T6[], files?: T16[] }`
|
||||||
- `T3` = `{ article: T4 }`
|
- `T3` = `{ article: T4 }`
|
||||||
- `T4` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T40, brand: T38|null, categoryIds: int[], photos: T35[], eans: str[], prices: T47[], suppliers: T17[] }`
|
- `T4` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T41, brand: T39|null, categoryIds: int[], photos: T36[], eans: str[], prices: T48[], suppliers: T18[] }`
|
||||||
- `T5` = `{ id: int, number: str, type: str, date: str, lines: T29[], shippings: T37[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T15[] }`
|
- `T5` = `{ shipment: T6 }`
|
||||||
- `T6` = `{ quote: T7 }`
|
- `T6` = `{ id: int, number: str, type: str, date: str, lines: T30[], shippings: T38[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T16[], state?: str }`
|
||||||
- `T7` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T29[], externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T15[] }`
|
- `T7` = `{ quote: T8 }`
|
||||||
- `T8` = `{ delivery: T9 }`
|
- `T8` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T30[], externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T16[] }`
|
||||||
- `T9` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T29[], files: T15[], externalNumber?: str|null, createdAt?: str, ledger?: T51 }`
|
- `T9` = `{ delivery: T10 }`
|
||||||
- `T10` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T11|null }`
|
- `T10` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T30[], files: T16[], externalNumber?: str|null, createdAt?: str, ledger?: T52 }`
|
||||||
- `T11` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T18, totalSizeBytes: int, durationMs: int }`
|
- `T11` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T12|null }`
|
||||||
- `T12` = `{ labeltemplate: T14 }`
|
- `T12` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T19, totalSizeBytes: int, durationMs: int }`
|
||||||
- `T13` = `{ attachment: T15, fileUrl: str }`
|
- `T13` = `{ labeltemplate: T15 }`
|
||||||
- `T14` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: T25, thumb?: str|null }`
|
- `T14` = `{ attachment: T16, fileUrl: str }`
|
||||||
- `T15` = `{ 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 }`
|
- `T15` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: T26, thumb?: str|null }`
|
||||||
- `T16` = `{ 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[] }`
|
- `T16` = `{ 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 }`
|
||||||
- `T17` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T54|null }`
|
- `T17` = `{ 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[] }`
|
||||||
- `T18` = `{ vfs: T52, storage: T52, database: T45 }`
|
- `T18` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T55|null }`
|
||||||
- `T19` = `{ 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 }`
|
- `T19` = `{ vfs: T53, storage: T53, database: T46 }`
|
||||||
- `T20` = `{ ledger: T21[] }`
|
- `T20` = `{ 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 }`
|
||||||
- `T21` = `{ 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 }`
|
- `T21` = `{ ledger: T22[] }`
|
||||||
- `T22` = `{ id: int, name: str, sort: int, exclusive: bool, values: T41[], articleCount?: int }`
|
- `T22` = `{ 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 }`
|
||||||
- `T23` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str, started_by?: integer,null, first_message?: string,null }`
|
- `T23` = `{ id: int, name: str, sort: int, exclusive: bool, values: T42[], articleCount?: int }`
|
||||||
- `T24` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num }`
|
- `T24` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str, started_by?: integer,null, first_message?: string,null }`
|
||||||
- `T25` = `{ objects: T30[] }`
|
- `T25` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num }`
|
||||||
- `T26` = `{ objects: T31[] }`
|
- `T26` = `{ objects: T31[] }`
|
||||||
- `T27` = `{ printer: T33 }`
|
- `T27` = `{ objects: T32[] }`
|
||||||
- `T28` = `{ entity: T36 }`
|
- `T28` = `{ printer: T34 }`
|
||||||
- `T29` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }`
|
- `T29` = `{ entity: T37 }`
|
||||||
- `T30` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
|
- `T30` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }`
|
||||||
- `T31` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
|
- `T31` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
|
||||||
- `T32` = `{ shipping: T37 }`
|
- `T32` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
|
||||||
- `T33` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }`
|
- `T33` = `{ shipping: T38 }`
|
||||||
- `T34` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }`
|
- `T34` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }`
|
||||||
- `T35` = `{ sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }`
|
- `T35` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }`
|
||||||
- `T36` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }`
|
- `T36` = `{ sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }`
|
||||||
- `T37` = `{ id: int, manifestId: int, number: str, type: str, state: str, trackingCode?: str|null, carrier?: str|null }`
|
- `T37` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }`
|
||||||
- `T38` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }`
|
- `T38` = `{ id: int, manifestId: int, number: str, type: str, state: str, trackingCode?: str|null, carrier?: str|null }`
|
||||||
- `T39` = `{ article: T44 }`
|
- `T39` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }`
|
||||||
- `T40` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
|
- `T40` = `{ article: T45 }`
|
||||||
- `T41` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }`
|
- `T41` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
|
||||||
- `T42` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }`
|
- `T42` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }`
|
||||||
- `T43` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }`
|
- `T43` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }`
|
||||||
- `T44` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }`
|
- `T44` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }`
|
||||||
- `T45` = `{ name: str|null, sizeBytes: int, backedUp: bool, reason?: str|null }`
|
- `T45` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }`
|
||||||
- `T46` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }`
|
- `T46` = `{ name: str|null, sizeBytes: int, backedUp: bool, reason?: str|null }`
|
||||||
- `T47` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }`
|
- `T47` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }`
|
||||||
- `T48` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
|
- `T48` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }`
|
||||||
- `T49` = `{ id: int, entityId: int, contactType: str, contact: str }`
|
- `T49` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
|
||||||
- `T50` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
|
- `T50` = `{ id: int, entityId: int, contactType: str, contact: str }`
|
||||||
- `T51` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
|
- `T51` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
|
||||||
- `T52` = `{ name: str, sizeBytes: int, sourceExists: bool }`
|
- `T52` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
|
||||||
- `T53` = `{ warehouse: T55 }`
|
- `T53` = `{ name: str, sizeBytes: int, sourceExists: bool }`
|
||||||
- `T54` = `{ net: num, taxId: int, taxRate?: num|null }`
|
- `T54` = `{ warehouse: T56 }`
|
||||||
- `T55` = `{ id: int, name: str, sort: int }`
|
- `T55` = `{ net: num, taxId: int, taxRate?: num|null }`
|
||||||
- `T56` = `{ id: int, categoryIds: int[] }`
|
- `T56` = `{ id: int, name: str, sort: int }`
|
||||||
|
- `T57` = `{ id: int, categoryIds: int[] }`
|
||||||
|
|
||||||
### Routes
|
### Routes
|
||||||
|
|
||||||
@@ -101,7 +102,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 T39`
|
- Returns: `201 T40`
|
||||||
#### 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`
|
||||||
@@ -137,10 +138,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 T39`
|
- Returns: `201 T40`
|
||||||
#### 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: T44, children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }`
|
- Returns: `200 { article: T45, 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 }`
|
||||||
@@ -167,7 +168,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 T39`
|
- Returns: `200 T40`
|
||||||
#### 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 T3`
|
- Returns: `200 T3`
|
||||||
@@ -185,10 +186,10 @@ _none_
|
|||||||
- Returns: `200 { ok: bool }`
|
- Returns: `200 { ok: bool }`
|
||||||
#### List article suppliers — `GET /article/{sku}/supplier`
|
#### List article suppliers — `GET /article/{sku}/supplier`
|
||||||
- Params: `sku: str`
|
- Params: `sku: str`
|
||||||
- Returns: `200 { suppliers: T17[] }`
|
- Returns: `200 { suppliers: T18[] }`
|
||||||
#### Assign supplier to article — `POST /article/{sku}/supplier`
|
#### Assign supplier to article — `POST /article/{sku}/supplier`
|
||||||
- Params: `sku: str, body: { entityId: int, sku?: str|null, pu?: num, stock?: num|null, sort?: int|null, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
|
- Params: `sku: str, body: { entityId: int, sku?: str|null, pu?: num, stock?: num|null, sort?: int|null, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
|
||||||
- Returns: `201 { supplier: T17 }`
|
- Returns: `201 { supplier: T18 }`
|
||||||
#### Reorder article suppliers — `PUT /article/{sku}/supplier/order`
|
#### Reorder article suppliers — `PUT /article/{sku}/supplier/order`
|
||||||
- Params: `sku: str, body: { ids: int[] }`
|
- Params: `sku: str, body: { ids: int[] }`
|
||||||
- Returns: `200 { ids: int[] }`
|
- Returns: `200 { ids: int[] }`
|
||||||
@@ -197,7 +198,7 @@ _none_
|
|||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Update article supplier assignment — `PATCH /article/{sku}/supplier/{id}`
|
#### Update article supplier assignment — `PATCH /article/{sku}/supplier/{id}`
|
||||||
- Params: `sku: str, id: int, body: { sku?: str|null, pu?: num, stock?: num|null, sort?: int, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
|
- Params: `sku: str, id: int, body: { sku?: str|null, pu?: num, stock?: num|null, sort?: int, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
|
||||||
- Returns: `200 { supplier: T17 }`
|
- Returns: `200 { supplier: T18 }`
|
||||||
#### Set article track_stock flag by SKU — `PUT /article/{sku}/track_stock`
|
#### Set article track_stock flag by SKU — `PUT /article/{sku}/track_stock`
|
||||||
- Params: `sku: str, body: { track_stock: bool }`
|
- Params: `sku: str, body: { track_stock: bool }`
|
||||||
- Returns: `200 T3`
|
- Returns: `200 T3`
|
||||||
@@ -209,19 +210,19 @@ _none_
|
|||||||
|
|
||||||
#### List attribute names — `GET /attributename`
|
#### List attribute names — `GET /attributename`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { attributenames: T22[] }`
|
- Returns: `200 { attributenames: T23[] }`
|
||||||
#### Create attribute name — `POST /attributename`
|
#### Create attribute name — `POST /attributename`
|
||||||
- Params: `body: { name: str, sort?: int, exclusive?: bool }`
|
- Params: `body: { name: str, sort?: int, exclusive?: bool }`
|
||||||
- Returns: `201 { attributename: T22 }`
|
- Returns: `201 { attributename: T23 }`
|
||||||
#### Reorder attribute names — `PUT /attributename/order`
|
#### Reorder attribute names — `PUT /attributename/order`
|
||||||
- Params: `body: { attributenameIds: int[] }`
|
- Params: `body: { attributenameIds: int[] }`
|
||||||
- Returns: `200 { ok: bool }`
|
- Returns: `200 { ok: bool }`
|
||||||
#### List values for an attribute — `GET /attributename/{attributeId}/value`
|
#### List values for an attribute — `GET /attributename/{attributeId}/value`
|
||||||
- Params: `attributeId: int`
|
- Params: `attributeId: int`
|
||||||
- Returns: `200 { values: T41[] }`
|
- Returns: `200 { values: T42[] }`
|
||||||
#### Create value for an attribute — `POST /attributename/{attributeId}/value`
|
#### Create value for an attribute — `POST /attributename/{attributeId}/value`
|
||||||
- Params: `attributeId: int, body: { value: str, sort?: int }`
|
- Params: `attributeId: int, body: { value: str, sort?: int }`
|
||||||
- Returns: `201 { attributevalue: T41 }`
|
- Returns: `201 { attributevalue: T42 }`
|
||||||
#### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order`
|
#### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order`
|
||||||
- Params: `attributeId: int, body: { attributevalueIds: int[] }`
|
- Params: `attributeId: int, body: { attributevalueIds: int[] }`
|
||||||
- Returns: `200 { ok: bool }`
|
- Returns: `200 { ok: bool }`
|
||||||
@@ -230,7 +231,7 @@ _none_
|
|||||||
- Returns: `200 { ok: bool }`
|
- Returns: `200 { ok: bool }`
|
||||||
#### Update attribute name — `PATCH /attributename/{id}`
|
#### Update attribute name — `PATCH /attributename/{id}`
|
||||||
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
|
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
|
||||||
- Returns: `200 { attributename: T22 }`
|
- Returns: `200 { attributename: T23 }`
|
||||||
|
|
||||||
### Section: /attributevalue
|
### Section: /attributevalue
|
||||||
|
|
||||||
@@ -239,34 +240,34 @@ _none_
|
|||||||
- Returns: `200 { ok: bool }`
|
- Returns: `200 { ok: bool }`
|
||||||
#### Update attribute value — `PATCH /attributevalue/{id}`
|
#### Update attribute value — `PATCH /attributevalue/{id}`
|
||||||
- Params: `id: int, body: { value?: str, sort?: int }`
|
- Params: `id: int, body: { value?: str, sort?: int }`
|
||||||
- Returns: `200 { attributevalue: T41 }`
|
- Returns: `200 { attributevalue: T42 }`
|
||||||
#### 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: T48[] } }`
|
- Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T49[] } }`
|
||||||
#### 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: T48[] } }`
|
- Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T49[] } }`
|
||||||
|
|
||||||
### 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: T46 }`
|
- Returns: `200 { message: str, user: T47 }`
|
||||||
#### 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: T46, googleClientId: str }`
|
- Returns: `200 { user: T47, 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: T46 }`
|
- Returns: `200 { user: T47 }`
|
||||||
|
|
||||||
### Section: /backup
|
### Section: /backup
|
||||||
|
|
||||||
#### List backups and service status — `GET /backup`
|
#### List backups and service status — `GET /backup`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { status: { enabled: bool, isRunning: bool, dbProvider: str, mssqlWarning: bool, backupDir: str, retention: { hourlyMax: int, dailyMax: int, monthly: str }, counts: { hourly: int, daily: int, monthly: int }, latestHourly?: T10|null, latestDaily?: T10|null, latestMonthly?: T10|null }, backups: { counts: { hourly: int, daily: int, monthly: int }, hourly: T10[], daily: T10[], monthly: T10[] } }`
|
- Returns: `200 { status: { enabled: bool, isRunning: bool, dbProvider: str, mssqlWarning: bool, backupDir: str, retention: { hourlyMax: int, dailyMax: int, monthly: str }, counts: { hourly: int, daily: int, monthly: int }, latestHourly?: T11|null, latestDaily?: T11|null, latestMonthly?: T11|null }, backups: { counts: { hourly: int, daily: int, monthly: int }, hourly: T11[], daily: T11[], monthly: T11[] } }`
|
||||||
#### List VFS files in a backup — `GET /backup/{id}/vfs`
|
#### List VFS files in a backup — `GET /backup/{id}/vfs`
|
||||||
- Params: `id: str, query: companyId?: int`
|
- Params: `id: str, query: companyId?: int`
|
||||||
- Returns: `200 { backupId: str, companyId: int, files: { name: str, size: int, mime: str, mtime: str }[] }`
|
- Returns: `200 { backupId: str, companyId: int, files: { name: str, size: int, mime: str, mtime: str }[] }`
|
||||||
@@ -281,7 +282,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: T48[], gsprpinfo?: str|null } }`
|
- Returns: `201 { brand: { id: int, name: str, pics: T49[], 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`
|
||||||
@@ -290,10 +291,10 @@ _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: T48[] } }`
|
- Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T49[] } }`
|
||||||
#### 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: T48[] } }`
|
- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T49[] } }`
|
||||||
|
|
||||||
### Section: /category
|
### Section: /category
|
||||||
|
|
||||||
@@ -317,7 +318,7 @@ _none_
|
|||||||
- Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }`
|
- Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }`
|
||||||
#### Articles in category — `GET /category/{id}/articles`
|
#### Articles in category — `GET /category/{id}/articles`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, brand?: T38|null }[] }`
|
- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, brand?: T39|null }[] }`
|
||||||
#### List category attribute values — `GET /category/{id}/attributevalue`
|
#### List category attribute values — `GET /category/{id}/attributevalue`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 { attributevalues: { id: int, attributeId: int, attributeName: str, attributeValueId: int, value: str, sort: int, categoryId?: int, exclusive?: bool }[] }`
|
- Returns: `200 { attributevalues: { id: int, attributeId: int, attributeName: str, attributeValueId: int, value: str, sort: int, categoryId?: int, exclusive?: bool }[] }`
|
||||||
@@ -344,10 +345,10 @@ _none_
|
|||||||
|
|
||||||
#### List companies — `GET /company`
|
#### List companies — `GET /company`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { companies: T24[] }`
|
- Returns: `200 { companies: T25[] }`
|
||||||
#### 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: T24, user: T46 }`
|
- Returns: `201 { company: T25, user: T47 }`
|
||||||
#### 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`
|
||||||
@@ -377,7 +378,7 @@ _none_
|
|||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Update company — `PUT /company/{id}`
|
#### Update company — `PUT /company/{id}`
|
||||||
- Params: `id: int, body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
|
- Params: `id: int, body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
|
||||||
- Returns: `200 { company: T24 }`
|
- Returns: `200 { company: T25 }`
|
||||||
|
|
||||||
### Section: /customergroup
|
### Section: /customergroup
|
||||||
|
|
||||||
@@ -386,7 +387,7 @@ _none_
|
|||||||
- Returns: `200 { customergroups: { id: int, name: str, sort: int, editable: bool }[] }`
|
- Returns: `200 { customergroups: { id: int, name: str, sort: int, editable: bool }[] }`
|
||||||
#### Create customer group — `POST /customergroup`
|
#### Create customer group — `POST /customergroup`
|
||||||
- Params: `body: { name: str }`
|
- Params: `body: { name: str }`
|
||||||
- Returns: `201 { customergroup: T55 }`
|
- Returns: `201 { customergroup: T56 }`
|
||||||
#### Reorder customer groups — `PUT /customergroup/order`
|
#### Reorder customer groups — `PUT /customergroup/order`
|
||||||
- Params: `body: { customergroupIds: int[] }`
|
- Params: `body: { customergroupIds: int[] }`
|
||||||
- Returns: `200 { customergroupIds: int[] }`
|
- Returns: `200 { customergroupIds: int[] }`
|
||||||
@@ -395,16 +396,16 @@ _none_
|
|||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Update customer group — `PATCH /customergroup/{id}`
|
#### Update customer group — `PATCH /customergroup/{id}`
|
||||||
- Params: `id: int, body: { name: str }`
|
- Params: `id: int, body: { name: str }`
|
||||||
- Returns: `200 { customergroup: T55 }`
|
- Returns: `200 { customergroup: T56 }`
|
||||||
|
|
||||||
### Section: /delivery
|
### Section: /delivery
|
||||||
|
|
||||||
#### List deliveries — `GET /delivery`
|
#### List deliveries — `GET /delivery`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T51 }[] }`
|
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T52 }[] }`
|
||||||
#### Create delivery — `POST /delivery`
|
#### Create delivery — `POST /delivery`
|
||||||
- Params: `body: { externalNumber: str, date: str, lines: T34[], 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: T35[], 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 T8`
|
- Returns: `201 T9`
|
||||||
#### Parse document (SSE) — `POST /delivery/parse`
|
#### Parse document (SSE) — `POST /delivery/parse`
|
||||||
- Params: `form: { file: str }`
|
- Params: `form: { file: str }`
|
||||||
- Returns: `200 SSE`
|
- Returns: `200 SSE`
|
||||||
@@ -422,10 +423,10 @@ _none_
|
|||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Get delivery — `GET /delivery/{id}`
|
#### Get delivery — `GET /delivery/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 T8`
|
- Returns: `200 T9`
|
||||||
#### Update delivery — `PATCH /delivery/{id}`
|
#### Update delivery — `PATCH /delivery/{id}`
|
||||||
- Params: `id: int, body: { externalNumber?: str|null, date?: str, supplierId?: int|null, lines?: T34[], 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?: T35[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
|
||||||
- Returns: `200 T8`
|
- Returns: `200 T9`
|
||||||
#### 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`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
@@ -434,52 +435,52 @@ _none_
|
|||||||
- Returns: `200 str`
|
- Returns: `200 str`
|
||||||
#### Generate and attach delivery note PDF (goods receipt) — `POST /delivery/{id}/pdf`
|
#### Generate and attach delivery note PDF (goods receipt) — `POST /delivery/{id}/pdf`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `201 T13`
|
- Returns: `201 T14`
|
||||||
|
|
||||||
### Section: /entity
|
### Section: /entity
|
||||||
|
|
||||||
#### List entities — `GET /entity`
|
#### List entities — `GET /entity`
|
||||||
- Params: `query: customergroupId?: int, role?: str`
|
- Params: `query: customergroupId?: int, role?: str`
|
||||||
- Returns: `200 { entities: T36[] }`
|
- Returns: `200 { entities: T37[] }`
|
||||||
#### 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 T28`
|
- Returns: `201 T29`
|
||||||
#### 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 }`
|
||||||
#### List entity addresses — `GET /entity/{entityId}/address`
|
#### List entity addresses — `GET /entity/{entityId}/address`
|
||||||
- Params: `entityId: int`
|
- Params: `entityId: int`
|
||||||
- Returns: `200 { addresses: T19[] }`
|
- Returns: `200 { addresses: T20[] }`
|
||||||
#### Create entity address — `POST /entity/{entityId}/address`
|
#### Create entity address — `POST /entity/{entityId}/address`
|
||||||
- Params: `entityId: int, body: { addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
|
- Params: `entityId: int, body: { addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
|
||||||
- Returns: `201 { address: T19 }`
|
- Returns: `201 { address: T20 }`
|
||||||
#### Delete entity address — `DELETE /entity/{entityId}/address/{id}`
|
#### Delete entity address — `DELETE /entity/{entityId}/address/{id}`
|
||||||
- Params: `entityId: int, id: int`
|
- Params: `entityId: int, id: int`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Update entity address — `PATCH /entity/{entityId}/address/{id}`
|
#### Update entity address — `PATCH /entity/{entityId}/address/{id}`
|
||||||
- Params: `entityId: int, id: int, body: { addressType?: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
|
- Params: `entityId: int, id: int, body: { addressType?: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
|
||||||
- Returns: `200 { address: T19 }`
|
- Returns: `200 { address: T20 }`
|
||||||
#### List entity contacts — `GET /entity/{entityId}/contact`
|
#### List entity contacts — `GET /entity/{entityId}/contact`
|
||||||
- Params: `entityId: int`
|
- Params: `entityId: int`
|
||||||
- Returns: `200 { contacts: T49[] }`
|
- Returns: `200 { contacts: T50[] }`
|
||||||
#### 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: T49 }`
|
- Returns: `201 { contact: T50 }`
|
||||||
#### 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: T49 }`
|
- Returns: `200 { contact: T50 }`
|
||||||
#### 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 T28`
|
- Returns: `200 T29`
|
||||||
#### 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 T28`
|
- Returns: `200 T29`
|
||||||
|
|
||||||
### Section: /events
|
### Section: /events
|
||||||
|
|
||||||
@@ -523,35 +524,35 @@ _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: T26 }`
|
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: T27 }`
|
||||||
- Returns: `201 T12`
|
- Returns: `201 T13`
|
||||||
#### Delete label template — `DELETE /labeltemplate/{id}`
|
#### Delete label template — `DELETE /labeltemplate/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Get label template by ID — `GET /labeltemplate/{id}`
|
#### Get label template by ID — `GET /labeltemplate/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 T12`
|
- Returns: `200 T13`
|
||||||
#### 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?: T26 }`
|
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T27 }`
|
||||||
- Returns: `200 T12`
|
- Returns: `200 T13`
|
||||||
#### 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?: T26 }`
|
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T27 }`
|
||||||
- Returns: `200 T12`
|
- Returns: `200 T13`
|
||||||
|
|
||||||
### Section: /ledger
|
### Section: /ledger
|
||||||
|
|
||||||
#### Query ledger by manifest article ID or SKU — `GET /ledger`
|
#### Query ledger by manifest article ID or SKU — `GET /ledger`
|
||||||
- Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str`
|
- Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str`
|
||||||
- Returns: `200 T20`
|
- Returns: `200 T21`
|
||||||
#### Add ledger entry — `POST /ledger`
|
#### Add ledger entry — `POST /ledger`
|
||||||
- Params: `body: { manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str }`
|
- Params: `body: { manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str }`
|
||||||
- Returns: `201 { ledger: T21 }`
|
- Returns: `201 { ledger: T22 }`
|
||||||
#### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}`
|
#### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}`
|
||||||
- Params: `manifestarticleId: int, query: transaction_type?: str`
|
- Params: `manifestarticleId: int, query: transaction_type?: str`
|
||||||
- Returns: `200 T20`
|
- Returns: `200 T21`
|
||||||
#### Query ledger by article SKU — `GET /ledger/sku/{sku}`
|
#### Query ledger by article SKU — `GET /ledger/sku/{sku}`
|
||||||
- Params: `sku: str, query: transaction_type?: str`
|
- Params: `sku: str, query: transaction_type?: str`
|
||||||
- Returns: `200 T20`
|
- Returns: `200 T21`
|
||||||
|
|
||||||
### Section: /mcp
|
### Section: /mcp
|
||||||
|
|
||||||
@@ -581,16 +582,16 @@ _none_
|
|||||||
|
|
||||||
#### List conversations — `GET /modelconversation`
|
#### List conversations — `GET /modelconversation`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { conversations: T23[] }`
|
- Returns: `200 { conversations: T24[] }`
|
||||||
#### Create conversation — `POST /modelconversation`
|
#### Create conversation — `POST /modelconversation`
|
||||||
- Params: `body: { provider: str, model: str }`
|
- Params: `body: { provider: str, model: str }`
|
||||||
- Returns: `201 { conversation: T23 }`
|
- Returns: `201 { conversation: T24 }`
|
||||||
#### Delete conversation — `DELETE /modelconversation/{id}`
|
#### Delete conversation — `DELETE /modelconversation/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Get conversation — `GET /modelconversation/{id}`
|
#### Get conversation — `GET /modelconversation/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 { conversation: T23 }`
|
- Returns: `200 { conversation: T24 }`
|
||||||
#### List conversation messages — `GET /modelconversation/{id}/message`
|
#### List conversation messages — `GET /modelconversation/{id}/message`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 { messages: { id: int, conversation_id: int, created_at: str, role: str, message: str }[] }`
|
- Returns: `200 { messages: { id: int, conversation_id: int, created_at: str, role: str, message: str }[] }`
|
||||||
@@ -631,7 +632,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: T2[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
- Returns: `200 { orders: T2[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
||||||
#### Create order — `POST /order`
|
#### Create order — `POST /order`
|
||||||
- Params: `body: { number: str, date: str, lines: T34[], entityId?: int|null, externalNumber?: str|null }`
|
- Params: `body: { number: str, date: str, lines: T35[], entityId?: int|null, externalNumber?: str|null }`
|
||||||
- Returns: `201 T1`
|
- Returns: `201 T1`
|
||||||
#### Delete order — `DELETE /order/{id}`
|
#### Delete order — `DELETE /order/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
@@ -640,7 +641,7 @@ _none_
|
|||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 T1`
|
- Returns: `200 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?: T34[] }`
|
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T35[] }`
|
||||||
- Returns: `200 T1`
|
- Returns: `200 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`
|
||||||
@@ -650,25 +651,25 @@ _none_
|
|||||||
- Returns: `200 str`
|
- Returns: `200 str`
|
||||||
#### List payments of an order — `GET /order/{id}/payment`
|
#### List payments of an order — `GET /order/{id}/payment`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 { payments: T43[] }`
|
- Returns: `200 { payments: T44[] }`
|
||||||
#### Add payment to an order — `POST /order/{id}/payment`
|
#### Add payment to an order — `POST /order/{id}/payment`
|
||||||
- Params: `id: int, body: { accountId: int, type: str, amount: num }`
|
- Params: `id: int, body: { accountId: int, type: str, amount: num }`
|
||||||
- Returns: `201 { payment: T43 }`
|
- Returns: `201 { payment: T44 }`
|
||||||
#### Generate and attach order PDF — `POST /order/{id}/pdf`
|
#### Generate and attach order PDF — `POST /order/{id}/pdf`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `201 T13`
|
- Returns: `201 T14`
|
||||||
#### List shipments of an order — `GET /order/{id}/shipment`
|
#### List shipments of an order — `GET /order/{id}/shipment`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 { shipments: T5[] }`
|
- Returns: `200 { shipments: T6[] }`
|
||||||
#### Create shipment for an order — `POST /order/{id}/shipment`
|
#### Create shipment for an order — `POST /order/{id}/shipment`
|
||||||
- Params: `id: int, body: { lines: { sku: str, amount: num, name?: str, pu?: num, net?: num, rebate?: num, taxrate?: num, comment?: str }[], date?: str, externalNumber?: str }`
|
- Params: `id: int, body?: { date?: str, externalNumber?: str, lines?: { sku: str, amount?: num, name?: str, pu?: num, net?: num, rebate?: num, taxrate?: num, comment?: str }[] }`
|
||||||
- Returns: `201 { shipment: T5 }`
|
- Returns: `201 T5`
|
||||||
#### 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: T37[] }`
|
- Returns: `200 { shippings: T38[] }`
|
||||||
#### 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: { type: str, state: str, warehouseId?: int }`
|
||||||
- Returns: `201 T32`
|
- Returns: `201 T33`
|
||||||
|
|
||||||
### Section: /package
|
### Section: /package
|
||||||
|
|
||||||
@@ -686,37 +687,37 @@ _none_
|
|||||||
|
|
||||||
#### List POS terminals — `GET /posterminal`
|
#### List POS terminals — `GET /posterminal`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { posterminals: T16[] }`
|
- Returns: `200 { posterminals: T17[] }`
|
||||||
#### Create POS terminal — `PUT /posterminal`
|
#### Create POS terminal — `PUT /posterminal`
|
||||||
- Params: `body: { name: str, rootCategoryId?: int|null, syncRootArticles?: bool }`
|
- Params: `body: { name: str, rootCategoryId?: int|null, syncRootArticles?: bool }`
|
||||||
- Returns: `200 { posterminal: T16 }`
|
- Returns: `200 { posterminal: T17 }`
|
||||||
#### Delete POS terminal — `DELETE /posterminal/{id}`
|
#### Delete POS terminal — `DELETE /posterminal/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Update POS terminal — `PATCH /posterminal/{id}`
|
#### Update POS terminal — `PATCH /posterminal/{id}`
|
||||||
- Params: `id: int, body: { name?: str, rootCategoryId?: int|null, syncRootArticles?: bool, pairingpin?: str|null }`
|
- Params: `id: int, body: { name?: str, rootCategoryId?: int|null, syncRootArticles?: bool, pairingpin?: str|null }`
|
||||||
- Returns: `200 { posterminal: T16 }`
|
- Returns: `200 { posterminal: T17 }`
|
||||||
#### Disable all categories on terminal — `DELETE /posterminal/{id}/all`
|
#### Disable all categories on terminal — `DELETE /posterminal/{id}/all`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 T56`
|
- Returns: `200 T57`
|
||||||
#### Enable all categories on terminal — `PUT /posterminal/{id}/all`
|
#### Enable all categories on terminal — `PUT /posterminal/{id}/all`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 T56`
|
- Returns: `200 T57`
|
||||||
#### Disable category on terminal — `DELETE /posterminal/{id}/category/{categoryId}`
|
#### Disable category on terminal — `DELETE /posterminal/{id}/category/{categoryId}`
|
||||||
- Params: `id: int, categoryId: int`
|
- Params: `id: int, categoryId: int`
|
||||||
- Returns: `200 T56`
|
- Returns: `200 T57`
|
||||||
#### Enable category on terminal — `PUT /posterminal/{id}/category/{categoryId}`
|
#### Enable category on terminal — `PUT /posterminal/{id}/category/{categoryId}`
|
||||||
- Params: `id: int, categoryId: int`
|
- Params: `id: int, categoryId: int`
|
||||||
- Returns: `200 T56`
|
- Returns: `200 T57`
|
||||||
|
|
||||||
### Section: /printer
|
### Section: /printer
|
||||||
|
|
||||||
#### List printers — `GET /printer`
|
#### List printers — `GET /printer`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { printers: T33[] }`
|
- Returns: `200 { printers: T34[] }`
|
||||||
#### 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 T27`
|
- Returns: `201 T28`
|
||||||
#### 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[] }`
|
||||||
@@ -725,22 +726,22 @@ _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 T27`
|
- Returns: `200 T28`
|
||||||
#### 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 T27`
|
- Returns: `200 T28`
|
||||||
#### 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 T27`
|
- Returns: `200 T28`
|
||||||
|
|
||||||
### Section: /propertyname
|
### Section: /propertyname
|
||||||
|
|
||||||
#### List property names — `GET /propertyname`
|
#### List property names — `GET /propertyname`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { propertynames: T42[] }`
|
- Returns: `200 { propertynames: T43[] }`
|
||||||
#### Create property name — `POST /propertyname`
|
#### Create property name — `POST /propertyname`
|
||||||
- Params: `body: { name: str, sort?: int, exclusive?: bool }`
|
- Params: `body: { name: str, sort?: int, exclusive?: bool }`
|
||||||
- Returns: `201 { propertyname: T42 }`
|
- Returns: `201 { propertyname: T43 }`
|
||||||
#### Reorder property names — `PUT /propertyname/order`
|
#### Reorder property names — `PUT /propertyname/order`
|
||||||
- Params: `body: { propertynameIds: int[] }`
|
- Params: `body: { propertynameIds: int[] }`
|
||||||
- Returns: `200 { ok: bool }`
|
- Returns: `200 { ok: bool }`
|
||||||
@@ -749,7 +750,7 @@ _none_
|
|||||||
- Returns: `200 { ok: bool }`
|
- Returns: `200 { ok: bool }`
|
||||||
#### Update property name — `PATCH /propertyname/{id}`
|
#### Update property name — `PATCH /propertyname/{id}`
|
||||||
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
|
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
|
||||||
- Returns: `200 { propertyname: T42 }`
|
- Returns: `200 { propertyname: T43 }`
|
||||||
|
|
||||||
### Section: /public
|
### Section: /public
|
||||||
|
|
||||||
@@ -770,19 +771,19 @@ _none_
|
|||||||
|
|
||||||
#### List quotes — `GET /quote`
|
#### List quotes — `GET /quote`
|
||||||
- Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int`
|
- Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int`
|
||||||
- Returns: `200 { quotes: T7[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
- Returns: `200 { quotes: T8[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
||||||
#### Create quote — `POST /quote`
|
#### Create quote — `POST /quote`
|
||||||
- Params: `body: { number: str, date: str, lines: T34[], entityId?: int|null, externalNumber?: str|null }`
|
- Params: `body: { number: str, date: str, lines: T35[], entityId?: int|null, externalNumber?: str|null }`
|
||||||
- Returns: `201 T6`
|
- Returns: `201 T7`
|
||||||
#### Delete quote — `DELETE /quote/{id}`
|
#### Delete quote — `DELETE /quote/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Get quote — `GET /quote/{id}`
|
#### Get quote — `GET /quote/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 T6`
|
- Returns: `200 T7`
|
||||||
#### 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?: T34[] }`
|
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T35[] }`
|
||||||
- Returns: `200 T6`
|
- Returns: `200 T7`
|
||||||
#### 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`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
@@ -790,11 +791,11 @@ _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?: T34[] }`
|
- Params: `id: int, body?: { number?: str, date?: str, createdAt?: str, entityId?: int|null, externalNumber?: str|null, lines?: T35[] }`
|
||||||
- Returns: `201 T1`
|
- Returns: `201 T1`
|
||||||
#### Generate and attach quote PDF — `POST /quote/{id}/pdf`
|
#### Generate and attach quote PDF — `POST /quote/{id}/pdf`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `201 T13`
|
- Returns: `201 T14`
|
||||||
|
|
||||||
### Section: /serper
|
### Section: /serper
|
||||||
|
|
||||||
@@ -813,12 +814,18 @@ _none_
|
|||||||
|
|
||||||
### Section: /shipment
|
### Section: /shipment
|
||||||
|
|
||||||
|
#### List shipments — `GET /shipment`
|
||||||
|
- Params: `query: offset?: int, limit?: int, search?: str, state?: str, seekMonths?: int`
|
||||||
|
- Returns: `200 { shipments: { id: int, number: str, type: str, date: str, positions: int, totalQuantity: num, packageCount: int, state: str, entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, orderId?: int|null, orderNumber?: str|null, customerName?: str|null, carrier?: str|null, trackingCode?: str|null }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
||||||
#### Delete shipment — `DELETE /shipment/{id}`
|
#### Delete shipment — `DELETE /shipment/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Get shipment by ID — `GET /shipment/{id}`
|
#### Get shipment by ID — `GET /shipment/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 { shipment: T5 }`
|
- Returns: `200 T5`
|
||||||
|
#### Update shipment state or metadata — `PATCH /shipment/{id}`
|
||||||
|
- Params: `id: int, body: { state?: str }`
|
||||||
|
- Returns: `200 T5`
|
||||||
#### Delete shipment file attachment — `DELETE /shipment/{id}/file/{fileId}`
|
#### Delete shipment file attachment — `DELETE /shipment/{id}/file/{fileId}`
|
||||||
- Params: `id: int, fileId: int`
|
- Params: `id: int, fileId: int`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
@@ -827,13 +834,13 @@ _none_
|
|||||||
- Returns: `200 str`
|
- Returns: `200 str`
|
||||||
#### Generate and attach shipment PDF (delivery note) — `POST /shipment/{id}/pdf`
|
#### Generate and attach shipment PDF (delivery note) — `POST /shipment/{id}/pdf`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `201 T13`
|
- Returns: `201 T14`
|
||||||
#### 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: T37[] }`
|
- Returns: `200 { shippings: T38[] }`
|
||||||
#### 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: { type: str, state: str, trackingCode?: str, carrier?: str, warehouseId?: int }`
|
||||||
- Returns: `201 T32`
|
- Returns: `201 T33`
|
||||||
|
|
||||||
### Section: /shipping
|
### Section: /shipping
|
||||||
|
|
||||||
@@ -842,7 +849,7 @@ _none_
|
|||||||
- 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, carrier?: str, warehouseId?: int }`
|
||||||
- Returns: `200 T32`
|
- Returns: `200 T33`
|
||||||
#### 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 }[] }`
|
||||||
@@ -860,28 +867,28 @@ _none_
|
|||||||
|
|
||||||
#### Adjust stock manually — `POST /stock/adjust`
|
#### Adjust stock manually — `POST /stock/adjust`
|
||||||
- Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }`
|
- Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }`
|
||||||
- Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T21[] }`
|
- Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T22[] }`
|
||||||
|
|
||||||
### Section: /supplier
|
### Section: /supplier
|
||||||
|
|
||||||
#### List suppliers — `GET /supplier`
|
#### List suppliers — `GET /supplier`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { suppliers: T50[] }`
|
- Returns: `200 { suppliers: T51[] }`
|
||||||
#### Create supplier — `POST /supplier`
|
#### Create supplier — `POST /supplier`
|
||||||
- Params: `body: { name: str }`
|
- Params: `body: { name: str }`
|
||||||
- Returns: `201 { supplier: T50 }`
|
- Returns: `201 { supplier: T51 }`
|
||||||
#### 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: T50 }`
|
- Returns: `200 { supplier: T51 }`
|
||||||
#### List supplier articles by SKUs — `GET /supplier/{id}/articles`
|
#### List supplier articles by SKUs — `GET /supplier/{id}/articles`
|
||||||
- Params: `id: int, query: skus?: string`
|
- Params: `id: int, query: skus?: string`
|
||||||
- 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?: T54|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?: T55|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?: T54|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?: T55|null }[] }`
|
||||||
|
|
||||||
### Section: /thumb
|
### Section: /thumb
|
||||||
|
|
||||||
@@ -896,10 +903,10 @@ _none_
|
|||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### List units — `GET /unit`
|
#### List units — `GET /unit`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { units: T40[] }`
|
- Returns: `200 { units: T41[] }`
|
||||||
#### Upsert unit — `PUT /unit`
|
#### Upsert unit — `PUT /unit`
|
||||||
- Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }`
|
- Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }`
|
||||||
- Returns: `200 { unit: T40 }`
|
- Returns: `200 { unit: T41 }`
|
||||||
|
|
||||||
### Section: /v1
|
### Section: /v1
|
||||||
|
|
||||||
@@ -953,10 +960,10 @@ _none_
|
|||||||
|
|
||||||
#### List warehouses — `GET /warehouse`
|
#### List warehouses — `GET /warehouse`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { warehouses: T55[] }`
|
- Returns: `200 { warehouses: T56[] }`
|
||||||
#### Create warehouse — `POST /warehouse`
|
#### Create warehouse — `POST /warehouse`
|
||||||
- Params: `body: { name: str, sort?: int }`
|
- Params: `body: { name: str, sort?: int }`
|
||||||
- Returns: `201 T53`
|
- Returns: `201 T54`
|
||||||
#### Reorder warehouses — `PUT /warehouse/order`
|
#### Reorder warehouses — `PUT /warehouse/order`
|
||||||
- Params: `body: { warehouseIds: int[] }`
|
- Params: `body: { warehouseIds: int[] }`
|
||||||
- Returns: `200 { warehouseIds: int[] }`
|
- Returns: `200 { warehouseIds: int[] }`
|
||||||
@@ -965,10 +972,10 @@ _none_
|
|||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Get warehouse by ID — `GET /warehouse/{id}`
|
#### Get warehouse by ID — `GET /warehouse/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 T53`
|
- Returns: `200 T54`
|
||||||
#### Update warehouse (partial) — `PATCH /warehouse/{id}`
|
#### Update warehouse (partial) — `PATCH /warehouse/{id}`
|
||||||
- Params: `id: int, body: { name?: str, sort?: int }`
|
- Params: `id: int, body: { name?: str, sort?: int }`
|
||||||
- Returns: `200 T53`
|
- Returns: `200 T54`
|
||||||
#### Update warehouse — `PUT /warehouse/{id}`
|
#### Update warehouse — `PUT /warehouse/{id}`
|
||||||
- Params: `id: int, body: { name?: str, sort?: int }`
|
- Params: `id: int, body: { name?: str, sort?: int }`
|
||||||
- Returns: `200 T53`
|
- Returns: `200 T54`
|
||||||
|
|||||||
@@ -87,4 +87,17 @@ export const order = {
|
|||||||
return http(`/api/order/${id}/pdf`, { method: 'POST' });
|
return http(`/api/order/${id}/pdf`, { method: 'POST' });
|
||||||
},
|
},
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/order/:id/shipment
|
||||||
|
* Creates shipment from order.
|
||||||
|
* @param {number|string} id Order ID
|
||||||
|
* @param {object} [body] Optional payload: { date?, externalNumber?, lines?: Array<{ sku: string, amount?: number }> }
|
||||||
|
* → 201 { shipment: Shipment }
|
||||||
|
*/
|
||||||
|
// #region POST /api/order/:id/shipment — createShipment
|
||||||
|
createShipment(id, body = {}) {
|
||||||
|
return http(`/api/order/${id}/shipment`, { method: 'POST', body });
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -86,5 +86,18 @@ export const quote = {
|
|||||||
return http(`/api/quote/${id}/file/${fileId}`, { method: 'DELETE' });
|
return http(`/api/quote/${id}/file/${fileId}`, { method: 'DELETE' });
|
||||||
},
|
},
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/quote/:id/order
|
||||||
|
* Converts quote into sales order.
|
||||||
|
* @param {number|string} id Quote ID
|
||||||
|
* @param {object} [body] Optional overrides: { number, date, entityId, externalNumber, lines }
|
||||||
|
* → 201 { order: Order }
|
||||||
|
*/
|
||||||
|
// #region POST /api/quote/:id/order — createOrder
|
||||||
|
createOrder(id, body = {}) {
|
||||||
|
return http(`/api/quote/${id}/order`, { method: 'POST', body });
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,26 @@ import { http } from './http.js';
|
|||||||
* Manifest number format: <orderNumber><shipmentPrefix><index>, e.g. AU-1001-L-1.
|
* Manifest number format: <orderNumber><shipmentPrefix><index>, e.g. AU-1001-L-1.
|
||||||
*/
|
*/
|
||||||
export const shipment = {
|
export const shipment = {
|
||||||
|
// #region GET /api/shipment — list
|
||||||
|
list(params = {}) {
|
||||||
|
const q = new URLSearchParams();
|
||||||
|
if (params.offset != null) q.set('offset', String(params.offset));
|
||||||
|
if (params.limit != null) q.set('limit', String(params.limit));
|
||||||
|
if (params.search) q.set('search', String(params.search));
|
||||||
|
if (params.state) q.set('state', String(params.state));
|
||||||
|
if (params.orderId != null) q.set('orderId', String(params.orderId));
|
||||||
|
if (params.seekMonths != null) q.set('seekMonths', String(params.seekMonths));
|
||||||
|
const qs = q.toString();
|
||||||
|
return http(`/api/shipment${qs ? `?${qs}` : ''}`);
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region PATCH /api/shipment/:id — update
|
||||||
|
update(id, body) {
|
||||||
|
return http(`/api/shipment/${id}`, { method: 'PATCH', body });
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
// #region GET /api/order/:id/shipment — listByOrder
|
// #region GET /api/order/:id/shipment — listByOrder
|
||||||
listByOrder(orderId) {
|
listByOrder(orderId) {
|
||||||
return http(`/api/order/${orderId}/shipment`);
|
return http(`/api/order/${orderId}/shipment`);
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export { default as customergroupAutomat } from './customergroupAutomat';
|
|||||||
export { default as numberAutomat } from './numberAutomat';
|
export { default as numberAutomat } from './numberAutomat';
|
||||||
export { default as accountAutomat } from './accountAutomat';
|
export { default as accountAutomat } from './accountAutomat';
|
||||||
export { default as backupAutomat } from './backupAutomat';
|
export { default as backupAutomat } from './backupAutomat';
|
||||||
|
export { default as shipmentAutomat } from './shipmentAutomat';
|
||||||
export { createListAutomat, selectListItems, selectListStatus } from './createListAutomat';
|
export { createListAutomat, selectListItems, selectListStatus } from './createListAutomat';
|
||||||
|
|
||||||
|
|
||||||
@@ -181,6 +182,18 @@ export {
|
|||||||
|
|
||||||
export { default as vfsAutomat } from './vfsAutomat';
|
export { default as vfsAutomat } from './vfsAutomat';
|
||||||
|
|
||||||
|
export { default as pinnedAppletAutomat } from './pinnedAppletAutomat';
|
||||||
|
export {
|
||||||
|
isPinned,
|
||||||
|
getPinnedForRoute,
|
||||||
|
pinApplet,
|
||||||
|
unpinApplet,
|
||||||
|
togglePin,
|
||||||
|
executeApplet,
|
||||||
|
closeExecution,
|
||||||
|
normalizeRoute,
|
||||||
|
} from './pinnedAppletAutomat';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
|
|||||||
304
src/automats/pinnedAppletAutomat.js
Normal file
304
src/automats/pinnedAppletAutomat.js
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
/**
|
||||||
|
* @file pinnedAppletAutomat.js
|
||||||
|
* @description State container for applets pinned to browser routes.
|
||||||
|
* Persists pin assignments in company settings using the 'applet.pinned' namespace,
|
||||||
|
* with local storage fallback per company.
|
||||||
|
*
|
||||||
|
* Upstream Triggers:
|
||||||
|
* - `authAutomat`: Subscribes to auth changes — loads pinned applet settings when authed
|
||||||
|
* with a selectedCompanyId, and resets when anon/logged out.
|
||||||
|
* - UI / VfsAppletRunner: Calls `pinApplet`, `unpinApplet`, `togglePin`.
|
||||||
|
* - UI / PinnedAppletFabs: Calls `executeApplet`, `closeExecution`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Automat } from 'react-automat';
|
||||||
|
import { setting as settingApi } from '../api';
|
||||||
|
import authAutomat from './authAutomat';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a browser route pathname (strips trailing slashes, hashes, query params).
|
||||||
|
* @param {string} rawRoute
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function normalizeRoute(rawRoute) {
|
||||||
|
if (!rawRoute) return '/';
|
||||||
|
const clean = String(rawRoute).split('?')[0].split('#')[0];
|
||||||
|
const trimmed = clean.replace(/\/+$/, '');
|
||||||
|
return trimmed || '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PinnedAppletState
|
||||||
|
* @property {Record<string, string[]>} pinnedByRoute Mapping of normalized route -> array of applet file paths
|
||||||
|
* @property {'idle'|'loading'|'ready'} status
|
||||||
|
* @property {string|null} activeExecution Currently active applet fileName to execute in dialog
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {PinnedAppletState} */
|
||||||
|
const initialState = {
|
||||||
|
pinnedByRoute: {},
|
||||||
|
status: 'idle',
|
||||||
|
activeExecution: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
let pinnedAppletAutomat;
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
/**
|
||||||
|
* Check if an applet is pinned to the specified route.
|
||||||
|
* @param {string} fileName
|
||||||
|
* @param {string} rawRoute
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isPinned(fileName, rawRoute) {
|
||||||
|
if (!fileName) return false;
|
||||||
|
const route = normalizeRoute(rawRoute);
|
||||||
|
const list = pinnedAppletAutomat.state.pinnedByRoute[route];
|
||||||
|
return Array.isArray(list) && list.includes(fileName);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all applet file paths pinned to the specified route.
|
||||||
|
* @param {string} rawRoute
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
getPinnedForRoute(rawRoute) {
|
||||||
|
const route = normalizeRoute(rawRoute);
|
||||||
|
const list = pinnedAppletAutomat.state.pinnedByRoute[route];
|
||||||
|
return Array.isArray(list) ? list : [];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pin an applet to a route.
|
||||||
|
* @param {string} fileName
|
||||||
|
* @param {string} rawRoute
|
||||||
|
*/
|
||||||
|
pinApplet(fileName, rawRoute) {
|
||||||
|
if (!fileName) return;
|
||||||
|
const route = normalizeRoute(rawRoute);
|
||||||
|
const current = pinnedAppletAutomat.state.pinnedByRoute;
|
||||||
|
const list = current[route] ? [...current[route]] : [];
|
||||||
|
if (!list.includes(fileName)) {
|
||||||
|
list.push(fileName);
|
||||||
|
const next = { ...current, [route]: list };
|
||||||
|
pinnedAppletAutomat.setState({ pinnedByRoute: next });
|
||||||
|
void actions.save(route);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unpin an applet from a route.
|
||||||
|
* @param {string} fileName
|
||||||
|
* @param {string} rawRoute
|
||||||
|
*/
|
||||||
|
unpinApplet(fileName, rawRoute) {
|
||||||
|
if (!fileName) return;
|
||||||
|
const route = normalizeRoute(rawRoute);
|
||||||
|
const current = pinnedAppletAutomat.state.pinnedByRoute;
|
||||||
|
const list = current[route] ? current[route].filter((f) => f !== fileName) : [];
|
||||||
|
const next = { ...current };
|
||||||
|
if (list.length > 0) {
|
||||||
|
next[route] = list;
|
||||||
|
} else {
|
||||||
|
delete next[route];
|
||||||
|
}
|
||||||
|
pinnedAppletAutomat.setState({ pinnedByRoute: next });
|
||||||
|
void actions.save(route);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle pin status for an applet on a route.
|
||||||
|
* @param {string} fileName
|
||||||
|
* @param {string} rawRoute
|
||||||
|
* @returns {boolean} New pinned status
|
||||||
|
*/
|
||||||
|
togglePin(fileName, rawRoute) {
|
||||||
|
if (!fileName) return false;
|
||||||
|
const route = normalizeRoute(rawRoute);
|
||||||
|
const isCurrentlyPinned = actions.isPinned(fileName, route);
|
||||||
|
if (isCurrentlyPinned) {
|
||||||
|
actions.unpinApplet(fileName, route);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
actions.pinApplet(fileName, route);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open dialog to execute the specified applet.
|
||||||
|
* @param {string} fileName
|
||||||
|
*/
|
||||||
|
executeApplet(fileName) {
|
||||||
|
pinnedAppletAutomat.setState({ activeExecution: fileName });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the execution dialog.
|
||||||
|
*/
|
||||||
|
closeExecution() {
|
||||||
|
pinnedAppletAutomat.setState({ activeExecution: null });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load pinned applets from company settings namespace and localStorage.
|
||||||
|
*/
|
||||||
|
async load() {
|
||||||
|
const companyId = authAutomat.state.user?.selectedCompanyId;
|
||||||
|
if (!companyId) return;
|
||||||
|
|
||||||
|
pinnedAppletAutomat.setState({ status: 'loading' });
|
||||||
|
const storageKey = `dash_company_${companyId}_applet_pinned`;
|
||||||
|
let loadedMap = null;
|
||||||
|
|
||||||
|
// Fast-path: Synchronous read from localStorage to eliminate blank flash
|
||||||
|
try {
|
||||||
|
const rawLocal = localStorage.getItem(storageKey);
|
||||||
|
if (rawLocal) {
|
||||||
|
loadedMap = JSON.parse(rawLocal);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[pinnedAppletAutomat] localStorage read failed:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadedMap && typeof loadedMap === 'object') {
|
||||||
|
pinnedAppletAutomat.setState({ pinnedByRoute: loadedMap, status: 'ready' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network read: Try company settings API with 'applet.pinned' namespace
|
||||||
|
try {
|
||||||
|
const res = await settingApi.get('applet.pinned');
|
||||||
|
const val = res?.setting?.value;
|
||||||
|
if (val) {
|
||||||
|
const parsed = JSON.parse(val);
|
||||||
|
if (parsed && typeof parsed === 'object') {
|
||||||
|
loadedMap = { ...(loadedMap || {}), ...parsed };
|
||||||
|
pinnedAppletAutomat.setState({ pinnedByRoute: loadedMap, status: 'ready' });
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(loadedMap));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore if key is not yet stored on backend
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also scan company settings list for any keys in namespace 'applet.pinned:'
|
||||||
|
try {
|
||||||
|
const listRes = await settingApi.list({ frontendOnly: true });
|
||||||
|
const settings = Array.isArray(listRes?.settings) ? listRes.settings : [];
|
||||||
|
let merged = false;
|
||||||
|
const current = { ...(loadedMap || pinnedAppletAutomat.state.pinnedByRoute) };
|
||||||
|
|
||||||
|
for (const s of settings) {
|
||||||
|
if (s.key === 'applet.pinned' && s.value) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(s.value);
|
||||||
|
if (parsed && typeof parsed === 'object') {
|
||||||
|
Object.assign(current, parsed);
|
||||||
|
merged = true;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
} else if (s.key && s.key.startsWith('applet.pinned:') && s.value) {
|
||||||
|
const route = s.key.slice('applet.pinned:'.length);
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(s.value);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
current[route] = parsed;
|
||||||
|
merged = true;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (merged) {
|
||||||
|
pinnedAppletAutomat.setState({ pinnedByRoute: current, status: 'ready' });
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(current));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore network errors
|
||||||
|
}
|
||||||
|
|
||||||
|
pinnedAppletAutomat.setState({ status: 'ready' });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist pinned applets into company settings namespace and localStorage.
|
||||||
|
* @param {string} [route] Optional route updated
|
||||||
|
*/
|
||||||
|
async save(route = null) {
|
||||||
|
const companyId = authAutomat.state.user?.selectedCompanyId;
|
||||||
|
const { pinnedByRoute } = pinnedAppletAutomat.state;
|
||||||
|
|
||||||
|
if (companyId) {
|
||||||
|
const storageKey = `dash_company_${companyId}_applet_pinned`;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(pinnedByRoute));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[pinnedAppletAutomat] localStorage write failed:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert into company settings namespace 'applet.pinned'
|
||||||
|
try {
|
||||||
|
await settingApi.upsert('applet.pinned', {
|
||||||
|
value: JSON.stringify(pinnedByRoute),
|
||||||
|
frontend: true,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Backend key validation fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also upsert route-specific key in namespace
|
||||||
|
if (route) {
|
||||||
|
try {
|
||||||
|
const routeKey = `applet.pinned:${route}`;
|
||||||
|
const routeVal = JSON.stringify(pinnedByRoute[route] || []);
|
||||||
|
await settingApi.upsert(routeKey, {
|
||||||
|
value: routeVal,
|
||||||
|
frontend: true,
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset on logout or company switch.
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
pinnedAppletAutomat.setState(initialState);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pinned Applet state container.
|
||||||
|
* @type {Automat<PinnedAppletState>}
|
||||||
|
*/
|
||||||
|
pinnedAppletAutomat = new Automat(initialState, actions, { name: 'pinnedApplet' });
|
||||||
|
|
||||||
|
// Subscribe to auth lifecycle
|
||||||
|
authAutomat.subscribe((auth) => {
|
||||||
|
const companyId = auth.user?.selectedCompanyId;
|
||||||
|
if (auth.status === 'authed' && companyId) {
|
||||||
|
void pinnedAppletAutomat.actions.load();
|
||||||
|
} else if (auth.status === 'anon') {
|
||||||
|
pinnedAppletAutomat.actions.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-load if auth is already established when module loads
|
||||||
|
if (authAutomat.state.status === 'authed' && authAutomat.state.user?.selectedCompanyId) {
|
||||||
|
void pinnedAppletAutomat.actions.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default pinnedAppletAutomat;
|
||||||
|
export const {
|
||||||
|
isPinned,
|
||||||
|
getPinnedForRoute,
|
||||||
|
pinApplet,
|
||||||
|
unpinApplet,
|
||||||
|
togglePin,
|
||||||
|
executeApplet,
|
||||||
|
closeExecution,
|
||||||
|
} = pinnedAppletAutomat.actions;
|
||||||
240
src/automats/shipmentAutomat.js
Normal file
240
src/automats/shipmentAutomat.js
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
/**
|
||||||
|
* @file shipmentAutomat.js
|
||||||
|
* @description Manages cached and paginated company shipment manifests (Lieferscheine) (`GET /api/shipment`).
|
||||||
|
*
|
||||||
|
* Upstream Triggers:
|
||||||
|
* - `authAutomat`: Triggers `actions.reset()` on `'anon'` or when `selectedCompanyId` changes.
|
||||||
|
* - `sseAutomat`: Triggers `actions.reload()` on `'shipment'` SSE events.
|
||||||
|
*
|
||||||
|
* Actions:
|
||||||
|
* - `load()`: Fetches shipments with current filters.
|
||||||
|
* - `reload()`: Silently re-fetches shipments.
|
||||||
|
* - `setFilter(filter)`: Updates free-text search filter.
|
||||||
|
* - `setStateFilter(state)`: Updates delivery state filter ('', 'new', 'delivered', 'returned').
|
||||||
|
* - `updateState(id, state)`: Calls `PATCH /api/shipment/:id` and updates local cache.
|
||||||
|
* - `deleteShipment(id)`: Calls `DELETE /api/shipment/:id` and removes from local cache.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Automat } from 'react-automat';
|
||||||
|
import { shipment as shipmentApi } from '../api';
|
||||||
|
import authAutomat from './authAutomat';
|
||||||
|
import orderAutomat from './orderAutomat';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ShipmentSummary
|
||||||
|
* @property {number} id
|
||||||
|
* @property {string} number
|
||||||
|
* @property {'shipment'} type
|
||||||
|
* @property {string} date
|
||||||
|
* @property {string|null} createdAt
|
||||||
|
* @property {number|null} orderId
|
||||||
|
* @property {string|null} orderNumber
|
||||||
|
* @property {number|null} entityId
|
||||||
|
* @property {string|null} customerName
|
||||||
|
* @property {number} positions
|
||||||
|
* @property {number} totalQuantity
|
||||||
|
* @property {number} packageCount
|
||||||
|
* @property {string|null} carrier
|
||||||
|
* @property {string|null} trackingCode
|
||||||
|
* @property {'new'|'delivered'|'returned'} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ShipmentAutomatState
|
||||||
|
* @property {ShipmentSummary[]|null} items
|
||||||
|
* @property {number} total
|
||||||
|
* @property {number} unfilteredTotal
|
||||||
|
* @property {string} filter
|
||||||
|
* @property {string} stateFilter
|
||||||
|
* @property {number} seekMonths
|
||||||
|
* @property {'idle'|'loading'|'ready'|'failed'} status
|
||||||
|
* @property {Error|string|null} error
|
||||||
|
*/
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
items: null,
|
||||||
|
total: 0,
|
||||||
|
unfilteredTotal: 0,
|
||||||
|
filter: '',
|
||||||
|
stateFilter: '',
|
||||||
|
seekMonths: 0,
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
let automat;
|
||||||
|
let inflightLoad = null;
|
||||||
|
let debounceTimer = null;
|
||||||
|
|
||||||
|
// #region fetch helper
|
||||||
|
async function fetchShipments(state) {
|
||||||
|
const params = {
|
||||||
|
offset: 0,
|
||||||
|
limit: 100,
|
||||||
|
};
|
||||||
|
if (state.filter && state.filter.trim()) {
|
||||||
|
params.search = state.filter.trim();
|
||||||
|
}
|
||||||
|
if (state.stateFilter) {
|
||||||
|
params.state = state.stateFilter;
|
||||||
|
}
|
||||||
|
if (state.seekMonths) {
|
||||||
|
params.seekMonths = state.seekMonths;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await shipmentApi.list(params);
|
||||||
|
return {
|
||||||
|
items: res?.shipments || [],
|
||||||
|
total: res?.total ?? (res?.shipments?.length || 0),
|
||||||
|
unfilteredTotal: res?.unfilteredTotal ?? res?.total ?? (res?.shipments?.length || 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
// #region load & reload
|
||||||
|
async load() {
|
||||||
|
if (inflightLoad) return inflightLoad;
|
||||||
|
|
||||||
|
automat.setState((s) => ({
|
||||||
|
status: s.items === null ? 'loading' : s.status,
|
||||||
|
error: null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
inflightLoad = (async () => {
|
||||||
|
try {
|
||||||
|
const { items, total, unfilteredTotal } = await fetchShipments(automat.state);
|
||||||
|
automat.setState({
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
unfilteredTotal,
|
||||||
|
status: 'ready',
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
} catch (err) {
|
||||||
|
automat.setState({
|
||||||
|
status: 'failed',
|
||||||
|
error: err,
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
inflightLoad = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return inflightLoad;
|
||||||
|
},
|
||||||
|
|
||||||
|
async reload() {
|
||||||
|
try {
|
||||||
|
const { items, total, unfilteredTotal } = await fetchShipments(automat.state);
|
||||||
|
automat.setState({
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
unfilteredTotal,
|
||||||
|
status: 'ready',
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[shipmentAutomat] silent reload failed:', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region filters
|
||||||
|
setFilter(filterText) {
|
||||||
|
const next = typeof filterText === 'string' ? filterText : '';
|
||||||
|
if (automat.state.filter === next) return;
|
||||||
|
|
||||||
|
automat.setState({ filter: next });
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
void automat.actions.load().catch(() => {});
|
||||||
|
}, 200);
|
||||||
|
},
|
||||||
|
|
||||||
|
setStateFilter(stateFilter) {
|
||||||
|
const next = typeof stateFilter === 'string' ? stateFilter : '';
|
||||||
|
if (automat.state.stateFilter === next) return;
|
||||||
|
|
||||||
|
automat.setState({ stateFilter: next });
|
||||||
|
void automat.actions.load().catch(() => {});
|
||||||
|
},
|
||||||
|
|
||||||
|
setSeekMonths(months) {
|
||||||
|
automat.setState({ seekMonths: months });
|
||||||
|
void automat.actions.load().catch(() => {});
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region item mutations
|
||||||
|
patchShipment(id, patch) {
|
||||||
|
automat.setState((s) => {
|
||||||
|
if (!Array.isArray(s.items)) return null;
|
||||||
|
const next = s.items.map((item) => (item.id === id ? { ...item, ...patch } : item));
|
||||||
|
return { items: next };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateState(id, newState) {
|
||||||
|
// Optimistic update
|
||||||
|
automat.actions.patchShipment(id, { state: newState });
|
||||||
|
try {
|
||||||
|
await shipmentApi.update(id, { state: newState });
|
||||||
|
void orderAutomat.actions.reload().catch(() => {});
|
||||||
|
} catch (err) {
|
||||||
|
// Rollback on error
|
||||||
|
void automat.actions.reload();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteShipment(id) {
|
||||||
|
await shipmentApi.delete(id);
|
||||||
|
automat.setState((s) => {
|
||||||
|
if (!Array.isArray(s.items)) return null;
|
||||||
|
const next = s.items.filter((item) => item.id !== id);
|
||||||
|
return {
|
||||||
|
items: next,
|
||||||
|
total: Math.max(0, s.total - 1),
|
||||||
|
unfilteredTotal: Math.max(0, s.unfilteredTotal - 1),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
void orderAutomat.actions.reload().catch(() => {});
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer);
|
||||||
|
inflightLoad = null;
|
||||||
|
automat.setState({ ...initialState });
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
};
|
||||||
|
|
||||||
|
automat = new Automat(initialState, actions, { name: 'shipment' });
|
||||||
|
|
||||||
|
// #region authAutomat synchronization
|
||||||
|
let lastCompanyId = null;
|
||||||
|
authAutomat.subscribe((auth) => {
|
||||||
|
const companyId = auth.user?.selectedCompanyId ?? null;
|
||||||
|
if (auth.status === 'anon') {
|
||||||
|
automat.actions.reset();
|
||||||
|
lastCompanyId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (lastCompanyId !== null && companyId !== lastCompanyId) {
|
||||||
|
automat.actions.reset();
|
||||||
|
}
|
||||||
|
if (auth.status === 'authed' && companyId != null) {
|
||||||
|
const isNew = lastCompanyId !== companyId;
|
||||||
|
lastCompanyId = companyId;
|
||||||
|
if (isNew) {
|
||||||
|
void automat.actions.load().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
export default automat;
|
||||||
@@ -34,6 +34,7 @@ import propertyAutomat from './propertyAutomat';
|
|||||||
import supplierAutomat from './supplierAutomat';
|
import supplierAutomat from './supplierAutomat';
|
||||||
import orderAutomat from './orderAutomat';
|
import orderAutomat from './orderAutomat';
|
||||||
import quoteAutomat from './quoteAutomat';
|
import quoteAutomat from './quoteAutomat';
|
||||||
|
import shipmentAutomat from './shipmentAutomat';
|
||||||
import { handleDraftRequest, handleDraftEvent } from './draftSync';
|
import { handleDraftRequest, handleDraftEvent } from './draftSync';
|
||||||
|
|
||||||
|
|
||||||
@@ -253,9 +254,12 @@ function connect() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addTrackedListener(es, 'shipment', () => {
|
addTrackedListener(es, 'shipment', () => {
|
||||||
void orderAutomat.actions.reload().catch((err) => {
|
void shipmentAutomat.actions.reload().catch((err) => {
|
||||||
console.error('[sse] shipment refresh failed', err);
|
console.error('[sse] shipment refresh failed', err);
|
||||||
});
|
});
|
||||||
|
void orderAutomat.actions.reload().catch((err) => {
|
||||||
|
console.error('[sse] order refresh failed', err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
addTrackedListener(es, 'draft:request', (event) => {
|
addTrackedListener(es, 'draft:request', (event) => {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
NAV_SURFACE,
|
NAV_SURFACE,
|
||||||
} from '../colors';
|
} from '../colors';
|
||||||
import { ChatFab, ChatDrawer } from './chat';
|
import { ChatFab, ChatDrawer } from './chat';
|
||||||
|
import { PinnedAppletFabs, PinnedAppletDialog } from './applet';
|
||||||
|
|
||||||
const BreakpointOverlay = 0 && import.meta.env.DEV
|
const BreakpointOverlay = 0 && import.meta.env.DEV
|
||||||
? lazy(() => import('./BreakpointOverlay'))
|
? lazy(() => import('./BreakpointOverlay'))
|
||||||
@@ -405,6 +406,8 @@ export default function AppShell() {
|
|||||||
</Box>
|
</Box>
|
||||||
<ChatFab />
|
<ChatFab />
|
||||||
<ChatDrawer />
|
<ChatDrawer />
|
||||||
|
<PinnedAppletFabs pathname={pathname} />
|
||||||
|
<PinnedAppletDialog />
|
||||||
{BreakpointOverlay && (
|
{BreakpointOverlay && (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<BreakpointOverlay />
|
<BreakpointOverlay />
|
||||||
|
|||||||
149
src/components/applet/PinnedAppletDialog.jsx
Normal file
149
src/components/applet/PinnedAppletDialog.jsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import pinnedAppletAutomat from '../../automats/pinnedAppletAutomat';
|
||||||
|
import vfsAutomat from '../../automats/vfsAutomat';
|
||||||
|
import { vfs as vfsApi } from '../../api';
|
||||||
|
import VfsAppletRunner from '../vfs/VfsAppletRunner';
|
||||||
|
import VfsTextEditor from '../vfs/VfsTextEditor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file PinnedAppletDialog.jsx
|
||||||
|
* @description Dialog modal that executes a pinned applet runner directly on the current route.
|
||||||
|
*/
|
||||||
|
export default class PinnedAppletDialog extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const activeExecution = pinnedAppletAutomat.state.activeExecution;
|
||||||
|
this.state = {
|
||||||
|
activeExecution,
|
||||||
|
content: '',
|
||||||
|
loadingContent: Boolean(activeExecution),
|
||||||
|
loadError: null,
|
||||||
|
viewMode: 'run',
|
||||||
|
files: vfsAutomat.state.files,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.unsubPinned = pinnedAppletAutomat.subscribe(this, (state) => ({
|
||||||
|
activeExecution: state.activeExecution,
|
||||||
|
}));
|
||||||
|
this.unsubVfs = vfsAutomat.subscribe(this, (state) => ({
|
||||||
|
files: state.files,
|
||||||
|
}));
|
||||||
|
if (this.state.activeExecution) {
|
||||||
|
void this.loadAppletContent(this.state.activeExecution);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (prevState.activeExecution !== this.state.activeExecution) {
|
||||||
|
if (this.state.activeExecution) {
|
||||||
|
this.setState({ viewMode: 'run' });
|
||||||
|
void this.loadAppletContent(this.state.activeExecution);
|
||||||
|
} else {
|
||||||
|
this.setState({ content: '', loadingContent: false, loadError: null, viewMode: 'run' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsubPinned?.();
|
||||||
|
this.unsubVfs?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAppletContent = async (fileName) => {
|
||||||
|
this.setState({ loadingContent: true, loadError: null });
|
||||||
|
try {
|
||||||
|
const text = await vfsApi.getText(fileName);
|
||||||
|
this.setState({ content: text, loadingContent: false });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[PinnedAppletDialog] failed to load applet content:', err);
|
||||||
|
this.setState({
|
||||||
|
loadError: err?.message || 'Fehler beim Laden des Applets',
|
||||||
|
loadingContent: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClose = () => {
|
||||||
|
pinnedAppletAutomat.actions.closeExecution();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { activeExecution, content, loadingContent, loadError, viewMode, files } = this.state;
|
||||||
|
if (!activeExecution) return null;
|
||||||
|
|
||||||
|
const file = files.find((f) => f.name === activeExecution);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={Boolean(activeExecution)}
|
||||||
|
onClose={this.handleClose}
|
||||||
|
maxWidth="md"
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
height: '85vh',
|
||||||
|
maxHeight: 800,
|
||||||
|
borderRadius: 3,
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent sx={{ p: 0, height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||||
|
{loadingContent ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 1.5,
|
||||||
|
color: 'text.secondary',
|
||||||
|
p: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={28} />
|
||||||
|
<Typography variant="body2">Lade Applet {activeExecution}…</Typography>
|
||||||
|
</Box>
|
||||||
|
) : loadError ? (
|
||||||
|
<Box sx={{ p: 3, textAlign: 'center' }}>
|
||||||
|
<Typography color="error" variant="body1">
|
||||||
|
{loadError}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : viewMode === 'run' ? (
|
||||||
|
<VfsAppletRunner
|
||||||
|
fileName={activeExecution}
|
||||||
|
file={file}
|
||||||
|
content={content}
|
||||||
|
loadingContent={false}
|
||||||
|
onClose={this.handleClose}
|
||||||
|
onShowCode={() => this.setState({ viewMode: 'code' })}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<VfsTextEditor
|
||||||
|
fileName={activeExecution}
|
||||||
|
file={file}
|
||||||
|
content={content}
|
||||||
|
originalContent={content}
|
||||||
|
isDirty={false}
|
||||||
|
loadingContent={false}
|
||||||
|
onClose={this.handleClose}
|
||||||
|
onRunApplet={() => this.setState({ viewMode: 'run' })}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
154
src/components/applet/PinnedAppletFabs.jsx
Normal file
154
src/components/applet/PinnedAppletFabs.jsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Fab from '@mui/material/Fab';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Badge from '@mui/material/Badge';
|
||||||
|
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
|
||||||
|
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||||
|
import pinnedAppletAutomat, { normalizeRoute } from '../../automats/pinnedAppletAutomat';
|
||||||
|
import vfsAutomat from '../../automats/vfsAutomat';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file PinnedAppletFabs.jsx
|
||||||
|
* @description Renders Floating Action Buttons for any applets pinned to the current browser route.
|
||||||
|
* Clicking a FAB triggers the execution dialog for that applet.
|
||||||
|
*/
|
||||||
|
export default class PinnedAppletFabs extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const route = normalizeRoute(props.pathname);
|
||||||
|
this.state = {
|
||||||
|
pinnedFileNames: pinnedAppletAutomat.actions.getPinnedForRoute(route),
|
||||||
|
files: vfsAutomat.state.files,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.unsubPinned = pinnedAppletAutomat.subscribe(this, () => {
|
||||||
|
const route = normalizeRoute(this.props.pathname);
|
||||||
|
return {
|
||||||
|
pinnedFileNames: pinnedAppletAutomat.actions.getPinnedForRoute(route),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.unsubVfs = vfsAutomat.subscribe(this, (state) => ({
|
||||||
|
files: state.files,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.pathname !== this.props.pathname) {
|
||||||
|
const route = normalizeRoute(this.props.pathname);
|
||||||
|
this.setState({
|
||||||
|
pinnedFileNames: pinnedAppletAutomat.actions.getPinnedForRoute(route),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsubPinned?.();
|
||||||
|
this.unsubVfs?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { pinnedFileNames, files } = this.state;
|
||||||
|
if (!pinnedFileNames || pinnedFileNames.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={(theme) => ({
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: { xs: 20, sm: 28 },
|
||||||
|
right: { xs: 20, sm: 28 },
|
||||||
|
zIndex: theme.zIndex.speedDial || 1050,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column-reverse',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
gap: 1.5,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{pinnedFileNames.map((fileName) => {
|
||||||
|
const file = files.find((f) => f.name === fileName);
|
||||||
|
const baseName = fileName.split('/').pop().replace(/\.applet\.js$/i, '');
|
||||||
|
const title = file?.description || baseName;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
key={fileName}
|
||||||
|
placement="left"
|
||||||
|
arrow
|
||||||
|
title={
|
||||||
|
<Box sx={{ p: 0.5, maxWidth: 280 }}>
|
||||||
|
<Typography variant="subtitle2" fontWeight={700} sx={{ fontSize: '0.82rem' }}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ display: 'block', opacity: 0.8, fontFamily: 'monospace', fontSize: '0.7rem' }}
|
||||||
|
>
|
||||||
|
{fileName}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="primary.light"
|
||||||
|
sx={{ display: 'block', mt: 0.5, fontStyle: 'italic', fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
Klicken zum Ausführen
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
overlap="circular"
|
||||||
|
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||||
|
badgeContent={
|
||||||
|
<PushPinIcon
|
||||||
|
sx={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#fff',
|
||||||
|
transform: 'rotate(25deg)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
'& .MuiBadge-badge': {
|
||||||
|
bgcolor: 'primary.dark',
|
||||||
|
border: '2px solid #fff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
minWidth: 20,
|
||||||
|
p: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Fab
|
||||||
|
color="primary"
|
||||||
|
aria-label={`Applet ${title} ausführen`}
|
||||||
|
onClick={() => pinnedAppletAutomat.actions.executeApplet(fileName)}
|
||||||
|
sx={(theme) => ({
|
||||||
|
width: 52,
|
||||||
|
height: 52,
|
||||||
|
background: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%)`,
|
||||||
|
boxShadow: '0 6px 20px rgba(0, 0, 0, 0.25)',
|
||||||
|
transition: 'transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'scale(1.08) translateY(-2px)',
|
||||||
|
boxShadow: '0 10px 26px rgba(0, 0, 0, 0.35)',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<PlayArrowRoundedIcon sx={{ fontSize: 28 }} />
|
||||||
|
</Fab>
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/components/applet/index.js
Normal file
2
src/components/applet/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as PinnedAppletFabs } from './PinnedAppletFabs';
|
||||||
|
export { default as PinnedAppletDialog } from './PinnedAppletDialog';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PureComponent } from 'react';
|
import { PureComponent, createRef } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
@@ -7,16 +7,18 @@ import { PanelTileGrid } from 'dashgui/panels';
|
|||||||
import { delivery as deliveryApi, quote as quoteApi, order as orderApi, shipment as shipmentApi, payment as paymentApi } from '../../api';
|
import { delivery as deliveryApi, quote as quoteApi, order as orderApi, shipment as shipmentApi, payment as paymentApi } from '../../api';
|
||||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError, notifyErrorMessage } from '../../utils/notify';
|
||||||
import withNavigate from '../withNavigate';
|
import withNavigate from '../withNavigate';
|
||||||
import ManifestHeader from './ManifestHeader';
|
import ManifestHeader from './ManifestHeader';
|
||||||
import ManifestLines from './ManifestLines';
|
import ManifestLines from './ManifestLines';
|
||||||
import ManifestPdfDialog from './ManifestPdfDialog';
|
import ManifestPdfDialog from './ManifestPdfDialog';
|
||||||
import ArticleSearchDialog from './ArticleSearchDialog';
|
import ArticleSearchDialog from './ArticleSearchDialog';
|
||||||
import OrderDocumentsTiles from './OrderDocumentsTiles';
|
import OrderDocumentsTiles from './OrderDocumentsTiles';
|
||||||
|
import OrderShipmentDialog from './OrderShipmentDialog';
|
||||||
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
|
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
|
||||||
import entityAutomat from '../../automats/entityAutomat';
|
import entityAutomat from '../../automats/entityAutomat';
|
||||||
import supplierAutomat from '../../automats/supplierAutomat';
|
import supplierAutomat from '../../automats/supplierAutomat';
|
||||||
|
import orderAutomat from '../../automats/orderAutomat';
|
||||||
import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat';
|
import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat';
|
||||||
import { netFromGross, grossFromNet, getArticleGross } from '../../utils/price';
|
import { netFromGross, grossFromNet, getArticleGross } from '../../utils/price';
|
||||||
import {
|
import {
|
||||||
@@ -95,14 +97,18 @@ class ManifestForm extends PureComponent {
|
|||||||
dirtyLineFields: initialEdit?.dirtyLineFields || {},
|
dirtyLineFields: initialEdit?.dirtyLineFields || {},
|
||||||
dirtySummary: initialEdit?.dirtySummary || [],
|
dirtySummary: initialEdit?.dirtySummary || [],
|
||||||
saving: false,
|
saving: false,
|
||||||
|
orderCreating: false,
|
||||||
|
partyError: false,
|
||||||
pdfUploading: false,
|
pdfUploading: false,
|
||||||
pdfCreating: false,
|
pdfCreating: false,
|
||||||
pdfDialogOpen: false,
|
pdfDialogOpen: false,
|
||||||
|
shipmentDialogOpen: false,
|
||||||
pdfDialogData: null,
|
pdfDialogData: null,
|
||||||
pdfFileName: '',
|
pdfFileName: '',
|
||||||
articleSearchDialogOpen: false,
|
articleSearchDialogOpen: false,
|
||||||
articleSearchLine: null,
|
articleSearchLine: null,
|
||||||
};
|
};
|
||||||
|
this.headerRef = createRef();
|
||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
this.handleRetry = this.handleRetry.bind(this);
|
this.handleRetry = this.handleRetry.bind(this);
|
||||||
this.handleRestore = this.handleRestore.bind(this);
|
this.handleRestore = this.handleRestore.bind(this);
|
||||||
@@ -122,11 +128,16 @@ class ManifestForm extends PureComponent {
|
|||||||
this.handleMoveLine = this.handleMoveLine.bind(this);
|
this.handleMoveLine = this.handleMoveLine.bind(this);
|
||||||
this.handleRemoveAllLines = this.handleRemoveAllLines.bind(this);
|
this.handleRemoveAllLines = this.handleRemoveAllLines.bind(this);
|
||||||
this.handleSave = this.handleSave.bind(this);
|
this.handleSave = this.handleSave.bind(this);
|
||||||
|
this.handleCreateOrder = this.handleCreateOrder.bind(this);
|
||||||
|
this.handleOpenCreateShipment = this.handleOpenCreateShipment.bind(this);
|
||||||
|
this.handleCloseShipmentDialog = this.handleCloseShipmentDialog.bind(this);
|
||||||
|
this.handleShipmentCreated = this.handleShipmentCreated.bind(this);
|
||||||
this.handlePdfUpload = this.handlePdfUpload.bind(this);
|
this.handlePdfUpload = this.handlePdfUpload.bind(this);
|
||||||
this.handleCreatePdf = this.handleCreatePdf.bind(this);
|
this.handleCreatePdf = this.handleCreatePdf.bind(this);
|
||||||
this.handleAttachmentDelete = this.handleAttachmentDelete.bind(this);
|
this.handleAttachmentDelete = this.handleAttachmentDelete.bind(this);
|
||||||
this.handleClosePdfDialog = this.handleClosePdfDialog.bind(this);
|
this.handleClosePdfDialog = this.handleClosePdfDialog.bind(this);
|
||||||
this.handleAbortParse = this.handleAbortParse.bind(this);
|
this.handleAbortParse = this.handleAbortParse.bind(this);
|
||||||
|
this.isFullyDelivered = this.isFullyDelivered.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -457,10 +468,10 @@ class ManifestForm extends PureComponent {
|
|||||||
const { mode } = this.props;
|
const { mode } = this.props;
|
||||||
const { requirePartyBeforeLines } = this.cfg;
|
const { requirePartyBeforeLines } = this.cfg;
|
||||||
if (mode === 'create' && requirePartyBeforeLines && partyId == null) {
|
if (mode === 'create' && requirePartyBeforeLines && partyId == null) {
|
||||||
this.setState({ partyId: null, lines: [emptyLine()] });
|
this.setState({ partyId: null, lines: [emptyLine()], partyError: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ partyId });
|
this.setState({ partyId, partyError: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDateChange(date) {
|
handleDateChange(date) {
|
||||||
@@ -755,10 +766,46 @@ class ManifestForm extends PureComponent {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFullyDelivered() {
|
||||||
|
if (this.state.delivered) return true;
|
||||||
|
const lines = this.state.lines || [];
|
||||||
|
const validLines = lines.filter((l) => l.sku && Number(l.amount) > 0);
|
||||||
|
if (validLines.length === 0) return false;
|
||||||
|
|
||||||
|
const shippedBySku = {};
|
||||||
|
for (const sh of this.state.shipments || []) {
|
||||||
|
for (const sl of sh.lines || []) {
|
||||||
|
if (sl.sku) {
|
||||||
|
const k = String(sl.sku).trim().toLowerCase();
|
||||||
|
shippedBySku[k] = (shippedBySku[k] || 0) + (Number(sl.amount) || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validLines.every((l) => {
|
||||||
|
const k = String(l.sku).trim().toLowerCase();
|
||||||
|
return (shippedBySku[k] || 0) >= Number(l.amount);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleRemoveLine(key) {
|
handleRemoveLine(key) {
|
||||||
this.setState((prev) => {
|
this.setState((prev) => {
|
||||||
|
const lineToRemove = prev.lines.find((line) => line.key === key);
|
||||||
|
if (lineToRemove && this.props.kind === 'order') {
|
||||||
|
const rawSku = lineToRemove.sku != null ? String(lineToRemove.sku).trim().toLowerCase() : '';
|
||||||
|
const isShipped = Boolean(
|
||||||
|
rawSku &&
|
||||||
|
(prev.shipments || []).some((sh) =>
|
||||||
|
(sh.lines || []).some((sl) => sl.sku && String(sl.sku).trim().toLowerCase() === rawSku)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (isShipped) {
|
||||||
|
enqueueSnackbar(`Artikel „${lineToRemove.sku || lineToRemove.name}“ ist bereits auf einem Lieferschein und kann nicht entfernt werden.`, { variant: 'warning' });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
const next = prev.lines.filter((line) => line.key !== key);
|
const next = prev.lines.filter((line) => line.key !== key);
|
||||||
return { lines: next.length > 0 ? next : [emptyLine()] };
|
return { lines: next.length > 0 ? next : [emptyLine()], isDirty: true };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,7 +823,29 @@ class ManifestForm extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleRemoveAllLines() {
|
handleRemoveAllLines() {
|
||||||
this.setState({ lines: [] });
|
this.setState((prev) => {
|
||||||
|
if (this.props.kind === 'order') {
|
||||||
|
const shippedSkus = new Set();
|
||||||
|
for (const sh of prev.shipments || []) {
|
||||||
|
for (const sl of sh.lines || []) {
|
||||||
|
if (sl.sku) shippedSkus.add(String(sl.sku).trim().toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shippedSkus.size > 0) {
|
||||||
|
const remaining = prev.lines.filter((l) => {
|
||||||
|
const raw = l.sku != null ? String(l.sku).trim().toLowerCase() : '';
|
||||||
|
return shippedSkus.has(raw);
|
||||||
|
});
|
||||||
|
if (remaining.length === prev.lines.length) {
|
||||||
|
enqueueSnackbar('Alle Positionen sind bereits auf Lieferscheinen und können nicht gelöscht werden.', { variant: 'warning' });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
enqueueSnackbar('Nicht ausgelieferte Positionen wurden entfernt.', { variant: 'info' });
|
||||||
|
return { lines: remaining.length > 0 ? remaining : [emptyLine()], isDirty: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { lines: [emptyLine()], isDirty: true };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async handlePdfUpload(file) {
|
async handlePdfUpload(file) {
|
||||||
@@ -945,6 +1014,74 @@ class ManifestForm extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: calls quoteApi.createOrder from ./api/index.js
|
||||||
|
// #region POST /api/quote/:id/order — createOrder
|
||||||
|
async handleCreateOrder(e) {
|
||||||
|
e?.preventDefault?.();
|
||||||
|
e?.stopPropagation?.();
|
||||||
|
const { docId, kind } = this.props;
|
||||||
|
if (this.state.orderCreating || this.state.saving) return;
|
||||||
|
if (docId == null || kind !== 'quote') return;
|
||||||
|
|
||||||
|
if (!this.state.partyId) {
|
||||||
|
this.setState({ partyError: true });
|
||||||
|
notifyErrorMessage(
|
||||||
|
'Für eine Bestellung muss ein Kunde ausgewählt sein. Bitte wählen Sie zuerst einen Kunden aus.',
|
||||||
|
);
|
||||||
|
this.headerRef.current?.focusPartyInput?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.isDirty) {
|
||||||
|
const saved = await this.handleSave();
|
||||||
|
if (!saved) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ orderCreating: true });
|
||||||
|
try {
|
||||||
|
const res = await quoteApi.createOrder(docId, {
|
||||||
|
entityId: this.state.partyId,
|
||||||
|
});
|
||||||
|
requireObject(res, 'POST /api/quote/:id/order');
|
||||||
|
const order = requireObject(res.order, 'order');
|
||||||
|
requireDefined(order.id, 'order.id');
|
||||||
|
|
||||||
|
void orderAutomat.actions.reload();
|
||||||
|
|
||||||
|
enqueueSnackbar(
|
||||||
|
order.number
|
||||||
|
? `Bestellung ${order.number} erfolgreich erstellt.`
|
||||||
|
: 'Bestellung erfolgreich erstellt.',
|
||||||
|
{ variant: 'success' },
|
||||||
|
);
|
||||||
|
|
||||||
|
this.props.navigate(`/auftrag/bestellung/${order.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
if (!this._cancelled) {
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ orderCreating: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
async handleOpenCreateShipment() {
|
||||||
|
if (this.state.isDirty) {
|
||||||
|
const saved = await this.handleSave();
|
||||||
|
if (!saved) return;
|
||||||
|
}
|
||||||
|
this.setState({ shipmentDialogOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseShipmentDialog() {
|
||||||
|
this.setState({ shipmentDialogOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleShipmentCreated() {
|
||||||
|
await this.loadEdit();
|
||||||
|
void orderAutomat.actions.reload();
|
||||||
|
}
|
||||||
|
|
||||||
async handleAttachmentDelete(attachment) {
|
async handleAttachmentDelete(attachment) {
|
||||||
if (!attachment) return;
|
if (!attachment) return;
|
||||||
const fileId = attachment.fileId ?? attachment.id;
|
const fileId = attachment.fileId ?? attachment.id;
|
||||||
@@ -975,8 +1112,8 @@ class ManifestForm extends PureComponent {
|
|||||||
async handleSave() {
|
async handleSave() {
|
||||||
const { mode, docId } = this.props;
|
const { mode, docId } = this.props;
|
||||||
const cfg = this.cfg;
|
const cfg = this.cfg;
|
||||||
if (this.state.saving) return;
|
if (this.state.saving) return false;
|
||||||
if (mode === 'edit' && docId == null) return;
|
if (mode === 'edit' && docId == null) return false;
|
||||||
|
|
||||||
const requireAtLeastOne = mode === 'create';
|
const requireAtLeastOne = mode === 'create';
|
||||||
let body;
|
let body;
|
||||||
@@ -999,7 +1136,7 @@ class ManifestForm extends PureComponent {
|
|||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifyError(err);
|
notifyError(err);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ saving: true });
|
this.setState({ saving: true });
|
||||||
@@ -1012,10 +1149,10 @@ class ManifestForm extends PureComponent {
|
|||||||
requireDefined(data[cfg.docField].id, `${cfg.docField}.id`);
|
requireDefined(data[cfg.docField].id, `${cfg.docField}.id`);
|
||||||
this.draftAutomat?.actions?.reset();
|
this.draftAutomat?.actions?.reset();
|
||||||
await cfg.listAutomat.actions.reload();
|
await cfg.listAutomat.actions.reload();
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return false;
|
||||||
enqueueSnackbar(cfg.createdMsg, { variant: 'success' });
|
enqueueSnackbar(cfg.createdMsg, { variant: 'success' });
|
||||||
this.props.navigate(cfg.editPath(data[cfg.docField].id));
|
this.props.navigate(cfg.editPath(data[cfg.docField].id));
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await api.update(docId, body);
|
const data = await api.update(docId, body);
|
||||||
@@ -1023,7 +1160,7 @@ class ManifestForm extends PureComponent {
|
|||||||
requireObject(data[cfg.docField], cfg.docField);
|
requireObject(data[cfg.docField], cfg.docField);
|
||||||
requireDefined(data[cfg.docField].id, `${cfg.docField}.id`);
|
requireDefined(data[cfg.docField].id, `${cfg.docField}.id`);
|
||||||
await cfg.listAutomat.actions.reload();
|
await cfg.listAutomat.actions.reload();
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return false;
|
||||||
const doc = data[cfg.docField];
|
const doc = data[cfg.docField];
|
||||||
const apiLines = requireArray(doc.lines, `${cfg.docField}.lines`);
|
const apiLines = requireArray(doc.lines, `${cfg.docField}.lines`);
|
||||||
const savedDoc = {
|
const savedDoc = {
|
||||||
@@ -1051,16 +1188,18 @@ class ManifestForm extends PureComponent {
|
|||||||
saving: false,
|
saving: false,
|
||||||
});
|
});
|
||||||
enqueueSnackbar(cfg.savedMsg, { variant: 'success' });
|
enqueueSnackbar(cfg.savedMsg, { variant: 'success' });
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return false;
|
||||||
notifyError(err);
|
notifyError(err);
|
||||||
this.setState({ saving: false });
|
this.setState({ saving: false });
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderArticles() {
|
renderArticles() {
|
||||||
const cfg = this.cfg;
|
const cfg = this.cfg;
|
||||||
const { lines, saving, dirtyLineFields } = this.state;
|
const { lines, saving, dirtyLineFields, shipments } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManifestLines
|
<ManifestLines
|
||||||
@@ -1071,6 +1210,7 @@ class ManifestForm extends PureComponent {
|
|||||||
wide={4}
|
wide={4}
|
||||||
linesColor={cfg.linesColor}
|
linesColor={cfg.linesColor}
|
||||||
dirtyLineFields={dirtyLineFields || {}}
|
dirtyLineFields={dirtyLineFields || {}}
|
||||||
|
shipments={shipments}
|
||||||
onLineChange={this.handleLineChange}
|
onLineChange={this.handleLineChange}
|
||||||
onSkuBlur={this.handleSkuBlur}
|
onSkuBlur={this.handleSkuBlur}
|
||||||
onAddLine={this.handleAddLine}
|
onAddLine={this.handleAddLine}
|
||||||
@@ -1120,6 +1260,7 @@ class ManifestForm extends PureComponent {
|
|||||||
<>
|
<>
|
||||||
<PanelTileGrid>
|
<PanelTileGrid>
|
||||||
<ManifestHeader
|
<ManifestHeader
|
||||||
|
ref={this.headerRef}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
headerColor={cfg.headerColor}
|
headerColor={cfg.headerColor}
|
||||||
dateLabel={cfg.dateLabel}
|
dateLabel={cfg.dateLabel}
|
||||||
@@ -1127,10 +1268,12 @@ class ManifestForm extends PureComponent {
|
|||||||
parties={parties}
|
parties={parties}
|
||||||
partiesLoading={partiesLoading || status === 'loadingParties'}
|
partiesLoading={partiesLoading || status === 'loadingParties'}
|
||||||
partyId={partyId}
|
partyId={partyId}
|
||||||
|
partyError={this.state.partyError}
|
||||||
date={date}
|
date={date}
|
||||||
number={number}
|
number={number}
|
||||||
externalNumber={externalNumber}
|
externalNumber={externalNumber}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
|
orderCreating={this.state.orderCreating}
|
||||||
pdfUploading={pdfUploading}
|
pdfUploading={pdfUploading}
|
||||||
pdfCreating={this.state.pdfCreating}
|
pdfCreating={this.state.pdfCreating}
|
||||||
lines={lines}
|
lines={lines}
|
||||||
@@ -1143,6 +1286,9 @@ class ManifestForm extends PureComponent {
|
|||||||
onPdfUpload={kind === 'delivery' ? this.handlePdfUpload : undefined}
|
onPdfUpload={kind === 'delivery' ? this.handlePdfUpload : undefined}
|
||||||
onPdfDelete={this.handleAttachmentDelete}
|
onPdfDelete={this.handleAttachmentDelete}
|
||||||
onPdfCreate={kind === 'quote' || kind === 'order' ? this.handleCreatePdf : undefined}
|
onPdfCreate={kind === 'quote' || kind === 'order' ? this.handleCreatePdf : undefined}
|
||||||
|
onCreateOrder={kind === 'quote' && this.props.mode === 'edit' ? this.handleCreateOrder : undefined}
|
||||||
|
onOpenCreateShipment={kind === 'order' && this.props.mode === 'edit' ? this.handleOpenCreateShipment : undefined}
|
||||||
|
deliverDisabled={kind === 'order' && this.isFullyDelivered()}
|
||||||
onSave={this.handleSave}
|
onSave={this.handleSave}
|
||||||
onPartyChange={this.handlePartyChange}
|
onPartyChange={this.handlePartyChange}
|
||||||
onDateChange={this.handleDateChange}
|
onDateChange={this.handleDateChange}
|
||||||
@@ -1163,9 +1309,12 @@ class ManifestForm extends PureComponent {
|
|||||||
shipments={this.state.shipments}
|
shipments={this.state.shipments}
|
||||||
payments={this.state.payments}
|
payments={this.state.payments}
|
||||||
attachments={this.state.attachments}
|
attachments={this.state.attachments}
|
||||||
|
deliverDisabled={this.isFullyDelivered()}
|
||||||
onPdfCreate={this.handleCreatePdf}
|
onPdfCreate={this.handleCreatePdf}
|
||||||
pdfCreating={this.state.pdfCreating}
|
pdfCreating={this.state.pdfCreating}
|
||||||
onAttachmentDelete={this.handleAttachmentDelete}
|
onAttachmentDelete={this.handleAttachmentDelete}
|
||||||
|
onOpenCreateShipment={this.handleOpenCreateShipment}
|
||||||
|
onShipmentChanged={this.handleShipmentCreated}
|
||||||
navigate={this.props.navigate}
|
navigate={this.props.navigate}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -1184,6 +1333,16 @@ class ManifestForm extends PureComponent {
|
|||||||
onClose={this.handleCloseArticleSearch}
|
onClose={this.handleCloseArticleSearch}
|
||||||
onAssign={this.handleAssignArticleForLine}
|
onAssign={this.handleAssignArticleForLine}
|
||||||
/>
|
/>
|
||||||
|
<OrderShipmentDialog
|
||||||
|
open={this.state.shipmentDialogOpen}
|
||||||
|
onClose={this.handleCloseShipmentDialog}
|
||||||
|
orderId={this.props.docId}
|
||||||
|
orderNumber={number}
|
||||||
|
externalNumber={externalNumber}
|
||||||
|
lines={lines}
|
||||||
|
shipments={this.state.shipments}
|
||||||
|
onSuccess={this.handleShipmentCreated}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ class ManifestFormLine extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleRemove() {
|
handleRemove() {
|
||||||
|
if (this.props.isOnShipment) return;
|
||||||
this.props.onRemove(this.props.line.key);
|
this.props.onRemove(this.props.line.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +309,8 @@ class ManifestFormLine extends PureComponent {
|
|||||||
type={type}
|
type={type}
|
||||||
value={val ?? ''}
|
value={val ?? ''}
|
||||||
error={isError}
|
error={isError}
|
||||||
disabled={saving}
|
disabled={saving || (id === 'sku' && Boolean(this.props.isOnShipment))}
|
||||||
|
title={id === 'sku' && this.props.isOnShipment ? 'Artikel ist bereits auf einem Lieferschein und kann nicht geändert werden' : undefined}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onChange={(e) => onChange(line.key, id, e.target.value)}
|
onChange={(e) => onChange(line.key, id, e.target.value)}
|
||||||
onBlur={(e) => this.handleBlur(e, id)}
|
onBlur={(e) => this.handleBlur(e, id)}
|
||||||
@@ -1025,19 +1027,16 @@ class ManifestFormLine extends PureComponent {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Tooltip title="Zeile entfernen">
|
<IconButton
|
||||||
<span>
|
size="small"
|
||||||
<IconButton
|
aria-label="Zeile entfernen"
|
||||||
size="small"
|
disabled={saving || Boolean(this.props.isOnShipment)}
|
||||||
aria-label="Zeile entfernen"
|
onClick={this.handleRemove}
|
||||||
disabled={saving}
|
title={this.props.isOnShipment ? 'Artikel ist bereits auf einem Lieferschein und kann nicht entfernt werden' : 'Zeile entfernen'}
|
||||||
onClick={this.handleRemove}
|
sx={{ p: 0.35 }}
|
||||||
sx={{ p: 0.35 }}
|
>
|
||||||
>
|
<DeleteOutlinedIcon sx={{ fontSize: 18 }} />
|
||||||
<DeleteOutlinedIcon sx={{ fontSize: 18 }} />
|
</IconButton>
|
||||||
</IconButton>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import TextField from '@mui/material/TextField';
|
|||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
||||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||||
|
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
||||||
|
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
|
||||||
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
||||||
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
||||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
@@ -36,6 +38,8 @@ class ManifestHeader extends PureComponent {
|
|||||||
createdParty: null,
|
createdParty: null,
|
||||||
};
|
};
|
||||||
this.fileInputRef = createRef();
|
this.fileInputRef = createRef();
|
||||||
|
this.partyInputRef = createRef();
|
||||||
|
this.focusPartyInput = this.focusPartyInput.bind(this);
|
||||||
this.handlePartyChange = this.handlePartyChange.bind(this);
|
this.handlePartyChange = this.handlePartyChange.bind(this);
|
||||||
this.handlePartyInputChange = this.handlePartyInputChange.bind(this);
|
this.handlePartyInputChange = this.handlePartyInputChange.bind(this);
|
||||||
this.handlePartyKeyDown = this.handlePartyKeyDown.bind(this);
|
this.handlePartyKeyDown = this.handlePartyKeyDown.bind(this);
|
||||||
@@ -56,6 +60,13 @@ class ManifestHeader extends PureComponent {
|
|||||||
this.setState({ partyInput: '', createdParty: null });
|
this.setState({ partyInput: '', createdParty: null });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!prevProps.partyError && this.props.partyError) {
|
||||||
|
this.partyInputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focusPartyInput() {
|
||||||
|
this.partyInputRef.current?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePartyChange(_event, option) {
|
handlePartyChange(_event, option) {
|
||||||
@@ -237,7 +248,12 @@ class ManifestHeader extends PureComponent {
|
|||||||
onExternalNumberChange,
|
onExternalNumberChange,
|
||||||
onSave,
|
onSave,
|
||||||
onRestore,
|
onRestore,
|
||||||
|
orderCreating,
|
||||||
|
onCreateOrder,
|
||||||
|
onOpenCreateShipment,
|
||||||
|
deliverDisabled,
|
||||||
actionsColor,
|
actionsColor,
|
||||||
|
partyError,
|
||||||
docId,
|
docId,
|
||||||
ledgerSummary,
|
ledgerSummary,
|
||||||
onOpenBookDialog,
|
onOpenBookDialog,
|
||||||
@@ -258,7 +274,8 @@ class ManifestHeader extends PureComponent {
|
|||||||
(this.state.createdParty?.id === partyId ? this.state.createdParty : null);
|
(this.state.createdParty?.id === partyId ? this.state.createdParty : null);
|
||||||
|
|
||||||
const isDateEmpty = !date || String(date).trim() === '';
|
const isDateEmpty = !date || String(date).trim() === '';
|
||||||
const isPartyEmpty = kind !== 'quote' && (partyId == null || partyId === '');
|
const isPartyEmpty =
|
||||||
|
(kind !== 'quote' || Boolean(partyError)) && (partyId == null || partyId === '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -342,8 +359,10 @@ class ManifestHeader extends PureComponent {
|
|||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
|
inputRef={this.partyInputRef}
|
||||||
label={dirtyFields?.partyId ? `${partyLabel} *` : partyLabel}
|
label={dirtyFields?.partyId ? `${partyLabel} *` : partyLabel}
|
||||||
error={isPartyEmpty}
|
error={isPartyEmpty}
|
||||||
|
helperText={Boolean(partyError) && isPartyEmpty ? 'Kunde für Bestellung erforderlich' : undefined}
|
||||||
onKeyDown={this.handlePartyKeyDown}
|
onKeyDown={this.handlePartyKeyDown}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
...params.slotProps,
|
...params.slotProps,
|
||||||
@@ -455,7 +474,7 @@ class ManifestHeader extends PureComponent {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color={isDirty ? 'warning' : 'primary'}
|
color={isDirty ? 'warning' : 'primary'}
|
||||||
disabled={saving}
|
disabled={saving || orderCreating}
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
sx={{
|
sx={{
|
||||||
height: 40,
|
height: 40,
|
||||||
@@ -475,7 +494,7 @@ class ManifestHeader extends PureComponent {
|
|||||||
color="warning"
|
color="warning"
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={<RestoreIcon />}
|
startIcon={<RestoreIcon />}
|
||||||
disabled={saving}
|
disabled={saving || orderCreating}
|
||||||
onClick={onRestore}
|
onClick={onRestore}
|
||||||
sx={{
|
sx={{
|
||||||
height: 34,
|
height: 34,
|
||||||
@@ -523,12 +542,56 @@ class ManifestHeader extends PureComponent {
|
|||||||
: 'Ins Lager buchen'}
|
: 'Ins Lager buchen'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{kind === 'quote' && docId != null && onCreateOrder && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outlined"
|
||||||
|
color={partyError && !partyId ? 'warning' : 'primary'}
|
||||||
|
startIcon={orderCreating ? <CircularProgress size={16} color="inherit" /> : <ShoppingCartIcon />}
|
||||||
|
disabled={saving || orderCreating || pdfCreating}
|
||||||
|
onClick={onCreateOrder}
|
||||||
|
title={!partyId ? 'Kunde für Bestellung erforderlich' : 'Bestellung aus diesem Angebot erstellen'}
|
||||||
|
sx={{
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
height: 40,
|
||||||
|
width: '100%',
|
||||||
|
px: 1.5,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{orderCreating ? 'Wird erstellt…' : 'Bestellung erstellen'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{kind === 'order' && docId != null && onOpenCreateShipment && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<LocalShippingOutlinedIcon />}
|
||||||
|
disabled={saving || pdfCreating || orderCreating || deliverDisabled}
|
||||||
|
onClick={onOpenCreateShipment}
|
||||||
|
title={deliverDisabled ? 'Alle Positionen bereits ausgeliefert' : 'Lieferschein erstellen'}
|
||||||
|
sx={{
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
height: 40,
|
||||||
|
width: '100%',
|
||||||
|
px: 1.5,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Liefern
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{(kind === 'quote' || kind === 'order') && onPdfCreate && docId != null && Boolean(number) && (
|
{(kind === 'quote' || kind === 'order') && onPdfCreate && docId != null && Boolean(number) && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={pdfCreating ? <CircularProgress size={16} color="inherit" /> : <PictureAsPdfIcon />}
|
startIcon={pdfCreating ? <CircularProgress size={16} color="inherit" /> : <PictureAsPdfIcon />}
|
||||||
disabled={saving || pdfCreating}
|
disabled={saving || pdfCreating || orderCreating}
|
||||||
onClick={onPdfCreate}
|
onClick={onPdfCreate}
|
||||||
title="PDF erstellen"
|
title="PDF erstellen"
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class ManifestLines extends PureComponent {
|
|||||||
saving,
|
saving,
|
||||||
linesColor,
|
linesColor,
|
||||||
dirtyLineFields = {},
|
dirtyLineFields = {},
|
||||||
|
shipments,
|
||||||
onLineChange,
|
onLineChange,
|
||||||
onAddLine,
|
onAddLine,
|
||||||
onRemoveLine,
|
onRemoveLine,
|
||||||
@@ -103,54 +104,67 @@ class ManifestLines extends PureComponent {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{Array.isArray(lines) &&
|
{(() => {
|
||||||
lines.map((line, index) => {
|
const shippedSkus = new Set();
|
||||||
const lineKey = line.key ?? index;
|
if (kind === 'order' && Array.isArray(shipments)) {
|
||||||
const rawSku = line.sku != null ? String(line.sku).trim() : '';
|
for (const sh of shipments) {
|
||||||
const isDelivery = kind === 'delivery';
|
for (const sl of sh.lines || []) {
|
||||||
const matched = isDelivery
|
if (sl.sku) shippedSkus.add(String(sl.sku).trim().toLowerCase());
|
||||||
? supplierArticlesByLineKey[lineKey] ||
|
}
|
||||||
(rawSku ? supplierArticlesMap[rawSku.toLowerCase()] || supplierArticlesMap[rawSku] : null)
|
}
|
||||||
: null;
|
}
|
||||||
const isLineLoading = isDelivery
|
|
||||||
? loadingLineKeys[lineKey] ||
|
|
||||||
(supplierArticlesLoading && !matched)
|
|
||||||
: false;
|
|
||||||
const ledgerLine = line.id != null ? ledgerLinesById[line.id] : null;
|
|
||||||
const lineDirtyFields =
|
|
||||||
dirtyLineFields[lineKey] ||
|
|
||||||
(line.key != null ? dirtyLineFields[line.key] : null) ||
|
|
||||||
(line.id != null ? dirtyLineFields[`id-${line.id}`] : null) ||
|
|
||||||
dirtyLineFields[`idx-${index}`] ||
|
|
||||||
null;
|
|
||||||
|
|
||||||
return (
|
return Array.isArray(lines) &&
|
||||||
<ManifestFormLine
|
lines.map((line, index) => {
|
||||||
key={lineKey}
|
const lineKey = line.key ?? index;
|
||||||
index={index}
|
const rawSku = line.sku != null ? String(line.sku).trim() : '';
|
||||||
positionIndex={index + 1}
|
const isOnShipment = kind === 'order' && Boolean(rawSku && shippedSkus.has(rawSku.toLowerCase()));
|
||||||
kind={kind}
|
const isDelivery = kind === 'delivery';
|
||||||
line={line.key != null ? line : { ...line, key: index }}
|
const matched = isDelivery
|
||||||
columns={columns}
|
? supplierArticlesByLineKey[lineKey] ||
|
||||||
saving={saving}
|
(rawSku ? supplierArticlesMap[rawSku.toLowerCase()] || supplierArticlesMap[rawSku] : null)
|
||||||
dirtyFields={lineDirtyFields}
|
: null;
|
||||||
supplierArticle={matched}
|
const isLineLoading = isDelivery
|
||||||
loading={Boolean(isLineLoading)}
|
? loadingLineKeys[lineKey] ||
|
||||||
supplierArticleBusy={Boolean(busySupplierArticleLineKeys[lineKey])}
|
(supplierArticlesLoading && !matched)
|
||||||
onChange={onLineChange}
|
: false;
|
||||||
onRemove={onRemoveLine}
|
const ledgerLine = line.id != null ? ledgerLinesById[line.id] : null;
|
||||||
onMove={onMoveLine}
|
const lineDirtyFields =
|
||||||
isFirstLine={index === 0}
|
dirtyLineFields[lineKey] ||
|
||||||
isLastLine={index === lines.length - 1}
|
(line.key != null ? dirtyLineFields[line.key] : null) ||
|
||||||
onSkuBlur={onSkuBlur}
|
(line.id != null ? dirtyLineFields[`id-${line.id}`] : null) ||
|
||||||
onCreateArticle={onCreateArticle}
|
dirtyLineFields[`idx-${index}`] ||
|
||||||
onSearchArticle={onSearchArticle}
|
null;
|
||||||
onRemoveSupplierArticle={onRemoveSupplierArticle}
|
|
||||||
ledgerLine={ledgerLine}
|
return (
|
||||||
onBookLine={onBookLine}
|
<ManifestFormLine
|
||||||
/>
|
key={lineKey}
|
||||||
);
|
index={index}
|
||||||
})}
|
positionIndex={index + 1}
|
||||||
|
kind={kind}
|
||||||
|
line={line.key != null ? line : { ...line, key: index }}
|
||||||
|
columns={columns}
|
||||||
|
saving={saving}
|
||||||
|
dirtyFields={lineDirtyFields}
|
||||||
|
supplierArticle={matched}
|
||||||
|
loading={Boolean(isLineLoading)}
|
||||||
|
supplierArticleBusy={Boolean(busySupplierArticleLineKeys[lineKey])}
|
||||||
|
isOnShipment={isOnShipment}
|
||||||
|
onChange={onLineChange}
|
||||||
|
onRemove={onRemoveLine}
|
||||||
|
onMove={onMoveLine}
|
||||||
|
isFirstLine={index === 0}
|
||||||
|
isLastLine={index === lines.length - 1}
|
||||||
|
onSkuBlur={onSkuBlur}
|
||||||
|
onCreateArticle={onCreateArticle}
|
||||||
|
onSearchArticle={onSearchArticle}
|
||||||
|
onRemoveSupplierArticle={onRemoveSupplierArticle}
|
||||||
|
ledgerLine={ledgerLine}
|
||||||
|
onBookLine={onBookLine}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
@@ -173,14 +187,20 @@ class ManifestLines extends PureComponent {
|
|||||||
Artikel suchen
|
Artikel suchen
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
{(() => {
|
||||||
size="small"
|
const hasShippedLines = kind === 'order' && Array.isArray(shipments) && shipments.some((sh) => (sh.lines?.length || 0) > 0);
|
||||||
startIcon={<DeleteSweepOutlinedIcon />}
|
return (
|
||||||
disabled={saving || !lines || lines.length === 0}
|
<Button
|
||||||
onClick={onRemoveAllLines}
|
size="small"
|
||||||
>
|
startIcon={<DeleteSweepOutlinedIcon />}
|
||||||
Alle Zeilen entfernen
|
disabled={saving || !lines || lines.length === 0 || hasShippedLines}
|
||||||
</Button>
|
onClick={onRemoveAllLines}
|
||||||
|
title={hasShippedLines ? 'Auftrag enthält bereits ausgelieferte Positionen' : 'Alle Zeilen entfernen'}
|
||||||
|
>
|
||||||
|
Alle Zeilen entfernen
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
{(totalNet > 0 || totalGross > 0) && (
|
{(totalNet > 0 || totalGross > 0) && (
|
||||||
<Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 2, pr: 0.5 }}>
|
<Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 2, pr: 0.5 }}>
|
||||||
<Typography variant="body2" sx={{ color: 'text.secondary', fontWeight: 600, fontSize: '0.8rem' }}>
|
<Typography variant="body2" sx={{ color: 'text.secondary', fontWeight: 600, fontSize: '0.8rem' }}>
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ import Chip from '@mui/material/Chip';
|
|||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
|
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
|
||||||
import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined';
|
import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||||
import { PanelTile } from 'dashgui/panels';
|
import { PanelTile } from 'dashgui/panels';
|
||||||
import { formatDate } from './manifestConfig';
|
import { formatDate } from './manifestConfig';
|
||||||
import { formatPrice } from '../../utils/price';
|
import { formatPrice } from '../../utils/price';
|
||||||
import { typeLabel, typeIcon } from './OrderPaymentList';
|
import { typeLabel, typeIcon } from './OrderPaymentList';
|
||||||
|
import ShipmentDetailDialog from './ShipmentDetailDialog';
|
||||||
import {
|
import {
|
||||||
GREEN_PANEL,
|
GREEN_PANEL,
|
||||||
YELLOW_PANEL,
|
YELLOW_PANEL,
|
||||||
@@ -25,20 +28,32 @@ import {
|
|||||||
* 2. Zahlungen (Payments)
|
* 2. Zahlungen (Payments)
|
||||||
*/
|
*/
|
||||||
export class OrderDocumentsTiles extends PureComponent {
|
export class OrderDocumentsTiles extends PureComponent {
|
||||||
handleOpenShipment = () => {
|
constructor(props) {
|
||||||
if (this.props.navigate) {
|
super(props);
|
||||||
this.props.navigate('/auftrag/ausliefern');
|
this.state = {
|
||||||
}
|
selectedShipment: null,
|
||||||
};
|
};
|
||||||
|
this.handleOpenShipment = this.handleOpenShipment.bind(this);
|
||||||
|
this.handleCloseShipment = this.handleCloseShipment.bind(this);
|
||||||
|
this.handleOpenPayments = this.handleOpenPayments.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
handleOpenPayments = () => {
|
handleOpenShipment(sh) {
|
||||||
|
this.setState({ selectedShipment: sh });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseShipment() {
|
||||||
|
this.setState({ selectedShipment: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOpenPayments() {
|
||||||
if (this.props.navigate) {
|
if (this.props.navigate) {
|
||||||
this.props.navigate('/auftrag/zahlungen');
|
this.props.navigate('/auftrag/zahlungen');
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
renderShipmentsTile() {
|
renderShipmentsTile() {
|
||||||
const { shipments = [], shipped, delivered, navigate } = this.props;
|
const { shipments = [], shipped, delivered, navigate, deliverDisabled } = this.props;
|
||||||
|
|
||||||
const statusLabel = delivered
|
const statusLabel = delivered
|
||||||
? 'Geliefert'
|
? 'Geliefert'
|
||||||
@@ -53,7 +68,7 @@ export class OrderDocumentsTiles extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<PanelTile wide={1} minHeight={220} color={GREEN_PANEL} title="Lieferscheine">
|
<PanelTile wide={1} minHeight={220} color={GREEN_PANEL} title="Lieferscheine">
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
|
||||||
<Stack direction="row" spacing={0.75} alignItems="center">
|
<Stack direction="row" spacing={0.75} sx={{ alignItems: 'center' }}>
|
||||||
<LocalShippingOutlinedIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
|
<LocalShippingOutlinedIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
|
||||||
<Chip size="small" label={statusLabel} color={statusColor} sx={{ fontWeight: 600, height: 22 }} />
|
<Chip size="small" label={statusLabel} color={statusColor} sx={{ fontWeight: 600, height: 22 }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -61,7 +76,9 @@ export class OrderDocumentsTiles extends PureComponent {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={<AddIcon />}
|
startIcon={<AddIcon />}
|
||||||
onClick={() => navigate?.('/auftrag/ausliefern')}
|
disabled={deliverDisabled}
|
||||||
|
onClick={this.props.onOpenCreateShipment || (() => navigate?.('/auftrag/ausliefern'))}
|
||||||
|
title={deliverDisabled ? 'Alle Positionen bereits ausgeliefert' : 'Lieferschein erstellen'}
|
||||||
sx={{ fontWeight: 600, textTransform: 'none', height: 26, fontSize: '0.75rem', px: 1 }}
|
sx={{ fontWeight: 600, textTransform: 'none', height: 26, fontSize: '0.75rem', px: 1 }}
|
||||||
>
|
>
|
||||||
Liefern
|
Liefern
|
||||||
@@ -79,7 +96,7 @@ export class OrderDocumentsTiles extends PureComponent {
|
|||||||
<Card
|
<Card
|
||||||
key={sh.id ?? sh.number}
|
key={sh.id ?? sh.number}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={this.handleOpenShipment}
|
onClick={() => this.handleOpenShipment(sh)}
|
||||||
sx={{
|
sx={{
|
||||||
p: 1.25,
|
p: 1.25,
|
||||||
bgcolor: WHITE,
|
bgcolor: WHITE,
|
||||||
@@ -95,9 +112,24 @@ export class OrderDocumentsTiles extends PureComponent {
|
|||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.85rem' }}>
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.85rem' }}>
|
||||||
{sh.number}
|
{sh.number}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Stack direction="row" spacing={0.5} sx={{ alignItems: 'center' }}>
|
||||||
{sh.date ? formatDate(sh.date) : '–'}
|
<Typography variant="caption" color="text.secondary">
|
||||||
</Typography>
|
{sh.date ? formatDate(sh.date) : '–'}
|
||||||
|
</Typography>
|
||||||
|
{navigate && (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
title="In Auslieferungen öffnen"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigate(`/auftrag/ausliefern?shipmentId=${sh.id}`);
|
||||||
|
}}
|
||||||
|
sx={{ p: 0.25, ml: 0.5, color: 'text.secondary', '&:hover': { color: 'primary.main' } }}
|
||||||
|
>
|
||||||
|
<OpenInNewIcon sx={{ fontSize: 13 }} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{sh.externalNumber ? (
|
{sh.externalNumber ? (
|
||||||
@@ -142,7 +174,7 @@ export class OrderDocumentsTiles extends PureComponent {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={<LocalShippingOutlinedIcon />}
|
startIcon={<LocalShippingOutlinedIcon />}
|
||||||
onClick={() => navigate?.('/auftrag/ausliefern')}
|
onClick={this.props.onOpenCreateShipment || (() => navigate?.('/auftrag/ausliefern'))}
|
||||||
sx={{ textTransform: 'none', fontWeight: 600, fontSize: '0.75rem' }}
|
sx={{ textTransform: 'none', fontWeight: 600, fontSize: '0.75rem' }}
|
||||||
>
|
>
|
||||||
Lieferschein anlegen
|
Lieferschein anlegen
|
||||||
@@ -166,7 +198,7 @@ export class OrderDocumentsTiles extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<PanelTile wide={1} minHeight={220} color={YELLOW_PANEL} title="Zahlungen">
|
<PanelTile wide={1} minHeight={220} color={YELLOW_PANEL} title="Zahlungen">
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
|
||||||
<Stack direction="row" spacing={0.75} alignItems="center">
|
<Stack direction="row" spacing={0.75} sx={{ alignItems: 'center' }}>
|
||||||
<PaymentsOutlinedIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
|
<PaymentsOutlinedIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
|
||||||
<Chip size="small" label={statusLabel} color={statusColor} sx={{ fontWeight: 600, height: 22 }} />
|
<Chip size="small" label={statusLabel} color={statusColor} sx={{ fontWeight: 600, height: 22 }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -239,7 +271,7 @@ export class OrderDocumentsTiles extends PureComponent {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ minWidth: 0, overflow: 'hidden' }}>
|
<Stack direction="row" spacing={0.75} sx={{ alignItems: 'center', minWidth: 0, overflow: 'hidden' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', color: 'text.secondary', flexShrink: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', color: 'text.secondary', flexShrink: 0 }}>
|
||||||
{typeIcon(pType, { fontSize: 16 })}
|
{typeIcon(pType, { fontSize: 16 })}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -248,7 +280,7 @@ export class OrderDocumentsTiles extends PureComponent {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack direction="row" spacing={1} alignItems="center" sx={{ flexShrink: 0, ml: 1 }}>
|
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flexShrink: 0, ml: 1 }}>
|
||||||
{pay.date ? (
|
{pay.date ? (
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{formatDate(pay.date)}
|
{formatDate(pay.date)}
|
||||||
@@ -291,6 +323,13 @@ export class OrderDocumentsTiles extends PureComponent {
|
|||||||
<>
|
<>
|
||||||
{this.renderShipmentsTile()}
|
{this.renderShipmentsTile()}
|
||||||
{this.renderPaymentsTile()}
|
{this.renderPaymentsTile()}
|
||||||
|
<ShipmentDetailDialog
|
||||||
|
open={Boolean(this.state.selectedShipment)}
|
||||||
|
shipment={this.state.selectedShipment}
|
||||||
|
onClose={this.handleCloseShipment}
|
||||||
|
onNavigate={this.props.navigate}
|
||||||
|
onChanged={this.props.onShipmentChanged}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
465
src/components/manifest/OrderShipmentDialog.jsx
Normal file
465
src/components/manifest/OrderShipmentDialog.jsx
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
import { PureComponent } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Table from '@mui/material/Table';
|
||||||
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
import TableCell from '@mui/material/TableCell';
|
||||||
|
import TableContainer from '@mui/material/TableContainer';
|
||||||
|
import TableHead from '@mui/material/TableHead';
|
||||||
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
|
||||||
|
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
import AppDialog from '../AppDialog';
|
||||||
|
import { order as orderApi } from '../../api';
|
||||||
|
import { notifyError } from '../../utils/notify';
|
||||||
|
import { todayInputValue } from './manifestConfig';
|
||||||
|
|
||||||
|
// MARK: helper to sum already shipped quantities per SKU across order shipments
|
||||||
|
function computeShippedBySku(shipments) {
|
||||||
|
const map = {};
|
||||||
|
for (const sh of shipments || []) {
|
||||||
|
for (const sl of sh.lines || []) {
|
||||||
|
if (sl.sku) {
|
||||||
|
map[sl.sku] = (map[sl.sku] || 0) + (Number(sl.amount) || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog for selecting items and quantities to create a shipment from a sales order.
|
||||||
|
* Calls POST /api/order/:id/shipment.
|
||||||
|
*/
|
||||||
|
export default class OrderShipmentDialog extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const { initialAmounts, initialSelected } = this.calculateInitialState(props);
|
||||||
|
this.state = {
|
||||||
|
date: todayInputValue(),
|
||||||
|
externalNumber: '',
|
||||||
|
lineAmounts: initialAmounts,
|
||||||
|
selectedSkus: initialSelected,
|
||||||
|
saving: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleAmountChange = this.handleAmountChange.bind(this);
|
||||||
|
this.handleToggleSku = this.handleToggleSku.bind(this);
|
||||||
|
this.handleToggleAll = this.handleToggleAll.bind(this);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
this.handleShipAll = this.handleShipAll.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (!prevProps.open && this.props.open) {
|
||||||
|
const { initialAmounts, initialSelected } = this.calculateInitialState(this.props);
|
||||||
|
this.setState({
|
||||||
|
date: todayInputValue(),
|
||||||
|
externalNumber: '',
|
||||||
|
lineAmounts: initialAmounts,
|
||||||
|
selectedSkus: initialSelected,
|
||||||
|
saving: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateInitialState(props) {
|
||||||
|
const shippedBySku = computeShippedBySku(props.shipments);
|
||||||
|
const initialAmounts = {};
|
||||||
|
const initialSelected = new Set();
|
||||||
|
|
||||||
|
for (const l of props.lines || []) {
|
||||||
|
if (!l.sku) continue;
|
||||||
|
const ordered = Number(l.amount) || 0;
|
||||||
|
const shipped = shippedBySku[l.sku] || 0;
|
||||||
|
const rem = Math.max(0, ordered - shipped);
|
||||||
|
if (rem > 0) {
|
||||||
|
initialAmounts[l.sku] = String(rem);
|
||||||
|
initialSelected.add(l.sku);
|
||||||
|
} else {
|
||||||
|
initialAmounts[l.sku] = '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { initialAmounts, initialSelected };
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggleSku(sku, remaining) {
|
||||||
|
this.setState((prev) => {
|
||||||
|
const nextSelected = new Set(prev.selectedSkus);
|
||||||
|
const nextAmounts = { ...prev.lineAmounts };
|
||||||
|
if (nextSelected.has(sku)) {
|
||||||
|
nextSelected.delete(sku);
|
||||||
|
} else {
|
||||||
|
nextSelected.add(sku);
|
||||||
|
if (!Number(nextAmounts[sku]) || Number(nextAmounts[sku]) <= 0) {
|
||||||
|
nextAmounts[sku] = String(remaining);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { selectedSkus: nextSelected, lineAmounts: nextAmounts };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggleAll(openItems) {
|
||||||
|
this.setState((prev) => {
|
||||||
|
const allSelected = openItems.length > 0 && openItems.every((it) => prev.selectedSkus.has(it.sku));
|
||||||
|
const nextSelected = new Set();
|
||||||
|
const nextAmounts = { ...prev.lineAmounts };
|
||||||
|
|
||||||
|
if (!allSelected) {
|
||||||
|
for (const it of openItems) {
|
||||||
|
nextSelected.add(it.sku);
|
||||||
|
if (!Number(nextAmounts[it.sku]) || Number(nextAmounts[it.sku]) <= 0) {
|
||||||
|
nextAmounts[it.sku] = String(it.remaining);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { selectedSkus: nextSelected, lineAmounts: nextAmounts };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAmountChange(sku, val) {
|
||||||
|
this.setState((prev) => {
|
||||||
|
const nextAmounts = { ...prev.lineAmounts, [sku]: val };
|
||||||
|
const nextSelected = new Set(prev.selectedSkus);
|
||||||
|
const num = Number(val);
|
||||||
|
if (num > 0) {
|
||||||
|
nextSelected.add(sku);
|
||||||
|
} else {
|
||||||
|
nextSelected.delete(sku);
|
||||||
|
}
|
||||||
|
return { lineAmounts: nextAmounts, selectedSkus: nextSelected };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: calls orderApi.createShipment from ./api/index.js
|
||||||
|
// #region POST /api/order/:id/shipment — submit selected lines
|
||||||
|
async handleSubmit(e) {
|
||||||
|
e?.preventDefault?.();
|
||||||
|
const { orderId, onClose, onSuccess } = this.props;
|
||||||
|
const { date, externalNumber, lineAmounts, selectedSkus, saving } = this.state;
|
||||||
|
if (saving || !orderId) return;
|
||||||
|
|
||||||
|
const linesToShip = [];
|
||||||
|
for (const sku of selectedSkus) {
|
||||||
|
const amount = Number(lineAmounts[sku]);
|
||||||
|
if (amount > 0) {
|
||||||
|
linesToShip.push({ sku, amount });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linesToShip.length === 0) {
|
||||||
|
enqueueSnackbar('Bitte wählen Sie mindestens einen Artikel mit Liefermenge > 0 aus.', { variant: 'warning' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ saving: true, error: null });
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
date: date || undefined,
|
||||||
|
externalNumber: externalNumber.trim() || undefined,
|
||||||
|
lines: linesToShip,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await orderApi.createShipment(orderId, payload);
|
||||||
|
const shipment = res?.shipment;
|
||||||
|
const shNum = shipment?.number || '';
|
||||||
|
|
||||||
|
enqueueSnackbar(
|
||||||
|
shNum ? `Lieferschein ${shNum} erfolgreich angelegt!` : 'Lieferschein erfolgreich angelegt!',
|
||||||
|
{ variant: 'success' },
|
||||||
|
);
|
||||||
|
|
||||||
|
onSuccess?.(shipment);
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ saving: false, error: err?.message || String(err) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// MARK: calls orderApi.createShipment to ship all remaining lines
|
||||||
|
// #region POST /api/order/:id/shipment — ship all remaining
|
||||||
|
async handleShipAll() {
|
||||||
|
const { orderId, onClose, onSuccess } = this.props;
|
||||||
|
const { date, externalNumber, saving } = this.state;
|
||||||
|
if (saving || !orderId) return;
|
||||||
|
|
||||||
|
this.setState({ saving: true, error: null });
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
date: date || undefined,
|
||||||
|
externalNumber: externalNumber.trim() || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await orderApi.createShipment(orderId, payload);
|
||||||
|
const shipment = res?.shipment;
|
||||||
|
const shNum = shipment?.number || '';
|
||||||
|
|
||||||
|
enqueueSnackbar(
|
||||||
|
shNum ? `Lieferschein ${shNum} erfolgreich angelegt!` : 'Lieferschein erfolgreich angelegt!',
|
||||||
|
{ variant: 'success' },
|
||||||
|
);
|
||||||
|
|
||||||
|
onSuccess?.(shipment);
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ saving: false, error: err?.message || String(err) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { open, onClose, orderNumber, lines = [], shipments = [] } = this.props;
|
||||||
|
const { date, externalNumber, lineAmounts, selectedSkus, saving, error } = this.state;
|
||||||
|
|
||||||
|
const shippedBySku = computeShippedBySku(shipments);
|
||||||
|
const activeLines = lines.filter((l) => Boolean(l.sku));
|
||||||
|
|
||||||
|
const items = activeLines.map((line) => {
|
||||||
|
const ordered = Number(line.amount) || 0;
|
||||||
|
const shipped = shippedBySku[line.sku] || 0;
|
||||||
|
const remaining = Math.max(0, ordered - shipped);
|
||||||
|
const isSelected = selectedSkus.has(line.sku);
|
||||||
|
const shipAmount = lineAmounts[line.sku] ?? (remaining > 0 ? String(remaining) : '0');
|
||||||
|
return {
|
||||||
|
...line,
|
||||||
|
ordered,
|
||||||
|
shipped,
|
||||||
|
remaining,
|
||||||
|
isSelected,
|
||||||
|
shipAmount,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const openItems = items.filter((it) => it.remaining > 0);
|
||||||
|
const allShipped = activeLines.length > 0 && openItems.length === 0;
|
||||||
|
const isAllSelected = openItems.length > 0 && openItems.every((it) => it.isSelected);
|
||||||
|
const selectedCount = openItems.filter((it) => it.isSelected).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppDialog open={open} onClose={saving ? undefined : onClose} maxWidth="md" fullWidth>
|
||||||
|
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Stack direction="row" spacing={1.5} sx={{ alignItems: 'center' }}>
|
||||||
|
<LocalShippingOutlinedIcon color="primary" />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" component="div" sx={{ fontWeight: 700, lineHeight: 1.2 }}>
|
||||||
|
Lieferschein erstellen
|
||||||
|
</Typography>
|
||||||
|
{orderNumber ? (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Zu Auftrag / Bestellung: <strong>{orderNumber}</strong>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={saving}
|
||||||
|
size="small"
|
||||||
|
sx={{ color: (theme) => theme.palette.grey[500] }}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent dividers sx={{ p: 2.5 }}>
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }} onClose={() => this.setState({ error: null })}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allShipped && (
|
||||||
|
<Alert severity="success" icon={<CheckCircleOutlineOutlinedIcon />} sx={{ mb: 2 }}>
|
||||||
|
Alle Artikel dieses Auftrags wurden bereits vollständig geliefert.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Header controls: date and external reference */}
|
||||||
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mb: 2.5 }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
type="date"
|
||||||
|
label="Lieferdatum"
|
||||||
|
value={date}
|
||||||
|
disabled={saving}
|
||||||
|
onChange={(e) => this.setState({ date: e.target.value })}
|
||||||
|
slotProps={{ inputLabel: { shrink: true } }}
|
||||||
|
sx={{ minWidth: 170 }}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label="Lieferschein-Nr. / Externe Referenz (optional)"
|
||||||
|
placeholder="z.B. LS-2026-0042"
|
||||||
|
value={externalNumber}
|
||||||
|
disabled={saving}
|
||||||
|
onChange={(e) => this.setState({ externalNumber: e.target.value })}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Items selection section */}
|
||||||
|
<Box sx={{ mb: 1, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
|
||||||
|
Zu liefernde Artikel ({selectedCount} von {openItems.length} ausgewählt)
|
||||||
|
</Typography>
|
||||||
|
{openItems.length > 0 && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
disabled={saving}
|
||||||
|
onClick={() => this.handleToggleAll(openItems)}
|
||||||
|
sx={{ textTransform: 'none', fontWeight: 600, fontSize: '0.8rem' }}
|
||||||
|
>
|
||||||
|
{isAllSelected ? 'Auswahl aufheben' : 'Alle offenen auswählen'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<TableContainer sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead sx={{ bgcolor: 'action.hover' }}>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell padding="checkbox" sx={{ width: 44 }}>
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={isAllSelected}
|
||||||
|
indeterminate={selectedCount > 0 && !isAllSelected}
|
||||||
|
disabled={saving || openItems.length === 0}
|
||||||
|
onChange={() => this.handleToggleAll(openItems)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600 }}>SKU</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600 }}>Artikel</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600 }}>Bestellt</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600 }}>Geliefert</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600 }}>Offen</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600, width: 130 }}>Liefermenge</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} align="center" sx={{ py: 3, color: 'text.secondary' }}>
|
||||||
|
Keine Artikel im Auftrag vorhanden.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
items.map((item) => {
|
||||||
|
const isClosed = item.remaining <= 0;
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={item.sku}
|
||||||
|
hover={!isClosed}
|
||||||
|
sx={{
|
||||||
|
opacity: isClosed ? 0.55 : 1,
|
||||||
|
bgcolor: isClosed ? 'action.hover' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell padding="checkbox">
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={item.isSelected && !isClosed}
|
||||||
|
disabled={saving || isClosed}
|
||||||
|
onChange={() => this.handleToggleSku(item.sku, item.remaining)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600 }}>{item.sku}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||||
|
{item.name || '–'}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">{item.ordered}</TableCell>
|
||||||
|
<TableCell align="right">{item.shipped}</TableCell>
|
||||||
|
<TableCell
|
||||||
|
align="right"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 700,
|
||||||
|
color: item.remaining > 0 ? 'primary.main' : 'text.disabled',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.remaining}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
{isClosed ? (
|
||||||
|
<Chip size="small" label="Geliefert" color="success" variant="outlined" sx={{ height: 22, fontSize: '0.7rem' }} />
|
||||||
|
) : (
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
type="number"
|
||||||
|
disabled={saving || !item.isSelected}
|
||||||
|
value={item.shipAmount}
|
||||||
|
onChange={(e) => this.handleAmountChange(item.sku, e.target.value)}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
min: 0,
|
||||||
|
max: item.remaining,
|
||||||
|
step: 'any',
|
||||||
|
style: { textAlign: 'right', fontWeight: 600 },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{ width: 100 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions sx={{ p: 2, justifyContent: 'space-between' }}>
|
||||||
|
<Button variant="outlined" onClick={onClose} disabled={saving}>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1.5}>
|
||||||
|
{!allShipped && openItems.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
disabled={saving}
|
||||||
|
onClick={this.handleShipAll}
|
||||||
|
sx={{ textTransform: 'none', fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
Alles liefern
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={saving || allShipped || selectedCount === 0}
|
||||||
|
startIcon={saving ? <CircularProgress size={16} color="inherit" /> : <LocalShippingOutlinedIcon />}
|
||||||
|
onClick={this.handleSubmit}
|
||||||
|
sx={{ textTransform: 'none', fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
{saving ? 'Wird erstellt…' : 'Lieferschein erstellen'}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</DialogActions>
|
||||||
|
</AppDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
531
src/components/manifest/ShipmentDetailDialog.jsx
Normal file
531
src/components/manifest/ShipmentDetailDialog.jsx
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
import { PureComponent } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Table from '@mui/material/Table';
|
||||||
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
import TableCell from '@mui/material/TableCell';
|
||||||
|
import TableContainer from '@mui/material/TableContainer';
|
||||||
|
import TableHead from '@mui/material/TableHead';
|
||||||
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
|
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
||||||
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||||
|
import Inventory2Icon from '@mui/icons-material/Inventory2';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
import AppDialog from '../AppDialog';
|
||||||
|
import { shipment as shipmentApi, shipping as shippingApi } from '../../api';
|
||||||
|
import { notifyError } from '../../utils/notify';
|
||||||
|
import { formatDate } from './manifestConfig';
|
||||||
|
import orderAutomat from '../../automats/orderAutomat';
|
||||||
|
import shipmentAutomat from '../../automats/shipmentAutomat';
|
||||||
|
import { SURFACE_RAISED, WHITE } from '../../colors';
|
||||||
|
|
||||||
|
const CARRIERS = ['DHL', 'UPS', 'DPD', 'GLS', 'Hermes', 'Spedition', 'Selbstabholung'];
|
||||||
|
|
||||||
|
// MARK: <AddPackageDialog/>
|
||||||
|
class AddPackageDialog extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
carrier: 'DHL',
|
||||||
|
trackingCode: '',
|
||||||
|
type: 'parcel',
|
||||||
|
state: 'draft',
|
||||||
|
saving: false,
|
||||||
|
};
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSubmit() {
|
||||||
|
const { shipment, onClose, onSuccess } = this.props;
|
||||||
|
const { carrier, trackingCode, type, state } = this.state;
|
||||||
|
if (!shipment?.id) return;
|
||||||
|
|
||||||
|
this.setState({ saving: true });
|
||||||
|
try {
|
||||||
|
await shippingApi.create(shipment.id, {
|
||||||
|
carrier: carrier.trim() || undefined,
|
||||||
|
trackingCode: trackingCode.trim() || undefined,
|
||||||
|
type,
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
enqueueSnackbar('Paket angelegt.', { variant: 'success' });
|
||||||
|
await Promise.all([
|
||||||
|
orderAutomat.actions.reload(),
|
||||||
|
shipmentAutomat.actions.reload(),
|
||||||
|
]);
|
||||||
|
onSuccess?.();
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ saving: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { open, onClose, shipment } = this.props;
|
||||||
|
const { carrier, trackingCode, type, state, saving } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppDialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
||||||
|
<Box sx={{ p: 2.5 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', pb: 2 }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||||
|
Paket zu Lieferschein hinzufügen
|
||||||
|
</Typography>
|
||||||
|
<IconButton onClick={onClose} size="small">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
Lieferschein: <strong>{shipment?.number}</strong>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Versanddienstleister (Carrier)"
|
||||||
|
size="small"
|
||||||
|
value={carrier}
|
||||||
|
onChange={(e) => this.setState({ carrier: e.target.value })}
|
||||||
|
>
|
||||||
|
{CARRIERS.map((c) => (
|
||||||
|
<MenuItem key={c} value={c}>{c}</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Tracking-Code / Sendungsnummer"
|
||||||
|
size="small"
|
||||||
|
value={trackingCode}
|
||||||
|
onChange={(e) => this.setState({ trackingCode: e.target.value })}
|
||||||
|
placeholder="z.B. 00340434234234234"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Paketart"
|
||||||
|
size="small"
|
||||||
|
value={type}
|
||||||
|
onChange={(e) => this.setState({ type: e.target.value })}
|
||||||
|
>
|
||||||
|
<MenuItem value="parcel">Paket (Parcel)</MenuItem>
|
||||||
|
<MenuItem value="pallet">Palette (Pallet)</MenuItem>
|
||||||
|
<MenuItem value="letter">Brief / Dokument</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Status"
|
||||||
|
size="small"
|
||||||
|
value={state}
|
||||||
|
onChange={(e) => this.setState({ state: e.target.value })}
|
||||||
|
helperText={state === 'shipped' ? 'Bucht bei Auswahl direkt das Lager ab.' : undefined}
|
||||||
|
>
|
||||||
|
<MenuItem value="draft">Entwurf (draft)</MenuItem>
|
||||||
|
<MenuItem value="packed">Gepackt (packed)</MenuItem>
|
||||||
|
<MenuItem value="shipped">Versendet (shipped — Lagerabbuchung)</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1.5 }}>
|
||||||
|
<Button variant="outlined" onClick={onClose} disabled={saving}>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={this.handleSubmit} disabled={saving}>
|
||||||
|
{saving ? 'Wird gespeichert…' : 'Paket anlegen'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</AppDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal to view and manage an existing Lieferschein (Shipment) directly from the Order page.
|
||||||
|
*/
|
||||||
|
export default class ShipmentDetailDialog extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
shipment: props.shipment || null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
addPackageDialogOpen: false,
|
||||||
|
busyAction: false,
|
||||||
|
};
|
||||||
|
this.handleRefresh = this.handleRefresh.bind(this);
|
||||||
|
this.handleDeletePackage = this.handleDeletePackage.bind(this);
|
||||||
|
this.handleMarkPackageShipped = this.handleMarkPackageShipped.bind(this);
|
||||||
|
this.handleStateChange = this.handleStateChange.bind(this);
|
||||||
|
this.handleNavigateFull = this.handleNavigateFull.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.props.open && this.props.shipment?.id) {
|
||||||
|
void this.handleRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (!prevProps.open && this.props.open) {
|
||||||
|
this.setState({ shipment: this.props.shipment, error: null });
|
||||||
|
if (this.props.shipment?.id) {
|
||||||
|
void this.handleRefresh();
|
||||||
|
}
|
||||||
|
} else if (prevProps.shipment?.id !== this.props.shipment?.id && this.props.shipment?.id) {
|
||||||
|
this.setState({ shipment: this.props.shipment });
|
||||||
|
void this.handleRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: calls shipmentApi.get from ./api/index.js
|
||||||
|
// #region GET /api/shipment/:id — refresh details
|
||||||
|
async handleRefresh() {
|
||||||
|
const shipmentId = this.props.shipment?.id;
|
||||||
|
if (!shipmentId) return;
|
||||||
|
|
||||||
|
this.setState({ loading: true, error: null });
|
||||||
|
try {
|
||||||
|
const res = await shipmentApi.get(shipmentId);
|
||||||
|
const enriched = res?.shipment || res;
|
||||||
|
this.setState({ shipment: enriched, loading: false });
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ error: err?.message || String(err), loading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
async handleDeletePackage(pkg) {
|
||||||
|
if (!window.confirm(`Paket „#${pkg.number || pkg.id}“ wirklich löschen?`)) return;
|
||||||
|
this.setState({ busyAction: true });
|
||||||
|
try {
|
||||||
|
await shippingApi.delete(pkg.id);
|
||||||
|
enqueueSnackbar('Paket gelöscht.', { variant: 'success' });
|
||||||
|
await Promise.all([
|
||||||
|
orderAutomat.actions.reload(),
|
||||||
|
shipmentAutomat.actions.reload(),
|
||||||
|
]);
|
||||||
|
this.props.onChanged?.();
|
||||||
|
await this.handleRefresh();
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
} finally {
|
||||||
|
this.setState({ busyAction: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleMarkPackageShipped(pkg) {
|
||||||
|
this.setState({ busyAction: true });
|
||||||
|
try {
|
||||||
|
await shippingApi.update(pkg.id, { state: 'shipped' });
|
||||||
|
enqueueSnackbar('Paket als versendet markiert (Lagerbestand abgebucht).', { variant: 'success' });
|
||||||
|
await Promise.all([
|
||||||
|
orderAutomat.actions.reload(),
|
||||||
|
shipmentAutomat.actions.reload(),
|
||||||
|
]);
|
||||||
|
this.props.onChanged?.();
|
||||||
|
await this.handleRefresh();
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
} finally {
|
||||||
|
this.setState({ busyAction: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStateChange(newState) {
|
||||||
|
const shipmentId = this.state.shipment?.id;
|
||||||
|
if (!shipmentId) return;
|
||||||
|
|
||||||
|
this.setState({ busyAction: true });
|
||||||
|
try {
|
||||||
|
await shipmentApi.update(shipmentId, { state: newState });
|
||||||
|
this.setState((prev) => ({
|
||||||
|
shipment: { ...prev.shipment, state: newState },
|
||||||
|
}));
|
||||||
|
const label = newState === 'delivered' ? 'Geliefert' : newState === 'returned' ? 'Retoure' : 'Neu';
|
||||||
|
enqueueSnackbar(`Lieferstatus geändert: ${label}`, { variant: 'success' });
|
||||||
|
await Promise.all([
|
||||||
|
orderAutomat.actions.reload(),
|
||||||
|
shipmentAutomat.actions.reload(),
|
||||||
|
]);
|
||||||
|
this.props.onChanged?.();
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
} finally {
|
||||||
|
this.setState({ busyAction: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNavigateFull() {
|
||||||
|
const { shipment, onClose, onNavigate } = this.props;
|
||||||
|
onClose?.();
|
||||||
|
if (onNavigate && shipment?.id) {
|
||||||
|
onNavigate(`/auftrag/ausliefern?shipmentId=${shipment.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { open, onClose } = this.props;
|
||||||
|
const { shipment, loading, error, addPackageDialogOpen, busyAction } = this.state;
|
||||||
|
|
||||||
|
if (!shipment) return null;
|
||||||
|
|
||||||
|
const shLines = Array.isArray(shipment.lines) ? shipment.lines : [];
|
||||||
|
const shPackages = Array.isArray(shipment.shippings) ? shipment.shippings : [];
|
||||||
|
const hasShippedPackages = shPackages.some((p) => p.state === 'shipped');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppDialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||||
|
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Stack direction="row" spacing={1.5} sx={{ alignItems: 'center' }}>
|
||||||
|
<LocalShippingIcon color="primary" sx={{ fontSize: 28 }} />
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, lineHeight: 1.2 }}>
|
||||||
|
{shipment.number || 'Lieferschein'}
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
color={hasShippedPackages ? 'success' : 'default'}
|
||||||
|
label={hasShippedPackages ? 'Im Versand' : 'Bereitstellung'}
|
||||||
|
sx={{ fontWeight: 600, height: 20, fontSize: '0.7rem' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{shipment.date ? `Datum: ${formatDate(shipment.date)}` : ''}
|
||||||
|
{shipment.externalNumber ? ` • Ref: ${shipment.externalNumber}` : ''}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
label="Lieferstatus"
|
||||||
|
value={shipment.state || 'new'}
|
||||||
|
onChange={(e) => this.handleStateChange(e.target.value)}
|
||||||
|
disabled={busyAction}
|
||||||
|
sx={{ minWidth: 130 }}
|
||||||
|
>
|
||||||
|
<MenuItem value="new">Neu</MenuItem>
|
||||||
|
<MenuItem value="delivered">Geliefert</MenuItem>
|
||||||
|
<MenuItem value="returned">Retoure</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={onClose}
|
||||||
|
size="small"
|
||||||
|
sx={{ color: (theme) => theme.palette.grey[500] }}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent dividers sx={{ p: 2.5, display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" onClose={() => this.setState({ error: null })}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Shipped Article Positions */}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1 }}>
|
||||||
|
Gelieferte Artikel-Positionen ({shLines.length})
|
||||||
|
</Typography>
|
||||||
|
<TableContainer sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1.5, bgcolor: WHITE }}>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead sx={{ bgcolor: SURFACE_RAISED }}>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell sx={{ fontWeight: 600, py: 0.75 }}>SKU</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600, py: 0.75 }}>Bezeichnung</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600, py: 0.75 }}>Menge</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{shLines.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={3} align="center" sx={{ py: 2, color: 'text.secondary' }}>
|
||||||
|
{loading ? 'Lädt Positionen…' : 'Keine Positionen auf diesem Lieferschein.'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
shLines.map((line, idx) => (
|
||||||
|
<TableRow key={line.id || idx}>
|
||||||
|
<TableCell sx={{ py: 0.75, fontWeight: 500 }}>{line.sku}</TableCell>
|
||||||
|
<TableCell sx={{ py: 0.75 }}>{line.name || '–'}</TableCell>
|
||||||
|
<TableCell align="right" sx={{ py: 0.75, fontWeight: 700 }}>
|
||||||
|
{line.amount}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Shipping Packages / Tracking */}
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
|
||||||
|
Pakete & Sendungsverfolgung ({shPackages.length})
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={() => this.setState({ addPackageDialogOpen: true })}
|
||||||
|
disabled={busyAction}
|
||||||
|
sx={{ textTransform: 'none', fontWeight: 600, height: 28, fontSize: '0.75rem' }}
|
||||||
|
>
|
||||||
|
Paket hinzufügen
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{shPackages.length === 0 ? (
|
||||||
|
<Box sx={{ p: 2.5, textAlign: 'center', bgcolor: WHITE, borderRadius: 1.5, border: '1px dashed', borderColor: 'divider' }}>
|
||||||
|
<Inventory2Icon sx={{ color: 'text.secondary', fontSize: 28, mb: 0.5 }} />
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Noch keine Pakete erfasst.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Stack spacing={1}>
|
||||||
|
{shPackages.map((pkg) => {
|
||||||
|
const isShipped = pkg.state === 'shipped';
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={pkg.id}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
p: 1.5,
|
||||||
|
bgcolor: WHITE,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||||
|
<Chip size="small" label={`#${pkg.number || pkg.id}`} sx={{ fontWeight: 700 }} />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
|
||||||
|
{pkg.carrier || 'DHL'} ({pkg.type === 'pallet' ? 'Palette' : 'Paket'})
|
||||||
|
</Typography>
|
||||||
|
{pkg.trackingCode ? (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.25 }}>
|
||||||
|
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 600, color: 'primary.main' }}>
|
||||||
|
{pkg.trackingCode}
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(pkg.trackingCode);
|
||||||
|
enqueueSnackbar('Tracking-Code kopiert!', { variant: 'info' });
|
||||||
|
}}
|
||||||
|
sx={{ p: 0.25 }}
|
||||||
|
>
|
||||||
|
<ContentCopyIcon sx={{ fontSize: 13 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Ohne Tracking
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
{isShipped ? (
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
color="success"
|
||||||
|
icon={<CheckCircleOutlineOutlinedIcon />}
|
||||||
|
label="Versendet (Lager abgebucht)"
|
||||||
|
sx={{ fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
disabled={busyAction}
|
||||||
|
onClick={() => this.handleMarkPackageShipped(pkg)}
|
||||||
|
sx={{ textTransform: 'none', py: 0.25, px: 1, height: 26, fontSize: '0.75rem' }}
|
||||||
|
>
|
||||||
|
Als versendet buchen
|
||||||
|
</Button>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
disabled={busyAction}
|
||||||
|
onClick={() => this.handleDeletePackage(pkg)}
|
||||||
|
>
|
||||||
|
<DeleteOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions sx={{ p: 2, justifyContent: 'space-between' }}>
|
||||||
|
<Button variant="outlined" onClick={onClose}>
|
||||||
|
Schließen
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{this.props.onNavigate && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<OpenInNewIcon />}
|
||||||
|
onClick={this.handleNavigateFull}
|
||||||
|
sx={{ textTransform: 'none', fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
In Auslieferungen öffnen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
|
||||||
|
{addPackageDialogOpen && (
|
||||||
|
<AddPackageDialog
|
||||||
|
open={addPackageDialogOpen}
|
||||||
|
shipment={shipment}
|
||||||
|
onClose={() => this.setState({ addPackageDialogOpen: false })}
|
||||||
|
onSuccess={async () => {
|
||||||
|
this.props.onChanged?.();
|
||||||
|
await this.handleRefresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AppDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,8 @@ export { default as TextEditorPanel } from './TextEditorPanel';
|
|||||||
export { default as PdfViewerPanel } from './PdfViewerPanel';
|
export { default as PdfViewerPanel } from './PdfViewerPanel';
|
||||||
export { default as ArticleSearchDialog } from './ArticleSearchDialog';
|
export { default as ArticleSearchDialog } from './ArticleSearchDialog';
|
||||||
export { default as BookDeliveryDialog } from './BookDeliveryDialog';
|
export { default as BookDeliveryDialog } from './BookDeliveryDialog';
|
||||||
|
export { default as OrderShipmentDialog } from './OrderShipmentDialog';
|
||||||
|
export { default as ShipmentDetailDialog } from './ShipmentDetailDialog';
|
||||||
export { default as OrderDocumentsTiles } from './OrderDocumentsTiles';
|
export { default as OrderDocumentsTiles } from './OrderDocumentsTiles';
|
||||||
export { withManifestId } from './withManifestId';
|
export { withManifestId } from './withManifestId';
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -14,11 +14,16 @@ import CodeRoundedIcon from '@mui/icons-material/CodeRounded';
|
|||||||
import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
|
import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
|
||||||
import InputRoundedIcon from '@mui/icons-material/InputRounded';
|
import InputRoundedIcon from '@mui/icons-material/InputRounded';
|
||||||
import OutputRoundedIcon from '@mui/icons-material/OutputRounded';
|
import OutputRoundedIcon from '@mui/icons-material/OutputRounded';
|
||||||
|
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||||
|
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
|
||||||
import VfsFileIcon from './VfsFileIcon';
|
import VfsFileIcon from './VfsFileIcon';
|
||||||
import VfsAppletForm from './VfsAppletForm';
|
import VfsAppletForm from './VfsAppletForm';
|
||||||
import { parseAppletSchemas, buildAppletDefaults, parseCsvRows } from './vfsUtils';
|
import { parseAppletSchemas, buildAppletDefaults, parseCsvRows } from './vfsUtils';
|
||||||
import { vfs as vfsApi } from '../../api';
|
import { vfs as vfsApi } from '../../api';
|
||||||
|
import pinnedAppletAutomat, { normalizeRoute } from '../../automats/pinnedAppletAutomat';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @file VfsAppletRunner.jsx
|
* @file VfsAppletRunner.jsx
|
||||||
@@ -46,9 +51,26 @@ export default class VfsAppletRunner extends PureComponent {
|
|||||||
stdout: '',
|
stdout: '',
|
||||||
durationMs: null,
|
durationMs: null,
|
||||||
truncated: false,
|
truncated: false,
|
||||||
|
isPinned: Boolean(
|
||||||
|
props.fileName &&
|
||||||
|
pinnedAppletAutomat.actions.isPinned(props.fileName, window.location.pathname)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.unsubPinned = pinnedAppletAutomat.subscribe(this, () => ({
|
||||||
|
isPinned: Boolean(
|
||||||
|
this.props.fileName &&
|
||||||
|
pinnedAppletAutomat.actions.isPinned(this.props.fileName, window.location.pathname)
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsubPinned?.();
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.content !== this.props.content || prevProps.fileName !== this.props.fileName) {
|
if (prevProps.content !== this.props.content || prevProps.fileName !== this.props.fileName) {
|
||||||
const { inputSchema, outputColumns, description } = parseAppletSchemas(this.props.content || '');
|
const { inputSchema, outputColumns, description } = parseAppletSchemas(this.props.content || '');
|
||||||
@@ -66,10 +88,27 @@ export default class VfsAppletRunner extends PureComponent {
|
|||||||
stdout: '',
|
stdout: '',
|
||||||
durationMs: null,
|
durationMs: null,
|
||||||
truncated: false,
|
truncated: false,
|
||||||
|
isPinned: Boolean(
|
||||||
|
this.props.fileName &&
|
||||||
|
pinnedAppletAutomat.actions.isPinned(this.props.fileName, window.location.pathname)
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTogglePin = () => {
|
||||||
|
const { fileName } = this.props;
|
||||||
|
if (!fileName) return;
|
||||||
|
const currentRoute = normalizeRoute(window.location.pathname);
|
||||||
|
const nextPinned = pinnedAppletAutomat.actions.togglePin(fileName, currentRoute);
|
||||||
|
this.setState({ isPinned: nextPinned });
|
||||||
|
if (nextPinned) {
|
||||||
|
enqueueSnackbar(`Applet an „${currentRoute}“ angepinnt`, { variant: 'success' });
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar(`Applet von „${currentRoute}“ gelöst`, { variant: 'info' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleInputChange = (nextValues) => {
|
handleInputChange = (nextValues) => {
|
||||||
this.setState({ inputValues: nextValues });
|
this.setState({ inputValues: nextValues });
|
||||||
};
|
};
|
||||||
@@ -248,6 +287,7 @@ export default class VfsAppletRunner extends PureComponent {
|
|||||||
stdout,
|
stdout,
|
||||||
durationMs,
|
durationMs,
|
||||||
truncated,
|
truncated,
|
||||||
|
isPinned,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const description = stateDescription || file?.description;
|
const description = stateDescription || file?.description;
|
||||||
|
|
||||||
@@ -308,6 +348,32 @@ export default class VfsAppletRunner extends PureComponent {
|
|||||||
Code
|
Code
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{fileName && (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
isPinned
|
||||||
|
? `Von dieser Route (${normalizeRoute(window.location.pathname)}) lösen`
|
||||||
|
: `An diese Route (${normalizeRoute(window.location.pathname)}) anpinnen`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant={isPinned ? 'contained' : 'outlined'}
|
||||||
|
color={isPinned ? 'primary' : 'inherit'}
|
||||||
|
startIcon={isPinned ? <PushPinIcon /> : <PushPinOutlinedIcon />}
|
||||||
|
onClick={this.handleTogglePin}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
fontSize: '0.78rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: 'none',
|
||||||
|
...(isPinned ? { boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)' } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isPinned ? 'Angepinnt' : 'Anpinnen'}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{onMentionInChat && (
|
{onMentionInChat && (
|
||||||
<IconButton size="small" onClick={onMentionInChat} aria-label="In Chat erwähnen" sx={{ p: 0.6 }}>
|
<IconButton size="small" onClick={onMentionInChat} aria-label="In Chat erwähnen" sx={{ p: 0.6 }}>
|
||||||
<ChatBubbleOutlineRoundedIcon fontSize="small" color="primary" />
|
<ChatBubbleOutlineRoundedIcon fontSize="small" color="primary" />
|
||||||
|
|||||||
@@ -669,7 +669,7 @@ class ChunkCacheMonitor extends PureComponent {
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||||
<IconButton size="small" onClick={() => this.setState({ selectedChunk: null })}>
|
<IconButton size="small" onClick={() => this.setState({ selectedChunk: null })}>
|
||||||
<ArrowBackIcon fontSize="small" />
|
<ArrowBackIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -832,7 +832,7 @@ class ChunkCacheMonitor extends PureComponent {
|
|||||||
bgcolor: isOldest ? 'rgba(237, 108, 2, 0.04)' : 'background.paper',
|
bgcolor: isOldest ? 'rgba(237, 108, 2, 0.04)' : 'background.paper',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ minWidth: 0, flex: 1 }}>
|
<Stack direction="row" spacing={1.5} sx={{ minWidth: 0, flex: 1, alignItems: 'center' }}>
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={`#${entry.lruRank}`}
|
label={`#${entry.lruRank}`}
|
||||||
@@ -856,7 +856,7 @@ class ChunkCacheMonitor extends PureComponent {
|
|||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums' }}>
|
||||||
{new Date(entry.accessedAt).toLocaleTimeString()}
|
{new Date(entry.accessedAt).toLocaleTimeString()}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ class SseMonitor extends PureComponent {
|
|||||||
key={entry.id}
|
key={entry.id}
|
||||||
sx={{ px: 1.25, py: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
sx={{ px: 1.25, py: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
||||||
>
|
>
|
||||||
<Stack direction="row" spacing={1} alignItems="baseline">
|
<Stack direction="row" spacing={1} sx={{ alignItems: 'baseline' }}>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ flexShrink: 0 }}>
|
<Typography variant="caption" color="text.secondary" sx={{ flexShrink: 0 }}>
|
||||||
{new Date(entry.time).toLocaleTimeString()}
|
{new Date(entry.time).toLocaleTimeString()}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function ActionLogPanel({ logOpen, log, logRef, onToggle, onCopy,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ borderTop: '1px solid', borderColor: 'divider', flexShrink: 0 }}>
|
<Box sx={{ borderTop: '1px solid', borderColor: 'divider', flexShrink: 0 }}>
|
||||||
<Stack direction="row" alignItems="center" spacing={0.5} sx={{ px: 1.5, py: 0.5 }}>
|
<Stack direction="row" spacing={0.5} sx={{ px: 1.5, py: 0.5, alignItems: 'center' }}>
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
component="div"
|
component="div"
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function AutomatEntry({ automat, isExpanded, flash, onToggle, onC
|
|||||||
) : null}
|
) : null}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack direction="row" alignItems="center" spacing={0.5} sx={{ mb: 0.5 }}>
|
<Stack direction="row" spacing={0.5} sx={{ mb: 0.5, alignItems: 'center' }}>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ flexGrow: 1 }}>
|
<Typography variant="caption" color="text.secondary" sx={{ flexGrow: 1 }}>
|
||||||
Aktionen ({actionNames.length})
|
Aktionen ({actionNames.length})
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
Reference in New Issue
Block a user