diff --git a/App.tsx b/App.tsx
index 310f186..22d4b75 100644
--- a/App.tsx
+++ b/App.tsx
@@ -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 = () => {
}>
-
+
diff --git a/components/VendorDataView.tsx b/components/VendorDataView.tsx
index 98b0151..20ffda5 100644
--- a/components/VendorDataView.tsx
+++ b/components/VendorDataView.tsx
@@ -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 = {
@@ -21,73 +20,25 @@ interface ChartPoint {
[key: string]: number | string | null;
}
-const VendorDataView: React.FC = ({ bsrData }) => {
- // Filters
- const [selectedMarkets, setSelectedMarkets] = useState([]);
- const [selectedAsins, setSelectedAsins] = useState([]);
- const [selectedTopCats, setSelectedTopCats] = useState([]);
- const [selectedDetailCats, setSelectedDetailCats] = useState([]);
-
- // Extract available filter options from the dataset
- const { availableMarkets, availableAsins, availableTopCats, availableDetailCats } = useMemo(() => {
- const markets = new Set();
- const asins = new Set();
- const topCats = new Set();
- const detailCats = new Set();
-
- 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 = ({ 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();
- 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 = ({ bsrData }) => {
});
return { topBsrChartData, detailBsrChartData, ratingChartData };
- }, [filteredData, activeMarkets, useDailyResolution]);
+ }, [bsrData, activeMarkets, useDailyResolution]);
if (bsrData.length === 0) {
return (
@@ -166,46 +117,8 @@ const VendorDataView: React.FC = ({ bsrData }) => {
return (
- {/* Filters */}
-
-
-
-
- {(selectedMarkets.length > 0 || selectedAsins.length > 0 || selectedTopCats.length > 0 || selectedDetailCats.length > 0) && (
-
- )}
- {filteredData.length.toLocaleString()} records filtered
+ {bsrData.length.toLocaleString()} records filtered
{/* Top Level BSR Trend Chart */}
diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts
index ae7b6ff..a7b1d48 100644
--- a/services/dataProcessor.ts
+++ b/services/dataProcessor.ts
@@ -1152,6 +1152,77 @@ export const filterData = (
});
};
+export const filterBsrData = (
+ bsrData: BSRRecord[],
+ filters: FilterState,
+ asinMetadata?: Map
,
+ stockMap?: Map,
+ vendorStockMap?: Map,
+ top50Mode: 'eu' | 'uk' = 'eu'
+): BSRRecord[] => {
+ return bsrData.filter(record => {
+ const asin = record.asin.trim().toUpperCase();
+ const meta = asinMetadata?.get(asin);
+
+ const marketMapping: Record = {
+ '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();
const seasonalityUnitsMap = new Map();