mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 18:15:23 +02:00
feat: upgrade column filters to support up to 5 dynamic terms with AND/OR logic
This commit is contained in:
@@ -80,8 +80,8 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
const [lineMultiFilter, setLineMultiFilter] = useState<string[]>([]);
|
const [lineMultiFilter, setLineMultiFilter] = useState<string[]>([]);
|
||||||
const [classificationFilter, setClassificationFilter] = useState<string[]>([]);
|
const [classificationFilter, setClassificationFilter] = useState<string[]>([]);
|
||||||
const [productTypeFilter, setProductTypeFilter] = useState<string[]>([]);
|
const [productTypeFilter, setProductTypeFilter] = useState<string[]>([]);
|
||||||
const [nameColFilter, setNameColFilter] = 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<{ t1: string; t2: string; op: 'and' | 'or' }>({ t1: '', t2: '', op: 'and' });
|
const [articleNoColFilter, setArticleNoColFilter] = useState<{ terms: string[]; op: 'and' | 'or' }>({ terms: [''], op: 'and' });
|
||||||
const [openFilter, setOpenFilter] = useState<string | null>(null);
|
const [openFilter, setOpenFilter] = useState<string | null>(null);
|
||||||
const [unitsOuterFilter, setUnitsOuterFilter] = useState<string[]>([]);
|
const [unitsOuterFilter, setUnitsOuterFilter] = useState<string[]>([]);
|
||||||
const [outerWFilter, setOuterWFilter] = useState<string[]>([]);
|
const [outerWFilter, setOuterWFilter] = useState<string[]>([]);
|
||||||
@@ -336,23 +336,29 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Column filters
|
// Column filters
|
||||||
// Column-specific name filter - Excel-style Custom Filter
|
// Column-specific name filter - Multi-term AND/OR
|
||||||
if (nameColFilter.t1 || nameColFilter.t2) {
|
if (nameColFilter.terms.some(t => t.trim() !== '')) {
|
||||||
result = result.filter(r => {
|
result = result.filter(r => {
|
||||||
const name = String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase();
|
const name = String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase();
|
||||||
const m1 = nameColFilter.t1 ? name.includes(nameColFilter.t1.toLowerCase()) : (nameColFilter.op === 'and');
|
const activeTerms = nameColFilter.terms.filter(t => t.trim() !== '').map(t => t.toLowerCase());
|
||||||
const m2 = nameColFilter.t2 ? name.includes(nameColFilter.t2.toLowerCase()) : (nameColFilter.op === 'and');
|
if (activeTerms.length === 0) return true;
|
||||||
return nameColFilter.op === 'and' ? (m1 && m2) : (m1 || m2);
|
|
||||||
|
return nameColFilter.op === 'and'
|
||||||
|
? activeTerms.every(term => name.includes(term))
|
||||||
|
: activeTerms.some(term => name.includes(term));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Column-specific SKU filter - Excel-style Custom Filter
|
// Column-specific SKU filter - Multi-term AND/OR
|
||||||
if (articleNoColFilter.t1 || articleNoColFilter.t2) {
|
if (articleNoColFilter.terms.some(t => t.trim() !== '')) {
|
||||||
result = result.filter(r => {
|
result = result.filter(r => {
|
||||||
const sku = String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase();
|
const sku = String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase();
|
||||||
const m1 = articleNoColFilter.t1 ? sku.includes(articleNoColFilter.t1.toLowerCase()) : (articleNoColFilter.op === 'and');
|
const activeTerms = articleNoColFilter.terms.filter(t => t.trim() !== '').map(t => t.toLowerCase());
|
||||||
const m2 = articleNoColFilter.t2 ? sku.includes(articleNoColFilter.t2.toLowerCase()) : (articleNoColFilter.op === 'and');
|
if (activeTerms.length === 0) return true;
|
||||||
return articleNoColFilter.op === 'and' ? (m1 && m2) : (m1 || m2);
|
|
||||||
|
return articleNoColFilter.op === 'and'
|
||||||
|
? activeTerms.every(term => sku.includes(term))
|
||||||
|
: activeTerms.some(term => sku.includes(term));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (lineMultiFilter.length > 0) {
|
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'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'sku' ? null : 'sku'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
'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'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Filter className="w-3 h-3" />
|
<Filter className="w-3 h-3" />
|
||||||
@@ -957,7 +963,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'name' ? null : 'name'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'name' ? null : 'name'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
'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'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Filter className="w-3 h-3" />
|
<Filter className="w-3 h-3" />
|
||||||
@@ -1642,80 +1648,107 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
|
|
||||||
// ── Text filter popover ───────────────────────────────────────────────────────
|
// ── Text filter popover ───────────────────────────────────────────────────────
|
||||||
function TextFilterPopover({ value, onChange, onClose }: {
|
function TextFilterPopover({ value, onChange, onClose }: {
|
||||||
value: { t1: string; t2: string; op: 'and' | 'or' };
|
value: { terms: string[]; op: 'and' | 'or' };
|
||||||
onChange: (v: { t1: string; t2: string; op: 'and' | 'or' }) => void;
|
onChange: (v: { terms: string[]; op: 'and' | 'or' }) => void;
|
||||||
onClose: () => 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 (
|
return (
|
||||||
<div className="absolute top-full left-0 mt-1 w-64 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl z-50 p-4 flex flex-col gap-4 animate-in fade-in zoom-in-95 duration-100">
|
<div className="absolute top-full left-0 mt-1 w-72 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl z-50 p-4 flex flex-col gap-4 animate-in fade-in zoom-in-95 duration-100" onClick={e => e.stopPropagation()}>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex flex-col gap-1.5">
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Show rows where field contains:</label>
|
||||||
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Show rows where name contains:</label>
|
|
||||||
<div className="relative">
|
<div className="flex flex-col gap-2.5">
|
||||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-400" />
|
{value.terms.map((term, idx) => (
|
||||||
<input
|
<div key={idx} className="relative group">
|
||||||
type="text"
|
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-500" />
|
||||||
placeholder="First term..."
|
<input
|
||||||
value={value.t1}
|
type="text"
|
||||||
onChange={e => onChange({ ...value, t1: e.target.value })}
|
placeholder={`Term ${idx + 1}...`}
|
||||||
autoFocus
|
value={term}
|
||||||
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"
|
onChange={e => updateTerm(idx, e.target.value)}
|
||||||
/>
|
autoFocus={idx === value.terms.length - 1}
|
||||||
</div>
|
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 && (
|
||||||
|
<button
|
||||||
|
onClick={() => removeTerm(idx)}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-slate-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-all"
|
||||||
|
>
|
||||||
|
<X className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 px-1">
|
{value.terms.length < 5 && (
|
||||||
|
<button
|
||||||
|
onClick={addTerm}
|
||||||
|
className="flex items-center gap-1.5 text-[10px] font-bold text-blue-400 hover:text-blue-300 transition-colors w-fit px-1"
|
||||||
|
>
|
||||||
|
+ Add another term (max 5)
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 px-1 py-1 bg-slate-900/30 rounded-md">
|
||||||
<label className="flex items-center gap-2 cursor-pointer group">
|
<label className="flex items-center gap-2 cursor-pointer group">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="op"
|
name={`op-${value.terms.length}`}
|
||||||
checked={value.op === 'and'}
|
checked={value.op === 'and'}
|
||||||
onChange={() => onChange({ ...value, op: 'and' })}
|
onChange={() => onChange({ ...value, op: 'and' })}
|
||||||
className="w-3 h-3 text-blue-600 bg-slate-900 border-slate-700 focus:ring-blue-500"
|
className="w-3 h-3 text-blue-600 bg-slate-900 border-slate-700 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<span className={cn("text-[10px] font-bold transition-colors", value.op === 'and' ? "text-blue-400" : "text-slate-500 group-hover:text-slate-300")}>AND</span>
|
<span className={cn("text-[10px] font-bold transition-colors", value.op === 'and' ? "text-blue-400" : "text-slate-500 group-hover:text-slate-300")}>AND (All match)</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2 cursor-pointer group">
|
<label className="flex items-center gap-2 cursor-pointer group">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="op"
|
name={`op-${value.terms.length}`}
|
||||||
checked={value.op === 'or'}
|
checked={value.op === 'or'}
|
||||||
onChange={() => onChange({ ...value, op: 'or' })}
|
onChange={() => onChange({ ...value, op: 'or' })}
|
||||||
className="w-3 h-3 text-blue-600 bg-slate-900 border-slate-700 focus:ring-blue-500"
|
className="w-3 h-3 text-blue-600 bg-slate-900 border-slate-700 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<span className={cn("text-[10px] font-bold transition-colors", value.op === 'or' ? "text-blue-400" : "text-slate-500 group-hover:text-slate-300")}>OR</span>
|
<span className={cn("text-[10px] font-bold transition-colors", value.op === 'or' ? "text-blue-400" : "text-slate-500 group-hover:text-slate-300")}>OR (Any match)</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-400" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Second term..."
|
|
||||||
value={value.t2}
|
|
||||||
onChange={e => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between pt-2 border-t border-slate-700/50">
|
<div className="flex items-center justify-between pt-2 border-t border-slate-700/50">
|
||||||
<button
|
<button
|
||||||
onClick={() => { onChange({ t1: '', t2: '', op: 'and' }); onClose(); }}
|
onClick={() => { onChange({ terms: [''], op: 'and' }); onClose(); }}
|
||||||
className="text-[10px] font-bold text-slate-500 hover:text-red-400 transition-colors"
|
className="text-[10px] font-bold text-slate-500 hover:text-red-400 transition-colors"
|
||||||
>
|
>
|
||||||
Clear All
|
Clear All
|
||||||
</button>
|
</button>
|
||||||
<div className="flex gap-2">
|
<button
|
||||||
<button
|
onClick={onClose}
|
||||||
onClick={onClose}
|
className="px-4 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-[10px] font-bold rounded shadow-lg shadow-blue-900/20 active:scale-95 transition-all"
|
||||||
className="px-4 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-[10px] font-bold rounded shadow-lg shadow-blue-900/20 active:scale-95 transition-all"
|
>
|
||||||
>
|
Apply Filter
|
||||||
Apply Filter
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user