This commit is contained in:
seb
2026-10-30 15:45:59 +01:00
parent 3bb861907e
commit d3e16fd2b7
4 changed files with 485 additions and 22 deletions

View File

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

View File

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

View File

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

View File

@@ -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
</Button>
<IconButton
onClick={this.handleOpenMoreFilters}
color={isAnyMoreFilterActive ? 'primary' : 'default'}
sx={{
height: 40,
width: 40,
border: '1px solid',
borderColor: isAnyMoreFilterActive ? 'primary.main' : 'rgba(0, 0, 0, 0.23)',
borderRadius: 1,
flexShrink: 0,
}}
>
<Badge badgeContent={activeMoreFilterCount} color="primary">
<FilterAltIcon fontSize="small" />
</Badge>
</IconButton>
<Button
variant="contained"
onClick={this.handleOpenNewArticle}
@@ -765,6 +909,8 @@ class ArtikelSearchPage extends Component {
0,
buildFilter(selectedBrands),
buildFilter(selectedSuppliers),
emptyParam,
hierarchyParam,
);
}}
sx={{ height: 40, borderRadius: 1.5, fontWeight: 500, flexShrink: 0 }}
@@ -791,6 +937,8 @@ class ArtikelSearchPage extends Component {
0,
buildFilter(next),
buildFilter(selectedSuppliers),
emptyParam,
hierarchyParam,
);
}}
sx={{ height: 40, borderRadius: 1.5, fontWeight: 500, flexShrink: 0 }}
@@ -817,12 +965,78 @@ class ArtikelSearchPage extends Component {
0,
buildFilter(selectedBrands),
buildFilter(next),
emptyParam,
hierarchyParam,
);
}}
sx={{ height: 40, borderRadius: 1.5, fontWeight: 500, flexShrink: 0 }}
/>
);
})}
{hierarchyParam === 'father' && (
<Chip
key="hierarchy-father"
label="Typ: Vater / Normal"
color="primary"
variant="outlined"
onDelete={() => {
this.updateParams(
input,
buildFilter(categories),
0,
buildFilter(selectedBrands),
buildFilter(selectedSuppliers),
emptyParam,
'',
);
}}
sx={{ height: 40, borderRadius: 1.5, fontWeight: 500, flexShrink: 0 }}
/>
)}
{hierarchyParam === 'child' && (
<Chip
key="hierarchy-child"
label="Typ: Kind / Normal"
color="primary"
variant="outlined"
onDelete={() => {
this.updateParams(
input,
buildFilter(categories),
0,
buildFilter(selectedBrands),
buildFilter(selectedSuppliers),
emptyParam,
'',
);
}}
sx={{ height: 40, borderRadius: 1.5, fontWeight: 500, flexShrink: 0 }}
/>
)}
{selectedEmpty.map((emptyKey) => (
<Chip
key={`empty-${emptyKey}`}
label={EMPTY_FILTER_LABELS[emptyKey] || emptyKey}
color="primary"
variant="outlined"
onDelete={() => {
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 }}
/>
))}
</Box>
{error && (
@@ -1100,6 +1314,91 @@ class ArtikelSearchPage extends Component {
onClose={this.handleCloseNewArticle}
onSubmit={this.handleCreateArticle}
/>
<Popover
open={Boolean(moreFilterAnchorEl)}
anchorEl={moreFilterAnchorEl}
onClose={this.handleCloseMoreFilters}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
slotProps={{
paper: {
sx: {
width: 320,
p: 2,
borderRadius: 2,
boxShadow: 4,
},
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
{isAnyMoreFilterActive && (
<Button
size="small"
color="secondary"
onClick={this.handleClearAllMoreFilters}
sx={{ py: 0.25, px: 1, minWidth: 0, textTransform: 'none', fontSize: 13 }}
>
Zurücksetzen
</Button>
)}
</Box>
<RadioGroup
value={hierarchyParam || 'all'}
onChange={(e) => this.handleHierarchyChange(e.target.value)}
sx={{ mb: 1.5, mt: 0.5 }}
>
<FormControlLabel
value="all"
control={<Radio size="small" />}
label={<Typography variant="body2">Alle Artikel</Typography>}
/>
<FormControlLabel
value="father"
control={<Radio size="small" />}
label={
<Box>
<Typography variant="body2">Vater- oder Normalartikel</Typography>
</Box>
}
/>
<FormControlLabel
value="child"
control={<Radio size="small" />}
label={
<Box>
<Typography variant="body2">Kind- oder Normalartikel</Typography>
</Box>
}
/>
</RadioGroup>
<Divider sx={{ my: 1.5 }} />
<Typography variant="caption" sx={{ fontWeight: 600, color: 'text.secondary', textTransform: 'uppercase', letterSpacing: 0.5 }}>
Artikel ohne (leer):
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', mt: 0.5 }}>
{EMPTY_FILTER_OPTIONS.map(({ key, label }) => {
const checked = selectedEmpty.includes(key);
return (
<FormControlLabel
key={key}
control={
<Checkbox
size="small"
checked={checked}
onChange={() => this.handleToggleEmptyFilter(key)}
/>
}
label={<Typography variant="body2">{label}</Typography>}
sx={{ my: -0.25 }}
/>
);
})}
</Box>
</Popover>
</>
);
}