diff --git a/App.tsx b/App.tsx index 49aaadc..d5dd49d 100644 --- a/App.tsx +++ b/App.tsx @@ -5,6 +5,7 @@ import Dashboard from './components/Dashboard'; import FilterBar from './components/FilterBar'; import AIChat from './components/AIChat'; import CrazeLogo from './components/CrazeLogo'; +import TopMovers from './components/TopMovers'; import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, processUKInventoryExcel, calculateVelocityMap, getUniqueValues, processForecastExcel, processBuyBoxExcel, processBSRExcel, filterBsrData } from './services/dataProcessor'; import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData, ActiveExperiment, BSRRecord } from './types'; import { queryGemini } from './services/geminiService'; @@ -18,7 +19,6 @@ import { // Lazy load heavy components for better initial performance const DataGrid = lazy(() => import('./components/DataGrid')); const WeeklyGrid = lazy(() => import('./components/WeeklyGrid')); -const TopMovers = lazy(() => import('./components/TopMovers')); const AdsPerformance = lazy(() => import('./components/AdsPerformance')); const ForecastView = lazy(() => import('./components/ForecastView')); const VendorDataView = lazy(() => import('./components/VendorDataView')); @@ -564,15 +564,20 @@ const App: React.FC = () => { // Determine which dataset to use for velocity calculation based on top50Mode + // Derived Data for Vendor (BSR) + const filteredBsrData = useMemo(() => { + return filterBsrData(bsrData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode); + }, [bsrData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode]); + // Combine Sales & Ads Data dynamically based on current filters const combinedAdsData = useMemo(() => { - return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap); - }, [filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap]); + return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap, filteredBsrData); + }, [filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap, filteredBsrData]); // Unfiltered combined data for Experiment calculations (so they aren't affected by UI filters) const unfilteredCombinedData = useMemo(() => { - return mergeSalesAndAdsData(rawData, adsData, globalAsinMetadata, trafficData, velocityMap); - }, [rawData, adsData, globalAsinMetadata, trafficData, velocityMap]); + return mergeSalesAndAdsData(rawData, adsData, globalAsinMetadata, trafficData, velocityMap, bsrData); + }, [rawData, adsData, globalAsinMetadata, trafficData, velocityMap, bsrData]); // Derived Data for Grid (YTD Filtered - Isolated) // 1. Identify "Current Year" max week @@ -627,11 +632,6 @@ 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]); @@ -823,7 +823,7 @@ const App: React.FC = () => {
- +
}> @@ -868,11 +868,9 @@ const App: React.FC = () => {
- }> -
- -
-
+
+ +
}>
diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx index 057c267..c32079d 100644 --- a/components/Dashboard.tsx +++ b/components/Dashboard.tsx @@ -1,15 +1,17 @@ import React, { useState, useMemo, useEffect } from 'react'; -import { AggregatedData, GrowthMetric, AdsRecord } from '../types'; +import { AggregatedData, GrowthMetric, AdsRecord, SalesRecord } from '../types'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, Legend } from 'recharts'; +import TopMovers from './TopMovers'; interface DashboardProps { data: AggregatedData; contextData?: AggregatedData | null; adsData?: AdsRecord[]; + rawData?: SalesRecord[]; } const COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#0ea5e9']; @@ -457,7 +459,7 @@ const GrowthTable: React.FC<{ ); } -const Dashboard: React.FC = ({ data, contextData, adsData = [] }) => { +const Dashboard: React.FC = ({ data, contextData, adsData = [], rawData = [] }) => { const [seasonalityMetric, setSeasonalityMetric] = useState<'sellOut' | 'units'>('sellOut'); const [top10Metric, setTop10Metric] = useState<'sellOut' | 'units'>('sellOut'); @@ -808,6 +810,13 @@ const Dashboard: React.FC = ({ data, contextData, adsData = [] }
+ + {/* Top Movers Table - Full Width */} + {rawData && rawData.length > 0 && ( +
+ +
+ )} ); }; diff --git a/components/ExperimentsView.tsx b/components/ExperimentsView.tsx index 257bef1..1aada7d 100644 --- a/components/ExperimentsView.tsx +++ b/components/ExperimentsView.tsx @@ -53,6 +53,7 @@ const METRIC_OPTIONS: { value: ExperimentMetric; label: string }[] = [ { value: 'roas', label: 'ROAS' }, { value: 'revenue', label: 'Revenue' }, { value: 'acos', label: 'ACOS' }, + { value: 'detail_bsr', label: 'Detail Level BSR' }, ]; // ============ Main Component ============ @@ -567,7 +568,7 @@ const ExperimentDetailView: React.FC<{ {/* Render specific metrics: Units, Revenue, CVR, BSR/ACOS */} - {['units', 'revenue', 'cvr', 'acos', 'sessions', 'roas', 'ctr'].map(metric => { + {['units', 'revenue', 'cvr', 'acos', 'sessions', 'roas', 'ctr', 'detail_bsr'].map(metric => { const res = didResults?.metrics?.[metric]; if (!res) { return ( @@ -584,7 +585,7 @@ const ExperimentDetailView: React.FC<{ ); } - const isPositiveGood = !['acos', 'bsr'].includes(metric); + const isPositiveGood = !['acos', 'detail_bsr'].includes(metric); const isGood = isPositiveGood ? res.lift_percent >= 0 : res.lift_percent <= 0; return ( diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 8b8a133..ba21dac 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -783,7 +783,8 @@ export const mergeSalesAndAdsData = ( adsData: AdsRecord[], asinMetadataMap?: Map, trafficData?: TrafficRecord[], - velocityMap?: Map + velocityMap?: Map, + bsrData?: BSRRecord[] ): CombinedKPIs[] => { const stringCache: Record = {}; const getNorm = (s: string) => { @@ -808,6 +809,20 @@ export const mergeSalesAndAdsData = ( } } + // Build BSR lookup map + const bsrMap = new Map(); + if (bsrData) { + for (let i = 0; i < bsrData.length; i++) { + const b = bsrData[i]; + const year = b.date ? parseInt(b.date.split('-')[0]) : new Date().getFullYear(); + const country = mapCountryToMarketplace(b.market); + const key = `${getNorm(b.asin)}|${getNorm(country)}|${year}|${b.week}`; + if (b.detailLevelBSR !== null && b.detailLevelBSR !== undefined) { + bsrMap.set(key, b.detailLevelBSR); + } + } + } + // 1. Initialize metadata lookup map with provided global map if available, otherwise build from current sales const asinMetadata = asinMetadataMap || new Map(); @@ -942,6 +957,7 @@ export const mergeSalesAndAdsData = ( cpc, cvrUnits, glanceViews: trafficMap.get(key) || 0, + detailLevelBSR: bsrMap.get(key), avgWeeklySales }); }); @@ -983,6 +999,7 @@ export const mergeSalesAndAdsData = ( cpc: ad.clicks > 0 ? ad.cost / ad.clicks : 0, cvrUnits: ad.clicks > 0 ? (ad.attributedUnits30d / ad.clicks) * 100 : 0, glanceViews: trafficMap.get(key) || 0, + detailLevelBSR: bsrMap.get(key), avgWeeklySales }); } diff --git a/services/experimentAnalysis.ts b/services/experimentAnalysis.ts index 10758d9..a18d24b 100644 --- a/services/experimentAnalysis.ts +++ b/services/experimentAnalysis.ts @@ -35,6 +35,8 @@ interface WeeklyMetrics { cost: number; clicks: number; impressions: number; + detail_bsr: number; + bsrCount: number; } interface ComputedWeeklyMetrics extends WeeklyMetrics { @@ -159,6 +161,8 @@ function aggregateWeeklyMetrics( cost: 0, clicks: 0, impressions: 0, + detail_bsr: 0, + bsrCount: 0, }); } @@ -170,6 +174,10 @@ function aggregateWeeklyMetrics( w.cost += Number(r.cost) || 0; w.clicks += r.clicks || 0; w.impressions += r.impressions || 0; + if (r.detailLevelBSR != null) { + w.detail_bsr += r.detailLevelBSR; + w.bsrCount += 1; + } } // Debug ad map @@ -189,6 +197,7 @@ function aggregateWeeklyMetrics( cvr: w.sessions > 0 ? (w.units / w.sessions) * 100 : 0, ctr: w.impressions > 0 ? (w.clicks / w.impressions) * 100 : 0, roas: w.cost > 0 ? w.adRevenue / w.cost : 0, + detail_bsr: w.bsrCount > 0 ? w.detail_bsr / w.bsrCount : 0, })); } @@ -201,6 +210,7 @@ function getMetricValue(w: ComputedWeeklyMetrics, metric: string): number { case 'roas': return w.roas; case 'revenue': return w.revenue; case 'acos': return w.cost > 0 && w.adRevenue > 0 ? (w.cost / w.adRevenue) * 100 : 0; + case 'detail_bsr': return w.detail_bsr; default: return w.units; } } @@ -265,6 +275,13 @@ function avgMetric(data: ComputedWeeklyMetrics[], metric: string, durationWeeks: const totalCost = data.reduce((s, w) => s + w.cost, 0); return totalAdRevenue > 0 && totalCost > 0 ? (totalCost / totalAdRevenue) * 100 : 0; } + if (metric === 'detail_bsr') { + // For detail_bsr, we want the average of the non-zero weeks + const validWeeks = data.filter(w => w.detail_bsr > 0); + if (validWeeks.length === 0) return 0; + const sum = validWeeks.reduce((s, w) => s + w.detail_bsr, 0); + return sum / validWeeks.length; + } // For absolute quantities (units, revenue, sessions), we sum them and divide by the number of weeks // Use actual data.length instead of theoretical durationWeeks for accuracy @@ -272,8 +289,8 @@ function avgMetric(data: ComputedWeeklyMetrics[], metric: string, durationWeeks: return sum / data.length; } -const METRICS = ['units', 'sessions', 'cvr', 'ctr', 'roas', 'revenue', 'acos']; -const LOWER_IS_BETTER = new Set(['acos']); +const METRICS = ['units', 'sessions', 'cvr', 'ctr', 'roas', 'revenue', 'acos', 'detail_bsr']; +const LOWER_IS_BETTER = new Set(['acos', 'detail_bsr']); function computeMetricDiD( treatmentBefore: ComputedWeeklyMetrics[], diff --git a/services/experiments.ts b/services/experiments.ts index 6a2f1aa..7ff2342 100644 --- a/services/experiments.ts +++ b/services/experiments.ts @@ -278,6 +278,7 @@ export const METRIC_LABELS: Record = { roas: 'ROAS', revenue: 'Revenue', acos: 'ACOS', + detail_bsr: 'Detail Level BSR', }; export const formatMetricValue = (value: number, metric: string): string => { @@ -290,6 +291,8 @@ export const formatMetricValue = (value: number, metric: string): string => { return `${value.toFixed(2)}x`; case 'revenue': return `€${value.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`; + case 'detail_bsr': + return `#${value.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`; default: return value.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); } diff --git a/types.ts b/types.ts index 7f2393a..4a8491c 100644 --- a/types.ts +++ b/types.ts @@ -207,6 +207,7 @@ export interface CombinedKPIs { cpc: number; cvrUnits: number; glanceViews: number; // Traffic / page views + detailLevelBSR?: number; // Added for Detail Level BSR in Experiments avgWeeklySales?: number; // Added for Weeks of Coverage } @@ -295,7 +296,7 @@ export interface BSRRecord { // Experiment Tracking Types export type ExperimentType = 'pricing' | 'advertising' | 'content' | 'promotion' | 'seo'; export type ExperimentStatus = 'planned' | 'active' | 'completed' | 'paused'; -export type ExperimentMetric = 'units' | 'sessions' | 'cvr' | 'ctr' | 'roas' | 'revenue' | 'acos'; +export type ExperimentMetric = 'units' | 'sessions' | 'cvr' | 'ctr' | 'roas' | 'revenue' | 'acos' | 'detail_bsr'; export type ExperimentVerdict = 'winner' | 'loser' | 'inconclusive'; export interface ExperimentChangeAnnotation {