feat: sync vendor tab filters with global header filters

This commit is contained in:
Christian Vidal Wolf
2026-03-03 10:52:35 +01:00
parent a6b7503c82
commit 98ba0d1708
3 changed files with 88 additions and 99 deletions
+7 -2
View File
@@ -5,7 +5,7 @@ import Dashboard from './components/Dashboard';
import FilterBar from './components/FilterBar';
import AIChat from './components/AIChat';
import CrazeLogo from './components/CrazeLogo';
import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, processUKInventoryExcel, calculateVelocityMap, getUniqueValues, processForecastExcel, processBuyBoxExcel, processVendorCSV, processBSRExcel } from './services/dataProcessor';
import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, processUKInventoryExcel, calculateVelocityMap, getUniqueValues, processForecastExcel, processBuyBoxExcel, processVendorCSV, processBSRExcel, filterBsrData } from './services/dataProcessor';
import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData, ActiveExperiment, BSRRecord } from './types';
import { queryGemini } from './services/geminiService';
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons';
@@ -667,6 +667,11 @@ const App: React.FC = () => {
return aggregateData(ytdFilteredData);
}, [ytdFilteredData]);
// Derived Data for Vendor (BSR)
const filteredBsrData = useMemo(() => {
return filterBsrData(bsrData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode);
}, [bsrData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode]);
// Derived Data for Views
const years = useMemo(() => getUniqueValues(rawData, 'year').sort().reverse(), [rawData]);
@@ -959,7 +964,7 @@ const App: React.FC = () => {
<Suspense fallback={<LoadingSpinner />}>
<div className={view === 'vendor' ? '' : 'hidden'}>
<VendorDataView bsrData={bsrData} />
<VendorDataView bsrData={filteredBsrData} />
</div>
</Suspense>
+10 -97
View File
@@ -1,6 +1,5 @@
import React, { useState, useMemo } from 'react';
import React, { useMemo } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import MultiSelectDropdown from './MultiSelectDropdown';
import { BSRRecord } from '../types';
const MARKET_COLORS: Record<string, string> = {
@@ -21,73 +20,25 @@ interface ChartPoint {
[key: string]: number | string | null;
}
const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData }) => {
// Filters
const [selectedMarkets, setSelectedMarkets] = useState<string[]>([]);
const [selectedAsins, setSelectedAsins] = useState<string[]>([]);
const [selectedTopCats, setSelectedTopCats] = useState<string[]>([]);
const [selectedDetailCats, setSelectedDetailCats] = useState<string[]>([]);
// Extract available filter options from the dataset
const { availableMarkets, availableAsins, availableTopCats, availableDetailCats } = useMemo(() => {
const markets = new Set<string>();
const asins = new Set<string>();
const topCats = new Set<string>();
const detailCats = new Set<string>();
bsrData.forEach(r => {
if (r.market) markets.add(r.market);
if (r.asin) asins.add(r.asin);
if (r.topLevelName) topCats.add(r.topLevelName);
if (r.detailLevelName) detailCats.add(r.detailLevelName);
});
return {
availableMarkets: Array.from(markets).sort(),
availableAsins: Array.from(asins).sort(),
availableTopCats: Array.from(topCats).sort(),
availableDetailCats: Array.from(detailCats).sort()
};
}, [bsrData]);
// Apply filters
const filteredData = useMemo(() => {
let result = bsrData;
if (selectedMarkets.length > 0) {
result = result.filter(r => selectedMarkets.includes(r.market));
}
if (selectedAsins.length > 0) {
result = result.filter(r => selectedAsins.includes(r.asin));
}
if (selectedTopCats.length > 0) {
result = result.filter(r => r.topLevelName && selectedTopCats.includes(r.topLevelName));
}
if (selectedDetailCats.length > 0) {
result = result.filter(r => r.detailLevelName && selectedDetailCats.includes(r.detailLevelName));
}
return result;
}, [bsrData, selectedMarkets, selectedAsins, selectedTopCats, selectedDetailCats]);
const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [] }) => {
// Determine active markets in filtered data for series generation
const activeMarkets = useMemo(() => {
const m = new Set(filteredData.map(r => r.market));
const m = new Set(bsrData.map(r => r.market));
return Array.from(m).sort();
}, [filteredData]);
}, [bsrData]);
// Determine if daily resolution is available (>= half the records have a date)
const useDailyResolution = useMemo(() => {
const withDate = filteredData.filter(r => r.date).length;
return withDate > filteredData.length / 2;
}, [filteredData]);
const withDate = bsrData.filter(r => r.date).length;
return withDate > bsrData.length / 2;
}, [bsrData]);
// Aggregate Data for Charts — daily if dates available, else weekly
const { topBsrChartData, detailBsrChartData, ratingChartData } = useMemo(() => {
// Group by date string (YYYY-MM-DD) or by week number
const byBucket = new Map<string, BSRRecord[]>();
filteredData.forEach(r => {
bsrData.forEach(r => {
const key = useDailyResolution && r.date ? r.date : `W${String(r.week).padStart(2, '0')}`;
if (!byBucket.has(key)) byBucket.set(key, []);
byBucket.get(key)!.push(r);
@@ -144,7 +95,7 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData }) => {
});
return { topBsrChartData, detailBsrChartData, ratingChartData };
}, [filteredData, activeMarkets, useDailyResolution]);
}, [bsrData, activeMarkets, useDailyResolution]);
if (bsrData.length === 0) {
return (
@@ -166,46 +117,8 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData }) => {
return (
<div className="space-y-6 p-4 pb-24 md:pb-4">
{/* Filters */}
<div className="flex flex-wrap gap-3 items-center">
<MultiSelectDropdown
label="Market"
options={availableMarkets}
selected={selectedMarkets}
onChange={setSelectedMarkets}
/>
<MultiSelectDropdown
label="ASIN"
options={availableAsins}
selected={selectedAsins}
onChange={setSelectedAsins}
/>
<MultiSelectDropdown
label="Top Level Category"
options={availableTopCats}
selected={selectedTopCats}
onChange={setSelectedTopCats}
/>
<MultiSelectDropdown
label="Detail Level Category"
options={availableDetailCats}
selected={selectedDetailCats}
onChange={setSelectedDetailCats}
/>
{(selectedMarkets.length > 0 || selectedAsins.length > 0 || selectedTopCats.length > 0 || selectedDetailCats.length > 0) && (
<button
onClick={() => {
setSelectedMarkets([]);
setSelectedAsins([]);
setSelectedTopCats([]);
setSelectedDetailCats([]);
}}
className="text-xs text-slate-400 hover:text-white underline"
>
Clear filters
</button>
)}
<span className="text-slate-500 text-xs ml-auto">{filteredData.length.toLocaleString()} records filtered</span>
<span className="text-slate-500 text-xs ml-auto">{bsrData.length.toLocaleString()} records filtered</span>
</div>
{/* Top Level BSR Trend Chart */}
+71
View File
@@ -1152,6 +1152,77 @@ export const filterData = (
});
};
export const filterBsrData = (
bsrData: BSRRecord[],
filters: FilterState,
asinMetadata?: Map<string, { sku: string; title: string; line: string }>,
stockMap?: Map<string, number>,
vendorStockMap?: Map<string, { eu: number; uk: number }>,
top50Mode: 'eu' | 'uk' = 'eu'
): BSRRecord[] => {
return bsrData.filter(record => {
const asin = record.asin.trim().toUpperCase();
const meta = asinMetadata?.get(asin);
const marketMapping: Record<string, string[]> = {
'DE': ['Amazon DE', 'Amazon SC'],
'UK': ['Amazon UK', 'Amazon SC'],
'IT': ['Amazon IT', 'Amazon SC'],
'FR': ['Amazon FR', 'Amazon SC'],
'ES': ['Amazon ES', 'Amazon SC'],
};
let countryMatch = true;
if (filters.customer.length > 0) {
const mappedCustomers = marketMapping[record.market] || [];
countryMatch = filters.customer.some(c => mappedCustomers.includes(c));
} else {
countryMatch = PAN_EU_COUNTRIES.some(c => c.toUpperCase().includes(record.market));
}
const weekStr = `W${record.week}`;
const weekMatch = filters.week.length === 0 || filters.week.includes(weekStr);
const asinMatch = filters.asin.length === 0 || filters.asin.some(a => a.toUpperCase() === asin);
let skuMatch = true;
if (filters.sku.length > 0) {
skuMatch = meta ? filters.sku.some(s => s.toUpperCase() === meta.sku.toUpperCase()) : false;
}
let lineMatch = true;
if (filters.line.length > 0) {
lineMatch = meta ? filters.line.includes(meta.line) : false;
}
let titleMatch = true;
if (filters.title.length > 0) {
titleMatch = meta ? filters.title.includes(meta.title) : false;
}
const stockMatch = checkStockFilter(meta?.sku || '', filters.stock, stockMap);
const vendorStockMatch = checkVendorStockFilter(asin, filters.vendorStock, vendorStockMap, top50Mode);
let bulkMatch = true;
if (filters.bulkSearch && filters.bulkSearch.trim()) {
const searchTerms = filters.bulkSearch
.split(/[\s,\n]+/)
.map(t => t.trim().toUpperCase())
.filter(t => t.length > 0);
if (searchTerms.length > 0) {
const itemAsin = (record.asin || '').toUpperCase();
const itemSku = (meta?.sku || '').toUpperCase();
bulkMatch = searchTerms.some(term =>
itemAsin.includes(term) || itemSku.includes(term)
);
}
}
return countryMatch && weekMatch && asinMatch && skuMatch && lineMatch && titleMatch && stockMatch && vendorStockMatch && bulkMatch;
});
};
const calculateSeasonality = (data: SalesRecord[]): { seasonality: SeasonalityPoint[], seasonalityUnits: SeasonalityPoint[], years: string[] } => {
const seasonalityMap = new Map<string, SeasonalityPoint>();
const seasonalityUnitsMap = new Map<string, SeasonalityPoint>();