Implement Excel-style filters (SKU, ASIN, Title, Line) across GRID, WEEKLY SALES, and FC26 tabs

This commit is contained in:
Christian Vidal Wolf
2026-02-09 12:00:39 +01:00
parent 827135a517
commit ac791b5265
6 changed files with 574 additions and 28 deletions
+100 -12
View File
@@ -1,7 +1,7 @@
import React, { useMemo, useState, useEffect, useCallback, useRef } from 'react';
import * as XLSX from 'xlsx';
import { CombinedKPIs } from '../types';
import { pivotWeeklySalesData, WeeklyPivotRow, PAN_EU_COUNTRIES, checkNumericConditions } from '../services/dataProcessor';
import { CombinedKPIs, ColumnFilterCondition, SalesRecord } from '../types';
import { pivotWeeklySalesData, WeeklyPivotRow, PAN_EU_COUNTRIES, checkNumericConditions, filterData } from '../services/dataProcessor';
import { StockBadge } from './StockBadge';
import { InColumnStockFilter } from './InColumnStockFilter';
import { NumericColumnFilter, NumericFilterConfig, passesNumericFilter } from './NumericColumnFilter';
@@ -9,6 +9,7 @@ import { Top50Badge } from './Top50Badge';
import { VendorStockBadge } from './VendorStockBadge';
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
import { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons';
import { ExcelFilter } from './ExcelFilter';
interface WeeklyGridProps {
data: CombinedKPIs[];
@@ -325,6 +326,19 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
const [displayCount, setDisplayCount] = useState(50);
const scrollContainerRef = useRef<HTMLDivElement>(null);
// State for Column Filters (SKU, ASIN, Title, Line)
const [columnFilters, setColumnFilters] = useState<Record<string, ColumnFilterCondition>>({});
const handleColumnFilterChange = (columnKey: string, condition: ColumnFilterCondition | undefined) => {
setColumnFilters(prev => {
const next = { ...prev };
if (condition) next[columnKey] = condition;
else delete next[columnKey];
return next;
});
setCurrentPage(1);
};
// Numeric filters for Units, Spend, GV
const [numericFilters, setNumericFilters] = useState<NumericFilterConfig[]>([]);
@@ -409,6 +423,40 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
});
}
// Column Filters (Excel-style) - using logic adapted from filterData
if (Object.keys(columnFilters).length > 0) {
(Object.entries(columnFilters) as [string, ColumnFilterCondition][]).forEach(([key, condition]) => {
if (!condition) return;
if (condition.selectedValues && condition.selectedValues.length > 0) {
result = result.filter(r => {
const val = String((r as any)[key] || '');
return condition.selectedValues?.includes(val);
});
}
if (condition.textFilter) {
const { operator, value } = condition.textFilter;
const lowerValue = value.toLowerCase();
result = result.filter(r => {
const rowVal = String((r as any)[key] || '').toLowerCase();
switch (operator) {
case 'equals': return rowVal === lowerValue;
case 'notEquals': return rowVal !== lowerValue;
case 'contains': return rowVal.includes(lowerValue);
case 'notContains': return !rowVal.includes(lowerValue);
case 'startsWith': return rowVal.startsWith(lowerValue);
case 'notStartsWith': return !rowVal.startsWith(lowerValue);
case 'endsWith': return rowVal.endsWith(lowerValue);
case 'notEndsWith': return !rowVal.endsWith(lowerValue);
default: return true;
}
});
}
});
}
// Search Filter
if (debouncedSearch) {
const searchTerms = debouncedSearch.toLowerCase().split(/[\s,]+/).filter(t => t.length > 0);
@@ -492,7 +540,7 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
}
return result;
}, [rows, debouncedSearch, showOnlyTop50, top50Ranking, growthFilterMode, growthThreshold, sortConfig, weeks, top50Mode, wocFilter, velocityMap, vendorStockMap, numericFilters]);
}, [rows, debouncedSearch, showOnlyTop50, top50Ranking, growthFilterMode, growthThreshold, sortConfig, weeks, top50Mode, wocFilter, velocityMap, vendorStockMap, numericFilters, columnFilters]);
// 2. Sort results
const sortedRows = useMemo(() => {
@@ -696,11 +744,56 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
<thead className="sticky top-0 z-20 bg-slate-900">
<tr className="bg-slate-900 border-b border-white/10 relative z-30">
<th
onClick={() => handleSort('rank', 'rank')}
className="p-3 text-[11px] font-black text-slate-400 uppercase tracking-widest sticky left-0 z-40 bg-slate-900 border-r border-white/10 min-w-[240px] cursor-pointer hover:bg-white/5 transition-colors group"
className="p-3 text-[11px] font-black text-slate-400 uppercase tracking-widest sticky left-0 z-40 bg-slate-900 border-r border-white/10 min-w-[240px]"
>
<div className="flex items-center gap-2">
<span>Product Details</span>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between pr-2">
<div
className="flex items-center gap-2 cursor-pointer hover:text-white group"
onClick={() => handleSort('rank', 'rank')}
>
<span>Product Details</span>
{sortConfig?.metric === 'rank' && (
<span className="text-amber-500 font-black text-sm animate-bounce-subtle">
{sortConfig.direction === 'asc' ? '↑' : '↓'}
</span>
)}
</div>
<div className="flex items-center gap-1">
<ExcelFilter
columnKey="sku"
title="SKU"
uniqueValues={Array.from(new Set(rows.map(r => r.sku))).sort()}
currentFilter={columnFilters['sku']}
onFilterChange={handleColumnFilterChange}
icon={<span className="text-[10px]">SKU</span>}
/>
<ExcelFilter
columnKey="asin"
title="ASIN"
uniqueValues={Array.from(new Set(rows.map(r => r.asin))).sort()}
currentFilter={columnFilters['asin']}
onFilterChange={handleColumnFilterChange}
icon={<span className="text-[10px]">ASIN</span>}
/>
<ExcelFilter
columnKey="title"
title="Title"
uniqueValues={Array.from(new Set(rows.map(r => r.title))).sort()}
currentFilter={columnFilters['title']}
onFilterChange={handleColumnFilterChange}
icon={<span className="text-[10px]">Title</span>}
/>
<ExcelFilter
columnKey="line"
title="Line"
uniqueValues={Array.from(new Set(rows.map(r => r.line))).sort()}
currentFilter={columnFilters['line']}
onFilterChange={handleColumnFilterChange}
icon={<span className="text-[10px]">Line</span>}
/>
</div>
</div>
<div className="flex items-center gap-1">
<InColumnStockFilter
currentFilters={stockFilter}
@@ -733,11 +826,6 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
]}
/>
</div>
{sortConfig?.metric === 'rank' && (
<span className="text-amber-500 font-black text-sm animate-bounce-subtle">
{sortConfig.direction === 'asc' ? '↑' : '↓'}
</span>
)}
</div>
</th>
{weeks.map(week => (