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