mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 12:25:22 +02:00
feat: Add Amazon Ads Weekly integration with Dashboard KPIs, Grid summary, and chart lines
- Update AdsRecord interface to support weekly data (year, week, cpc, ctr, acos, conversions) - Rewrite processAdsExcel to parse multiple sheets (2025, 2026) with 12-column format - Update mergeSalesAndAdsData to match by ASIN+Country+Year+Week - Add Advertising Performance section to Dashboard with Ad Spend, Attributed Sales, ACOS, ROAS - Add Ads toggle button and summary bar to DataGrid - Add Ad Spend lines (fuchsia dashed) to weekly comparison chart - Fix import paths in Dashboard.tsx and AdvertisingDashboard.tsx
This commit is contained in:
+120
-17
@@ -2,13 +2,14 @@ import React, { useState, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer
|
||||
} from 'recharts';
|
||||
import { SalesRecord, PivotRow } from '../types';
|
||||
import { SalesRecord, PivotRow, AdsRecord } from '../types';
|
||||
import { pivotSalesData, generateCSV, aggregateForTimeSeries, aggregateForComparisonTimeSeries, applyPanEUGrouping } from '../services/dataProcessor';
|
||||
import { DownloadIcon, FunnelIcon, CloseIcon, ChartIcon } from './Icons';
|
||||
|
||||
interface DataGridProps {
|
||||
data: SalesRecord[];
|
||||
hasCustomerFilter: boolean;
|
||||
adsData?: AdsRecord[];
|
||||
}
|
||||
|
||||
type SortConfig = {
|
||||
@@ -225,11 +226,32 @@ const ExpandableChartCard: React.FC<{ title: string; children: React.ReactNode;
|
||||
};
|
||||
|
||||
|
||||
const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter }) => {
|
||||
const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData = [] }) => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [sortConfig, setSortConfig] = useState<SortConfig>({ key: null, direction: 'desc' });
|
||||
const [showChart, setShowChart] = useState(true);
|
||||
const [visibleMetrics, setVisibleMetrics] = useState<('sellOut' | 'units')[]>(['sellOut', 'units']);
|
||||
const [showAdsMetrics, setShowAdsMetrics] = useState(true);
|
||||
|
||||
// Calculate Ads Summary for the Grid
|
||||
const adsSummary = useMemo(() => {
|
||||
if (!adsData || adsData.length === 0) return null;
|
||||
|
||||
const totals = adsData.reduce((acc, ad) => ({
|
||||
cost: acc.cost + ad.cost,
|
||||
attributedSales: acc.attributedSales + ad.attributedSales30d,
|
||||
clicks: acc.clicks + ad.clicks,
|
||||
impressions: acc.impressions + ad.impressions,
|
||||
}), { cost: 0, attributedSales: 0, clicks: 0, impressions: 0 });
|
||||
|
||||
return {
|
||||
totalSpend: totals.cost,
|
||||
attributedSales: totals.attributedSales,
|
||||
acos: totals.attributedSales > 0 ? (totals.cost / totals.attributedSales) * 100 : 0,
|
||||
roas: totals.cost > 0 ? totals.attributedSales / totals.cost : 0,
|
||||
recordCount: adsData.length,
|
||||
};
|
||||
}, [adsData]);
|
||||
|
||||
// State for dynamic grouping
|
||||
const [selectedDimensions, setSelectedDimensions] = useState<string[]>(['line', 'customer', 'sku', 'title']);
|
||||
@@ -260,22 +282,42 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter }) => {
|
||||
const yearsInView = Array.from(new Set(data.map(d => d.year.toString()))).sort((a: string, b: string) => parseInt(b) - parseInt(a));
|
||||
const isMultiYear = yearsInView.length > 1;
|
||||
|
||||
if (isMultiYear) {
|
||||
return {
|
||||
chartData: aggregateForComparisonTimeSeries(data),
|
||||
uniqueYears: yearsInView,
|
||||
isComparisonView: true,
|
||||
chartTitle: `Weekly Sales Comparison: ${yearsInView.join(' vs ')}`
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
chartData: aggregateForTimeSeries(data),
|
||||
uniqueYears: yearsInView,
|
||||
isComparisonView: false,
|
||||
chartTitle: `Weekly Sales Evolution ${yearsInView[0] || ''}`
|
||||
};
|
||||
// Get base sales data
|
||||
let salesChartData = isMultiYear
|
||||
? aggregateForComparisonTimeSeries(data)
|
||||
: aggregateForTimeSeries(data);
|
||||
|
||||
// Aggregate ads data by week/year and merge into chartData
|
||||
if (adsData && adsData.length > 0) {
|
||||
const adsMap = new Map<number, { [key: string]: number }>();
|
||||
|
||||
adsData.forEach(ad => {
|
||||
if (ad.week >= 1 && ad.week <= 53) {
|
||||
if (!adsMap.has(ad.week)) {
|
||||
adsMap.set(ad.week, {});
|
||||
}
|
||||
const weekData = adsMap.get(ad.week)!;
|
||||
const adSpendKey = `${ad.year}_adSpend`;
|
||||
weekData[adSpendKey] = (weekData[adSpendKey] || 0) + ad.cost;
|
||||
}
|
||||
});
|
||||
|
||||
// Merge ads data into sales chart data
|
||||
salesChartData = salesChartData.map(point => {
|
||||
const adsWeekData = adsMap.get(point.week) || {};
|
||||
return { ...point, ...adsWeekData };
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return {
|
||||
chartData: salesChartData,
|
||||
uniqueYears: yearsInView,
|
||||
isComparisonView: isMultiYear,
|
||||
chartTitle: isMultiYear
|
||||
? `Weekly Sales Comparison: ${yearsInView.join(' vs ')}`
|
||||
: `Weekly Sales Evolution ${yearsInView[0] || ''}`
|
||||
};
|
||||
}, [data, adsData]);
|
||||
|
||||
// Filter Options based on available data
|
||||
const metricOptions = useMemo(() => {
|
||||
@@ -484,6 +526,19 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter }) => {
|
||||
strokeDasharray="5 5"
|
||||
dot={false}
|
||||
/>
|
||||
),
|
||||
// Ad Spend line (only when ads data exists and showAdsMetrics is on)
|
||||
showAdsMetrics && adsSummary && (
|
||||
<Line
|
||||
key={`${year}_adSpend`}
|
||||
type="monotone"
|
||||
dataKey={`${year}_adSpend`}
|
||||
name={`Ad Spend ${year}`}
|
||||
stroke="#d946ef"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="3 3"
|
||||
dot={false}
|
||||
/>
|
||||
)
|
||||
])
|
||||
) : (
|
||||
@@ -571,6 +626,19 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter }) => {
|
||||
<ChartIcon />
|
||||
<span className="hidden sm:inline">{showChart ? 'Hide Chart' : 'Show Chart'}</span>
|
||||
</button>
|
||||
|
||||
{/* Ads Toggle - Only show when ads data is loaded */}
|
||||
{adsSummary && (
|
||||
<button
|
||||
onClick={() => setShowAdsMetrics(!showAdsMetrics)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${showAdsMetrics ? 'bg-fuchsia-600/20 text-fuchsia-400 border-fuchsia-500/50' : 'bg-slate-800 text-slate-300 border-slate-700 hover:bg-slate-700'}`}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-4 h-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 18.75a60.07 60.07 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 0 1 3 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 0 0-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 0 1-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 0 0 3 15h-.75M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm3 0h.008v.008H18V10.5Zm-12 0h.008v.008H6V10.5Z" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">{showAdsMetrics ? 'Hide Ads' : 'Show Ads'}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 w-full lg:w-auto justify-between lg:justify-end">
|
||||
@@ -738,6 +806,41 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter }) => {
|
||||
</div>
|
||||
|
||||
|
||||
{/* Ads Performance Summary - Shows when ads data is loaded */}
|
||||
{adsSummary && showAdsMetrics && (
|
||||
<div className="bg-gradient-to-r from-fuchsia-900/20 to-indigo-900/20 border-y border-fuchsia-500/30 px-4 py-3">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-fuchsia-400 animate-pulse"></span>
|
||||
<span className="text-xs font-bold text-fuchsia-300 uppercase tracking-wide">Advertising Data:</span>
|
||||
<span className="text-xs text-slate-400">{adsSummary.recordCount.toLocaleString('de-DE')} records</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="px-3 py-1 bg-slate-900/50 rounded-lg">
|
||||
<span className="text-xs text-slate-400 mr-2">Ad Spend:</span>
|
||||
<span className="text-sm font-bold text-fuchsia-400">€{adsSummary.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
<div className="px-3 py-1 bg-slate-900/50 rounded-lg">
|
||||
<span className="text-xs text-slate-400 mr-2">Attr. Sales:</span>
|
||||
<span className="text-sm font-bold text-emerald-400">€{adsSummary.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
<div className="px-3 py-1 bg-slate-900/50 rounded-lg">
|
||||
<span className="text-xs text-slate-400 mr-2">ACOS:</span>
|
||||
<span className={`text-sm font-bold ${adsSummary.acos <= 30 ? 'text-emerald-400' : adsSummary.acos <= 50 ? 'text-amber-400' : 'text-red-400'}`}>
|
||||
{adsSummary.acos.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="px-3 py-1 bg-slate-900/50 rounded-lg">
|
||||
<span className="text-xs text-slate-400 mr-2">ROAS:</span>
|
||||
<span className={`text-sm font-bold ${adsSummary.roas >= 3 ? 'text-emerald-400' : adsSummary.roas >= 2 ? 'text-amber-400' : 'text-red-400'}`}>
|
||||
{adsSummary.roas.toFixed(2)}x
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Data Table */}
|
||||
<div className="overflow-x-auto min-h-[400px]">
|
||||
<table className="w-full text-left text-sm border-collapse">
|
||||
|
||||
Reference in New Issue
Block a user