743 lines
24 KiB
JavaScript
743 lines
24 KiB
JavaScript
import { requireArray, requireDefined, requireObject } from '../utils/apiShape';
|
|
import { notifyError } from '../utils/notify';
|
|
import { parseCategoryTreeResponse } from '../utils/categoryUtils';
|
|
import { ApiClient } from './ApiClient';
|
|
import artikelAutomat from '../automats/artikelAutomat';
|
|
import companyAutomat from '../automats/companyAutomat';
|
|
import brandAutomat from '../automats/brandAutomat';
|
|
import unitAutomat from '../automats/unitAutomat';
|
|
import authAutomat from '../automats/authAutomat';
|
|
import categoryAutomat from '../automats/categoryAutomat';
|
|
|
|
|
|
/**
|
|
* Stale-while-revalidate cache: readers keep the current promise; update()
|
|
* swaps only after a successful reload. Failed updates leave the cache intact.
|
|
* replace() writes local data without a round-trip.
|
|
*/
|
|
function createCache(label, load) {
|
|
/** Settled value, or `undefined` if not loaded yet / last load failed. */
|
|
let current;
|
|
let promise = load()
|
|
.then((data) => {
|
|
current = data;
|
|
return data;
|
|
})
|
|
.catch((err) => {
|
|
console.error(`[storageMgr] ${label} failed`, err);
|
|
throw err;
|
|
});
|
|
|
|
async function update() {
|
|
try {
|
|
const data = await load();
|
|
current = data;
|
|
promise = Promise.resolve(data);
|
|
return data;
|
|
} catch (err) {
|
|
console.error(`[storageMgr] update ${label} failed`, err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
function replace(data) {
|
|
current = data;
|
|
promise = Promise.resolve(data);
|
|
return data;
|
|
}
|
|
|
|
function clear() {
|
|
current = undefined;
|
|
promise = load()
|
|
.then((data) => {
|
|
current = data;
|
|
return data;
|
|
})
|
|
.catch((err) => {
|
|
console.error(`[storageMgr] ${label} failed`, err);
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
return {
|
|
update,
|
|
replace,
|
|
clear,
|
|
get promise() {
|
|
return promise;
|
|
},
|
|
/** Loaded value, or `undefined` if the initial load has not settled successfully. */
|
|
get value() {
|
|
return current;
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* ApiClient plus stale-while-revalidate caches and GET /api/events invalidation.
|
|
*/
|
|
export class StorageManager extends ApiClient {
|
|
/** @type {EventSource | null} */
|
|
#events = null;
|
|
/** @type {ReturnType<typeof createCache> | null} */
|
|
#articleIndexCache = null;
|
|
/** @type {Map<string, Set<(article: object) => void>>} sku → article listeners */
|
|
#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);
|
|
}
|
|
|
|
/** Cached article detail from the shared artikelAutomat, or null. */
|
|
getArticle(sku) {
|
|
return artikelAutomat.state.bySku[String(sku)] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Keep the shared article cache in sync after GET/PATCH/PUT responses and
|
|
* notify any StorageManager-level watchers.
|
|
*/
|
|
setArticle(article) {
|
|
requireObject(article, 'article');
|
|
requireDefined(article.sku, 'article.sku');
|
|
const key = String(article.sku);
|
|
const stored = artikelAutomat.actions.setArticle(article);
|
|
|
|
const watchers = this.#articleWatchers.get(key);
|
|
if (watchers) {
|
|
for (const onArticle of watchers) onArticle(stored);
|
|
}
|
|
return stored;
|
|
}
|
|
|
|
/**
|
|
* GET /api/article/{sku} via the shared cache (deduplicated), notify watchers.
|
|
* @param {string} sku
|
|
*/
|
|
async loadArticle(sku) {
|
|
const article = await artikelAutomat.actions.loadArticle(sku);
|
|
const watchers = this.#articleWatchers.get(String(sku));
|
|
if (watchers) {
|
|
for (const onArticle of watchers) onArticle(article);
|
|
}
|
|
return article;
|
|
}
|
|
|
|
/**
|
|
* Subscribe to article detail updates (local setArticle + SSE `article-<sku>`).
|
|
* @param {string} sku
|
|
* @param {(article: object) => void} onArticle
|
|
* @returns {{ article: object | null, unwatch: () => void }}
|
|
*/
|
|
watchArticle(sku, onArticle) {
|
|
const key = String(sku);
|
|
const needListen =
|
|
!this.#articleWatchers.has(key) &&
|
|
!this.#historyWatchers.has(key) &&
|
|
!this.#supplierWatchers.has(key);
|
|
|
|
let watchers = this.#articleWatchers.get(key);
|
|
if (!watchers) {
|
|
watchers = new Set();
|
|
this.#articleWatchers.set(key, watchers);
|
|
}
|
|
watchers.add(onArticle);
|
|
if (needListen) {
|
|
this.#events?.addEventListener(`article-${key}`, this.#onArticleSignal);
|
|
}
|
|
|
|
return {
|
|
article: this.getArticle(key) ?? null,
|
|
unwatch: () => {
|
|
const set = this.#articleWatchers.get(key);
|
|
if (!set) return;
|
|
set.delete(onArticle);
|
|
if (set.size === 0) {
|
|
this.#articleWatchers.delete(key);
|
|
if (
|
|
!this.#historyWatchers.has(key) &&
|
|
!this.#supplierWatchers.has(key)
|
|
) {
|
|
this.#events?.removeEventListener(`article-${key}`, this.#onArticleSignal);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parsed category map + root id from settled caches, or null if any piece missing.
|
|
* @returns {{ categories: object, rootCategoryId: unknown } | null}
|
|
*/
|
|
getCategoryState() {
|
|
const company = this.company;
|
|
if (company == null) return null;
|
|
|
|
const treeData = this.categoryTree;
|
|
if (treeData != null) {
|
|
// Legacy: tree was loaded via the old cache mechanism
|
|
const parsed = parseCategoryTreeResponse(treeData, company.rootCategoryId);
|
|
return {
|
|
categories: parsed.categories,
|
|
rootCategoryId: parsed.rootCategoryId,
|
|
};
|
|
}
|
|
|
|
// New path: use categoryAutomat state directly
|
|
const catState = categoryAutomat.state;
|
|
if (catState.categories == null || catState.treeStatus !== 'succeeded') return null;
|
|
return {
|
|
categories: catState.categories,
|
|
rootCategoryId: catState.rootCategoryId,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Fetch article history, then refetch whenever SSE emits `article-<sku>`.
|
|
* Paginates in the store (full list from HTTP; slice via setSkipAndOffset).
|
|
* @param {string} sku
|
|
* @param {(history: unknown[], total: number) => void} onHistory
|
|
* @returns {{ unwatch: () => void, setSkipAndOffset: (skip: number, offset: number) => void }}
|
|
*/
|
|
watchArticleHistory(sku, onHistory) {
|
|
const key = String(sku);
|
|
let active = true;
|
|
let loaded = false;
|
|
/** @type {unknown[]} */
|
|
let all = [];
|
|
let skip = 0;
|
|
/** Page size (how many rows to emit). */
|
|
let offset = Number.POSITIVE_INFINITY;
|
|
|
|
const emit = () => {
|
|
if (!active || !loaded) return;
|
|
const total = all.length;
|
|
const end =
|
|
offset === Number.POSITIVE_INFINITY ? total : skip + offset;
|
|
onHistory(all.slice(skip, end), total);
|
|
};
|
|
|
|
const refresh = () =>
|
|
this.article.history(key).then((res) => {
|
|
if (!active) return;
|
|
requireObject(res, 'GET /api/article/{sku}/history');
|
|
all = requireArray(res.history, 'history');
|
|
loaded = true;
|
|
emit();
|
|
});
|
|
|
|
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.#historyWatchers.get(key);
|
|
if (!watchers) {
|
|
watchers = new Set();
|
|
this.#historyWatchers.set(key, watchers);
|
|
}
|
|
watchers.add(run);
|
|
if (needListen) {
|
|
this.#events?.addEventListener(`article-${key}`, this.#onArticleSignal);
|
|
}
|
|
|
|
run();
|
|
|
|
return {
|
|
unwatch: () => {
|
|
active = false;
|
|
const set = this.#historyWatchers.get(key);
|
|
if (!set) return;
|
|
set.delete(run);
|
|
if (set.size === 0) {
|
|
this.#historyWatchers.delete(key);
|
|
if (
|
|
!this.#articleWatchers.has(key) &&
|
|
!this.#supplierWatchers.has(key)
|
|
) {
|
|
this.#events?.removeEventListener(`article-${key}`, this.#onArticleSignal);
|
|
}
|
|
}
|
|
},
|
|
setSkipAndOffset: (nextSkip, nextOffset) => {
|
|
skip = nextSkip;
|
|
offset = nextOffset;
|
|
emit();
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
|
|
// Refresh the shared cache; silently when nothing is watching this SKU.
|
|
if (this.#articleWatchers.has(sku)) {
|
|
void artikelAutomat.actions.reloadArticle(sku).catch((err) => notifyError(err));
|
|
} else {
|
|
void artikelAutomat.actions.reloadArticle(sku).catch(() => {});
|
|
}
|
|
const historyWatchers = this.#historyWatchers.get(sku);
|
|
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. */
|
|
setArticleIndexName(sku, name) {
|
|
const articles = this.#articleIndexCache.value;
|
|
if (!articles) return;
|
|
this.#articleIndexCache.replace(
|
|
articles.map((a) => (String(a.sku) === String(sku) ? { ...a, name } : a)),
|
|
);
|
|
}
|
|
|
|
/** Patch cached article detail name if already cached; no-op otherwise. */
|
|
setArticleName(sku, name) {
|
|
const current = this.getArticle(sku);
|
|
if (!current) return;
|
|
this.setArticle({ ...current, name });
|
|
}
|
|
|
|
/** Drop local articleIndex if loaded (forces reload on next read). */
|
|
clearArticleIndex() {
|
|
if (!this.#articleIndexCache.value) return;
|
|
this.#articleIndexCache.clear();
|
|
}
|
|
|
|
#initCaches() {
|
|
const authMeCache = createCache('GET /auth/me', () => this.#loadAuthMe());
|
|
const deliveryCache = createCache('GET /delivery', () => this.#loadDeliveries());
|
|
const customergroupCache = createCache('GET /customergroup', () =>
|
|
this.#loadCustomergroups(),
|
|
);
|
|
const entityCache = createCache('GET /entity', () => this.#loadEntities());
|
|
const numberCache = createCache('GET /number', () => this.#loadNumbers());
|
|
const quoteCache = createCache('GET /quote', () => this.#loadQuotes());
|
|
const orderCache = createCache('GET /order', () => this.#loadOrders());
|
|
|
|
// articleIndex is auth-gated — create a deferred cache that only loads after auth
|
|
this.#articleIndexCache = this.#createDeferredCache(
|
|
'GET /articleIndex',
|
|
() => this.#loadArticleIndex(),
|
|
);
|
|
const articleIndexCache = this.#articleIndexCache;
|
|
|
|
// category tree: deferred, reloaded via categoryAutomat
|
|
const categoryTreeCache = this.#createDeferredCache(
|
|
'GET /category/tree',
|
|
() => this.#loadCategoryTree(),
|
|
);
|
|
|
|
Object.defineProperties(this, {
|
|
authMePromise: { get: () => authMeCache.promise },
|
|
updateAuthMe: { value: authMeCache.update },
|
|
/** Settled GET /auth/me body, or undefined. */
|
|
authMe: { get: () => authMeCache.value },
|
|
|
|
articleIndexPromise: { get: () => articleIndexCache.promise },
|
|
updateArticleIndex: { value: articleIndexCache.update },
|
|
|
|
// ── Automat bridges (brand, unit, company, posterminal) ──────────────────
|
|
/** Brand list from brandAutomat (may be empty until authed). */
|
|
brands: { get: () => brandAutomat.state.brands },
|
|
/** Promise that resolves when brands are first loaded. */
|
|
brandPromise: {
|
|
get: () => brandAutomat.state.status === 'succeeded'
|
|
? Promise.resolve(brandAutomat.state.brands)
|
|
: new Promise((resolve) => {
|
|
const unsub = brandAutomat.subscribe((s) => {
|
|
if (s.status === 'succeeded') { unsub(); resolve(s.brands); }
|
|
});
|
|
}),
|
|
},
|
|
/** Reload brands from API. */
|
|
updateBrand: { value: () => brandAutomat.actions.load() },
|
|
|
|
/** Unit list from unitAutomat. */
|
|
units: { get: () => unitAutomat.state.units },
|
|
/** Promise that resolves when units are first loaded. */
|
|
unitPromise: {
|
|
get: () => unitAutomat.state.status === 'succeeded'
|
|
? Promise.resolve(unitAutomat.state.units)
|
|
: new Promise((resolve) => {
|
|
const unsub = unitAutomat.subscribe((s) => {
|
|
if (s.status === 'succeeded') { unsub(); resolve(s.units); }
|
|
});
|
|
}),
|
|
},
|
|
/** Reload units from API. */
|
|
updateUnit: { value: () => unitAutomat.actions.load() },
|
|
|
|
/**
|
|
* Current company object from companyAutomat, or null.
|
|
* Previously: companies array from GET /api/company.
|
|
* Consumers that used companies.find(c => c.id === companyId) can use .company directly.
|
|
*/
|
|
company: { get: () => companyAutomat.state.current ?? null },
|
|
/** Promise that resolves when company data is loaded. */
|
|
companyPromise: {
|
|
get: () => companyAutomat.state.status === 'succeeded'
|
|
? Promise.resolve(companyAutomat.state.current)
|
|
: new Promise((resolve, reject) => {
|
|
const unsub = companyAutomat.subscribe((s) => {
|
|
if (s.status === 'succeeded') { unsub(); resolve(s.current); }
|
|
else if (s.status === 'failed') { unsub(); reject(new Error(s.error)); }
|
|
});
|
|
}),
|
|
},
|
|
/** Reload company data from API. */
|
|
updateCompany: { value: () => companyAutomat.actions.load() },
|
|
|
|
// Legacy compat: some components call store.companies.find(c => c.id === companyId).
|
|
// Return a single-element array so .find() still works.
|
|
companies: { get: () => companyAutomat.state.current ? [companyAutomat.state.current] : [] },
|
|
|
|
// ── Category tree bridge ─────────────────────────────────────────────────
|
|
categoryTreePromise: { get: () => categoryTreeCache.promise },
|
|
updateCategoryTree: { value: categoryTreeCache.update },
|
|
/** Settled GET /category/tree body, or undefined. */
|
|
categoryTree: { get: () => categoryTreeCache.value },
|
|
|
|
// ── Remaining non-auth-gated caches ─────────────────────────────────────
|
|
deliveryPromise: { get: () => deliveryCache.promise },
|
|
updateDelivery: { value: deliveryCache.update },
|
|
/** Settled GET /delivery list, or undefined. */
|
|
deliveries: { get: () => deliveryCache.value },
|
|
|
|
customergroupPromise: { get: () => customergroupCache.promise },
|
|
updateCustomergroup: { value: customergroupCache.update },
|
|
/** Settled GET /customergroup list, or undefined. */
|
|
customergroups: { get: () => customergroupCache.value },
|
|
|
|
entityPromise: { get: () => entityCache.promise },
|
|
updateEntity: { value: entityCache.update },
|
|
/** Settled GET /entity list, or undefined. */
|
|
entities: { get: () => entityCache.value },
|
|
|
|
numberPromise: { get: () => numberCache.promise },
|
|
updateNumber: { value: numberCache.update },
|
|
/** Settled GET /number list, or undefined. */
|
|
numbers: { get: () => numberCache.value },
|
|
|
|
quotePromise: { get: () => quoteCache.promise },
|
|
updateQuote: { value: quoteCache.update },
|
|
/** Settled GET /quote list, or undefined. */
|
|
quotes: { get: () => quoteCache.value },
|
|
|
|
orderPromise: { get: () => orderCache.promise },
|
|
updateOrder: { value: orderCache.update },
|
|
/** Settled GET /order list, or undefined. */
|
|
orders: { get: () => orderCache.value },
|
|
|
|
/** Close and reopen GET /api/events (required after selected-company change). */
|
|
reconnectEvents: { value: () => this.#reconnectEvents() },
|
|
});
|
|
|
|
// Trigger articleIndex load once auth is established
|
|
authAutomat.subscribe((auth) => {
|
|
if (auth.status === 'authed' && auth.user?.selectedCompanyId) {
|
|
if (!articleIndexCache.value) {
|
|
void articleIndexCache.update?.().catch(() => {});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a cache that does NOT load immediately but waits for an explicit trigger.
|
|
* Differs from createCache in that the initial load promise is not started.
|
|
*/
|
|
#createDeferredCache(label, load) {
|
|
let current;
|
|
let resolveInitial;
|
|
let rejectInitial;
|
|
let promise = new Promise((res, rej) => {
|
|
resolveInitial = res;
|
|
rejectInitial = rej;
|
|
});
|
|
|
|
async function update() {
|
|
try {
|
|
const data = await load();
|
|
current = data;
|
|
promise = Promise.resolve(data);
|
|
resolveInitial?.(data);
|
|
resolveInitial = null;
|
|
return data;
|
|
} catch (err) {
|
|
console.error(`[storageMgr] update ${label} failed`, err);
|
|
rejectInitial?.(err);
|
|
rejectInitial = null;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
function replace(data) {
|
|
current = data;
|
|
promise = Promise.resolve(data);
|
|
resolveInitial?.(data);
|
|
resolveInitial = null;
|
|
return data;
|
|
}
|
|
|
|
return {
|
|
update,
|
|
replace,
|
|
get promise() { return promise; },
|
|
get value() { return current; },
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* GET /api/events — SSE signals for the selected company.
|
|
* Stream is bound at connect time; reconnect after company switch.
|
|
*/
|
|
#reconnectEvents() {
|
|
this.#disconnectEvents();
|
|
const es = new EventSource(this.urlFor('/api/events'), { withCredentials: true });
|
|
es.addEventListener('category', () => {
|
|
void this.updateCategoryTree().catch((err) => {
|
|
console.error('[storageMgr] category SSE refresh failed', err);
|
|
});
|
|
});
|
|
es.addEventListener('articles', () => {
|
|
void this.updateArticleIndex().catch((err) => {
|
|
console.error('[storageMgr] articles SSE refresh failed', err);
|
|
});
|
|
});
|
|
for (const sku of new Set([
|
|
...this.#articleWatchers.keys(),
|
|
...this.#historyWatchers.keys(),
|
|
...this.#supplierWatchers.keys(),
|
|
])) {
|
|
es.addEventListener(`article-${sku}`, this.#onArticleSignal);
|
|
}
|
|
es.onerror = () => {
|
|
console.error('[storageMgr] events stream error');
|
|
};
|
|
this.#events = es;
|
|
}
|
|
|
|
#disconnectEvents() {
|
|
if (!this.#events) return;
|
|
this.#events.close();
|
|
this.#events = null;
|
|
}
|
|
|
|
#syncEvents(user) {
|
|
if (user?.selectedCompanyId != null) {
|
|
this.#reconnectEvents();
|
|
} else {
|
|
this.#disconnectEvents();
|
|
}
|
|
}
|
|
|
|
async #loadAuthMe() {
|
|
try {
|
|
const data = await this.auth.me();
|
|
requireObject(data, 'GET /api/auth/me');
|
|
const session = {
|
|
user: data.user,
|
|
googleClientId: data.googleClientId,
|
|
};
|
|
this.#syncEvents(session.user);
|
|
return session;
|
|
} catch (err) {
|
|
if (err.status === 401 || err.status === 403) {
|
|
this.#syncEvents(null);
|
|
return {
|
|
user: null,
|
|
googleClientId: err.data?.googleClientId,
|
|
forbidden: err.status === 403,
|
|
message: err.message,
|
|
};
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async #loadArticleIndex() {
|
|
const data = await this.article.index();
|
|
requireObject(data, 'GET /api/articleIndex');
|
|
const articles = requireArray(data.articles, 'articleIndex.articles');
|
|
// Map backend rows to the recordset shape used by the result list.
|
|
// `categories` is a comma-joined string (formatCategoryList splits on ',').
|
|
return articles.map((a) => ({
|
|
id: a.id,
|
|
sku: a.sku,
|
|
name: a.name,
|
|
eans: a.eans ?? [],
|
|
categories: Array.isArray(a.categoryIds) ? a.categoryIds.join(',') : null,
|
|
}));
|
|
}
|
|
|
|
async #loadBrands() {
|
|
const data = await this.brand.list();
|
|
requireObject(data, 'GET /api/brand');
|
|
return requireArray(data.brands, 'brands');
|
|
}
|
|
|
|
async #loadUnits() {
|
|
const data = await this.unit.list();
|
|
requireObject(data, 'GET /api/unit');
|
|
return requireArray(data.units, 'units');
|
|
}
|
|
|
|
async #loadCompanies() {
|
|
const data = await this.company.list();
|
|
requireObject(data, 'GET /api/company');
|
|
return requireArray(data.companies, 'companies');
|
|
}
|
|
|
|
async #loadCategoryTree() {
|
|
const data = await this.category.tree();
|
|
requireObject(data, 'GET /api/category/tree');
|
|
requireArray(data.tree, 'category/tree.tree');
|
|
requireArray(data.articles, 'category/tree.articles');
|
|
requireDefined(data.languageId, 'category/tree.languageId');
|
|
return data;
|
|
}
|
|
|
|
async #loadPosterminals() {
|
|
const data = await this.posterminal.list();
|
|
requireObject(data, 'GET /api/posterminal');
|
|
return requireArray(data.posterminals, 'posterminals');
|
|
}
|
|
|
|
async #loadDeliveries() {
|
|
const data = await this.delivery.list();
|
|
requireObject(data, 'GET /api/delivery');
|
|
return requireArray(data.deliveries, 'deliveries');
|
|
}
|
|
|
|
async #loadCustomergroups() {
|
|
const data = await this.customergroup.list();
|
|
requireObject(data, 'GET /api/customergroup');
|
|
return requireArray(data.customergroups, 'customergroups');
|
|
}
|
|
|
|
async #loadEntities() {
|
|
const data = await this.entity.list();
|
|
requireObject(data, 'GET /api/entity');
|
|
return requireArray(data.entities, 'entities');
|
|
}
|
|
|
|
async #loadNumbers() {
|
|
const data = await this.number.list();
|
|
requireObject(data, 'GET /api/number');
|
|
return requireArray(data.numbers, 'numbers');
|
|
}
|
|
|
|
async #loadQuotes() {
|
|
const data = await this.quote.list();
|
|
requireObject(data, 'GET /api/quote');
|
|
return requireArray(data.quotes, 'quotes');
|
|
}
|
|
|
|
async #loadOrders() {
|
|
const data = await this.order.list();
|
|
requireObject(data, 'GET /api/order');
|
|
return requireArray(data.orders, 'orders');
|
|
}
|
|
|
|
async doSearch({ target, filter, offset, maxCount, onUpdate }) {
|
|
const articles = await this.articleIndexPromise;
|
|
const query = target.trim().toLowerCase();
|
|
const matches = articles.filter(
|
|
(p) =>
|
|
p.name.toLowerCase().includes(query) ||
|
|
p.sku.toLowerCase().includes(query) ||
|
|
(p.eans ?? []).some((ean) => String(ean).includes(query)),
|
|
);
|
|
onUpdate({
|
|
categoryFacets: {},
|
|
totalCount: matches.length,
|
|
recordset: matches,
|
|
error: null,
|
|
});
|
|
}
|
|
}
|
|
|
|
export default StorageManager;
|