mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:45:23 +02:00
Implement Excel-style filters (SKU, ASIN, Title, Line) across GRID, WEEKLY SALES, and FC26 tabs
This commit is contained in:
+58
-10
@@ -2,13 +2,14 @@ import React, { useState, useMemo, useEffect, useRef } from 'react';
|
|||||||
import {
|
import {
|
||||||
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer
|
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import { SalesRecord, PivotRow, AdsRecord, CombinedKPIs } from '../types';
|
import { SalesRecord, PivotRow, AdsRecord, CombinedKPIs, ColumnFilterCondition } from '../types';
|
||||||
import { pivotSalesData, generateXLSX, aggregateForTimeSeries, aggregateForComparisonTimeSeries, applyPanEUGrouping } from '../services/dataProcessor';
|
import { pivotSalesData, generateXLSX, aggregateForTimeSeries, aggregateForComparisonTimeSeries, applyPanEUGrouping, filterData } from '../services/dataProcessor';
|
||||||
import { DownloadIcon, FunnelIcon, CloseIcon, ChartIcon, TrendingIcon } from './Icons';
|
import { DownloadIcon, FunnelIcon, CloseIcon, ChartIcon, TrendingIcon } from './Icons';
|
||||||
import { StockBadge } from './StockBadge';
|
import { StockBadge } from './StockBadge';
|
||||||
import { Top50Badge } from './Top50Badge';
|
import { Top50Badge } from './Top50Badge';
|
||||||
import { VendorStockBadge } from './VendorStockBadge';
|
import { VendorStockBadge } from './VendorStockBadge';
|
||||||
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
|
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
|
||||||
|
import { ExcelFilter } from './ExcelFilter';
|
||||||
|
|
||||||
interface DataGridProps {
|
interface DataGridProps {
|
||||||
data: SalesRecord[] | CombinedKPIs[];
|
data: SalesRecord[] | CombinedKPIs[];
|
||||||
@@ -297,6 +298,19 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
|||||||
const [newFilterOperator, setNewFilterOperator] = useState<'gt' | 'lt'>('gt');
|
const [newFilterOperator, setNewFilterOperator] = useState<'gt' | 'lt'>('gt');
|
||||||
const [newFilterValue, setNewFilterValue] = useState<string>('');
|
const [newFilterValue, setNewFilterValue] = useState<string>('');
|
||||||
|
|
||||||
|
// State for Column Filters
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
// Effective dimensions for rendering
|
// Effective dimensions for rendering
|
||||||
const effectiveDimensions = useMemo(() =>
|
const effectiveDimensions = useMemo(() =>
|
||||||
selectedDimensions.length > 0 ? selectedDimensions : ['customer'],
|
selectedDimensions.length > 0 ? selectedDimensions : ['customer'],
|
||||||
@@ -307,10 +321,28 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
|||||||
// Apply Pan-EU grouping when no customer filter is applied
|
// Apply Pan-EU grouping when no customer filter is applied
|
||||||
const processedData = applyPanEUGrouping(data as SalesRecord[], hasCustomerFilter);
|
const processedData = applyPanEUGrouping(data as SalesRecord[], hasCustomerFilter);
|
||||||
|
|
||||||
|
// Apply our comprehensive filters (includes Column Filters now)
|
||||||
|
const filterState: any = {
|
||||||
|
customer: [],
|
||||||
|
year: [],
|
||||||
|
month: [],
|
||||||
|
line: [],
|
||||||
|
asin: [],
|
||||||
|
sku: [],
|
||||||
|
title: [],
|
||||||
|
week: [],
|
||||||
|
stock: [],
|
||||||
|
vendorStock: [],
|
||||||
|
woc: [],
|
||||||
|
bulkSearch: searchTerm,
|
||||||
|
columnFilters
|
||||||
|
};
|
||||||
|
const filteredFlatData = filterData(processedData, filterState, stockMap, vendorStockMap, top50Mode);
|
||||||
|
|
||||||
// pivotSalesData now handles ads aggregation correctly because it receives CombinedKPIs
|
// pivotSalesData now handles ads aggregation correctly because it receives CombinedKPIs
|
||||||
const { rows } = pivotSalesData(processedData, effectiveDimensions);
|
const { rows } = pivotSalesData(filteredFlatData, effectiveDimensions);
|
||||||
return rows;
|
return rows;
|
||||||
}, [data, effectiveDimensions, hasCustomerFilter]);
|
}, [data, effectiveDimensions, hasCustomerFilter, columnFilters, searchTerm, stockMap, vendorStockMap, top50Mode]);
|
||||||
|
|
||||||
const { years } = useMemo(() => {
|
const { years } = useMemo(() => {
|
||||||
// We still need unique years for columns
|
// We still need unique years for columns
|
||||||
@@ -452,13 +484,9 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
|||||||
|
|
||||||
// 1. Filter
|
// 1. Filter
|
||||||
if (rowFilters.length > 0) {
|
if (rowFilters.length > 0) {
|
||||||
const latestYear = years[0];
|
|
||||||
const prevYear = years[1];
|
|
||||||
|
|
||||||
result = result.filter(row => {
|
result = result.filter(row => {
|
||||||
return rowFilters.every(filter => {
|
return rowFilters.every(filter => {
|
||||||
let rowValue = 0;
|
let rowValue = 0;
|
||||||
|
|
||||||
if (filter.metric.startsWith('total_sellOut_')) {
|
if (filter.metric.startsWith('total_sellOut_')) {
|
||||||
const y = filter.metric.split('_')[2];
|
const y = filter.metric.split('_')[2];
|
||||||
rowValue = row.totalsByYear[y]?.sellOut || 0;
|
rowValue = row.totalsByYear[y]?.sellOut || 0;
|
||||||
@@ -468,6 +496,8 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
|||||||
rowValue = row.totalsByYear[y]?.units || 0;
|
rowValue = row.totalsByYear[y]?.units || 0;
|
||||||
}
|
}
|
||||||
else if (filter.metric === 'growth_sellOut') {
|
else if (filter.metric === 'growth_sellOut') {
|
||||||
|
const latestYear = years[0];
|
||||||
|
const prevYear = years[1];
|
||||||
if (!prevYear) return true;
|
if (!prevYear) return true;
|
||||||
const curr = row.totalsByYear[latestYear]?.sellOut || 0;
|
const curr = row.totalsByYear[latestYear]?.sellOut || 0;
|
||||||
const prev = row.totalsByYear[prevYear]?.sellOut || 0;
|
const prev = row.totalsByYear[prevYear]?.sellOut || 0;
|
||||||
@@ -475,6 +505,8 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
|||||||
rowValue = ((curr - prev) / prev) * 100;
|
rowValue = ((curr - prev) / prev) * 100;
|
||||||
}
|
}
|
||||||
else if (filter.metric === 'growth_units') {
|
else if (filter.metric === 'growth_units') {
|
||||||
|
const latestYear = years[0];
|
||||||
|
const prevYear = years[1];
|
||||||
if (!prevYear) return true;
|
if (!prevYear) return true;
|
||||||
const curr = row.totalsByYear[latestYear]?.units || 0;
|
const curr = row.totalsByYear[latestYear]?.units || 0;
|
||||||
const prev = row.totalsByYear[prevYear]?.units || 0;
|
const prev = row.totalsByYear[prevYear]?.units || 0;
|
||||||
@@ -1186,15 +1218,31 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
|||||||
{/* Dynamic Dimension Headers */}
|
{/* Dynamic Dimension Headers */}
|
||||||
{effectiveDimensions.map(dim => {
|
{effectiveDimensions.map(dim => {
|
||||||
const label = DIMENSION_OPTIONS.find(d => d.value === dim)?.label || dim;
|
const label = DIMENSION_OPTIONS.find(d => d.value === dim)?.label || dim;
|
||||||
|
// Extract unique values for this dimension from ALL pivot rows (pre-filter)
|
||||||
|
const uniqueValues = Array.from(new Set(pivotRows.map(r => String((r as any)[dim] || ''))))
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
key={dim}
|
key={dim}
|
||||||
className={`px-4 py-3 border-b border-border cursor-pointer hover:text-white group bg-slate-950 ${dim === 'title' ? 'min-w-[450px]' : 'min-w-[150px]'}`}
|
className={`px-4 py-3 border-b border-border group bg-slate-950 ${dim === 'title' ? 'min-w-[450px]' : 'min-w-[150px]'}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
className="flex items-center cursor-pointer hover:text-white"
|
||||||
onClick={() => requestSort(dim)}
|
onClick={() => requestSort(dim)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
|
||||||
{label} {getSortIcon(dim)}
|
{label} {getSortIcon(dim)}
|
||||||
</div>
|
</div>
|
||||||
|
<ExcelFilter
|
||||||
|
columnKey={dim}
|
||||||
|
title={label}
|
||||||
|
uniqueValues={uniqueValues}
|
||||||
|
currentFilter={columnFilters[dim]}
|
||||||
|
onFilterChange={handleColumnFilterChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||||
|
import { ColumnFilterCondition } from '../types';
|
||||||
|
import { CloseIcon, FunnelIcon } from './Icons';
|
||||||
|
|
||||||
|
interface ExcelFilterProps {
|
||||||
|
columnKey: string;
|
||||||
|
title: string;
|
||||||
|
uniqueValues: string[];
|
||||||
|
currentFilter?: ColumnFilterCondition;
|
||||||
|
onFilterChange: (columnKey: string, condition: ColumnFilterCondition | undefined) => void;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPERATORS = [
|
||||||
|
{ label: 'Elija uno', value: '' },
|
||||||
|
{ label: 'Es igual a', value: 'equals' },
|
||||||
|
{ label: 'No es igual a', value: 'notEquals' },
|
||||||
|
{ label: 'Contiene', value: 'contains' },
|
||||||
|
{ label: 'No contiene', value: 'notContains' },
|
||||||
|
{ label: 'Comienza por', value: 'startsWith' },
|
||||||
|
{ label: 'No comienza por', value: 'notStartsWith' },
|
||||||
|
{ label: 'Termina con', value: 'endsWith' },
|
||||||
|
{ label: 'No termina con', value: 'notEndsWith' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const ExcelFilter: React.FC<ExcelFilterProps> = ({
|
||||||
|
columnKey,
|
||||||
|
title,
|
||||||
|
uniqueValues,
|
||||||
|
currentFilter,
|
||||||
|
onFilterChange,
|
||||||
|
icon
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [operator, setOperator] = useState<typeof OPERATORS[number]['value']>(currentFilter?.textFilter?.operator || '');
|
||||||
|
const [textValue, setTextValue] = useState(currentFilter?.textFilter?.value || '');
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const [tempSelectedValues, setTempSelectedValues] = useState<string[]>(currentFilter?.selectedValues || []);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Synchronization with currentFilter prop
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setOperator(currentFilter?.textFilter?.operator || '');
|
||||||
|
setTextValue(currentFilter?.textFilter?.value || '');
|
||||||
|
setTempSelectedValues(currentFilter?.selectedValues || []);
|
||||||
|
}
|
||||||
|
}, [isOpen, currentFilter]);
|
||||||
|
|
||||||
|
// Close when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filteredValues = useMemo(() => {
|
||||||
|
if (!searchValue) return uniqueValues;
|
||||||
|
const lowerSearch = searchValue.toLowerCase();
|
||||||
|
return uniqueValues.filter(v => String(v).toLowerCase().includes(lowerSearch));
|
||||||
|
}, [uniqueValues, searchValue]);
|
||||||
|
|
||||||
|
const handleToggleSelectAll = () => {
|
||||||
|
if (tempSelectedValues.length === uniqueValues.length) {
|
||||||
|
setTempSelectedValues([]);
|
||||||
|
} else {
|
||||||
|
setTempSelectedValues([...uniqueValues]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleValue = (val: string) => {
|
||||||
|
setTempSelectedValues(prev =>
|
||||||
|
prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApply = () => {
|
||||||
|
const newCondition: ColumnFilterCondition = {
|
||||||
|
...currentFilter,
|
||||||
|
selectedValues: tempSelectedValues.length > 0 ? tempSelectedValues : undefined,
|
||||||
|
textFilter: operator ? { operator: operator as any, value: textValue } : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove empty properties
|
||||||
|
if (!newCondition.selectedValues) delete newCondition.selectedValues;
|
||||||
|
if (!newCondition.textFilter) delete newCondition.textFilter;
|
||||||
|
|
||||||
|
onFilterChange(columnKey, Object.keys(newCondition).length > 0 ? newCondition : undefined);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
onFilterChange(columnKey, undefined);
|
||||||
|
setOperator('');
|
||||||
|
setTextValue('');
|
||||||
|
setTempSelectedValues([]);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSort = (direction: 'asc' | 'desc') => {
|
||||||
|
onFilterChange(columnKey, { ...currentFilter, sort: direction });
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasActiveFilter = !!(currentFilter?.textFilter || currentFilter?.selectedValues);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative inline-block ml-1" ref={containerRef}>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}}
|
||||||
|
className={`flex flex-col items-center gap-0.5 p-1 rounded hover:bg-white/10 transition-all ${hasActiveFilter ? 'text-indigo-400 bg-indigo-500/10' : 'text-slate-500'}`}
|
||||||
|
title={`Filter by ${title}`}
|
||||||
|
>
|
||||||
|
{icon || <FunnelIcon className="w-3 h-3" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute left-0 mt-2 w-72 bg-slate-900 border border-slate-700 rounded-xl shadow-2xl z-[999] overflow-hidden animate-in fade-in zoom-in duration-150">
|
||||||
|
<div className="bg-slate-950 px-4 py-3 border-b border-slate-800 flex justify-between items-center">
|
||||||
|
<span className="text-xs font-black text-slate-300 uppercase tracking-widest">{title}</span>
|
||||||
|
<button onClick={() => setIsOpen(false)} className="text-slate-500 hover:text-white">
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 space-y-4 max-h-[80vh] overflow-y-auto custom-scrollbar">
|
||||||
|
{/* Sort Section */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Ordenar</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleSort('asc')}
|
||||||
|
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg border text-xs font-bold transition-all ${currentFilter?.sort === 'asc' ? 'bg-indigo-600 border-indigo-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300 hover:bg-slate-700'}`}
|
||||||
|
>
|
||||||
|
<span className="text-lg">A↓Z</span> Ascendente
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSort('desc')}
|
||||||
|
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg border text-xs font-bold transition-all ${currentFilter?.sort === 'desc' ? 'bg-indigo-600 border-indigo-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300 hover:bg-slate-700'}`}
|
||||||
|
>
|
||||||
|
<span className="text-lg">Z↓A</span> Descendente
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Condition Filter Section */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Filtro</span>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<select
|
||||||
|
value={operator}
|
||||||
|
onChange={(e) => setOperator(e.target.value as any)}
|
||||||
|
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white focus:outline-none focus:border-indigo-500"
|
||||||
|
>
|
||||||
|
{OPERATORS.map(op => (
|
||||||
|
<option key={op.value} value={op.value}>{op.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{operator && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={textValue}
|
||||||
|
onChange={(e) => setTextValue(e.target.value)}
|
||||||
|
placeholder="Valor..."
|
||||||
|
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white focus:outline-none focus:border-indigo-500"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* List Selection Section */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
<span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Seleccionar valores</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Buscar en la lista..."
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(e) => setSearchValue(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-lg px-3 py-1.5 text-xs text-white focus:outline-none focus:border-indigo-500 mb-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="bg-slate-950 border border-slate-800 rounded-lg p-1 max-h-48 overflow-y-auto custom-scrollbar">
|
||||||
|
<div
|
||||||
|
onClick={handleToggleSelectAll}
|
||||||
|
className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-white/5 cursor-pointer text-xs font-bold text-indigo-400"
|
||||||
|
>
|
||||||
|
<div className={`w-3.5 h-3.5 rounded border flex items-center justify-center ${tempSelectedValues.length === uniqueValues.length ? 'bg-indigo-600 border-indigo-500' : 'border-slate-600'}`}>
|
||||||
|
{tempSelectedValues.length === uniqueValues.length && <CheckMark />}
|
||||||
|
</div>
|
||||||
|
(Seleccionar todo)
|
||||||
|
</div>
|
||||||
|
{filteredValues.map(val => {
|
||||||
|
const isSelected = tempSelectedValues.includes(val);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={val}
|
||||||
|
onClick={() => handleToggleValue(val)}
|
||||||
|
className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-white/5 cursor-pointer text-xs text-slate-300"
|
||||||
|
>
|
||||||
|
<div className={`w-3.5 h-3.5 rounded border flex items-center justify-center ${isSelected ? 'bg-indigo-600 border-indigo-500' : 'border-slate-600'}`}>
|
||||||
|
{isSelected && <CheckMark />}
|
||||||
|
</div>
|
||||||
|
<span className="truncate">{val}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-950 p-4 border-t border-slate-800 flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleApply}
|
||||||
|
className="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white py-2 rounded-lg text-xs font-black uppercase tracking-widest transition-all"
|
||||||
|
>
|
||||||
|
Aplicar filtro
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleClear}
|
||||||
|
className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 py-2 rounded-lg text-xs font-black uppercase tracking-widest transition-all"
|
||||||
|
>
|
||||||
|
Borrar filtro
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CheckMark = () => (
|
||||||
|
<svg className="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={4}>
|
||||||
|
<path d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
+117
-2
@@ -13,6 +13,8 @@ import {
|
|||||||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||||
LineChart, Line, Legend, ComposedChart, Area
|
LineChart, Line, Legend, ComposedChart, Area
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
import { ExcelFilter } from './ExcelFilter';
|
||||||
|
import { ColumnFilterCondition } from '../types';
|
||||||
|
|
||||||
interface ForecastViewProps {
|
interface ForecastViewProps {
|
||||||
data: ProductForecastData[];
|
data: ProductForecastData[];
|
||||||
@@ -195,6 +197,18 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
|||||||
const [displayCount, setDisplayCount] = useState(50);
|
const [displayCount, setDisplayCount] = useState(50);
|
||||||
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'asc' | 'desc' }>({ key: 'actualUnits', direction: 'desc' });
|
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(() => {
|
const activeMonths = useMemo(() => {
|
||||||
if (filters.month && filters.month.length > 0) {
|
if (filters.month && filters.month.length > 0) {
|
||||||
return filters.month.map(m => m.split('-')[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
|
// Apply WOC Filter
|
||||||
if (wocFilter && wocFilter.length > 0 && vendorStockMap) {
|
if (wocFilter && wocFilter.length > 0 && vendorStockMap) {
|
||||||
result = result.filter(p => {
|
result = result.filter(p => {
|
||||||
@@ -304,7 +352,7 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
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
|
// [MOVED HERE] Global Summary - Now respects all filters including Search/WOC
|
||||||
const globalSummary = useMemo(() => {
|
const globalSummary = useMemo(() => {
|
||||||
@@ -492,8 +540,75 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
|||||||
<table className="w-full text-left border-collapse relative">
|
<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">
|
<thead className="sticky top-0 z-20 bg-slate-950 shadow-sm text-[10px] font-black text-slate-500 uppercase tracking-wider">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-4 text-white w-[30%] cursor-pointer hover:bg-white/5 transition-colors" onClick={() => handleSort('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" />
|
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>
|
||||||
<th className="px-6 py-4 text-center w-[12%] cursor-pointer hover:bg-white/5 transition-colors" onClick={() => handleSort('annualForecast')}>
|
<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" />
|
Annual Forecast <SortIndicator column="annualForecast" />
|
||||||
|
|||||||
+98
-10
@@ -1,7 +1,7 @@
|
|||||||
import React, { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { CombinedKPIs } from '../types';
|
import { CombinedKPIs, ColumnFilterCondition, SalesRecord } from '../types';
|
||||||
import { pivotWeeklySalesData, WeeklyPivotRow, PAN_EU_COUNTRIES, checkNumericConditions } from '../services/dataProcessor';
|
import { pivotWeeklySalesData, WeeklyPivotRow, PAN_EU_COUNTRIES, checkNumericConditions, filterData } from '../services/dataProcessor';
|
||||||
import { StockBadge } from './StockBadge';
|
import { StockBadge } from './StockBadge';
|
||||||
import { InColumnStockFilter } from './InColumnStockFilter';
|
import { InColumnStockFilter } from './InColumnStockFilter';
|
||||||
import { NumericColumnFilter, NumericFilterConfig, passesNumericFilter } from './NumericColumnFilter';
|
import { NumericColumnFilter, NumericFilterConfig, passesNumericFilter } from './NumericColumnFilter';
|
||||||
@@ -9,6 +9,7 @@ import { Top50Badge } from './Top50Badge';
|
|||||||
import { VendorStockBadge } from './VendorStockBadge';
|
import { VendorStockBadge } from './VendorStockBadge';
|
||||||
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
|
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
|
||||||
import { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons';
|
import { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons';
|
||||||
|
import { ExcelFilter } from './ExcelFilter';
|
||||||
|
|
||||||
interface WeeklyGridProps {
|
interface WeeklyGridProps {
|
||||||
data: CombinedKPIs[];
|
data: CombinedKPIs[];
|
||||||
@@ -325,6 +326,19 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
|||||||
const [displayCount, setDisplayCount] = useState(50);
|
const [displayCount, setDisplayCount] = useState(50);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
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
|
// Numeric filters for Units, Spend, GV
|
||||||
const [numericFilters, setNumericFilters] = useState<NumericFilterConfig[]>([]);
|
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
|
// Search Filter
|
||||||
if (debouncedSearch) {
|
if (debouncedSearch) {
|
||||||
const searchTerms = debouncedSearch.toLowerCase().split(/[\s,]+/).filter(t => t.length > 0);
|
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;
|
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
|
// 2. Sort results
|
||||||
const sortedRows = useMemo(() => {
|
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">
|
<thead className="sticky top-0 z-20 bg-slate-900">
|
||||||
<tr className="bg-slate-900 border-b border-white/10 relative z-30">
|
<tr className="bg-slate-900 border-b border-white/10 relative z-30">
|
||||||
<th
|
<th
|
||||||
|
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 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')}
|
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"
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span>Product Details</span>
|
<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">
|
<div className="flex items-center gap-1">
|
||||||
<InColumnStockFilter
|
<InColumnStockFilter
|
||||||
currentFilters={stockFilter}
|
currentFilters={stockFilter}
|
||||||
@@ -733,11 +826,6 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{sortConfig?.metric === 'rank' && (
|
|
||||||
<span className="text-amber-500 font-black text-sm animate-bounce-subtle">
|
|
||||||
{sortConfig.direction === 'asc' ? '↑' : '↓'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
{weeks.map(week => (
|
{weeks.map(week => (
|
||||||
|
|||||||
@@ -871,7 +871,46 @@ export const filterData = (
|
|||||||
vendorStockMap?: Map<string, { eu: number; uk: number }>,
|
vendorStockMap?: Map<string, { eu: number; uk: number }>,
|
||||||
top50Mode: 'eu' | 'uk' = 'eu'
|
top50Mode: 'eu' | 'uk' = 'eu'
|
||||||
): SalesRecord[] => {
|
): SalesRecord[] => {
|
||||||
return data.filter(item => {
|
let result = data; // Changed from rawData to data
|
||||||
|
|
||||||
|
// 1. Column Filters (Excel-style)
|
||||||
|
if (filters.columnFilters) {
|
||||||
|
Object.entries(filters.columnFilters).forEach(([key, condition]) => {
|
||||||
|
if (!condition) return;
|
||||||
|
|
||||||
|
// Apply selected values filter
|
||||||
|
if (condition.selectedValues && condition.selectedValues.length > 0) {
|
||||||
|
result = result.filter(r => {
|
||||||
|
const val = String((r as any)[key] || '');
|
||||||
|
return condition.selectedValues?.includes(val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply text condition filter
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Standard Filters
|
||||||
|
return result.filter(item => { // Apply remaining filters to the 'result'
|
||||||
// 1. Month Logic: Handle "Apr-23" matching "Apr" filter
|
// 1. Month Logic: Handle "Apr-23" matching "Apr" filter
|
||||||
const recordMonth = item.month; // e.g. "Apr-23"
|
const recordMonth = item.month; // e.g. "Apr-23"
|
||||||
const pureMonth = recordMonth.split('-')[0]; // "Apr"
|
const pureMonth = recordMonth.split('-')[0]; // "Apr"
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ export interface FilterState {
|
|||||||
vendorStock: string[]; // Added Vendor Stock filter
|
vendorStock: string[]; // Added Vendor Stock filter
|
||||||
woc: string[]; // Added Weeks of Coverage filter
|
woc: string[]; // Added Weeks of Coverage filter
|
||||||
bulkSearch: string; // Added Bulk Search support
|
bulkSearch: string; // Added Bulk Search support
|
||||||
|
columnFilters: Record<string, ColumnFilterCondition>; // Added Excel-style column filters
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnFilterCondition {
|
||||||
|
textFilter?: {
|
||||||
|
operator: 'equals' | 'notEquals' | 'contains' | 'notContains' | 'startsWith' | 'notStartsWith' | 'endsWith' | 'notEndsWith';
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
selectedValues?: string[];
|
||||||
|
sort?: 'asc' | 'desc';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GrowthMetric {
|
export interface GrowthMetric {
|
||||||
|
|||||||
Reference in New Issue
Block a user