From 5fcc5670d92699aa4b651e392447339f9086094f Mon Sep 17 00:00:00 2001 From: seb Date: Wed, 28 Oct 2026 23:25:54 +0100 Subject: [PATCH] u --- src/automats/articleDetailAutomat.js | 20 ++++-- src/automats/companyAutomat.js | 99 +++++++++++++++++++++++++--- src/automats/sseAutomat.js | 46 +++++++++---- src/routes/lieferung/lieferanten.jsx | 21 +++++- 4 files changed, 153 insertions(+), 33 deletions(-) diff --git a/src/automats/articleDetailAutomat.js b/src/automats/articleDetailAutomat.js index af6d71d..3668a74 100644 --- a/src/automats/articleDetailAutomat.js +++ b/src/automats/articleDetailAutomat.js @@ -11,7 +11,7 @@ * history or supplier details of a specific article. */ -import { KeyedAutomat } from 'react-automat'; +import { Automat, KeyedAutomat } from 'react-automat'; import { article as articleApi } from '../api'; import { requireArray, requireObject } from '../utils/apiShape'; @@ -50,14 +50,15 @@ const skuHistoryAutomats = new Map(); * @returns {import('react-automat').Automat} Configured chunk-list automat for the SKU. */ export function getArticleHistoryAutomat(sku) { - const key = String(sku); + const rawKey = String(sku); + const key = rawKey.trim().toLowerCase(); let inst = skuHistoryAutomats.get(key); if (!inst) { inst = createChunkListAutomat({ - name: `articleHistory:${key}`, + name: `articleHistory:${rawKey}`, chunkSize: 50, maxLruChunks: 50, - fetchPage: (params) => articleApi.getBySkuHistory(key, params), + fetchPage: (params) => articleApi.getBySkuHistory(rawKey, params), }); skuHistoryAutomats.set(key, inst); } @@ -74,14 +75,14 @@ export const articleHistoryAutomat = { get: getArticleHistoryAutomat, key: getArticleHistoryAutomat, has(sku) { - return skuHistoryAutomats.has(String(sku)); + return skuHistoryAutomats.has(String(sku).trim().toLowerCase()); }, async load(sku) { const inst = getArticleHistoryAutomat(sku); return inst.getItems(); }, async reload(sku) { - const inst = skuHistoryAutomats.get(String(sku)); + const inst = skuHistoryAutomats.get(String(sku).trim().toLowerCase()); if (inst) { await inst.actions.reload(); } @@ -114,7 +115,12 @@ export const articleSupplierAutomat = new KeyedAutomat( loader: async (sku) => { const res = await articleApi.getSuppliers(String(sku)); requireObject(res, 'GET /api/article/:sku/supplier'); - return requireArray(res.suppliers, 'suppliers'); + const list = requireArray(res.suppliers, 'suppliers'); + const comp = Automat.get('company'); + if (comp?.actions?.syncArticleSuppliers) { + comp.actions.syncArticleSuppliers(sku, list); + } + return list; }, }, ); diff --git a/src/automats/companyAutomat.js b/src/automats/companyAutomat.js index 0fd8276..657b328 100644 --- a/src/automats/companyAutomat.js +++ b/src/automats/companyAutomat.js @@ -39,6 +39,7 @@ import { import { requireArray, requireDefined, requireObject } from '../utils/apiShape'; import { normalizeCompany } from '../utils/companyForm'; import authAutomat from './authAutomat'; +import sseAutomat from './sseAutomat'; // #region userSettings persistence helpers const USER_SETTINGS_STORAGE_KEY = 'dash.userSettings'; @@ -704,15 +705,21 @@ const actions = { }, async reloadSuppliers() { - try { - const suppliers = await fetchSuppliersList(); - companyAutomat.setState({ suppliers, suppliersStatus: 'ready', suppliersError: null }); - void actions.loadAllSupplierArticles().catch(() => {}); - return suppliers; - } catch (error) { - companyAutomat.setState({ suppliersError: error }); - throw error; - } + if (inflightSuppliers) return inflightSuppliers; + inflightSuppliers = (async () => { + try { + const suppliers = await fetchSuppliersList(); + companyAutomat.setState({ suppliers, suppliersStatus: 'ready', suppliersError: null }); + void actions.loadAllSupplierArticles().catch(() => {}); + return suppliers; + } catch (error) { + companyAutomat.setState({ suppliersError: error }); + throw error; + } finally { + inflightSuppliers = null; + } + })(); + return inflightSuppliers; }, async loadArticlesForSupplier(supplierId) { @@ -756,6 +763,44 @@ const actions = { } }, + syncArticleSuppliers(sku, suppliers) { + const artSku = String(sku).trim().toLowerCase(); + const assignedSupplierIds = new Set( + (suppliers || []).map((s) => String(s.entityId ?? s.supplierId ?? s.id)) + ); + const prevMap = companyAutomat.state.articlesBySupplier || {}; + let changed = false; + const nextMap = { ...prevMap }; + + for (const sId of assignedSupplierIds) { + const currentList = nextMap[sId] ? [...nextMap[sId]] : []; + const hasSku = currentList.some( + (item) => String(item.article?.sku ?? item.sku ?? '').toLowerCase() === artSku + ); + if (!hasSku) { + currentList.push({ sku, article: { sku } }); + nextMap[sId] = currentList; + changed = true; + } + } + + for (const [sId, list] of Object.entries(nextMap)) { + if (!assignedSupplierIds.has(sId) && Array.isArray(list)) { + const filtered = list.filter( + (item) => String(item.article?.sku ?? item.sku ?? '').toLowerCase() !== artSku + ); + if (filtered.length !== list.length) { + nextMap[sId] = filtered; + changed = true; + } + } + } + + if (changed) { + companyAutomat.setState({ articlesBySupplier: nextMap }); + } + }, + async createSupplier(body) { const data = await supplierApi.create(body); requireObject(data, 'POST /api/supplier'); @@ -1474,6 +1519,42 @@ authAutomat.subscribe(syncWithAuth); syncWithAuth(authAutomat.state); // #endregion +// #region sseAutomat synchronization +sseAutomat.actions.on('supplier', () => { + void companyAutomat.actions.reloadSuppliers().catch(() => {}); +}); +sseAutomat.actions.on('warehouse', () => { + void companyAutomat.actions.reloadWarehouses().catch(() => {}); +}); +sseAutomat.actions.on('brand', () => { + void companyAutomat.actions.loadBrands(true).catch(() => {}); +}); +sseAutomat.actions.on('unit', () => { + void companyAutomat.actions.loadUnits().catch(() => {}); +}); +sseAutomat.actions.on('carrier', () => { + void companyAutomat.actions.reloadCarriers().catch(() => {}); +}); +sseAutomat.actions.on('posterminal', () => { + void companyAutomat.actions.loadTerminals().catch(() => {}); +}); +sseAutomat.actions.on('customergroup', () => { + void companyAutomat.actions.reloadCustomergroups().catch(() => {}); +}); +sseAutomat.actions.on('propertyname', () => { + void companyAutomat.actions.loadProperties().catch(() => {}); +}); +sseAutomat.actions.on('attributename', () => { + void companyAutomat.actions.loadAttributes().catch(() => {}); +}); +sseAutomat.actions.on('account', () => { + void companyAutomat.actions.reloadAccounts().catch(() => {}); +}); +sseAutomat.actions.on('company', () => { + void companyAutomat.actions.reload().catch(() => {}); +}); +// #endregion + // #region selectors & exported action helpers // Company profile selectors export const selectCompanies = () => companyAutomat.state.companies; diff --git a/src/automats/sseAutomat.js b/src/automats/sseAutomat.js index 805fbc7..d8e7fd0 100644 --- a/src/automats/sseAutomat.js +++ b/src/automats/sseAutomat.js @@ -21,6 +21,7 @@ import { Automat } from 'react-automat'; import authAutomat from './authAutomat'; +import { articleHistoryAutomat, articleSupplierAutomat } from './articleDetailAutomat'; /** * @typedef {Object} SseState @@ -202,6 +203,24 @@ function dispatchSseEvent(event) { void Automat.get('company')?.actions?.reloadWarehouses?.().catch(() => {}); Automat.get('category')?.actions?.invalidateAllCategoryViews?.(); void Automat.get('articleIndex')?.actions?.reload?.().catch(() => {}); + } else if (eventType === 'supplier') { + void Automat.get('company')?.actions?.reloadSuppliers?.().catch(() => {}); + } else if (eventType === 'brand') { + void Automat.get('company')?.actions?.loadBrands?.(true).catch(() => {}); + } else if (eventType === 'unit') { + void Automat.get('company')?.actions?.loadUnits?.().catch(() => {}); + } else if (eventType === 'carrier') { + void Automat.get('company')?.actions?.reloadCarriers?.().catch(() => {}); + } else if (eventType === 'posterminal') { + void Automat.get('company')?.actions?.loadTerminals?.().catch(() => {}); + } else if (eventType === 'customergroup') { + void Automat.get('company')?.actions?.reloadCustomergroups?.().catch(() => {}); + } else if (eventType === 'propertyname') { + void Automat.get('company')?.actions?.loadProperties?.().catch(() => {}); + } else if (eventType === 'attributename') { + void Automat.get('company')?.actions?.loadAttributes?.().catch(() => {}); + } else if (eventType === 'account') { + void Automat.get('company')?.actions?.reloadAccounts?.().catch(() => {}); } else if (eventType === 'order') { void Automat.get('order')?.actions?.reload?.().catch(() => {}); void Automat.get('shipment')?.actions?.reload?.().catch(() => {}); @@ -219,9 +238,10 @@ function dispatchSseEvent(event) { // 3. Dispatch to article-specific handlers if it starts with article- if (eventType.startsWith(ARTICLE_EVENT_PREFIX)) { const sku = eventType.slice(ARTICLE_EVENT_PREFIX.length); + const lowerSku = sku.trim().toLowerCase(); // Watchers registered via watchArticle(sku, handler) - const watchers = articleWatchers.get(sku); + const watchers = articleWatchers.get(lowerSku) || articleWatchers.get(sku); if (watchers) { for (const handler of watchers) { try { @@ -522,20 +542,21 @@ const actions = { * @returns {() => void} Unsubscribe callback. */ watchArticle(sku, handler) { - const key = String(sku); - let set = articleWatchers.get(key); + const rawKey = String(sku).trim(); + const lowerKey = rawKey.toLowerCase(); + let set = articleWatchers.get(lowerKey); if (!set) { set = new Set(); - articleWatchers.set(key, set); - attachArticleListener(key); + articleWatchers.set(lowerKey, set); + attachArticleListener(rawKey); } set.add(handler); return () => { - const current = articleWatchers.get(key); + const current = articleWatchers.get(lowerKey); if (!current) return; current.delete(handler); - if (current.size === 0) articleWatchers.delete(key); + if (current.size === 0) articleWatchers.delete(lowerKey); }; }, @@ -575,14 +596,11 @@ export function invalidateArticle(sku) { if (art?.actions?.reloadArticle) { void art.actions.reloadArticle(key).catch(() => {}); } - const hist = Automat.get('articleHistory'); - if (hist?.has?.(key)) { - void hist.reload(key).catch(() => {}); - } - const sup = Automat.get('articleSupplier'); - if (sup?.has?.(key)) { - void sup.reload(key).catch(() => {}); + if (articleHistoryAutomat.has(key)) { + void articleHistoryAutomat.reload(key).catch(() => {}); } + // Reload assigned suppliers and sync with companyAutomat + void articleSupplierAutomat.reload(key).catch(() => {}); } authAutomat.subscribe(handleAuthSync); diff --git a/src/routes/lieferung/lieferanten.jsx b/src/routes/lieferung/lieferanten.jsx index bb88387..e9f0acc 100644 --- a/src/routes/lieferung/lieferanten.jsx +++ b/src/routes/lieferung/lieferanten.jsx @@ -32,8 +32,19 @@ function sortSuppliers(suppliers) { ); } -function draftsFrom(suppliers) { - return Object.fromEntries(suppliers.map((row) => [row.id, row.name])); +function draftsFrom(suppliers, currentDrafts = {}, prevSuppliers = []) { + const prevNames = new Map((prevSuppliers || []).map((s) => [s.id, s.name])); + const drafts = {}; + for (const row of suppliers) { + const draft = currentDrafts[row.id]; + const prevName = prevNames.get(row.id); + if (draft !== undefined && prevName !== undefined && draft !== prevName) { + drafts[row.id] = draft; + } else { + drafts[row.id] = row.name; + } + } + return drafts; } function getInitialParams(props) { @@ -82,6 +93,10 @@ class LieferungLieferantenPage extends PureComponent { } this._cancelled = false; + if (companyAutomat.state.suppliersStatus === 'idle') { + void companyAutomat.actions.loadSuppliers().catch((err) => notifyError(err)); + } + this.unsub = companyAutomat.subscribe( this, (state) => { @@ -90,7 +105,7 @@ class LieferungLieferantenPage extends PureComponent { status: state.suppliersStatus, error: state.suppliersError ? backendErrorFrom(state.suppliersError) : null, suppliers: sorted, - drafts: draftsFrom(sorted), + drafts: draftsFrom(sorted, this.state?.drafts, this.state?.suppliers), }; }, ['suppliers', 'suppliersStatus', 'suppliersError'],