mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 12:15:23 +02:00
Implement Excel-style filters (SKU, ASIN, Title, Line) across GRID, WEEKLY SALES, and FC26 tabs
This commit is contained in:
+100
-12
@@ -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 => (
|
||||
|
||||
Reference in New Issue
Block a user