mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 15:05:22 +02:00
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:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user