From 0214509ffe9926147e145905da52f8168c3f79f9 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Thu, 22 Jan 2026 13:01:04 +0100 Subject: [PATCH] feat: implement country-aware Top 50 ranking - App.tsx: Update top50Ranking2025 to calculate ranks based on active country filters - App.tsx: Add split Pan-EU vs UK ranking when no filters are active - WeeklyGrid.tsx: Add support for multiple ranking badges (EU/UK) - WeeklyGrid.tsx: Enhance Top50Badge with labels and color themes - WeeklyGrid.tsx: Update sorting and filtering logic for new ranking structure --- App.tsx | 53 ++++++++++++++++--------- components/WeeklyGrid.tsx | 83 +++++++++++++++++++++++++++++++-------- 2 files changed, 102 insertions(+), 34 deletions(-) diff --git a/App.tsx b/App.tsx index a8b6a85..c1cea30 100644 --- a/App.tsx +++ b/App.tsx @@ -254,31 +254,48 @@ const App: React.FC = () => { const filteredAdsData = useMemo(() => filterAdsData(adsData, filters), [adsData, filters]); const aggregatedData = useMemo(() => aggregateData(filteredData), [filteredData]); - // Calculate static Top 50 Best Sellers for 2025 (independent of current filters) + // Calculate country-aware Top 50 Best Sellers for 2025 const top50Ranking2025 = useMemo(() => { // Filter only 2025 data const data2025 = rawData.filter(r => r.year === 2025); - // Aggregate Sell Out by ASIN - const asinTotals = new Map(); - data2025.forEach(r => { - const asin = r.asin.trim().toUpperCase(); - asinTotals.set(asin, (asinTotals.get(asin) || 0) + r.sellOut); - }); + const calculateTop50 = (entries: SalesRecord[]) => { + const asinTotals = new Map(); + entries.forEach(r => { + const asin = r.asin.trim().toUpperCase(); + asinTotals.set(asin, (asinTotals.get(asin) || 0) + r.sellOut); + }); - // Sort by total Sell Out descending and take top 50 - const sorted = Array.from(asinTotals.entries()) - .sort((a, b) => b[1] - a[1]) - .slice(0, 50); + const sorted = Array.from(asinTotals.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 50); - // Create a Map of ASIN -> rank (1-50) - const rankMap = new Map(); - sorted.forEach(([asin], index) => { - rankMap.set(asin, index + 1); - }); + const rankMap = new Map(); + sorted.forEach(([asin], index) => { + rankMap.set(asin, index + 1); + }); + return rankMap; + }; - return rankMap; - }, [rawData]); + if (filters.customer.length > 0) { + // Filtered mode: Rank products based on currently selected countries + const filtered2025 = data2025.filter(r => filters.customer.includes(r.customer)); + return { + type: 'filtered' as const, + overall: calculateTop50(filtered2025) + }; + } else { + // Dual mode: Separate Pan-EU and UK rankings when no countries are selected + const euData = data2025.filter(r => !r.customer.toLowerCase().includes('uk')); + const ukData = data2025.filter(r => r.customer.toLowerCase().includes('uk')); + + return { + type: 'dual' as const, + eu: calculateTop50(euData), + uk: calculateTop50(ukData) + }; + } + }, [rawData, filters.customer]); // Combine Sales & Ads Data dynamically based on current filters const combinedAdsData = useMemo(() => { diff --git a/components/WeeklyGrid.tsx b/components/WeeklyGrid.tsx index 8b50292..bea9dd9 100644 --- a/components/WeeklyGrid.tsx +++ b/components/WeeklyGrid.tsx @@ -5,7 +5,12 @@ import { pivotWeeklySalesData, WeeklyPivotRow } from '../services/dataProcessor' interface WeeklyGridProps { data: CombinedKPIs[]; - top50Ranking?: Map; // Static ranking of Top 50 ASINs by 2025 Sell Out + top50Ranking?: { + type: 'filtered' | 'dual'; + overall?: Map; + eu?: Map; + uk?: Map; + }; } type SortConfig = { @@ -28,14 +33,24 @@ const useDebounce = (value: string, delay: number) => { }; // Top 50 Badge Component -const Top50Badge: React.FC<{ rank: number }> = ({ rank }) => ( - - 🏆 {rank} - -); +const Top50Badge: React.FC<{ rank: number; label?: string; theme?: 'amber' | 'blue' | 'indigo' }> = ({ rank, label, theme = 'amber' }) => { + const themeClasses = { + amber: 'from-amber-500 to-orange-500 border-amber-400/50', + blue: 'from-blue-500 to-cyan-500 border-blue-400/50', + indigo: 'from-indigo-500 to-purple-500 border-indigo-400/50', + }; + + return ( + + 🏆 + {label && {label}} + {rank} + + ); +}; const WeeklyGrid: React.FC = ({ data, top50Ranking }) => { // Pivot data - memoized @@ -96,7 +111,14 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking }) => { // Top 50 Filter if (showOnlyTop50 && top50Ranking) { - result = result.filter(r => top50Ranking.has(r.asin.trim().toUpperCase())); + result = result.filter(r => { + const asin = r.asin.trim().toUpperCase(); + if (top50Ranking.type === 'filtered') { + return top50Ranking.overall?.has(asin); + } else { + return top50Ranking.eu?.has(asin) || top50Ranking.uk?.has(asin); + } + }); } // Search Filter @@ -150,8 +172,21 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking }) => { result.sort((a, b) => { if (sortConfig.metric === 'rank') { - const rankA = top50Ranking?.get(a.asin.trim().toUpperCase()) || 999; - const rankB = top50Ranking?.get(b.asin.trim().toUpperCase()) || 999; + const asinA = a.asin.trim().toUpperCase(); + const asinB = b.asin.trim().toUpperCase(); + + let rankA = 999; + let rankB = 999; + + if (top50Ranking?.type === 'filtered') { + rankA = top50Ranking.overall?.get(asinA) || 999; + rankB = top50Ranking.overall?.get(asinB) || 999; + } else if (top50Ranking?.type === 'dual') { + // In dual mode, prioritize EU rank, then UK rank + rankA = top50Ranking.eu?.get(asinA) || top50Ranking.uk?.get(asinA) || 999; + rankB = top50Ranking.eu?.get(asinB) || top50Ranking.uk?.get(asinB) || 999; + } + return direction === 'asc' ? rankA - rankB : rankB - rankA; } const valA = a[metricKey][weekKey] || 0; @@ -261,8 +296,8 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking }) => { } }} className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-all border ${showOnlyTop50 - ? 'bg-gradient-to-r from-amber-500 to-orange-500 text-white border-amber-400 shadow-lg shadow-amber-500/20' - : 'bg-slate-950/50 text-slate-400 border-white/10 hover:border-amber-500/50 hover:text-amber-400' + ? 'bg-gradient-to-r from-amber-500 to-orange-500 text-white border-amber-400 shadow-lg shadow-amber-500/20' + : 'bg-slate-950/50 text-slate-400 border-white/10 hover:border-amber-500/50 hover:text-amber-400' }`} > 🏆 @@ -387,13 +422,29 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking }) => { {paginatedRows.length > 0 ? ( paginatedRows.map((row) => { - const top50Rank = top50Ranking?.get(row.asin.trim().toUpperCase()); + const asin = row.asin.trim().toUpperCase(); + const ranks = []; + + if (top50Ranking) { + if (top50Ranking.type === 'filtered') { + const rank = top50Ranking.overall?.get(asin); + if (rank) ranks.push({ rank, label: '', theme: 'amber' as const }); + } else { + const euRank = top50Ranking.eu?.get(asin); + const ukRank = top50Ranking.uk?.get(asin); + if (euRank) ranks.push({ rank: euRank, label: 'EU', theme: 'indigo' as const }); + if (ukRank) ranks.push({ rank: ukRank, label: 'UK', theme: 'blue' as const }); + } + } + return (
- {top50Rank && } + {ranks.map((r, i) => ( + + ))} {row.sku || '-'} {row.asin}