From 04d198ea59eb98702f9df951f027fe3d62673534 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Wed, 8 Apr 2026 16:18:24 +0200 Subject: [PATCH] feat: add Excel-like column filters and License/Classification columns to Product Descriptions - Added per-column filter popover (Excel-style) with search, multi-select checkboxes, and clear button - Filter icon appears on hover for each column header; turns blue when active - "Clear All Column Filters" button appears in toolbar when any column filter is active - Added License and Classification columns to the table with OOC badge styling Co-Authored-By: Claude Sonnet 4.6 --- src/components/ProductDescriptions.tsx | 167 +++++++++++++++++++++++-- 1 file changed, 159 insertions(+), 8 deletions(-) diff --git a/src/components/ProductDescriptions.tsx b/src/components/ProductDescriptions.tsx index 120f03b..5234057 100644 --- a/src/components/ProductDescriptions.tsx +++ b/src/components/ProductDescriptions.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo } from 'react'; import { ExcelRow, COLUMNS } from '../types'; -import { Search, Filter, Edit2, ChevronDown, ChevronUp } from 'lucide-react'; +import { Search, Filter, Edit2, ChevronDown, ChevronUp, X, Check } from 'lucide-react'; import { cn } from '../lib/utils'; interface ProductDescriptionsProps { @@ -19,6 +19,8 @@ export function ProductDescriptions({ data, onEdit }: ProductDescriptionsProps) 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]); const licenses = useMemo(() => Array.from(new Set(data.map(r => r[COLUMNS.LICENSE]).filter(Boolean))), [data]); @@ -56,6 +58,14 @@ export function ProductDescriptions({ data, onEdit }: ProductDescriptionsProps) if (lineFilter) result = result.filter(r => r.row[COLUMNS.LINE] === lineFilter); if (licenseFilter) result = result.filter(r => r.row[COLUMNS.LICENSE] === licenseFilter); + // 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) => { @@ -84,6 +94,22 @@ export function ProductDescriptions({ data, onEdit }: ProductDescriptionsProps) } }; + 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 getRowColor = (row: ExcelRow) => { // EOL Rule: OOC Classification and 0 or negative stock (Item Available) const classification = String(row[COLUMNS.CLASSIFICATION] || '').toUpperCase().trim(); @@ -159,6 +185,18 @@ export function ProductDescriptions({ data, onEdit }: ProductDescriptionsProps) className="w-full pl-9 pr-4 py-2 bg-slate-900 border border-slate-700 rounded-md text-sm text-white focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-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-blue-500" + autoFocus + /> + + +
+ {filteredValues.map(val => ( + + ))} + {filteredValues.length === 0 && ( +
No values found
+ )} +
+ +
+ + +
+ + ); +}