u
This commit is contained in:
@@ -14,6 +14,8 @@ import { Automat } from 'react-automat';
|
|||||||
import FlexSearch from 'flexsearch';
|
import FlexSearch from 'flexsearch';
|
||||||
import articleIndexAutomat from './articleIndexAutomat';
|
import articleIndexAutomat from './articleIndexAutomat';
|
||||||
import categoryAutomat from './categoryAutomat';
|
import categoryAutomat from './categoryAutomat';
|
||||||
|
import brandAutomat from './brandAutomat';
|
||||||
|
import supplierAutomat from './supplierAutomat';
|
||||||
import { parseFilter } from '../components/article/searchFormat';
|
import { parseFilter } from '../components/article/searchFormat';
|
||||||
|
|
||||||
// #region Types & Initial State
|
// #region Types & Initial State
|
||||||
@@ -22,6 +24,8 @@ import { parseFilter } from '../components/article/searchFormat';
|
|||||||
/**
|
/**
|
||||||
* @typedef {Object} ArticleSearchResult
|
* @typedef {Object} ArticleSearchResult
|
||||||
* @property {Record<string,object>} categoryFacets Facet counts per category id.
|
* @property {Record<string,object>} categoryFacets Facet counts per category id.
|
||||||
|
* @property {Record<string,object>} brandFacets Facet counts per brand.
|
||||||
|
* @property {Record<string,object>} supplierFacets Facet counts per supplier.
|
||||||
* @property {number} totalCount Number of matching records.
|
* @property {number} totalCount Number of matching records.
|
||||||
* @property {Array<object>} recordset Sliced matching article index records for the current page.
|
* @property {Array<object>} recordset Sliced matching article index records for the current page.
|
||||||
* @property {number} offset Current 0-based offset.
|
* @property {number} offset Current 0-based offset.
|
||||||
@@ -36,6 +40,8 @@ import { parseFilter } from '../components/article/searchFormat';
|
|||||||
* @property {ArticleSearchResult} result Latest search outcome.
|
* @property {ArticleSearchResult} result Latest search outcome.
|
||||||
* @property {string} target Active search term.
|
* @property {string} target Active search term.
|
||||||
* @property {string} filter Active category filter string.
|
* @property {string} filter Active category filter string.
|
||||||
|
* @property {string} brand Active brand filter string.
|
||||||
|
* @property {string} supplier Active supplier filter string.
|
||||||
* @property {number} offset Current 0-based offset.
|
* @property {number} offset Current 0-based offset.
|
||||||
* @property {number|null} pageSize Current page size.
|
* @property {number|null} pageSize Current page size.
|
||||||
* @property {boolean} loading Whether a search or index load is in flight.
|
* @property {boolean} loading Whether a search or index load is in flight.
|
||||||
@@ -44,6 +50,8 @@ import { parseFilter } from '../components/article/searchFormat';
|
|||||||
/** @type {ArticleSearchResult} */
|
/** @type {ArticleSearchResult} */
|
||||||
const emptyResult = {
|
const emptyResult = {
|
||||||
categoryFacets: {},
|
categoryFacets: {},
|
||||||
|
brandFacets: {},
|
||||||
|
supplierFacets: {},
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
recordset: [],
|
recordset: [],
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@@ -58,6 +66,8 @@ const initialState = {
|
|||||||
result: emptyResult,
|
result: emptyResult,
|
||||||
target: '',
|
target: '',
|
||||||
filter: '',
|
filter: '',
|
||||||
|
brand: '',
|
||||||
|
supplier: '',
|
||||||
offset: 0,
|
offset: 0,
|
||||||
pageSize: null,
|
pageSize: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -565,6 +575,103 @@ function buildCategoryFacets(matchedArticles) {
|
|||||||
}
|
}
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
// #region buildBrandFacets
|
||||||
|
// MARK: computes brand facet counts from matched articles
|
||||||
|
/**
|
||||||
|
* Computes facet counts for each brand represented in matched articles.
|
||||||
|
*
|
||||||
|
* @param {Array<object>} matchedArticles Articles matching text query.
|
||||||
|
* @returns {Record<string, {id: string, name: string, count: number, brand: object, thumbHash: string|null}>} Brand facets.
|
||||||
|
*/
|
||||||
|
function buildBrandFacets(matchedArticles) {
|
||||||
|
const facets = {};
|
||||||
|
const allBrands = brandAutomat.state?.brands ?? [];
|
||||||
|
const brandById = new Map();
|
||||||
|
const brandByName = new Map();
|
||||||
|
for (const b of allBrands) {
|
||||||
|
if (b.id != null) brandById.set(String(b.id), b);
|
||||||
|
if (b.name) brandByName.set(String(b.name).toLowerCase(), b);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const article of matchedArticles) {
|
||||||
|
const brandObj = article.brand;
|
||||||
|
const bId = brandObj?.id ?? article.brandId;
|
||||||
|
const bName = brandObj?.name ?? article.brandName;
|
||||||
|
if (!bId && !bName) continue;
|
||||||
|
|
||||||
|
const brandKey = bId != null ? String(bId) : String(bName);
|
||||||
|
const knownBrand = (bId != null ? brandById.get(String(bId)) : null) ||
|
||||||
|
(bName ? brandByName.get(String(bName).toLowerCase()) : null);
|
||||||
|
|
||||||
|
const name = knownBrand?.name || bName || `Marke ${brandKey}`;
|
||||||
|
const id = knownBrand?.id != null ? String(knownBrand.id) : brandKey;
|
||||||
|
const thumbHash = knownBrand?.pics?.[0]?.thumbHash || brandObj?.thumbHash || article.brandThumbHash || null;
|
||||||
|
|
||||||
|
if (!facets[id]) {
|
||||||
|
facets[id] = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
thumbHash,
|
||||||
|
brand: knownBrand || brandObj || { id, name, thumbHash },
|
||||||
|
count: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
facets[id].count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return facets;
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region buildSupplierFacets
|
||||||
|
// MARK: computes supplier facet counts from matched articles
|
||||||
|
/**
|
||||||
|
* Computes facet counts for each supplier represented in matched articles.
|
||||||
|
*
|
||||||
|
* @param {Array<object>} matchedArticles Articles matching text query.
|
||||||
|
* @returns {Record<string, {id: string, name: string, count: number}>} Supplier facets.
|
||||||
|
*/
|
||||||
|
function buildSupplierFacets(matchedArticles) {
|
||||||
|
const facets = {};
|
||||||
|
const suppliers = supplierAutomat.state?.suppliers ?? [];
|
||||||
|
const articlesBySupplier = supplierAutomat.state?.articlesBySupplier ?? {};
|
||||||
|
|
||||||
|
const supplierArticleSets = new Map();
|
||||||
|
for (const s of suppliers) {
|
||||||
|
const sId = String(s.id);
|
||||||
|
const list = articlesBySupplier[sId] ?? [];
|
||||||
|
const skuSet = new Set();
|
||||||
|
for (const item of list) {
|
||||||
|
if (item.article?.sku) skuSet.add(String(item.article.sku).toLowerCase());
|
||||||
|
if (item.sku) skuSet.add(String(item.sku).toLowerCase());
|
||||||
|
}
|
||||||
|
supplierArticleSets.set(sId, skuSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const article of matchedArticles) {
|
||||||
|
const artSku = String(article.sku ?? '').toLowerCase();
|
||||||
|
if (!artSku) continue;
|
||||||
|
|
||||||
|
for (const s of suppliers) {
|
||||||
|
const sId = String(s.id);
|
||||||
|
const skuSet = supplierArticleSets.get(sId);
|
||||||
|
if (skuSet && skuSet.has(artSku)) {
|
||||||
|
if (!facets[sId]) {
|
||||||
|
facets[sId] = {
|
||||||
|
id: sId,
|
||||||
|
name: s.name,
|
||||||
|
count: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
facets[sId].count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return facets;
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Actions
|
// #region Actions
|
||||||
@@ -581,6 +688,8 @@ const actions = {
|
|||||||
const currentState = articleSearchAutomat.state;
|
const currentState = articleSearchAutomat.state;
|
||||||
const target = params.target !== undefined ? String(params.target ?? '') : currentState.target;
|
const target = params.target !== undefined ? String(params.target ?? '') : currentState.target;
|
||||||
const filter = params.filter !== undefined ? String(params.filter ?? '') : currentState.filter;
|
const filter = params.filter !== undefined ? String(params.filter ?? '') : currentState.filter;
|
||||||
|
const brand = params.brand !== undefined ? String(params.brand ?? '') : (currentState.brand ?? '');
|
||||||
|
const supplier = params.supplier !== undefined ? String(params.supplier ?? '') : (currentState.supplier ?? '');
|
||||||
const pageSize = params.pageSize !== undefined ? params.pageSize : currentState.pageSize;
|
const pageSize = params.pageSize !== undefined ? params.pageSize : currentState.pageSize;
|
||||||
const offset = params.offset !== undefined ? Math.max(0, Number(params.offset) || 0) : currentState.offset;
|
const offset = params.offset !== undefined ? Math.max(0, Number(params.offset) || 0) : currentState.offset;
|
||||||
|
|
||||||
@@ -592,8 +701,10 @@ const actions = {
|
|||||||
? queryFlexSearch(query, articles)
|
? queryFlexSearch(query, articles)
|
||||||
: articles;
|
: articles;
|
||||||
|
|
||||||
// Build facets for all text matches (before category filtering)
|
// Build facets for all text matches (before facet filtering)
|
||||||
const categoryFacets = buildCategoryFacets(textMatches);
|
const categoryFacets = buildCategoryFacets(textMatches);
|
||||||
|
const brandFacets = buildBrandFacets(textMatches);
|
||||||
|
const supplierFacets = buildSupplierFacets(textMatches);
|
||||||
|
|
||||||
// Apply category filter if set (expanding to include all child categories)
|
// Apply category filter if set (expanding to include all child categories)
|
||||||
const selectedCategories = parseFilter(filter);
|
const selectedCategories = parseFilter(filter);
|
||||||
@@ -606,11 +717,42 @@ const actions = {
|
|||||||
categoriesMap,
|
categoriesMap,
|
||||||
categoryFacets,
|
categoryFacets,
|
||||||
);
|
);
|
||||||
filteredArticles = textMatches.filter((row) =>
|
filteredArticles = filteredArticles.filter((row) =>
|
||||||
matchesCategory(row, expandedCategoryIds),
|
matchesCategory(row, expandedCategoryIds),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply brand filter if set
|
||||||
|
const selectedBrands = parseFilter(brand);
|
||||||
|
if (selectedBrands.length > 0) {
|
||||||
|
const brandSet = new Set(selectedBrands.map((b) => String(b).trim().toLowerCase()));
|
||||||
|
filteredArticles = filteredArticles.filter((row) => {
|
||||||
|
const bId = row.brand?.id ?? row.brandId;
|
||||||
|
const bName = row.brand?.name ?? row.brandName;
|
||||||
|
if (bId != null && brandSet.has(String(bId).toLowerCase())) return true;
|
||||||
|
if (bName && brandSet.has(String(bName).trim().toLowerCase())) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply supplier filter if set
|
||||||
|
const selectedSuppliers = parseFilter(supplier);
|
||||||
|
if (selectedSuppliers.length > 0) {
|
||||||
|
const articlesBySupplier = supplierAutomat.state?.articlesBySupplier ?? {};
|
||||||
|
const allowedSkus = new Set();
|
||||||
|
for (const sId of selectedSuppliers) {
|
||||||
|
const list = articlesBySupplier[String(sId)] ?? [];
|
||||||
|
for (const item of list) {
|
||||||
|
if (item.article?.sku) allowedSkus.add(String(item.article.sku).toLowerCase());
|
||||||
|
if (item.sku) allowedSkus.add(String(item.sku).toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filteredArticles = filteredArticles.filter((row) => {
|
||||||
|
const rowSku = String(row.sku ?? '').toLowerCase();
|
||||||
|
return allowedSkus.has(rowSku);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const sortedArticles = sortArticlesByRelevance(filteredArticles, query);
|
const sortedArticles = sortArticlesByRelevance(filteredArticles, query);
|
||||||
|
|
||||||
const totalCount = sortedArticles.length;
|
const totalCount = sortedArticles.length;
|
||||||
@@ -623,6 +765,8 @@ const actions = {
|
|||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
categoryFacets,
|
categoryFacets,
|
||||||
|
brandFacets,
|
||||||
|
supplierFacets,
|
||||||
totalCount,
|
totalCount,
|
||||||
recordset,
|
recordset,
|
||||||
offset,
|
offset,
|
||||||
@@ -636,6 +780,8 @@ const actions = {
|
|||||||
result,
|
result,
|
||||||
target,
|
target,
|
||||||
filter,
|
filter,
|
||||||
|
brand,
|
||||||
|
supplier,
|
||||||
offset,
|
offset,
|
||||||
pageSize: safePageSize,
|
pageSize: safePageSize,
|
||||||
loading: articleIndexAutomat.state.status === 'loading',
|
loading: articleIndexAutomat.state.status === 'loading',
|
||||||
@@ -751,6 +897,16 @@ categoryAutomat.subscribe((catState) => {
|
|||||||
actions.runSearch();
|
actions.runSearch();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update facets when brands are loaded or updated.
|
||||||
|
brandAutomat.subscribe(() => {
|
||||||
|
actions.runSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update facets when suppliers or supplier articles are loaded or updated.
|
||||||
|
supplierAutomat.subscribe(() => {
|
||||||
|
actions.runSearch();
|
||||||
|
});
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Selectors & Exports
|
// #region Selectors & Exports
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import authAutomat from './authAutomat';
|
|||||||
/** @type {SupplierState} */
|
/** @type {SupplierState} */
|
||||||
const initialState = {
|
const initialState = {
|
||||||
suppliers: [],
|
suppliers: [],
|
||||||
|
articlesBySupplier: {},
|
||||||
|
articlesLoading: false,
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
@@ -62,6 +64,7 @@ function loadSuppliers() {
|
|||||||
try {
|
try {
|
||||||
const suppliers = await fetchSuppliers();
|
const suppliers = await fetchSuppliers();
|
||||||
supplierAutomat.setState({ suppliers, status: 'ready', error: null });
|
supplierAutomat.setState({ suppliers, status: 'ready', error: null });
|
||||||
|
void actions.loadAllSupplierArticles().catch(() => {});
|
||||||
return suppliers;
|
return suppliers;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
supplierAutomat.setState({ status: 'failed', error });
|
supplierAutomat.setState({ status: 'failed', error });
|
||||||
@@ -92,6 +95,7 @@ const actions = {
|
|||||||
try {
|
try {
|
||||||
const suppliers = await fetchSuppliers();
|
const suppliers = await fetchSuppliers();
|
||||||
supplierAutomat.setState({ suppliers, status: 'ready', error: null });
|
supplierAutomat.setState({ suppliers, status: 'ready', error: null });
|
||||||
|
void actions.loadAllSupplierArticles().catch(() => {});
|
||||||
return suppliers;
|
return suppliers;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
supplierAutomat.setState({ error });
|
supplierAutomat.setState({ error });
|
||||||
@@ -99,6 +103,58 @@ const actions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads articles for a single supplier, caching them in state.articlesBySupplier.
|
||||||
|
*
|
||||||
|
* @param {string|number} supplierId
|
||||||
|
* @returns {Promise<Array<object>>}
|
||||||
|
*/
|
||||||
|
async loadArticlesForSupplier(supplierId) {
|
||||||
|
const sId = String(supplierId);
|
||||||
|
if (supplierAutomat.state.articlesBySupplier?.[sId]) {
|
||||||
|
return supplierAutomat.state.articlesBySupplier[sId];
|
||||||
|
}
|
||||||
|
const res = await supplierApi.getArticles(supplierId);
|
||||||
|
const articles = Array.isArray(res?.articles) ? res.articles : [];
|
||||||
|
supplierAutomat.setState({
|
||||||
|
articlesBySupplier: {
|
||||||
|
...(supplierAutomat.state.articlesBySupplier || {}),
|
||||||
|
[sId]: articles,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return articles;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads articles for all suppliers in the supplier list, caching them in state.articlesBySupplier.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Record<string, Array<object>>>}
|
||||||
|
*/
|
||||||
|
async loadAllSupplierArticles() {
|
||||||
|
const suppliers = supplierAutomat.state.suppliers;
|
||||||
|
if (!suppliers.length) return {};
|
||||||
|
supplierAutomat.setState({ articlesLoading: true });
|
||||||
|
try {
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
suppliers.map(async (s) => {
|
||||||
|
const res = await supplierApi.getArticles(s.id);
|
||||||
|
return { id: String(s.id), articles: Array.isArray(res?.articles) ? res.articles : [] };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const nextMap = { ...(supplierAutomat.state.articlesBySupplier || {}) };
|
||||||
|
for (const r of results) {
|
||||||
|
if (r.status === 'fulfilled') {
|
||||||
|
nextMap[r.value.id] = r.value.articles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
supplierAutomat.setState({ articlesBySupplier: nextMap, articlesLoading: false });
|
||||||
|
return nextMap;
|
||||||
|
} catch {
|
||||||
|
supplierAutomat.setState({ articlesLoading: false });
|
||||||
|
return supplierAutomat.state.articlesBySupplier || {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a supplier and refreshes the cached list.
|
* Creates a supplier and refreshes the cached list.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -4,14 +4,28 @@ export function queryFromParams(searchParams) {
|
|||||||
return {
|
return {
|
||||||
target: searchParams.get('target') ?? '',
|
target: searchParams.get('target') ?? '',
|
||||||
filter: searchParams.get('filter') ?? '',
|
filter: searchParams.get('filter') ?? '',
|
||||||
|
brand: searchParams.get('brand') ?? '',
|
||||||
|
supplier: searchParams.get('supplier') ?? '',
|
||||||
offset: Math.max(0, Number(searchParams.get('offset')) || 0),
|
offset: Math.max(0, Number(searchParams.get('offset')) || 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSearchUrl(target, filter = '', offset = 0) {
|
export function buildSearchUrl(target, filter = '', offset = 0, options = {}) {
|
||||||
|
let brand = '';
|
||||||
|
let supplier = '';
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
brand = options;
|
||||||
|
supplier = arguments[4] || '';
|
||||||
|
} else if (options && typeof options === 'object') {
|
||||||
|
brand = options.brand || '';
|
||||||
|
supplier = options.supplier || '';
|
||||||
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (target) params.set('target', target);
|
if (target) params.set('target', target);
|
||||||
if (filter) params.set('filter', filter);
|
if (filter) params.set('filter', filter);
|
||||||
|
if (brand) params.set('brand', brand);
|
||||||
|
if (supplier) params.set('supplier', supplier);
|
||||||
if (offset > 0) params.set('offset', String(offset));
|
if (offset > 0) params.set('offset', String(offset));
|
||||||
const qs = params.toString();
|
const qs = params.toString();
|
||||||
return qs ? `/artikel?${qs}` : '/artikel';
|
return qs ? `/artikel?${qs}` : '/artikel';
|
||||||
@@ -26,7 +40,13 @@ export function readArtikelQuery() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function writeArtikelQuery({ target, filter = '', offset = 0 }) {
|
export function writeArtikelQuery({
|
||||||
|
target,
|
||||||
|
filter = '',
|
||||||
|
offset = 0,
|
||||||
|
brand = '',
|
||||||
|
supplier = '',
|
||||||
|
}) {
|
||||||
try {
|
try {
|
||||||
if (!target || target.trim().length < 3) {
|
if (!target || target.trim().length < 3) {
|
||||||
localStorage.removeItem(ARTIKEL_QUERY_KEY);
|
localStorage.removeItem(ARTIKEL_QUERY_KEY);
|
||||||
@@ -34,7 +54,7 @@ export function writeArtikelQuery({ target, filter = '', offset = 0 }) {
|
|||||||
}
|
}
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
ARTIKEL_QUERY_KEY,
|
ARTIKEL_QUERY_KEY,
|
||||||
JSON.stringify({ target, filter, offset }),
|
JSON.stringify({ target, filter, offset, brand, supplier }),
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore quota / private mode
|
// ignore quota / private mode
|
||||||
@@ -53,5 +73,8 @@ export function clearArtikelQuery() {
|
|||||||
export function artikelQueryPath() {
|
export function artikelQueryPath() {
|
||||||
const query = readArtikelQuery();
|
const query = readArtikelQuery();
|
||||||
if (!query?.target || query.target.trim().length < 3) return '/artikel';
|
if (!query?.target || query.target.trim().length < 3) return '/artikel';
|
||||||
return buildSearchUrl(query.target, query.filter ?? '', query.offset ?? 0);
|
return buildSearchUrl(query.target, query.filter ?? '', query.offset ?? 0, {
|
||||||
|
brand: query.brand ?? '',
|
||||||
|
supplier: query.supplier ?? '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,10 @@ import {
|
|||||||
navigationAutomat,
|
navigationAutomat,
|
||||||
categoryAutomat,
|
categoryAutomat,
|
||||||
companyAutomat,
|
companyAutomat,
|
||||||
|
brandAutomat,
|
||||||
|
supplierAutomat,
|
||||||
} from '../automats';
|
} from '../automats';
|
||||||
import { BrandLabel } from '../components/brand';
|
import { BrandLabel, BrandThumb } from '../components/brand';
|
||||||
import { SCRIM_LABEL } from '../colors';
|
import { SCRIM_LABEL } from '../colors';
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
@@ -266,8 +268,12 @@ function initialSearchState(searchParams) {
|
|||||||
return {
|
return {
|
||||||
input,
|
input,
|
||||||
categoryFilterOpen: false,
|
categoryFilterOpen: false,
|
||||||
|
brandFilterOpen: false,
|
||||||
|
supplierFilterOpen: false,
|
||||||
error: automatResult.error,
|
error: automatResult.error,
|
||||||
categoryFacets: automatResult.categoryFacets,
|
categoryFacets: automatResult.categoryFacets,
|
||||||
|
brandFacets: automatResult.brandFacets,
|
||||||
|
supplierFacets: automatResult.supplierFacets,
|
||||||
totalCount: automatResult.totalCount,
|
totalCount: automatResult.totalCount,
|
||||||
recordset: automatResult.recordset,
|
recordset: automatResult.recordset,
|
||||||
pageSize: automatResult.pageSize,
|
pageSize: automatResult.pageSize,
|
||||||
@@ -313,7 +319,27 @@ class ArtikelSearchPage extends Component {
|
|||||||
|
|
||||||
// #region Query Persistence
|
// #region Query Persistence
|
||||||
toggleCategoryFilter = () => {
|
toggleCategoryFilter = () => {
|
||||||
this.setState((prev) => ({ categoryFilterOpen: !prev.categoryFilterOpen }));
|
this.setState((prev) => ({
|
||||||
|
categoryFilterOpen: !prev.categoryFilterOpen,
|
||||||
|
brandFilterOpen: false,
|
||||||
|
supplierFilterOpen: false,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleBrandFilter = () => {
|
||||||
|
this.setState((prev) => ({
|
||||||
|
brandFilterOpen: !prev.brandFilterOpen,
|
||||||
|
categoryFilterOpen: false,
|
||||||
|
supplierFilterOpen: false,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleSupplierFilter = () => {
|
||||||
|
this.setState((prev) => ({
|
||||||
|
supplierFilterOpen: !prev.supplierFilterOpen,
|
||||||
|
categoryFilterOpen: false,
|
||||||
|
brandFilterOpen: false,
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
handleOpenArticle = (row) => {
|
handleOpenArticle = (row) => {
|
||||||
@@ -327,6 +353,8 @@ class ArtikelSearchPage extends Component {
|
|||||||
writeArtikelQuery({
|
writeArtikelQuery({
|
||||||
target,
|
target,
|
||||||
filter: query.filter,
|
filter: query.filter,
|
||||||
|
brand: query.brand,
|
||||||
|
supplier: query.supplier,
|
||||||
offset: query.offset,
|
offset: query.offset,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -339,16 +367,30 @@ class ArtikelSearchPage extends Component {
|
|||||||
|
|
||||||
// Push search results into local component state.
|
// Push search results into local component state.
|
||||||
this.unsubSearch = articleSearchAutomat.subscribe(this, (state) => state.result);
|
this.unsubSearch = articleSearchAutomat.subscribe(this, (state) => state.result);
|
||||||
|
this.unsubBrand = brandAutomat.subscribe(this, () => ({}));
|
||||||
|
this.unsubSupplier = supplierAutomat.subscribe(this, () => ({}));
|
||||||
|
|
||||||
|
void brandAutomat.actions.load().catch(() => {});
|
||||||
|
void supplierAutomat.actions.load().catch(() => {});
|
||||||
|
void supplierAutomat.actions.loadAllSupplierArticles().catch(() => {});
|
||||||
|
|
||||||
// Initial search: kicks off index loading if idle and derives current items
|
// Initial search: kicks off index loading if idle and derives current items
|
||||||
const { filter: urlFilter, offset } = queryFromParams(this.props.searchParams);
|
const {
|
||||||
|
filter: urlFilter,
|
||||||
|
offset,
|
||||||
|
brand: urlBrand,
|
||||||
|
supplier: urlSupplier,
|
||||||
|
} = queryFromParams(this.props.searchParams);
|
||||||
const hasFilterInUrl = this.props.searchParams.has('filter');
|
const hasFilterInUrl = this.props.searchParams.has('filter');
|
||||||
const navCategory = navigationAutomat.state.lastCategoryId;
|
const navCategory = navigationAutomat.state.lastCategoryId;
|
||||||
const filter = hasFilterInUrl ? urlFilter : (navCategory || urlFilter);
|
const filter = hasFilterInUrl ? urlFilter : (navCategory || urlFilter);
|
||||||
const activeTarget = this.state.input;
|
const activeTarget = this.state.input;
|
||||||
|
|
||||||
if (!hasFilterInUrl && navCategory) {
|
if (!hasFilterInUrl && navCategory) {
|
||||||
const syncedUrl = buildSearchUrl(activeTarget, filter, offset);
|
const syncedUrl = buildSearchUrl(activeTarget, filter, offset, {
|
||||||
|
brand: urlBrand,
|
||||||
|
supplier: urlSupplier,
|
||||||
|
});
|
||||||
setLastSearchTerm(activeTarget, syncedUrl);
|
setLastSearchTerm(activeTarget, syncedUrl);
|
||||||
this.props.navigate(syncedUrl, { replace: true });
|
this.props.navigate(syncedUrl, { replace: true });
|
||||||
} else if (hasFilterInUrl) {
|
} else if (hasFilterInUrl) {
|
||||||
@@ -357,10 +399,18 @@ class ArtikelSearchPage extends Component {
|
|||||||
setLastCategoryId(primaryCatId);
|
setLastCategoryId(primaryCatId);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLastSearchTerm(activeTarget, buildSearchUrl(activeTarget, filter, offset));
|
setLastSearchTerm(
|
||||||
|
activeTarget,
|
||||||
|
buildSearchUrl(activeTarget, filter, offset, {
|
||||||
|
brand: urlBrand,
|
||||||
|
supplier: urlSupplier,
|
||||||
|
}),
|
||||||
|
);
|
||||||
void articleSearchAutomat.actions.search({
|
void articleSearchAutomat.actions.search({
|
||||||
target: activeTarget,
|
target: activeTarget,
|
||||||
filter,
|
filter,
|
||||||
|
brand: urlBrand,
|
||||||
|
supplier: urlSupplier,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -369,10 +419,20 @@ class ArtikelSearchPage extends Component {
|
|||||||
document.removeEventListener('mouseup', preserveSearchFocus, true);
|
document.removeEventListener('mouseup', preserveSearchFocus, true);
|
||||||
this.unsubSearch?.();
|
this.unsubSearch?.();
|
||||||
this.unsubSearch = null;
|
this.unsubSearch = null;
|
||||||
|
this.unsubBrand?.();
|
||||||
|
this.unsubBrand = null;
|
||||||
|
this.unsubSupplier?.();
|
||||||
|
this.unsubSupplier = null;
|
||||||
this.persistQuery();
|
this.persistQuery();
|
||||||
const query = queryFromParams(this.props.searchParams);
|
const query = queryFromParams(this.props.searchParams);
|
||||||
const target = this.state.input;
|
const target = this.state.input;
|
||||||
setLastSearchTerm(target, buildSearchUrl(target, query.filter, query.offset));
|
setLastSearchTerm(
|
||||||
|
target,
|
||||||
|
buildSearchUrl(target, query.filter, query.offset, {
|
||||||
|
brand: query.brand,
|
||||||
|
supplier: query.supplier,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@@ -389,6 +449,8 @@ class ArtikelSearchPage extends Component {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
searchParams.get('filter') !== prev.get('filter')
|
searchParams.get('filter') !== prev.get('filter')
|
||||||
|
|| searchParams.get('brand') !== prev.get('brand')
|
||||||
|
|| searchParams.get('supplier') !== prev.get('supplier')
|
||||||
|| searchParams.get('offset') !== prev.get('offset')
|
|| searchParams.get('offset') !== prev.get('offset')
|
||||||
) {
|
) {
|
||||||
this.fetchResults();
|
this.fetchResults();
|
||||||
@@ -397,13 +459,24 @@ class ArtikelSearchPage extends Component {
|
|||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Search & Navigation Handlers
|
// #region Search & Navigation Handlers
|
||||||
updateParams = (nextTarget, nextFilter, nextOffset = 0) => {
|
updateParams = (
|
||||||
|
nextTarget,
|
||||||
|
nextFilter,
|
||||||
|
nextOffset = 0,
|
||||||
|
nextBrand = this.props.searchParams.get('brand') ?? '',
|
||||||
|
nextSupplier = this.props.searchParams.get('supplier') ?? '',
|
||||||
|
) => {
|
||||||
writeArtikelQuery({
|
writeArtikelQuery({
|
||||||
target: nextTarget,
|
target: nextTarget,
|
||||||
filter: nextFilter,
|
filter: nextFilter,
|
||||||
|
brand: nextBrand,
|
||||||
|
supplier: nextSupplier,
|
||||||
offset: nextOffset,
|
offset: nextOffset,
|
||||||
});
|
});
|
||||||
const nextUrl = buildSearchUrl(nextTarget, nextFilter, nextOffset);
|
const nextUrl = buildSearchUrl(nextTarget, nextFilter, nextOffset, {
|
||||||
|
brand: nextBrand,
|
||||||
|
supplier: nextSupplier,
|
||||||
|
});
|
||||||
setLastSearchTerm(nextTarget, nextUrl);
|
setLastSearchTerm(nextTarget, nextUrl);
|
||||||
|
|
||||||
// Synchronize category with navigationAutomat and /kategorien/
|
// Synchronize category with navigationAutomat and /kategorien/
|
||||||
@@ -427,14 +500,16 @@ class ArtikelSearchPage extends Component {
|
|||||||
handleInputChange = (event) => {
|
handleInputChange = (event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
this.setState({ input: value });
|
this.setState({ input: value });
|
||||||
const { filter } = queryFromParams(this.props.searchParams);
|
const { filter, brand, supplier } = queryFromParams(this.props.searchParams);
|
||||||
const currentUrl = buildSearchUrl(value, filter, 0);
|
const currentUrl = buildSearchUrl(value, filter, 0, { brand, supplier });
|
||||||
setLastSearchTerm(value, currentUrl);
|
setLastSearchTerm(value, currentUrl);
|
||||||
writeArtikelQuery({ target: value, filter, offset: 0 });
|
writeArtikelQuery({ target: value, filter, brand, supplier, offset: 0 });
|
||||||
this.props.navigate(currentUrl, { replace: true });
|
this.props.navigate(currentUrl, { replace: true });
|
||||||
void articleSearchAutomat.actions.search({
|
void articleSearchAutomat.actions.search({
|
||||||
target: value,
|
target: value,
|
||||||
filter,
|
filter,
|
||||||
|
brand,
|
||||||
|
supplier,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
};
|
};
|
||||||
@@ -444,13 +519,15 @@ class ArtikelSearchPage extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchResults = () => {
|
fetchResults = () => {
|
||||||
const { target, filter, offset } = queryFromParams(this.props.searchParams);
|
const { target, filter, brand, supplier, offset } = queryFromParams(this.props.searchParams);
|
||||||
|
|
||||||
writeArtikelQuery({ target, filter, offset });
|
writeArtikelQuery({ target, filter, brand, supplier, offset });
|
||||||
|
|
||||||
void articleSearchAutomat.actions.search({
|
void articleSearchAutomat.actions.search({
|
||||||
target: target.trim(),
|
target: target.trim(),
|
||||||
filter,
|
filter,
|
||||||
|
brand,
|
||||||
|
supplier,
|
||||||
offset,
|
offset,
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
};
|
};
|
||||||
@@ -461,13 +538,22 @@ class ArtikelSearchPage extends Component {
|
|||||||
const {
|
const {
|
||||||
input,
|
input,
|
||||||
categoryFilterOpen,
|
categoryFilterOpen,
|
||||||
|
brandFilterOpen,
|
||||||
|
supplierFilterOpen,
|
||||||
error,
|
error,
|
||||||
totalCount,
|
totalCount,
|
||||||
categoryFacets,
|
categoryFacets,
|
||||||
|
brandFacets,
|
||||||
|
supplierFacets,
|
||||||
recordset,
|
recordset,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const filterParam = this.props.searchParams.get('filter') ?? '';
|
const filterParam = this.props.searchParams.get('filter') ?? '';
|
||||||
|
const brandParam = this.props.searchParams.get('brand') ?? '';
|
||||||
|
const supplierParam = this.props.searchParams.get('supplier') ?? '';
|
||||||
|
|
||||||
const categories = parseFilter(filterParam);
|
const categories = parseFilter(filterParam);
|
||||||
|
const selectedBrands = parseFilter(brandParam);
|
||||||
|
const selectedSuppliers = parseFilter(supplierParam);
|
||||||
const rows = recordset ?? [];
|
const rows = recordset ?? [];
|
||||||
|
|
||||||
const categoriesMap = categoryAutomat.state?.categories;
|
const categoriesMap = categoryAutomat.state?.categories;
|
||||||
@@ -503,6 +589,20 @@ class ArtikelSearchPage extends Component {
|
|||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const brandFacetEntries = brandFacets
|
||||||
|
? Object.values(brandFacets).sort((a, b) => {
|
||||||
|
if (b.count !== a.count) return b.count - a.count;
|
||||||
|
return String(a.name).localeCompare(String(b.name), 'de');
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const supplierFacetEntries = supplierFacets
|
||||||
|
? Object.values(supplierFacets).sort((a, b) => {
|
||||||
|
if (b.count !== a.count) return b.count - a.count;
|
||||||
|
return String(a.name).localeCompare(String(b.name), 'de');
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
// Per-render values the static `renderItem` row reads through the context.
|
// Per-render values the static `renderItem` row reads through the context.
|
||||||
resultRowsContext.categoryFacets = categoryFacets ?? {};
|
resultRowsContext.categoryFacets = categoryFacets ?? {};
|
||||||
resultRowsContext.onOpenArticle = this.handleOpenArticle;
|
resultRowsContext.onOpenArticle = this.handleOpenArticle;
|
||||||
@@ -517,6 +617,7 @@ class ArtikelSearchPage extends Component {
|
|||||||
gap: 1.5,
|
gap: 1.5,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minHeight: 40,
|
minHeight: 40,
|
||||||
|
flexWrap: 'wrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -557,7 +658,7 @@ class ArtikelSearchPage extends Component {
|
|||||||
<Button
|
<Button
|
||||||
variant={categoryFilterOpen ? 'contained' : 'outlined'}
|
variant={categoryFilterOpen ? 'contained' : 'outlined'}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
disabled={facetEntries.length === 0}
|
disabled={facetEntries.length === 0 && categories.length === 0}
|
||||||
onClick={this.toggleCategoryFilter}
|
onClick={this.toggleCategoryFilter}
|
||||||
startIcon={<FilterListIcon />}
|
startIcon={<FilterListIcon />}
|
||||||
endIcon={categoryFilterOpen ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
endIcon={categoryFilterOpen ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||||
@@ -565,6 +666,29 @@ class ArtikelSearchPage extends Component {
|
|||||||
>
|
>
|
||||||
Kategorien
|
Kategorien
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={brandFilterOpen ? 'contained' : 'outlined'}
|
||||||
|
color="inherit"
|
||||||
|
disabled={brandFacetEntries.length === 0 && selectedBrands.length === 0}
|
||||||
|
onClick={this.toggleBrandFilter}
|
||||||
|
startIcon={<FilterListIcon />}
|
||||||
|
endIcon={brandFilterOpen ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||||
|
sx={{ height: 40, whiteSpace: 'nowrap', flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
Marken
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={supplierFilterOpen ? 'contained' : 'outlined'}
|
||||||
|
color="inherit"
|
||||||
|
disabled={supplierFacetEntries.length === 0 && selectedSuppliers.length === 0}
|
||||||
|
onClick={this.toggleSupplierFilter}
|
||||||
|
startIcon={<FilterListIcon />}
|
||||||
|
endIcon={supplierFilterOpen ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||||
|
sx={{ height: 40, whiteSpace: 'nowrap', flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
Lieferanten
|
||||||
|
</Button>
|
||||||
|
|
||||||
{categories.map((catId) => {
|
{categories.map((catId) => {
|
||||||
const label = getCategoryBreadcrumbLabel(
|
const label = getCategoryBreadcrumbLabel(
|
||||||
catId,
|
catId,
|
||||||
@@ -574,13 +698,71 @@ class ArtikelSearchPage extends Component {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
key={catId}
|
key={`cat-${catId}`}
|
||||||
label={label}
|
label={label}
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
const next = categories.filter((id) => id !== catId);
|
const next = categories.filter((id) => id !== catId);
|
||||||
this.updateParams(input, buildFilter(next), 0);
|
this.updateParams(
|
||||||
|
input,
|
||||||
|
buildFilter(next),
|
||||||
|
0,
|
||||||
|
buildFilter(selectedBrands),
|
||||||
|
buildFilter(selectedSuppliers),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
sx={{ height: 40, borderRadius: 1.5, fontWeight: 500, flexShrink: 0 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{selectedBrands.map((brandId) => {
|
||||||
|
const label =
|
||||||
|
brandFacets?.[brandId]?.name ||
|
||||||
|
brandAutomat.state.brands?.find((b) => String(b.id) === String(brandId) || b.name === brandId)?.name ||
|
||||||
|
brandId;
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
key={`brand-${brandId}`}
|
||||||
|
label={label}
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
onDelete={() => {
|
||||||
|
const next = selectedBrands.filter((id) => id !== brandId);
|
||||||
|
this.updateParams(
|
||||||
|
input,
|
||||||
|
buildFilter(categories),
|
||||||
|
0,
|
||||||
|
buildFilter(next),
|
||||||
|
buildFilter(selectedSuppliers),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
sx={{ height: 40, borderRadius: 1.5, fontWeight: 500, flexShrink: 0 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{selectedSuppliers.map((supplierId) => {
|
||||||
|
const label =
|
||||||
|
supplierFacets?.[supplierId]?.name ||
|
||||||
|
supplierAutomat.state.suppliers?.find((s) => String(s.id) === String(supplierId))?.name ||
|
||||||
|
`Lieferant #${supplierId}`;
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
key={`sup-${supplierId}`}
|
||||||
|
label={label}
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
onDelete={() => {
|
||||||
|
const next = selectedSuppliers.filter((id) => id !== supplierId);
|
||||||
|
this.updateParams(
|
||||||
|
input,
|
||||||
|
buildFilter(categories),
|
||||||
|
0,
|
||||||
|
buildFilter(selectedBrands),
|
||||||
|
buildFilter(next),
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
sx={{ height: 40, borderRadius: 1.5, fontWeight: 500, flexShrink: 0 }}
|
sx={{ height: 40, borderRadius: 1.5, fontWeight: 500, flexShrink: 0 }}
|
||||||
/>
|
/>
|
||||||
@@ -602,8 +784,6 @@ class ArtikelSearchPage extends Component {
|
|||||||
sx={{
|
sx={{
|
||||||
width: 240,
|
width: 240,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
// No intrinsic height: the results tile drives the row height
|
|
||||||
// and the absolutely-filled panel matches it exactly.
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -616,8 +796,6 @@ class ArtikelSearchPage extends Component {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
// Match PanelTile (PANEL_TILE_BORDER_RADIUS: borderRadius 4 →
|
|
||||||
// theme.shape.borderRadius × 4 = 16px) so both columns line up.
|
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -654,7 +832,13 @@ class ArtikelSearchPage extends Component {
|
|||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const next = selected ? [] : [id];
|
const next = selected ? [] : [id];
|
||||||
this.updateParams(input, buildFilter(next), 0);
|
this.updateParams(
|
||||||
|
input,
|
||||||
|
buildFilter(next),
|
||||||
|
0,
|
||||||
|
buildFilter(selectedBrands),
|
||||||
|
buildFilter(selectedSuppliers),
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
sx={{ pl: 2 + depth * 1.5 }}
|
sx={{ pl: 2 + depth * 1.5 }}
|
||||||
>
|
>
|
||||||
@@ -673,6 +857,167 @@ class ArtikelSearchPage extends Component {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{brandFilterOpen && brandFacetEntries.length > 0 && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 240,
|
||||||
|
flexShrink: 0,
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
py: 1.5,
|
||||||
|
bgcolor: 'action.hover',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||||
|
Marken
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={this.toggleBrandFilter}
|
||||||
|
title="Marken einklappen"
|
||||||
|
aria-label="Marken einklappen"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<List dense disablePadding sx={{ flexGrow: 1, overflowY: 'auto' }}>
|
||||||
|
{brandFacetEntries.map(({ id, count, name, brand }) => {
|
||||||
|
const selected = selectedBrands.includes(String(id));
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
key={id}
|
||||||
|
selected={selected}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onClick={() => {
|
||||||
|
const next = selected ? [] : [String(id)];
|
||||||
|
this.updateParams(
|
||||||
|
input,
|
||||||
|
buildFilter(categories),
|
||||||
|
0,
|
||||||
|
buildFilter(next),
|
||||||
|
buildFilter(selectedSuppliers),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
sx={{ pl: 2 }}
|
||||||
|
>
|
||||||
|
{brand ? (
|
||||||
|
<Box sx={{ mr: 1, display: 'inline-flex', alignItems: 'center' }}>
|
||||||
|
<BrandThumb brand={brand} size={18} />
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
<ListItemText
|
||||||
|
primary={`${name} (${count})`}
|
||||||
|
slotProps={{ primary: { variant: 'body2', noWrap: true } }}
|
||||||
|
/>
|
||||||
|
{selected && (
|
||||||
|
<ClearIcon fontSize="small" color="action" sx={{ ml: 1, flexShrink: 0 }} />
|
||||||
|
)}
|
||||||
|
</ListItemButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{supplierFilterOpen && supplierFacetEntries.length > 0 && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 240,
|
||||||
|
flexShrink: 0,
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
py: 1.5,
|
||||||
|
bgcolor: 'action.hover',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||||
|
Lieferanten
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={this.toggleSupplierFilter}
|
||||||
|
title="Lieferanten einklappen"
|
||||||
|
aria-label="Lieferanten einklappen"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<List dense disablePadding sx={{ flexGrow: 1, overflowY: 'auto' }}>
|
||||||
|
{supplierFacetEntries.map(({ id, count, name }) => {
|
||||||
|
const selected = selectedSuppliers.includes(String(id));
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
key={id}
|
||||||
|
selected={selected}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onClick={() => {
|
||||||
|
const next = selected ? [] : [String(id)];
|
||||||
|
this.updateParams(
|
||||||
|
input,
|
||||||
|
buildFilter(categories),
|
||||||
|
0,
|
||||||
|
buildFilter(selectedBrands),
|
||||||
|
buildFilter(next),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
sx={{ pl: 2 }}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={`${name} (${count})`}
|
||||||
|
slotProps={{ primary: { variant: 'body2', noWrap: true } }}
|
||||||
|
/>
|
||||||
|
{selected && (
|
||||||
|
<ClearIcon fontSize="small" color="action" sx={{ ml: 1, flexShrink: 0 }} />
|
||||||
|
)}
|
||||||
|
</ListItemButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||||
{!error && input.trim().length > 0 && totalCount === 0 && (
|
{!error && input.trim().length > 0 && totalCount === 0 && (
|
||||||
<Typography color="text.secondary">Keine Artikel gefunden.</Typography>
|
<Typography color="text.secondary">Keine Artikel gefunden.</Typography>
|
||||||
|
|||||||
Reference in New Issue
Block a user