From 60330cb2f54dbf8f3a6c0c19116f978efd21a1b3 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Fri, 23 Jan 2026 09:35:04 +0100 Subject: [PATCH] feat: add Glance Views (GV) to Weekly Sales grid - Updated WeeklyPivotRow interface and pivotWeeklySalesData to aggregate GV data per week. - Modified WeeklyGrid.tsx to display GV next to Spend in total header and individual rows. - Added GV sorting support in the Weekly Sales grid. --- components/WeeklyGrid.tsx | 45 +++++++++++++++++++++++++++++---------- services/dataProcessor.ts | 5 ++++- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/components/WeeklyGrid.tsx b/components/WeeklyGrid.tsx index a252fb8..7d1a611 100644 --- a/components/WeeklyGrid.tsx +++ b/components/WeeklyGrid.tsx @@ -17,7 +17,7 @@ interface WeeklyGridProps { type SortConfig = { key: string; // weekKey or 'rank' direction: 'asc' | 'desc'; - metric: 'units' | 'spend' | 'rank'; + metric: 'units' | 'spend' | 'rank' | 'gv'; } | null; const ROWS_PER_PAGE = 50; @@ -80,7 +80,7 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown setCurrentPage(1); }, [debouncedSearch, showOnlyTop50]); - const handleSort = useCallback((weekKey: string, metric: 'units' | 'spend' | 'rank') => { + const handleSort = useCallback((weekKey: string, metric: 'units' | 'spend' | 'rank' | 'gv') => { setSortConfig(prev => { if (prev?.key === weekKey && prev.metric === metric) { return { key: weekKey, direction: prev.direction === 'asc' ? 'desc' : 'asc', metric }; @@ -91,16 +91,17 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown // Calculate totals in a SINGLE PASS (O(rows) instead of O(weeks × rows)) const weekTotals = useMemo(() => { - const totals: { [weekKey: string]: { units: number, spend: number } } = {}; + const totals: { [weekKey: string]: { units: number, spend: number, gv: number } } = {}; // Initialize all weeks weeks.forEach(week => { - totals[week] = { units: 0, spend: 0 }; + totals[week] = { units: 0, spend: 0, gv: 0 }; }); // Single pass through rows rows.forEach(row => { weeks.forEach(week => { totals[week].units += (row.unitsByWeek[week] || 0); totals[week].spend += (row.spendByWeek[week] || 0); + totals[week].gv += (row.gvByWeek?.[week] || 0); }); }); return totals; @@ -167,7 +168,8 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown if (!sortConfig) return filteredRows; const result = [...filteredRows]; - const metricKey = sortConfig.metric === 'units' ? 'unitsByWeek' : 'spendByWeek'; + const metricKey = sortConfig.metric === 'units' ? 'unitsByWeek' : + sortConfig.metric === 'spend' ? 'spendByWeek' : 'gvByWeek'; const weekKey = sortConfig.key; const direction = sortConfig.direction; @@ -396,13 +398,24 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown {/* Spend Sort Trigger */}
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 ${sortConfig?.key === week && sortConfig.metric === 'spend' ? 'bg-amber-500/5 text-amber-400' : 'text-slate-500 hover:text-slate-300'}`} + 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'}`} > Spend {sortConfig?.key === week && sortConfig.metric === 'spend' && ( {sortConfig.direction === 'asc' ? '↑' : '↓'} )}
+ + {/* GV Sort Trigger */} +
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'}`} + > + GV + {sortConfig?.key === week && sortConfig.metric === 'gv' && ( + {sortConfig.direction === 'asc' ? '↑' : '↓'} + )} +
))} @@ -419,6 +432,9 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown
€{(weekTotals[week]?.spend || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
+
+ GV: {(weekTotals[week]?.gv || 0).toLocaleString('de-DE')} +
))} @@ -477,11 +493,18 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown {val > 0 && renderGrowth(val, prevVal)} - {spend > 0 && ( - - €{spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} - - )} +
+ {spend > 0 && ( + + €{spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} + + )} + {row.gvByWeek?.[week] > 0 && ( + + GV: {row.gvByWeek[week].toLocaleString('de-DE')} + + )} +
); diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index cb0a408..9c92c56 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -1437,6 +1437,7 @@ export interface WeeklyPivotRow { customer: string; unitsByWeek: { [weekKey: string]: number }; // Key: "YYYY-WW" spendByWeek: { [weekKey: string]: number }; // Key: "YYYY-WW" + gvByWeek: { [weekKey: string]: number }; // Key: "YYYY-WW" } export const pivotWeeklySalesData = (data: CombinedKPIs[]): { @@ -1469,7 +1470,8 @@ export const pivotWeeklySalesData = (data: CombinedKPIs[]): { line: record.line || '', customer: record.customer || record.marketplace || '', unitsByWeek: {}, - spendByWeek: {} + spendByWeek: {}, + gvByWeek: {} }); } @@ -1480,6 +1482,7 @@ export const pivotWeeklySalesData = (data: CombinedKPIs[]): { // Only add cost if we haven't already added it for this ASIN/week // Since mergeSalesAndAdsData now outputs one record per ASIN/week, this should be clean row.spendByWeek[weekKey] = (row.spendByWeek[weekKey] || 0) + (record.cost || 0); + row.gvByWeek[weekKey] = (row.gvByWeek[weekKey] || 0) + (record.glanceViews || 0); } });