feat: separate Pan-EU and UK Top 50 results in Weekly Sales

- App.tsx: Always calculate both EU and UK Top 50 maps.
- WeeklyGrid.tsx: Added top50Mode state and EU/UK toggle.
- WeeklyGrid.tsx: Updated filtering and sorting to respect the selected Top 50 mode.
- WeeklyGrid.tsx: Enhanced product badges to show selected group rank.
This commit is contained in:
Christian Vidal Wolf
2026-01-23 10:07:37 +01:00
parent 60330cb2f5
commit 0fc6f2324d
2 changed files with 74 additions and 62 deletions
+1 -12
View File
@@ -340,25 +340,14 @@ const App: React.FC = () => {
return rankMap; return rankMap;
}; };
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 euData = data2025.filter(r => !r.customer.toLowerCase().includes('uk'));
const ukData = data2025.filter(r => r.customer.toLowerCase().includes('uk')); const ukData = data2025.filter(r => r.customer.toLowerCase().includes('uk'));
return { return {
type: 'dual' as const,
eu: calculateTop50(euData), eu: calculateTop50(euData),
uk: calculateTop50(ukData) uk: calculateTop50(ukData)
}; };
} }, [rawData]);
}, [rawData, filters.customer]);
// Combine Sales & Ads Data dynamically based on current filters // Combine Sales & Ads Data dynamically based on current filters
const combinedAdsData = useMemo(() => { const combinedAdsData = useMemo(() => {
+60 -37
View File
@@ -6,10 +6,8 @@ import { pivotWeeklySalesData, WeeklyPivotRow } from '../services/dataProcessor'
interface WeeklyGridProps { interface WeeklyGridProps {
data: CombinedKPIs[]; data: CombinedKPIs[];
top50Ranking?: { top50Ranking?: {
type: 'filtered' | 'dual'; eu: Map<string, number>;
overall?: Map<string, number>; uk: Map<string, number>;
eu?: Map<string, number>;
uk?: Map<string, number>;
}; };
onDrillDown?: (sku: string) => void; onDrillDown?: (sku: string) => void;
} }
@@ -66,6 +64,7 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
const [growthFilterMode, setGrowthFilterMode] = useState<'all' | 'up' | 'down' | 'stable'>('all'); const [growthFilterMode, setGrowthFilterMode] = useState<'all' | 'up' | 'down' | 'stable'>('all');
const [growthThreshold, setGrowthThreshold] = useState(10); const [growthThreshold, setGrowthThreshold] = useState(10);
const [showOnlyTop50, setShowOnlyTop50] = useState(false); const [showOnlyTop50, setShowOnlyTop50] = useState(false);
const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu');
// Default sort: most recent week, descending, units // Default sort: most recent week, descending, units
const [sortConfig, setSortConfig] = useState<SortConfig>(() => { const [sortConfig, setSortConfig] = useState<SortConfig>(() => {
@@ -78,7 +77,19 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
// Reset pagination when search changes // Reset pagination when search changes
useEffect(() => { useEffect(() => {
setCurrentPage(1); setCurrentPage(1);
}, [debouncedSearch, showOnlyTop50]); }, [debouncedSearch, showOnlyTop50, top50Mode]);
// Auto-detect best Top 50 mode based on currently selected data
useEffect(() => {
const hasUK = rows.some(r => r.customer.toLowerCase().includes('uk'));
const hasEU = rows.some(r => !r.customer.toLowerCase().includes('uk'));
if (hasUK && !hasEU) {
setTop50Mode('uk');
} else if (hasEU && !hasUK) {
setTop50Mode('eu');
}
}, [rows]);
const handleSort = useCallback((weekKey: string, metric: 'units' | 'spend' | 'rank' | 'gv') => { const handleSort = useCallback((weekKey: string, metric: 'units' | 'spend' | 'rank' | 'gv') => {
setSortConfig(prev => { setSortConfig(prev => {
@@ -115,10 +126,12 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
if (showOnlyTop50 && top50Ranking) { if (showOnlyTop50 && top50Ranking) {
result = result.filter(r => { result = result.filter(r => {
const asin = r.asin.trim().toUpperCase(); const asin = r.asin.trim().toUpperCase();
if (top50Ranking.type === 'filtered') { const isUK = r.customer.toLowerCase().includes('uk');
return top50Ranking.overall?.has(asin);
if (top50Mode === 'eu') {
return !isUK && top50Ranking.eu.has(asin);
} else { } else {
return top50Ranking.eu?.has(asin) || top50Ranking.uk?.has(asin); return isUK && top50Ranking.uk.has(asin);
} }
}); });
} }
@@ -181,13 +194,14 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
let rankA = 999; let rankA = 999;
let rankB = 999; let rankB = 999;
if (top50Ranking?.type === 'filtered') { if (top50Ranking) {
rankA = top50Ranking.overall?.get(asinA) || 999; if (top50Mode === 'eu') {
rankB = top50Ranking.overall?.get(asinB) || 999; rankA = top50Ranking.eu.get(asinA) || 999;
} else if (top50Ranking?.type === 'dual') { rankB = top50Ranking.eu.get(asinB) || 999;
// In dual mode, prioritize EU rank, then UK rank } else {
rankA = top50Ranking.eu?.get(asinA) || top50Ranking.uk?.get(asinA) || 999; rankA = top50Ranking.uk.get(asinA) || 999;
rankB = top50Ranking.eu?.get(asinB) || top50Ranking.uk?.get(asinB) || 999; rankB = top50Ranking.uk.get(asinB) || 999;
}
} }
return direction === 'asc' ? rankA - rankB : rankB - rankA; return direction === 'asc' ? rankA - rankB : rankB - rankA;
@@ -290,31 +304,42 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
{/* Top 50 Filter Toggle */} {/* Top 50 Filter Toggle */}
{top50Ranking && ( {top50Ranking && (
(top50Ranking.overall?.size || 0) > 0 || (top50Ranking.eu.size || 0) > 0 ||
(top50Ranking.eu?.size || 0) > 0 || (top50Ranking.uk.size || 0) > 0
(top50Ranking.uk?.size || 0) > 0
) && ( ) && (
<div className="flex bg-slate-950/50 p-1 rounded-xl border border-white/10 shadow-sm">
<button <button
onClick={() => { onClick={() => {
const newMode = !showOnlyTop50; const newMode = !showOnlyTop50;
setShowOnlyTop50(newMode); setShowOnlyTop50(newMode);
if (newMode) { if (newMode) handleSort('rank', 'rank');
handleSort('rank', 'rank');
}
}} }}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-all border ${showOnlyTop50 className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${showOnlyTop50
? 'bg-gradient-to-r from-amber-500 to-orange-500 text-white border-amber-400 shadow-lg shadow-amber-500/20' ? 'bg-gradient-to-r from-amber-500 to-orange-500 text-white 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' : 'text-slate-400 hover:text-amber-400'
}`} }`}
> >
<span className="text-sm">🏆</span> <span className="text-sm">🏆</span>
Top 50 Only Top 50
{showOnlyTop50 && (
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</button> </button>
{showOnlyTop50 && (
<div className="flex ml-1 p-0.5 bg-slate-900 rounded-lg border border-white/5">
<button
onClick={() => setTop50Mode('eu')}
className={`px-2 py-1 rounded-md text-[10px] font-black uppercase tracking-widest transition-all ${top50Mode === 'eu' ? 'bg-indigo-500 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'}`}
>
EU
</button>
<button
onClick={() => setTop50Mode('uk')}
className={`px-2 py-1 rounded-md text-[10px] font-black uppercase tracking-widest transition-all ${top50Mode === 'uk' ? 'bg-blue-500 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'}`}
>
UK
</button>
</div>
)}
</div>
)} )}
{/* Export Button */} {/* Export Button */}
@@ -447,14 +472,12 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
const ranks = []; const ranks = [];
if (top50Ranking) { if (top50Ranking) {
if (top50Ranking.type === 'filtered') { if (top50Mode === 'eu') {
const rank = top50Ranking.overall?.get(asin); const rank = top50Ranking.eu.get(asin);
if (rank) ranks.push({ rank, label: '', theme: 'amber' as const }); if (rank) ranks.push({ rank, label: 'EU', theme: 'indigo' as const });
} else { } else {
const euRank = top50Ranking.eu?.get(asin); const rank = top50Ranking.uk.get(asin);
const ukRank = top50Ranking.uk?.get(asin); if (rank) ranks.push({ rank, label: 'UK', theme: 'blue' as const });
if (euRank) ranks.push({ rank: euRank, label: 'EU', theme: 'indigo' as const });
if (ukRank) ranks.push({ rank: ukRank, label: 'UK', theme: 'blue' as const });
} }
} }