mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 12:15:23 +02:00
Feat: add hover tooltip on TOTALS row in Weekly Sales with WoW and YoY comparison
Shows previous week and same week prior year values (Units, Revenue, Ads Spend, GV) with % change indicators when hovering any week cell in the TOTALS row. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
550269724a
commit
eadbaf3492
+87
-13
@@ -94,6 +94,7 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
||||
const [displayCount, setDisplayCount] = useState(50);
|
||||
const [primaryMetric, setPrimaryMetric] = useState<'units' | 'revenue'>('units');
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [hoveredTotalWeek, setHoveredTotalWeek] = useState<string | null>(null);
|
||||
|
||||
// State for Column Filters (SKU, ASIN, Title, Line)
|
||||
const [columnFilters, setColumnFilters] = useState<Record<string, ColumnFilterCondition>>({});
|
||||
@@ -175,6 +176,24 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
||||
return totals;
|
||||
}, [rows, weeks]);
|
||||
|
||||
// Totals for ALL weeks (for YoY tooltip comparison)
|
||||
const allWeekTotals = useMemo(() => {
|
||||
const totals: { [weekKey: string]: { units: number, spend: number, revenue: number, gv: number } } = {};
|
||||
for (let i = 0; i < allWeeks.length; i++) {
|
||||
totals[allWeeks[i]] = { units: 0, spend: 0, revenue: 0, gv: 0 };
|
||||
}
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
for (let j = 0; j < allWeeks.length; j++) {
|
||||
const w = allWeeks[j];
|
||||
totals[w].units += (row.unitsByWeek[w] || 0);
|
||||
totals[w].spend += (row.spendByWeek[w] || 0);
|
||||
totals[w].revenue += (row.revenueByWeek[w] || 0);
|
||||
totals[w].gv += (row.gvByWeek?.[w] || 0);
|
||||
}
|
||||
}
|
||||
return totals;
|
||||
}, [rows, allWeeks]);
|
||||
|
||||
// 1. Filter by search term, Top 50, and growth (using debounced value)
|
||||
const filteredRows = useMemo(() => {
|
||||
@@ -770,39 +789,94 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
{weeks.map((week, idx) => (
|
||||
<th key={week} className="p-3 text-sm font-black text-white text-center border-r border-white/10">
|
||||
{weeks.map((week, idx) => {
|
||||
const [yearStr, weekNum] = week.split('-');
|
||||
const prevYearWeek = `${parseInt(yearStr) - 1}-${weekNum}`;
|
||||
const prevWeekTotals = allWeekTotals[weeks[idx + 1]];
|
||||
const yoyTotals = allWeekTotals[prevYearWeek];
|
||||
const cur = weekTotals[week];
|
||||
const isHovered = hoveredTotalWeek === week;
|
||||
|
||||
return (
|
||||
<th key={week} className="p-3 text-sm font-black text-white text-center border-r border-white/10 relative"
|
||||
onMouseEnter={() => setHoveredTotalWeek(week)}
|
||||
onMouseLeave={() => setHoveredTotalWeek(null)}
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
{primaryMetric === 'units' ? (
|
||||
<>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{weekTotals[week]?.units.toLocaleString('de-DE') || 0}</span>
|
||||
{renderGrowth(weekTotals[week]?.units || 0, weekTotals[weeks[idx + 1]]?.units || 0)}
|
||||
<span>{cur?.units.toLocaleString('de-DE') || 0}</span>
|
||||
{renderGrowth(cur?.units || 0, prevWeekTotals?.units || 0)}
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 text-rose-400/80 italic font-bold text-[10px]">
|
||||
<span>Ads: €{(weekTotals[week]?.spend || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}</span>
|
||||
{renderGrowth(weekTotals[week]?.spend || 0, weekTotals[weeks[idx + 1]]?.spend || 0)}
|
||||
<span>Ads: €{(cur?.spend || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}</span>
|
||||
{renderGrowth(cur?.spend || 0, prevWeekTotals?.spend || 0)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>€{(weekTotals[week]?.revenue || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}</span>
|
||||
{renderGrowth(weekTotals[week]?.revenue || 0, weekTotals[weeks[idx + 1]]?.revenue || 0)}
|
||||
<span>€{(cur?.revenue || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}</span>
|
||||
{renderGrowth(cur?.revenue || 0, prevWeekTotals?.revenue || 0)}
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 text-rose-400/80 italic font-bold text-[10px]">
|
||||
<span>Ads: €{(weekTotals[week]?.spend || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}</span>
|
||||
{renderGrowth(weekTotals[week]?.spend || 0, weekTotals[weeks[idx + 1]]?.spend || 0)}
|
||||
<span>Ads: €{(cur?.spend || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}</span>
|
||||
{renderGrowth(cur?.spend || 0, prevWeekTotals?.spend || 0)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-center gap-1 text-[9px] text-teal-400 font-black tracking-tight mt-0.5">
|
||||
<span>GV: {(weekTotals[week]?.gv || 0).toLocaleString('de-DE')}</span>
|
||||
{renderGrowth(weekTotals[week]?.gv || 0, weekTotals[weeks[idx + 1]]?.gv || 0)}
|
||||
<span>GV: {(cur?.gv || 0).toLocaleString('de-DE')}</span>
|
||||
{renderGrowth(cur?.gv || 0, prevWeekTotals?.gv || 0)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isHovered && (
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-50 w-64 bg-slate-900 border border-white/20 rounded-xl shadow-2xl p-3 text-left pointer-events-none">
|
||||
<div className="text-[10px] font-black text-white/50 uppercase tracking-widest mb-2">{week}</div>
|
||||
|
||||
{/* Semana anterior */}
|
||||
<div className="mb-2">
|
||||
<div className="text-[9px] font-black text-indigo-300 uppercase tracking-widest mb-1">Semana anterior ({weeks[idx + 1] || '—'})</div>
|
||||
{prevWeekTotals ? (
|
||||
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5 text-[11px]">
|
||||
<span className="text-white/60">Units</span>
|
||||
<span className="text-white font-bold text-right">{prevWeekTotals.units.toLocaleString('de-DE')} {renderGrowth(cur?.units || 0, prevWeekTotals.units)}</span>
|
||||
<span className="text-white/60">Revenue</span>
|
||||
<span className="text-white font-bold text-right">€{prevWeekTotals.revenue.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.revenue || 0, prevWeekTotals.revenue)}</span>
|
||||
<span className="text-white/60">Ads Spend</span>
|
||||
<span className="text-rose-400 font-bold text-right">€{prevWeekTotals.spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.spend || 0, prevWeekTotals.spend)}</span>
|
||||
<span className="text-white/60">GV</span>
|
||||
<span className="text-teal-400 font-bold text-right">{prevWeekTotals.gv.toLocaleString('de-DE')} {renderGrowth(cur?.gv || 0, prevWeekTotals.gv)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-white/30 text-[11px]">Sin datos</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-white/10 pt-2">
|
||||
<div className="text-[9px] font-black text-amber-300 uppercase tracking-widest mb-1">Misma semana año anterior ({prevYearWeek})</div>
|
||||
{yoyTotals && (yoyTotals.units > 0 || yoyTotals.revenue > 0) ? (
|
||||
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5 text-[11px]">
|
||||
<span className="text-white/60">Units</span>
|
||||
<span className="text-white font-bold text-right">{yoyTotals.units.toLocaleString('de-DE')} {renderGrowth(cur?.units || 0, yoyTotals.units)}</span>
|
||||
<span className="text-white/60">Revenue</span>
|
||||
<span className="text-white font-bold text-right">€{yoyTotals.revenue.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.revenue || 0, yoyTotals.revenue)}</span>
|
||||
<span className="text-white/60">Ads Spend</span>
|
||||
<span className="text-rose-400 font-bold text-right">€{yoyTotals.spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.spend || 0, yoyTotals.spend)}</span>
|
||||
<span className="text-white/60">GV</span>
|
||||
<span className="text-teal-400 font-bold text-right">{yoyTotals.gv.toLocaleString('de-DE')} {renderGrowth(cur?.revenue || 0, yoyTotals.gv)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-white/30 text-[11px]">Sin datos del año anterior</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
|
||||
Reference in New Issue
Block a user