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:
Christian Vidal Wolf
2026-04-27 11:22:53 +02:00
co-authored by Claude Sonnet 4.6
parent 550269724a
commit eadbaf3492
7 changed files with 172 additions and 31 deletions
+20 -2
View File
@@ -921,6 +921,7 @@ const App: React.FC = () => {
onWocFilterChange={(s) => setFilters(prev => ({ ...prev, woc: s }))} onWocFilterChange={(s) => setFilters(prev => ({ ...prev, woc: s }))}
top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'} top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'}
buyBoxLostMap={buyBoxLostMap} buyBoxLostMap={buyBoxLostMap}
velocityMap={velocityMap}
/> />
</div> </div>
</Suspense> </Suspense>
@@ -947,7 +948,16 @@ const App: React.FC = () => {
<Suspense fallback={<LoadingSpinner />}> <Suspense fallback={<LoadingSpinner />}>
<div className={view === 'vendor' ? '' : 'hidden'}> <div className={view === 'vendor' ? '' : 'hidden'}>
<VendorDataView bsrData={filteredBsrData} asinMetadata={globalAsinMetadata} buyBoxLostMap={buyBoxLostMap} combinedSalesData={combinedAdsData} /> <VendorDataView
bsrData={filteredBsrData}
asinMetadata={globalAsinMetadata}
buyBoxLostMap={buyBoxLostMap}
combinedSalesData={combinedAdsData}
stockMap={stockMap}
vendorStockMap={vendorStockMap}
top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'}
velocityMap={velocityMap}
/>
</div> </div>
</Suspense> </Suspense>
@@ -962,7 +972,15 @@ const App: React.FC = () => {
<Suspense fallback={<LoadingSpinner />}> <Suspense fallback={<LoadingSpinner />}>
<div className={view === 'mkt' ? '' : 'hidden'}> <div className={view === 'mkt' ? '' : 'hidden'}>
<MktDataView rawData={filteredData} adsData={mktAdsData} /> <MktDataView
rawData={filteredData}
adsData={mktAdsData}
stockMap={stockMap}
vendorStockMap={vendorStockMap}
top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'}
velocityMap={velocityMap}
buyBoxLostMap={buyBoxLostMap}
/>
</div> </div>
</Suspense> </Suspense>
+7 -4
View File
@@ -26,6 +26,7 @@ interface AdsPerformanceProps {
wocFilter: string[]; wocFilter: string[];
onWocFilterChange: (newFilters: string[]) => void; onWocFilterChange: (newFilters: string[]) => void;
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>; buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
velocityMap?: Map<string, number>;
} }
type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits'; type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits';
@@ -48,7 +49,8 @@ const AdsRow: React.FC<{
stockMap?: Map<string, number>; stockMap?: Map<string, number>;
vendorStockMap?: Map<string, { eu: number; uk: number }>; vendorStockMap?: Map<string, { eu: number; uk: number }>;
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>; buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
}> = React.memo(({ item, top50Ranking, top50Mode, stockMap, vendorStockMap, buyBoxLostMap }) => { velocityMap?: Map<string, number>;
}> = React.memo(({ item, top50Ranking, top50Mode, stockMap, vendorStockMap, buyBoxLostMap, velocityMap }) => {
const asin = item.asin.trim().toUpperCase(); const asin = item.asin.trim().toUpperCase();
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = []; const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
@@ -80,7 +82,7 @@ const AdsRow: React.FC<{
{stockMap && ( {stockMap && (
<StockBadge stock={stockMap.get(item.sku?.replace(/(DE|EN)$/i, ''))} /> <StockBadge stock={stockMap.get(item.sku?.replace(/(DE|EN)$/i, ''))} />
)} )}
<VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={item.avgWeeklySales} /> <VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={velocityMap?.get(asin)} />
<BuyBoxWarningBadge asin={asin} buyBoxLostMap={buyBoxLostMap} /> <BuyBoxWarningBadge asin={asin} buyBoxLostMap={buyBoxLostMap} />
</div> </div>
</div> </div>
@@ -116,7 +118,8 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
wocFilter, wocFilter,
onWocFilterChange, onWocFilterChange,
top50Mode, top50Mode,
buyBoxLostMap buyBoxLostMap,
velocityMap
}) => { }) => {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [showOnlyTop50, setShowOnlyTop50] = useState(false); const [showOnlyTop50, setShowOnlyTop50] = useState(false);
@@ -487,7 +490,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
</thead> </thead>
<tbody className="divide-y divide-white/[0.03]"> <tbody className="divide-y divide-white/[0.03]">
{filteredAndSorted.map((p) => ( {filteredAndSorted.map((p) => (
<AdsRow key={p.id} item={p} top50Ranking={top50Ranking} top50Mode={top50Mode} stockMap={stockMap} vendorStockMap={vendorStockMap} buyBoxLostMap={buyBoxLostMap} /> <AdsRow key={p.id} item={p} top50Ranking={top50Ranking} top50Mode={top50Mode} stockMap={stockMap} vendorStockMap={vendorStockMap} buyBoxLostMap={buyBoxLostMap} velocityMap={velocityMap} />
))} ))}
</tbody> </tbody>
</table> </table>
+1 -3
View File
@@ -1385,12 +1385,10 @@ const DataGrid: React.FC<DataGridProps> = ({ data, filters, hasCustomerFilter, a
{row.title} {row.title}
</div> </div>
{stockMap && ( {stockMap && (
<>
<StockBadge stock={stockMap.get(row.sku?.replace(/(DE|EN)$/i, ''))} /> <StockBadge stock={stockMap.get(row.sku?.replace(/(DE|EN)$/i, ''))} />
)}
<VendorStockBadge asin={row.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={velocityMap?.get(row.asin.trim().toUpperCase())} /> <VendorStockBadge asin={row.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={velocityMap?.get(row.asin.trim().toUpperCase())} />
<BuyBoxWarningBadge asin={row.asin} buyBoxLostMap={buyBoxLostMap} /> <BuyBoxWarningBadge asin={row.asin} buyBoxLostMap={buyBoxLostMap} />
</>
)}
</div> </div>
: dim === 'asin' || dim === 'sku' : dim === 'asin' || dim === 'sku'
? <div className="flex items-center gap-2 min-w-max"> ? <div className="flex items-center gap-2 min-w-max">
+7 -3
View File
@@ -30,6 +30,7 @@ interface ForecastViewProps {
wocFilter: string[]; wocFilter: string[];
onWocFilterChange: (newFilters: string[]) => void; onWocFilterChange: (newFilters: string[]) => void;
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>; buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
velocityMap?: Map<string, number>;
} }
const MONTH_ORDER = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const MONTH_ORDER = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
@@ -42,7 +43,8 @@ const ForecastRow: React.FC<{
stockMap?: Map<string, number>; stockMap?: Map<string, number>;
vendorStockMap?: Map<string, { eu: number; uk: number }>; vendorStockMap?: Map<string, { eu: number; uk: number }>;
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>; buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
}> = React.memo(({ item, activeMonths, top50Ranking, top50Mode, stockMap, vendorStockMap, buyBoxLostMap }) => { velocityMap?: Map<string, number>;
}> = React.memo(({ item, activeMonths, top50Ranking, top50Mode, stockMap, vendorStockMap, buyBoxLostMap, velocityMap }) => {
const asin = item.asin.trim().toUpperCase(); const asin = item.asin.trim().toUpperCase();
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = []; const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
@@ -109,7 +111,7 @@ const ForecastRow: React.FC<{
<StockBadge stock={stockMap.get(item.sku?.replace(/(DE|EN)$/i, ''))} /> <StockBadge stock={stockMap.get(item.sku?.replace(/(DE|EN)$/i, ''))} />
)} )}
{/* WOC Indicator preserved */} {/* WOC Indicator preserved */}
<VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={item.avgWeeklySales} /> <VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={velocityMap?.get(asin)} />
<BuyBoxWarningBadge asin={asin} buyBoxLostMap={buyBoxLostMap} /> <BuyBoxWarningBadge asin={asin} buyBoxLostMap={buyBoxLostMap} />
</div> </div>
</div> </div>
@@ -181,7 +183,8 @@ const ForecastView: React.FC<ForecastViewProps> = ({
wocFilter, wocFilter,
onWocFilterChange, onWocFilterChange,
top50Mode, top50Mode,
buyBoxLostMap buyBoxLostMap,
velocityMap
}) => { }) => {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState('');
@@ -660,6 +663,7 @@ const ForecastView: React.FC<ForecastViewProps> = ({
stockMap={stockMap} stockMap={stockMap}
vendorStockMap={vendorStockMap} vendorStockMap={vendorStockMap}
buyBoxLostMap={buyBoxLostMap} buyBoxLostMap={buyBoxLostMap}
velocityMap={velocityMap}
/> />
))} ))}
</tbody> </tbody>
+28 -2
View File
@@ -2,6 +2,8 @@ import React, { useMemo } from 'react';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { BSRRecord, CombinedKPIs } from '../types'; import { BSRRecord, CombinedKPIs } from '../types';
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge'; import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
import { StockBadge } from './StockBadge';
import { VendorStockBadge } from './VendorStockBadge';
import { BSRUnitsCorrelationChart } from './BSRUnitsCorrelationChart'; import { BSRUnitsCorrelationChart } from './BSRUnitsCorrelationChart';
interface VendorDataViewProps { interface VendorDataViewProps {
@@ -9,6 +11,10 @@ interface VendorDataViewProps {
asinMetadata?: Map<string, { sku: string; title: string; line: string }>; asinMetadata?: Map<string, { sku: string; title: string; line: string }>;
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>; buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
combinedSalesData?: CombinedKPIs[]; combinedSalesData?: CombinedKPIs[];
stockMap?: Map<string, number>;
vendorStockMap?: Map<string, { eu: number; uk: number }>;
top50Mode?: 'eu' | 'uk';
velocityMap?: Map<string, number>;
} }
interface ChartPoint { interface ChartPoint {
@@ -306,7 +312,16 @@ const CATEGORY_TRANSLATIONS: Record<string, string> = {
const translateCategory = (name: string): string => const translateCategory = (name: string): string =>
CATEGORY_TRANSLATIONS[name] ?? name; CATEGORY_TRANSLATIONS[name] ?? name;
const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [], asinMetadata, buyBoxLostMap, combinedSalesData = [] }) => { const VendorDataView: React.FC<VendorDataViewProps> = ({
bsrData = [],
asinMetadata,
buyBoxLostMap,
combinedSalesData = [],
stockMap,
vendorStockMap,
top50Mode = 'eu',
velocityMap
}) => {
const activeMarkets = useMemo(() => { const activeMarkets = useMemo(() => {
const m = new Set(bsrData.map(r => r.market)); const m = new Set(bsrData.map(r => r.market));
return Array.from(m).sort(); return Array.from(m).sort();
@@ -461,11 +476,22 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [], asinMetad
</div> </div>
</div> </div>
</div> </div>
<div className="flex-shrink-0 pt-1"> <div className="flex-shrink-0 pt-1 flex flex-col items-end gap-3">
<div className="flex items-center gap-3">
{stockMap && (
<StockBadge stock={stockMap.get(productInfo.sku?.replace(/(DE|EN)$/i, ''))} />
)}
<VendorStockBadge
asin={productInfo.asin}
vendorStockMap={vendorStockMap}
mode={top50Mode}
avgWeeklySales={velocityMap?.get(productInfo.asin.trim().toUpperCase())}
/>
<BuyBoxWarningBadge asin={productInfo.asin} buyBoxLostMap={buyBoxLostMap} /> <BuyBoxWarningBadge asin={productInfo.asin} buyBoxLostMap={buyBoxLostMap} />
</div> </div>
</div> </div>
</div> </div>
</div>
)} )}
{/* BSR vs Units Correlation Chart */} {/* BSR vs Units Correlation Chart */}
+87 -13
View File
@@ -94,6 +94,7 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
const [displayCount, setDisplayCount] = useState(50); const [displayCount, setDisplayCount] = useState(50);
const [primaryMetric, setPrimaryMetric] = useState<'units' | 'revenue'>('units'); const [primaryMetric, setPrimaryMetric] = useState<'units' | 'revenue'>('units');
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement>(null);
const [hoveredTotalWeek, setHoveredTotalWeek] = useState<string | null>(null);
// State for Column Filters (SKU, ASIN, Title, Line) // State for Column Filters (SKU, ASIN, Title, Line)
const [columnFilters, setColumnFilters] = useState<Record<string, ColumnFilterCondition>>({}); const [columnFilters, setColumnFilters] = useState<Record<string, ColumnFilterCondition>>({});
@@ -175,6 +176,24 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
return totals; return totals;
}, [rows, weeks]); }, [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) // 1. Filter by search term, Top 50, and growth (using debounced value)
const filteredRows = useMemo(() => { const filteredRows = useMemo(() => {
@@ -770,39 +789,94 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
</span> </span>
</div> </div>
</th> </th>
{weeks.map((week, idx) => ( {weeks.map((week, idx) => {
<th key={week} className="p-3 text-sm font-black text-white text-center border-r border-white/10"> 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"> <div className="flex flex-col items-center">
{primaryMetric === 'units' ? ( {primaryMetric === 'units' ? (
<> <>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span>{weekTotals[week]?.units.toLocaleString('de-DE') || 0}</span> <span>{cur?.units.toLocaleString('de-DE') || 0}</span>
{renderGrowth(weekTotals[week]?.units || 0, weekTotals[weeks[idx + 1]]?.units || 0)} {renderGrowth(cur?.units || 0, prevWeekTotals?.units || 0)}
</div> </div>
<div className="flex items-center gap-0.5 text-rose-400/80 italic font-bold text-[10px]"> <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> <span>Ads: {(cur?.spend || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}</span>
{renderGrowth(weekTotals[week]?.spend || 0, weekTotals[weeks[idx + 1]]?.spend || 0)} {renderGrowth(cur?.spend || 0, prevWeekTotals?.spend || 0)}
</div> </div>
</> </>
) : ( ) : (
<> <>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span>{(weekTotals[week]?.revenue || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}</span> <span>{(cur?.revenue || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}</span>
{renderGrowth(weekTotals[week]?.revenue || 0, weekTotals[weeks[idx + 1]]?.revenue || 0)} {renderGrowth(cur?.revenue || 0, prevWeekTotals?.revenue || 0)}
</div> </div>
<div className="flex items-center gap-0.5 text-rose-400/80 italic font-bold text-[10px]"> <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> <span>Ads: {(cur?.spend || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}</span>
{renderGrowth(weekTotals[week]?.spend || 0, weekTotals[weeks[idx + 1]]?.spend || 0)} {renderGrowth(cur?.spend || 0, prevWeekTotals?.spend || 0)}
</div> </div>
</> </>
)} )}
<div className="flex items-center justify-center gap-1 text-[9px] text-teal-400 font-black tracking-tight mt-0.5"> <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> <span>GV: {(cur?.gv || 0).toLocaleString('de-DE')}</span>
{renderGrowth(weekTotals[week]?.gv || 0, weekTotals[weeks[idx + 1]]?.gv || 0)} {renderGrowth(cur?.gv || 0, prevWeekTotals?.gv || 0)}
</div> </div>
</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> </th>
))} );
})}
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-white/5"> <tbody className="divide-y divide-white/5">
+19 -1
View File
@@ -90,9 +90,22 @@ function parseChargebacksExcel(buffer: ArrayBuffer): Map<string, number> {
interface MktDataViewProps { interface MktDataViewProps {
rawData: SalesRecord[]; rawData: SalesRecord[];
adsData: AdsRecord[]; adsData: AdsRecord[];
stockMap?: Map<string, number>;
vendorStockMap?: Map<string, { eu: number; uk: number }>;
top50Mode?: 'eu' | 'uk';
velocityMap?: Map<string, number>;
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
} }
export default function MktDataView({ rawData, adsData }: MktDataViewProps) { export default function MktDataView({
rawData,
adsData,
stockMap,
vendorStockMap,
top50Mode = 'eu',
velocityMap,
buyBoxLostMap
}: MktDataViewProps) {
const [includeCOGS, setIncludeCOGS] = useState(true); const [includeCOGS, setIncludeCOGS] = useState(true);
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null); const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
@@ -314,6 +327,11 @@ export default function MktDataView({ rawData, adsData }: MktDataViewProps) {
products={filteredProducts} products={filteredProducts}
includeCOGS={includeCOGS} includeCOGS={includeCOGS}
onProductClick={setSelectedProduct} onProductClick={setSelectedProduct}
stockMap={stockMap}
vendorStockMap={vendorStockMap}
top50Mode={top50Mode}
velocityMap={velocityMap}
buyBoxLostMap={buyBoxLostMap}
/> />
</div> </div>