From 64babee90d0a64fc78af070545ef278df7018362 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Fri, 24 Apr 2026 12:34:00 +0200 Subject: [PATCH] feat: upgrade column filters to support up to 5 dynamic terms with AND/OR logic --- src/components/PricingView.tsx | 147 ++++++++++++++++++++------------- 1 file changed, 90 insertions(+), 57 deletions(-) diff --git a/src/components/PricingView.tsx b/src/components/PricingView.tsx index 3d4db04..77c52bf 100644 --- a/src/components/PricingView.tsx +++ b/src/components/PricingView.tsx @@ -80,8 +80,8 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, const [lineMultiFilter, setLineMultiFilter] = useState([]); const [classificationFilter, setClassificationFilter] = useState([]); const [productTypeFilter, setProductTypeFilter] = useState([]); - const [nameColFilter, setNameColFilter] = useState<{ t1: string; t2: string; op: 'and' | 'or' }>({ t1: '', t2: '', op: 'and' }); - const [articleNoColFilter, setArticleNoColFilter] = useState<{ t1: string; t2: string; op: 'and' | 'or' }>({ t1: '', t2: '', op: 'and' }); + const [nameColFilter, setNameColFilter] = useState<{ terms: string[]; op: 'and' | 'or' }>({ terms: [''], op: 'and' }); + const [articleNoColFilter, setArticleNoColFilter] = useState<{ terms: string[]; op: 'and' | 'or' }>({ terms: [''], op: 'and' }); const [openFilter, setOpenFilter] = useState(null); const [unitsOuterFilter, setUnitsOuterFilter] = useState([]); const [outerWFilter, setOuterWFilter] = useState([]); @@ -336,23 +336,29 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, } // Column filters - // Column-specific name filter - Excel-style Custom Filter - if (nameColFilter.t1 || nameColFilter.t2) { + // Column-specific name filter - Multi-term AND/OR + if (nameColFilter.terms.some(t => t.trim() !== '')) { result = result.filter(r => { const name = String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase(); - const m1 = nameColFilter.t1 ? name.includes(nameColFilter.t1.toLowerCase()) : (nameColFilter.op === 'and'); - const m2 = nameColFilter.t2 ? name.includes(nameColFilter.t2.toLowerCase()) : (nameColFilter.op === 'and'); - return nameColFilter.op === 'and' ? (m1 && m2) : (m1 || m2); + const activeTerms = nameColFilter.terms.filter(t => t.trim() !== '').map(t => t.toLowerCase()); + if (activeTerms.length === 0) return true; + + return nameColFilter.op === 'and' + ? activeTerms.every(term => name.includes(term)) + : activeTerms.some(term => name.includes(term)); }); } - // Column-specific SKU filter - Excel-style Custom Filter - if (articleNoColFilter.t1 || articleNoColFilter.t2) { + // Column-specific SKU filter - Multi-term AND/OR + if (articleNoColFilter.terms.some(t => t.trim() !== '')) { result = result.filter(r => { const sku = String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase(); - const m1 = articleNoColFilter.t1 ? sku.includes(articleNoColFilter.t1.toLowerCase()) : (articleNoColFilter.op === 'and'); - const m2 = articleNoColFilter.t2 ? sku.includes(articleNoColFilter.t2.toLowerCase()) : (articleNoColFilter.op === 'and'); - return articleNoColFilter.op === 'and' ? (m1 && m2) : (m1 || m2); + const activeTerms = articleNoColFilter.terms.filter(t => t.trim() !== '').map(t => t.toLowerCase()); + if (activeTerms.length === 0) return true; + + return articleNoColFilter.op === 'and' + ? activeTerms.every(term => sku.includes(term)) + : activeTerms.some(term => sku.includes(term)); }); } if (lineMultiFilter.length > 0) { @@ -935,7 +941,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'sku' ? null : 'sku'); }} className={cn( 'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0', - (articleNoColFilter.t1 || articleNoColFilter.t2) ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100' + articleNoColFilter.terms.some(t => t.trim() !== '') ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100' )} > @@ -957,7 +963,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'name' ? null : 'name'); }} className={cn( 'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0', - (nameColFilter.t1 || nameColFilter.t2) ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100' + nameColFilter.terms.some(t => t.trim() !== '') ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100' )} > @@ -1642,80 +1648,107 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, // ── Text filter popover ─────────────────────────────────────────────────────── function TextFilterPopover({ value, onChange, onClose }: { - value: { t1: string; t2: string; op: 'and' | 'or' }; - onChange: (v: { t1: string; t2: string; op: 'and' | 'or' }) => void; + value: { terms: string[]; op: 'and' | 'or' }; + onChange: (v: { terms: string[]; op: 'and' | 'or' }) => void; onClose: () => void; }) { + const addTerm = () => { + if (value.terms.length < 5) { + onChange({ ...value, terms: [...value.terms, ''] }); + } + }; + + const removeTerm = (index: number) => { + if (value.terms.length > 1) { + const next = [...value.terms]; + next.splice(index, 1); + onChange({ ...value, terms: next }); + } else { + onChange({ ...value, terms: [''] }); + } + }; + + const updateTerm = (index: number, text: string) => { + const next = [...value.terms]; + next[index] = text; + onChange({ ...value, terms: next }); + }; + return ( -
+
e.stopPropagation()}>
-
- -
- - onChange({ ...value, t1: e.target.value })} - autoFocus - className="w-full bg-slate-900 border border-slate-700 rounded-md p-2 pl-8 text-xs text-white focus:outline-none focus:border-blue-500 transition-all" - /> -
+ + +
+ {value.terms.map((term, idx) => ( +
+ + updateTerm(idx, e.target.value)} + autoFocus={idx === value.terms.length - 1} + className="w-full bg-slate-900 border border-slate-700 rounded-md p-2 pl-8 pr-8 text-xs text-white focus:outline-none focus:border-blue-500 transition-all" + /> + {value.terms.length > 1 && ( + + )} +
+ ))}
-
+ {value.terms.length < 5 && ( + + )} + +
- -
-
- - onChange({ ...value, t2: e.target.value })} - className="w-full bg-slate-900 border border-slate-700 rounded-md p-2 pl-8 text-xs text-white focus:outline-none focus:border-blue-500 transition-all" - /> -
-
-
- -
+
);