feat(article): implement articlesupplier editor tab
- ApiClient: add getSuppliers, addSupplier, updateSupplier, orderSuppliers, deleteSupplier on article namespace - api/index.js: expose same methods in legacy http client - StorageManager: add #supplierWatchers Map and watchArticleSuppliers() backed by article-<sku> SSE signal; wired up as article.supplierWatch - ArticleTileTab: pass article prop to ArticleSupplierTab - ArticleSupplierDialog (new): dialog to assign / edit a supplier link (supplier autocomplete, Lief.-SKU, VPE, stock, EK Netto + MwSt.) - ArticleSupplierTab: full supplier management table with reordering (up/down arrows → PUT /supplier/order), inline edit and delete, stock badge, purchase price display and empty state
This commit is contained in:
@@ -316,6 +316,20 @@ export class ApiClient {
|
||||
/** PUT /api/article/{sku}/categories */
|
||||
setCategories: (sku, body) =>
|
||||
this.#json('PUT', `/api/article/${enc(sku)}/categories`, { body }),
|
||||
/** GET /api/article/{sku}/supplier */
|
||||
getSuppliers: (sku) => this.#json('GET', `/api/article/${enc(sku)}/supplier`),
|
||||
/** POST /api/article/{sku}/supplier */
|
||||
addSupplier: (sku, body) =>
|
||||
this.#json('POST', `/api/article/${enc(sku)}/supplier`, { body }),
|
||||
/** PATCH /api/article/{sku}/supplier/{id} */
|
||||
updateSupplier: (sku, id, body) =>
|
||||
this.#json('PATCH', `/api/article/${enc(sku)}/supplier/${enc(id)}`, { body }),
|
||||
/** PUT /api/article/{sku}/supplier/order */
|
||||
orderSuppliers: (sku, body) =>
|
||||
this.#json('PUT', `/api/article/${enc(sku)}/supplier/order`, { body }),
|
||||
/** DELETE /api/article/{sku}/supplier/{id} */
|
||||
deleteSupplier: (sku, id) =>
|
||||
this.#empty('DELETE', `/api/article/${enc(sku)}/supplier/${enc(id)}`),
|
||||
};
|
||||
|
||||
this.posterminal = {
|
||||
|
||||
@@ -80,11 +80,15 @@ export class StorageManager extends ApiClient {
|
||||
#articleWatchers = new Map();
|
||||
/** @type {Map<string, Set<() => void>>} sku → history refreshers */
|
||||
#historyWatchers = new Map();
|
||||
/** @type {Map<string, Set<() => void>>} sku → supplier refreshers */
|
||||
#supplierWatchers = new Map();
|
||||
|
||||
constructor(baseUrl = window.location.origin) {
|
||||
super(baseUrl);
|
||||
this.#initCaches();
|
||||
this.article.watch = (sku, onArticle) => this.watchArticle(sku, onArticle);
|
||||
this.article.supplierWatch = (sku, onSuppliers) =>
|
||||
this.watchArticleSuppliers(sku, onSuppliers);
|
||||
}
|
||||
|
||||
/** Settled article detail for sku, or undefined. */
|
||||
@@ -124,7 +128,10 @@ export class StorageManager extends ApiClient {
|
||||
*/
|
||||
watchArticle(sku, onArticle) {
|
||||
const key = String(sku);
|
||||
const needListen = !this.#articleWatchers.has(key) && !this.#historyWatchers.has(key);
|
||||
const needListen =
|
||||
!this.#articleWatchers.has(key) &&
|
||||
!this.#historyWatchers.has(key) &&
|
||||
!this.#supplierWatchers.has(key);
|
||||
|
||||
let watchers = this.#articleWatchers.get(key);
|
||||
if (!watchers) {
|
||||
@@ -144,7 +151,10 @@ export class StorageManager extends ApiClient {
|
||||
set.delete(onArticle);
|
||||
if (set.size === 0) {
|
||||
this.#articleWatchers.delete(key);
|
||||
if (!this.#historyWatchers.has(key)) {
|
||||
if (
|
||||
!this.#historyWatchers.has(key) &&
|
||||
!this.#supplierWatchers.has(key)
|
||||
) {
|
||||
this.#events?.removeEventListener(`article-${key}`, this.#onArticleSignal);
|
||||
}
|
||||
}
|
||||
@@ -214,7 +224,10 @@ export class StorageManager extends ApiClient {
|
||||
notifyError(err);
|
||||
});
|
||||
|
||||
const needListen = !this.#articleWatchers.has(key) && !this.#historyWatchers.has(key);
|
||||
const needListen =
|
||||
!this.#articleWatchers.has(key) &&
|
||||
!this.#historyWatchers.has(key) &&
|
||||
!this.#supplierWatchers.has(key);
|
||||
|
||||
let watchers = this.#historyWatchers.get(key);
|
||||
if (!watchers) {
|
||||
@@ -236,7 +249,10 @@ export class StorageManager extends ApiClient {
|
||||
set.delete(run);
|
||||
if (set.size === 0) {
|
||||
this.#historyWatchers.delete(key);
|
||||
if (!this.#articleWatchers.has(key)) {
|
||||
if (
|
||||
!this.#articleWatchers.has(key) &&
|
||||
!this.#supplierWatchers.has(key)
|
||||
) {
|
||||
this.#events?.removeEventListener(`article-${key}`, this.#onArticleSignal);
|
||||
}
|
||||
}
|
||||
@@ -249,6 +265,68 @@ export class StorageManager extends ApiClient {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load suppliers for an article and refetch on SSE `article-<sku>`.
|
||||
* @param {string} sku
|
||||
* @param {(suppliers: Array<object>) => void} onSuppliers
|
||||
* @returns {{ unwatch: () => void, refresh: () => Promise<void> }}
|
||||
*/
|
||||
watchArticleSuppliers(sku, onSuppliers) {
|
||||
const key = String(sku);
|
||||
let active = true;
|
||||
|
||||
const refresh = async () => {
|
||||
const res = await this.article.getSuppliers(key);
|
||||
if (!active) return;
|
||||
requireObject(res, 'GET /api/article/{sku}/supplier');
|
||||
const list = requireArray(res.suppliers, 'suppliers');
|
||||
onSuppliers(list);
|
||||
};
|
||||
|
||||
const run = () => {
|
||||
void refresh().catch((err) => {
|
||||
if (!active) return;
|
||||
notifyError(err);
|
||||
});
|
||||
};
|
||||
|
||||
const needListen =
|
||||
!this.#articleWatchers.has(key) &&
|
||||
!this.#historyWatchers.has(key) &&
|
||||
!this.#supplierWatchers.has(key);
|
||||
|
||||
let watchers = this.#supplierWatchers.get(key);
|
||||
if (!watchers) {
|
||||
watchers = new Set();
|
||||
this.#supplierWatchers.set(key, watchers);
|
||||
}
|
||||
watchers.add(run);
|
||||
if (needListen) {
|
||||
this.#events?.addEventListener(`article-${key}`, this.#onArticleSignal);
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
return {
|
||||
refresh,
|
||||
unwatch: () => {
|
||||
active = false;
|
||||
const set = this.#supplierWatchers.get(key);
|
||||
if (!set) return;
|
||||
set.delete(run);
|
||||
if (set.size === 0) {
|
||||
this.#supplierWatchers.delete(key);
|
||||
if (
|
||||
!this.#articleWatchers.has(key) &&
|
||||
!this.#historyWatchers.has(key)
|
||||
) {
|
||||
this.#events?.removeEventListener(`article-${key}`, this.#onArticleSignal);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#onArticleSignal = (event) => {
|
||||
const sku = event.type.slice('article-'.length);
|
||||
if (this.#articleWatchers.has(sku)) {
|
||||
@@ -258,6 +336,10 @@ export class StorageManager extends ApiClient {
|
||||
if (historyWatchers) {
|
||||
for (const run of historyWatchers) run();
|
||||
}
|
||||
const supplierWatchers = this.#supplierWatchers.get(sku);
|
||||
if (supplierWatchers) {
|
||||
for (const run of supplierWatchers) run();
|
||||
}
|
||||
};
|
||||
|
||||
/** Patch local articleIndex name if already loaded; no-op otherwise. */
|
||||
@@ -393,6 +475,7 @@ export class StorageManager extends ApiClient {
|
||||
for (const sku of new Set([
|
||||
...this.#articleWatchers.keys(),
|
||||
...this.#historyWatchers.keys(),
|
||||
...this.#supplierWatchers.keys(),
|
||||
])) {
|
||||
es.addEventListener(`article-${sku}`, this.#onArticleSignal);
|
||||
}
|
||||
|
||||
@@ -329,6 +329,64 @@ export const article = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* GET /api/article/:sku/supplier → { suppliers: ArticleSupplier[] }
|
||||
*/
|
||||
getSuppliers(sku) {
|
||||
return http(`/api/article/${encodeURIComponent(sku)}/supplier`);
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/article/:sku/supplier
|
||||
* Body: { entityId, sku?, pu?, stock?, sort?, netPrice?, taxId?, taxRate? }
|
||||
* → 201 { supplier: ArticleSupplier }
|
||||
*/
|
||||
addSupplier(sku, body) {
|
||||
return http(`/api/article/${encodeURIComponent(sku)}/supplier`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* PATCH /api/article/:sku/supplier/:id
|
||||
* Body: { sku?, pu?, stock?, sort?, netPrice?, taxId?, taxRate? }
|
||||
* → 200 { supplier: ArticleSupplier }
|
||||
*/
|
||||
updateSupplier(sku, id, body) {
|
||||
return http(
|
||||
`/api/article/${encodeURIComponent(sku)}/supplier/${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT /api/article/:sku/supplier/order
|
||||
* Body: { ids: number[] }
|
||||
* → 200 { ids: number[] }
|
||||
*/
|
||||
orderSuppliers(sku, ids) {
|
||||
return http(`/api/article/${encodeURIComponent(sku)}/supplier/order`, {
|
||||
method: 'PUT',
|
||||
body: { ids },
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* DELETE /api/article/:sku/supplier/:id → 204
|
||||
*/
|
||||
deleteSupplier(sku, id) {
|
||||
return http(
|
||||
`/api/article/${encodeURIComponent(sku)}/supplier/${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* GET /api/article/export.zip → application/zip (binary download)
|
||||
* `artikel.zip` = artikel.csv (semicolon-separated, Windows-1252) +
|
||||
|
||||
Reference in New Issue
Block a user