feat: add Top 50 Best Sellers badge in Weekly Sales

- Calculate static Top 50 ranking based on 2025 Sell Out (independent of filters)
- Display trophy badge with position number for Top 50 ASINs
- Badge shows 🏆 followed by rank (1-50)
- Ranking stays static regardless of date/country filters applied
- Pre-computed ranking based on cumulative 2025 data
This commit is contained in:
Christian Vidal Wolf
2026-01-22 12:02:21 +01:00
parent 15f5b08c71
commit 1c8ec7b835
2 changed files with 78 additions and 37 deletions
+27 -1
View File
@@ -254,6 +254,32 @@ 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)
const top50Ranking2025 = useMemo(() => {
// Filter only 2025 data
const data2025 = rawData.filter(r => r.year === 2025);
// Aggregate Sell Out by ASIN
const asinTotals = new Map<string, number>();
data2025.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);
// Create a Map of ASIN -> rank (1-50)
const rankMap = new Map<string, number>();
sorted.forEach(([asin], index) => {
rankMap.set(asin, index + 1);
});
return rankMap;
}, [rawData]);
// Combine Sales & Ads Data dynamically based on current filters
const combinedAdsData = useMemo(() => {
return mergeSalesAndAdsData(filteredData, filteredAdsData);
@@ -427,7 +453,7 @@ const App: React.FC = () => {
)}
<Suspense fallback={<LoadingSpinner />}>
{view === 'table' && <DataGrid data={combinedAdsData} hasCustomerFilter={filters.customer.length > 0} adsData={filteredAdsData} />}
{view === 'weekly' && <WeeklyGrid data={combinedAdsData} />}
{view === 'weekly' && <WeeklyGrid data={combinedAdsData} top50Ranking={top50Ranking2025} />}
{view === 'movers' && <TopMovers data={filteredData} />}
</Suspense>
</div>
+18 -3
View File
@@ -5,6 +5,7 @@ import { pivotWeeklySalesData, WeeklyPivotRow } from '../services/dataProcessor'
interface WeeklyGridProps {
data: CombinedKPIs[];
top50Ranking?: Map<string, number>; // Static ranking of Top 50 ASINs by 2025 Sell Out
}
type SortConfig = {
@@ -26,7 +27,17 @@ const useDebounce = (value: string, delay: number) => {
return debouncedValue;
};
const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
// Top 50 Badge Component
const Top50Badge: React.FC<{ rank: number }> = ({ rank }) => (
<span
className="inline-flex items-center justify-center px-1.5 py-0.5 rounded text-[9px] font-black bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-sm border border-amber-400/50"
title={`Top ${rank} Best Seller 2025`}
>
🏆 {rank}
</span>
);
const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking }) => {
// Pivot data - memoized
const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
@@ -327,11 +338,14 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
</thead>
<tbody className="divide-y divide-white/5">
{paginatedRows.length > 0 ? (
paginatedRows.map((row) => (
paginatedRows.map((row) => {
const top50Rank = top50Ranking?.get(row.asin.trim().toUpperCase());
return (
<tr key={row.id} className="hover:bg-white/[0.02] transition-colors group">
<td className="p-3 py-2 sticky left-0 z-10 bg-slate-900 group-hover:bg-slate-800 border-r border-white/10">
<div className="flex flex-col">
<div className="flex items-center gap-2 mb-0.5">
{top50Rank && <Top50Badge rank={top50Rank} />}
<span className="text-xs font-black text-indigo-400 uppercase tracking-tighter truncate max-w-[120px]">{row.sku || '-'}</span>
<span className="text-[10px] font-bold text-slate-500 bg-slate-800 px-1.5 py-0.5 rounded border border-white/5">{row.asin}</span>
</div>
@@ -362,7 +376,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
);
})}
</tr>
))
);
})
) : (
<tr>
<td colSpan={weeks.length + 1} className="p-10 text-center text-slate-500 italic text-base">