From 46c85fe29e17a40e26c0220d67abcf24b8c3619f Mon Sep 17 00:00:00 2001 From: seb Date: Wed, 5 Aug 2026 09:17:17 +0200 Subject: [PATCH] Add articlehistory on updateBySku and getHistoryBySku --- README.md | 6 ++- scripts/test-api.js | 17 ++++++++ src/index.js | 2 + src/sql/articlehistory.js | 11 ++++++ src/store/article/getHistoryBySku.js | 21 ++++++++++ src/store/article/updateBySku.js | 59 ++++++++++++++++++++++++++-- 6 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 src/sql/articlehistory.js create mode 100644 src/store/article/getHistoryBySku.js diff --git a/README.md b/README.md index 2f12410..b8179b6 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,7 @@ Company-scoped POS terminals. Auth via `resolveCompanyAccess`. | `listExport` | `(googleAuthId: string, companyId: number, categoryId?: number)` | `ArticleExportEntry[]` (see below) | | `createUnderParent` | `({ googleAuthId: string, parentId: number, name: string })` | `{ id: number, parentId: number, name: string, sku: string }` | | `getBySku` | `(googleAuthId: string, sku: string)` | `ArticleDetail` (see below) | +| `getHistoryBySku` | `(googleAuthId: string, sku: string)` | `{ id: number, date: Date, text: string }[]` | | `updateBySku` | `(googleAuthId: string, sku: string, { name?: string, unitamount?: number, unitSi?: string })` | `ArticleDetail` | | `setPricesBySku` | `(googleAuthId: string, sku: string, PriceInput[])` | `ArticleDetail` | | `setEansBySku` | `(googleAuthId: string, sku: string, string[])` | `ArticleDetail` | @@ -380,7 +381,9 @@ type PriceEntry = { Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Errors: `Article not found`, `Not authorized`. -`updateBySku` patches article fields in a transaction. At least one of `name`, `unitamount`, `unitSi` required. `name` → `articletext` (slugified `slug`); empty name → `Name required`. `unitSi` → existing `unit.si` lookup (no silent ensure); unknown → `Unit not found`. `unitamount` must be finite and `> 0` → `Invalid unitamount`. Returns `ArticleDetail`. Errors: `Article not found`, `Not authorized`, `Name required`, `Unit not found`, `Invalid unitamount`. +`updateBySku` patches article fields in a transaction. At least one of `name`, `unitamount`, `unitSi` required. `name` → `articletext` (slugified `slug`); empty name → `Name required`. `unitSi` → existing `unit.si` lookup (no silent ensure); unknown → `Unit not found`. `unitamount` must be finite and `> 0` → `Invalid unitamount`. When `name` and/or unit (`unitamount`/`unitSi`) actually change, inserts one `articlehistory` row in the same transaction (`text` lines joined by newline), e.g. `Name geändert von "Alt" auf "Neu"` / `Einheit geändert von "500g" auf "1kg"` (label = `unitamount` + `unittext.symbol`, fallback `unit.si`). Returns `ArticleDetail`. Errors: `Article not found`, `Not authorized`, `Name required`, `Unit not found`, `Invalid unitamount`. + +`getHistoryBySku` returns `articlehistory` for the article, newest first (`date` DESC, `id` DESC). Auth like `getBySku`. Errors: `Article not found`, `Not authorized`. `setPricesBySku` replaces all `price` rows for the article’s company `poscustomergroup_id` in one transaction. Each entry: `price` → `net`, `tax` → tax rate (ensured via `tax.ensure`), `minmultiplier` defaults to `1`. Empty array clears all POS prices. Returns `ArticleDetail`. Errors: `Article not found`, `Not authorized`, `POS customer group required`, `Invalid prices`. @@ -461,6 +464,7 @@ res.set('Content-Type', thumb.contentType).send(thumb.buffer); | `DELETE /api/posterminal/:id/categories` | `posterminal.setOffAll(googleAuthId, companyId, id)` | | `POST /api/article` | `article.createUnderParent({ googleAuthId, parentId, name })` | | `GET /api/article/:sku` | `article.getBySku(googleAuthId, sku)` | +| `GET /api/article/:sku/history` | `article.getHistoryBySku(googleAuthId, sku)` | | `GET /api/articleIndex` | `article.listSearchIndex(googleAuthId, companyId)` | | `GET /api/article/export.zip` | `article.listExport(googleAuthId, companyId, categoryId?)` | | `PATCH /api/article/:sku` | `article.updateBySku(googleAuthId, sku, { name?, unitamount?, unitSi? })` | diff --git a/scripts/test-api.js b/scripts/test-api.js index 309b858..b0425e8 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -183,6 +183,11 @@ const MENU = { label: 'getBySku', hint: 'googleAuthId, sku → row', }, + { + value: 'article.getHistoryBySku', + label: 'getHistoryBySku', + hint: 'googleAuthId, sku → history[]', + }, { value: 'article.updateBySku', label: 'updateBySku', @@ -779,6 +784,18 @@ async function runMethod(client, action) { await client.article.getBySku(Number(googleAuthId), sku), ]; } + case 'article.getHistoryBySku': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const sku = await askText('sku', { + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (sku == null) return; + return [ + 'result', + await client.article.getHistoryBySku(Number(googleAuthId), sku), + ]; + } case 'article.updateBySku': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; diff --git a/src/index.js b/src/index.js index 7c3c12a..ac45f4f 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,7 @@ import articleOrderImages from './store/article/orderImages.js'; import articleAddImage from './store/article/addImage.js'; import articleCreateUnderParent from './store/article/createUnderParent.js'; import articleGetBySku from './store/article/getBySku.js'; +import articleGetHistoryBySku from './store/article/getHistoryBySku.js'; import articleListSearchIndex from './store/article/listSearchIndex.js'; import articleListExport from './store/article/listExport.js'; import articleUpdateBySku from './store/article/updateBySku.js'; @@ -97,6 +98,7 @@ export default class LibDashStore { this.article = { createUnderParent: articleCreateUnderParent(pool), getBySku: articleGetBySku(pool), + getHistoryBySku: articleGetHistoryBySku(pool), listSearchIndex: articleListSearchIndex(pool), listExport: articleListExport(pool), updateBySku: articleUpdateBySku(pool), diff --git a/src/sql/articlehistory.js b/src/sql/articlehistory.js new file mode 100644 index 0000000..0c2cd6a --- /dev/null +++ b/src/sql/articlehistory.js @@ -0,0 +1,11 @@ +export const INSERT = ` +INSERT INTO articlehistory (article_id, [text]) +VALUES (@articleId, @text) +`; + +export const LIST_BY_ARTICLE_ID = ` +SELECT id, [date], [text] +FROM articlehistory +WHERE article_id = @articleId +ORDER BY [date] DESC, id DESC +`; diff --git a/src/store/article/getHistoryBySku.js b/src/store/article/getHistoryBySku.js new file mode 100644 index 0000000..190df71 --- /dev/null +++ b/src/store/article/getHistoryBySku.js @@ -0,0 +1,21 @@ +import { LIST_BY_ARTICLE_ID } from '../../sql/articlehistory.js'; +import resolveArticleBySku from './resolveArticleBySku.js'; + +export default function getHistoryBySku(pool) { + const resolve = resolveArticleBySku(pool); + + return async (googleAuthId, sku) => { + const { articleId } = await resolve(googleAuthId, sku); + + const result = await pool + .request() + .input('articleId', articleId) + .query(LIST_BY_ARTICLE_ID); + + return result.recordset.map((row) => ({ + id: row.id, + date: row.date, + text: row.text, + })); + }; +} diff --git a/src/store/article/updateBySku.js b/src/store/article/updateBySku.js index ecd3bd6..7f8de51 100644 --- a/src/store/article/updateBySku.js +++ b/src/store/article/updateBySku.js @@ -3,13 +3,23 @@ import { UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, } from '../../sql/article.js'; +import { INSERT as INSERT_HISTORY } from '../../sql/articlehistory.js'; import { UPDATE_BY_ARTICLE_AND_LANGUAGE } from '../../sql/articletext.js'; -import { GET_BY_SI } from '../../sql/unit.js'; +import { GET_DETAIL_BY_ARTICLE_ID } from '../../sql/articleDetail.js'; +import { GET_FULL_BY_SI } from '../../sql/unit.js'; import request from '../request.js'; import queryBySku from './queryBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js'; import slugify from '../slugify.js'; +function formatUnitLabel(amount, symbol) { + const sym = symbol ?? ''; + if (amount == null || !Number.isFinite(Number(amount))) { + return sym; + } + return `${Number(amount)}${sym}`; +} + export default function updateBySku(pool) { const resolve = resolveArticleBySku(pool); const load = queryBySku(pool); @@ -33,7 +43,18 @@ export default function updateBySku(pool) { const { articleId, languageId, companyId } = await resolve(googleAuthId, sku); + const currentResult = await pool + .request() + .input('articleId', articleId) + .input('languageId', languageId) + .query(GET_DETAIL_BY_ARTICLE_ID); + const current = currentResult.recordset[0]; + if (!current) { + throw new Error('Article not found'); + } + let unitId; + let newUnitSymbol = current.unitSymbol ?? current.unitSi ?? ''; if (hasUnitSi) { if (!patch.unitSi?.trim()) { throw new Error('Unit not found'); @@ -42,11 +63,36 @@ export default function updateBySku(pool) { .request() .input('companyId', companyId) .input('si', patch.unitSi.trim()) - .query(GET_BY_SI); - unitId = unit.recordset[0]?.id; + .input('languageId', languageId) + .query(GET_FULL_BY_SI); + const unitRow = unit.recordset[0]; + unitId = unitRow?.id; if (!unitId) { throw new Error('Unit not found'); } + newUnitSymbol = unitRow.symbol ?? unitRow.si ?? ''; + } + + const historyLines = []; + if (hasName) { + const oldName = current.name ?? ''; + const newName = patch.name.trim(); + if (oldName !== newName) { + historyLines.push(`Name geändert von "${oldName}" auf "${newName}"`); + } + } + if (hasUnitSi || hasUnitamount) { + const oldLabel = formatUnitLabel( + current.unitamount, + current.unitSymbol ?? current.unitSi ?? '', + ); + const newLabel = formatUnitLabel( + hasUnitamount ? patch.unitamount : current.unitamount, + newUnitSymbol, + ); + if (oldLabel !== newLabel) { + historyLines.push(`Einheit geändert von "${oldLabel}" auf "${newLabel}"`); + } } const tx = pool.transaction(); @@ -85,6 +131,13 @@ export default function updateBySku(pool) { .query(UPDATE_UNITAMOUNT); } + if (historyLines.length) { + await request(pool, tx) + .input('articleId', articleId) + .input('text', historyLines.join('\n')) + .query(INSERT_HISTORY); + } + await tx.commit(); } catch (err) { try {