mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 12:15:23 +02:00
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:
+34
-11
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user