This commit is contained in:
seb
2026-10-28 23:25:54 +01:00
parent 6851e7c16f
commit 5fcc5670d9
4 changed files with 153 additions and 33 deletions

View File

@@ -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;
},
},
);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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'],