From a20668cd610d87990318c6c284cbae11357a2c02 Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 10 Oct 2026 18:17:14 +0200 Subject: [PATCH] u --- package-lock.json | 27 +- package.json | 1 + src/api/attributename.js | 10 + src/api/attributevalue.js | 10 + src/api/auth.js | 6 + src/api/brand.js | 12 + src/api/category.js | 22 ++ src/api/company.js | 30 ++- src/api/customergroup.js | 10 + src/api/delivery.js | 14 ++ src/api/entity.js | 28 +++ src/api/globalHistory.js | 2 + src/api/labeltemplate.js | 10 + src/api/ledger.js | 8 + src/api/modelconversation.js | 12 + src/api/number.js | 4 + src/api/order.js | 10 + src/api/posterminal.js | 16 ++ src/api/printer.js | 12 + src/api/propertyname.js | 10 + src/api/quote.js | 10 + src/api/setting.js | 6 + src/api/stock.js | 2 + src/api/supplier.js | 10 + src/api/thumb.js | 2 + src/api/unit.js | 6 + src/api/vfs.js | 16 ++ src/api/warehouse.js | 12 + src/components/PageMinimap.jsx | 2 - .../settings/ImportExportSettingsPanel.jsx | 234 +++++++++++++----- src/components/settings/SkeleHashImage.jsx | 94 +++++++ 31 files changed, 572 insertions(+), 76 deletions(-) create mode 100644 src/components/settings/SkeleHashImage.jsx diff --git a/package-lock.json b/package-lock.json index 0bd0660..6c657d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,12 +29,13 @@ "react-router-dom": "^7.18.4", "react-virtual-aggridautomat": "file:../react-virtual-aggridautomat", "react-window": "^2.3.3", + "skelehash-wasm": "^0.1.0", "sortablejs": "^1.15.7", "thumbhash": "^0.1.1" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@vitejs/plugin-react": "6.1.1", + "@vitejs/plugin-react": "6.1.2", "eslint": "^10.12.0", "eslint-plugin-react-hooks": "^7.1.1", "globals": "^17.13.0", @@ -3795,16 +3796,6 @@ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "license": "MIT" }, - "node_modules/@types/react": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", - "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.2.2" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.12", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", @@ -3829,9 +3820,9 @@ "license": "MIT" }, "node_modules/@vitejs/plugin-react": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", - "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", + "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", "dev": true, "license": "MIT", "dependencies": { @@ -3843,7 +3834,7 @@ "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", - "oxc-transform-react": "^0.145.0", + "oxc-transform-react": "^0.152.0", "vite": "^8.0.0" }, "peerDependenciesMeta": { @@ -8154,6 +8145,12 @@ "simple-concat": "^1.0.0" } }, + "node_modules/skelehash-wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/skelehash-wasm/-/skelehash-wasm-0.1.0.tgz", + "integrity": "sha512-QdxF9YsJvKlT93O70dXFaWnsKVX7KDD0RgYf9cwgOT6kGnSMOdsO0rsdge+5Tg+vv6BjivbJ/2o5P1KcQsavEw==", + "license": "MIT" + }, "node_modules/smob": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", diff --git a/package.json b/package.json index 0713222..a5eeac9 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "react-router-dom": "^7.18.4", "react-virtual-aggridautomat": "file:../react-virtual-aggridautomat", "react-window": "^2.3.3", + "skelehash-wasm": "^0.1.0", "sortablejs": "^1.15.7", "thumbhash": "^0.1.1" }, diff --git a/src/api/attributename.js b/src/api/attributename.js index 9489169..7b15fd4 100644 --- a/src/api/attributename.js +++ b/src/api/attributename.js @@ -2,27 +2,37 @@ import { http } from './http.js'; export const attributename = { /** GET /api/attributename → { attributenames: Attributename[] } */ + // #region GET /api/attributename — list list() { return http('/api/attributename'); }, + // #endregion /** POST /api/attributename → 201 { attributename } */ + // #region POST /api/attributename — create create(body) { return http('/api/attributename', { method: 'POST', body }); }, + // #endregion /** PATCH /api/attributename/:id → 200 { attributename } */ + // #region PATCH /api/attributename/:id — update update(id, body) { return http(`/api/attributename/${encodeURIComponent(id)}`, { method: 'PATCH', body }); }, + // #endregion /** DELETE /api/attributename/:id → 200 */ + // #region DELETE /api/attributename/:id — delete delete(id) { return http(`/api/attributename/${encodeURIComponent(id)}`, { method: 'DELETE' }); }, + // #endregion /** PUT /api/attributename/order → 200 */ + // #region PUT /api/attributename/order — order order(attributenameIds) { return http('/api/attributename/order', { method: 'PUT', body: { attributenameIds } }); }, + // #endregion }; diff --git a/src/api/attributevalue.js b/src/api/attributevalue.js index d93bc2d..bc529cc 100644 --- a/src/api/attributevalue.js +++ b/src/api/attributevalue.js @@ -2,33 +2,43 @@ import { http } from './http.js'; export const attributevalue = { /** GET /api/attributename/:attributeId/value → { values: AttributeValue[] } */ + // #region GET /api/attributename/:attributeId/value — listByAttributeId listByAttributeId(attributeId) { return http(`/api/attributename/${encodeURIComponent(attributeId)}/value`); }, + // #endregion /** POST /api/attributename/:attributeId/value → 201 { attributevalue } */ + // #region POST /api/attributename/:attributeId/value — create create(attributeId, body) { return http(`/api/attributename/${encodeURIComponent(attributeId)}/value`, { method: 'POST', body, }); }, + // #endregion /** PATCH /api/attributevalue/:id → 200 { attributevalue } */ + // #region PATCH /api/attributevalue/:id — update update(id, body) { return http(`/api/attributevalue/${encodeURIComponent(id)}`, { method: 'PATCH', body }); }, + // #endregion /** DELETE /api/attributevalue/:id → 200 */ + // #region DELETE /api/attributevalue/:id — delete delete(id) { return http(`/api/attributevalue/${encodeURIComponent(id)}`, { method: 'DELETE' }); }, + // #endregion /** PUT /api/attributename/:attributeId/value/order → 200 */ + // #region PUT /api/attributename/:attributeId/value/order — order order(attributeId, attributevalueIds) { return http(`/api/attributename/${encodeURIComponent(attributeId)}/value/order`, { method: 'PUT', body: { attributevalueIds }, }); }, + // #endregion }; diff --git a/src/api/auth.js b/src/api/auth.js index 187e4ef..94f4307 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -1,6 +1,7 @@ import { http, logApi } from './http.js'; export const auth = { + // #region GET /api/auth/me — me async me() { try { const data = await http('/api/auth/me'); @@ -20,7 +21,9 @@ export const auth = { throw err; } }, + // #endregion + // #region POST /api/auth/login — loginWithGoogle async loginWithGoogle(idToken) { logApi('loginWithGoogle', { idTokenLength: idToken?.length ?? 0, @@ -41,7 +44,9 @@ export const auth = { }); return { user: data.user, session }; }, + // #endregion + // #region POST /api/auth/logout — logout async logout() { try { await http('/api/auth/logout', { method: 'POST' }); @@ -49,4 +54,5 @@ export const auth = { if (err.status !== 401) throw err; } }, + // #endregion }; diff --git a/src/api/brand.js b/src/api/brand.js index 29e7ead..3f5eaf2 100644 --- a/src/api/brand.js +++ b/src/api/brand.js @@ -9,6 +9,7 @@ export const brand = { * GET /api/brand → { brands: Brand[], total, offset, limit } * @param {{ offset?: number, limit?: number, includeSmallThumbs?: boolean }} [params] */ + // #region GET /api/brand — list list(params) { const offset = Number(params?.offset ?? 0); const limit = Number(params?.limit ?? 0); @@ -20,6 +21,7 @@ export const brand = { const qs = query.toString(); return http(qs ? `/api/brand?${qs}` : '/api/brand'); }, + // #endregion /** * POST /api/brand @@ -27,32 +29,39 @@ export const brand = { * Empty name ⇒ 400 Invalid brand. * → 201 { brand: Brand } · 409 Brand already exists */ + // #region POST /api/brand — create create(body) { return http('/api/brand', { method: 'POST', body }); }, + // #endregion /** * PATCH /api/brand/:id * Body: { name, gsprpinfo? } — rename / update info. * → 200 { id, name, gsprpinfo, touchedArticleCount } · 404 Brand not found */ + // #region PATCH /api/brand/:id — rename rename(id, body) { return http(`/api/brand/${encodeURIComponent(id)}`, { method: 'PATCH', body }); }, + // #endregion /** * DELETE /api/brand/:id * → 204 No Content · 404 Brand not found */ + // #region DELETE /api/brand/:id — delete delete(id) { return http(`/api/brand/${encodeURIComponent(id)}`, { method: 'DELETE' }); }, + // #endregion /** * POST /api/brand/:id/image * Accepts full-size image body (Content-Type: image/webp). * → 201 { brand: { id, fullHash, thumbW, thumbH, thumbHash, pics } } */ + // #region POST /api/brand/:id/image — addImage addImage(brandId, file) { return httpBinary(`/api/brand/${brandId}/image`, { method: 'POST', @@ -60,12 +69,15 @@ export const brand = { body: file, }); }, + // #endregion /** * DELETE /api/brand/:id/image * → 200 { brand: { id, thumbW: null, thumbH: null, thumbHash: null, pics: [] } } */ + // #region DELETE /api/brand/:id/image — deleteImage deleteImage(brandId) { return http(`/api/brand/${brandId}/image`, { method: 'DELETE' }); }, + // #endregion }; diff --git a/src/api/category.js b/src/api/category.js index 1ad7db5..57c0fcb 100644 --- a/src/api/category.js +++ b/src/api/category.js @@ -2,34 +2,42 @@ import { http, httpBinary, httpGetBinary } from './http.js'; export const category = { /** GET /api/category/tree → { languageId, tree, articles } (company from session) */ + // #region GET /api/category/tree — getTree getTree() { return http('/api/category/tree'); }, + // #endregion /** * GET /api/category/:id/articles → { articles: [...] } * Article rows: sku, name, net, taxRate, fullHash, thumbW, thumbH, thumbHash, childCount, … */ + // #region GET /api/category/:id/articles — getArticles getArticles(categoryId) { return http(`/api/category/${categoryId}/articles`); }, + // #endregion /** POST /api/category → 201 { category } */ + // #region POST /api/category — create create({ parentId, name }) { return http('/api/category', { method: 'POST', body: { parentId: Number(parentId), name }, }); }, + // #endregion /** * DELETE /api/category/:id → 204 * Query: ?recursive=true (cascades deletion of subcategories and articles) */ + // #region DELETE /api/category/:id — delete delete(categoryId, { recursive = false } = {}) { const qs = recursive ? '?recursive=true' : ''; return http(`/api/category/${categoryId}${qs}`, { method: 'DELETE' }); }, + // #endregion /** * PATCH /api/category/:id @@ -37,12 +45,14 @@ export const category = { * Empty name → 400 name is required. * → 200 { category: { id, name, slug } } */ + // #region PATCH /api/category/:id — updateName updateName(categoryId, name) { return http(`/api/category/${categoryId}`, { method: 'PATCH', body: { name }, }); }, + // #endregion /** * PUT /api/category/order @@ -51,24 +61,28 @@ export const category = { * set of parentId (same count, no foreign/duplicate ids) → 400 Invalid category order. * → 200 { categoryIds: number[] } */ + // #region PUT /api/category/order — reorder reorder(parentId, categoryIds) { return http('/api/category/order', { method: 'PUT', body: { parentId, categoryIds }, }); }, + // #endregion /** * PATCH /api/category/:id * Body: { parentId } — moves the category under a new parent. * → 200 { category: { id, parentId, sort } } (kein name/slug im Response) */ + // #region PATCH /api/category/:id — move move(categoryId, parentId) { return http(`/api/category/${categoryId}`, { method: 'PATCH', body: { parentId }, }); }, + // #endregion /** * PATCH /api/category/:id @@ -77,18 +91,21 @@ export const category = { * * PROPOSED — backend needs to implement this field. */ + // #region PATCH /api/category/:id — updatePos updatePos(categoryId, pos) { return http(`/api/category/${categoryId}`, { method: 'PATCH', body: { pos }, }); }, + // #endregion /** * POST /api/category/:id/image * Body: raw image bytes (Content-Type: image/webp) * → 201 { category: { id, fullHash, thumbW, thumbH, thumbHash } } */ + // #region POST /api/category/:id/image — uploadImage uploadImage(categoryId, file) { return httpBinary(`/api/category/${categoryId}/image`, { method: 'POST', @@ -96,14 +113,17 @@ export const category = { body: file, }); }, + // #endregion /** * DELETE /api/category/:id/image * → 200 { category: { id, thumbW: null, thumbH: null, thumbHash: null } } */ + // #region DELETE /api/category/:id/image — deleteImage deleteImage(categoryId) { return http(`/api/category/${categoryId}/image`, { method: 'DELETE' }); }, + // #endregion /** * GET /api/category/export.zip → application/zip (binary download) @@ -112,7 +132,9 @@ export const category = { * entry per category image (content-addressed, deduplicated). * Errors: 400 No company selected, 401, 403, 404 (JSON { error }). */ + // #region GET /api/category/export.zip — exportZip exportZip() { return httpGetBinary('/api/category/export.zip'); }, + // #endregion }; diff --git a/src/api/company.js b/src/api/company.js index 85318e9..e107f85 100644 --- a/src/api/company.js +++ b/src/api/company.js @@ -2,42 +2,52 @@ import { http } from './http.js'; export const company = { /** GET /api/company → { companies: [...] } */ + // #region GET /api/company — list list() { return http('/api/company'); }, + // #endregion /** Uses GET /api/company until a dedicated endpoint exists. */ + // #region GET /api/company — getById async getById(companyId) { const data = await http('/api/company'); const found = data.companies?.find((item) => item.id === companyId); if (!found) throw new Error('Unternehmen nicht gefunden.'); return { company: found }; }, + // #endregion + // #region POST /api/company — create create({ name, iso639, taxrate, reducedtaxrate }) { return http('/api/company', { method: 'POST', body: { name, iso639, taxrate, reducedtaxrate }, }); }, + // #endregion /** * PUT /api/company/:companyId * Body: { name, iso639, taxrate, reducedtaxrate } */ + // #region PUT /api/company/:companyId — update update(companyId, { name, iso639, taxrate, reducedtaxrate }) { return http(`/api/company/${companyId}`, { method: 'PUT', body: { name, iso639, taxrate, reducedtaxrate }, }); }, + // #endregion + // #region PUT /api/auth/selected-company — select select(companyId) { return http('/api/auth/selected-company', { method: 'PUT', body: { companyId }, }); }, + // #endregion /** * POST /api/company/import @@ -48,12 +58,14 @@ export const company = { * @param {object} [callbacks] * @param {(progress: { loaded: number, total: number, percent: number }) => void} [callbacks.onUploadProgress] * @param {(message: string) => void} [callbacks.onStatus] - * @param {(progress: { phase: string, current: number, total: number, item?: string }) => void} [callbacks.onProgress] + * @param {(progress: { categoriestotal: number, categoriesdone: number, articlestotal: number, articlesdone: number, imagestotal: number, imagesdone: number, receiptstotal: number, receiptsdone: number }) => void} [callbacks.onProgress] Counter-based progress snapshot across all phases. + * @param {(images: string[]) => void} [callbacks.onImages] Batched `images` events (~every 100ms) carrying Base62 SkeleHash strings. * @param {(stats: object) => void} [callbacks.onDone] * @param {(error: string) => void} [callbacks.onError] * @param {object} [options] * @param {AbortSignal} [options.signal] */ + // #region POST /api/company/import — importCatalog importCatalog(file, callbacks = {}, { signal } = {}) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); @@ -122,6 +134,21 @@ export const company = { case 'progress': callbacks.onProgress?.(eventData); break; + case 'images': { + // Payload is a plain array of Base62 SkeleHash strings; + // also tolerate the { images: [...] } wrapper shape. + const list = Array.isArray(eventData) + ? eventData + : Array.isArray(eventData?.images) + ? eventData.images + : []; + if (list.length) callbacks.onImages?.(list); + break; + } + case 'image': + // Legacy: single image event (pre-batching). + if (eventData) callbacks.onImages?.([eventData]); + break; case 'done': callbacks.onDone?.(eventData?.stats ?? eventData); break; @@ -165,4 +192,5 @@ export const company = { xhr.send(formData); }); }, + // #endregion }; diff --git a/src/api/customergroup.js b/src/api/customergroup.js index 0eb8008..866abbc 100644 --- a/src/api/customergroup.js +++ b/src/api/customergroup.js @@ -8,9 +8,11 @@ import { http } from './http.js'; */ export const customergroup = { /** GET /api/customergroup → { customergroups: (CustomerGroup & { editable })[] } ordered by sort */ + // #region GET /api/customergroup — list list() { return http('/api/customergroup'); }, + // #endregion /** * POST /api/customergroup @@ -18,18 +20,22 @@ export const customergroup = { * reserved POS / Lieferanten ⇒ 400 Name not allowed; sort appends next index. * → 201 { customergroup: CustomerGroup } */ + // #region POST /api/customergroup — create create(body) { return http('/api/customergroup', { method: 'POST', body }); }, + // #endregion /** * PATCH /api/customergroup/:id * Body: { name } — locked groups cannot be renamed (Name cannot be changed). * → 200 { customergroup: CustomerGroup } */ + // #region PATCH /api/customergroup/:id — update update(id, body) { return http(`/api/customergroup/${id}`, { method: 'PATCH', body }); }, + // #endregion /** * PUT /api/customergroup/order @@ -37,12 +43,16 @@ export const customergroup = { * else 400 Invalid customer group order. Sets sort to each id’s index 0…n-1. * → 200 { customergroupIds: number[] } */ + // #region PUT /api/customergroup/order — order order(body) { return http('/api/customergroup/order', { method: 'PUT', body }); }, + // #endregion /** DELETE /api/customergroup/:id → 204 · 409 Customer group in use */ + // #region DELETE /api/customergroup/:id — delete delete(id) { return http(`/api/customergroup/${id}`, { method: 'DELETE' }); }, + // #endregion }; diff --git a/src/api/delivery.js b/src/api/delivery.js index a117fa6..d57c3dc 100644 --- a/src/api/delivery.js +++ b/src/api/delivery.js @@ -11,14 +11,18 @@ import { http, clientIdHeaders } from './http.js'; */ export const delivery = { /** GET /api/delivery → { deliveries: DeliverySummary[] } */ + // #region GET /api/delivery — list list() { return http('/api/delivery'); }, + // #endregion /** GET /api/delivery/:id → { delivery: Delivery } */ + // #region GET /api/delivery/:id — get get(id) { return http(`/api/delivery/${id}`); }, + // #endregion /** * POST /api/delivery @@ -28,9 +32,11 @@ export const delivery = { * Line requires sku, amount, net, tax. rebate→0, transactedAmount→amount. * → 201 { delivery: Delivery } */ + // #region POST /api/delivery — create create(body) { return http('/api/delivery', { method: 'POST', body }); }, + // #endregion /** * PATCH /api/delivery/:id @@ -40,19 +46,24 @@ export const delivery = { * If lines present, replaces all lines; if omitted, lines unchanged. * → 200 { delivery: Delivery } */ + // #region PATCH /api/delivery/:id — update update(id, body) { return http(`/api/delivery/${id}`, { method: 'PATCH', body }); }, + // #endregion /** DELETE /api/delivery/:id → 204 */ + // #region DELETE /api/delivery/:id — delete delete(id) { return http(`/api/delivery/${id}`, { method: 'DELETE' }); }, + // #endregion /** * GET /api/delivery/:id/file/:fileId → Blob * Full binary content of attached file (PDF, text, etc.) */ + // #region GET /api/delivery/:id/file/:fileId — getFile async getFile(id, fileId, { download = false } = {}) { const query = download ? '?download=true' : ''; const res = await fetch(`/api/delivery/${encodeURIComponent(id)}/file/${encodeURIComponent(fileId)}${query}`, { @@ -71,12 +82,14 @@ export const delivery = { } return res.blob(); }, + // #endregion /** * POST /api/delivery/parse — multipart/form-data, field "file" * Accepts PDF, TXT or CSV document (max 50 MB) * Streams SSE events: status, llm_call, tool_call, usage, progress, result, error */ + // #region POST /api/delivery/parse — parseSse async *parseSse(file, { signal } = {}) { const formData = new FormData(); formData.append('file', file, file.name); @@ -148,4 +161,5 @@ export const delivery = { } catch { /* ignore */ } } }, + // #endregion }; diff --git a/src/api/entity.js b/src/api/entity.js index 4e0f11f..978f505 100644 --- a/src/api/entity.js +++ b/src/api/entity.js @@ -9,6 +9,7 @@ export const entity = { * GET /api/entity → { entities: Entity[] } * Optional query: customergroupId */ + // #region GET /api/entity — list list({ customergroupId } = {}) { const qs = customergroupId != null @@ -16,83 +17,110 @@ export const entity = { : ''; return http(`/api/entity${qs}`); }, + // #endregion /** GET /api/entity/:id → { entity: Entity } */ + // #region GET /api/entity/:id — get get(id) { return http(`/api/entity/${id}`); }, + // #endregion /** * POST /api/entity * Body: { customergroupId, name, firstName? } * → 201 { entity: Entity } */ + // #region POST /api/entity — create create(body) { return http('/api/entity', { method: 'POST', body }); }, + // #endregion /** * PATCH /api/entity/:id * Body: { name?, firstName? } * → 200 { entity: Entity } */ + // #region PATCH /api/entity/:id — update update(id, body) { return http(`/api/entity/${id}`, { method: 'PATCH', body }); }, + // #endregion /** DELETE /api/entity/:id → 204 */ + // #region DELETE /api/entity/:id — delete delete(id) { return http(`/api/entity/${id}`, { method: 'DELETE' }); }, + // #endregion /** GET /api/entity/next-number → { number: string } */ + // #region GET /api/entity/next-number — nextNumber nextNumber() { return http('/api/entity/next-number'); }, + // #endregion /** GET /api/entity/:entityId/address → { addresses: EntityAddress[] } */ + // #region GET /api/entity/:entityId/address — listAddresses listAddresses(entityId) { return http(`/api/entity/${entityId}/address`); }, + // #endregion /** POST /api/entity/:entityId/address → 201 { address } */ + // #region POST /api/entity/:entityId/address — createAddress createAddress(entityId, body) { return http(`/api/entity/${entityId}/address`, { method: 'POST', body }); }, + // #endregion /** PATCH /api/entity/:entityId/address/:id → { address } */ + // #region PATCH /api/entity/:entityId/address/:id — updateAddress updateAddress(entityId, id, body) { return http(`/api/entity/${entityId}/address/${id}`, { method: 'PATCH', body, }); }, + // #endregion /** DELETE /api/entity/:entityId/address/:id → 204 */ + // #region DELETE /api/entity/:entityId/address/:id — deleteAddress deleteAddress(entityId, id) { return http(`/api/entity/${entityId}/address/${id}`, { method: 'DELETE' }); }, + // #endregion /** GET /api/entity/:entityId/contact → { contacts: EntityContact[] } */ + // #region GET /api/entity/:entityId/contact — listContacts listContacts(entityId) { return http(`/api/entity/${entityId}/contact`); }, + // #endregion /** POST /api/entity/:entityId/contact → 201 { contact } */ + // #region POST /api/entity/:entityId/contact — createContact createContact(entityId, body) { return http(`/api/entity/${entityId}/contact`, { method: 'POST', body }); }, + // #endregion /** PATCH /api/entity/:entityId/contact/:id → { contact } */ + // #region PATCH /api/entity/:entityId/contact/:id — updateContact updateContact(entityId, id, body) { return http(`/api/entity/${entityId}/contact/${id}`, { method: 'PATCH', body, }); }, + // #endregion /** DELETE /api/entity/:entityId/contact/:id → 204 */ + // #region DELETE /api/entity/:entityId/contact/:id — deleteContact deleteContact(entityId, id) { return http(`/api/entity/${entityId}/contact/${id}`, { method: 'DELETE' }); }, + // #endregion }; diff --git a/src/api/globalHistory.js b/src/api/globalHistory.js index b648b21..d1fdf3b 100644 --- a/src/api/globalHistory.js +++ b/src/api/globalHistory.js @@ -5,6 +5,7 @@ export const globalHistory = { * GET /api/history * Global system/entity history with paging/sorting/filtering */ + // #region GET /api/history — list list({ startRow, endRow, offset, limit, sortModel, filterModel } = {}) { const params = new URLSearchParams(); if (startRow != null) params.set('startRow', String(startRow)); @@ -16,4 +17,5 @@ export const globalHistory = { const qs = params.toString(); return http(`/api/history${qs ? `?${qs}` : ''}`); }, + // #endregion }; diff --git a/src/api/labeltemplate.js b/src/api/labeltemplate.js index bb1fcfe..b400029 100644 --- a/src/api/labeltemplate.js +++ b/src/api/labeltemplate.js @@ -6,37 +6,47 @@ import { http } from './http.js'; */ export const labeltemplate = { /** GET /api/labeltemplate → { labeltemplates: LabelTemplate[] } */ + // #region GET /api/labeltemplate — list list() { return http('/api/labeltemplate'); }, + // #endregion /** GET /api/labeltemplate/:id → { labeltemplate: LabelTemplate } */ + // #region GET /api/labeltemplate/:id — get get(id) { return http(`/api/labeltemplate/${encodeURIComponent(id)}`); }, + // #endregion /** * POST /api/labeltemplate * Body: { name, width, height, dpi, type, content } * → 201 { labeltemplate: LabelTemplate } */ + // #region POST /api/labeltemplate — create create(body) { return http('/api/labeltemplate', { method: 'POST', body }); }, + // #endregion /** * PATCH /api/labeltemplate/:id * Body: partial { name?, width?, height?, dpi?, type?, content? } * → 200 { labeltemplate: LabelTemplate } */ + // #region PATCH /api/labeltemplate/:id — update update(id, body) { return http(`/api/labeltemplate/${encodeURIComponent(id)}`, { method: 'PATCH', body }); }, + // #endregion /** DELETE /api/labeltemplate/:id → 204 */ + // #region DELETE /api/labeltemplate/:id — delete delete(id) { return http(`/api/labeltemplate/${encodeURIComponent(id)}`, { method: 'DELETE' }); }, + // #endregion // thumb is returned inline as a base64 string in list/get responses }; diff --git a/src/api/ledger.js b/src/api/ledger.js index ffce3da..d9ccd63 100644 --- a/src/api/ledger.js +++ b/src/api/ledger.js @@ -11,35 +11,42 @@ export const ledger = { * Body: { manifestarticle_id, transaction_type, amount, transaction_text } * → 201 { ledger: LedgerEntry } */ + // #region POST /api/ledger — add add(body) { return http('/api/ledger', { method: 'POST', body }); }, + // #endregion /** * GET /api/ledger/manifestarticle/:manifestarticleId * Query: ?transaction_type=... * → 200 { ledger: LedgerEntry[] } */ + // #region GET /api/ledger/manifestarticle/:manifestarticleId — listByManifestarticleId listByManifestarticleId(manifestarticleId, transactionType) { const query = transactionType ? `?transaction_type=${encodeURIComponent(transactionType)}` : ''; return http(`/api/ledger/manifestarticle/${encodeURIComponent(manifestarticleId)}${query}`); }, + // #endregion /** * GET /api/ledger/sku/:sku * Query: ?transaction_type=... * → 200 { ledger: LedgerEntry[] } */ + // #region GET /api/ledger/sku/:sku — listBySku listBySku(sku, transactionType) { const query = transactionType ? `?transaction_type=${encodeURIComponent(transactionType)}` : ''; return http(`/api/ledger/sku/${encodeURIComponent(sku)}${query}`); }, + // #endregion /** * GET /api/ledger * Query: { manifestarticle_id?, sku?, transaction_type? } * → 200 { ledger: LedgerEntry[] } */ + // #region GET /api/ledger — list list(query) { const params = new URLSearchParams(); if (query?.manifestarticle_id != null) params.set('manifestarticle_id', query.manifestarticle_id); @@ -50,4 +57,5 @@ export const ledger = { const qs = params.toString(); return http(`/api/ledger${qs ? `?${qs}` : ''}`); }, + // #endregion }; diff --git a/src/api/modelconversation.js b/src/api/modelconversation.js index f3c21f4..e4f7da7 100644 --- a/src/api/modelconversation.js +++ b/src/api/modelconversation.js @@ -6,18 +6,22 @@ export const modelconversation = { * Lists the user's model conversations in the selected company. * → 200 { conversations: [{ id, companyuser_id, provider, model, created_at, updated_at, state }] } */ + // #region GET /api/modelconversation — list list() { return http('/api/modelconversation'); }, + // #endregion /** * GET /api/modelconversation/:id * Fetch conversation details by ID. * @param {number|string} id */ + // #region GET /api/modelconversation/:id — get get(id) { return http(`/api/modelconversation/${encodeURIComponent(id)}`); }, + // #endregion /** * POST /api/modelconversation @@ -25,27 +29,35 @@ export const modelconversation = { * Body: { provider, model } * → 201 { conversation: { id, companyuser_id, provider, model, created_at, updated_at, state } } */ + // #region POST /api/modelconversation — create create(body) { return http('/api/modelconversation', { method: 'POST', body }); }, + // #endregion /** * GET /api/modelconversation/:id/message * Lists conversation messages, oldest first. * → 200 { messages: [{ id, conversation_id, created_at, role, message }] } */ + // #region GET /api/modelconversation/:id/message — listMessages listMessages(id) { return http(`/api/modelconversation/${encodeURIComponent(id)}/message`); }, + // #endregion + // #region POST /api/modelconversation/:id/message — addMessage addMessage(id, body) { return http(`/api/modelconversation/${encodeURIComponent(id)}/message`, { method: 'POST', body }); }, + // #endregion /** * DELETE /api/modelconversation/:id */ + // #region DELETE /api/modelconversation/:id — delete delete(id) { return http(`/api/modelconversation/${encodeURIComponent(id)}`, { method: 'DELETE' }); }, + // #endregion }; diff --git a/src/api/number.js b/src/api/number.js index 0c7d555..edb6bc3 100644 --- a/src/api/number.js +++ b/src/api/number.js @@ -5,16 +5,20 @@ export const number = { * GET /api/number → { numbers: [{ id, type, prefix, padding, next }] } * Lists the number sequences for the selected company. */ + // #region GET /api/number — list list() { return http('/api/number'); }, + // #endregion /** * PATCH /api/number/:id * Body: at least one of { prefix, padding, next } — only present keys are patched. * → 200 { number: { id, type, prefix, padding, next } } */ + // #region PATCH /api/number/:id — update update(id, body) { return http(`/api/number/${id}`, { method: 'PATCH', body }); }, + // #endregion }; diff --git a/src/api/order.js b/src/api/order.js index 97a4f1c..2bf565c 100644 --- a/src/api/order.js +++ b/src/api/order.js @@ -11,6 +11,7 @@ export const order = { * @param {{ offset?: number, limit?: number }} [params] * → { orders: Order[], total: number, offset: number, limit: number } */ + // #region GET /api/order — list list({ offset = 0, limit = 20 } = {}) { const params = new URLSearchParams(); if (offset != null) params.set('offset', String(offset)); @@ -18,32 +19,41 @@ export const order = { const qs = params.toString(); return http(`/api/order${qs ? `?${qs}` : ''}`); }, + // #endregion /** GET /api/order/:id → { order: Order } */ + // #region GET /api/order/:id — get get(id) { return http(`/api/order/${id}`); }, + // #endregion /** * POST /api/order * Body: { number, date, entityId?, lines } * → 201 { order: Order } */ + // #region POST /api/order — create create(body) { return http('/api/order', { method: 'POST', body }); }, + // #endregion /** * PATCH /api/order/:id * Body: partial { number?, date?, entityId?, lines? } * → 200 { order: Order } */ + // #region PATCH /api/order/:id — update update(id, body) { return http(`/api/order/${id}`, { method: 'PATCH', body }); }, + // #endregion /** DELETE /api/order/:id → 204 */ + // #region DELETE /api/order/:id — delete delete(id) { return http(`/api/order/${id}`, { method: 'DELETE' }); }, + // #endregion }; diff --git a/src/api/posterminal.js b/src/api/posterminal.js index c289701..0aa4f00 100644 --- a/src/api/posterminal.js +++ b/src/api/posterminal.js @@ -5,9 +5,11 @@ export const posterminal = { * GET /api/posterminal → { posterminals: [{ id, name, token, pairingpin }] } * List all terminals for the selected company. */ + // #region GET /api/posterminal — list list() { return http('/api/posterminal'); }, + // #endregion /** * PUT /api/posterminal @@ -15,12 +17,14 @@ export const posterminal = { * Creates a new terminal (auto-generates 6-digit pin). * → 200 { posterminal: { id, name, token, pairingpin } } */ + // #region PUT /api/posterminal — create create({ name }) { return http('/api/posterminal', { method: 'PUT', body: { name }, }); }, + // #endregion /** * PATCH /api/posterminal/:id @@ -28,64 +32,76 @@ export const posterminal = { * Update posterminal configuration. * → 200 { posterminal } */ + // #region PATCH /api/posterminal/:id — update update(terminalId, patch) { return http(`/api/posterminal/${terminalId}`, { method: 'PATCH', body: patch, }); }, + // #endregion /** * DELETE /api/posterminal/:id → 204 * Deletes a terminal. */ + // #region DELETE /api/posterminal/:id — delete delete(terminalId) { return http(`/api/posterminal/${terminalId}`, { method: 'DELETE', }); }, + // #endregion /** * PUT /api/posterminal/:id/category/:categoryId * Enable a category on a terminal. * → 200 { id: number, categoryIds: number[] } */ + // #region PUT /api/posterminal/:id/category/:categoryId — enableCategory enableCategory(terminalId, categoryId) { return http(`/api/posterminal/${terminalId}/category/${categoryId}`, { method: 'PUT', }); }, + // #endregion /** * DELETE /api/posterminal/:id/category/:categoryId * Disable a category on a terminal. * → 200 { id: number, categoryIds: number[] } */ + // #region DELETE /api/posterminal/:id/category/:categoryId — disableCategory disableCategory(terminalId, categoryId) { return http(`/api/posterminal/${terminalId}/category/${categoryId}`, { method: 'DELETE', }); }, + // #endregion /** * PUT /api/posterminal/:id/all * Enable all categories on a terminal. * → 200 { id: number, categoryIds: number[] } */ + // #region PUT /api/posterminal/:id/all — enableAllCategories enableAllCategories(terminalId) { return http(`/api/posterminal/${terminalId}/all`, { method: 'PUT', }); }, + // #endregion /** * DELETE /api/posterminal/:id/all * Disable all categories on a terminal. * → 200 { id: number, categoryIds: number[] } */ + // #region DELETE /api/posterminal/:id/all — disableAllCategories disableAllCategories(terminalId) { return http(`/api/posterminal/${terminalId}/all`, { method: 'DELETE', }); }, + // #endregion }; diff --git a/src/api/printer.js b/src/api/printer.js index 1fbf20d..aced695 100644 --- a/src/api/printer.js +++ b/src/api/printer.js @@ -6,44 +6,56 @@ import { http } from './http.js'; */ export const printer = { /** GET /api/printer → { printers: Printer[] } */ + // #region GET /api/printer — list list() { return http('/api/printer'); }, + // #endregion /** GET /api/printer/:id → { printer: Printer } */ + // #region GET /api/printer/:id — getById getById(id) { return http(`/api/printer/${id}`); }, + // #endregion /** * POST /api/printer * Body: { address, type, model, serial, sort, desc } * → 201 { printer: Printer } */ + // #region POST /api/printer — create create(body) { return http('/api/printer', { method: 'POST', body }); }, + // #endregion /** * PATCH /api/printer/:id * Body: partial printer fields * → 200 { printer: Printer } */ + // #region PATCH /api/printer/:id — update update(id, body) { return http(`/api/printer/${id}`, { method: 'PATCH', body }); }, + // #endregion /** DELETE /api/printer/:id → 204 */ + // #region DELETE /api/printer/:id — delete delete(id) { return http(`/api/printer/${id}`, { method: 'DELETE' }); }, + // #endregion /** * PUT /api/printer/order * Body: { printerIds: number[] } * → 200 { printerIds: number[] } */ + // #region PUT /api/printer/order — order order({ printerIds }) { return http('/api/printer/order', { method: 'PUT', body: { printerIds } }); }, + // #endregion }; diff --git a/src/api/propertyname.js b/src/api/propertyname.js index 8c6d947..2cabb15 100644 --- a/src/api/propertyname.js +++ b/src/api/propertyname.js @@ -2,27 +2,37 @@ import { http } from './http.js'; export const propertyname = { /** GET /api/propertyname → { propertynames: Propertyname[] } */ + // #region GET /api/propertyname — list list() { return http('/api/propertyname'); }, + // #endregion /** POST /api/propertyname → 201 { propertyname } */ + // #region POST /api/propertyname — create create(body) { return http('/api/propertyname', { method: 'POST', body }); }, + // #endregion /** PATCH /api/propertyname/:id → 200 { propertyname } */ + // #region PATCH /api/propertyname/:id — update update(id, body) { return http(`/api/propertyname/${encodeURIComponent(id)}`, { method: 'PATCH', body }); }, + // #endregion /** DELETE /api/propertyname/:id → 200 */ + // #region DELETE /api/propertyname/:id — delete delete(id) { return http(`/api/propertyname/${encodeURIComponent(id)}`, { method: 'DELETE' }); }, + // #endregion /** PUT /api/propertyname/order → 200 */ + // #region PUT /api/propertyname/order — order order(propertynameIds) { return http('/api/propertyname/order', { method: 'PUT', body: { propertynameIds } }); }, + // #endregion }; diff --git a/src/api/quote.js b/src/api/quote.js index ec9c4cb..e8eb5a7 100644 --- a/src/api/quote.js +++ b/src/api/quote.js @@ -8,35 +8,45 @@ import { http } from './http.js'; */ export const quote = { /** GET /api/quote → { quotes: QuoteSummary[] } */ + // #region GET /api/quote — list list() { return http('/api/quote'); }, + // #endregion /** GET /api/quote/:id → { quote: Quote } */ + // #region GET /api/quote/:id — get get(id) { return http(`/api/quote/${id}`); }, + // #endregion /** * POST /api/quote * Body: { number, date, entityId?, lines } * → 201 { quote: Quote } */ + // #region POST /api/quote — create create(body) { return http('/api/quote', { method: 'POST', body }); }, + // #endregion /** * PATCH /api/quote/:id * Body: partial { number?, date?, entityId?, lines? } * → 200 { quote: Quote } */ + // #region PATCH /api/quote/:id — update update(id, body) { return http(`/api/quote/${id}`, { method: 'PATCH', body }); }, + // #endregion /** DELETE /api/quote/:id → 204 */ + // #region DELETE /api/quote/:id — delete delete(id) { return http(`/api/quote/${id}`, { method: 'DELETE' }); }, + // #endregion }; diff --git a/src/api/setting.js b/src/api/setting.js index 3d9a54c..e7c686e 100644 --- a/src/api/setting.js +++ b/src/api/setting.js @@ -9,24 +9,30 @@ export const setting = { * GET /api/company/setting * Optional query: { frontendOnly } → 200 { settings: Setting[] } */ + // #region GET /api/company/setting — list list({ frontendOnly } = {}) { const qs = frontendOnly ? '?frontendOnly=1' : ''; return http(`/api/company/setting${qs}`); }, + // #endregion /** GET /api/company/setting/:key → { setting: Setting } */ + // #region GET /api/company/setting/:key — get get(key) { return http(`/api/company/setting/${encodeURIComponent(key)}`); }, + // #endregion /** * PUT /api/company/setting/:key * Body: { value, frontend } → 200 { setting: Setting } */ + // #region PUT /api/company/setting/:key — upsert upsert(key, body) { return http(`/api/company/setting/${encodeURIComponent(key)}`, { method: 'PUT', body, }); }, + // #endregion }; diff --git a/src/api/stock.js b/src/api/stock.js index 8c0df6b..e43bf81 100644 --- a/src/api/stock.js +++ b/src/api/stock.js @@ -8,7 +8,9 @@ export const stock = { * POST /api/stock/adjust * Body: { sku, warehouseId, amount, text?, date?, externalNumber?, lines? } */ + // #region POST /api/stock/adjust — adjust adjust(body) { return http('/api/stock/adjust', { method: 'POST', body }); }, + // #endregion }; diff --git a/src/api/supplier.js b/src/api/supplier.js index 717b094..15155b2 100644 --- a/src/api/supplier.js +++ b/src/api/supplier.js @@ -6,37 +6,46 @@ import { http } from './http.js'; */ export const supplier = { /** GET /api/supplier → { suppliers: Supplier[] } */ + // #region GET /api/supplier — list list() { return http('/api/supplier'); }, + // #endregion /** * POST /api/supplier * Body: { name } — empty name ⇒ 400 Name required. * → 201 { supplier: Supplier } */ + // #region POST /api/supplier — create create(body) { return http('/api/supplier', { method: 'POST', body }); }, + // #endregion /** * PATCH /api/supplier/:id * Body: { name } — empty name ⇒ 400 Name required. * → 200 { supplier: Supplier } */ + // #region PATCH /api/supplier/:id — update update(id, body) { return http(`/api/supplier/${id}`, { method: 'PATCH', body }); }, + // #endregion /** DELETE /api/supplier/:id → 204 */ + // #region DELETE /api/supplier/:id — delete delete(id) { return http(`/api/supplier/${id}`, { method: 'DELETE' }); }, + // #endregion /** * GET /api/supplier/:id/articles?skus=... * → 200 { articles: SupplierArticle[] } */ + // #region GET /api/supplier/:id/articles — getArticles getArticles(id, skus) { let query = ''; if (skus != null) { @@ -48,4 +57,5 @@ export const supplier = { } return http(`/api/supplier/${encodeURIComponent(id)}/articles${query}`); }, + // #endregion }; diff --git a/src/api/thumb.js b/src/api/thumb.js index 2f5d68b..b14d864 100644 --- a/src/api/thumb.js +++ b/src/api/thumb.js @@ -7,7 +7,9 @@ export const thumb = { * GET /api/thumb/:fullHash → image/avif (thumb bytes) * Use as (same-origin; cookies sent). */ + // #region GET /api/thumb/:fullHash — url url(fullHash) { return `/api/thumb/${encodeURIComponent(fullHash)}`; }, + // #endregion }; diff --git a/src/api/unit.js b/src/api/unit.js index 5fa2df2..a181bfc 100644 --- a/src/api/unit.js +++ b/src/api/unit.js @@ -6,25 +6,31 @@ import { http } from './http.js'; */ export const unit = { /** GET /api/unit → { units: [...] } · 400 No company selected */ + // #region GET /api/unit — list list() { return http('/api/unit'); }, + // #endregion /** * PUT /api/unit * Body: { si, basesi, basesifactor, symbol, name } * → 200 { unit } */ + // #region PUT /api/unit — upsert upsert(body) { return http('/api/unit', { method: 'PUT', body }); }, + // #endregion /** * DELETE /api/unit?si=… → 204 * 409 Unit in use */ + // #region DELETE /api/unit?si=… — delete delete(si) { const params = new URLSearchParams({ si }); return http(`/api/unit?${params}`, { method: 'DELETE' }); }, + // #endregion }; diff --git a/src/api/vfs.js b/src/api/vfs.js index c62956e..e562694 100644 --- a/src/api/vfs.js +++ b/src/api/vfs.js @@ -6,15 +6,18 @@ export const vfs = { * List files in the virtual filesystem for the selected company. * → 200 { files: [{ name, size, mime, updated_at, created_at, isText, isImage, isPdf }] } */ + // #region GET /api/vfs — list list() { return http('/api/vfs'); }, + // #endregion /** * GET /api/vfs/*path * Fetch raw content of a file (e.g. text for the editor). * @param {string} path */ + // #region GET /api/vfs/*path — getText async getText(path) { const clean = String(path || '').replace(/^\/+/, ''); const res = await fetch(`/api/vfs/${clean}?raw=1`, { @@ -25,6 +28,7 @@ export const vfs = { } return res.text(); }, + // #endregion /** * PUT /api/vfs/*path @@ -33,6 +37,7 @@ export const vfs = { * @param {string} content * @param {string} [mime] */ + // #region PUT /api/vfs/*path — save save(path, content, mime) { const clean = String(path || '').replace(/^\/+/, ''); return http(`/api/vfs/${clean}`, { @@ -40,18 +45,21 @@ export const vfs = { body: { content, mime }, }); }, + // #endregion /** * DELETE /api/vfs/*path * Delete a file. * @param {string} path */ + // #region DELETE /api/vfs/*path — delete delete(path) { const clean = String(path || '').replace(/^\/+/, ''); return http(`/api/vfs/${clean}`, { method: 'DELETE', }); }, + // #endregion /** * POST /api/vfs/upload @@ -59,6 +67,7 @@ export const vfs = { * @param {File[]|FileList|File} files * @param {string} [targetDir] */ + // #region POST /api/vfs/upload — upload async upload(files, targetDir = '') { const formData = new FormData(); if (targetDir) formData.append('targetDir', targetDir); @@ -78,12 +87,14 @@ export const vfs = { } return res.json(); }, + // #endregion /** * POST /api/vfs/mkdir * Create a directory in VFS. * @param {string} path */ + // #region POST /api/vfs/mkdir — mkdir mkdir(path) { const clean = String(path || '').replace(/^\/+/, ''); return http('/api/vfs/mkdir', { @@ -91,6 +102,7 @@ export const vfs = { body: { path: clean }, }); }, + // #endregion /** * POST /api/vfs/rename @@ -98,6 +110,7 @@ export const vfs = { * @param {string} oldPath * @param {string} newPath */ + // #region POST /api/vfs/rename — rename rename(oldPath, newPath) { const cleanOld = String(oldPath || '').replace(/^\/+/, ''); const cleanNew = String(newPath || '').replace(/^\/+/, ''); @@ -106,6 +119,7 @@ export const vfs = { body: { oldPath: cleanOld, newPath: cleanNew }, }); }, + // #endregion /** * POST /api/vfs/*path — run a `.applet.js` file from VFS. @@ -116,6 +130,7 @@ export const vfs = { * `result` is a typed CSV string (`;`-separated, header row first), `columns` are the * declared `{ name, type }` pairs, `rows` is the data-row count. */ + // #region POST /api/vfs/*path — runApplet runApplet(path, options = {}) { const clean = String(path || '').replace(/^\/+/, ''); const body = {}; @@ -127,4 +142,5 @@ export const vfs = { body, }); }, + // #endregion }; diff --git a/src/api/warehouse.js b/src/api/warehouse.js index 25181b5..79bdc17 100644 --- a/src/api/warehouse.js +++ b/src/api/warehouse.js @@ -6,44 +6,56 @@ import { http } from './http.js'; */ export const warehouse = { /** GET /api/warehouse → { warehouses: Warehouse[] } */ + // #region GET /api/warehouse — list list() { return http('/api/warehouse'); }, + // #endregion /** GET /api/warehouse/:id → { warehouse: Warehouse } */ + // #region GET /api/warehouse/:id — getById getById(id) { return http(`/api/warehouse/${id}`); }, + // #endregion /** * POST /api/warehouse * Body: { name, sort? } — empty name ⇒ 400 Name required. * → 201 { warehouse: Warehouse } */ + // #region POST /api/warehouse — create create(body) { return http('/api/warehouse', { method: 'POST', body }); }, + // #endregion /** * PATCH /api/warehouse/:id * Body: { name?, sort? } * → 200 { warehouse: Warehouse } */ + // #region PATCH /api/warehouse/:id — update update(id, body) { return http(`/api/warehouse/${id}`, { method: 'PATCH', body }); }, + // #endregion /** * PUT /api/warehouse/order * Body: { warehouseIds: number[] } * → 200 { warehouseIds: number[] } */ + // #region PUT /api/warehouse/order — order order(body) { return http('/api/warehouse/order', { method: 'PUT', body }); }, + // #endregion /** DELETE /api/warehouse/:id → 204 */ + // #region DELETE /api/warehouse/:id — delete delete(id) { return http(`/api/warehouse/${id}`, { method: 'DELETE' }); }, + // #endregion }; diff --git a/src/components/PageMinimap.jsx b/src/components/PageMinimap.jsx index ec181f1..17d853c 100644 --- a/src/components/PageMinimap.jsx +++ b/src/components/PageMinimap.jsx @@ -2,8 +2,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { matchPath, useLocation } from 'react-router-dom'; import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; -import Tooltip from '@mui/material/Tooltip'; -import Typography from '@mui/material/Typography'; import MapRoundedIcon from '@mui/icons-material/MapRounded'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import { ARTICLE_TAB, readPreferredArticleTab } from './article/articleRoute'; diff --git a/src/components/settings/ImportExportSettingsPanel.jsx b/src/components/settings/ImportExportSettingsPanel.jsx index a2326c8..e1b6374 100644 --- a/src/components/settings/ImportExportSettingsPanel.jsx +++ b/src/components/settings/ImportExportSettingsPanel.jsx @@ -24,6 +24,11 @@ import categoryAutomat from '../../automats/categoryAutomat'; import articleIndexAutomat from '../../automats/articleIndexAutomat'; import brandAutomat from '../../automats/brandAutomat'; import { orderAutomat } from '../../automats/orderAutomat'; +import SkeleHashImage from './SkeleHashImage'; + +// Keep only the most recent N image events in state to avoid unbounded growth +// on large imports (thousands of images). +const MAX_STREAMED_IMAGES = 60; function formatBytes(bytes) { if (bytes == null || isNaN(bytes) || bytes === 0) return '0 B'; @@ -42,14 +47,18 @@ function isZipFile(file) { ); } -const PHASE_LABELS = { - brands: 'Marken werden importiert…', - categories: 'Kategorien werden importiert…', - articles: 'Artikel werden abgeglichen…', - variants: 'Varianten werden verknüpft…', - images: 'Bilder werden optimiert…', - orders: 'Aufträge werden importiert…', -}; +function shortHash(hash) { + if (typeof hash !== 'string') return ''; + return hash.length > 10 ? `${hash.slice(0, 8)}…` : hash; +} + +// Counter-based phases emitted by the `progress` event. +const PROGRESS_PHASES = [ + { key: 'categories', label: 'Kategorien', totalKey: 'categoriestotal', doneKey: 'categoriesdone' }, + { key: 'articles', label: 'Artikel', totalKey: 'articlestotal', doneKey: 'articlesdone' }, + { key: 'images', label: 'Medien', totalKey: 'imagestotal', doneKey: 'imagesdone' }, + { key: 'receipts', label: 'Belege', totalKey: 'receiptstotal', doneKey: 'receiptsdone' }, +]; const EXPORT_OPTIONS = [ { @@ -94,10 +103,10 @@ export default class ImportExportSettingsPanel extends Component { uploadProgress: 0, uploadLoaded: 0, uploadTotal: 0, - importPhase: null, // 'categories' | 'articles' | 'variants' | 'images' | null - importPhaseCurrent: 0, - importPhaseTotal: 0, - importItem: null, + importProgress: null, // raw counters from the `progress` event + // Most recent SkeleHashes from the stream ({ id, hash }) + importedImages: [], + importedImageCount: 0, statusMessage: null, importStats: null, uploadError: null, @@ -243,16 +252,16 @@ export default class ImportExportSettingsPanel extends Component { this.uploadAbortController = new AbortController(); + this.imageSeq = 0; this.setState({ uploading: true, uploadStage: 'upload', uploadProgress: 0, uploadLoaded: 0, uploadTotal: selectedFile.size, - importPhase: null, - importPhaseCurrent: 0, - importPhaseTotal: 0, - importItem: null, + importProgress: null, + importedImages: [], + importedImageCount: 0, statusMessage: null, importStats: null, uploadError: null, @@ -278,12 +287,31 @@ export default class ImportExportSettingsPanel extends Component { }); }, onProgress: (progress) => { - this.setState({ - uploadStage: 'server', - importPhase: progress?.phase ?? null, - importPhaseCurrent: progress?.current ?? 0, - importPhaseTotal: progress?.total ?? 0, - importItem: progress?.item ?? null, + if (!progress || typeof progress !== 'object') return; + // Counter-based protocol: { categoriestotal, categoriesdone, ... } + this.setState({ uploadStage: 'server', importProgress: progress }); + }, + onImage: (images) => { + const list = Array.isArray(images) ? images.filter(Boolean) : []; + if (!list.length) return; + this.setState((prev) => { + let seq = this.imageSeq ?? 0; + const batch = list.map((item) => { + seq += 1; + return { + id: seq, + hash: typeof item === 'string' ? item : (item?.skelehash ?? null), + }; + }); + this.imageSeq = seq; + const next = [...batch.reverse(), ...prev.importedImages]; + if (next.length > MAX_STREAMED_IMAGES) next.length = MAX_STREAMED_IMAGES; + return { + uploadStage: 'server', + importedImages: next, + importedImageCount: prev.importedImageCount + list.length, + statusMessage: null, + }; }); }, onDone: (stats) => { @@ -293,7 +321,7 @@ export default class ImportExportSettingsPanel extends Component { importStats: stats, uploadSuccess: 'Import erfolgreich abgeschlossen.', statusMessage: null, - importItem: null, + importProgress: null, selectedFile: null, }); if (this.fileInputRef.current) this.fileInputRef.current.value = ''; @@ -354,20 +382,30 @@ export default class ImportExportSettingsPanel extends Component { uploadProgress, uploadLoaded, uploadTotal, - importPhase, - importPhaseCurrent, - importPhaseTotal, - importItem, + importProgress, + importedImages, + importedImageCount, statusMessage, importStats, uploadError, uploadSuccess, } = this.state; - const progressPercent = - importPhaseTotal > 0 - ? Math.min(100, Math.round((importPhaseCurrent / importPhaseTotal) * 100)) - : null; + const phaseRows = PROGRESS_PHASES.map((phase) => { + const total = importProgress?.[phase.totalKey] ?? 0; + const done = importProgress?.[phase.doneKey] ?? 0; + return { + ...phase, + total, + done, + percent: total > 0 ? Math.min(100, Math.round((done / total) * 100)) : 0, + }; + }).filter((row) => row.total > 0); + + const totalUnits = phaseRows.reduce((sum, row) => sum + row.total, 0); + const doneUnits = phaseRows.reduce((sum, row) => sum + Math.min(row.done, row.total), 0); + const overallPercent = + totalUnits > 0 ? Math.min(100, Math.round((doneUnits / totalUnits) * 100)) : null; return ( @@ -750,7 +788,8 @@ export default class ImportExportSettingsPanel extends Component { <> - {(importPhase && PHASE_LABELS[importPhase]) || statusMessage || 'Verarbeitung läuft…'} + {statusMessage || 'Daten werden verarbeitet…'} + {overallPercent != null ? ` · ${overallPercent}%` : ''}