This commit is contained in:
seb
2026-10-23 22:40:28 +02:00
parent b9bd0380cb
commit 5dd45916b8
8 changed files with 1175 additions and 573 deletions

View File

@@ -257,8 +257,8 @@ _none_
- Params: `sku: str, body: { eans: str[] }`
- Returns: `204 void`
#### Get article history by SKU — `GET /article/{sku}/history`
- Params: `sku: str, query: limit?: int`
- Returns: `200 { history: { id: int, date: str, text: str }[], truncated: bool }`
- Params: `sku: str, query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { history: { id: int, date: str, text: str }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Add article image — `POST /article/{sku}/image`
- Params: `sku: str, image/avif`
- Returns: `201 { image: { picId: int, sort: int, full: { w: int, h: int, size: int }, thumb: { w: int, h: int }, hash: str, thumbHash: str } }`

570
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -33,8 +33,6 @@
"@mui/material": "^9.5.0",
"@shopify/draggable": "^1.2.1",
"@uiw/react-codemirror": "^4.25.12",
"ag-grid-community": "^36.2.0",
"ag-grid-react": "^36.2.0",
"dashgui": "file:../dashgui",
"flexsearch": "^0.8.212",
"jsbarcode": "^3.12.3",
@@ -45,9 +43,8 @@
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor",
"react-dom": "^19.3.1",
"react-router-dom": "^7.18.4",
"react-virtual-aggridautomat": "file:../react-virtual-aggridautomat",
"react-window": "^2.3.4",
"skelehash-wasm": "^0.1.0",
"skelehash-wasm": "file:../skelehash-wasm",
"sortablejs": "^1.15.7",
"thumbhash": "^0.1.1"
},

View File

@@ -50,11 +50,19 @@ export const article = {
// #endregion
/**
* GET /api/article/:sku/history
* GET /api/article/:sku/history?offset=0&limit=50&search=...&seekMonths=-1
* @param {string} sku
* @param {{ offset?: number, limit?: number, search?: string, seekMonths?: number }} [params]
* → { history: ArticleHistoryEntry[], total: number, unfilteredTotal?: number, offset: number, limit: number }
*/
getBySkuHistory(sku, { limit } = {}) {
getBySkuHistory(sku, { offset = 0, limit = 50, search, seekMonths } = {}) {
const params = new URLSearchParams();
if (limit != null) params.set('limit', String(limit));
if (offset != null) params.set('offset', String(Math.max(0, Math.floor(Number(offset) || 0))));
if (limit != null) params.set('limit', String(Math.max(1, Math.floor(Number(limit) || 50))));
if (search && String(search).trim()) params.set('search', String(search).trim());
if (seekMonths != null && Number(seekMonths) < 0) {
params.set('seekMonths', String(Math.floor(Number(seekMonths))));
}
const qs = params.toString();
return http(`/api/article/${encodeURIComponent(sku)}/history${qs ? `?${qs}` : ''}`);
},

View File

@@ -34,26 +34,72 @@ import { requireArray, requireObject } from '../utils/apiShape';
* @property {number|null} taxRate Tax rate in percent.
*/
import { createChunkListAutomat } from './createChunkListAutomat';
import authAutomat from './authAutomat';
/**
* Per-SKU article change history (`GET /api/article/{sku}/history`).
*
* The backend returns the full list; pagination happens client-side in
* `ArticleHistoryTab`. Keyed by SKU so several tabs can watch the same article.
*
* @type {KeyedAutomat<Array<ArticleHistoryEntry>>}
* Registry of SKU-specific chunk-list automats for article history.
* @type {Map<string, import('react-automat').Automat>}
*/
export const articleHistoryAutomat = new KeyedAutomat(
null,
{},
{
name: 'articleHistory',
loader: async (sku) => {
const res = await articleApi.getBySkuHistory(String(sku));
requireObject(res, 'GET /api/article/:sku/history');
return requireArray(res.history, 'history');
const skuHistoryAutomats = new Map();
/**
* Gets or creates the chunk-list automat for a specific article's history.
*
* @param {string|number} sku Article SKU.
* @returns {import('react-automat').Automat} Configured chunk-list automat for the SKU.
*/
export function getArticleHistoryAutomat(sku) {
const key = String(sku);
let inst = skuHistoryAutomats.get(key);
if (!inst) {
inst = createChunkListAutomat({
name: `articleHistory:${key}`,
chunkSize: 50,
maxLruChunks: 50,
fetchPage: (params) => articleApi.getBySkuHistory(key, params),
});
skuHistoryAutomats.set(key, inst);
}
return inst;
}
/**
* Per-SKU article change history manager (`GET /api/article/{sku}/history`).
*
* Backed by chunk-list automats (`createChunkListAutomat`) keyed by SKU with
* multi-filter memory caching, LRU eviction, and infinite-scrolling / FillSpace support.
*/
export const articleHistoryAutomat = {
get: getArticleHistoryAutomat,
key: getArticleHistoryAutomat,
has(sku) {
return skuHistoryAutomats.has(String(sku));
},
async load(sku) {
const inst = getArticleHistoryAutomat(sku);
return inst.getItems();
},
);
async reload(sku) {
const inst = skuHistoryAutomats.get(String(sku));
if (inst) {
await inst.actions.reload();
}
},
reset() {
for (const inst of skuHistoryAutomats.values()) {
inst.actions.reset();
}
skuHistoryAutomats.clear();
},
};
// Reset on logout or company change
authAutomat.subscribe((auth) => {
if (auth.status === 'anon') {
articleHistoryAutomat.reset();
}
});
/**
* Suppliers assigned to an article (`GET /api/article/{sku}/supplier`).

View File

@@ -117,7 +117,7 @@ export { createListAutomat, selectListItems, selectListStatus } from './createLi
/** Per-SKU article detail views keyed by SKU. */
export { articleHistoryAutomat, articleSupplierAutomat } from './articleDetailAutomat';
export { articleHistoryAutomat, getArticleHistoryAutomat, articleSupplierAutomat } from './articleDetailAutomat';
/** Single shared GET /api/events connection. */
export { default as sseAutomat, invalidateArticle } from './sseAutomat';

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,3 @@
@import 'ag-grid-community/styles/ag-grid.css';
@import 'ag-grid-community/styles/ag-theme-alpine.css';
/* App background. Provided as a CSS custom property by src/colors.js
(installCssVars), with a hard-coded fallback for the very first paint. */
html,