From 786c66fd8734eb39f13516203c9da51c7cbac40c Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Wed, 8 Apr 2026 16:50:53 +0200 Subject: [PATCH] feat: add Select All to column filters and refine views --- src/components/ArticleDetails.tsx | 186 +++++++++++++++++++++++-- src/components/PricingView.tsx | 31 ++++- src/components/ProductDescriptions.tsx | 34 ++++- 3 files changed, 241 insertions(+), 10 deletions(-) diff --git a/src/components/ArticleDetails.tsx b/src/components/ArticleDetails.tsx index 893ef21..1456378 100644 --- a/src/components/ArticleDetails.tsx +++ b/src/components/ArticleDetails.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo } from 'react'; import { ExcelRow, COLUMNS } from '../types'; -import { Search, Filter, Edit2, ChevronDown, ChevronUp, Info, Package } from 'lucide-react'; +import { Search, Filter, Edit2, ChevronDown, ChevronUp, Info, Package, X as XIcon, Check } from 'lucide-react'; import { cn } from '../lib/utils'; interface ArticleDetailsProps { @@ -18,6 +18,8 @@ export function ArticleDetails({ data, onEdit }: ArticleDetailsProps) { const [sortDesc, setSortDesc] = useState(false); const [pageSize, setPageSize] = useState(25); const [page, setPage] = useState(1); + const [columnFilters, setColumnFilters] = useState>({}); + const [openFilterCol, setOpenFilterCol] = useState(null); const lines = useMemo(() => Array.from(new Set(data.map(r => r[COLUMNS.LINE]).filter(Boolean))), [data]); @@ -42,6 +44,14 @@ export function ArticleDetails({ data, onEdit }: ArticleDetailsProps) { // Dropdown filters if (lineFilter) result = result.filter(r => r.row[COLUMNS.LINE] === lineFilter); + // Column-specific filters (Excel-like) + Object.entries(columnFilters).forEach(([colIdx, selectedValues]) => { + const vals = selectedValues as string[]; + if (vals.length > 0) { + result = result.filter(r => vals.includes(String(r.row[Number(colIdx)] || ''))); + } + }); + // Sorting if (sortCol !== null) { result.sort((a, b) => { @@ -77,6 +87,27 @@ export function ArticleDetails({ data, onEdit }: ArticleDetailsProps) { } }; + const getUniqueValues = (col: number) => { + const values = data.map(r => String(r[col] || '')); + return Array.from(new Set(values)).sort(); + }; + + const toggleColumnFilter = (col: number, value: string) => { + setColumnFilters(prev => { + const current = prev[col] || []; + const next = current.includes(value) + ? current.filter(v => v !== value) + : [...current, value]; + return { ...prev, [col]: next }; + }); + setPage(1); + }; + + const setBatchColumnFilter = (col: number, values: string[]) => { + setColumnFilters(prev => ({ ...prev, [col]: values })); + setPage(1); + }; + const getBadge = (val: any, type: 'success' | 'warning' | 'error' | 'info' = 'info') => { if (!val) return Empty; @@ -128,6 +159,18 @@ export function ArticleDetails({ data, onEdit }: ArticleDetailsProps) { className="w-full pl-9 pr-4 py-2 bg-slate-900/50 border border-slate-700 rounded-md text-sm text-white focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500" /> + {(Object.values(columnFilters) as string[][]).some(v => v.length > 0) && ( + + )} setSearch(e.target.value)} + className="w-full bg-slate-900 border border-slate-700 rounded p-1.5 pl-8 text-xs text-white focus:outline-none focus:border-indigo-500" + autoFocus + /> + + +
+ + {selectedValues.length} selected +
+ +
+ {filteredValues.map(val => ( + + ))} + {filteredValues.length === 0 && ( +
No values found
+ )} +
+ +
+ + +
+ + ); +} diff --git a/src/components/PricingView.tsx b/src/components/PricingView.tsx index 96a5868..a4baedb 100644 --- a/src/components/PricingView.tsx +++ b/src/components/PricingView.tsx @@ -387,6 +387,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit } uniqueValues={uniqueLines} selectedValues={lineMultiFilter} onToggle={val => setLineMultiFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])} + onSelectAll={vals => setLineMultiFilter(vals)} onClear={() => { setLineMultiFilter([]); setOpenFilter(null); }} onClose={() => setOpenFilter(null)} /> @@ -411,6 +412,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit } uniqueValues={uniqueClassifications} selectedValues={classificationFilter} onToggle={val => setClassificationFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])} + onSelectAll={vals => setClassificationFilter(vals)} onClear={() => { setClassificationFilter([]); setOpenFilter(null); }} onClose={() => setOpenFilter(null)} /> @@ -647,10 +649,11 @@ function TextFilterPopover({ value, onChange, onClose }: { } // ── Multi-select column filter popover ──────────────────────────────────────── -function ColumnFilterPopover({ uniqueValues, selectedValues, onToggle, onClear, onClose }: { +function ColumnFilterPopover({ uniqueValues, selectedValues, onToggle, onSelectAll, onClear, onClose }: { uniqueValues: string[]; selectedValues: string[]; onToggle: (val: string) => void; + onSelectAll: (vals: string[]) => void; onClear: () => void; onClose: () => void; }) { @@ -670,6 +673,32 @@ function ColumnFilterPopover({ uniqueValues, selectedValues, onToggle, onClear, className="w-full bg-slate-900 border border-slate-700 rounded p-1.5 pl-8 text-xs text-white focus:outline-none focus:border-blue-500" /> + +
+ + {selectedValues.length} selected +
+
{filtered.map(val => (
+
+ + {selectedValues.length} selected +
+
{filteredValues.map(val => (