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
+67 -44
View File
@@ -6,10 +6,8 @@ import { pivotWeeklySalesData, WeeklyPivotRow } from '../services/dataProcessor'
interface WeeklyGridProps {
data: CombinedKPIs[];
top50Ranking?: {
type: 'filtered' | 'dual';
overall?: Map<string, number>;
eu?: Map<string, number>;
uk?: Map<string, number>;
eu: Map<string, number>;
uk: Map<string, number>;
};
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 [growthThreshold, setGrowthThreshold] = useState(10);
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu');
// Default sort: most recent week, descending, units
const [sortConfig, setSortConfig] = useState<SortConfig>(() => {
@@ -78,7 +77,19 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
// Reset pagination when search changes
useEffect(() => {
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') => {
setSortConfig(prev => {
@@ -115,10 +126,12 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
if (showOnlyTop50 && top50Ranking) {
result = result.filter(r => {
const asin = r.asin.trim().toUpperCase();
if (top50Ranking.type === 'filtered') {
return top50Ranking.overall?.has(asin);
const isUK = r.customer.toLowerCase().includes('uk');
if (top50Mode === 'eu') {
return !isUK && top50Ranking.eu.has(asin);
} 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 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;
if (top50Ranking) {
if (top50Mode === 'eu') {
rankA = top50Ranking.eu.get(asinA) || 999;
rankB = top50Ranking.eu.get(asinB) || 999;
} else {
rankA = top50Ranking.uk.get(asinA) || 999;
rankB = top50Ranking.uk.get(asinB) || 999;
}
}
return direction === 'asc' ? rankA - rankB : rankB - rankA;
@@ -290,31 +304,42 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
{/* Top 50 Filter Toggle */}
{top50Ranking && (
(top50Ranking.overall?.size || 0) > 0 ||
(top50Ranking.eu?.size || 0) > 0 ||
(top50Ranking.uk?.size || 0) > 0
(top50Ranking.eu.size || 0) > 0 ||
(top50Ranking.uk.size || 0) > 0
) && (
<button
onClick={() => {
const newMode = !showOnlyTop50;
setShowOnlyTop50(newMode);
if (newMode) {
handleSort('rank', 'rank');
}
}}
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'
}`}
>
<span className="text-sm">🏆</span>
Top 50 Only
<div className="flex bg-slate-950/50 p-1 rounded-xl border border-white/10 shadow-sm">
<button
onClick={() => {
const newMode = !showOnlyTop50;
setShowOnlyTop50(newMode);
if (newMode) handleSort('rank', 'rank');
}}
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 shadow-lg shadow-amber-500/20'
: 'text-slate-400 hover:text-amber-400'
}`}
>
<span className="text-sm">🏆</span>
Top 50
</button>
{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>
<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>
)}
</button>
</div>
)}
{/* Export Button */}
@@ -447,14 +472,12 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
const ranks = [];
if (top50Ranking) {
if (top50Ranking.type === 'filtered') {
const rank = top50Ranking.overall?.get(asin);
if (rank) ranks.push({ rank, label: '', theme: 'amber' as const });
if (top50Mode === 'eu') {
const rank = top50Ranking.eu.get(asin);
if (rank) ranks.push({ rank, label: 'EU', theme: 'indigo' 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 });
const rank = top50Ranking.uk.get(asin);
if (rank) ranks.push({ rank, label: 'UK', theme: 'blue' as const });
}
}