diff --git a/src/automats/articleIndexAutomat.js b/src/automats/articleIndexAutomat.js
index 8ff9b4d..5feab7d 100644
--- a/src/automats/articleIndexAutomat.js
+++ b/src/automats/articleIndexAutomat.js
@@ -92,10 +92,6 @@ function scheduleRebuild() {
/**
* Normalizes a category-view article row into an index record.
*
- * NOTE: `eans` is not part of the `GET /api/category/:id/articles` payload, so the
- * field stays empty until that endpoint exposes EANs. EAN matching in
- * `articleSearchAutomat` is therefore not functional yet.
- *
* @param {object} article Raw category-view article row.
* @param {number|string|null} categoryId Category the article was loaded from.
* @returns {ArticleIndexRecord} Index record.
@@ -126,11 +122,15 @@ function toIndexRecord(article, categoryId) {
const smallThumbBinary = article.smallThumbBinary ?? firstPhoto?.smallThumbBinary ?? null;
const photos = article.photos ?? (fullHash ? [{ fullHash, thumbHash }] : []);
+ const descLength = Number(article.descLength ?? article.desclength ?? (article.desc ? String(article.desc).trim().length : 0)) || 0;
+
return {
...article,
id: article.id,
sku: article.sku,
name: article.name,
+ descLength,
+ seo: article.seo ?? null,
price,
net: article.net ?? price,
stock,
@@ -143,6 +143,8 @@ function toIndexRecord(article, categoryId) {
categories,
isParent: Boolean(article.isParent ?? article.is_parent ?? (childCount > 0)),
childCount,
+ parentArticleId: article.parentArticleId ?? article.parentarticle_id ?? null,
+ parentSku: article.parentSku ?? article.parent_sku ?? null,
fullHash,
thumbHash,
photos,
@@ -200,6 +202,24 @@ export function buildArticleIndex(views) {
existing.articlewarehousesummary = rec.articlewarehousesummary;
}
}
+ const incomingDescLen = Number(article.descLength ?? article.desclength ?? (article.desc ? String(article.desc).trim().length : 0)) || 0;
+ if ((existing.descLength == null || existing.descLength === 0) && incomingDescLen > 0) {
+ existing.descLength = incomingDescLen;
+ }
+ if ((!existing.seo || existing.seo.trim() === '') && article.seo) {
+ existing.seo = article.seo;
+ }
+ if (Array.isArray(article.eans) && article.eans.length > 0) {
+ const mergedEans = new Set(existing.eans ?? []);
+ for (const e of article.eans) if (e) mergedEans.add(e);
+ existing.eans = Array.from(mergedEans);
+ }
+ if (!existing.parentArticleId && (article.parentArticleId || article.parentarticle_id)) {
+ existing.parentArticleId = article.parentArticleId ?? article.parentarticle_id;
+ }
+ if (!existing.parentSku && (article.parentSku || article.parent_sku)) {
+ existing.parentSku = article.parentSku ?? article.parent_sku;
+ }
}
}
diff --git a/src/automats/articleSearchAutomat.js b/src/automats/articleSearchAutomat.js
index c8131ba..5df0a9b 100644
--- a/src/automats/articleSearchAutomat.js
+++ b/src/automats/articleSearchAutomat.js
@@ -67,6 +67,8 @@ const initialState = {
filter: '',
brand: '',
supplier: '',
+ empty: '',
+ hierarchy: '',
offset: 0,
pageSize: null,
loading: false,
@@ -343,6 +345,8 @@ export function searchArticlesIndex(query, options = {}) {
extraArticles = [],
allowParent = false,
categories = [],
+ hierarchy = '',
+ empty = '',
offset = 0,
pageSize = 10,
limit,
@@ -367,7 +371,7 @@ export function searchArticlesIndex(query, options = {}) {
const excludedSet = new Set((excludeSkus ?? []).map((s) => String(s).toLowerCase()));
let pool = rawMatches;
- if (!allowParent) {
+ if (!allowParent && !hierarchy) {
pool = pool.filter((a) => !a.isParent && !a.is_parent);
}
if (excludedSet.size > 0) {
@@ -378,6 +382,58 @@ export function searchArticlesIndex(query, options = {}) {
pool = pool.filter((a) => a.sku && includedSet.has(String(a.sku).toLowerCase()));
}
+ if (hierarchy === 'father') {
+ pool = pool.filter((a) => !a.parentArticleId && !a.parentSku);
+ } else if (hierarchy === 'child') {
+ pool = pool.filter((a) => !a.isParent && !a.is_parent && Number(a.childCount || 0) === 0);
+ }
+
+ const emptyFilters = parseFilter(empty);
+ if (emptyFilters.length > 0) {
+ const skusWithSupplier = emptyFilters.includes('supplier') ? getSkusWithSupplier() : null;
+ pool = pool.filter((row) => {
+ for (const f of emptyFilters) {
+ if (f === 'ean') {
+ const hasEan = (Array.isArray(row.eans) && row.eans.some((e) => e && String(e).trim())) ||
+ (row.ean && String(row.ean).trim());
+ if (hasEan) return false;
+ } else if (f === 'photo') {
+ const firstPhoto = Array.isArray(row.photos) && row.photos.length > 0 ? row.photos[0] : null;
+ const hasPhoto = Boolean(
+ row.fullHash ||
+ row.thumbHash ||
+ row.smallThumbBinary ||
+ firstPhoto?.fullHash ||
+ firstPhoto?.thumbHash ||
+ firstPhoto?.smallThumbBinary
+ );
+ if (hasPhoto) return false;
+ } else if (f === 'price') {
+ const p = row.price ?? row.net;
+ const hasPrice = p != null && Number(p) > 0;
+ if (hasPrice) return false;
+ } else if (f === 'brand') {
+ const hasBrand = Boolean(row.brand?.id || row.brand?.name || row.brandId || row.brandName);
+ if (hasBrand) return false;
+ } else if (f === 'supplier') {
+ const hasSupplier = Boolean(
+ (row.suppliers && row.suppliers.length > 0) ||
+ row.supplierId != null ||
+ (skusWithSupplier && skusWithSupplier.has(String(row.sku ?? '').toLowerCase()))
+ );
+ if (hasSupplier) return false;
+ } else if (f === 'seo') {
+ const hasSeo = Boolean(row.seo && String(row.seo).trim());
+ if (hasSeo) return false;
+ } else if (f === 'desc') {
+ const hasDesc = (Number(row.descLength) > 0) || Boolean((row.desc && String(row.desc).trim()) || (row.description && String(row.description).trim()));
+ if (hasDesc) return false;
+ }
+ }
+ return true;
+ });
+ }
+
let matchesList = pool;
if (categories && categories.length > 0) {
@@ -669,6 +725,20 @@ function buildSupplierFacets(matchedArticles) {
return facets;
}
+function getSkusWithSupplier() {
+ const articlesBySupplier = companyAutomat.state?.articlesBySupplier ?? {};
+ const set = new Set();
+ for (const list of Object.values(articlesBySupplier)) {
+ if (Array.isArray(list)) {
+ for (const item of list) {
+ if (item.article?.sku) set.add(String(item.article.sku).toLowerCase());
+ if (item.sku) set.add(String(item.sku).toLowerCase());
+ }
+ }
+ }
+ return set;
+}
+
// #endregion
// #endregion
@@ -689,6 +759,8 @@ const actions = {
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 empty = params.empty !== undefined ? String(params.empty ?? '') : (currentState.empty ?? '');
+ const hierarchy = params.hierarchy !== undefined ? String(params.hierarchy ?? '') : (currentState.hierarchy ?? '');
const pageSize = params.pageSize !== undefined ? params.pageSize : currentState.pageSize;
const offset = params.offset !== undefined ? Math.max(0, Number(params.offset) || 0) : currentState.offset;
@@ -752,6 +824,64 @@ const actions = {
});
}
+ // Apply hierarchy filter:
+ // - "father": "father or normal article" -> brand editable, exclude child variants
+ // - "child": "child or normal article" -> price editable, exclude parents
+ if (hierarchy === 'father') {
+ filteredArticles = filteredArticles.filter((row) => !row.parentArticleId && !row.parentSku);
+ } else if (hierarchy === 'child') {
+ filteredArticles = filteredArticles.filter(
+ (row) => !row.isParent && !row.is_parent && Number(row.childCount || 0) === 0,
+ );
+ }
+
+ // Apply empty filters: articles missing ean, photo, price, brand, supplier, seo, desc
+ const emptyFilters = parseFilter(empty);
+ if (emptyFilters.length > 0) {
+ const skusWithSupplier = emptyFilters.includes('supplier') ? getSkusWithSupplier() : null;
+ filteredArticles = filteredArticles.filter((row) => {
+ for (const f of emptyFilters) {
+ if (f === 'ean') {
+ const hasEan = (Array.isArray(row.eans) && row.eans.some((e) => e && String(e).trim())) ||
+ (row.ean && String(row.ean).trim());
+ if (hasEan) return false;
+ } else if (f === 'photo') {
+ const firstPhoto = Array.isArray(row.photos) && row.photos.length > 0 ? row.photos[0] : null;
+ const hasPhoto = Boolean(
+ row.fullHash ||
+ row.thumbHash ||
+ row.smallThumbBinary ||
+ firstPhoto?.fullHash ||
+ firstPhoto?.thumbHash ||
+ firstPhoto?.smallThumbBinary
+ );
+ if (hasPhoto) return false;
+ } else if (f === 'price') {
+ const p = row.price ?? row.net;
+ const hasPrice = p != null && Number(p) > 0;
+ if (hasPrice) return false;
+ } else if (f === 'brand') {
+ const hasBrand = Boolean(row.brand?.id || row.brand?.name || row.brandId || row.brandName);
+ if (hasBrand) return false;
+ } else if (f === 'supplier') {
+ const hasSupplier = Boolean(
+ (row.suppliers && row.suppliers.length > 0) ||
+ row.supplierId != null ||
+ (skusWithSupplier && skusWithSupplier.has(String(row.sku ?? '').toLowerCase()))
+ );
+ if (hasSupplier) return false;
+ } else if (f === 'seo') {
+ const hasSeo = Boolean(row.seo && String(row.seo).trim());
+ if (hasSeo) return false;
+ } else if (f === 'desc') {
+ const hasDesc = (Number(row.descLength) > 0) || Boolean((row.desc && String(row.desc).trim()) || (row.description && String(row.description).trim()));
+ if (hasDesc) return false;
+ }
+ }
+ return true;
+ });
+ }
+
const sortedArticles = sortArticlesByRelevance(filteredArticles, query);
const totalCount = sortedArticles.length;
@@ -781,6 +911,8 @@ const actions = {
filter,
brand,
supplier,
+ empty,
+ hierarchy,
offset,
pageSize: safePageSize,
loading: articleIndexAutomat.state.status === 'loading',
diff --git a/src/components/article/artikelQuery.js b/src/components/article/artikelQuery.js
index 62232db..ee27397 100644
--- a/src/components/article/artikelQuery.js
+++ b/src/components/article/artikelQuery.js
@@ -6,6 +6,8 @@ export function queryFromParams(searchParams) {
filter: searchParams.get('filter') ?? '',
brand: searchParams.get('brand') ?? '',
supplier: searchParams.get('supplier') ?? '',
+ empty: searchParams.get('empty') ?? '',
+ hierarchy: searchParams.get('hierarchy') ?? '',
offset: Math.max(0, Number(searchParams.get('offset')) || 0),
};
}
@@ -13,12 +15,16 @@ export function queryFromParams(searchParams) {
export function buildSearchUrl(target, filter = '', offset = 0, options = {}) {
let brand = '';
let supplier = '';
+ let empty = '';
+ let hierarchy = '';
if (typeof options === 'string') {
brand = options;
supplier = arguments[4] || '';
} else if (options && typeof options === 'object') {
brand = options.brand || '';
supplier = options.supplier || '';
+ empty = options.empty || '';
+ hierarchy = options.hierarchy || '';
}
const params = new URLSearchParams();
@@ -26,6 +32,8 @@ export function buildSearchUrl(target, filter = '', offset = 0, options = {}) {
if (filter) params.set('filter', filter);
if (brand) params.set('brand', brand);
if (supplier) params.set('supplier', supplier);
+ if (empty) params.set('empty', empty);
+ if (hierarchy) params.set('hierarchy', hierarchy);
if (offset > 0) params.set('offset', String(offset));
const qs = params.toString();
return qs ? `/artikel?${qs}` : '/artikel';
@@ -46,6 +54,8 @@ export function writeArtikelQuery({
offset = 0,
brand = '',
supplier = '',
+ empty = '',
+ hierarchy = '',
}) {
try {
if (!target || target.trim().length < 3) {
@@ -54,7 +64,7 @@ export function writeArtikelQuery({
}
localStorage.setItem(
ARTIKEL_QUERY_KEY,
- JSON.stringify({ target, filter, offset, brand, supplier }),
+ JSON.stringify({ target, filter, offset, brand, supplier, empty, hierarchy }),
);
} catch {
// ignore quota / private mode
@@ -76,5 +86,7 @@ export function artikelQueryPath() {
return buildSearchUrl(query.target, query.filter ?? '', query.offset ?? 0, {
brand: query.brand ?? '',
supplier: query.supplier ?? '',
+ empty: query.empty ?? '',
+ hierarchy: query.hierarchy ?? '',
});
}
diff --git a/src/routes/artikel.jsx b/src/routes/artikel.jsx
index 20986bf..3db5d25 100644
--- a/src/routes/artikel.jsx
+++ b/src/routes/artikel.jsx
@@ -11,11 +11,19 @@ import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import Paper from '@mui/material/Paper';
+import Popover from '@mui/material/Popover';
+import Badge from '@mui/material/Badge';
+import Checkbox from '@mui/material/Checkbox';
+import Radio from '@mui/material/Radio';
+import RadioGroup from '@mui/material/RadioGroup';
+import FormControlLabel from '@mui/material/FormControlLabel';
+import Divider from '@mui/material/Divider';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear';
import FilterListIcon from '@mui/icons-material/FilterList';
+import FilterAltIcon from '@mui/icons-material/FilterAlt';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
@@ -265,6 +273,28 @@ class ResultRow extends Component {
}
// #endregion
+// #region Constants
+const EMPTY_FILTER_OPTIONS = [
+ { key: 'ean', label: 'EAN' },
+ { key: 'photo', label: 'Foto' },
+ { key: 'price', label: 'Preis' },
+ { key: 'brand', label: 'Marke' },
+ { key: 'supplier', label: 'Lieferant' },
+ { key: 'seo', label: 'SEO' },
+ { key: 'desc', label: 'Beschreibung' },
+];
+
+const EMPTY_FILTER_LABELS = {
+ ean: 'Ohne EAN',
+ photo: 'Ohne Foto',
+ price: 'Ohne Preis',
+ brand: 'Ohne Marke',
+ supplier: 'Ohne Lieferant',
+ seo: 'Ohne SEO',
+ desc: 'Ohne Beschreibung',
+};
+// #endregion
+
// #region Helpers
function initialSearchState(searchParams) {
const query = queryFromParams(searchParams);
@@ -284,6 +314,7 @@ function initialSearchState(searchParams) {
categoryFilterOpen: false,
brandFilterOpen: false,
supplierFilterOpen: false,
+ moreFilterAnchorEl: null,
newArticleOpen: false,
error: automatResult.error,
categoryFacets: automatResult.categoryFacets,
@@ -357,6 +388,73 @@ class ArtikelSearchPage extends Component {
}));
};
+ handleOpenMoreFilters = (event) => {
+ this.setState({ moreFilterAnchorEl: event.currentTarget });
+ };
+
+ handleCloseMoreFilters = () => {
+ this.setState({ moreFilterAnchorEl: null });
+ };
+
+ handleToggleEmptyFilter = (emptyKey) => {
+ const { input } = this.state;
+ const filterParam = this.props.searchParams.get('filter') ?? '';
+ const brandParam = this.props.searchParams.get('brand') ?? '';
+ const supplierParam = this.props.searchParams.get('supplier') ?? '';
+ const emptyParam = this.props.searchParams.get('empty') ?? '';
+ const hierarchyParam = this.props.searchParams.get('hierarchy') ?? '';
+ const selectedEmpty = parseFilter(emptyParam);
+ const nextEmpty = selectedEmpty.includes(emptyKey)
+ ? selectedEmpty.filter((k) => k !== emptyKey)
+ : [...selectedEmpty, emptyKey];
+
+ this.updateParams(
+ input,
+ filterParam,
+ 0,
+ brandParam,
+ supplierParam,
+ buildFilter(nextEmpty),
+ hierarchyParam,
+ );
+ };
+
+ handleHierarchyChange = (nextHierarchy) => {
+ const { input } = this.state;
+ const filterParam = this.props.searchParams.get('filter') ?? '';
+ const brandParam = this.props.searchParams.get('brand') ?? '';
+ const supplierParam = this.props.searchParams.get('supplier') ?? '';
+ const emptyParam = this.props.searchParams.get('empty') ?? '';
+ const hierarchy = nextHierarchy === 'all' ? '' : nextHierarchy;
+
+ this.updateParams(
+ input,
+ filterParam,
+ 0,
+ brandParam,
+ supplierParam,
+ emptyParam,
+ hierarchy,
+ );
+ };
+
+ handleClearAllMoreFilters = () => {
+ const { input } = this.state;
+ const filterParam = this.props.searchParams.get('filter') ?? '';
+ const brandParam = this.props.searchParams.get('brand') ?? '';
+ const supplierParam = this.props.searchParams.get('supplier') ?? '';
+
+ this.updateParams(
+ input,
+ filterParam,
+ 0,
+ brandParam,
+ supplierParam,
+ '',
+ '',
+ );
+ };
+
handleOpenNewArticle = () => {
this.setState({ newArticleOpen: true });
};
@@ -380,14 +478,14 @@ class ArtikelSearchPage extends Component {
parentId,
name: list[0] ?? '',
});
- void articleIndexAutomat.actions.reload().catch(() => {});
+ void articleIndexAutomat.actions.reload().catch(() => { });
if (created) {
this.props.navigate(artikelDetailPath(created));
}
return;
}
const createdList = await categoryAutomat.actions.createArticles({ parentId, names: list });
- void articleIndexAutomat.actions.reload().catch(() => {});
+ void articleIndexAutomat.actions.reload().catch(() => { });
if (createdList?.[0]) {
this.props.navigate(artikelDetailPath(createdList[0]));
}
@@ -406,6 +504,8 @@ class ArtikelSearchPage extends Component {
filter: query.filter,
brand: query.brand,
supplier: query.supplier,
+ empty: query.empty,
+ hierarchy: query.hierarchy,
offset: query.offset,
});
};
@@ -429,6 +529,8 @@ class ArtikelSearchPage extends Component {
offset,
brand: urlBrand,
supplier: urlSupplier,
+ empty: urlEmpty,
+ hierarchy: urlHierarchy,
} = queryFromParams(this.props.searchParams);
const hasFilterInUrl = this.props.searchParams.has('filter');
const navCategory = navigationAutomat.state.lastCategoryId;
@@ -439,6 +541,8 @@ class ArtikelSearchPage extends Component {
const syncedUrl = buildSearchUrl(activeTarget, filter, offset, {
brand: urlBrand,
supplier: urlSupplier,
+ empty: urlEmpty,
+ hierarchy: urlHierarchy,
});
setLastSearchTerm(activeTarget, syncedUrl);
this.props.navigate(syncedUrl, { replace: true });
@@ -453,6 +557,8 @@ class ArtikelSearchPage extends Component {
buildSearchUrl(activeTarget, filter, offset, {
brand: urlBrand,
supplier: urlSupplier,
+ empty: urlEmpty,
+ hierarchy: urlHierarchy,
}),
);
void articleSearchAutomat.actions.search({
@@ -460,8 +566,10 @@ class ArtikelSearchPage extends Component {
filter,
brand: urlBrand,
supplier: urlSupplier,
+ empty: urlEmpty,
+ hierarchy: urlHierarchy,
offset: 0,
- }).catch(() => {});
+ }).catch(() => { });
}
componentWillUnmount() {
@@ -478,6 +586,8 @@ class ArtikelSearchPage extends Component {
buildSearchUrl(target, query.filter, query.offset, {
brand: query.brand,
supplier: query.supplier,
+ empty: query.empty,
+ hierarchy: query.hierarchy,
}),
);
}
@@ -498,6 +608,8 @@ class ArtikelSearchPage extends Component {
searchParams.get('filter') !== prev.get('filter')
|| searchParams.get('brand') !== prev.get('brand')
|| searchParams.get('supplier') !== prev.get('supplier')
+ || searchParams.get('empty') !== prev.get('empty')
+ || searchParams.get('hierarchy') !== prev.get('hierarchy')
|| searchParams.get('offset') !== prev.get('offset')
) {
this.fetchResults();
@@ -512,17 +624,23 @@ class ArtikelSearchPage extends Component {
nextOffset = 0,
nextBrand = this.props.searchParams.get('brand') ?? '',
nextSupplier = this.props.searchParams.get('supplier') ?? '',
+ nextEmpty = this.props.searchParams.get('empty') ?? '',
+ nextHierarchy = this.props.searchParams.get('hierarchy') ?? '',
) => {
writeArtikelQuery({
target: nextTarget,
filter: nextFilter,
brand: nextBrand,
supplier: nextSupplier,
+ empty: nextEmpty,
+ hierarchy: nextHierarchy,
offset: nextOffset,
});
const nextUrl = buildSearchUrl(nextTarget, nextFilter, nextOffset, {
brand: nextBrand,
supplier: nextSupplier,
+ empty: nextEmpty,
+ hierarchy: nextHierarchy,
});
setLastSearchTerm(nextTarget, nextUrl);
@@ -547,18 +665,20 @@ class ArtikelSearchPage extends Component {
handleInputChange = (event) => {
const value = event.target.value;
this.setState({ input: value });
- const { filter, brand, supplier } = queryFromParams(this.props.searchParams);
- const currentUrl = buildSearchUrl(value, filter, 0, { brand, supplier });
+ const { filter, brand, supplier, empty, hierarchy } = queryFromParams(this.props.searchParams);
+ const currentUrl = buildSearchUrl(value, filter, 0, { brand, supplier, empty, hierarchy });
setLastSearchTerm(value, currentUrl);
- writeArtikelQuery({ target: value, filter, brand, supplier, offset: 0 });
+ writeArtikelQuery({ target: value, filter, brand, supplier, empty, hierarchy, offset: 0 });
this.props.navigate(currentUrl, { replace: true });
void articleSearchAutomat.actions.search({
target: value,
filter,
brand,
supplier,
+ empty,
+ hierarchy,
offset: 0,
- }).catch(() => {});
+ }).catch(() => { });
};
handleClearInput = () => {
@@ -566,17 +686,19 @@ class ArtikelSearchPage extends Component {
};
fetchResults = () => {
- const { target, filter, brand, supplier, offset } = queryFromParams(this.props.searchParams);
+ const { target, filter, brand, supplier, empty, hierarchy, offset } = queryFromParams(this.props.searchParams);
- writeArtikelQuery({ target, filter, brand, supplier, offset });
+ writeArtikelQuery({ target, filter, brand, supplier, empty, hierarchy, offset });
void articleSearchAutomat.actions.search({
target: target.trim(),
filter,
brand,
supplier,
+ empty,
+ hierarchy,
offset,
- }).catch(() => {});
+ }).catch(() => { });
};
// #endregion
@@ -587,6 +709,7 @@ class ArtikelSearchPage extends Component {
categoryFilterOpen,
brandFilterOpen,
supplierFilterOpen,
+ moreFilterAnchorEl,
newArticleOpen,
error,
totalCount,
@@ -598,10 +721,15 @@ class ArtikelSearchPage extends Component {
const filterParam = this.props.searchParams.get('filter') ?? '';
const brandParam = this.props.searchParams.get('brand') ?? '';
const supplierParam = this.props.searchParams.get('supplier') ?? '';
+ const emptyParam = this.props.searchParams.get('empty') ?? '';
+ const hierarchyParam = this.props.searchParams.get('hierarchy') ?? '';
const categories = parseFilter(filterParam);
const selectedBrands = parseFilter(brandParam);
const selectedSuppliers = parseFilter(supplierParam);
+ const selectedEmpty = parseFilter(emptyParam);
+ const activeMoreFilterCount = selectedEmpty.length + (hierarchyParam ? 1 : 0);
+ const isAnyMoreFilterActive = activeMoreFilterCount > 0;
const rows = recordset ?? [];
const categoriesMap = categoryAutomat.state?.categories;
@@ -639,16 +767,16 @@ 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');
- })
+ 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');
- })
+ 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.
@@ -736,6 +864,22 @@ class ArtikelSearchPage extends Component {
>
Lieferanten
+
+
+
+
+
);
})}
+
+ {hierarchyParam === 'father' && (
+ {
+ this.updateParams(
+ input,
+ buildFilter(categories),
+ 0,
+ buildFilter(selectedBrands),
+ buildFilter(selectedSuppliers),
+ emptyParam,
+ '',
+ );
+ }}
+ sx={{ height: 40, borderRadius: 1.5, fontWeight: 500, flexShrink: 0 }}
+ />
+ )}
+
+ {hierarchyParam === 'child' && (
+ {
+ this.updateParams(
+ input,
+ buildFilter(categories),
+ 0,
+ buildFilter(selectedBrands),
+ buildFilter(selectedSuppliers),
+ emptyParam,
+ '',
+ );
+ }}
+ sx={{ height: 40, borderRadius: 1.5, fontWeight: 500, flexShrink: 0 }}
+ />
+ )}
+
+ {selectedEmpty.map((emptyKey) => (
+ {
+ const next = selectedEmpty.filter((k) => k !== emptyKey);
+ this.updateParams(
+ input,
+ buildFilter(categories),
+ 0,
+ buildFilter(selectedBrands),
+ buildFilter(selectedSuppliers),
+ buildFilter(next),
+ hierarchyParam,
+ );
+ }}
+ sx={{ height: 40, borderRadius: 1.5, fontWeight: 500, flexShrink: 0 }}
+ />
+ ))}
{error && (
@@ -1100,6 +1314,91 @@ class ArtikelSearchPage extends Component {
onClose={this.handleCloseNewArticle}
onSubmit={this.handleCreateArticle}
/>
+
+
+ {isAnyMoreFilterActive && (
+
+ )}
+
+
+ this.handleHierarchyChange(e.target.value)}
+ sx={{ mb: 1.5, mt: 0.5 }}
+ >
+ }
+ label={Alle Artikel}
+ />
+ }
+ label={
+
+ Vater- oder Normalartikel
+
+ }
+ />
+ }
+ label={
+
+ Kind- oder Normalartikel
+
+ }
+ />
+
+
+
+
+
+ Artikel ohne (leer):
+
+
+ {EMPTY_FILTER_OPTIONS.map(({ key, label }) => {
+ const checked = selectedEmpty.includes(key);
+ return (
+ this.handleToggleEmptyFilter(key)}
+ />
+ }
+ label={{label}}
+ sx={{ my: -0.25 }}
+ />
+ );
+ })}
+
+
>
);
}