mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14:05:24 +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 = {
|
type SortConfig = {
|
||||||
key: string; // weekKey or 'rank'
|
key: string; // weekKey or 'rank'
|
||||||
direction: 'asc' | 'desc';
|
direction: 'asc' | 'desc';
|
||||||
metric: 'units' | 'spend' | 'rank';
|
metric: 'units' | 'spend' | 'rank' | 'gv';
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
const ROWS_PER_PAGE = 50;
|
const ROWS_PER_PAGE = 50;
|
||||||
@@ -80,7 +80,7 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}, [debouncedSearch, showOnlyTop50]);
|
}, [debouncedSearch, showOnlyTop50]);
|
||||||
|
|
||||||
const handleSort = useCallback((weekKey: string, metric: 'units' | 'spend' | 'rank') => {
|
const handleSort = useCallback((weekKey: string, metric: 'units' | 'spend' | 'rank' | 'gv') => {
|
||||||
setSortConfig(prev => {
|
setSortConfig(prev => {
|
||||||
if (prev?.key === weekKey && prev.metric === metric) {
|
if (prev?.key === weekKey && prev.metric === metric) {
|
||||||
return { key: weekKey, direction: prev.direction === 'asc' ? 'desc' : 'asc', 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))
|
// Calculate totals in a SINGLE PASS (O(rows) instead of O(weeks × rows))
|
||||||
const weekTotals = useMemo(() => {
|
const weekTotals = useMemo(() => {
|
||||||
const totals: { [weekKey: string]: { units: number, spend: number } } = {};
|
const totals: { [weekKey: string]: { units: number, spend: number, gv: number } } = {};
|
||||||
// Initialize all weeks
|
// Initialize all weeks
|
||||||
weeks.forEach(week => {
|
weeks.forEach(week => {
|
||||||
totals[week] = { units: 0, spend: 0 };
|
totals[week] = { units: 0, spend: 0, gv: 0 };
|
||||||
});
|
});
|
||||||
// Single pass through rows
|
// Single pass through rows
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
weeks.forEach(week => {
|
weeks.forEach(week => {
|
||||||
totals[week].units += (row.unitsByWeek[week] || 0);
|
totals[week].units += (row.unitsByWeek[week] || 0);
|
||||||
totals[week].spend += (row.spendByWeek[week] || 0);
|
totals[week].spend += (row.spendByWeek[week] || 0);
|
||||||
|
totals[week].gv += (row.gvByWeek?.[week] || 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return totals;
|
return totals;
|
||||||
@@ -167,7 +168,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
|
|||||||
if (!sortConfig) return filteredRows;
|
if (!sortConfig) return filteredRows;
|
||||||
|
|
||||||
const result = [...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 weekKey = sortConfig.key;
|
||||||
const direction = sortConfig.direction;
|
const direction = sortConfig.direction;
|
||||||
|
|
||||||
@@ -396,13 +398,24 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
|
|||||||
{/* Spend Sort Trigger */}
|
{/* Spend Sort Trigger */}
|
||||||
<div
|
<div
|
||||||
onClick={() => handleSort(week, 'spend')}
|
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>
|
<span className="text-[9px]">Spend</span>
|
||||||
{sortConfig?.key === week && sortConfig.metric === 'spend' && (
|
{sortConfig?.key === week && sortConfig.metric === 'spend' && (
|
||||||
<span className="text-xs font-bold leading-none">{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>
|
<span className="text-xs font-bold leading-none">{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
@@ -419,6 +432,9 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
|
|||||||
<div className="text-[10px] text-indigo-400 font-bold">
|
<div className="text-[10px] text-indigo-400 font-bold">
|
||||||
€{(weekTotals[week]?.spend || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
|
€{(weekTotals[week]?.spend || 0).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
@@ -477,11 +493,18 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
|
|||||||
</span>
|
</span>
|
||||||
{val > 0 && renderGrowth(val, prevVal)}
|
{val > 0 && renderGrowth(val, prevVal)}
|
||||||
</div>
|
</div>
|
||||||
{spend > 0 && (
|
<div className="flex flex-col items-center">
|
||||||
<span className={`text-[11px] font-medium ${sortConfig?.key === week && sortConfig.metric === 'spend' ? 'text-amber-300' : 'text-indigo-400/80'}`}>
|
{spend > 0 && (
|
||||||
€{spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
|
<span className={`text-[11px] font-medium ${sortConfig?.key === week && sortConfig.metric === 'spend' ? 'text-amber-300' : 'text-indigo-400/80'}`}>
|
||||||
</span>
|
€{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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1437,6 +1437,7 @@ export interface WeeklyPivotRow {
|
|||||||
customer: string;
|
customer: string;
|
||||||
unitsByWeek: { [weekKey: string]: number }; // Key: "YYYY-WW"
|
unitsByWeek: { [weekKey: string]: number }; // Key: "YYYY-WW"
|
||||||
spendByWeek: { [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[]): {
|
export const pivotWeeklySalesData = (data: CombinedKPIs[]): {
|
||||||
@@ -1469,7 +1470,8 @@ export const pivotWeeklySalesData = (data: CombinedKPIs[]): {
|
|||||||
line: record.line || '',
|
line: record.line || '',
|
||||||
customer: record.customer || record.marketplace || '',
|
customer: record.customer || record.marketplace || '',
|
||||||
unitsByWeek: {},
|
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
|
// 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
|
// Since mergeSalesAndAdsData now outputs one record per ASIN/week, this should be clean
|
||||||
row.spendByWeek[weekKey] = (row.spendByWeek[weekKey] || 0) + (record.cost || 0);
|
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