Compare commits
15 Commits
7fe9f90f2c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90936a449d | ||
|
|
283ce3dc5f | ||
|
|
863422df8e | ||
|
|
9841002398 | ||
|
|
bc11a2c3ab | ||
|
|
ec9f2d9816 | ||
|
|
bc284c6d42 | ||
|
|
0181e08657 | ||
|
|
6071e59d30 | ||
|
|
e7103ac340 | ||
|
|
9c6d5d79e4 | ||
|
|
7c33c097d4 | ||
|
|
76e911ebaf | ||
|
|
d2139fee45 | ||
|
|
8a416ce384 |
@@ -131,7 +131,7 @@ Strip supplierName?: str from T11 (resolve via entityId in cached supplier list)
|
|||||||
|
|
||||||
# Dash API Routes Overview (All)
|
# Dash API Routes Overview (All)
|
||||||
|
|
||||||
Overview of all 305 API endpoints across all modules.
|
Overview of all 314 API endpoints across all modules.
|
||||||
|
|
||||||
### Notation
|
### Notation
|
||||||
|
|
||||||
@@ -140,17 +140,17 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
|
|||||||
|
|
||||||
### Reusable types
|
### Reusable types
|
||||||
|
|
||||||
- `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, shipping?: T23|null, shippings?: T23[], payment?: T32|null, payments?: T32[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T2[], files?: T10[] }`
|
- `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, shipping?: T23|null, shippings?: T23[], payment?: T33|null, payments?: T33[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T2[], files?: T10[] }`
|
||||||
- `T2` = `{ id: int, number: str, type: str, date: str, lines: T20[], shippings: T23[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T10[], state?: str }`
|
- `T2` = `{ id: int, number: str, type: str, date: str, lines: T20[], shippings: T23[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T10[], state?: str }`
|
||||||
- `T3` = `{ id: int, number: str, type: str, date: str, lines: T20[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T10[] }`
|
- `T3` = `{ id: int, number: str, type: str, date: str, lines: T20[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T10[] }`
|
||||||
- `T4` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T24[], files: T10[], externalNumber?: str|null, createdAt?: str, ledger?: T39 }`
|
- `T4` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T24[], files: T10[], externalNumber?: str|null, createdAt?: str, ledger?: T40 }`
|
||||||
- `T5` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T6|null }`
|
- `T5` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T6|null }`
|
||||||
- `T6` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T11, totalSizeBytes: int, durationMs: int }`
|
- `T6` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T11, totalSizeBytes: int, durationMs: int }`
|
||||||
- `T7` = `{ id: int, name: str, pairingpin?: str|null, token?: str|null, pairedAt?: str|null, clientserial?: str|null, rootCategoryId?: int|null, syncRootArticles?: bool, sort?: int, enabled?: bool, clientversion?: str|null, lastseenAt?: str|null, updatedAt?: str|null, categoryIds?: int[] }`
|
- `T7` = `{ id: int, name: str, pairingpin?: str|null, token?: str|null, pairedAt?: str|null, clientserial?: str|null, rootCategoryId?: int|null, syncRootArticles?: bool, sort?: int, enabled?: bool, clientversion?: str|null, lastseenAt?: str|null, updatedAt?: str|null, categoryIds?: int[] }`
|
||||||
- `T8` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T41|null, incoming?: { date: str, amount: num }[] }`
|
- `T8` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T42|null, incoming?: { date: str, amount: num }[] }`
|
||||||
- `T9` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T25[] }, thumb?: str|null }`
|
- `T9` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T25[] }, thumb?: str|null }`
|
||||||
- `T10` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }`
|
- `T10` = `{ id: int, 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 }`
|
||||||
- `T11` = `{ vfs: T40, storage: T40, database: T34 }`
|
- `T11` = `{ vfs: T41, storage: T41, database: T35 }`
|
||||||
- `T12` = `{ id: int, name: str, companyId?: int, token?: str|null, pairingpin?: str|null, pairedAt?: str|null, agentversion?: str|null, lastseenAt?: str|null, updatedAt?: str, sort?: int, enabled?: bool }`
|
- `T12` = `{ id: int, name: str, companyId?: int, token?: str|null, pairingpin?: str|null, pairedAt?: str|null, agentversion?: str|null, lastseenAt?: str|null, updatedAt?: str, sort?: int, enabled?: bool }`
|
||||||
- `T13` = `{ id: int, printeragentId: int, type: str, enabled: bool, sort: int, model?: str|null, serial?: str|null, open?: bool, pause?: bool, error?: bool, online?: bool, labelLength?: int|null }`
|
- `T13` = `{ id: int, printeragentId: int, type: str, enabled: bool, sort: int, model?: str|null, serial?: str|null, open?: bool, pause?: bool, error?: bool, online?: bool, labelLength?: int|null }`
|
||||||
- `T14` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
|
- `T14` = `{ id: int, 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 }`
|
||||||
@@ -158,7 +158,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
|
|||||||
- `T16` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }`
|
- `T16` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }`
|
||||||
- `T17` = `{ id: int, name: str, sort: int, exclusive: bool, values: T30[], articleCount?: int }`
|
- `T17` = `{ id: int, name: str, sort: int, exclusive: bool, values: T30[], articleCount?: int }`
|
||||||
- `T18` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str, started_by?: int|null, first_message?: str|null }`
|
- `T18` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str, started_by?: int|null, first_message?: str|null }`
|
||||||
- `T19` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num }`
|
- `T19` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num, role: str }`
|
||||||
- `T20` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null, shippable: bool }`
|
- `T20` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null, shippable: bool }`
|
||||||
- `T21` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null, shippable?: bool }`
|
- `T21` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null, shippable?: bool }`
|
||||||
- `T22` = `{ id: int, agentprinterId: int, type: str, state: str, data: str, createdAt: str, printeragentId?: int|null, completedAt?: str|null }`
|
- `T22` = `{ id: int, agentprinterId: int, type: str, state: str, data: str, createdAt: str, printeragentId?: int|null, completedAt?: str|null }`
|
||||||
@@ -170,17 +170,18 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
|
|||||||
- `T28` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }`
|
- `T28` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }`
|
||||||
- `T29` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
|
- `T29` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
|
||||||
- `T30` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }`
|
- `T30` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }`
|
||||||
- `T31` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }`
|
- `T31` = `{ id: int, googleAuthId: int, email: str, role: str, picture?: str|null }`
|
||||||
- `T32` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }`
|
- `T32` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }`
|
||||||
- `T33` = `{ id: int, name: str, sort: int, hasPackages: bool, isReverse: bool }`
|
- `T33` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }`
|
||||||
- `T34` = `{ name: str|null, sizeBytes: int, backedUp: bool, reason?: str|null }`
|
- `T34` = `{ id: int, name: str, sort: int, hasPackages: bool, isReverse: bool }`
|
||||||
- `T35` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }`
|
- `T35` = `{ name: str|null, sizeBytes: int, backedUp: bool, reason?: str|null }`
|
||||||
- `T36` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
|
- `T36` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }`
|
||||||
- `T37` = `{ id: int, entityId: int, contactType: str, contact: str }`
|
- `T37` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
|
||||||
- `T38` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
|
- `T38` = `{ id: int, entityId: int, contactType: str, contact: str }`
|
||||||
- `T39` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
|
- `T39` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
|
||||||
- `T40` = `{ name: str, sizeBytes: int, sourceExists: bool }`
|
- `T40` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
|
||||||
- `T41` = `{ net: num, taxId: int, taxRate?: num|null }`
|
- `T41` = `{ name: str, sizeBytes: int, sourceExists: bool }`
|
||||||
|
- `T42` = `{ net: num, taxId: int, taxRate?: num|null }`
|
||||||
|
|
||||||
### Routes
|
### Routes
|
||||||
|
|
||||||
@@ -227,7 +228,7 @@ _none_
|
|||||||
- Params: `query: printeragentId?: int`
|
- Params: `query: printeragentId?: int`
|
||||||
- Returns: `200 { agentprinters: T13[] }`
|
- Returns: `200 { agentprinters: T13[] }`
|
||||||
#### Reorder agent printers — `PUT /agentprinter/order`
|
#### Reorder agent printers — `PUT /agentprinter/order`
|
||||||
- Params: `body: { printeragentId: int, agentprinterIds: int[] }`
|
- Params: `body: { agentprinterIds: int[], printeragentId?: int }`
|
||||||
- Returns: `200 { agentprinterIds: int[] }`
|
- Returns: `200 { agentprinterIds: int[] }`
|
||||||
#### Delete agent printer — `DELETE /agentprinter/{id}`
|
#### Delete agent printer — `DELETE /agentprinter/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
@@ -313,11 +314,11 @@ _none_
|
|||||||
#### 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: `204 void`
|
- Returns: `204 void`
|
||||||
#### Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) — `PUT /article/{sku}/prices`
|
#### Replace POS prices by SKU (net > gross) — `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, taxRate?: num, minmultiplier?: num }[] }`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Print article label — `POST /article/{sku}/print`
|
#### Print article label — `POST /article/{sku}/print`
|
||||||
- Params: `sku: str, body: { templateId: int, printerId: int, quantity: int }`
|
- Params: `sku: str, body: { templateId: int, agentprinterId: int, quantity: int }`
|
||||||
- Returns: `200 { success: bool }`
|
- Returns: `200 { success: bool }`
|
||||||
#### List article properties — `GET /article/{sku}/property`
|
#### List article properties — `GET /article/{sku}/property`
|
||||||
- Params: `sku: str`
|
- Params: `sku: str`
|
||||||
@@ -387,25 +388,37 @@ _none_
|
|||||||
- Returns: `200 { attributevalue: T30 }`
|
- Returns: `200 { attributevalue: T30 }`
|
||||||
#### 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: T36[] } }`
|
- Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T37[] } }`
|
||||||
#### 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: T36[] } }`
|
- Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T37[] } }`
|
||||||
|
|
||||||
### 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: T35 }`
|
- Returns: `200 { message: str, user: T36 }`
|
||||||
#### 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: T35, googleClientId: str, schemaVersion: int, vhost: bool, vhostSettings: str }`
|
- Returns: `200 { user: T36, googleClientId: str, schemaVersion: int, vhost: bool, vhostSettings: 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: T35 }`
|
- Returns: `200 { user: T36 }`
|
||||||
|
|
||||||
|
### Section: /autoinvite
|
||||||
|
|
||||||
|
#### List auto-invitations — `GET /autoinvite`
|
||||||
|
_none_
|
||||||
|
- Returns: `200 { autoinvites: { id: int, email: str, role: str, createdAt: str }[] }`
|
||||||
|
#### Create auto-invitation — `POST /autoinvite`
|
||||||
|
- Params: `body: { email: str, role?: str }`
|
||||||
|
- Returns: `201 { autoinvite: { id: int, email: str, role: str } }`
|
||||||
|
#### Delete auto-invitation — `DELETE /autoinvite/{id}`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `204 void`
|
||||||
|
|
||||||
### Section: /backup
|
### Section: /backup
|
||||||
|
|
||||||
@@ -426,7 +439,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: T36[], gsprpinfo?: str|null } }`
|
- Returns: `201 { brand: { id: int, name: str, pics: T37[], 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`
|
||||||
@@ -435,19 +448,19 @@ _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: T36[] } }`
|
- Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T37[] } }`
|
||||||
#### 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: T36[] } }`
|
- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T37[] } }`
|
||||||
|
|
||||||
### Section: /carrier
|
### Section: /carrier
|
||||||
|
|
||||||
#### List carriers — `GET /carrier`
|
#### List carriers — `GET /carrier`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { carriers: T33[] }`
|
- Returns: `200 { carriers: T34[] }`
|
||||||
#### Create carrier — `POST /carrier`
|
#### Create carrier — `POST /carrier`
|
||||||
- Params: `body: { name: str, sort?: int, hasPackages?: bool, isReverse?: bool }`
|
- Params: `body: { name: str, sort?: int, hasPackages?: bool, isReverse?: bool }`
|
||||||
- Returns: `201 { carrier: T33 }`
|
- Returns: `201 { carrier: T34 }`
|
||||||
#### Reorder carriers — `PUT /carrier/order`
|
#### Reorder carriers — `PUT /carrier/order`
|
||||||
- Params: `body: { carrierIds: int[] }`
|
- Params: `body: { carrierIds: int[] }`
|
||||||
- Returns: `200 { carrierIds: int[] }`
|
- Returns: `200 { carrierIds: int[] }`
|
||||||
@@ -456,10 +469,10 @@ _none_
|
|||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
#### Get carrier by ID — `GET /carrier/{id}`
|
#### Get carrier by ID — `GET /carrier/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 { carrier: T33 }`
|
- Returns: `200 { carrier: T34 }`
|
||||||
#### Update carrier — `PATCH /carrier/{id}`
|
#### Update carrier — `PATCH /carrier/{id}`
|
||||||
- Params: `id: int, body: { name?: str, sort?: int, hasPackages?: bool, isReverse?: bool }`
|
- Params: `id: int, body: { name?: str, sort?: int, hasPackages?: bool, isReverse?: bool }`
|
||||||
- Returns: `200 { carrier: T33 }`
|
- Returns: `200 { carrier: T34 }`
|
||||||
|
|
||||||
### Section: /category
|
### Section: /category
|
||||||
|
|
||||||
@@ -516,7 +529,7 @@ _none_
|
|||||||
- Returns: `200 { companies: T19[] }`
|
- Returns: `200 { companies: T19[] }`
|
||||||
#### 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: T19, user: T35 }`
|
- Returns: `201 { company: T19, user: T36 }`
|
||||||
#### 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`
|
||||||
@@ -548,6 +561,21 @@ _none_
|
|||||||
- 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: T19 }`
|
- Returns: `200 { company: T19 }`
|
||||||
|
|
||||||
|
### Section: /companyuser
|
||||||
|
|
||||||
|
#### List company users — `GET /companyuser`
|
||||||
|
_none_
|
||||||
|
- Returns: `200 { companyusers: T31[] }`
|
||||||
|
#### Add an existing user to the company — `POST /companyuser`
|
||||||
|
- Params: `body: { email: str, role?: str }`
|
||||||
|
- Returns: `201 { companyuser: T31 }`
|
||||||
|
#### Remove a user from the company — `DELETE /companyuser/{id}`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `204 void`
|
||||||
|
#### Change a company user's role — `PATCH /companyuser/{id}`
|
||||||
|
- Params: `id: int, body: { role: str }`
|
||||||
|
- Returns: `200 { companyuser: T31 }`
|
||||||
|
|
||||||
### Section: /customergroup
|
### Section: /customergroup
|
||||||
|
|
||||||
#### List customer groups — `GET /customergroup`
|
#### List customer groups — `GET /customergroup`
|
||||||
@@ -570,7 +598,7 @@ _none_
|
|||||||
|
|
||||||
#### List deliveries — `GET /delivery`
|
#### List deliveries — `GET /delivery`
|
||||||
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int`
|
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int`
|
||||||
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T39 }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T40 }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
||||||
#### Create delivery — `POST /delivery`
|
#### Create delivery — `POST /delivery`
|
||||||
- Params: `body: { externalNumber: str, date: str, supplierId: int, lines: { amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|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, supplierId: int, lines: { amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
|
||||||
- Returns: `201 { delivery: T4 }`
|
- Returns: `201 { delivery: T4 }`
|
||||||
@@ -618,16 +646,16 @@ _none_
|
|||||||
- Returns: `200 { address: T14 }`
|
- Returns: `200 { address: T14 }`
|
||||||
#### List entity contacts — `GET /entity/{entityId}/contact`
|
#### List entity contacts — `GET /entity/{entityId}/contact`
|
||||||
- Params: `entityId: int`
|
- Params: `entityId: int`
|
||||||
- Returns: `200 { contacts: T37[] }`
|
- Returns: `200 { contacts: T38[] }`
|
||||||
#### 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: T37 }`
|
- Returns: `201 { contact: T38 }`
|
||||||
#### 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: T37 }`
|
- Returns: `200 { contact: T38 }`
|
||||||
#### Delete entity — `DELETE /entity/{id}`
|
#### Delete entity — `DELETE /entity/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
@@ -654,6 +682,8 @@ _none_
|
|||||||
- `ledger`: `{}`
|
- `ledger`: `{}`
|
||||||
- `attributename`: `{}`
|
- `attributename`: `{}`
|
||||||
- `propertyname`: `{}`
|
- `propertyname`: `{}`
|
||||||
|
- `companyuser`: `{}`
|
||||||
|
- `autoinvite`: `{}`
|
||||||
- `labeltemplate`: `{}`
|
- `labeltemplate`: `{}`
|
||||||
- `printer`: `{}`
|
- `printer`: `{}`
|
||||||
- `printeragent`: `{}`
|
- `printeragent`: `{}`
|
||||||
@@ -689,6 +719,9 @@ _none_
|
|||||||
#### Create label template — `POST /labeltemplate`
|
#### Create label template — `POST /labeltemplate`
|
||||||
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T25[] } }`
|
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T25[] } }`
|
||||||
- Returns: `201 { labeltemplate: T9 }`
|
- Returns: `201 { labeltemplate: T9 }`
|
||||||
|
#### Test print label template — `POST /labeltemplate/testprint`
|
||||||
|
- Params: `body: { agentprinterId: int, templateId?: int, objects?: any[], width?: int, height?: int, dpi?: int, sku?: str, quantity?: int }`
|
||||||
|
- Returns: `200 { success: bool }`
|
||||||
#### Delete label template — `DELETE /labeltemplate/{id}`
|
#### Delete label template — `DELETE /labeltemplate/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
@@ -814,10 +847,10 @@ _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: T32[] }`
|
- Returns: `200 { payments: T33[] }`
|
||||||
#### 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: T32 }`
|
- Returns: `201 { payment: T33 }`
|
||||||
#### 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 { attachment: T10, fileUrl: str }`
|
- Returns: `201 { attachment: T10, fileUrl: str }`
|
||||||
@@ -926,6 +959,9 @@ _none_
|
|||||||
#### Create print job — `POST /printerjob`
|
#### Create print job — `POST /printerjob`
|
||||||
- Params: `body: { agentprinterId: int, data: str, type?: str }`
|
- Params: `body: { agentprinterId: int, data: str, type?: str }`
|
||||||
- Returns: `201 { printerjob: T22 }`
|
- Returns: `201 { printerjob: T22 }`
|
||||||
|
#### Cancel all pending and processing printer jobs — `POST /printerjob/cancel-all`
|
||||||
|
- Params: `body?: { agentprinterId?: int }`
|
||||||
|
- Returns: `200 { success: bool, cancelledCount: int, jobs: T22[] }`
|
||||||
#### Cancel and delete printer job — `DELETE /printerjob/{id}`
|
#### Cancel and delete printer job — `DELETE /printerjob/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 { success: bool, deletedJob: T22 }`
|
- Returns: `200 { success: bool, deletedJob: T22 }`
|
||||||
@@ -940,10 +976,10 @@ _none_
|
|||||||
|
|
||||||
#### List property names — `GET /propertyname`
|
#### List property names — `GET /propertyname`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { propertynames: T31[] }`
|
- Returns: `200 { propertynames: T32[] }`
|
||||||
#### 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: T31 }`
|
- Returns: `201 { propertyname: T32 }`
|
||||||
#### 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 }`
|
||||||
@@ -952,7 +988,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: T31 }`
|
- Returns: `200 { propertyname: T32 }`
|
||||||
|
|
||||||
### Section: /public
|
### Section: /public
|
||||||
|
|
||||||
@@ -1147,22 +1183,22 @@ _none_
|
|||||||
|
|
||||||
#### List suppliers — `GET /supplier`
|
#### List suppliers — `GET /supplier`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { suppliers: T38[] }`
|
- Returns: `200 { suppliers: T39[] }`
|
||||||
#### Create supplier — `POST /supplier`
|
#### Create supplier — `POST /supplier`
|
||||||
- Params: `body: { name: str }`
|
- Params: `body: { name: str }`
|
||||||
- Returns: `201 { supplier: T38 }`
|
- Returns: `201 { supplier: T39 }`
|
||||||
#### 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: T38 }`
|
- Returns: `200 { supplier: T39 }`
|
||||||
#### List supplier articles by SKUs — `GET /supplier/{id}/articles`
|
#### List supplier articles by SKUs — `GET /supplier/{id}/articles`
|
||||||
- Params: `id: int, query: skus?: str`
|
- Params: `id: int, query: skus?: str`
|
||||||
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T41|null, incoming?: { date: str, amount: num }[] }[] }`
|
- 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?: T42|null, incoming?: { date: str, amount: num }[] }[] }`
|
||||||
#### 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?: T41|null, incoming?: { date: str, amount: num }[] }[] }`
|
- 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?: T42|null, incoming?: { date: str, amount: num }[] }[] }`
|
||||||
|
|
||||||
### Section: /thumb
|
### Section: /thumb
|
||||||
|
|
||||||
|
|||||||
1081
package-lock.json
generated
@@ -41,7 +41,7 @@
|
|||||||
"pdfconvert-wasm": "^0.3.0",
|
"pdfconvert-wasm": "^0.3.0",
|
||||||
"react": "^19.3.1",
|
"react": "^19.3.1",
|
||||||
"react-automat": "file:../automat",
|
"react-automat": "file:../automat",
|
||||||
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor",
|
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor#v20.0.0",
|
||||||
"react-dom": "^19.3.1",
|
"react-dom": "^19.3.1",
|
||||||
"react-router-dom": "^7.18.4",
|
"react-router-dom": "^7.18.4",
|
||||||
"react-window": "^2.3.4",
|
"react-window": "^2.3.4",
|
||||||
@@ -62,6 +62,7 @@
|
|||||||
},
|
},
|
||||||
"allowScripts": {
|
"allowScripts": {
|
||||||
"canvas@3.2.3": true,
|
"canvas@3.2.3": true,
|
||||||
|
"react-canvas-label-editor@20.0.0": true,
|
||||||
"github:sebseb7/react-canvas-label-editor#7f0e31b66ff22da011e872b201017398b14e9dd9": true
|
"github:sebseb7/react-canvas-label-editor#7f0e31b66ff22da011e872b201017398b14e9dd9": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/api/autoinvite.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { http } from './http.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pending invites for emails that are not registered yet (admin only).
|
||||||
|
*/
|
||||||
|
export const autoinvite = {
|
||||||
|
// #region GET /api/autoinvite — list pending invites
|
||||||
|
list() {
|
||||||
|
return http('/api/autoinvite');
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region POST /api/autoinvite — create an invite
|
||||||
|
create(body) {
|
||||||
|
return http('/api/autoinvite', { method: 'POST', body });
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region DELETE /api/autoinvite/:id — delete an invite
|
||||||
|
delete(id) {
|
||||||
|
return http(`/api/autoinvite/${id}`, { method: 'DELETE' });
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
};
|
||||||
30
src/api/companyuser.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { http } from './http.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Company members (admin only).
|
||||||
|
*/
|
||||||
|
export const companyuser = {
|
||||||
|
// #region GET /api/companyuser — list members
|
||||||
|
list() {
|
||||||
|
return http('/api/companyuser');
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region POST /api/companyuser — add an existing user by email
|
||||||
|
create(body) {
|
||||||
|
return http('/api/companyuser', { method: 'POST', body });
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region PATCH /api/companyuser/:id — change role
|
||||||
|
setRole(id, role) {
|
||||||
|
return http(`/api/companyuser/${id}`, { method: 'PATCH', body: { role } });
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region DELETE /api/companyuser/:id — remove a member
|
||||||
|
delete(id) {
|
||||||
|
return http(`/api/companyuser/${id}`, { method: 'DELETE' });
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
};
|
||||||
@@ -34,6 +34,8 @@ export { shipment } from './shipment.js';
|
|||||||
export { shipping } from './shipping.js';
|
export { shipping } from './shipping.js';
|
||||||
export { payment } from './payment.js';
|
export { payment } from './payment.js';
|
||||||
export { account } from './account.js';
|
export { account } from './account.js';
|
||||||
|
export { companyuser } from './companyuser.js';
|
||||||
|
export { autoinvite } from './autoinvite.js';
|
||||||
export { mcp } from './mcp.js';
|
export { mcp } from './mcp.js';
|
||||||
export { backup } from './backup.js';
|
export { backup } from './backup.js';
|
||||||
export { carrier } from './carrier.js';
|
export { carrier } from './carrier.js';
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import { http } from './http.js';
|
|||||||
* Shape: LabelTemplate = { id, company_id, name, width, height, dpi, type, content, thumb }
|
* Shape: LabelTemplate = { id, company_id, name, width, height, dpi, type, content, thumb }
|
||||||
*/
|
*/
|
||||||
export const labeltemplate = {
|
export const labeltemplate = {
|
||||||
/** GET /api/labeltemplate → { labeltemplates: LabelTemplate[] } */
|
/** GET /api/labeltemplate(?sku=...) → { labeltemplates: LabelTemplate[] } */
|
||||||
// #region GET /api/labeltemplate — list
|
// #region GET /api/labeltemplate — list
|
||||||
list() {
|
list(params) {
|
||||||
return http('/api/labeltemplate');
|
const search = params?.sku ? `?sku=${encodeURIComponent(params.sku)}` : '';
|
||||||
|
return http(`/api/labeltemplate${search}`);
|
||||||
},
|
},
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
@@ -48,5 +49,23 @@ export const labeltemplate = {
|
|||||||
},
|
},
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/labeltemplate/testprint (or /api/labeltemplate/:id/testprint)
|
||||||
|
* @param {number|object} idOrBody
|
||||||
|
* @param {object} [maybeBody]
|
||||||
|
* → 200 { success: true, printerjob?: PrinterJob }
|
||||||
|
*/
|
||||||
|
// #region POST /api/labeltemplate/testprint — testprint
|
||||||
|
testprint(idOrBody, maybeBody) {
|
||||||
|
if (typeof idOrBody === 'object' && idOrBody !== null) {
|
||||||
|
return http('/api/labeltemplate/testprint', { method: 'POST', body: idOrBody });
|
||||||
|
}
|
||||||
|
return http(`/api/labeltemplate/${encodeURIComponent(idOrBody)}/testprint`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: maybeBody || {},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
// thumb is returned inline as a base64 string in list/get responses
|
// thumb is returned inline as a base64 string in list/get responses
|
||||||
};
|
};
|
||||||
|
|||||||
15
src/assets/printers/index.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import zd41x from './zd41x.png';
|
||||||
|
|
||||||
|
// Library of printer images. Entries are matched in order against the
|
||||||
|
// printer model (as reported by the agent), case-insensitively.
|
||||||
|
const PRINTER_IMAGES = [
|
||||||
|
{ pattern: /^ZD41/i, image: zd41x }, // ZD410, ZD411, ZD410-200dpi, ...
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getPrinterImage(model) {
|
||||||
|
if (!model) return null;
|
||||||
|
const entry = PRINTER_IMAGES.find(({ pattern }) => pattern.test(String(model).trim()));
|
||||||
|
return entry?.image ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PRINTER_IMAGES;
|
||||||
BIN
src/assets/printers/zd41x.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
@@ -84,7 +84,7 @@ export function setAuthEventDispatcher(dispatch) {
|
|||||||
emit = dispatch;
|
emit = dispatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePicture(picture) {
|
export function normalizePicture(picture) {
|
||||||
if (!picture || typeof picture !== 'string') return null;
|
if (!picture || typeof picture !== 'string') return null;
|
||||||
if (picture.startsWith('http://') || picture.startsWith('https://') || picture.startsWith('data:')) {
|
if (picture.startsWith('http://') || picture.startsWith('https://') || picture.startsWith('data:')) {
|
||||||
return picture;
|
return picture;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
* - Brands (`brands`, `filteredBrands`, `brandFilter`)
|
* - Brands (`brands`, `filteredBrands`, `brandFilter`)
|
||||||
* - Product attributes & values (`attributes`)
|
* - Product attributes & values (`attributes`)
|
||||||
* - Payment accounts (`accounts`)
|
* - Payment accounts (`accounts`)
|
||||||
|
* - Members & pending invites (`members`, `invites`; admin only, loaded on demand)
|
||||||
*
|
*
|
||||||
* Lives with the 1st-level navigation shell (`AppShell`) and triggers when
|
* Lives with the 1st-level navigation shell (`AppShell`) and triggers when
|
||||||
* `authAutomat.state.hasCompany` is true (`hasCompany` requires `hasAuth`).
|
* `authAutomat.state.hasCompany` is true (`hasCompany` requires `hasAuth`).
|
||||||
@@ -39,6 +40,8 @@ import {
|
|||||||
attributename as attributenameApi,
|
attributename as attributenameApi,
|
||||||
attributevalue as attributevalueApi,
|
attributevalue as attributevalueApi,
|
||||||
account as accountApi,
|
account as accountApi,
|
||||||
|
companyuser as companyuserApi,
|
||||||
|
autoinvite as autoinviteApi,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import { requireArray, requireDefined, requireObject } from '../utils/apiShape';
|
import { requireArray, requireDefined, requireObject } from '../utils/apiShape';
|
||||||
import { normalizeCompany } from '../utils/companyForm';
|
import { normalizeCompany } from '../utils/companyForm';
|
||||||
@@ -330,6 +333,14 @@ const initialState = {
|
|||||||
accounts: [],
|
accounts: [],
|
||||||
accountsStatus: 'idle',
|
accountsStatus: 'idle',
|
||||||
accountsError: null,
|
accountsError: null,
|
||||||
|
|
||||||
|
// 14. Members & invites (admin only; never part of loadAll)
|
||||||
|
members: [],
|
||||||
|
membersStatus: 'idle',
|
||||||
|
membersError: null,
|
||||||
|
invites: [],
|
||||||
|
invitesStatus: 'idle',
|
||||||
|
invitesError: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
let companyAutomat;
|
let companyAutomat;
|
||||||
@@ -350,6 +361,8 @@ let inflightCarriers = null;
|
|||||||
let inflightBrands = null;
|
let inflightBrands = null;
|
||||||
let inflightAttributes = null;
|
let inflightAttributes = null;
|
||||||
let inflightAccounts = null;
|
let inflightAccounts = null;
|
||||||
|
let inflightMembers = null;
|
||||||
|
let inflightInvites = null;
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region internal state updaters & fetchers
|
// #region internal state updaters & fetchers
|
||||||
@@ -565,6 +578,8 @@ const actions = {
|
|||||||
inflightBrands = null;
|
inflightBrands = null;
|
||||||
inflightAttributes = null;
|
inflightAttributes = null;
|
||||||
inflightAccounts = null;
|
inflightAccounts = null;
|
||||||
|
inflightMembers = null;
|
||||||
|
inflightInvites = null;
|
||||||
companyAutomat.setState({
|
companyAutomat.setState({
|
||||||
...initialState,
|
...initialState,
|
||||||
viewerPosition: companyAutomat.state.viewerPosition,
|
viewerPosition: companyAutomat.state.viewerPosition,
|
||||||
@@ -1709,6 +1724,122 @@ const actions = {
|
|||||||
return accounts;
|
return accounts;
|
||||||
},
|
},
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
// #region 14. Members (companyuser) & invites (autoinvite) — admin only
|
||||||
|
// MARK: calls companyuser.* / autoinvite.* from ../api/index.js
|
||||||
|
loadMembers() {
|
||||||
|
if (inflightMembers) return inflightMembers;
|
||||||
|
inflightMembers = (async () => {
|
||||||
|
companyAutomat.setState({ membersStatus: 'loading', membersError: null });
|
||||||
|
try {
|
||||||
|
const res = await companyuserApi.list();
|
||||||
|
const members = requireArray(res?.companyusers, 'companyusers');
|
||||||
|
companyAutomat.setState({ members, membersStatus: 'ready', membersError: null });
|
||||||
|
return members;
|
||||||
|
} catch (error) {
|
||||||
|
companyAutomat.setState({ membersStatus: 'failed', membersError: error });
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
inflightMembers = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return inflightMembers;
|
||||||
|
},
|
||||||
|
|
||||||
|
async reloadMembers() {
|
||||||
|
try {
|
||||||
|
const res = await companyuserApi.list();
|
||||||
|
const members = requireArray(res?.companyusers, 'companyusers');
|
||||||
|
companyAutomat.setState({ members, membersStatus: 'ready', membersError: null });
|
||||||
|
return members;
|
||||||
|
} catch (error) {
|
||||||
|
companyAutomat.setState({ membersError: error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
ensureMembersLoaded() {
|
||||||
|
if (companyAutomat.state.membersStatus === 'ready') {
|
||||||
|
return Promise.resolve(companyAutomat.state.members);
|
||||||
|
}
|
||||||
|
return actions.loadMembers();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Adds an already registered user by email. Rejects with 404/409 from the backend. */
|
||||||
|
async addMember(email, role = 'user') {
|
||||||
|
const res = await companyuserApi.create({ email, role });
|
||||||
|
const member = requireObject(res?.companyuser, 'companyuser');
|
||||||
|
companyAutomat.setState({ members: [...companyAutomat.state.members, member] });
|
||||||
|
return member;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Rejects with 409 when demoting the last admin. */
|
||||||
|
async setMemberRole(id, role) {
|
||||||
|
const res = await companyuserApi.setRole(id, role);
|
||||||
|
const member = requireObject(res?.companyuser, 'companyuser');
|
||||||
|
companyAutomat.setState({
|
||||||
|
members: companyAutomat.state.members.map((m) => (m.id === member.id ? member : m)),
|
||||||
|
});
|
||||||
|
return member;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Rejects with 409 when removing the last admin. */
|
||||||
|
async removeMember(id) {
|
||||||
|
await companyuserApi.delete(id);
|
||||||
|
companyAutomat.setState({ members: companyAutomat.state.members.filter((m) => m.id !== id) });
|
||||||
|
},
|
||||||
|
|
||||||
|
loadInvites() {
|
||||||
|
if (inflightInvites) return inflightInvites;
|
||||||
|
inflightInvites = (async () => {
|
||||||
|
companyAutomat.setState({ invitesStatus: 'loading', invitesError: null });
|
||||||
|
try {
|
||||||
|
const res = await autoinviteApi.list();
|
||||||
|
const invites = requireArray(res?.autoinvites, 'autoinvites');
|
||||||
|
companyAutomat.setState({ invites, invitesStatus: 'ready', invitesError: null });
|
||||||
|
return invites;
|
||||||
|
} catch (error) {
|
||||||
|
companyAutomat.setState({ invitesStatus: 'failed', invitesError: error });
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
inflightInvites = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return inflightInvites;
|
||||||
|
},
|
||||||
|
|
||||||
|
async reloadInvites() {
|
||||||
|
try {
|
||||||
|
const res = await autoinviteApi.list();
|
||||||
|
const invites = requireArray(res?.autoinvites, 'autoinvites');
|
||||||
|
companyAutomat.setState({ invites, invitesStatus: 'ready', invitesError: null });
|
||||||
|
return invites;
|
||||||
|
} catch (error) {
|
||||||
|
companyAutomat.setState({ invitesError: error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
ensureInvitesLoaded() {
|
||||||
|
if (companyAutomat.state.invitesStatus === 'ready') {
|
||||||
|
return Promise.resolve(companyAutomat.state.invites);
|
||||||
|
}
|
||||||
|
return actions.loadInvites();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Rejects with 409 when the email is already registered (use addMember then). */
|
||||||
|
async createInvite(email, role = 'user') {
|
||||||
|
const res = await autoinviteApi.create({ email, role });
|
||||||
|
const invite = requireObject(res?.autoinvite, 'autoinvite');
|
||||||
|
companyAutomat.setState({ invites: [...companyAutomat.state.invites, invite] });
|
||||||
|
return invite;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteInvite(id) {
|
||||||
|
await autoinviteApi.delete(id);
|
||||||
|
companyAutomat.setState({ invites: companyAutomat.state.invites.filter((i) => i.id !== id) });
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1807,6 +1938,15 @@ sseAutomat.actions.on('attributename', () => {
|
|||||||
sseAutomat.actions.on('account', () => {
|
sseAutomat.actions.on('account', () => {
|
||||||
void companyAutomat.actions.reloadAccounts().catch(() => {});
|
void companyAutomat.actions.reloadAccounts().catch(() => {});
|
||||||
});
|
});
|
||||||
|
// Admin-only lists: only refresh once an admin view has loaded them.
|
||||||
|
sseAutomat.actions.on('companyuser', () => {
|
||||||
|
if (companyAutomat.state.membersStatus !== 'ready') return;
|
||||||
|
void companyAutomat.actions.reloadMembers().catch(() => {});
|
||||||
|
});
|
||||||
|
sseAutomat.actions.on('autoinvite', () => {
|
||||||
|
if (companyAutomat.state.invitesStatus !== 'ready') return;
|
||||||
|
void companyAutomat.actions.reloadInvites().catch(() => {});
|
||||||
|
});
|
||||||
sseAutomat.actions.on('company', () => {
|
sseAutomat.actions.on('company', () => {
|
||||||
void companyAutomat.actions.reload().catch(() => {});
|
void companyAutomat.actions.reload().catch(() => {});
|
||||||
});
|
});
|
||||||
@@ -1885,6 +2025,16 @@ export const selectAttributeStatus = () => companyAutomat.state.attributesStatus
|
|||||||
// Accounts selectors
|
// Accounts selectors
|
||||||
export const selectAccounts = () => companyAutomat.state.accounts;
|
export const selectAccounts = () => companyAutomat.state.accounts;
|
||||||
export const selectAccountsStatus = () => companyAutomat.state.accountsStatus;
|
export const selectAccountsStatus = () => companyAutomat.state.accountsStatus;
|
||||||
|
|
||||||
|
// Own role in the selected company ('admin' | 'user' | undefined)
|
||||||
|
export const selectCompanyRole = () => companyAutomat.state.current?.role;
|
||||||
|
export const selectIsCompanyAdmin = () => companyAutomat.state.current?.role === 'admin';
|
||||||
|
|
||||||
|
// Members & invites selectors (admin only)
|
||||||
|
export const selectMembers = () => companyAutomat.state.members;
|
||||||
|
export const selectMembersStatus = () => companyAutomat.state.membersStatus;
|
||||||
|
export const selectInvites = () => companyAutomat.state.invites;
|
||||||
|
export const selectInvitesStatus = () => companyAutomat.state.invitesStatus;
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
export default companyAutomat;
|
export default companyAutomat;
|
||||||
|
|||||||
@@ -178,6 +178,12 @@ export {
|
|||||||
selectAttributeStatus,
|
selectAttributeStatus,
|
||||||
selectAccounts,
|
selectAccounts,
|
||||||
selectAccountsStatus,
|
selectAccountsStatus,
|
||||||
|
selectCompanyRole,
|
||||||
|
selectIsCompanyAdmin,
|
||||||
|
selectMembers,
|
||||||
|
selectMembersStatus,
|
||||||
|
selectInvites,
|
||||||
|
selectInvitesStatus,
|
||||||
} from './companyAutomat';
|
} from './companyAutomat';
|
||||||
|
|
||||||
export { default as categoryAutomat } from './categoryAutomat';
|
export { default as categoryAutomat } from './categoryAutomat';
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import SettingsIcon from '@mui/icons-material/Settings';
|
|||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import companyAutomat from '../automats/companyAutomat';
|
import companyAutomat from '../automats/companyAutomat';
|
||||||
import { notifyError } from '../utils/notify';
|
import { notifyError } from '../utils/notify';
|
||||||
|
import { getPrinterImage } from '../assets/printers';
|
||||||
|
|
||||||
export default class OnlinePrinterIcons extends PureComponent {
|
export default class OnlinePrinterIcons extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -247,6 +248,14 @@ export default class OnlinePrinterIcons extends PureComponent {
|
|||||||
>
|
>
|
||||||
{activePrinter && (
|
{activePrinter && (
|
||||||
<Box sx={{ px: 2, py: 1, minWidth: 210 }}>
|
<Box sx={{ px: 2, py: 1, minWidth: 210 }}>
|
||||||
|
{getPrinterImage(activePrinter.model) ? (
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={getPrinterImage(activePrinter.model)}
|
||||||
|
alt={activePrinter.model}
|
||||||
|
sx={{ display: 'block', mx: 'auto', mb: 1, maxWidth: 80, height: 'auto' }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||||
<Typography variant="subtitle2" fontWeight={600} noWrap>
|
<Typography variant="subtitle2" fontWeight={600} noWrap>
|
||||||
{activePrinter.model || 'Drucker'}
|
{activePrinter.model || 'Drucker'}
|
||||||
@@ -283,8 +292,7 @@ export default class OnlinePrinterIcons extends PureComponent {
|
|||||||
<PrintOutlinedIcon fontSize="small" color="primary" />
|
<PrintOutlinedIcon fontSize="small" color="primary" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Testdruck senden"
|
primary="Testdruck"
|
||||||
secondary="Minimales ZPL-Etikett"
|
|
||||||
/>
|
/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
@@ -293,8 +301,7 @@ export default class OnlinePrinterIcons extends PureComponent {
|
|||||||
<CancelScheduleSendOutlinedIcon fontSize="small" color="error" />
|
<CancelScheduleSendOutlinedIcon fontSize="small" color="error" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Alle Aufträge abbrechen"
|
primary="Aufträge abbrechen"
|
||||||
secondary="Warteschlange leeren"
|
|
||||||
/>
|
/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
@@ -302,7 +309,7 @@ export default class OnlinePrinterIcons extends PureComponent {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<SettingsIcon fontSize="small" />
|
<SettingsIcon fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Drucker-Einstellungen" />
|
<ListItemText primary="Einstellungen" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -12,20 +12,15 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|||||||
import CancelScheduleSendOutlinedIcon from '@mui/icons-material/CancelScheduleSendOutlined';
|
import CancelScheduleSendOutlinedIcon from '@mui/icons-material/CancelScheduleSendOutlined';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
|
||||||
import { labeltemplate, printer, article } from '../../../api';
|
import { labeltemplate, article } from '../../../api';
|
||||||
import { companyAutomat } from '../../../automats';
|
import { companyAutomat } from '../../../automats';
|
||||||
import { notifyError } from '../../../utils/notify';
|
import { notifyError } from '../../../utils/notify';
|
||||||
import BackendError from '../../BackendError';
|
import BackendError from '../../BackendError';
|
||||||
|
|
||||||
function filterZplPrinters(agentprinters, fallbackPrinters = []) {
|
function filterZplPrinters(agentprinters) {
|
||||||
const filteredAgent = (Array.isArray(agentprinters) ? agentprinters : []).filter(
|
return (Array.isArray(agentprinters) ? agentprinters : []).filter(
|
||||||
(p) => p.enabled !== false && (!p.type || p.type.toLowerCase() === 'zpl'),
|
(p) => p.enabled !== false && (!p.type || p.type.toLowerCase() === 'zpl'),
|
||||||
);
|
);
|
||||||
if (filteredAgent.length > 0) return filteredAgent;
|
|
||||||
|
|
||||||
return (Array.isArray(fallbackPrinters) ? fallbackPrinters : []).filter(
|
|
||||||
(p) => !p.type || p.type.toLowerCase() === 'zpl',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPrinterLabel(p) {
|
function getPrinterLabel(p) {
|
||||||
@@ -52,6 +47,20 @@ function getPrinterLabel(p) {
|
|||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPrinterLabelLength(printer, livePrinters = []) {
|
||||||
|
if (!printer) return null;
|
||||||
|
const live = (livePrinters || []).find(
|
||||||
|
(lp) =>
|
||||||
|
(lp.agentprinterId && lp.agentprinterId === printer.id) ||
|
||||||
|
(lp.id && lp.id === printer.id) ||
|
||||||
|
(printer.serial && lp.serial && String(printer.serial).toLowerCase() === String(lp.serial).toLowerCase()),
|
||||||
|
);
|
||||||
|
const raw = printer.labelLength ?? live?.labelLength;
|
||||||
|
if (raw == null) return null;
|
||||||
|
const num = Number(raw);
|
||||||
|
return Number.isFinite(num) && num > 0 ? num : null;
|
||||||
|
}
|
||||||
|
|
||||||
export default class ArticleLabelTab extends PureComponent {
|
export default class ArticleLabelTab extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -59,20 +68,23 @@ export default class ArticleLabelTab extends PureComponent {
|
|||||||
this.state = {
|
this.state = {
|
||||||
templates: null,
|
templates: null,
|
||||||
printers: initialPrinters,
|
printers: initialPrinters,
|
||||||
|
livePrinters: Array.isArray(companyAutomat.state.livePrinters)
|
||||||
|
? companyAutomat.state.livePrinters
|
||||||
|
: [],
|
||||||
selectedTemplateId: '',
|
selectedTemplateId: '',
|
||||||
selectedPrinterId: initialPrinters.length > 0 ? initialPrinters[0].id : '',
|
selectedPrinterId: initialPrinters.length > 0 ? initialPrinters[0].id : '',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
this.legacyPrinters = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unsub = companyAutomat.subscribe(this, (state) => {
|
this.unsub = companyAutomat.subscribe(this, (state) => {
|
||||||
const zplPrinters = filterZplPrinters(state.agentprinters, this.legacyPrinters);
|
const zplPrinters = filterZplPrinters(state.agentprinters);
|
||||||
this.setState((prev) => ({
|
this.setState((prev) => ({
|
||||||
printers: zplPrinters,
|
printers: zplPrinters,
|
||||||
|
livePrinters: Array.isArray(state.livePrinters) ? state.livePrinters : [],
|
||||||
selectedPrinterId:
|
selectedPrinterId:
|
||||||
prev.selectedPrinterId && zplPrinters.some((p) => p.id === prev.selectedPrinterId)
|
prev.selectedPrinterId && zplPrinters.some((p) => p.id === prev.selectedPrinterId)
|
||||||
? prev.selectedPrinterId
|
? prev.selectedPrinterId
|
||||||
@@ -82,34 +94,35 @@ export default class ArticleLabelTab extends PureComponent {
|
|||||||
this.loadData();
|
this.loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (
|
||||||
|
prevProps.article?.sku !== this.props.article?.sku ||
|
||||||
|
prevProps.article?.name !== this.props.article?.name ||
|
||||||
|
prevProps.article?.prices !== this.props.article?.prices ||
|
||||||
|
prevProps.article?.eans !== this.props.article?.eans
|
||||||
|
) {
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unsub?.();
|
this.unsub?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadData() {
|
async loadData() {
|
||||||
this.setState({ loading: true, error: null });
|
this.setState({ loading: true, error: null });
|
||||||
|
const sku = this.props.article?.sku;
|
||||||
try {
|
try {
|
||||||
const [resTemplates] = await Promise.all([
|
const resTemplates = await labeltemplate.list(sku ? { sku } : undefined);
|
||||||
labeltemplate.list(),
|
|
||||||
companyAutomat.actions.loadAgentPrinters().catch(() => companyAutomat.state.agentprinters),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const templates = resTemplates.labeltemplates || [];
|
const templates = resTemplates.labeltemplates || [];
|
||||||
let zplPrinters = filterZplPrinters(companyAutomat.state.agentprinters);
|
const zplPrinters = filterZplPrinters(companyAutomat.state.agentprinters);
|
||||||
|
|
||||||
if (zplPrinters.length === 0) {
|
|
||||||
try {
|
|
||||||
const resPrinters = await printer.list();
|
|
||||||
this.legacyPrinters = (resPrinters.printers || []).filter((p) => p.type === 'zpl');
|
|
||||||
zplPrinters = filterZplPrinters(companyAutomat.state.agentprinters, this.legacyPrinters);
|
|
||||||
} catch {
|
|
||||||
// ignore legacy fallback error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState((prev) => ({
|
this.setState((prev) => ({
|
||||||
templates,
|
templates,
|
||||||
printers: zplPrinters,
|
printers: zplPrinters,
|
||||||
|
livePrinters: Array.isArray(companyAutomat.state.livePrinters)
|
||||||
|
? companyAutomat.state.livePrinters
|
||||||
|
: prev.livePrinters,
|
||||||
selectedTemplateId:
|
selectedTemplateId:
|
||||||
templates.length > 0
|
templates.length > 0
|
||||||
? prev.selectedTemplateId && templates.some((t) => t.id === prev.selectedTemplateId)
|
? prev.selectedTemplateId && templates.some((t) => t.id === prev.selectedTemplateId)
|
||||||
@@ -139,7 +152,7 @@ export default class ArticleLabelTab extends PureComponent {
|
|||||||
try {
|
try {
|
||||||
await article.printLabel(articleData.sku, {
|
await article.printLabel(articleData.sku, {
|
||||||
templateId: selectedTemplateId,
|
templateId: selectedTemplateId,
|
||||||
printerId: selectedPrinterId,
|
agentprinterId: selectedPrinterId,
|
||||||
quantity,
|
quantity,
|
||||||
});
|
});
|
||||||
enqueueSnackbar('Druckauftrag erfolgreich gesendet', { variant: 'success' });
|
enqueueSnackbar('Druckauftrag erfolgreich gesendet', { variant: 'success' });
|
||||||
@@ -165,6 +178,7 @@ export default class ArticleLabelTab extends PureComponent {
|
|||||||
const {
|
const {
|
||||||
templates,
|
templates,
|
||||||
printers,
|
printers,
|
||||||
|
livePrinters,
|
||||||
selectedTemplateId,
|
selectedTemplateId,
|
||||||
selectedPrinterId,
|
selectedPrinterId,
|
||||||
quantity,
|
quantity,
|
||||||
@@ -172,6 +186,9 @@ export default class ArticleLabelTab extends PureComponent {
|
|||||||
error,
|
error,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const currentPrinter = (printers || []).find((p) => p.id === selectedPrinterId);
|
||||||
|
const printerLabelLength = getPrinterLabelLength(currentPrinter, livePrinters);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 4, display: 'flex', justifyContent: 'center' }}>
|
<Box sx={{ p: 4, display: 'flex', justifyContent: 'center' }}>
|
||||||
@@ -214,6 +231,16 @@ export default class ArticleLabelTab extends PureComponent {
|
|||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{templates.map((t) => {
|
{templates.map((t) => {
|
||||||
const isSelected = t.id === selectedTemplateId;
|
const isSelected = t.id === selectedTemplateId;
|
||||||
|
const dpi = t.dpi === 300 ? 300 : 203;
|
||||||
|
const tmplHeight = t.height || 200;
|
||||||
|
const tmplWidth = t.width || 448;
|
||||||
|
const hasPrinterLength = printerLabelLength != null && printerLabelLength > 0;
|
||||||
|
const totalHeight = hasPrinterLength ? Math.max(tmplHeight, printerLabelLength) : tmplHeight;
|
||||||
|
const isCutoff = hasPrinterLength && printerLabelLength < tmplHeight;
|
||||||
|
const outlineColor = isCutoff ? '#d32f2f' : '#1976d2';
|
||||||
|
const tmplHeightPercent = (tmplHeight / totalHeight) * 100;
|
||||||
|
const printerHeightPercent = hasPrinterLength ? (printerLabelLength / totalHeight) * 100 : 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={t.id}>
|
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={t.id}>
|
||||||
<Paper
|
<Paper
|
||||||
@@ -259,18 +286,81 @@ export default class ArticleLabelTab extends PureComponent {
|
|||||||
bgcolor: 'background.default',
|
bgcolor: 'background.default',
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t.thumb ? (
|
{t.thumb ? (
|
||||||
<img
|
<Box
|
||||||
src={`data:image/png;base64,${t.thumb}`}
|
sx={{
|
||||||
alt={t.name}
|
position: 'relative',
|
||||||
style={{
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
objectFit: 'contain',
|
aspectRatio: `${tmplWidth} / ${totalHeight}`,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
bgcolor: '#fff',
|
||||||
|
borderRadius: 0.5,
|
||||||
|
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
{/* Template preview image */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: `${tmplHeightPercent}%`,
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderRadius: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`data:image/png;base64,${t.thumb}`}
|
||||||
|
alt={t.name}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
display: 'block',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* If template exceeds printer label length, shade the cutoff overflow */}
|
||||||
|
{isCutoff && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: `${(printerLabelLength / tmplHeight) * 100}%`,
|
||||||
|
bottom: 0,
|
||||||
|
bgcolor: 'rgba(211, 47, 47, 0.22)',
|
||||||
|
zIndex: 1,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Dotted outline for printer label length */}
|
||||||
|
{hasPrinterLength && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: `${printerHeightPercent}%`,
|
||||||
|
border: `2px dotted ${outlineColor}`,
|
||||||
|
borderRadius: 0.5,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="caption" color="text.disabled">
|
<Typography variant="caption" color="text.disabled">
|
||||||
Keine Vorschau
|
Keine Vorschau
|
||||||
@@ -282,7 +372,7 @@ export default class ArticleLabelTab extends PureComponent {
|
|||||||
{t.name}
|
{t.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{t.width}x{t.height} ({t.dpi} dpi)
|
{t.width}x{t.height} ({dpi} dpi)
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -301,11 +391,14 @@ export default class ArticleLabelTab extends PureComponent {
|
|||||||
onChange={(e) => this.setState({ selectedPrinterId: e.target.value })}
|
onChange={(e) => this.setState({ selectedPrinterId: e.target.value })}
|
||||||
sx={{ minWidth: 200 }}
|
sx={{ minWidth: 200 }}
|
||||||
>
|
>
|
||||||
{printers.map((p) => (
|
{printers.map((p) => {
|
||||||
<MenuItem key={p.id} value={p.id}>
|
const len = getPrinterLabelLength(p, livePrinters);
|
||||||
{getPrinterLabel(p)}
|
return (
|
||||||
</MenuItem>
|
<MenuItem key={p.id} value={p.id}>
|
||||||
))}
|
{getPrinterLabel(p)}{len != null ? ` (${len} px)` : ''}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TextInput>
|
</TextInput>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class CarrierItem extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleBlur = () => {
|
handleBlur = () => {
|
||||||
|
if (this.props.disabled) return;
|
||||||
const item = this.props.carrier ?? this.props.item;
|
const item = this.props.carrier ?? this.props.item;
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
const draft = this.state.draft.trim();
|
const draft = this.state.draft.trim();
|
||||||
@@ -63,6 +64,7 @@ class CarrierItem extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleToggleHasPackages = (e) => {
|
handleToggleHasPackages = (e) => {
|
||||||
|
if (this.props.disabled) return;
|
||||||
const item = this.props.carrier ?? this.props.item;
|
const item = this.props.carrier ?? this.props.item;
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
const nextVal = e.target.checked;
|
const nextVal = e.target.checked;
|
||||||
@@ -70,6 +72,7 @@ class CarrierItem extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleToggleIsReverse = (e) => {
|
handleToggleIsReverse = (e) => {
|
||||||
|
if (this.props.disabled) return;
|
||||||
const item = this.props.carrier ?? this.props.item;
|
const item = this.props.carrier ?? this.props.item;
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
const nextVal = e.target.checked;
|
const nextVal = e.target.checked;
|
||||||
@@ -77,6 +80,7 @@ class CarrierItem extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleKeyDown = (event) => {
|
handleKeyDown = (event) => {
|
||||||
|
if (this.props.disabled) return;
|
||||||
const item = this.props.carrier ?? this.props.item;
|
const item = this.props.carrier ?? this.props.item;
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -90,6 +94,7 @@ class CarrierItem extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleDelete = () => {
|
handleDelete = () => {
|
||||||
|
if (this.props.disabled) return;
|
||||||
const item = this.props.carrier ?? this.props.item;
|
const item = this.props.carrier ?? this.props.item;
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
if (!window.confirm(`Versandart „${item.name}“ wirklich löschen?`)) return;
|
if (!window.confirm(`Versandart „${item.name}“ wirklich löschen?`)) return;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class KundeGruppe extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleBlur = () => {
|
handleBlur = () => {
|
||||||
|
if (this.props.disabled) return;
|
||||||
const item = this.props.group ?? this.props.item;
|
const item = this.props.group ?? this.props.item;
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
const editable = item.editable !== false;
|
const editable = item.editable !== false;
|
||||||
@@ -79,6 +80,7 @@ class KundeGruppe extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleDelete = () => {
|
handleDelete = () => {
|
||||||
|
if (this.props.disabled) return;
|
||||||
const item = this.props.group ?? this.props.item;
|
const item = this.props.group ?? this.props.item;
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
const editable = item.editable !== false;
|
const editable = item.editable !== false;
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ import WidgetsOutlinedIcon from '@mui/icons-material/WidgetsOutlined';
|
|||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { vfs } from '../../api';
|
import { vfs } from '../../api';
|
||||||
import backupAutomat from '../../automats/backupAutomat';
|
import backupAutomat from '../../automats/backupAutomat';
|
||||||
|
import companyAutomat from '../../automats/companyAutomat';
|
||||||
|
import { isProtectedVfsFile } from '../vfs/vfsUtils';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
import {
|
import {
|
||||||
BORDER,
|
BORDER,
|
||||||
@@ -93,6 +95,7 @@ export default class BackupSettingsPanel extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
const { status, error, backupStatus, backups, vfsFilesByBackup, vfsLoadingByBackup } = backupAutomat.state;
|
const { status, error, backupStatus, backups, vfsFilesByBackup, vfsLoadingByBackup } = backupAutomat.state;
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
backupStatus,
|
backupStatus,
|
||||||
@@ -115,6 +118,9 @@ export default class BackupSettingsPanel extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
|
}));
|
||||||
this.unsub = backupAutomat.subscribe(this, (state) => ({
|
this.unsub = backupAutomat.subscribe(this, (state) => ({
|
||||||
status: state.status,
|
status: state.status,
|
||||||
error: state.error,
|
error: state.error,
|
||||||
@@ -130,6 +136,7 @@ export default class BackupSettingsPanel extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
this.unsubCompany?.();
|
||||||
this.unsub?.();
|
this.unsub?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +162,7 @@ export default class BackupSettingsPanel extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleOpenConfirm(type) {
|
handleOpenConfirm(type) {
|
||||||
|
if (!this.state.isAdmin) return;
|
||||||
this.setState({ confirmRestore: type });
|
this.setState({ confirmRestore: type });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +223,7 @@ export default class BackupSettingsPanel extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
isAdmin,
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
backupStatus,
|
backupStatus,
|
||||||
@@ -463,6 +472,7 @@ export default class BackupSettingsPanel extends Component {
|
|||||||
<BackupTableRow
|
<BackupTableRow
|
||||||
key={item.name}
|
key={item.name}
|
||||||
item={item}
|
item={item}
|
||||||
|
isAdmin={isAdmin}
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
manifest={manifest}
|
manifest={manifest}
|
||||||
vfsFiles={vfsFiles}
|
vfsFiles={vfsFiles}
|
||||||
@@ -549,7 +559,7 @@ export default class BackupSettingsPanel extends Component {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={restoringSkills ? <CircularProgress size={14} color="inherit" /> : <SettingsBackupRestoreIcon fontSize="small" />}
|
startIcon={restoringSkills ? <CircularProgress size={14} color="inherit" /> : <SettingsBackupRestoreIcon fontSize="small" />}
|
||||||
onClick={() => this.handleOpenConfirm('skills')}
|
onClick={() => this.handleOpenConfirm('skills')}
|
||||||
disabled={restoringSkills}
|
disabled={!isAdmin || restoringSkills}
|
||||||
sx={{ textTransform: 'none', minHeight: 30, fontSize: '0.8rem', fontWeight: 600 }}
|
sx={{ textTransform: 'none', minHeight: 30, fontSize: '0.8rem', fontWeight: 600 }}
|
||||||
>
|
>
|
||||||
Skills wiederherstellen
|
Skills wiederherstellen
|
||||||
@@ -607,7 +617,7 @@ export default class BackupSettingsPanel extends Component {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={restoringApplets ? <CircularProgress size={14} color="inherit" /> : <SettingsBackupRestoreIcon fontSize="small" />}
|
startIcon={restoringApplets ? <CircularProgress size={14} color="inherit" /> : <SettingsBackupRestoreIcon fontSize="small" />}
|
||||||
onClick={() => this.handleOpenConfirm('applets')}
|
onClick={() => this.handleOpenConfirm('applets')}
|
||||||
disabled={restoringApplets}
|
disabled={!isAdmin || restoringApplets}
|
||||||
sx={{ textTransform: 'none', minHeight: 30, fontSize: '0.8rem', fontWeight: 600 }}
|
sx={{ textTransform: 'none', minHeight: 30, fontSize: '0.8rem', fontWeight: 600 }}
|
||||||
>
|
>
|
||||||
Applets wiederherstellen
|
Applets wiederherstellen
|
||||||
@@ -740,6 +750,7 @@ class BackupTableRow extends Component {
|
|||||||
// MARK: calls POST /backup/:id/vfs/recover via backupAutomat.actions.recoverBackupVfs from ./api/index.js
|
// MARK: calls POST /backup/:id/vfs/recover via backupAutomat.actions.recoverBackupVfs from ./api/index.js
|
||||||
async handleRecoverFile(e, path) {
|
async handleRecoverFile(e, path) {
|
||||||
e?.stopPropagation?.();
|
e?.stopPropagation?.();
|
||||||
|
if (!this.props.isAdmin && isProtectedVfsFile(path)) return;
|
||||||
if (this.state.recoveringPath) return;
|
if (this.state.recoveringPath) return;
|
||||||
|
|
||||||
this.setState({ recoveringPath: path });
|
this.setState({ recoveringPath: path });
|
||||||
@@ -991,7 +1002,7 @@ class BackupTableRow extends Component {
|
|||||||
color="primary"
|
color="primary"
|
||||||
startIcon={isRecovering ? <CircularProgress size={12} color="inherit" /> : <SettingsBackupRestoreIcon fontSize="small" />}
|
startIcon={isRecovering ? <CircularProgress size={12} color="inherit" /> : <SettingsBackupRestoreIcon fontSize="small" />}
|
||||||
onClick={(e) => this.handleRecoverFile(e, f.name)}
|
onClick={(e) => this.handleRecoverFile(e, f.name)}
|
||||||
disabled={Boolean(recoveringPath)}
|
disabled={Boolean(recoveringPath) || (!this.props.isAdmin && isProtectedVfsFile(f.name))}
|
||||||
sx={{ textTransform: 'none', py: 0.25, px: 1, minHeight: 26, fontSize: '0.75rem' }}
|
sx={{ textTransform: 'none', py: 0.25, px: 1, minHeight: 26, fontSize: '0.75rem' }}
|
||||||
>
|
>
|
||||||
{isRecovering ? 'Wiederherstellen...' : 'Wiederherstellen'}
|
{isRecovering ? 'Wiederherstellen...' : 'Wiederherstellen'}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default class CompanySettingsPanel extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
const current = companyAutomat.state.current;
|
const current = companyAutomat.state.current;
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: current?.role === 'admin',
|
||||||
status: current ? 'ready' : 'loading',
|
status: current ? 'ready' : 'loading',
|
||||||
busy: false,
|
busy: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -42,6 +43,9 @@ export default class CompanySettingsPanel extends Component {
|
|||||||
if (state.current && this.state.status === 'loading') {
|
if (state.current && this.state.status === 'loading') {
|
||||||
this.syncFormFromCompany(state.current);
|
this.syncFormFromCompany(state.current);
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
if (companyAutomat.state.current) {
|
if (companyAutomat.state.current) {
|
||||||
this.syncFormFromCompany(companyAutomat.state.current);
|
this.syncFormFromCompany(companyAutomat.state.current);
|
||||||
@@ -86,6 +90,7 @@ export default class CompanySettingsPanel extends Component {
|
|||||||
|
|
||||||
async handleSubmit(event) {
|
async handleSubmit(event) {
|
||||||
event?.preventDefault?.();
|
event?.preventDefault?.();
|
||||||
|
if (!this.state.isAdmin) return;
|
||||||
const { name, iso639, taxrate, reducedtaxrate } = this.state;
|
const { name, iso639, taxrate, reducedtaxrate } = this.state;
|
||||||
const parsedTaxrate = parseTaxRate(taxrate);
|
const parsedTaxrate = parseTaxRate(taxrate);
|
||||||
const parsedReducedtaxrate = parseTaxRate(reducedtaxrate);
|
const parsedReducedtaxrate = parseTaxRate(reducedtaxrate);
|
||||||
@@ -123,7 +128,7 @@ export default class CompanySettingsPanel extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { status, busy, error, name, iso639, taxrate, reducedtaxrate } = this.state;
|
const { isAdmin, status, busy, error, name, iso639, taxrate, reducedtaxrate } = this.state;
|
||||||
|
|
||||||
if (status === 'loading' && !name) {
|
if (status === 'loading' && !name) {
|
||||||
return (
|
return (
|
||||||
@@ -146,6 +151,7 @@ export default class CompanySettingsPanel extends Component {
|
|||||||
taxrate.trim() !== this._initial.taxrate.trim() ||
|
taxrate.trim() !== this._initial.taxrate.trim() ||
|
||||||
reducedtaxrate.trim() !== this._initial.reducedtaxrate.trim());
|
reducedtaxrate.trim() !== this._initial.reducedtaxrate.trim());
|
||||||
const canSave =
|
const canSave =
|
||||||
|
isAdmin &&
|
||||||
isDirty &&
|
isDirty &&
|
||||||
name.trim() !== '' &&
|
name.trim() !== '' &&
|
||||||
ISO639_OPTIONS.includes(iso639) &&
|
ISO639_OPTIONS.includes(iso639) &&
|
||||||
@@ -175,7 +181,7 @@ export default class CompanySettingsPanel extends Component {
|
|||||||
size="small"
|
size="small"
|
||||||
label="Name"
|
label="Name"
|
||||||
value={name}
|
value={name}
|
||||||
disabled={busy}
|
disabled={!isAdmin || busy}
|
||||||
onChange={(nextName) => this.setState({ name: nextName })}
|
onChange={(nextName) => this.setState({ name: nextName })}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -188,7 +194,7 @@ export default class CompanySettingsPanel extends Component {
|
|||||||
required
|
required
|
||||||
size="small"
|
size="small"
|
||||||
value={iso639}
|
value={iso639}
|
||||||
disabled={busy}
|
disabled={!isAdmin || busy}
|
||||||
onChange={(_event, value) => {
|
onChange={(_event, value) => {
|
||||||
if (value) this.setState({ iso639: value });
|
if (value) this.setState({ iso639: value });
|
||||||
}}
|
}}
|
||||||
@@ -206,7 +212,7 @@ export default class CompanySettingsPanel extends Component {
|
|||||||
type="number"
|
type="number"
|
||||||
label="Normalsatz"
|
label="Normalsatz"
|
||||||
value={taxrate}
|
value={taxrate}
|
||||||
disabled={busy}
|
disabled={!isAdmin || busy}
|
||||||
onChange={(e) => this.setState({ taxrate: e.target.value })}
|
onChange={(e) => this.setState({ taxrate: e.target.value })}
|
||||||
error={taxrate !== '' && parsedTaxrate == null}
|
error={taxrate !== '' && parsedTaxrate == null}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@@ -223,7 +229,7 @@ export default class CompanySettingsPanel extends Component {
|
|||||||
type="number"
|
type="number"
|
||||||
label="Ermäßigter Satz"
|
label="Ermäßigter Satz"
|
||||||
value={reducedtaxrate}
|
value={reducedtaxrate}
|
||||||
disabled={busy}
|
disabled={!isAdmin || busy}
|
||||||
onChange={(e) => this.setState({ reducedtaxrate: e.target.value })}
|
onChange={(e) => this.setState({ reducedtaxrate: e.target.value })}
|
||||||
error={reducedtaxrate !== '' && parsedReducedtaxrate == null}
|
error={reducedtaxrate !== '' && parsedReducedtaxrate == null}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@@ -240,7 +246,7 @@ export default class CompanySettingsPanel extends Component {
|
|||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color={canSave ? 'success' : 'inherit'}
|
color={canSave ? 'success' : 'inherit'}
|
||||||
disabled={busy || !canSave}
|
disabled={!isAdmin || busy || !canSave}
|
||||||
startIcon={busy ? <CircularProgress size={16} color="inherit" /> : null}
|
startIcon={busy ? <CircularProgress size={16} color="inherit" /> : null}
|
||||||
sx={
|
sx={
|
||||||
canSave
|
canSave
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export default class ImportExportSettingsPanel extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
// Export state
|
// Export state
|
||||||
downloading: false,
|
downloading: false,
|
||||||
downloadProgress: null, // null = unknown, number 0-100
|
downloadProgress: null, // null = unknown, number 0-100
|
||||||
@@ -84,7 +85,14 @@ export default class ImportExportSettingsPanel extends Component {
|
|||||||
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
this.unsub?.();
|
||||||
if (this.uploadAbortController) {
|
if (this.uploadAbortController) {
|
||||||
this.uploadAbortController.abort();
|
this.uploadAbortController.abort();
|
||||||
}
|
}
|
||||||
@@ -147,6 +155,7 @@ export default class ImportExportSettingsPanel extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleFileChange(event) {
|
handleFileChange(event) {
|
||||||
|
if (!this.state.isAdmin) return;
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
if (!isZipFile(file)) {
|
if (!isZipFile(file)) {
|
||||||
@@ -169,6 +178,7 @@ export default class ImportExportSettingsPanel extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleStartUpload() {
|
async handleStartUpload() {
|
||||||
|
if (!this.state.isAdmin) return;
|
||||||
const { selectedFile } = this.state;
|
const { selectedFile } = this.state;
|
||||||
if (!selectedFile) return;
|
if (!selectedFile) return;
|
||||||
|
|
||||||
@@ -282,6 +292,7 @@ export default class ImportExportSettingsPanel extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
isAdmin,
|
||||||
downloading,
|
downloading,
|
||||||
downloadProgress,
|
downloadProgress,
|
||||||
downloadStatus,
|
downloadStatus,
|
||||||
@@ -583,7 +594,7 @@ export default class ImportExportSettingsPanel extends Component {
|
|||||||
accept=".zip,application/zip,application/x-zip-compressed"
|
accept=".zip,application/zip,application/x-zip-compressed"
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={this.handleFileChange}
|
onChange={this.handleFileChange}
|
||||||
disabled={uploading}
|
disabled={!isAdmin || uploading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!selectedFile ? (
|
{!selectedFile ? (
|
||||||
@@ -592,7 +603,7 @@ export default class ImportExportSettingsPanel extends Component {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={<InsertDriveFileOutlinedIcon />}
|
startIcon={<InsertDriveFileOutlinedIcon />}
|
||||||
onClick={() => this.fileInputRef.current?.click()}
|
onClick={() => this.fileInputRef.current?.click()}
|
||||||
disabled={uploading}
|
disabled={!isAdmin || uploading}
|
||||||
>
|
>
|
||||||
ZIP-Datei auswählen
|
ZIP-Datei auswählen
|
||||||
</Button>
|
</Button>
|
||||||
@@ -625,6 +636,7 @@ export default class ImportExportSettingsPanel extends Component {
|
|||||||
{!uploading && (
|
{!uploading && (
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
|
disabled={!isAdmin}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.setState({ selectedFile: null });
|
this.setState({ selectedFile: null });
|
||||||
if (this.fileInputRef.current) this.fileInputRef.current.value = '';
|
if (this.fileInputRef.current) this.fileInputRef.current.value = '';
|
||||||
@@ -760,7 +772,7 @@ export default class ImportExportSettingsPanel extends Component {
|
|||||||
<CloudUploadOutlinedIcon />
|
<CloudUploadOutlinedIcon />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
disabled={uploading || !selectedFile}
|
disabled={!isAdmin || uploading || !selectedFile}
|
||||||
onClick={this.handleStartUpload}
|
onClick={this.handleStartUpload}
|
||||||
>
|
>
|
||||||
{uploading ? 'Import läuft…' : 'Upload & Import starten'}
|
{uploading ? 'Import läuft…' : 'Upload & Import starten'}
|
||||||
|
|||||||
219
src/components/settings/MembersSettingsPanel.jsx
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import { PureComponent } from 'react';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
|
import MailOutlinedIcon from '@mui/icons-material/MailOutlined';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
import companyAutomat, { selectIsCompanyAdmin } from '../../automats/companyAutomat';
|
||||||
|
import { normalizePicture } from '../../automats/authAutomat';
|
||||||
|
import { notifyError } from '../../utils/notify';
|
||||||
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
|
|
||||||
|
const ROLES = [
|
||||||
|
{ value: 'user', label: 'Benutzer' },
|
||||||
|
{ value: 'admin', label: 'Administrator' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectState = (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
|
members: state.members,
|
||||||
|
membersStatus: state.membersStatus,
|
||||||
|
invites: state.invites,
|
||||||
|
invitesStatus: state.invitesStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Company members and pending auto-invites. Visible and usable by admins only.
|
||||||
|
*/
|
||||||
|
export default class MembersSettingsPanel extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
...selectState(companyAutomat.state),
|
||||||
|
memberEmail: '',
|
||||||
|
inviteEmail: '',
|
||||||
|
inviteRole: 'user',
|
||||||
|
busy: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.unsub = companyAutomat.subscribe(this, selectState);
|
||||||
|
this.loadIfAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(_prevProps, prevState) {
|
||||||
|
if (!prevState.isAdmin && this.state.isAdmin) this.loadIfAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsub?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadIfAdmin() {
|
||||||
|
if (!selectIsCompanyAdmin()) return;
|
||||||
|
companyAutomat.actions.ensureMembersLoaded().catch(notifyError);
|
||||||
|
companyAutomat.actions.ensureInvitesLoaded().catch(notifyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs a mutation, mapping the backend's 404/409 to friendly messages.
|
||||||
|
run = async (fn, messages = {}) => {
|
||||||
|
this.setState({ busy: true });
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
const normalized = backendErrorFrom(err);
|
||||||
|
const text = messages[normalized.status];
|
||||||
|
if (text) enqueueSnackbar(text, { variant: 'warning' });
|
||||||
|
else notifyError(err);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.setState({ busy: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleAddMember = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const email = this.state.memberEmail.trim();
|
||||||
|
if (!email) return;
|
||||||
|
const ok = await this.run(() => companyAutomat.actions.addMember(email, 'user'), {
|
||||||
|
404: 'Kein registrierter Benutzer mit dieser E-Mail. Bitte stattdessen einladen.',
|
||||||
|
409: 'Benutzer gehört bereits zum Unternehmen.',
|
||||||
|
});
|
||||||
|
if (ok) this.setState({ memberEmail: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSetRole = (member, role) =>
|
||||||
|
this.run(() => companyAutomat.actions.setMemberRole(member.id, role), {
|
||||||
|
409: 'Der letzte Administrator kann nicht herabgestuft werden.',
|
||||||
|
});
|
||||||
|
|
||||||
|
handleRemoveMember = (member) =>
|
||||||
|
this.run(() => companyAutomat.actions.removeMember(member.id), {
|
||||||
|
409: 'Der letzte Administrator kann nicht entfernt werden.',
|
||||||
|
});
|
||||||
|
|
||||||
|
handleCreateInvite = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const email = this.state.inviteEmail.trim();
|
||||||
|
if (!email) return;
|
||||||
|
const ok = await this.run(() => companyAutomat.actions.createInvite(email, this.state.inviteRole), {
|
||||||
|
409: 'Diese E-Mail ist bereits eingeladen oder registriert (dann direkt als Mitglied hinzufügen).',
|
||||||
|
});
|
||||||
|
if (ok) this.setState({ inviteEmail: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDeleteInvite = (invite) => this.run(() => companyAutomat.actions.deleteInvite(invite.id));
|
||||||
|
|
||||||
|
renderRoleSelect(value, onChange, disabled) {
|
||||||
|
return (
|
||||||
|
<Select size="small" value={value} disabled={disabled} onChange={(e) => onChange(e.target.value)}>
|
||||||
|
{ROLES.map((r) => (
|
||||||
|
<MenuItem key={r.value} value={r.value}>
|
||||||
|
{r.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isAdmin, members, membersStatus, invites, invitesStatus, memberEmail, inviteEmail, inviteRole, busy } =
|
||||||
|
this.state;
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return <Typography color="text.secondary">Nur Administratoren können Mitglieder verwalten.</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = membersStatus === 'loading' || invitesStatus === 'loading';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Mitglieder
|
||||||
|
</Typography>
|
||||||
|
<Stack component="form" direction="row" spacing={1} onSubmit={this.handleAddMember} sx={{ mb: 2 }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
type="email"
|
||||||
|
label="E-Mail"
|
||||||
|
value={memberEmail}
|
||||||
|
onChange={(e) => this.setState({ memberEmail: e.target.value })}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<Button type="submit" variant="contained" disabled={busy || !memberEmail.trim()}>
|
||||||
|
Hinzufügen
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
{loading && members.length === 0 && <CircularProgress size={20} />}
|
||||||
|
{members.map((m) => (
|
||||||
|
<Stack key={m.id} direction="row" spacing={1.5} sx={{ alignItems: 'center', py: 0.75 }}>
|
||||||
|
<Avatar
|
||||||
|
src={normalizePicture(m.picture) || undefined}
|
||||||
|
alt={m.email}
|
||||||
|
sx={{ width: 32, height: 32, fontSize: '0.875rem' }}
|
||||||
|
>
|
||||||
|
{m.email?.[0]?.toUpperCase()}
|
||||||
|
</Avatar>
|
||||||
|
<Typography sx={{ flex: 1 }}>{m.email}</Typography>
|
||||||
|
{this.renderRoleSelect(m.role, (role) => this.handleSetRole(m, role), busy)}
|
||||||
|
<IconButton aria-label="Entfernen" disabled={busy} onClick={() => this.handleRemoveMember(m)}>
|
||||||
|
<DeleteOutlineIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Einladungen
|
||||||
|
</Typography>
|
||||||
|
<Stack component="form" direction="row" spacing={1} onSubmit={this.handleCreateInvite} sx={{ mb: 2 }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
type="email"
|
||||||
|
label="E-Mail"
|
||||||
|
value={inviteEmail}
|
||||||
|
onChange={(e) => this.setState({ inviteEmail: e.target.value })}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
{this.renderRoleSelect(inviteRole, (role) => this.setState({ inviteRole: role }), busy)}
|
||||||
|
<Button type="submit" variant="contained" disabled={busy || !inviteEmail.trim()}>
|
||||||
|
Einladen
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
{invites.map((i) => (
|
||||||
|
<Stack key={i.id} direction="row" spacing={1.5} sx={{ alignItems: 'center', py: 0.75 }}>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
bgcolor: 'action.selected',
|
||||||
|
color: 'text.secondary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MailOutlinedIcon sx={{ fontSize: 18 }} />
|
||||||
|
</Avatar>
|
||||||
|
<Typography sx={{ flex: 1 }}>{i.email}</Typography>
|
||||||
|
<Typography color="text.secondary">{ROLES.find((r) => r.value === i.role)?.label ?? i.role}</Typography>
|
||||||
|
<IconButton aria-label="Einladung löschen" disabled={busy} onClick={() => this.handleDeleteInvite(i)}>
|
||||||
|
<DeleteOutlineIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
|||||||
: [];
|
: [];
|
||||||
const numbersStatus = companyAutomat.state.numbersStatus;
|
const numbersStatus = companyAutomat.state.numbersStatus;
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
status:
|
status:
|
||||||
numbersStatus === 'ready'
|
numbersStatus === 'ready'
|
||||||
? 'ready'
|
? 'ready'
|
||||||
@@ -81,6 +82,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
|||||||
this.unsub = companyAutomat.subscribe(this, (state) => {
|
this.unsub = companyAutomat.subscribe(this, (state) => {
|
||||||
const numbers = Array.isArray(state.numbers) ? state.numbers : [];
|
const numbers = Array.isArray(state.numbers) ? state.numbers : [];
|
||||||
return {
|
return {
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
status:
|
status:
|
||||||
state.numbersStatus === 'ready'
|
state.numbersStatus === 'ready'
|
||||||
? 'ready'
|
? 'ready'
|
||||||
@@ -121,6 +123,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSave(id) {
|
async handleSave(id) {
|
||||||
|
if (!this.state.isAdmin) return;
|
||||||
const draft = this.state.drafts[id];
|
const draft = this.state.drafts[id];
|
||||||
if (!draft || draft.busy) return;
|
if (!draft || draft.busy) return;
|
||||||
const row = this.state.numbers.find((r) => r.id === id);
|
const row = this.state.numbers.find((r) => r.id === id);
|
||||||
@@ -211,7 +214,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { status, error, numbers, drafts } = this.state;
|
const { isAdmin, status, error, numbers, drafts } = this.state;
|
||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
@@ -249,7 +252,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
|||||||
<TextInput
|
<TextInput
|
||||||
size="small"
|
size="small"
|
||||||
value={draft.prefix}
|
value={draft.prefix}
|
||||||
disabled={draft.busy}
|
disabled={!isAdmin || draft.busy}
|
||||||
onChange={(e) => this.handleChange(row.id, 'prefix', e.target.value)}
|
onChange={(e) => this.handleChange(row.id, 'prefix', e.target.value)}
|
||||||
sx={{ input: { fontFamily: 'monospace' } }}
|
sx={{ input: { fontFamily: 'monospace' } }}
|
||||||
/>
|
/>
|
||||||
@@ -259,7 +262,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
|||||||
size="small"
|
size="small"
|
||||||
type="number"
|
type="number"
|
||||||
value={draft.padding}
|
value={draft.padding}
|
||||||
disabled={draft.busy}
|
disabled={!isAdmin || draft.busy}
|
||||||
slotProps={{ htmlInput: { min: 1, step: 1 } }}
|
slotProps={{ htmlInput: { min: 1, step: 1 } }}
|
||||||
onChange={(e) => this.handleChange(row.id, 'padding', e.target.value)}
|
onChange={(e) => this.handleChange(row.id, 'padding', e.target.value)}
|
||||||
sx={{ input: { fontFamily: 'monospace' } }}
|
sx={{ input: { fontFamily: 'monospace' } }}
|
||||||
@@ -270,7 +273,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
|||||||
size="small"
|
size="small"
|
||||||
type="number"
|
type="number"
|
||||||
value={draft.next}
|
value={draft.next}
|
||||||
disabled={draft.busy}
|
disabled={!isAdmin || draft.busy}
|
||||||
slotProps={{ htmlInput: { min: 0, step: 1 } }}
|
slotProps={{ htmlInput: { min: 0, step: 1 } }}
|
||||||
onChange={(e) => this.handleChange(row.id, 'next', e.target.value)}
|
onChange={(e) => this.handleChange(row.id, 'next', e.target.value)}
|
||||||
sx={{ input: { fontFamily: 'monospace' } }}
|
sx={{ input: { fontFamily: 'monospace' } }}
|
||||||
@@ -295,7 +298,7 @@ export default class NumberSettingsPanel extends PureComponent {
|
|||||||
size="small"
|
size="small"
|
||||||
aria-label="Speichern"
|
aria-label="Speichern"
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={draft.busy}
|
disabled={!isAdmin || draft.busy}
|
||||||
onClick={() => void this.handleSave(row.id)}
|
onClick={() => void this.handleSave(row.id)}
|
||||||
>
|
>
|
||||||
<SaveOutlinedIcon fontSize="small" />
|
<SaveOutlinedIcon fontSize="small" />
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
const terminals = companyAutomat.state.terminals ?? [];
|
const terminals = companyAutomat.state.terminals ?? [];
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
status: companyAutomat.state.terminalsStatus === 'succeeded' ? 'ready' : 'loading',
|
status: companyAutomat.state.terminalsStatus === 'succeeded' ? 'ready' : 'loading',
|
||||||
error: null,
|
error: null,
|
||||||
terminals,
|
terminals,
|
||||||
@@ -93,6 +94,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
terminals: Array.isArray(state.terminals) ? state.terminals : [],
|
terminals: Array.isArray(state.terminals) ? state.terminals : [],
|
||||||
status: state.terminalsStatus === 'succeeded' ? 'ready' : state.terminalsStatus === 'failed' ? 'failed' : this.state.status,
|
status: state.terminalsStatus === 'succeeded' ? 'ready' : state.terminalsStatus === 'failed' ? 'failed' : this.state.status,
|
||||||
}));
|
}));
|
||||||
@@ -133,6 +135,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleCreateTerminal() {
|
async handleCreateTerminal() {
|
||||||
|
if (!this.state.isAdmin) return;
|
||||||
const { newTerminalName, enableAllForNew, terminals } = this.state;
|
const { newTerminalName, enableAllForNew, terminals } = this.state;
|
||||||
const trimmed = newTerminalName.trim();
|
const trimmed = newTerminalName.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
@@ -294,6 +297,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
isAdmin,
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
terminals,
|
terminals,
|
||||||
@@ -362,7 +366,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
placeholder="Neues Terminal..."
|
placeholder="Neues Terminal..."
|
||||||
value={newTerminalName}
|
value={newTerminalName}
|
||||||
onChange={(e) => this.setState({ newTerminalName: e.target.value })}
|
onChange={(e) => this.setState({ newTerminalName: e.target.value })}
|
||||||
disabled={creatingTerminal}
|
disabled={!isAdmin || creatingTerminal}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
slotProps={{
|
slotProps={{
|
||||||
htmlInput: {
|
htmlInput: {
|
||||||
@@ -387,7 +391,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
size="small"
|
size="small"
|
||||||
checked={enableAllForNew}
|
checked={enableAllForNew}
|
||||||
onChange={(e) => this.setState({ enableAllForNew: e.target.checked })}
|
onChange={(e) => this.setState({ enableAllForNew: e.target.checked })}
|
||||||
disabled={creatingTerminal}
|
disabled={!isAdmin || creatingTerminal}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -396,7 +400,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
size="small"
|
size="small"
|
||||||
startIcon={creatingTerminal ? <CircularProgress size={16} color="inherit" /> : <AddIcon />}
|
startIcon={creatingTerminal ? <CircularProgress size={16} color="inherit" /> : <AddIcon />}
|
||||||
onClick={this.handleCreateTerminal}
|
onClick={this.handleCreateTerminal}
|
||||||
disabled={creatingTerminal || !newTerminalName.trim()}
|
disabled={!isAdmin || creatingTerminal || !newTerminalName.trim()}
|
||||||
>
|
>
|
||||||
Hinzufügen
|
Hinzufügen
|
||||||
</Button>
|
</Button>
|
||||||
@@ -492,7 +496,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={isEnabled}
|
checked={isEnabled}
|
||||||
disabled={isPatching || isDeleting}
|
disabled={!isAdmin || isPatching || isDeleting}
|
||||||
onChange={(e) => void this.handleToggleEnabled(terminal, e.target.checked)}
|
onChange={(e) => void this.handleToggleEnabled(terminal, e.target.checked)}
|
||||||
color="success"
|
color="success"
|
||||||
/>
|
/>
|
||||||
@@ -516,7 +520,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
size="small"
|
size="small"
|
||||||
aria-label={`Terminal ${terminal.name} löschen`}
|
aria-label={`Terminal ${terminal.name} löschen`}
|
||||||
onClick={() => void this.handleDeleteTerminal(terminal)}
|
onClick={() => void this.handleDeleteTerminal(terminal)}
|
||||||
disabled={isDeleting || creatingTerminal || isPatching}
|
disabled={!isAdmin || isDeleting || creatingTerminal || isPatching}
|
||||||
color="error"
|
color="error"
|
||||||
>
|
>
|
||||||
{isDeleting ? (
|
{isDeleting ? (
|
||||||
@@ -546,13 +550,14 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
<Chip
|
<Chip
|
||||||
icon={<FolderIcon />}
|
icon={<FolderIcon />}
|
||||||
label={this.getCategoryLabel(terminal)}
|
label={this.getCategoryLabel(terminal)}
|
||||||
onClick={() => this.setState({ pickerTerminal: terminal })}
|
onClick={isAdmin ? () => this.setState({ pickerTerminal: terminal }) : undefined}
|
||||||
onDelete={
|
onDelete={
|
||||||
terminal.rootCategoryId != null
|
isAdmin && terminal.rootCategoryId != null
|
||||||
? () => void this.handleSetRootCategory(terminal, null)
|
? () => void this.handleSetRootCategory(terminal, null)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
clickable
|
clickable={isAdmin}
|
||||||
|
disabled={!isAdmin || isPatching || isDeleting}
|
||||||
variant={terminal.rootCategoryId != null ? 'filled' : 'outlined'}
|
variant={terminal.rootCategoryId != null ? 'filled' : 'outlined'}
|
||||||
color={terminal.rootCategoryId != null ? 'primary' : 'default'}
|
color={terminal.rootCategoryId != null ? 'primary' : 'default'}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -560,6 +565,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="text"
|
variant="text"
|
||||||
|
disabled={!isAdmin || isPatching || isDeleting}
|
||||||
onClick={() => this.setState({ pickerTerminal: terminal })}
|
onClick={() => this.setState({ pickerTerminal: terminal })}
|
||||||
sx={{ minWidth: 'auto', p: 0.5, fontSize: '0.75rem' }}
|
sx={{ minWidth: 'auto', p: 0.5, fontSize: '0.75rem' }}
|
||||||
>
|
>
|
||||||
@@ -576,7 +582,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
disabled={index === 0 || isPatching || isDeleting}
|
disabled={!isAdmin || index === 0 || isPatching || isDeleting}
|
||||||
onClick={() => void this.handleMoveTerminal(terminal, 'up')}
|
onClick={() => void this.handleMoveTerminal(terminal, 'up')}
|
||||||
sx={{ p: 0.25 }}
|
sx={{ p: 0.25 }}
|
||||||
>
|
>
|
||||||
@@ -588,7 +594,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
disabled={index === terminals.length - 1 || isPatching || isDeleting}
|
disabled={!isAdmin || index === terminals.length - 1 || isPatching || isDeleting}
|
||||||
onClick={() => void this.handleMoveTerminal(terminal, 'down')}
|
onClick={() => void this.handleMoveTerminal(terminal, 'down')}
|
||||||
sx={{ p: 0.25 }}
|
sx={{ p: 0.25 }}
|
||||||
>
|
>
|
||||||
@@ -599,7 +605,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
size="small"
|
size="small"
|
||||||
disabled={isPatching || isDeleting}
|
disabled={!isAdmin || isPatching || isDeleting}
|
||||||
value={this.state.sortDrafts[terminal.id] ?? terminal.sort ?? 0}
|
value={this.state.sortDrafts[terminal.id] ?? terminal.sort ?? 0}
|
||||||
onChange={(e) => this.handleSortDraftChange(terminal.id, e.target.value)}
|
onChange={(e) => this.handleSortDraftChange(terminal.id, e.target.value)}
|
||||||
onBlur={() => void this.handleSortSubmit(terminal)}
|
onBlur={() => void this.handleSortSubmit(terminal)}
|
||||||
@@ -624,7 +630,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={Boolean(terminal.syncRootArticles)}
|
checked={Boolean(terminal.syncRootArticles)}
|
||||||
disabled={isPatching || isDeleting}
|
disabled={!isAdmin || isPatching || isDeleting}
|
||||||
onChange={(e) => void this.handleToggleSyncRootArticles(terminal, e.target.checked)}
|
onChange={(e) => void this.handleToggleSyncRootArticles(terminal, e.target.checked)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
printeragents: Array.isArray(companyAutomat.state.printeragents)
|
printeragents: Array.isArray(companyAutomat.state.printeragents)
|
||||||
? companyAutomat.state.printeragents
|
? companyAutomat.state.printeragents
|
||||||
: [],
|
: [],
|
||||||
@@ -254,6 +255,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
printeragents: Array.isArray(state.printeragents) ? state.printeragents : [],
|
printeragents: Array.isArray(state.printeragents) ? state.printeragents : [],
|
||||||
agentprinters: Array.isArray(state.agentprinters) ? state.agentprinters : [],
|
agentprinters: Array.isArray(state.agentprinters) ? state.agentprinters : [],
|
||||||
livePrinters: Array.isArray(state.livePrinters) ? state.livePrinters : [],
|
livePrinters: Array.isArray(state.livePrinters) ? state.livePrinters : [],
|
||||||
@@ -279,6 +281,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleCreateAgent() {
|
async handleCreateAgent() {
|
||||||
|
if (!this.state.isAdmin) return;
|
||||||
const { newAgentName, printeragents } = this.state;
|
const { newAgentName, printeragents } = this.state;
|
||||||
const trimmed = newAgentName.trim();
|
const trimmed = newAgentName.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
@@ -497,6 +500,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
isAdmin,
|
||||||
error,
|
error,
|
||||||
printeragents,
|
printeragents,
|
||||||
agentprinters,
|
agentprinters,
|
||||||
@@ -540,23 +544,42 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
<Typography variant="h6" fontWeight={600}>
|
<Typography variant="h6" fontWeight={600}>
|
||||||
Drucker-Agenten
|
Drucker-Agenten
|
||||||
</Typography>
|
</Typography>
|
||||||
<Link
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
|
||||||
href="https://github.com/sebseb7/zebragent/releases/latest"
|
<Link
|
||||||
target="_blank"
|
href="https://github.com/sebseb7/zebragent/releases/latest"
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
underline="hover"
|
rel="noopener noreferrer"
|
||||||
sx={{
|
underline="hover"
|
||||||
display: 'inline-flex',
|
sx={{
|
||||||
alignItems: 'center',
|
display: 'inline-flex',
|
||||||
gap: 0.5,
|
alignItems: 'center',
|
||||||
fontSize: '0.8125rem',
|
gap: 0.5,
|
||||||
color: 'primary.main',
|
fontSize: '0.8125rem',
|
||||||
fontWeight: 500,
|
color: 'primary.main',
|
||||||
}}
|
fontWeight: 500,
|
||||||
>
|
}}
|
||||||
zebragent Releases
|
>
|
||||||
<OpenInNewIcon sx={{ fontSize: 15 }} />
|
zebragent (Linux / Windows)
|
||||||
</Link>
|
<OpenInNewIcon sx={{ fontSize: 15 }} />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="https://github.com/QuixPos-com/AndroidPrinterAgent/releases/latest"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
underline="hover"
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
fontSize: '0.8125rem',
|
||||||
|
color: 'primary.main',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Android Agent
|
||||||
|
<OpenInNewIcon sx={{ fontSize: 15 }} />
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Add new agent row */}
|
{/* Add new agent row */}
|
||||||
@@ -572,7 +595,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
void this.handleCreateAgent();
|
void this.handleCreateAgent();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={creatingAgent}
|
disabled={!isAdmin || creatingAgent}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
slotProps={{
|
slotProps={{
|
||||||
htmlInput: {
|
htmlInput: {
|
||||||
@@ -594,7 +617,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
size="small"
|
size="small"
|
||||||
startIcon={<AddIcon />}
|
startIcon={<AddIcon />}
|
||||||
onClick={this.handleCreateAgent}
|
onClick={this.handleCreateAgent}
|
||||||
disabled={creatingAgent || !newAgentName.trim()}
|
disabled={!isAdmin || creatingAgent || !newAgentName.trim()}
|
||||||
>
|
>
|
||||||
Hinzufügen
|
Hinzufügen
|
||||||
</Button>
|
</Button>
|
||||||
@@ -716,7 +739,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => this.handleStartRename(agent)}
|
onClick={() => this.handleStartRename(agent)}
|
||||||
disabled={isPatching || isDeleting}
|
disabled={!isAdmin || isPatching || isDeleting}
|
||||||
sx={{ p: 0.5, color: 'text.secondary' }}
|
sx={{ p: 0.5, color: 'text.secondary' }}
|
||||||
>
|
>
|
||||||
<EditOutlinedIcon sx={{ fontSize: 16 }} />
|
<EditOutlinedIcon sx={{ fontSize: 16 }} />
|
||||||
@@ -764,7 +787,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={isEnabled}
|
checked={isEnabled}
|
||||||
disabled={isPatching || isDeleting}
|
disabled={!isAdmin || isPatching || isDeleting}
|
||||||
onChange={(e) => void this.handleToggleAgentEnabled(agent, e.target.checked)}
|
onChange={(e) => void this.handleToggleAgentEnabled(agent, e.target.checked)}
|
||||||
color="success"
|
color="success"
|
||||||
/>
|
/>
|
||||||
@@ -788,7 +811,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
size="small"
|
size="small"
|
||||||
aria-label={`Drucker-Agent ${agent.name} löschen`}
|
aria-label={`Drucker-Agent ${agent.name} löschen`}
|
||||||
onClick={() => void this.handleDeleteAgent(agent)}
|
onClick={() => void this.handleDeleteAgent(agent)}
|
||||||
disabled={isDeleting || creatingAgent || isPatching}
|
disabled={!isAdmin || isDeleting || creatingAgent || isPatching}
|
||||||
color="error"
|
color="error"
|
||||||
>
|
>
|
||||||
<DeleteOutlinedIcon fontSize="small" />
|
<DeleteOutlinedIcon fontSize="small" />
|
||||||
@@ -904,7 +927,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
disabled={index === 0 || isPatching || isDeleting}
|
disabled={!isAdmin || index === 0 || isPatching || isDeleting}
|
||||||
onClick={() => void this.handleMoveAgent(agent, 'up')}
|
onClick={() => void this.handleMoveAgent(agent, 'up')}
|
||||||
sx={{ p: 0.25 }}
|
sx={{ p: 0.25 }}
|
||||||
>
|
>
|
||||||
@@ -914,7 +937,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
disabled={index === printeragents.length - 1 || isPatching || isDeleting}
|
disabled={!isAdmin || index === printeragents.length - 1 || isPatching || isDeleting}
|
||||||
onClick={() => void this.handleMoveAgent(agent, 'down')}
|
onClick={() => void this.handleMoveAgent(agent, 'down')}
|
||||||
sx={{ p: 0.25 }}
|
sx={{ p: 0.25 }}
|
||||||
>
|
>
|
||||||
@@ -924,7 +947,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
size="small"
|
size="small"
|
||||||
disabled={isPatching || isDeleting}
|
disabled={!isAdmin || isPatching || isDeleting}
|
||||||
value={this.state.sortDrafts[agent.id] ?? agent.sort ?? 0}
|
value={this.state.sortDrafts[agent.id] ?? agent.sort ?? 0}
|
||||||
onChange={(e) => this.handleSortDraftChange(agent.id, e.target.value)}
|
onChange={(e) => this.handleSortDraftChange(agent.id, e.target.value)}
|
||||||
onBlur={() => void this.handleSortSubmit(agent)}
|
onBlur={() => void this.handleSortSubmit(agent)}
|
||||||
@@ -1078,6 +1101,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={pEnabled}
|
checked={pEnabled}
|
||||||
|
disabled={!isAdmin}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
void this.handleTogglePrinterEnabled(p, e.target.checked)
|
void this.handleTogglePrinterEnabled(p, e.target.checked)
|
||||||
}
|
}
|
||||||
@@ -1099,7 +1123,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
disabled={pIdx === 0}
|
disabled={!isAdmin || pIdx === 0}
|
||||||
onClick={() => void this.handleMovePrinter(p, 'up')}
|
onClick={() => void this.handleMovePrinter(p, 'up')}
|
||||||
sx={{ p: 0.25 }}
|
sx={{ p: 0.25 }}
|
||||||
>
|
>
|
||||||
@@ -1110,7 +1134,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
disabled={pIdx === agentprinters.length - 1}
|
disabled={!isAdmin || pIdx === agentprinters.length - 1}
|
||||||
onClick={() => void this.handleMovePrinter(p, 'down')}
|
onClick={() => void this.handleMovePrinter(p, 'down')}
|
||||||
sx={{ p: 0.25 }}
|
sx={{ p: 0.25 }}
|
||||||
>
|
>
|
||||||
@@ -1124,7 +1148,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="error"
|
color="error"
|
||||||
disabled={isOnline}
|
disabled={!isAdmin || isOnline}
|
||||||
onClick={() => void this.handleDeletePrinter(p)}
|
onClick={() => void this.handleDeletePrinter(p)}
|
||||||
sx={{ p: 0.5 }}
|
sx={{ p: 0.5 }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
|||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import BackendError from '../BackendError';
|
import BackendError from '../BackendError';
|
||||||
import { printer as printerApi } from '../../api';
|
import { printer as printerApi } from '../../api';
|
||||||
|
import companyAutomat from '../../automats/companyAutomat';
|
||||||
import { requireArray, requireObject } from '../../utils/apiShape';
|
import { requireArray, requireObject } from '../../utils/apiShape';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
@@ -189,6 +190,7 @@ export default class PrinterSettingsPanel extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
status: 'loading',
|
status: 'loading',
|
||||||
error: null,
|
error: null,
|
||||||
printers: [],
|
printers: [],
|
||||||
@@ -208,6 +210,9 @@ export default class PrinterSettingsPanel extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
|
}));
|
||||||
void this.load();
|
void this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,10 +331,12 @@ export default class PrinterSettingsPanel extends Component {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._unmounted = true;
|
this._unmounted = true;
|
||||||
|
this.unsub?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
isAdmin,
|
||||||
status, error, printers,
|
status, error, printers,
|
||||||
draggingId, dragOverId,
|
draggingId, dragOverId,
|
||||||
addOpen, editPrinter,
|
addOpen, editPrinter,
|
||||||
@@ -354,6 +361,7 @@ export default class PrinterSettingsPanel extends Component {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={<AddIcon />}
|
startIcon={<AddIcon />}
|
||||||
|
disabled={!isAdmin}
|
||||||
onClick={this.handleAdd}
|
onClick={this.handleAdd}
|
||||||
>
|
>
|
||||||
Drucker hinzufügen
|
Drucker hinzufügen
|
||||||
@@ -386,17 +394,17 @@ export default class PrinterSettingsPanel extends Component {
|
|||||||
<TableRow
|
<TableRow
|
||||||
key={printer.id}
|
key={printer.id}
|
||||||
hover
|
hover
|
||||||
draggable
|
draggable={isAdmin}
|
||||||
onDragStart={() => this.handleDragStart(printer.id)}
|
onDragStart={isAdmin ? () => this.handleDragStart(printer.id) : undefined}
|
||||||
onDragOver={(e) => this.handleDragOver(e, printer.id)}
|
onDragOver={isAdmin ? (e) => this.handleDragOver(e, printer.id) : undefined}
|
||||||
onDragLeave={() => this.handleDragLeave()}
|
onDragLeave={isAdmin ? () => this.handleDragLeave() : undefined}
|
||||||
onDrop={() => this.handleDrop(printer.id)}
|
onDrop={isAdmin ? () => this.handleDrop(printer.id) : undefined}
|
||||||
onDragEnd={() => this.handleDragEnd()}
|
onDragEnd={isAdmin ? () => this.handleDragEnd() : undefined}
|
||||||
sx={{
|
sx={{
|
||||||
opacity: isDragging ? 0.4 : 1,
|
opacity: isDragging ? 0.4 : 1,
|
||||||
outline: isDragOver ? '2px solid' : 'none',
|
outline: isDragOver ? '2px solid' : 'none',
|
||||||
outlineColor: 'primary.main',
|
outlineColor: 'primary.main',
|
||||||
cursor: 'grab',
|
cursor: isAdmin ? 'grab' : 'default',
|
||||||
transition: 'opacity 0.15s',
|
transition: 'opacity 0.15s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -426,7 +434,7 @@ export default class PrinterSettingsPanel extends Component {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
aria-label="Bearbeiten"
|
aria-label="Bearbeiten"
|
||||||
disabled={isDeleting}
|
disabled={!isAdmin || isDeleting}
|
||||||
onClick={() => this.setState({ editPrinter: printer })}
|
onClick={() => this.setState({ editPrinter: printer })}
|
||||||
>
|
>
|
||||||
<EditOutlinedIcon fontSize="small" />
|
<EditOutlinedIcon fontSize="small" />
|
||||||
@@ -435,7 +443,7 @@ export default class PrinterSettingsPanel extends Component {
|
|||||||
size="small"
|
size="small"
|
||||||
aria-label="Löschen"
|
aria-label="Löschen"
|
||||||
color="error"
|
color="error"
|
||||||
disabled={isDeleting}
|
disabled={!isAdmin || isDeleting}
|
||||||
onClick={() => void this.handleDelete(printer.id)}
|
onClick={() => void this.handleDelete(printer.id)}
|
||||||
>
|
>
|
||||||
{isDeleting ? (
|
{isDeleting ? (
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import SettingsKeysPanel from './SettingsKeysPanel';
|
|||||||
import ImportExportSettingsPanel from './ImportExportSettingsPanel';
|
import ImportExportSettingsPanel from './ImportExportSettingsPanel';
|
||||||
import PosTerminalSettingsPanel from './PosTerminalSettingsPanel';
|
import PosTerminalSettingsPanel from './PosTerminalSettingsPanel';
|
||||||
import McpAuthorizationsPanel from './McpAuthorizationsPanel';
|
import McpAuthorizationsPanel from './McpAuthorizationsPanel';
|
||||||
|
import MembersSettingsPanel from './MembersSettingsPanel';
|
||||||
|
import GroupOutlinedIcon from '@mui/icons-material/GroupOutlined';
|
||||||
|
import companyAutomat from '../../automats/companyAutomat';
|
||||||
import BackupSettingsPanel from './BackupSettingsPanel';
|
import BackupSettingsPanel from './BackupSettingsPanel';
|
||||||
import HealthSettingsPanel from './HealthSettingsPanel';
|
import HealthSettingsPanel from './HealthSettingsPanel';
|
||||||
import MonitorHeartOutlinedIcon from '@mui/icons-material/MonitorHeartOutlined';
|
import MonitorHeartOutlinedIcon from '@mui/icons-material/MonitorHeartOutlined';
|
||||||
@@ -42,6 +45,7 @@ const TAB_KEYS = {
|
|||||||
backup: 'backup',
|
backup: 'backup',
|
||||||
posterminal: 'posterminal',
|
posterminal: 'posterminal',
|
||||||
security: 'security',
|
security: 'security',
|
||||||
|
members: 'members',
|
||||||
};
|
};
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
@@ -54,6 +58,7 @@ const TABS = [
|
|||||||
{ id: TAB_KEYS.importExport, label: 'Import/Export', icon: <ImportExportIcon />, Panel: ImportExportSettingsPanel },
|
{ id: TAB_KEYS.importExport, label: 'Import/Export', icon: <ImportExportIcon />, Panel: ImportExportSettingsPanel },
|
||||||
{ id: TAB_KEYS.backup, label: 'Backups', icon: <BackupTableOutlinedIcon />, Panel: BackupSettingsPanel },
|
{ id: TAB_KEYS.backup, label: 'Backups', icon: <BackupTableOutlinedIcon />, Panel: BackupSettingsPanel },
|
||||||
{ id: TAB_KEYS.posterminal, label: 'POS Terminal', icon: <PointOfSaleIcon />, Panel: PosTerminalSettingsPanel },
|
{ id: TAB_KEYS.posterminal, label: 'POS Terminal', icon: <PointOfSaleIcon />, Panel: PosTerminalSettingsPanel },
|
||||||
|
{ id: TAB_KEYS.members, label: 'Mitglieder', icon: <GroupOutlinedIcon />, Panel: MembersSettingsPanel },
|
||||||
{ id: TAB_KEYS.security, label: 'Sicherheit & MCP', icon: <SecurityIcon />, Panel: McpAuthorizationsPanel },
|
{ id: TAB_KEYS.security, label: 'Sicherheit & MCP', icon: <SecurityIcon />, Panel: McpAuthorizationsPanel },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -61,9 +66,10 @@ function getDefaultTab(vhost) {
|
|||||||
return vhost ? TAB_KEYS.company : TAB_KEYS.health;
|
return vhost ? TAB_KEYS.company : TAB_KEYS.health;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVisibleTabs(vhost) {
|
function getVisibleTabs(vhost, isAdmin) {
|
||||||
if (!vhost) return TABS;
|
const tabs = isAdmin ? TABS : TABS.filter((t) => t.id !== TAB_KEYS.members);
|
||||||
return TABS.filter(
|
if (!vhost) return tabs;
|
||||||
|
return tabs.filter(
|
||||||
(t) => t.id !== TAB_KEYS.health && t.id !== TAB_KEYS.backup && t.id !== TAB_KEYS.printers
|
(t) => t.id !== TAB_KEYS.health && t.id !== TAB_KEYS.backup && t.id !== TAB_KEYS.printers
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -76,6 +82,7 @@ export default class SettingsDialog extends Component {
|
|||||||
tab: getDefaultTab(vhost),
|
tab: getDefaultTab(vhost),
|
||||||
schemaVersion: authAutomat.state.schemaVersion,
|
schemaVersion: authAutomat.state.schemaVersion,
|
||||||
vhost,
|
vhost,
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
};
|
};
|
||||||
this.handleTabChange = this.handleTabChange.bind(this);
|
this.handleTabChange = this.handleTabChange.bind(this);
|
||||||
}
|
}
|
||||||
@@ -85,10 +92,14 @@ export default class SettingsDialog extends Component {
|
|||||||
schemaVersion: state.schemaVersion,
|
schemaVersion: state.schemaVersion,
|
||||||
vhost: Boolean(state.vhost),
|
vhost: Boolean(state.vhost),
|
||||||
}));
|
}));
|
||||||
|
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unsubAuth?.();
|
this.unsubAuth?.();
|
||||||
|
this.unsubCompany?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@@ -129,8 +140,8 @@ export default class SettingsDialog extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { open, onClose } = this.props;
|
const { open, onClose } = this.props;
|
||||||
const { tab, schemaVersion, vhost } = this.state;
|
const { tab, schemaVersion, vhost, isAdmin } = this.state;
|
||||||
const visibleTabs = getVisibleTabs(vhost);
|
const visibleTabs = getVisibleTabs(vhost, isAdmin);
|
||||||
const activeTab = visibleTabs.some((t) => t.id === tab) ? tab : visibleTabs[0]?.id;
|
const activeTab = visibleTabs.some((t) => t.id === tab) ? tab : visibleTabs[0]?.id;
|
||||||
|
|
||||||
const paperSx = {
|
const paperSx = {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
|||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import BackendError from '../BackendError';
|
import BackendError from '../BackendError';
|
||||||
import { setting as settingApi } from '../../api';
|
import { setting as settingApi } from '../../api';
|
||||||
|
import companyAutomat from '../../automats/companyAutomat';
|
||||||
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 } from '../../utils/notify';
|
||||||
@@ -68,6 +69,7 @@ export default class SettingsKeysPanel extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
status: 'loading',
|
status: 'loading',
|
||||||
error: null,
|
error: null,
|
||||||
settings: [],
|
settings: [],
|
||||||
@@ -79,6 +81,9 @@ export default class SettingsKeysPanel extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
|
}));
|
||||||
void this.load();
|
void this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +129,7 @@ export default class SettingsKeysPanel extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSave(key) {
|
async handleSave(key) {
|
||||||
|
if (!this.state.isAdmin) return;
|
||||||
const draft = this.state.drafts[key];
|
const draft = this.state.drafts[key];
|
||||||
if (!draft || draft.busy) return;
|
if (!draft || draft.busy) return;
|
||||||
const row = this.state.settings.find((s) => s.key === key);
|
const row = this.state.settings.find((s) => s.key === key);
|
||||||
@@ -172,10 +178,11 @@ export default class SettingsKeysPanel extends Component {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._unmounted = true;
|
this._unmounted = true;
|
||||||
|
this.unsub?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { status, error, settings, drafts } = this.state;
|
const { isAdmin, status, error, settings, drafts } = this.state;
|
||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
@@ -224,7 +231,7 @@ export default class SettingsKeysPanel extends Component {
|
|||||||
size="small"
|
size="small"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={draft.value !== undefined && draft.value !== null ? draft.value : (DEFAULT_SETTINGS[row.key] ?? '')}
|
value={draft.value !== undefined && draft.value !== null ? draft.value : (DEFAULT_SETTINGS[row.key] ?? '')}
|
||||||
disabled={draft.busy}
|
disabled={!isAdmin || draft.busy}
|
||||||
onChange={(e) => this.handleChange(row.key, 'value', e.target.value)}
|
onChange={(e) => this.handleChange(row.key, 'value', e.target.value)}
|
||||||
slotProps={{ htmlInput: { spellCheck: false } }}
|
slotProps={{ htmlInput: { spellCheck: false } }}
|
||||||
>
|
>
|
||||||
@@ -247,7 +254,7 @@ export default class SettingsKeysPanel extends Component {
|
|||||||
maxRows={isMultiline ? 6 : undefined}
|
maxRows={isMultiline ? 6 : undefined}
|
||||||
placeholder={DEFAULT_SETTINGS[row.key] ?? undefined}
|
placeholder={DEFAULT_SETTINGS[row.key] ?? undefined}
|
||||||
value={draft.value}
|
value={draft.value}
|
||||||
disabled={draft.busy}
|
disabled={!isAdmin || draft.busy}
|
||||||
onChange={(e) => this.handleChange(row.key, 'value', e.target.value)}
|
onChange={(e) => this.handleChange(row.key, 'value', e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (!isMultiline && e.key === 'Enter') {
|
if (!isMultiline && e.key === 'Enter') {
|
||||||
@@ -279,7 +286,7 @@ export default class SettingsKeysPanel extends Component {
|
|||||||
size="small"
|
size="small"
|
||||||
aria-label="Speichern"
|
aria-label="Speichern"
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={draft.busy}
|
disabled={!isAdmin || draft.busy}
|
||||||
onClick={() => void this.handleSave(row.key)}
|
onClick={() => void this.handleSave(row.key)}
|
||||||
>
|
>
|
||||||
<SaveOutlinedIcon fontSize="small" />
|
<SaveOutlinedIcon fontSize="small" />
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export { default as ImportExportSettingsPanel } from './ImportExportSettingsPane
|
|||||||
export { default as McpAuthorizationsPanel } from './McpAuthorizationsPanel';
|
export { default as McpAuthorizationsPanel } from './McpAuthorizationsPanel';
|
||||||
export { default as BackupSettingsPanel } from './BackupSettingsPanel';
|
export { default as BackupSettingsPanel } from './BackupSettingsPanel';
|
||||||
export { default as HealthSettingsPanel } from './HealthSettingsPanel';
|
export { default as HealthSettingsPanel } from './HealthSettingsPanel';
|
||||||
|
export { default as MembersSettingsPanel } from './MembersSettingsPanel';
|
||||||
|
|||||||
@@ -5,19 +5,21 @@ import IconButton from '@mui/material/IconButton';
|
|||||||
import FolderOpenRoundedIcon from '@mui/icons-material/FolderOpenRounded';
|
import FolderOpenRoundedIcon from '@mui/icons-material/FolderOpenRounded';
|
||||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||||
|
|
||||||
|
import companyAutomat from '../../automats/companyAutomat';
|
||||||
import vfsAutomat from '../../automats/vfsAutomat';
|
import vfsAutomat from '../../automats/vfsAutomat';
|
||||||
import VfsFileExplorer from './VfsFileExplorer';
|
import VfsFileExplorer from './VfsFileExplorer';
|
||||||
import VfsTextEditor from './VfsTextEditor';
|
import VfsTextEditor from './VfsTextEditor';
|
||||||
import VfsFilePreview from './VfsFilePreview';
|
import VfsFilePreview from './VfsFilePreview';
|
||||||
import VfsAppletRunner from './VfsAppletRunner';
|
import VfsAppletRunner from './VfsAppletRunner';
|
||||||
import VfsNewFileDialog from './VfsNewFileDialog';
|
import VfsNewFileDialog from './VfsNewFileDialog';
|
||||||
import { isAppletFile } from './vfsUtils';
|
import { isAppletFile, isProtectedVfsFile } from './vfsUtils';
|
||||||
|
|
||||||
export default class VfsEditorPanel extends PureComponent {
|
export default class VfsEditorPanel extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const s = vfsAutomat.state;
|
const s = vfsAutomat.state;
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
files: s.files,
|
files: s.files,
|
||||||
loadingFiles: s.loadingFiles,
|
loadingFiles: s.loadingFiles,
|
||||||
loadError: s.loadError,
|
loadError: s.loadError,
|
||||||
@@ -39,6 +41,9 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
|
}));
|
||||||
this.unsub = vfsAutomat.subscribe(this, (state) => ({
|
this.unsub = vfsAutomat.subscribe(this, (state) => ({
|
||||||
files: state.files,
|
files: state.files,
|
||||||
loadingFiles: state.loadingFiles,
|
loadingFiles: state.loadingFiles,
|
||||||
@@ -72,6 +77,7 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
this.unsubCompany?.();
|
||||||
this.unsub?.();
|
this.unsub?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +114,7 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
isAdmin,
|
||||||
files,
|
files,
|
||||||
loadingFiles,
|
loadingFiles,
|
||||||
selectedFileName,
|
selectedFileName,
|
||||||
@@ -124,6 +131,7 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const isApplet = Boolean(selectedFileName && isAppletFile(selectedFileName));
|
const isApplet = Boolean(selectedFileName && isAppletFile(selectedFileName));
|
||||||
|
const isProtectedFile = Boolean(selectedFileName && isProtectedVfsFile(selectedFileName));
|
||||||
const showAppletRunner = isApplet && selectedFile?.isText && appletView === 'run';
|
const showAppletRunner = isApplet && selectedFile?.isText && appletView === 'run';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -161,6 +169,8 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
saveError={saveError}
|
saveError={saveError}
|
||||||
loadingContent={loadingContent}
|
loadingContent={loadingContent}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
readOnly={!isAdmin && isProtectedFile}
|
||||||
onChange={(c) => vfsAutomat.actions.updateContent(c)}
|
onChange={(c) => vfsAutomat.actions.updateContent(c)}
|
||||||
onSave={() => vfsAutomat.actions.saveCurrentFile()}
|
onSave={() => vfsAutomat.actions.saveCurrentFile()}
|
||||||
onClose={() => vfsAutomat.actions.selectFile(null)}
|
onClose={() => vfsAutomat.actions.selectFile(null)}
|
||||||
@@ -216,6 +226,7 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
<VfsFileExplorer
|
<VfsFileExplorer
|
||||||
files={files}
|
files={files}
|
||||||
loading={loadingFiles}
|
loading={loadingFiles}
|
||||||
|
isAdmin={isAdmin}
|
||||||
selectedFileName={selectedFileName}
|
selectedFileName={selectedFileName}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
onSelectFile={(name) => vfsAutomat.actions.selectFile(name)}
|
onSelectFile={(name) => vfsAutomat.actions.selectFile(name)}
|
||||||
@@ -238,6 +249,7 @@ export default class VfsEditorPanel extends PureComponent {
|
|||||||
<VfsNewFileDialog
|
<VfsNewFileDialog
|
||||||
open={newFileDialogOpen}
|
open={newFileDialogOpen}
|
||||||
defaultFolder={this.state.newFileDefaultFolder}
|
defaultFolder={this.state.newFileDefaultFolder}
|
||||||
|
isAdmin={isAdmin}
|
||||||
onClose={() => vfsAutomat.actions.setNewFileDialogOpen(false)}
|
onClose={() => vfsAutomat.actions.setNewFileDialogOpen(false)}
|
||||||
onCreate={(name) => vfsAutomat.actions.createFile(name)}
|
onCreate={(name) => vfsAutomat.actions.createFile(name)}
|
||||||
isCreating={isSaving}
|
isCreating={isSaving}
|
||||||
|
|||||||
@@ -27,11 +27,25 @@ import CreateNewFolderRoundedIcon from '@mui/icons-material/CreateNewFolderRound
|
|||||||
import DriveFileRenameOutlineRoundedIcon from '@mui/icons-material/DriveFileRenameOutlineRounded';
|
import DriveFileRenameOutlineRoundedIcon from '@mui/icons-material/DriveFileRenameOutlineRounded';
|
||||||
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
|
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
|
||||||
|
|
||||||
|
import companyAutomat from '../../automats/companyAutomat';
|
||||||
import VfsFileIcon from './VfsFileIcon';
|
import VfsFileIcon from './VfsFileIcon';
|
||||||
import { formatVfsSize, formatVfsDate, isAppletFile } from './vfsUtils';
|
import { formatVfsSize, formatVfsDate, isAppletFile, isProtectedVfsFile } from './vfsUtils';
|
||||||
import VfsNewFolderDialog from './VfsNewFolderDialog';
|
import VfsNewFolderDialog from './VfsNewFolderDialog';
|
||||||
import VfsRenameDialog from './VfsRenameDialog';
|
import VfsRenameDialog from './VfsRenameDialog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given folder path contains any admin-protected VFS files
|
||||||
|
* (`.skill` or `.applet.js`, case-insensitive).
|
||||||
|
*/
|
||||||
|
function folderContainsProtectedFiles(folderPath, files) {
|
||||||
|
if (!folderPath || !Array.isArray(files)) return false;
|
||||||
|
const prefix = `${String(folderPath).replace(/^\/+/, '').replace(/\/+$/, '')}/`;
|
||||||
|
return files.some((f) => {
|
||||||
|
const clean = String(f?.name || '').replace(/^\/+/, '');
|
||||||
|
return clean.startsWith(prefix) && isProtectedVfsFile(clean);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all ancestor directory paths for a file or folder path.
|
* Returns all ancestor directory paths for a file or folder path.
|
||||||
* e.g. "docs/api/v1/spec.json" -> ["docs", "docs/api", "docs/api/v1"]
|
* e.g. "docs/api/v1/spec.json" -> ["docs", "docs/api", "docs/api/v1"]
|
||||||
@@ -126,6 +140,7 @@ export default class VfsFileExplorer extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
menuAnchor: null,
|
menuAnchor: null,
|
||||||
menuFile: null,
|
menuFile: null,
|
||||||
folderMenuAnchor: null,
|
folderMenuAnchor: null,
|
||||||
@@ -143,6 +158,16 @@ export default class VfsFileExplorer extends PureComponent {
|
|||||||
this.fileInputRef = React.createRef();
|
this.fileInputRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsubCompany?.();
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.selectedFileName && this.props.selectedFileName !== prevProps.selectedFileName) {
|
if (this.props.selectedFileName && this.props.selectedFileName !== prevProps.selectedFileName) {
|
||||||
const parents = getParentFolderPaths(this.props.selectedFileName);
|
const parents = getParentFolderPaths(this.props.selectedFileName);
|
||||||
@@ -632,7 +657,12 @@ export default class VfsFileExplorer extends PureComponent {
|
|||||||
isRenaming,
|
isRenaming,
|
||||||
isDraggingOver,
|
isDraggingOver,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||||
const isText = menuFile?.isText;
|
const isText = menuFile?.isText;
|
||||||
|
const isMenuFileProtected = Boolean(menuFile && isProtectedVfsFile(menuFile.name));
|
||||||
|
const isFolderProtected = Boolean(
|
||||||
|
folderMenuNode && folderContainsProtectedFiles(folderMenuNode.path, this.props.files)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -832,6 +862,7 @@ export default class VfsFileExplorer extends PureComponent {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
disabled={!isAdmin && isMenuFileProtected}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const file = menuFile;
|
const file = menuFile;
|
||||||
this.handleCloseMenu();
|
this.handleCloseMenu();
|
||||||
@@ -860,6 +891,7 @@ export default class VfsFileExplorer extends PureComponent {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
disabled={!isAdmin && isMenuFileProtected}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (menuFile && window.confirm(`Möchtest du "${menuFile.name}" wirklich aus dem VFS löschen?`)) {
|
if (menuFile && window.confirm(`Möchtest du "${menuFile.name}" wirklich aus dem VFS löschen?`)) {
|
||||||
this.props.onDelete(menuFile.name);
|
this.props.onDelete(menuFile.name);
|
||||||
@@ -915,6 +947,7 @@ export default class VfsFileExplorer extends PureComponent {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
disabled={!isAdmin && isFolderProtected}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const folder = folderMenuNode;
|
const folder = folderMenuNode;
|
||||||
this.handleCloseFolderMenu();
|
this.handleCloseFolderMenu();
|
||||||
@@ -934,6 +967,7 @@ export default class VfsFileExplorer extends PureComponent {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
disabled={!isAdmin && isFolderProtected}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const folder = folderMenuNode;
|
const folder = folderMenuNode;
|
||||||
this.handleCloseFolderMenu();
|
this.handleCloseFolderMenu();
|
||||||
@@ -963,6 +997,7 @@ export default class VfsFileExplorer extends PureComponent {
|
|||||||
<VfsRenameDialog
|
<VfsRenameDialog
|
||||||
open={renameDialogOpen}
|
open={renameDialogOpen}
|
||||||
item={renameItem}
|
item={renameItem}
|
||||||
|
isAdmin={isAdmin}
|
||||||
onClose={() => this.setState({ renameDialogOpen: false, renameItem: null })}
|
onClose={() => this.setState({ renameDialogOpen: false, renameItem: null })}
|
||||||
onRename={this.handleRenameItem}
|
onRename={this.handleRenameItem}
|
||||||
isRenaming={isRenaming}
|
isRenaming={isRenaming}
|
||||||
|
|||||||
@@ -7,17 +7,30 @@ import Button from '@mui/material/Button';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import NoteAddRoundedIcon from '@mui/icons-material/NoteAddRounded';
|
import NoteAddRoundedIcon from '@mui/icons-material/NoteAddRounded';
|
||||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
|
import companyAutomat from '../../automats/companyAutomat';
|
||||||
|
import { isProtectedVfsFile } from './vfsUtils';
|
||||||
|
|
||||||
export default class VfsNewFileDialog extends PureComponent {
|
export default class VfsNewFileDialog extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const folder = props.defaultFolder ? `${String(props.defaultFolder).replace(/^\/+/, '').replace(/\/+$/, '')}/` : '';
|
const folder = props.defaultFolder ? `${String(props.defaultFolder).replace(/^\/+/, '').replace(/\/+$/, '')}/` : '';
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
filename: folder,
|
filename: folder,
|
||||||
error: '',
|
error: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsub?.();
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (!prevProps.open && this.props.open) {
|
if (!prevProps.open && this.props.open) {
|
||||||
const folder = this.props.defaultFolder ? `${String(this.props.defaultFolder).replace(/^\/+/, '').replace(/\/+$/, '')}/` : '';
|
const folder = this.props.defaultFolder ? `${String(this.props.defaultFolder).replace(/^\/+/, '').replace(/\/+$/, '')}/` : '';
|
||||||
@@ -27,6 +40,7 @@ export default class VfsNewFileDialog extends PureComponent {
|
|||||||
|
|
||||||
handleSubmit = (e) => {
|
handleSubmit = (e) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
|
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||||
const clean = this.state.filename.trim().replace(/^[/\\]+/, '');
|
const clean = this.state.filename.trim().replace(/^[/\\]+/, '');
|
||||||
if (!clean) {
|
if (!clean) {
|
||||||
this.setState({ error: 'Bitte gib einen Dateinamen ein.' });
|
this.setState({ error: 'Bitte gib einen Dateinamen ein.' });
|
||||||
@@ -36,12 +50,21 @@ export default class VfsNewFileDialog extends PureComponent {
|
|||||||
this.setState({ error: 'Ungültige Zeichen im Dateinamen.' });
|
this.setState({ error: 'Ungültige Zeichen im Dateinamen.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isAdmin && isProtectedVfsFile(clean)) {
|
||||||
|
this.setState({ error: 'Nur Administratoren dürfen .SKILL- und .applet.js-Dateien erstellen.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.props.onCreate(clean);
|
this.props.onCreate(clean);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { open, onClose, isCreating } = this.props;
|
const { open, onClose, isCreating } = this.props;
|
||||||
const { filename, error } = this.state;
|
const { filename, error } = this.state;
|
||||||
|
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||||
|
const clean = filename.trim().replace(/^[/\\]+/, '');
|
||||||
|
const isProtected = Boolean(!isAdmin && clean && isProtectedVfsFile(clean));
|
||||||
|
const displayError =
|
||||||
|
error || (isProtected ? 'Nur Administratoren dürfen .SKILL- und .applet.js-Dateien erstellen.' : '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -70,8 +93,8 @@ export default class VfsNewFileDialog extends PureComponent {
|
|||||||
label=""
|
label=""
|
||||||
value={filename}
|
value={filename}
|
||||||
onChange={(nextVal) => this.setState({ filename: nextVal, error: '' })}
|
onChange={(nextVal) => this.setState({ filename: nextVal, error: '' })}
|
||||||
error={Boolean(error)}
|
error={Boolean(displayError)}
|
||||||
helperText={error}
|
helperText={displayError}
|
||||||
disabled={isCreating}
|
disabled={isCreating}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -83,7 +106,7 @@ export default class VfsNewFileDialog extends PureComponent {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={isCreating || !filename.trim()}
|
disabled={isCreating || !clean || isProtected}
|
||||||
sx={{ borderRadius: 2 }}
|
sx={{ borderRadius: 2 }}
|
||||||
>
|
>
|
||||||
{isCreating ? 'Erstelle…' : 'Erstellen & Öffnen'}
|
{isCreating ? 'Erstelle…' : 'Erstellen & Öffnen'}
|
||||||
|
|||||||
@@ -7,16 +7,29 @@ import Button from '@mui/material/Button';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import DriveFileRenameOutlineRoundedIcon from '@mui/icons-material/DriveFileRenameOutlineRounded';
|
import DriveFileRenameOutlineRoundedIcon from '@mui/icons-material/DriveFileRenameOutlineRounded';
|
||||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
|
import companyAutomat from '../../automats/companyAutomat';
|
||||||
|
import { isProtectedVfsFile } from './vfsUtils';
|
||||||
|
|
||||||
export default class VfsRenameDialog extends PureComponent {
|
export default class VfsRenameDialog extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
targetPath: props.item?.path || '',
|
targetPath: props.item?.path || '',
|
||||||
error: '',
|
error: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsub?.();
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (!prevProps.open && this.props.open) {
|
if (!prevProps.open && this.props.open) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -28,6 +41,7 @@ export default class VfsRenameDialog extends PureComponent {
|
|||||||
|
|
||||||
handleSubmit = (e) => {
|
handleSubmit = (e) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
|
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||||
const raw = this.state.targetPath.trim();
|
const raw = this.state.targetPath.trim();
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
this.setState({ error: 'Bitte gib einen neuen Namen bzw. Pfad ein.' });
|
this.setState({ error: 'Bitte gib einen neuen Namen bzw. Pfad ein.' });
|
||||||
@@ -41,6 +55,11 @@ export default class VfsRenameDialog extends PureComponent {
|
|||||||
const cleanNew = raw.replace(/^[/\\]+/, '').replace(/[/\\]+$/, '');
|
const cleanNew = raw.replace(/^[/\\]+/, '').replace(/[/\\]+$/, '');
|
||||||
const cleanOld = String(this.props.item?.path || '').replace(/^[/\\]+/, '').replace(/[/\\]+$/, '');
|
const cleanOld = String(this.props.item?.path || '').replace(/^[/\\]+/, '').replace(/[/\\]+$/, '');
|
||||||
|
|
||||||
|
if (!isAdmin && (isProtectedVfsFile(cleanOld) || isProtectedVfsFile(cleanNew))) {
|
||||||
|
this.setState({ error: 'Nur Administratoren dürfen .SKILL- und .applet.js-Dateien umbenennen.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (cleanNew === cleanOld) {
|
if (cleanNew === cleanOld) {
|
||||||
this.props.onClose?.();
|
this.props.onClose?.();
|
||||||
return;
|
return;
|
||||||
@@ -53,6 +72,11 @@ export default class VfsRenameDialog extends PureComponent {
|
|||||||
const { open, onClose, isRenaming, item } = this.props;
|
const { open, onClose, isRenaming, item } = this.props;
|
||||||
const { targetPath, error } = this.state;
|
const { targetPath, error } = this.state;
|
||||||
const isDir = Boolean(item?.isDirectory);
|
const isDir = Boolean(item?.isDirectory);
|
||||||
|
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||||
|
const cleanNew = targetPath.trim().replace(/^[/\\]+/, '').replace(/[/\\]+$/, '');
|
||||||
|
const isProtected = Boolean(!isAdmin && cleanNew && isProtectedVfsFile(cleanNew));
|
||||||
|
const displayError =
|
||||||
|
error || (isProtected ? 'Nur Administratoren dürfen .SKILL- und .applet.js-Dateien umbenennen.' : '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -81,8 +105,8 @@ export default class VfsRenameDialog extends PureComponent {
|
|||||||
label=""
|
label=""
|
||||||
value={targetPath}
|
value={targetPath}
|
||||||
onChange={(nextVal) => this.setState({ targetPath: nextVal, error: '' })}
|
onChange={(nextVal) => this.setState({ targetPath: nextVal, error: '' })}
|
||||||
error={Boolean(error)}
|
error={Boolean(displayError)}
|
||||||
helperText={error}
|
helperText={displayError}
|
||||||
disabled={isRenaming}
|
disabled={isRenaming}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -94,7 +118,7 @@ export default class VfsRenameDialog extends PureComponent {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={isRenaming || !targetPath.trim()}
|
disabled={isRenaming || !cleanNew || isProtected}
|
||||||
sx={{ borderRadius: 2 }}
|
sx={{ borderRadius: 2 }}
|
||||||
>
|
>
|
||||||
{isRenaming ? 'Speichere…' : 'Umbenennen'}
|
{isRenaming ? 'Speichere…' : 'Umbenennen'}
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineR
|
|||||||
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
|
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
|
||||||
import WrapTextRoundedIcon from '@mui/icons-material/WrapTextRounded';
|
import WrapTextRoundedIcon from '@mui/icons-material/WrapTextRounded';
|
||||||
|
|
||||||
|
import companyAutomat from '../../automats/companyAutomat';
|
||||||
import VfsFileIcon from './VfsFileIcon';
|
import VfsFileIcon from './VfsFileIcon';
|
||||||
import { formatVfsSize } from './vfsUtils';
|
import { formatVfsSize, isProtectedVfsFile } from './vfsUtils';
|
||||||
|
|
||||||
const CodeMirrorCore = React.lazy(() => import('./CodeMirrorCore'));
|
const CodeMirrorCore = React.lazy(() => import('./CodeMirrorCore'));
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
super(props);
|
super(props);
|
||||||
this.editorView = null;
|
this.editorView = null;
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
wrapLines: true,
|
wrapLines: true,
|
||||||
cursorLine: 1,
|
cursorLine: 1,
|
||||||
cursorCol: 1,
|
cursorCol: 1,
|
||||||
@@ -79,9 +81,16 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
|
}));
|
||||||
this.focusEditor();
|
this.focusEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsub?.();
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.fileName !== this.props.fileName) {
|
if (prevProps.fileName !== this.props.fileName) {
|
||||||
this.focusEditor();
|
this.focusEditor();
|
||||||
@@ -145,6 +154,9 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleSaveShortcut = () => {
|
handleSaveShortcut = () => {
|
||||||
|
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||||
|
const isProtectedFile = isProtectedVfsFile(this.props.fileName);
|
||||||
|
if (this.props.readOnly || (!isAdmin && isProtectedFile)) return;
|
||||||
if (this.props.isDirty && !this.props.isSaving) {
|
if (this.props.isDirty && !this.props.isSaving) {
|
||||||
this.props.onSave?.();
|
this.props.onSave?.();
|
||||||
}
|
}
|
||||||
@@ -168,6 +180,9 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { wrapLines, cursorLine, cursorCol } = this.state;
|
const { wrapLines, cursorLine, cursorCol } = this.state;
|
||||||
|
const isAdmin = this.props.isAdmin ?? this.state.isAdmin;
|
||||||
|
const isProtectedFile = isProtectedVfsFile(fileName);
|
||||||
|
const isReadOnly = Boolean(this.props.readOnly || (!isAdmin && isProtectedFile));
|
||||||
const text = content || '';
|
const text = content || '';
|
||||||
const lineCount = text.split('\n').length;
|
const lineCount = text.split('\n').length;
|
||||||
const charCount = text.length;
|
const charCount = text.length;
|
||||||
@@ -292,17 +307,17 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color={isDirty ? 'success' : 'inherit'}
|
color={!isReadOnly && isDirty ? 'success' : 'inherit'}
|
||||||
startIcon={isSaving ? <CircularProgress size={14} color="inherit" /> : <SaveRoundedIcon />}
|
startIcon={isSaving ? <CircularProgress size={14} color="inherit" /> : <SaveRoundedIcon />}
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
disabled={!isDirty || isSaving}
|
disabled={isReadOnly || !isDirty || isSaving}
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
px: 1.5,
|
px: 1.5,
|
||||||
py: 0.4,
|
py: 0.4,
|
||||||
fontSize: '0.78rem',
|
fontSize: '0.78rem',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
...(isDirty && {
|
...(!isReadOnly && isDirty && {
|
||||||
bgcolor: 'success.main',
|
bgcolor: 'success.main',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
'&:hover': { bgcolor: 'success.dark' },
|
'&:hover': { bgcolor: 'success.dark' },
|
||||||
@@ -396,7 +411,7 @@ export default class VfsTextEditor extends PureComponent {
|
|||||||
fileName={fileName}
|
fileName={fileName}
|
||||||
mime={file?.mime}
|
mime={file?.mime}
|
||||||
wrapLines={wrapLines}
|
wrapLines={wrapLines}
|
||||||
readOnly={this.props.readOnly}
|
readOnly={isReadOnly}
|
||||||
onSaveShortcut={this.handleSaveShortcut}
|
onSaveShortcut={this.handleSaveShortcut}
|
||||||
onCreateEditor={this.handleEditorCreate}
|
onCreateEditor={this.handleEditorCreate}
|
||||||
onUpdate={this.handleViewUpdate}
|
onUpdate={this.handleViewUpdate}
|
||||||
|
|||||||
@@ -87,10 +87,30 @@ export function getVfsCategory(filePath, mime = '') {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isAppletFile(filePath) {
|
export function isAppletFile(filePath) {
|
||||||
const p = String(filePath || '').toLowerCase();
|
const p = String(filePath || '').trim().toLowerCase();
|
||||||
return p.endsWith('.applet.js');
|
return p.endsWith('.applet.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a VFS path is a skill file (`*.skill`, case-insensitive).
|
||||||
|
* @param {string} filePath
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isSkillFile(filePath) {
|
||||||
|
const p = String(filePath || '').trim().toLowerCase();
|
||||||
|
return p.endsWith('.skill');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a VFS path is an admin-protected file (`*.skill` or `*.applet.js`, case-insensitive).
|
||||||
|
* @param {string} filePath
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isProtectedVfsFile(filePath) {
|
||||||
|
const p = String(filePath || '').trim().toLowerCase();
|
||||||
|
return p.endsWith('.skill') || p.endsWith('.applet.js');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split a VFS file path into directory and basename.
|
* Split a VFS file path into directory and basename.
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class ArtikelLager extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleBlur = () => {
|
handleBlur = () => {
|
||||||
|
if (this.props.disabled) return;
|
||||||
const item = this.props.warehouse ?? this.props.item;
|
const item = this.props.warehouse ?? this.props.item;
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
const draft = this.state.draft.trim();
|
const draft = this.state.draft.trim();
|
||||||
@@ -60,6 +61,7 @@ class ArtikelLager extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown = (event) => {
|
handleKeyDown = (event) => {
|
||||||
|
if (this.props.disabled) return;
|
||||||
const item = this.props.warehouse ?? this.props.item;
|
const item = this.props.warehouse ?? this.props.item;
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -73,6 +75,7 @@ class ArtikelLager extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleDelete = () => {
|
handleDelete = () => {
|
||||||
|
if (this.props.disabled) return;
|
||||||
const item = this.props.warehouse ?? this.props.item;
|
const item = this.props.warehouse ?? this.props.item;
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
if (!window.confirm(`Lager „${item.name}" wirklich löschen?`)) return;
|
if (!window.confirm(`Lager „${item.name}" wirklich löschen?`)) return;
|
||||||
|
|||||||
38
src/nav.js
@@ -101,13 +101,13 @@ export const NAV_ITEMS = [
|
|||||||
},
|
},
|
||||||
...(import.meta.env.DEV
|
...(import.meta.env.DEV
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
path: '/debug',
|
path: '/debug',
|
||||||
label: 'Debug',
|
label: 'Debug',
|
||||||
icon: BugReportIcon,
|
icon: BugReportIcon,
|
||||||
help: 'Entwickler- und Diagnosewerkzeuge für Automats (nur im Dev-Modus)',
|
help: 'Entwickler- und Diagnosewerkzeuge für Automats (nur im Dev-Modus)',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -282,8 +282,8 @@ export const SUB_NAV = {
|
|||||||
'/infos': [
|
'/infos': [
|
||||||
{
|
{
|
||||||
segment: '',
|
segment: '',
|
||||||
label: 'ArticleHistory',
|
label: 'Artikel-Historie',
|
||||||
title: 'ArticleHistory',
|
title: 'Artikel-Historie',
|
||||||
icon: HistoryIcon,
|
icon: HistoryIcon,
|
||||||
help: 'Historie aller Artikeländerungen und Ereignisse',
|
help: 'Historie aller Artikeländerungen und Ereignisse',
|
||||||
},
|
},
|
||||||
@@ -311,16 +311,16 @@ export const SUB_NAV = {
|
|||||||
],
|
],
|
||||||
...(import.meta.env.DEV
|
...(import.meta.env.DEV
|
||||||
? {
|
? {
|
||||||
'/debug': [
|
'/debug': [
|
||||||
{
|
{
|
||||||
segment: '',
|
segment: '',
|
||||||
label: 'Company',
|
label: 'Company',
|
||||||
title: 'Company Automat',
|
title: 'Company Automat',
|
||||||
icon: BusinessIcon,
|
icon: BusinessIcon,
|
||||||
help: 'companyAutomat Eingänge, Ausgänge (Actions & Selectors) und internen State inspizieren & steuern',
|
help: 'companyAutomat Eingänge, Ausgänge (Actions & Selectors) und internen State inspizieren & steuern',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class ArtikelLagerSearchField extends PureComponent {
|
|||||||
super(props);
|
super(props);
|
||||||
const initial = companyAutomat.state.warehouseFilter || props.prefill || '';
|
const initial = companyAutomat.state.warehouseFilter || props.prefill || '';
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
input: initial,
|
input: initial,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
};
|
};
|
||||||
@@ -50,6 +51,7 @@ class ArtikelLagerSearchField extends PureComponent {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
input: state.warehouseFilter,
|
input: state.warehouseFilter,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -64,6 +66,7 @@ class ArtikelLagerSearchField extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSubmit() {
|
async handleSubmit() {
|
||||||
|
if (!this.state.isAdmin) return;
|
||||||
const name = this.state.input.trim();
|
const name = this.state.input.trim();
|
||||||
if (!name || this.state.submitting) return;
|
if (!name || this.state.submitting) return;
|
||||||
|
|
||||||
@@ -84,8 +87,8 @@ class ArtikelLagerSearchField extends PureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { inputRef, returnTo, navigate } = this.props;
|
const { inputRef, returnTo, navigate } = this.props;
|
||||||
const { input, submitting } = this.state;
|
const { isAdmin, input, submitting } = this.state;
|
||||||
const canSubmit = Boolean(input.trim()) && !submitting;
|
const canCreate = isAdmin && Boolean(input.trim()) && !submitting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelTileGrid>
|
<PanelTileGrid>
|
||||||
@@ -141,7 +144,7 @@ class ArtikelLagerSearchField extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={!canSubmit}
|
disabled={!canCreate}
|
||||||
onClick={() => void this.handleSubmit()}
|
onClick={() => void this.handleSubmit()}
|
||||||
>
|
>
|
||||||
Hinzufügen
|
Hinzufügen
|
||||||
@@ -159,6 +162,7 @@ class ArtikelLagerPage extends PureComponent {
|
|||||||
const initialWarehouses =
|
const initialWarehouses =
|
||||||
companyAutomat.state.filteredWarehouses ?? companyAutomat.state.warehouses ?? [];
|
companyAutomat.state.filteredWarehouses ?? companyAutomat.state.warehouses ?? [];
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
status: companyAutomat.state.warehousesStatus === 'ready' ? 'ready' : 'loading',
|
status: companyAutomat.state.warehousesStatus === 'ready' ? 'ready' : 'loading',
|
||||||
error: companyAutomat.state.warehousesError ? backendErrorFrom(companyAutomat.state.warehousesError) : null,
|
error: companyAutomat.state.warehousesError ? backendErrorFrom(companyAutomat.state.warehousesError) : null,
|
||||||
warehouses: initialWarehouses,
|
warehouses: initialWarehouses,
|
||||||
@@ -178,6 +182,7 @@ class ArtikelLagerPage extends PureComponent {
|
|||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
|
|
||||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
warehouses: state.filteredWarehouses ?? state.warehouses,
|
warehouses: state.filteredWarehouses ?? state.warehouses,
|
||||||
filter: state.warehouseFilter ?? '',
|
filter: state.warehouseFilter ?? '',
|
||||||
status: state.warehousesStatus,
|
status: state.warehousesStatus,
|
||||||
@@ -191,6 +196,7 @@ class ArtikelLagerPage extends PureComponent {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (
|
if (
|
||||||
|
prevState.isAdmin !== this.state.isAdmin ||
|
||||||
prevState.status !== this.state.status ||
|
prevState.status !== this.state.status ||
|
||||||
prevState.warehouses !== this.state.warehouses ||
|
prevState.warehouses !== this.state.warehouses ||
|
||||||
prevState.filter !== this.state.filter
|
prevState.filter !== this.state.filter
|
||||||
@@ -219,7 +225,7 @@ class ArtikelLagerPage extends PureComponent {
|
|||||||
animation: 150,
|
animation: 150,
|
||||||
ghostClass: 'sortable-ghost',
|
ghostClass: 'sortable-ghost',
|
||||||
chosenClass: 'sortable-chosen',
|
chosenClass: 'sortable-chosen',
|
||||||
disabled: isFiltered,
|
disabled: !this.state.isAdmin || isFiltered,
|
||||||
onEnd: (evt) => {
|
onEnd: (evt) => {
|
||||||
const { oldIndex, newIndex } = evt;
|
const { oldIndex, newIndex } = evt;
|
||||||
if (oldIndex === newIndex) return;
|
if (oldIndex === newIndex) return;
|
||||||
@@ -260,7 +266,7 @@ class ArtikelLagerPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { status, error, warehouses, filter } = this.state;
|
const { isAdmin, status, error, warehouses, filter } = this.state;
|
||||||
const { prefill, returnTo } = getInitialParams(this.props);
|
const { prefill, returnTo } = getInitialParams(this.props);
|
||||||
|
|
||||||
if (status === 'loading' || status === 'idle') {
|
if (status === 'loading' || status === 'idle') {
|
||||||
@@ -299,6 +305,7 @@ class ArtikelLagerPage extends PureComponent {
|
|||||||
<ArtikelLager
|
<ArtikelLager
|
||||||
key={wh.id}
|
key={wh.id}
|
||||||
warehouse={wh}
|
warehouse={wh}
|
||||||
|
disabled={!isAdmin}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,20 +9,27 @@ import DialogTitle from '@mui/material/DialogTitle';
|
|||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import InputAdornment from '@mui/material/InputAdornment';
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
|
import Menu from '@mui/material/Menu';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import ToggleButton from '@mui/material/ToggleButton';
|
import ToggleButton from '@mui/material/ToggleButton';
|
||||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import LabelIcon from '@mui/icons-material/Label';
|
import LabelIcon from '@mui/icons-material/Label';
|
||||||
|
import PrintIcon from '@mui/icons-material/Print';
|
||||||
import BackendError from '../../components/BackendError';
|
import BackendError from '../../components/BackendError';
|
||||||
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
import { setLastArticleSku } from '../../automats/navigationAutomat';
|
import { setLastArticleSku } from '../../automats/navigationAutomat';
|
||||||
|
import companyAutomat from '../../automats/companyAutomat';
|
||||||
import { labeltemplate } from '../../api';
|
import { labeltemplate } from '../../api';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import {
|
import {
|
||||||
@@ -30,6 +37,36 @@ import {
|
|||||||
SURFACE_BASE,
|
SURFACE_BASE,
|
||||||
} from '../../colors';
|
} from '../../colors';
|
||||||
|
|
||||||
|
function filterZplPrinters(agentprinters) {
|
||||||
|
return (Array.isArray(agentprinters) ? agentprinters : []).filter(
|
||||||
|
(p) => p.enabled !== false && (!p.type || p.type.toLowerCase() === 'zpl'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrinterLabel(p) {
|
||||||
|
let label;
|
||||||
|
if (p.model && p.serial) {
|
||||||
|
label = `${p.model} (${p.serial})`;
|
||||||
|
} else if (p.model) {
|
||||||
|
label = p.model;
|
||||||
|
} else if (p.serial) {
|
||||||
|
label = `Drucker (${p.serial})`;
|
||||||
|
} else if (p.desc && p.address) {
|
||||||
|
label = `${p.desc} (${p.address})`;
|
||||||
|
} else if (p.address) {
|
||||||
|
label = p.address;
|
||||||
|
} else if (p.desc) {
|
||||||
|
label = p.desc;
|
||||||
|
} else {
|
||||||
|
label = `Drucker #${p.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.online === false) {
|
||||||
|
return `${label} (offline)`;
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
const CanvasEditor = lazy(async () => {
|
const CanvasEditor = lazy(async () => {
|
||||||
await import('react-canvas-label-editor/style.css');
|
await import('react-canvas-label-editor/style.css');
|
||||||
const mod = await import('react-canvas-label-editor');
|
const mod = await import('react-canvas-label-editor');
|
||||||
@@ -65,11 +102,15 @@ class ArtikelVorlagenPage extends PureComponent {
|
|||||||
const initialDpi = DEFAULT_DPI;
|
const initialDpi = DEFAULT_DPI;
|
||||||
const initialWidthClass = '2';
|
const initialWidthClass = '2';
|
||||||
const initialHeightMm = DEFAULT_HEIGHT_MM;
|
const initialHeightMm = DEFAULT_HEIGHT_MM;
|
||||||
|
const initialPrinters = filterZplPrinters(companyAutomat.state.agentprinters);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
status: 'loading',
|
status: 'loading',
|
||||||
error: null,
|
error: null,
|
||||||
templates: [],
|
templates: [],
|
||||||
|
printers: initialPrinters,
|
||||||
|
printerMenuAnchor: null,
|
||||||
|
printing: false,
|
||||||
// Editor dialog state
|
// Editor dialog state
|
||||||
editorOpen: false,
|
editorOpen: false,
|
||||||
editorLoading: false,
|
editorLoading: false,
|
||||||
@@ -97,16 +138,23 @@ class ArtikelVorlagenPage extends PureComponent {
|
|||||||
this.handleDpiChange = this.handleDpiChange.bind(this);
|
this.handleDpiChange = this.handleDpiChange.bind(this);
|
||||||
this.handleHeightMmChange = this.handleHeightMmChange.bind(this);
|
this.handleHeightMmChange = this.handleHeightMmChange.bind(this);
|
||||||
this.handleHeightMmBlur = this.handleHeightMmBlur.bind(this);
|
this.handleHeightMmBlur = this.handleHeightMmBlur.bind(this);
|
||||||
|
this.handlePrintClick = this.handlePrintClick.bind(this);
|
||||||
|
this.handleClosePrinterMenu = this.handleClosePrinterMenu.bind(this);
|
||||||
|
this.handleSelectPrinterForPrint = this.handleSelectPrinterForPrint.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
setLastArticleSku(null);
|
setLastArticleSku(null);
|
||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
void this.loadTemplates();
|
void this.loadTemplates();
|
||||||
|
this.unsubPrinters = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
printers: filterZplPrinters(state.agentprinters),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._cancelled = true;
|
this._cancelled = true;
|
||||||
|
this.unsubPrinters?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadTemplates() {
|
async loadTemplates() {
|
||||||
@@ -246,9 +294,66 @@ class ArtikelVorlagenPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleEditorClose() {
|
handleEditorClose() {
|
||||||
if (this.state.saving) return;
|
if (this.state.saving || this.state.printing) return;
|
||||||
this._currentEditId = null;
|
this._currentEditId = null;
|
||||||
this.setState({ editorOpen: false, editorLoading: false, editorTemplate: null });
|
this.setState({
|
||||||
|
editorOpen: false,
|
||||||
|
editorLoading: false,
|
||||||
|
editorTemplate: null,
|
||||||
|
printerMenuAnchor: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePrintClick(event) {
|
||||||
|
const { printers } = this.state;
|
||||||
|
if (!printers || printers.length === 0) {
|
||||||
|
enqueueSnackbar('Kein ZPL-Drucker gefunden', { variant: 'warning' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (printers.length === 1) {
|
||||||
|
void this.executeTestPrint(printers[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ printerMenuAnchor: event.currentTarget });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClosePrinterMenu() {
|
||||||
|
this.setState({ printerMenuAnchor: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelectPrinterForPrint(targetPrinter) {
|
||||||
|
this.setState({ printerMenuAnchor: null });
|
||||||
|
void this.executeTestPrint(targetPrinter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeTestPrint(targetPrinter) {
|
||||||
|
if (!targetPrinter?.id) return;
|
||||||
|
const {
|
||||||
|
editorTemplate, editorObjects, editorWidth, editorHeight, editorDpi, editorName,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
this.setState({ printing: true });
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
agentprinterId: targetPrinter.id,
|
||||||
|
templateId: editorTemplate?.id,
|
||||||
|
name: editorName,
|
||||||
|
width: editorWidth,
|
||||||
|
height: editorHeight,
|
||||||
|
dpi: editorDpi,
|
||||||
|
objects: editorObjects,
|
||||||
|
};
|
||||||
|
await labeltemplate.testprint(payload);
|
||||||
|
enqueueSnackbar(`Testdruck an ${getPrinterLabel(targetPrinter)} gesendet.`, {
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
} finally {
|
||||||
|
if (!this._cancelled) {
|
||||||
|
this.setState({ printing: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleEditorSave() {
|
async handleEditorSave() {
|
||||||
@@ -320,7 +425,7 @@ class ArtikelVorlagenPage extends PureComponent {
|
|||||||
const {
|
const {
|
||||||
editorOpen, editorLoading, editorTemplate, editorName, editorObjects,
|
editorOpen, editorLoading, editorTemplate, editorName, editorObjects,
|
||||||
editorHeight, editorWidth, editorDpi, editorWidthClass, editorHeightMm,
|
editorHeight, editorWidth, editorDpi, editorWidthClass, editorHeightMm,
|
||||||
editorClipboard, saving,
|
editorClipboard, saving, printing, printers, printerMenuAnchor,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const minHeightPx = getMinHeightPx(editorDpi);
|
const minHeightPx = getMinHeightPx(editorDpi);
|
||||||
@@ -438,18 +543,66 @@ class ArtikelVorlagenPage extends PureComponent {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
<DialogActions sx={{ px: 3, pb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<Button onClick={this.handleEditorClose} disabled={saving}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
Abbrechen
|
<Tooltip
|
||||||
</Button>
|
title={
|
||||||
<Button
|
printers.length === 0
|
||||||
variant="contained"
|
? 'Kein ZPL-Drucker verfügbar'
|
||||||
onClick={this.handleEditorSave}
|
: printers.length === 1
|
||||||
disabled={saving || editorLoading || !editorName.trim()}
|
? `Drucken auf ${getPrinterLabel(printers[0])}`
|
||||||
>
|
: 'Testdruck an Drucker senden'
|
||||||
{saving ? <CircularProgress size={18} sx={{ mr: 1 }} /> : null}
|
}
|
||||||
{saving ? 'Speichert…' : 'Speichern'}
|
>
|
||||||
</Button>
|
<span>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
startIcon={printing ? <CircularProgress size={18} /> : <PrintIcon />}
|
||||||
|
endIcon={printers.length > 1 ? <ArrowDropDownIcon /> : null}
|
||||||
|
onClick={this.handlePrintClick}
|
||||||
|
disabled={printing || saving || editorLoading || printers.length === 0}
|
||||||
|
>
|
||||||
|
{printing ? 'Druckt…' : 'Testdruck'}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
{printers.length > 1 && (
|
||||||
|
<Menu
|
||||||
|
anchorEl={printerMenuAnchor}
|
||||||
|
open={Boolean(printerMenuAnchor)}
|
||||||
|
onClose={this.handleClosePrinterMenu}
|
||||||
|
>
|
||||||
|
{printers.map((p) => (
|
||||||
|
<MenuItem
|
||||||
|
key={p.id}
|
||||||
|
onClick={() => this.handleSelectPrinterForPrint(p)}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<PrintIcon fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={getPrinterLabel(p)}
|
||||||
|
secondary={p.serial ? `Seriennummer: ${p.serial}` : undefined}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Button onClick={this.handleEditorClose} disabled={saving || printing}>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.handleEditorSave}
|
||||||
|
disabled={saving || printing || editorLoading || !editorName.trim()}
|
||||||
|
>
|
||||||
|
{saving ? <CircularProgress size={18} sx={{ mr: 1 }} /> : null}
|
||||||
|
{saving ? 'Speichert…' : 'Speichern'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</AppDialog>
|
</AppDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class VersandartenSearchField extends PureComponent {
|
|||||||
super(props);
|
super(props);
|
||||||
const initial = companyAutomat.state.carrierFilter || '';
|
const initial = companyAutomat.state.carrierFilter || '';
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
input: initial,
|
input: initial,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
};
|
};
|
||||||
@@ -29,6 +30,7 @@ class VersandartenSearchField extends PureComponent {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
input: state.carrierFilter,
|
input: state.carrierFilter,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -43,6 +45,7 @@ class VersandartenSearchField extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSubmit() {
|
async handleSubmit() {
|
||||||
|
if (!this.state.isAdmin) return;
|
||||||
const name = this.state.input.trim();
|
const name = this.state.input.trim();
|
||||||
if (!name || this.state.submitting) return;
|
if (!name || this.state.submitting) return;
|
||||||
|
|
||||||
@@ -64,8 +67,8 @@ class VersandartenSearchField extends PureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { inputRef } = this.props;
|
const { inputRef } = this.props;
|
||||||
const { input, submitting } = this.state;
|
const { isAdmin, input, submitting } = this.state;
|
||||||
const canSubmit = Boolean(input.trim()) && !submitting;
|
const canCreate = isAdmin && Boolean(input.trim()) && !submitting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelTileGrid>
|
<PanelTileGrid>
|
||||||
@@ -102,7 +105,7 @@ class VersandartenSearchField extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={!canSubmit}
|
disabled={!canCreate}
|
||||||
onClick={() => void this.handleSubmit()}
|
onClick={() => void this.handleSubmit()}
|
||||||
>
|
>
|
||||||
Hinzufügen
|
Hinzufügen
|
||||||
@@ -120,6 +123,7 @@ class AuftragVersandartenPage extends PureComponent {
|
|||||||
const initialCarriers =
|
const initialCarriers =
|
||||||
companyAutomat.state.filteredCarriers ?? companyAutomat.state.carriers ?? [];
|
companyAutomat.state.filteredCarriers ?? companyAutomat.state.carriers ?? [];
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
status: companyAutomat.state.carriersStatus === 'ready' ? 'ready' : 'loading',
|
status: companyAutomat.state.carriersStatus === 'ready' ? 'ready' : 'loading',
|
||||||
error: companyAutomat.state.carriersError ? backendErrorFrom(companyAutomat.state.carriersError) : null,
|
error: companyAutomat.state.carriersError ? backendErrorFrom(companyAutomat.state.carriersError) : null,
|
||||||
carriers: initialCarriers,
|
carriers: initialCarriers,
|
||||||
@@ -138,6 +142,7 @@ class AuftragVersandartenPage extends PureComponent {
|
|||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
|
|
||||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
carriers: state.filteredCarriers ?? state.carriers,
|
carriers: state.filteredCarriers ?? state.carriers,
|
||||||
filter: state.carrierFilter ?? '',
|
filter: state.carrierFilter ?? '',
|
||||||
status: state.carriersStatus,
|
status: state.carriersStatus,
|
||||||
@@ -151,6 +156,7 @@ class AuftragVersandartenPage extends PureComponent {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (
|
if (
|
||||||
|
prevState.isAdmin !== this.state.isAdmin ||
|
||||||
prevState.status !== this.state.status ||
|
prevState.status !== this.state.status ||
|
||||||
prevState.carriers !== this.state.carriers ||
|
prevState.carriers !== this.state.carriers ||
|
||||||
prevState.filter !== this.state.filter
|
prevState.filter !== this.state.filter
|
||||||
@@ -179,7 +185,7 @@ class AuftragVersandartenPage extends PureComponent {
|
|||||||
animation: 150,
|
animation: 150,
|
||||||
ghostClass: 'sortable-ghost',
|
ghostClass: 'sortable-ghost',
|
||||||
chosenClass: 'sortable-chosen',
|
chosenClass: 'sortable-chosen',
|
||||||
disabled: isFiltered,
|
disabled: !this.state.isAdmin || isFiltered,
|
||||||
onEnd: (evt) => {
|
onEnd: (evt) => {
|
||||||
const { oldIndex, newIndex } = evt;
|
const { oldIndex, newIndex } = evt;
|
||||||
if (oldIndex === newIndex) return;
|
if (oldIndex === newIndex) return;
|
||||||
@@ -220,7 +226,7 @@ class AuftragVersandartenPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { status, error, carriers, filter } = this.state;
|
const { isAdmin, status, error, carriers, filter } = this.state;
|
||||||
|
|
||||||
if (status === 'loading' || status === 'idle') {
|
if (status === 'loading' || status === 'idle') {
|
||||||
return (
|
return (
|
||||||
@@ -253,6 +259,7 @@ class AuftragVersandartenPage extends PureComponent {
|
|||||||
<CarrierItem
|
<CarrierItem
|
||||||
key={c.id}
|
key={c.id}
|
||||||
carrier={c}
|
carrier={c}
|
||||||
|
disabled={!isAdmin}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,13 +22,6 @@ export default class StatisticsPage extends PureComponent {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BarChartIcon sx={{ fontSize: 48, opacity: 0.4 }} />
|
<BarChartIcon sx={{ fontSize: 48, opacity: 0.4 }} />
|
||||||
<Typography variant="h6" color="text.primary" fontWeight={600}>
|
|
||||||
Statistik & Auswertungen
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 420 }}>
|
|
||||||
Hier entstehen demnächst grafische Auswertungen, Umsatzstatistiken und
|
|
||||||
Bestandskennzahlen.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class KundeGruppenSearchField extends PureComponent {
|
|||||||
super(props);
|
super(props);
|
||||||
const initial = companyAutomat.state.customergroupFilter || props.prefill || '';
|
const initial = companyAutomat.state.customergroupFilter || props.prefill || '';
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
input: initial,
|
input: initial,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
};
|
};
|
||||||
@@ -49,6 +50,7 @@ class KundeGruppenSearchField extends PureComponent {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
input: state.customergroupFilter,
|
input: state.customergroupFilter,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -63,6 +65,7 @@ class KundeGruppenSearchField extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSubmit() {
|
async handleSubmit() {
|
||||||
|
if (!this.state.isAdmin) return;
|
||||||
const name = this.state.input.trim();
|
const name = this.state.input.trim();
|
||||||
if (!name || this.state.submitting) return;
|
if (!name || this.state.submitting) return;
|
||||||
|
|
||||||
@@ -83,8 +86,8 @@ class KundeGruppenSearchField extends PureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { inputRef, returnTo, navigate } = this.props;
|
const { inputRef, returnTo, navigate } = this.props;
|
||||||
const { input, submitting } = this.state;
|
const { isAdmin, input, submitting } = this.state;
|
||||||
const canSubmit = Boolean(input.trim()) && !submitting;
|
const canCreate = isAdmin && Boolean(input.trim()) && !submitting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelTileGrid>
|
<PanelTileGrid>
|
||||||
@@ -141,7 +144,7 @@ class KundeGruppenSearchField extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={!canSubmit}
|
disabled={!canCreate}
|
||||||
onClick={() => void this.handleSubmit()}
|
onClick={() => void this.handleSubmit()}
|
||||||
>
|
>
|
||||||
Hinzufügen
|
Hinzufügen
|
||||||
@@ -159,6 +162,7 @@ class KundeGruppenPage extends PureComponent {
|
|||||||
const initialGroups =
|
const initialGroups =
|
||||||
companyAutomat.state.filteredCustomergroups ?? companyAutomat.state.customergroups ?? [];
|
companyAutomat.state.filteredCustomergroups ?? companyAutomat.state.customergroups ?? [];
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isAdmin: companyAutomat.state.current?.role === 'admin',
|
||||||
status: companyAutomat.state.customergroupsStatus === 'ready' ? 'ready' : 'loading',
|
status: companyAutomat.state.customergroupsStatus === 'ready' ? 'ready' : 'loading',
|
||||||
error: companyAutomat.state.customergroupsError ? backendErrorFrom(companyAutomat.state.customergroupsError) : null,
|
error: companyAutomat.state.customergroupsError ? backendErrorFrom(companyAutomat.state.customergroupsError) : null,
|
||||||
groups: initialGroups,
|
groups: initialGroups,
|
||||||
@@ -177,6 +181,7 @@ class KundeGruppenPage extends PureComponent {
|
|||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
|
|
||||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
isAdmin: state.current?.role === 'admin',
|
||||||
groups: state.filteredCustomergroups ?? state.customergroups,
|
groups: state.filteredCustomergroups ?? state.customergroups,
|
||||||
filter: state.customergroupFilter ?? '',
|
filter: state.customergroupFilter ?? '',
|
||||||
status: state.customergroupsStatus,
|
status: state.customergroupsStatus,
|
||||||
@@ -190,6 +195,7 @@ class KundeGruppenPage extends PureComponent {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (
|
if (
|
||||||
|
prevState.isAdmin !== this.state.isAdmin ||
|
||||||
prevState.status !== this.state.status ||
|
prevState.status !== this.state.status ||
|
||||||
prevState.groups !== this.state.groups ||
|
prevState.groups !== this.state.groups ||
|
||||||
prevState.filter !== this.state.filter
|
prevState.filter !== this.state.filter
|
||||||
@@ -218,7 +224,7 @@ class KundeGruppenPage extends PureComponent {
|
|||||||
animation: 150,
|
animation: 150,
|
||||||
ghostClass: 'sortable-ghost',
|
ghostClass: 'sortable-ghost',
|
||||||
chosenClass: 'sortable-chosen',
|
chosenClass: 'sortable-chosen',
|
||||||
disabled: isFiltered,
|
disabled: !this.state.isAdmin || isFiltered,
|
||||||
onEnd: (evt) => {
|
onEnd: (evt) => {
|
||||||
const { oldIndex, newIndex } = evt;
|
const { oldIndex, newIndex } = evt;
|
||||||
if (oldIndex === newIndex) return;
|
if (oldIndex === newIndex) return;
|
||||||
@@ -259,7 +265,7 @@ class KundeGruppenPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { status, error, groups, filter } = this.state;
|
const { isAdmin, status, error, groups, filter } = this.state;
|
||||||
const { prefill, returnTo } = getInitialParams(this.props);
|
const { prefill, returnTo } = getInitialParams(this.props);
|
||||||
|
|
||||||
if (status === 'loading' || status === 'idle') {
|
if (status === 'loading' || status === 'idle') {
|
||||||
@@ -298,6 +304,7 @@ class KundeGruppenPage extends PureComponent {
|
|||||||
<KundeGruppe
|
<KundeGruppe
|
||||||
key={group.id}
|
key={group.id}
|
||||||
group={group}
|
group={group}
|
||||||
|
disabled={!isAdmin}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,5 +22,6 @@ export function normalizeCompany(raw, overrides = {}) {
|
|||||||
reducedtaxrate: overrides.reducedtaxrate !== undefined ? overrides.reducedtaxrate : raw.reducedtaxrate,
|
reducedtaxrate: overrides.reducedtaxrate !== undefined ? overrides.reducedtaxrate : raw.reducedtaxrate,
|
||||||
taxfreerate: overrides.taxfreerate !== undefined ? overrides.taxfreerate : raw.taxfreerate,
|
taxfreerate: overrides.taxfreerate !== undefined ? overrides.taxfreerate : raw.taxfreerate,
|
||||||
rootCategoryId: overrides.rootCategoryId !== undefined ? overrides.rootCategoryId : raw.rootCategoryId,
|
rootCategoryId: overrides.rootCategoryId !== undefined ? overrides.rootCategoryId : raw.rootCategoryId,
|
||||||
|
role: overrides.role !== undefined ? overrides.role : raw.role,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 420 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 445 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 695 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 430 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 430 KiB |
|
Before Width: | Height: | Size: 530 KiB After Width: | Height: | Size: 529 KiB |
|
Before Width: | Height: | Size: 546 KiB After Width: | Height: | Size: 540 KiB |
|
Before Width: | Height: | Size: 574 KiB After Width: | Height: | Size: 564 KiB |
|
Before Width: | Height: | Size: 598 KiB After Width: | Height: | Size: 586 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 499 KiB After Width: | Height: | Size: 936 KiB |
|
Before Width: | Height: | Size: 594 KiB After Width: | Height: | Size: 693 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 322 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 380 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 330 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 348 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 167 KiB |