Refactor: Unified Top 50 Badge logic and shared component integration

This commit is contained in:
Christian Vidal Wolf
2026-01-28 08:55:09 +01:00
parent 87e097224b
commit 6c788d335f
6 changed files with 378 additions and 715 deletions
+9 -71
View File
@@ -4,6 +4,7 @@ import { CombinedKPIs } from '../types';
import { pivotWeeklySalesData, WeeklyPivotRow, PAN_EU_COUNTRIES } from '../services/dataProcessor';
import { StockBadge } from './StockBadge';
import { InColumnStockFilter } from './InColumnStockFilter';
import { Top50Badge } from './Top50Badge';
interface WeeklyGridProps {
data: CombinedKPIs[];
@@ -37,26 +38,6 @@ const useDebounce = (value: string, delay: number) => {
return debouncedValue;
};
// Top 50 Badge Component
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 (
<span
className={`inline-flex items-center justify-center px-1.5 py-0.5 rounded text-[9px] font-black bg-gradient-to-r ${themeClasses[theme]} text-white shadow-sm border`}
title={`Top ${rank} Best Seller 2025 ${label ? `(${label})` : ''}`}
>
<span className="mr-0.5">🏆</span>
{label && <span className="mr-0.5 opacity-90">{label}</span>}
{rank}
</span>
);
};
const WeeklyRow: React.FC<{
row: WeeklyPivotRow;
weeks: string[];
@@ -71,22 +52,13 @@ const WeeklyRow: React.FC<{
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
const asin = row.asin.trim().toUpperCase();
const isUKSelected = customerFilters.some(c => c.toLowerCase().includes('uk'));
const isEUPartialSelected = customerFilters.some(c => PAN_EU_COUNTRIES.includes(c));
const hasNoCustomerFilter = customerFilters.length === 0;
if (top50Ranking) {
// Only show UK rank if UK is explicitly selected
if (top50Mode === 'uk' && isUKSelected) {
const rank = top50Ranking.uk.get(asin);
if (rank) ranks.push({ rank, label: 'UK', theme: 'blue' });
}
// Only show EU rank if at least one Pan-EU country is selected
// OR if no filters are selected (User might want to see them all then, but user said "only when... is selected")
// Actually the prompt says: "only appear when in the filters... is selected"
else if (top50Mode === 'eu' && isEUPartialSelected) {
if (top50Mode === 'eu') {
const rank = top50Ranking.eu.get(asin);
if (rank) ranks.push({ rank, label: 'EU', theme: 'indigo' });
} else {
const rank = top50Ranking.uk.get(asin);
if (rank) ranks.push({ rank, label: 'UK', theme: 'blue' });
}
}
@@ -157,7 +129,7 @@ const WeeklyRow: React.FC<{
);
});
const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown, stockMap, stockFilter, onStockFilterChange, customerFilters }) => {
const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({ data, top50Ranking, onDrillDown, stockMap, stockFilter, onStockFilterChange, customerFilters, top50Mode }) => {
// Pivot data - memoized
const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
@@ -170,7 +142,6 @@ 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');
const [displayCount, setDisplayCount] = useState(50);
const scrollContainerRef = useRef<HTMLDivElement>(null);
@@ -185,20 +156,9 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
// Reset pagination when search changes
useEffect(() => {
setCurrentPage(1);
setDisplayCount(50);
}, [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 => {
if (prev?.key === weekKey && prev.metric === metric) {
@@ -287,7 +247,7 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
}
return result;
}, [rows, debouncedSearch, showOnlyTop50, top50Ranking, growthFilterMode, growthThreshold, sortConfig, weeks]);
}, [rows, debouncedSearch, showOnlyTop50, top50Ranking, growthFilterMode, growthThreshold, sortConfig, weeks, top50Mode]);
// 2. Sort results
const sortedRows = useMemo(() => {
@@ -359,7 +319,6 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
return rowData;
});
// Add Totals row
const totalsRow: any = {
SKU: 'TOTALS',
ASIN: '',
@@ -443,25 +402,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
}`}
>
<span className="text-sm">🏆</span>
Top 50
Top 50 ({top50Mode.toUpperCase()})
</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>
)}
@@ -531,12 +473,10 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
className={`p-0 text-[10px] font-black uppercase tracking-widest text-center border-r border-white/10 min-w-[130px] transition-colors select-none ${sortConfig?.key === week ? 'bg-white/[0.02]' : ''}`}
>
<div className="flex flex-col h-full">
{/* Week Label */}
<div className="p-2 border-b border-white/5 bg-slate-800/30 text-xs text-white">
{week.split('-')[1]}/{week.split('-')[0].slice(-2)}
</div>
{/* Units Sort Trigger */}
<div
onClick={() => handleSort(week, 'units')}
className={`flex-1 p-1.5 cursor-pointer hover:bg-indigo-500/10 transition-colors flex items-center justify-center gap-1 border-b border-white/5 ${sortConfig?.key === week && sortConfig.metric === 'units' ? 'bg-indigo-500/5 text-indigo-400' : 'text-slate-500 hover:text-slate-300'}`}
@@ -547,7 +487,6 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
)}
</div>
{/* Spend Sort Trigger */}
<div
onClick={() => handleSort(week, 'spend')}
className={`flex-1 p-1.5 cursor-pointer hover:bg-amber-500/10 transition-colors flex items-center justify-center gap-1 border-b border-white/5 ${sortConfig?.key === week && sortConfig.metric === 'spend' ? 'bg-amber-500/5 text-amber-400' : 'text-slate-500 hover:text-slate-300'}`}
@@ -558,7 +497,6 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
)}
</div>
{/* GV Sort Trigger */}
<div
onClick={() => handleSort(week, 'gv')}
className={`flex-1 p-1.5 cursor-pointer hover:bg-teal-500/10 transition-colors flex items-center justify-center gap-1 ${sortConfig?.key === week && sortConfig.metric === 'gv' ? 'bg-teal-500/5 text-teal-400' : 'text-slate-500 hover:text-slate-300'}`}