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
+118 -3
View File
@@ -13,6 +13,8 @@ import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
LineChart, Line, Legend, ComposedChart, Area
} from 'recharts';
import { ExcelFilter } from './ExcelFilter';
import { ColumnFilterCondition } from '../types';
interface ForecastViewProps {
data: ProductForecastData[];
@@ -195,6 +197,18 @@ const ForecastView: React.FC<ForecastViewProps> = ({
const [displayCount, setDisplayCount] = useState(50);
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'asc' | 'desc' }>({ key: 'actualUnits', direction: 'desc' });
// 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;
});
};
const activeMonths = useMemo(() => {
if (filters.month && filters.month.length > 0) {
return filters.month.map(m => m.split('-')[0]);
@@ -262,6 +276,40 @@ const ForecastView: React.FC<ForecastViewProps> = ({
}
}
// Column Filters (Excel-style)
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(p => {
const val = String((p as any)[key] || '');
return condition.selectedValues?.includes(val);
});
}
if (condition.textFilter) {
const { operator, value } = condition.textFilter;
const lowerValue = value.toLowerCase();
result = result.filter(p => {
const rowVal = String((p 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;
}
});
}
});
}
// Apply WOC Filter
if (wocFilter && wocFilter.length > 0 && vendorStockMap) {
result = result.filter(p => {
@@ -304,7 +352,7 @@ const ForecastView: React.FC<ForecastViewProps> = ({
});
return result;
}, [calculatedData, debouncedSearch, showOnlyTop50, top50Ranking, top50Mode, sortConfig, wocFilter, vendorStockMap]);
}, [calculatedData, debouncedSearch, showOnlyTop50, top50Ranking, top50Mode, sortConfig, wocFilter, vendorStockMap, columnFilters]);
// [MOVED HERE] Global Summary - Now respects all filters including Search/WOC
const globalSummary = useMemo(() => {
@@ -492,8 +540,75 @@ const ForecastView: React.FC<ForecastViewProps> = ({
<table className="w-full text-left border-collapse relative">
<thead className="sticky top-0 z-20 bg-slate-950 shadow-sm text-[10px] font-black text-slate-500 uppercase tracking-wider">
<tr>
<th className="px-6 py-4 text-white w-[30%] cursor-pointer hover:bg-white/5 transition-colors" onClick={() => handleSort('sku')}>
Product Info <SortIndicator column="sku" />
<th className="px-6 py-4 text-left w-[30%]">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span
className="text-[10px] font-black text-slate-400 uppercase tracking-widest cursor-pointer hover:text-white"
onClick={() => handleSort('sku')}
>
Product Info <SortIndicator column="sku" />
</span>
<div className="flex items-center gap-1">
<ExcelFilter
columnKey="sku"
title="SKU"
uniqueValues={Array.from(new Set(data.map(d => d.sku))).sort()}
currentFilter={columnFilters['sku']}
onFilterChange={handleColumnFilterChange}
icon={<span className="text-[10px]">SKU</span>}
/>
<ExcelFilter
columnKey="asin"
title="ASIN"
uniqueValues={Array.from(new Set(data.map(d => d.asin))).sort()}
currentFilter={columnFilters['asin']}
onFilterChange={handleColumnFilterChange}
icon={<span className="text-[10px]">ASIN</span>}
/>
<ExcelFilter
columnKey="title"
title="Title"
uniqueValues={Array.from(new Set(data.map(d => d.title))).sort()}
currentFilter={columnFilters['title']}
onFilterChange={handleColumnFilterChange}
icon={<span className="text-[10px]">Title</span>}
/>
</div>
</div>
<div className="flex items-center gap-1">
<InColumnStockFilter
currentFilters={stockFilter}
onFilterChange={onStockFilterChange}
title="Warehouse Stock"
icon={<WarehouseIcon className="w-3" />}
/>
<InColumnStockFilter
currentFilters={vendorStockFilter}
onFilterChange={onVendorStockFilterChange}
title="Vendor Stock"
icon={<AmazonSmileIcon className="w-max" />}
options={[
'Out of Stock (0)',
'In Stock (>0)',
'In Stock (>20)',
'Low Stock (<10)',
]}
/>
<InColumnStockFilter
currentFilters={wocFilter}
onFilterChange={onWocFilterChange}
title="Week Coverage"
icon={<CoverageIcon className="w-3" />}
options={[
'< 4 Weeks',
'> 4 Weeks',
'Out of Stock',
'Infinite Cover'
]}
/>
</div>
</div>
</th>
<th className="px-6 py-4 text-center w-[12%] cursor-pointer hover:bg-white/5 transition-colors" onClick={() => handleSort('annualForecast')}>
Annual Forecast <SortIndicator column="annualForecast" />