import React, { useMemo, useState } from 'react'; import { CombinedKPIs, FilterState } from '../types'; interface AdsPerformanceProps { data: CombinedKPIs[]; filters: FilterState; } type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits'; const AdsPerformance: React.FC = ({ data, filters }) => { const [searchTerm, setSearchTerm] = useState(''); const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: 'asc' | 'desc' }>({ key: 'cost', direction: 'desc' }); // Detect comparison mode: exactly 2 weeks, 2 months, or 2 years const comparisonConfig = useMemo(() => { let periods: string[] = []; let type: 'week' | 'month' | 'year' | null = null; if (filters.week.length === 2) { periods = [...filters.week].sort((a, b) => { const wA = parseInt(a.replace('W', '')); const wB = parseInt(b.replace('W', '')); return wA - wB; }); type = 'week'; } else if (filters.month.length === 2) { // Simple sort for abbreviated months might be tricky without year, // but usually users pick consecutive ones. periods = [...filters.month]; type = 'month'; } else if (filters.year.length === 2) { periods = [...filters.year].sort(); type = 'year'; } if (type && periods.length === 2) { return { type, prev: periods[0], curr: periods[1] }; } return null; }, [filters]); // 1. Aggregate and Compare data by ASIN const aggregatedByAsin = useMemo(() => { const map = new Map(); data.forEach(item => { const asin = item.asin.trim().toUpperCase(); const periodVal = comparisonConfig?.type === 'week' ? `W${item.week}` : comparisonConfig?.type === 'month' ? item.month : comparisonConfig?.type === 'year' ? item.year.toString() : null; const isCurrent = !comparisonConfig || periodVal === comparisonConfig.curr; const isPrevious = comparisonConfig && periodVal === comparisonConfig.prev; if (!map.has(asin)) { // Initialize with empty record if it's new const defaultRec: CombinedKPIs = { ...item, salesTotal: 0, unitsTotal: 0, salesAds: 0, unitsAds: 0, cost: 0, clicks: 0, impressions: 0, conversions: 0, salesOrganic: 0 }; map.set(asin, { curr: isCurrent ? { ...item } : { ...defaultRec }, prev: isPrevious ? { ...item } : undefined }); } else { const entry = map.get(asin)!; const target = isCurrent ? entry.curr : (isPrevious ? (entry.prev || (entry.prev = { ...item, salesTotal: 0, unitsTotal: 0, salesAds: 0, unitsAds: 0, cost: 0, clicks: 0, impressions: 0, conversions: 0, salesOrganic: 0 })) : null); if (target) { target.salesTotal += item.salesTotal; target.unitsTotal += item.unitsTotal; target.salesAds += item.salesAds; target.unitsAds += item.unitsAds; target.cost += item.cost; target.clicks += item.clicks; target.impressions += item.impressions; target.conversions += item.conversions; target.salesOrganic += item.salesOrganic; } } }); return Array.from(map.values()).map(({ curr, prev }) => { const calcMetrics = (item: CombinedKPIs) => ({ ...item, acos: item.salesAds > 0 ? (item.cost / item.salesAds) * 100 : 0, tacos: item.salesTotal > 0 ? (item.cost / item.salesTotal) * 100 : 0, roas: item.cost > 0 ? item.salesAds / item.cost : 0, ctr: item.impressions > 0 ? (item.clicks / item.impressions) * 100 : 0, cpc: item.clicks > 0 ? item.cost / item.clicks : 0, cvrUnits: item.clicks > 0 ? (item.conversions / item.clicks) * 100 : 0, }); const currFinal = calcMetrics(curr); const prevFinal = prev ? calcMetrics(prev) : undefined; return { ...currFinal, prevData: prevFinal }; }); }, [data, comparisonConfig]); // 2. Global Totals const totals = useMemo(() => { const sum = (items: any[]) => items.reduce((acc, curr) => ({ salesTotal: acc.salesTotal + curr.salesTotal, salesAds: acc.salesAds + curr.salesAds, salesOrganic: acc.salesOrganic + curr.salesOrganic, cost: acc.cost + curr.cost, impressions: acc.impressions + curr.impressions, clicks: acc.clicks + curr.clicks, conversions: acc.conversions + curr.conversions, }), { salesTotal: 0, salesAds: 0, salesOrganic: 0, cost: 0, impressions: 0, clicks: 0, conversions: 0 }); const currTotals = sum(aggregatedByAsin); const prevTotals = comparisonConfig ? sum(aggregatedByAsin.map(a => a.prevData).filter(Boolean)) : null; return { curr: currTotals, prev: prevTotals }; }, [aggregatedByAsin, comparisonConfig]); const calculateDerived = (t: any) => ({ acos: t.salesAds > 0 ? (t.cost / t.salesAds) * 100 : 0, tacos: t.salesTotal > 0 ? (t.cost / t.salesTotal) * 100 : 0, roas: t.cost > 0 ? t.salesAds / t.cost : 0, ctr: t.impressions > 0 ? (t.clicks / t.impressions) * 100 : 0, cpc: t.clicks > 0 ? t.cost / t.clicks : 0, cvr: t.clicks > 0 ? (t.conversions / t.clicks) * 100 : 0, }); const currMetrics = calculateDerived(totals.curr); const prevMetrics = totals.prev ? calculateDerived(totals.prev) : null; // 3. Filter and Sort const filteredAndSorted = useMemo(() => { let result = aggregatedByAsin.filter(item => item.asin.toLowerCase().includes(searchTerm.toLowerCase()) || item.sku.toLowerCase().includes(searchTerm.toLowerCase()) || item.title.toLowerCase().includes(searchTerm.toLowerCase()) ); result.sort((a, b) => { const valA = a[sortConfig.key] as number; const valB = b[sortConfig.key] as number; return sortConfig.direction === 'asc' ? valA - valB : valB - valA; }); return result; }, [aggregatedByAsin, searchTerm, sortConfig]); const handleSort = (key: SortKey) => { setSortConfig(prev => ({ key, direction: prev.key === key && prev.direction === 'desc' ? 'asc' : 'desc' })); }; const formatCurrency = (val: number) => `$${Math.round(val).toLocaleString('de-DE')}`; const formatNumber = (val: number) => Math.round(val).toLocaleString('de-DE'); const formatPercent = (val: number) => `${val.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%`; return (
{/* Header */}

