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.
This commit is contained in:
Christian Vidal Wolf
2026-01-23 09:35:04 +01:00
parent 7a0375fca7
commit 60330cb2f5
2 changed files with 38 additions and 12 deletions
+34 -11
View File
@@ -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<WeeklyGridProps> = ({ 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<WeeklyGridProps> = ({ 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<WeeklyGridProps> = ({ 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<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
{/* Spend Sort Trigger */}
<div
onClick={() => 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'}`}
>
<span className="text-[9px]">Spend</span>
{sortConfig?.key === week && sortConfig.metric === 'spend' && (
<span className="text-xs font-bold leading-none">{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>
)}
</div>
{/* GV Sort Trigger */}
<div
onClick={() => 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'}`}
>
<span className="text-[9px]">GV</span>
{sortConfig?.key === week && sortConfig.metric === 'gv' && (
<span className="text-xs font-bold leading-none">{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>
)}
</div>
</div>
</th>
))}
@@ -419,6 +432,9 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
<div className="text-[10px] text-indigo-400 font-bold">
{(weekTotals[week]?.spend || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
</div>
<div className="text-[9px] text-teal-400 font-black tracking-tight mt-0.5">
GV: {(weekTotals[week]?.gv || 0).toLocaleString('de-DE')}
</div>
</div>
</th>
))}
@@ -477,11 +493,18 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
</span>
{val > 0 && renderGrowth(val, prevVal)}
</div>
{spend > 0 && (
<span className={`text-[11px] font-medium ${sortConfig?.key === week && sortConfig.metric === 'spend' ? 'text-amber-300' : 'text-indigo-400/80'}`}>
{spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
</span>
)}
<div className="flex flex-col items-center">
{spend > 0 && (
<span className={`text-[11px] font-medium ${sortConfig?.key === week && sortConfig.metric === 'spend' ? 'text-amber-300' : 'text-indigo-400/80'}`}>
{spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
</span>
)}
{row.gvByWeek?.[week] > 0 && (
<span className={`text-[9px] font-black tracking-tighter ${sortConfig?.key === week && sortConfig.metric === 'gv' ? 'text-teal-300' : 'text-teal-500/70'}`}>
GV: {row.gvByWeek[week].toLocaleString('de-DE')}
</span>
)}
</div>
</div>
</td>
);