mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14:45:24 +02:00
feat: enable ads metrics at any grouping level (SKU, Line, etc.) in Grid
This commit is contained in:
@@ -409,7 +409,7 @@ const App: React.FC = () => {
|
|||||||
adsData={filteredAdsData}
|
adsData={filteredAdsData}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{view === 'table' && <DataGrid data={filteredData} hasCustomerFilter={filters.customer.length > 0} adsData={filteredAdsData} />}
|
{view === 'table' && <DataGrid data={combinedAdsData} hasCustomerFilter={filters.customer.length > 0} adsData={filteredAdsData} />}
|
||||||
{view === 'movers' && <TopMovers data={filteredData} />}
|
{view === 'movers' && <TopMovers data={filteredData} />}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+12
-51
@@ -2,12 +2,12 @@ import React, { useState, useMemo, useEffect } from 'react';
|
|||||||
import {
|
import {
|
||||||
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer
|
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import { SalesRecord, PivotRow, AdsRecord } from '../types';
|
import { SalesRecord, PivotRow, AdsRecord, CombinedKPIs } from '../types';
|
||||||
import { pivotSalesData, generateCSV, aggregateForTimeSeries, aggregateForComparisonTimeSeries, applyPanEUGrouping } from '../services/dataProcessor';
|
import { pivotSalesData, generateCSV, aggregateForTimeSeries, aggregateForComparisonTimeSeries, applyPanEUGrouping } from '../services/dataProcessor';
|
||||||
import { DownloadIcon, FunnelIcon, CloseIcon, ChartIcon, TrendingIcon } from './Icons';
|
import { DownloadIcon, FunnelIcon, CloseIcon, ChartIcon, TrendingIcon } from './Icons';
|
||||||
|
|
||||||
interface DataGridProps {
|
interface DataGridProps {
|
||||||
data: SalesRecord[];
|
data: SalesRecord[] | CombinedKPIs[];
|
||||||
hasCustomerFilter: boolean;
|
hasCustomerFilter: boolean;
|
||||||
adsData?: AdsRecord[];
|
adsData?: AdsRecord[];
|
||||||
}
|
}
|
||||||
@@ -271,59 +271,20 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData =
|
|||||||
[selectedDimensions]);
|
[selectedDimensions]);
|
||||||
|
|
||||||
// Transform flat data into Pivot structure
|
// Transform flat data into Pivot structure
|
||||||
const { rows: basePivotRows, years } = useMemo(() => {
|
const pivotRows = useMemo(() => {
|
||||||
// Apply Pan-EU grouping when no customer filter is applied
|
// Apply Pan-EU grouping when no customer filter is applied
|
||||||
const processedData = applyPanEUGrouping(data, hasCustomerFilter);
|
const processedData = applyPanEUGrouping(data as SalesRecord[], hasCustomerFilter);
|
||||||
|
|
||||||
return pivotSalesData(processedData, effectiveDimensions);
|
// pivotSalesData now handles ads aggregation correctly because it receives CombinedKPIs
|
||||||
|
const { rows } = pivotSalesData(processedData, effectiveDimensions);
|
||||||
|
return rows;
|
||||||
}, [data, effectiveDimensions, hasCustomerFilter]);
|
}, [data, effectiveDimensions, hasCustomerFilter]);
|
||||||
|
|
||||||
// Enrich pivot rows with ads data aggregated by ASIN
|
const { years } = useMemo(() => {
|
||||||
const pivotRows = useMemo(() => {
|
// We still need unique years for columns
|
||||||
if (!adsData || adsData.length === 0) return basePivotRows;
|
const yearsSet = new Set(data.map(d => String((d as any).year)));
|
||||||
|
return { years: Array.from(yearsSet).sort((a, b) => parseInt(b) - parseInt(a)) };
|
||||||
// Aggregate ads by ASIN + Year
|
}, [data]);
|
||||||
const adsAggMap = new Map<string, Map<string, { adSpend: number; attributedSales: number }>>();
|
|
||||||
|
|
||||||
adsData.forEach(ad => {
|
|
||||||
const asinKey = ad.asin.toUpperCase();
|
|
||||||
const yearKey = ad.year.toString();
|
|
||||||
|
|
||||||
if (!adsAggMap.has(asinKey)) {
|
|
||||||
adsAggMap.set(asinKey, new Map());
|
|
||||||
}
|
|
||||||
const yearMap = adsAggMap.get(asinKey)!;
|
|
||||||
|
|
||||||
if (!yearMap.has(yearKey)) {
|
|
||||||
yearMap.set(yearKey, { adSpend: 0, attributedSales: 0 });
|
|
||||||
}
|
|
||||||
const yearData = yearMap.get(yearKey)!;
|
|
||||||
yearData.adSpend += ad.cost;
|
|
||||||
yearData.attributedSales += ad.attributedSales30d;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Enrich each pivot row with ads data
|
|
||||||
return basePivotRows.map(row => {
|
|
||||||
const asinKey = row.asin.toUpperCase();
|
|
||||||
const yearMap = adsAggMap.get(asinKey);
|
|
||||||
|
|
||||||
if (!yearMap) return row;
|
|
||||||
|
|
||||||
const adsByYear: Record<string, { adSpend: number; attributedSales: number; acos: number; tacos: number }> = {};
|
|
||||||
|
|
||||||
yearMap.forEach((adsYearData, yearKey) => {
|
|
||||||
const salesForYear = row.totalsByYear[yearKey]?.sellOut || 0;
|
|
||||||
adsByYear[yearKey] = {
|
|
||||||
adSpend: adsYearData.adSpend,
|
|
||||||
attributedSales: adsYearData.attributedSales,
|
|
||||||
acos: adsYearData.attributedSales > 0 ? (adsYearData.adSpend / adsYearData.attributedSales) * 100 : 0,
|
|
||||||
tacos: salesForYear > 0 ? (adsYearData.adSpend / salesForYear) * 100 : 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return { ...row, adsByYear };
|
|
||||||
});
|
|
||||||
}, [basePivotRows, adsData]);
|
|
||||||
|
|
||||||
// Data for the time series chart, supporting single and multi-year comparison
|
// Data for the time series chart, supporting single and multi-year comparison
|
||||||
const { chartData, uniqueYears, isComparisonView, chartTitle } = useMemo(() => {
|
const { chartData, uniqueYears, isComparisonView, chartTitle } = useMemo(() => {
|
||||||
|
|||||||
@@ -1013,7 +1013,7 @@ export const getUniqueValues = (data: SalesRecord[], field: keyof SalesRecord):
|
|||||||
return Array.from(values).sort();
|
return Array.from(values).sort();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const pivotSalesData = (data: SalesRecord[], dimensions: string[] = ['title', 'customer', 'line', 'sku']): { rows: PivotRow[], years: string[] } => {
|
export const pivotSalesData = (data: any[], dimensions: string[] = ['title', 'customer', 'line', 'sku']): { rows: PivotRow[], years: string[] } => {
|
||||||
// 1. Determine all years present in the data for columns
|
// 1. Determine all years present in the data for columns
|
||||||
const yearsSet = new Set(data.map(d => d.year));
|
const yearsSet = new Set(data.map(d => d.year));
|
||||||
const years = Array.from(yearsSet).sort((a, b) => b - a).map(String);
|
const years = Array.from(yearsSet).sort((a, b) => b - a).map(String);
|
||||||
@@ -1022,7 +1022,7 @@ export const pivotSalesData = (data: SalesRecord[], dimensions: string[] = ['tit
|
|||||||
|
|
||||||
data.forEach(record => {
|
data.forEach(record => {
|
||||||
// Group by Dynamic Dimensions
|
// Group by Dynamic Dimensions
|
||||||
const keyParts = dimensions.map(dim => String(record[dim as keyof SalesRecord] || ''));
|
const keyParts = dimensions.map(dim => String(record[dim] || ''));
|
||||||
const key = keyParts.join('||');
|
const key = keyParts.join('||');
|
||||||
|
|
||||||
if (!map.has(key)) {
|
if (!map.has(key)) {
|
||||||
@@ -1039,12 +1039,14 @@ export const pivotSalesData = (data: SalesRecord[], dimensions: string[] = ['tit
|
|||||||
monthIndex: i,
|
monthIndex: i,
|
||||||
byYear: {}
|
byYear: {}
|
||||||
})),
|
})),
|
||||||
totalsByYear: {}
|
totalsByYear: {},
|
||||||
|
adsByYear: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = map.get(key)!;
|
const row = map.get(key)!;
|
||||||
const monthPart = record.month.split('-')[0]; // Handle "Apr-23" -> "Apr"
|
const monthRaw = record.month || '';
|
||||||
|
const monthPart = monthRaw.split('-')[0]; // Handle "Apr-23" -> "Apr"
|
||||||
const monthIdx = MONTH_ORDER.indexOf(monthPart);
|
const monthIdx = MONTH_ORDER.indexOf(monthPart);
|
||||||
const yearStr = record.year.toString();
|
const yearStr = record.year.toString();
|
||||||
|
|
||||||
@@ -1052,17 +1054,33 @@ export const pivotSalesData = (data: SalesRecord[], dimensions: string[] = ['tit
|
|||||||
if (!row.totalsByYear[yearStr]) {
|
if (!row.totalsByYear[yearStr]) {
|
||||||
row.totalsByYear[yearStr] = { sellOut: 0, units: 0 };
|
row.totalsByYear[yearStr] = { sellOut: 0, units: 0 };
|
||||||
}
|
}
|
||||||
row.totalsByYear[yearStr].sellOut += record.sellOut;
|
row.totalsByYear[yearStr].sellOut += (record.sellOut || record.salesTotal || 0);
|
||||||
row.totalsByYear[yearStr].units += record.units;
|
row.totalsByYear[yearStr].units += (record.units || record.unitsTotal || 0);
|
||||||
|
|
||||||
// 2. Update Monthly Data
|
// 2. Update Ads Data (if present in the record)
|
||||||
|
if (record.cost !== undefined || record.salesAds !== undefined) {
|
||||||
|
if (!row.adsByYear) row.adsByYear = {};
|
||||||
|
if (!row.adsByYear[yearStr]) {
|
||||||
|
row.adsByYear[yearStr] = { adSpend: 0, attributedSales: 0, acos: 0, tacos: 0 };
|
||||||
|
}
|
||||||
|
row.adsByYear[yearStr].adSpend += (record.cost || 0);
|
||||||
|
row.adsByYear[yearStr].attributedSales += (record.salesAds || 0);
|
||||||
|
|
||||||
|
// Recalculate ACOS/TACOS at the aggregated level
|
||||||
|
const ads = row.adsByYear[yearStr];
|
||||||
|
const sales = row.totalsByYear[yearStr].sellOut;
|
||||||
|
ads.acos = ads.attributedSales > 0 ? (ads.adSpend / ads.attributedSales) * 100 : 0;
|
||||||
|
ads.tacos = sales > 0 ? (ads.adSpend / sales) * 100 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Update Monthly Data
|
||||||
if (monthIdx !== -1) {
|
if (monthIdx !== -1) {
|
||||||
const m = row.months[monthIdx];
|
const m = row.months[monthIdx];
|
||||||
if (!m.byYear[yearStr]) {
|
if (!m.byYear[yearStr]) {
|
||||||
m.byYear[yearStr] = { sellOut: 0, units: 0 };
|
m.byYear[yearStr] = { sellOut: 0, units: 0 };
|
||||||
}
|
}
|
||||||
m.byYear[yearStr].sellOut += record.sellOut;
|
m.byYear[yearStr].sellOut += (record.sellOut || record.salesTotal || 0);
|
||||||
m.byYear[yearStr].units += record.units;
|
m.byYear[yearStr].units += (record.units || record.unitsTotal || 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user