Performance Overview

{aggregatedByAsin.length} products • {comparisonConfig ? ` Comparison: ${comparisonConfig.prev} vs ${comparisonConfig.curr}` : ' Direct Overview'}
{/* Summary Grid */}
{/* Table Section */}

Product Inventory

{comparisonConfig ? `Comparing ${comparisonConfig.curr} vs ${comparisonConfig.prev}` : 'Displaying total data for selected filters.'}

setSearchTerm(e.target.value)} className="bg-[#0B0E14] border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 w-64" />
{filteredAndSorted.map((row: any) => ( ))}
PRODUCT
{row.asin} SKU: {row.sku} {row.title}
); }; const SummaryCard = ({ label, value, prevValue, color = "text-white", inverse = false }: { label: string; value: string; prevValue?: number; color?: string; inverse?: boolean }) => { // Correctly parse value regardless of locale formatting (handles dots as thousands and commas as decimals) const numericValue = parseFloat(value.replace(/[^\d,]/g, '').replace(',', '.')); const growth = prevValue !== undefined && prevValue > 0 ? ((numericValue - prevValue) / prevValue) * 100 : null; return (
{label}
{value} {growth !== null && ( 0 ? !inverse : inverse) ? 'text-emerald-400' : 'text-rose-500'}`}> {growth > 0 ? '↑' : '↓'} {Math.abs(growth).toFixed(1)}% )}
); }; const TableCell = ({ value, prevValue, format, prefix = "", inverse = false, highlight = false }: { value: number; prevValue?: number; format: 'currency' | 'number' | 'percent'; prefix?: string; inverse?: boolean; highlight?: boolean }) => { const growth = prevValue !== undefined && prevValue > 0 ? ((value - prevValue) / prevValue) * 100 : null; let displayValue = ""; if (format === 'currency') displayValue = Math.round(value).toLocaleString('de-DE'); else if (format === 'number') { if (prefix === "$") { displayValue = "$" + value.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } else { displayValue = prefix + Math.round(value).toLocaleString('de-DE'); } } else if (format === 'percent') displayValue = value.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + "%"; return (
{displayValue} {growth !== null && ( 0 ? !inverse : inverse) ? 'text-emerald-400' : 'text-rose-400'} opacity-80`}> {growth > 0 ? '+' : ''}{growth.toFixed(1)}% )}
); } const SortableHeader = ({ label, sortKey, activeSort, onSort }: { label: string; sortKey: SortKey; activeSort: { key: SortKey; direction: 'asc' | 'desc' }; onSort: (key: SortKey) => void; }) => { const isActive = activeSort.key === sortKey; return ( onSort(sortKey)} className={`p-4 font-black text-slate-400 uppercase tracking-widest text-right cursor-pointer hover:text-white transition-colors group min-w-[100px] whitespace-nowrap`} >
{label}
); }; export default AdsPerformance;