u
This commit is contained in:
@@ -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;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in New Issue
Block a user