diff --git a/API.article.md b/API.article.md index 9991133..7008620 100644 --- a/API.article.md +++ b/API.article.md @@ -123,7 +123,7 @@ Returns the full detail of an article by its SKU, including unit, brand, categor **Returns** -`Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {name: string, thumbW: number, thumbH: number, thumbHash: string}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>` — The full article detail. +`Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {name: string, thumbW: number, thumbH: number, thumbHash: string}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>` — The full article detail. **Throws** diff --git a/API.articlesupplier.md b/API.articlesupplier.md index 55462af..7bd1e03 100644 --- a/API.articlesupplier.md +++ b/API.articlesupplier.md @@ -19,13 +19,14 @@ Assigns a supplier to an article, optionally creating a purchase price. | `[input.sku]` | `string` | The supplier's item number/SKU. | | `[input.pu]` | `number` | Packaging unit (defaults to 1). | | `[input.stock]` | `number|null` | Current stock at the supplier. | +| `[input.sort]` | `number` | Display sort order (defaults to next sort). | | `[input.netPrice]` | `number|null` | Net purchase price. | | `[input.taxId]` | `number` | The tax id for the purchase price. | | `[input.taxRate]` | `number` | The tax rate in percent (used if taxId is not provided). | **Returns** -`Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>` — The created assignment. +`Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>` — The created assignment. **Throws** @@ -40,7 +41,7 @@ const created = await store.articlesupplier.create(42, 'SKU-1', { pu: 1, netPrice: 4.20, }); -// => { id, articleId, entityId, supplierName, sku, pu, stock, priceId, price } +// => { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price } ``` ## `articlesupplier.delete` @@ -86,7 +87,7 @@ Lists the supplier assignments for an article by its SKU. **Returns** -`Promise>` — The supplier assignments. +`Promise>` — The supplier assignments. **Throws** @@ -99,6 +100,36 @@ const suppliers = await store.articlesupplier.listByArticleSku(42, 'SKU-1'); // => [{ id, articleId, entityId, supplierName, sku, pu, stock, priceId, price }] ``` +## `articlesupplier.order` + +*Source: `src/store/articlesupplier/order.js`* + +Reorders the supplier assignments of an article by its SKU. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `input` | `object` | The reorder input. | +| `input.ids` | `Array` | The articlesupplier ids in their new order. | + +**Returns** + +`Promise<{ids: Array}>` — The applied order. + +**Throws** + +`Error` — If the order is invalid. + +**Example** + +```js +const result = await store.articlesupplier.order(42, 'SKU-1', { ids: [12, 10, 15] }); +// => { ids: [12, 10, 15] } +``` + ## `articlesupplier.update` *Source: `src/store/articlesupplier/update.js`* @@ -116,13 +147,14 @@ Updates an existing supplier assignment and its associated purchase price. | `[patch.sku]` | `string|null` | The supplier's item number/SKU. | | `[patch.pu]` | `number` | Packaging unit (> 0). | | `[patch.stock]` | `number|null` | Current stock at the supplier. | +| `[patch.sort]` | `number` | Display sort order. | | `[patch.netPrice]` | `number|null` | Net purchase price. | | `[patch.taxId]` | `number` | The tax id for the purchase price. | | `[patch.taxRate]` | `number` | The tax rate in percent. | **Returns** -`Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>` — The updated assignment. +`Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>` — The updated assignment. **Throws** @@ -136,5 +168,5 @@ const updated = await store.articlesupplier.update(42, 'SKU-1', 12, { pu: 6, netPrice: 22.50, }); -// => { id, articleId, entityId, supplierName, sku, pu, stock, priceId, price } +// => { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price } ``` diff --git a/API.md b/API.md index ae228fd..cd92a91 100644 --- a/API.md +++ b/API.md @@ -33,7 +33,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `createUnderParent(input: {googleAuthId: number, parentId: number, name: string}): Promise<{id: number, parentId: number, name: string, sku: string}>` - `delete(googleAuthId: number, sku: string): Promise` - `deleteImage(googleAuthId: number, sku: string, fullHash: string): Promise` - - `getBySku(googleAuthId: number, sku: string): Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {name: string, thumbW: number, thumbH: number, thumbHash: string}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>` + - `getBySku(googleAuthId: number, sku: string): Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {name: string, thumbW: number, thumbH: number, thumbHash: string}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>` - `getHistoryBySku(googleAuthId: number, sku: string): Promise>` - `importRows(input: {googleAuthId: number, rows: Array}): Promise<{imported: Array, skipped: Array, errors: Array}>` - `listExport(googleAuthId: number, categoryId?: number|null): Promise>` @@ -99,10 +99,11 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `list(googleAuthId: number): Promise>` - `update(googleAuthId: number, id: number, input: {name: string}): Promise<{id: number, name: string, createdAt: string, updatedAt: string}>` - [articlesupplier](API.articlesupplier.md) - - `create(googleAuthId: number, sku: string, input: {entityId: number, sku?: string, pu?: number, stock?: number|null, netPrice?: number|null, taxId?: number, taxRate?: number}): Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>` + - `create(googleAuthId: number, sku: string, input: {entityId: number, sku?: string, pu?: number, stock?: number|null, sort?: number, netPrice?: number|null, taxId?: number, taxRate?: number}): Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>` - `delete(googleAuthId: number, sku: string, id: number): Promise` - - `listByArticleSku(googleAuthId: number, sku: string): Promise>` - - `update(googleAuthId: number, sku: string, id: number, patch: {sku?: string|null, pu?: number, stock?: number|null, netPrice?: number|null, taxId?: number, taxRate?: number}): Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>` + - `listByArticleSku(googleAuthId: number, sku: string): Promise>` + - `order(googleAuthId: number, sku: string, input: {ids: Array}): Promise<{ids: Array}>` + - `update(googleAuthId: number, sku: string, id: number, patch: {sku?: string|null, pu?: number, stock?: number|null, sort?: number, netPrice?: number|null, taxId?: number, taxRate?: number}): Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>` - [delivery](API.delivery.md) - `create(googleAuthId: number, body: {date: string, supplierId: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}): Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` - `delete(googleAuthId: number, id: number): Promise` diff --git a/README.md b/README.md index 527a7f1..6a614af 100644 --- a/README.md +++ b/README.md @@ -300,17 +300,19 @@ Maps articles to suppliers (`entity`) with purchase pricing, stock, packaging un | `sku` | `NVARCHAR(100)` | Supplier's SKU code, nullable | | `stock` | `FLOAT` | Stock available at supplier, nullable | | `pu` | `FLOAT` | Packaging unit (Verpackungseinheit), default `1` | +| `sort` | `INT` | Display sort order (default `0`) | -Unique constraint: `UQ_articlesupplier_article_entity (article_id, entity_id)` +Unique constraints: `UQ_articlesupplier_article_entity (article_id, entity_id)`, `UQ_articlesupplier_article_sort (article_id, sort)` | Method | Signature | Returns | | --- | --- | --- | | `listByArticleSku` | `(googleAuthId, sku)` | `ArticleSupplierSummary[]` | -| `create` | `(googleAuthId, sku, { entityId, sku?, pu?, stock?, netPrice?, taxId?, taxRate? })` | `ArticleSupplierSummary` | -| `update` | `(googleAuthId, sku, id, { sku?, pu?, stock?, netPrice?, taxId?, taxRate? })` | `ArticleSupplierSummary` | +| `create` | `(googleAuthId, sku, { entityId, sku?, pu?, stock?, sort?, netPrice?, taxId?, taxRate? })` | `ArticleSupplierSummary` | +| `update` | `(googleAuthId, sku, id, { sku?, pu?, stock?, sort?, netPrice?, taxId?, taxRate? })` | `ArticleSupplierSummary` | +| `order` | `(googleAuthId, sku, { ids })` | `{ ids: number[] }` | | `delete` | `(googleAuthId, sku, id)` | `void` | -Errors: `Not authorized`, `Article not found`, `Supplier required`, `Supplier not found`, `Supplier assignment not found`, `Supplier already assigned`, `Invalid pu`, `Invalid stock`, `Invalid price`. +Errors: `Not authorized`, `Article not found`, `Supplier required`, `Supplier not found`, `Supplier assignment not found`, `Supplier already assigned`, `Invalid pu`, `Invalid stock`, `Invalid sort`, `Invalid supplier order`, `Invalid price`. --- @@ -760,6 +762,7 @@ Company-scoped number sequences (`numbers` table). Auth via selected company (`g | `GET /api/article/:sku/supplier` | `articlesupplier.listByArticleSku(googleAuthId, sku)` | | `POST /api/article/:sku/supplier` | `articlesupplier.create(googleAuthId, sku, { entityId, sku?, pu?, stock?, netPrice?, taxId?, taxRate? })` | | `PATCH /api/article/:sku/supplier/:id` | `articlesupplier.update(googleAuthId, sku, id, patch)` | +| `PUT /api/article/:sku/supplier/order` | `articlesupplier.order(googleAuthId, sku, { ids })` | | `DELETE /api/article/:sku/supplier/:id` | `articlesupplier.delete(googleAuthId, sku, id)` | | `GET /api/delivery` | `delivery.list(googleAuthId)` | | `GET /api/delivery/:id` | `delivery.getById(googleAuthId, id)` | diff --git a/schema-graph.dot b/schema-graph.dot index 4c13ced..215f27c 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -42,7 +42,7 @@ digraph schema { articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; articlelist [label=<
articlelist
idINT PK
>]; articlelistarticle [label=<
articlelistarticle
idINT PK
article_idINT FK
articlelist_idINT FK
countINT
sortINT
>]; - articlesupplier [label=<
articlesupplier
idINT PK
entity_idINT FK
price_id?INT FK
article_idINT FK
sku?NVARCHAR(100)
stock?FLOAT
puFLOAT
>]; + articlesupplier [label=<
articlesupplier
idINT PK
entity_idINT FK
price_id?INT FK
article_idINT FK
sku?NVARCHAR(100)
stock?FLOAT
puFLOAT
sortINT
>]; brand [label=<
brand
idINT PK
company_idINT FK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; optionslist [label=<
optionslist
idINT PK
>]; optionslistarticle [label=<
optionslistarticle
idINT PK
article_idINT FK
optionslist_idINT FK
countINT
sortINT
mandatedBIT
>]; @@ -133,9 +133,9 @@ digraph schema { price:tax_id:w -> tax:in3:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_price_tax", constraint=false, weight=1]; price:article_id:sw -> article:in2:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_price_article", weight=3]; price:customergroup_id:e -> customergroup:in1:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_price_customergroup", constraint=false, weight=1]; - articlesupplier:article_id:se -> article:in7:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_articlesupplier_article", weight=3]; + articlesupplier:article_id:sw -> article:in7:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_articlesupplier_article", weight=3]; articlesupplier:entity_id:w -> entity:in4:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_articlesupplier_entity", constraint=false, weight=1]; - articlesupplier:price_id:w -> price:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articlesupplier_price", constraint=false, weight=1]; + articlesupplier:price_id:e -> price:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articlesupplier_price", constraint=false, weight=1]; manifest:company_id:e -> company:in1:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_manifest_company", weight=3]; manifest:entity_id:e -> entity:in0:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_manifest_entity", constraint=false, weight=1]; manifestarticle:manifest_id:ne -> manifest:in0:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_manifestarticle_manifest", weight=3]; diff --git a/schema-graph.svg b/schema-graph.svg index 126db4f..ad75f08 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -4,11 +4,11 @@ - - + + schema - + cluster_identity @@ -21,8 +21,8 @@ cluster_catalog - -Catalog + +Catalog cluster_media @@ -1242,58 +1242,62 @@ articlesupplier - - -articlesupplier - - -id -INT -PK - - -entity_id -INT -FK - - -price_id -? -INT -FK - - -article_id -INT -FK - - -sku? -NVARCHAR(100) - - -stock? -FLOAT - - -pu -FLOAT + + +articlesupplier + + +id +INT +PK + + +entity_id +INT +FK + + +price_id +? +INT +FK + + +article_id +INT +FK + + +sku? +NVARCHAR(100) + + +stock? +FLOAT + + +pu +FLOAT + + +sort +INT articlesupplier:w->entity:n - + -articlesupplier:se->article:n +articlesupplier:sw->article:n - - + + @@ -1335,9 +1339,9 @@ -articlesupplier:w->price:n +articlesupplier:e->price:n - + diff --git a/src/index.js b/src/index.js index f0317d3..3a7c106 100644 --- a/src/index.js +++ b/src/index.js @@ -69,6 +69,7 @@ import articlesupplierListByArticleSku from './store/articlesupplier/listByArtic import articlesupplierCreate from './store/articlesupplier/create.js'; import articlesupplierUpdate from './store/articlesupplier/update.js'; import articlesupplierDelete from './store/articlesupplier/delete.js'; +import articlesupplierOrder from './store/articlesupplier/order.js'; import deliveryList from './store/delivery/list.js'; import deliveryGetById from './store/delivery/getById.js'; import deliveryCreate from './store/delivery/create.js'; @@ -245,6 +246,7 @@ export default class LibDashStore { listByArticleSku: articlesupplierListByArticleSku(pool), create: articlesupplierCreate(pool), update: articlesupplierUpdate(pool), + order: articlesupplierOrder(pool), delete: articlesupplierDelete(pool), }; diff --git a/src/schema.js b/src/schema.js index 28ec668..2d3bb7e 100644 --- a/src/schema.js +++ b/src/schema.js @@ -539,10 +539,12 @@ BEGIN sku NVARCHAR(100) NULL, stock FLOAT NULL, pu FLOAT NOT NULL CONSTRAINT DF_articlesupplier_pu DEFAULT 1, + sort INT NOT NULL CONSTRAINT DF_articlesupplier_sort DEFAULT 0, CONSTRAINT FK_articlesupplier_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, CONSTRAINT FK_articlesupplier_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE, - CONSTRAINT FK_articlesupplier_price FOREIGN KEY (price_id) REFERENCES price(id) ON DELETE SET NULL, - CONSTRAINT UQ_articlesupplier_article_entity UNIQUE (article_id, entity_id) + CONSTRAINT FK_articlesupplier_price FOREIGN KEY (price_id) REFERENCES price(id) ON DELETE NO ACTION, + CONSTRAINT UQ_articlesupplier_article_entity UNIQUE (article_id, entity_id), + CONSTRAINT UQ_articlesupplier_article_sort UNIQUE (article_id, sort) ); CREATE INDEX IX_articlesupplier_article_entity diff --git a/src/sql/article.js b/src/sql/article.js index da3b4d0..2447175 100644 --- a/src/sql/article.js +++ b/src/sql/article.js @@ -471,6 +471,7 @@ SELECT asup.sku, asup.pu, asup.stock, + asup.sort, asup.price_id AS priceId, p.net AS [price.net], p.tax_id AS [price.taxId], @@ -480,7 +481,7 @@ SELECT LEFT JOIN price p ON p.id = asup.price_id LEFT JOIN tax t ON t.id = p.tax_id WHERE asup.article_id = a.id - ORDER BY e.name, asup.id + ORDER BY asup.sort ASC, asup.id ASC FOR JSON PATH ) AS suppliersJson FROM article a diff --git a/src/sql/articlesupplier.js b/src/sql/articlesupplier.js index 7e6eb97..3d0f0df 100644 --- a/src/sql/articlesupplier.js +++ b/src/sql/articlesupplier.js @@ -7,6 +7,7 @@ SELECT asup.sku, asup.pu, asup.stock, + asup.sort, asup.price_id AS priceId, p.net AS priceNet, p.tax_id AS priceTaxId, @@ -18,7 +19,7 @@ LEFT JOIN price p ON p.id = asup.price_id LEFT JOIN tax t ON t.id = p.tax_id WHERE asup.article_id = @articleId AND cg.company_id = @companyId -ORDER BY e.name, asup.id; +ORDER BY asup.sort ASC, asup.id ASC; `; export const GET_BY_ID_AND_ARTICLE_ID = ` @@ -30,6 +31,7 @@ SELECT asup.sku, asup.pu, asup.stock, + asup.sort, asup.price_id AS priceId, p.net AS priceNet, p.tax_id AS priceTaxId, @@ -50,10 +52,16 @@ FROM articlesupplier WHERE article_id = @articleId AND entity_id = @entityId; `; +export const GET_NEXT_SORT = ` +SELECT ISNULL(MAX(sort), -1) + 1 AS sort +FROM articlesupplier +WHERE article_id = @articleId; +`; + export const CREATE = ` -INSERT INTO articlesupplier (article_id, entity_id, price_id, sku, stock, pu) +INSERT INTO articlesupplier (article_id, entity_id, price_id, sku, stock, pu, sort) OUTPUT INSERTED.id -VALUES (@articleId, @entityId, @priceId, @sku, @stock, @pu); +VALUES (@articleId, @entityId, @priceId, @sku, @stock, @pu, @sort); `; export const UPDATE = ` @@ -62,7 +70,8 @@ SET sku = @sku, stock = @stock, pu = @pu, - price_id = @priceId + price_id = @priceId, + sort = @sort WHERE id = @id AND article_id = @articleId; `; @@ -71,6 +80,25 @@ DELETE FROM articlesupplier WHERE id = @id AND article_id = @articleId; `; +export const LIST_IDS_BY_ARTICLE = ` +SELECT id +FROM articlesupplier +WHERE article_id = @articleId +ORDER BY sort ASC, id ASC; +`; + +export const SET_TEMP_SORTS = ` +UPDATE articlesupplier +SET sort = -id +WHERE article_id = @articleId; +`; + +export const UPDATE_SORT = ` +UPDATE articlesupplier +SET sort = @sort +WHERE id = @id AND article_id = @articleId; +`; + export const UPDATE_PRICE = ` UPDATE price SET diff --git a/src/store/article/getBySku.js b/src/store/article/getBySku.js index c2dfd38..12a3d90 100644 --- a/src/store/article/getBySku.js +++ b/src/store/article/getBySku.js @@ -13,7 +13,7 @@ export default function getBySku(pool) { * * @param {number} googleAuthId - The id of the Google auth user. * @param {string} sku - The article SKU. - * @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {name: string, thumbW: number, thumbH: number, thumbHash: string}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>} The full article detail. + * @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {name: string, thumbW: number, thumbH: number, thumbHash: string}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>} The full article detail. * @throws {Error} If the article is not found or the user is not authorized. * * @example @@ -80,6 +80,7 @@ export default function getBySku(pool) { sku: s.sku ?? null, pu: s.pu, stock: s.stock ?? null, + sort: s.sort, priceId: s.priceId ?? null, price: s.priceId != null && s.price?.net != null ? s.price : null, })), diff --git a/src/store/articlesupplier/create.js b/src/store/articlesupplier/create.js index f61dcab..4f0c216 100644 --- a/src/store/articlesupplier/create.js +++ b/src/store/articlesupplier/create.js @@ -2,6 +2,7 @@ import { CHECK_EXISTS, CREATE as CREATE_ARTICLE_SUPPLIER, GET_BY_ID_AND_ARTICLE_ID, + GET_NEXT_SORT, } from '../../sql/articlesupplier.js'; import { CREATE as CREATE_PRICE } from '../../sql/price.js'; import request from '../request.js'; @@ -29,10 +30,11 @@ export default function create(pool) { * @param {string} [input.sku] - The supplier's item number/SKU. * @param {number} [input.pu] - Packaging unit (defaults to 1). * @param {number|null} [input.stock] - Current stock at the supplier. + * @param {number} [input.sort] - Display sort order (defaults to next sort). * @param {number|null} [input.netPrice] - Net purchase price. * @param {number} [input.taxId] - The tax id for the purchase price. * @param {number} [input.taxRate] - The tax rate in percent (used if taxId is not provided). - * @returns {Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>} The created assignment. + * @returns {Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>} The created assignment. * @throws {Error} If input is invalid, article or supplier is not found, or supplier is already assigned. * * @example @@ -42,7 +44,7 @@ export default function create(pool) { * pu: 1, * netPrice: 4.20, * }); - * // => { id, articleId, entityId, supplierName, sku, pu, stock, priceId, price } + * // => { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price } */ return async (googleAuthId, sku, input = {}) => { const entityId = input.entityId; @@ -63,6 +65,12 @@ export default function create(pool) { throw new Error('Invalid stock'); } + if (input.sort !== undefined && input.sort !== null) { + if (!Number.isFinite(input.sort)) { + throw new Error('Invalid sort'); + } + } + const netPrice = input.netPrice === undefined || input.netPrice === null ? null @@ -89,6 +97,14 @@ export default function create(pool) { await tx.begin(); try { + let sort = input.sort; + if (sort === undefined || sort === null) { + const sortRes = await request(pool, tx) + .input('articleId', articleId) + .query(GET_NEXT_SORT); + sort = sortRes.recordset[0].sort; + } + let priceId = null; if (netPrice !== null) { @@ -130,6 +146,7 @@ export default function create(pool) { .input('sku', supplierSku) .input('stock', stock) .input('pu', pu) + .input('sort', sort) .query(CREATE_ARTICLE_SUPPLIER); const createdId = createdRes.recordset[0].id; diff --git a/src/store/articlesupplier/helpers.js b/src/store/articlesupplier/helpers.js index 6d53cff..89db819 100644 --- a/src/store/articlesupplier/helpers.js +++ b/src/store/articlesupplier/helpers.js @@ -17,6 +17,7 @@ export function shapeArticleSupplier(row) { sku: row.sku ?? null, pu: row.pu, stock: row.stock ?? null, + sort: row.sort, priceId: row.priceId ?? null, price: row.priceId != null && row.priceNet != null diff --git a/src/store/articlesupplier/listByArticleSku.js b/src/store/articlesupplier/listByArticleSku.js index 551e6fa..cfac06a 100644 --- a/src/store/articlesupplier/listByArticleSku.js +++ b/src/store/articlesupplier/listByArticleSku.js @@ -10,7 +10,7 @@ export default function listByArticleSku(pool) { * * @param {number} googleAuthId - The id of the Google auth user. * @param {string} sku - The article SKU. - * @returns {Promise>} The supplier assignments. + * @returns {Promise>} The supplier assignments. * @throws {Error} If the article is not found or the user is not authorized. * * @example diff --git a/src/store/articlesupplier/order.js b/src/store/articlesupplier/order.js new file mode 100644 index 0000000..27e1fb4 --- /dev/null +++ b/src/store/articlesupplier/order.js @@ -0,0 +1,84 @@ +import { + LIST_IDS_BY_ARTICLE, + SET_TEMP_SORTS, + UPDATE_SORT, +} from '../../sql/articlesupplier.js'; +import request from '../request.js'; +import resolveArticleBySku from '../article/resolveArticleBySku.js'; + +export default function order(pool) { + const resolve = resolveArticleBySku(pool); + + /** + * Reorders the supplier assignments of an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {object} input - The reorder input. + * @param {Array} input.ids - The articlesupplier ids in their new order. + * @returns {Promise<{ids: Array}>} The applied order. + * @throws {Error} If the order is invalid. + * + * @example + * const result = await store.articlesupplier.order(42, 'SKU-1', { ids: [12, 10, 15] }); + * // => { ids: [12, 10, 15] } + */ + return async (googleAuthId, sku, { ids } = {}) => { + if (!Array.isArray(ids)) { + throw new Error('Invalid supplier order'); + } + + const { articleId } = await resolve(googleAuthId, sku); + + const current = await pool + .request() + .input('articleId', articleId) + .query(LIST_IDS_BY_ARTICLE); + + const existing = new Set(current.recordset.map((r) => r.id)); + + if (ids.length !== existing.size) { + throw new Error('Invalid supplier order'); + } + + const seen = new Set(); + for (const id of ids) { + if (!Number.isFinite(id) || !existing.has(id) || seen.has(id)) { + throw new Error('Invalid supplier order'); + } + seen.add(id); + } + + const tx = pool.transaction(); + await tx.begin(); + + try { + await request(pool, tx) + .input('articleId', articleId) + .query(SET_TEMP_SORTS); + + for (let sort = 0; sort < ids.length; sort++) { + const updateRes = await request(pool, tx) + .input('id', ids[sort]) + .input('articleId', articleId) + .input('sort', sort) + .query(UPDATE_SORT); + + if (!updateRes.rowsAffected[0]) { + throw new Error('Invalid supplier order'); + } + } + + await tx.commit(); + + return { ids }; + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +} diff --git a/src/store/articlesupplier/update.js b/src/store/articlesupplier/update.js index 0b1e991..e2447a4 100644 --- a/src/store/articlesupplier/update.js +++ b/src/store/articlesupplier/update.js @@ -29,10 +29,11 @@ export default function update(pool) { * @param {string|null} [patch.sku] - The supplier's item number/SKU. * @param {number} [patch.pu] - Packaging unit (> 0). * @param {number|null} [patch.stock] - Current stock at the supplier. + * @param {number} [patch.sort] - Display sort order. * @param {number|null} [patch.netPrice] - Net purchase price. * @param {number} [patch.taxId] - The tax id for the purchase price. * @param {number} [patch.taxRate] - The tax rate in percent. - * @returns {Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>} The updated assignment. + * @returns {Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>} The updated assignment. * @throws {Error} If input is invalid or supplier assignment is not found. * * @example @@ -41,7 +42,7 @@ export default function update(pool) { * pu: 6, * netPrice: 22.50, * }); - * // => { id, articleId, entityId, supplierName, sku, pu, stock, priceId, price } + * // => { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price } */ return async (googleAuthId, sku, id, patch = {}) => { if (id == null || !Number.isFinite(id)) { @@ -60,6 +61,12 @@ export default function update(pool) { } } + if (patch.sort !== undefined && patch.sort !== null) { + if (!Number.isFinite(patch.sort)) { + throw new Error('Invalid sort'); + } + } + if (patch.netPrice !== undefined && patch.netPrice !== null) { if (!Number.isFinite(patch.netPrice)) { throw new Error('Invalid price'); @@ -98,6 +105,7 @@ export default function update(pool) { .input('sku', existing.sku) .input('stock', existing.stock) .input('pu', existing.pu) + .input('sort', existing.sort) .input('priceId', null) .query(UPDATE_ARTICLE_SUPPLIER); @@ -168,6 +176,7 @@ export default function update(pool) { : patch.stock : existing.stock; const nextPu = patch.pu !== undefined ? patch.pu : existing.pu; + const nextSort = patch.sort !== undefined ? patch.sort : existing.sort; await request(pool, tx) .input('id', id) @@ -175,6 +184,7 @@ export default function update(pool) { .input('sku', nextSku) .input('stock', nextStock) .input('pu', nextPu) + .input('sort', nextSort) .input('priceId', priceId) .query(UPDATE_ARTICLE_SUPPLIER);