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();