import React, { useMemo, useState, useCallback } from 'react'; import * as XLSX from 'xlsx'; import { CombinedKPIs, FilterState } from '../types'; import { DownloadIcon } from './Icons'; import { StockBadge } from './StockBadge'; interface AdsPerformanceProps { data: CombinedKPIs[]; filters: FilterState; top50Ranking?: { eu: Map; uk: Map; }; stockMap?: Map; } type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits'; // Top 50 Badge Component const Top50Badge: React.FC<{ rank: number; label?: string; theme?: 'amber' | 'blue' | 'indigo' }> = ({ rank, label, theme = 'amber' }) => { const themeClasses = { amber: 'from-amber-500 to-orange-500 border-amber-400/50', blue: 'from-blue-500 to-cyan-500 border-blue-400/50', indigo: 'from-indigo-500 to-purple-500 border-indigo-400/50', }; return ( 🏆 {label && {label}} {rank} ); }; const AdsPerformance: React.FC = ({ data, filters, top50Ranking, stockMap }) => { const [searchTerm, setSearchTerm] = useState(''); const [showOnlyTop50, setShowOnlyTop50] = useState(false); const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu'); 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; target.glanceViews = (target.glanceViews || 0) + (item.glanceViews || 0); } } }); 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, glanceViews: acc.glanceViews + (curr.glanceViews || 0), }), { salesTotal: 0, salesAds: 0, salesOrganic: 0, cost: 0, impressions: 0, clicks: 0, conversions: 0, glanceViews: 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, cvrGv: t.glanceViews > 0 ? (t.conversions / t.glanceViews) * 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; // Top 50 Filter if (showOnlyTop50 && top50Ranking) { result = result.filter(item => { const asin = item.asin.trim().toUpperCase(); if (top50Mode === 'eu') { return top50Ranking.eu.has(asin); } else { return top50Ranking.uk.has(asin); } }); } // Search Filter result = result.filter(item => item.asin.toLowerCase().includes(searchTerm.toLowerCase()) || item.sku.toLowerCase().includes(searchTerm.toLowerCase()) || item.title.toLowerCase().includes(searchTerm.toLowerCase()) ); // Sort: by ranking when Top 50 is active, otherwise by sortConfig if (showOnlyTop50 && top50Ranking) { result.sort((a, b) => { const asinA = a.asin.trim().toUpperCase(); const asinB = b.asin.trim().toUpperCase(); const rankA = top50Mode === 'eu' ? (top50Ranking.eu.get(asinA) || 999) : (top50Ranking.uk.get(asinA) || 999); const rankB = top50Mode === 'eu' ? (top50Ranking.eu.get(asinB) || 999) : (top50Ranking.uk.get(asinB) || 999); return rankA - rankB; }); } else { 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, showOnlyTop50, top50Ranking, top50Mode]); const handleSort = (key: SortKey) => { setSortConfig(prev => ({ key, direction: prev.key === key && prev.direction === 'desc' ? 'asc' : 'desc' })); }; const handleExportExcel = useCallback(() => { const exportData = filteredAndSorted.map(row => { const rowData: any = { ASIN: row.asin, SKU: row.sku, Title: row.title, 'Total Sales': Math.round(row.salesTotal), 'Ads Sales': Math.round(row.salesAds), 'Organic Sales': Math.round(row.salesOrganic), 'Ad Spend': Math.round(row.cost), Impressions: row.impressions, Clicks: row.clicks, Orders: row.conversions, CPC: Number(row.cpc.toFixed(2)), 'CTR (%)': Number(row.ctr.toFixed(2)), 'CVR (%)': Number(row.cvrUnits.toFixed(2)), 'ACOS (%)': Number(row.acos.toFixed(2)), ROAS: Number(row.roas.toFixed(2)), 'TACOS (%)': Number(row.tacos.toFixed(2)), 'Glance Views': row.glanceViews || 0, }; if (row.prevData) { rowData['Prev Total Sales'] = Math.round(row.prevData.salesTotal); rowData['Prev Ads Sales'] = Math.round(row.prevData.salesAds); rowData['Prev Ad Spend'] = Math.round(row.prevData.cost); rowData['Growth Sales (%)'] = row.prevData.salesTotal > 0 ? Number((((row.salesTotal - row.prevData.salesTotal) / row.prevData.salesTotal) * 100).toFixed(1)) : 0; } return rowData; }); const ws = XLSX.utils.json_to_sheet(exportData); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Ads Performance'); XLSX.writeFile(wb, `Ads_Performance_Export_${new Date().toISOString().slice(0, 10)}.xlsx`); }, [filteredAndSorted]); 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.'}

{/* Top 50 Filter Toggle */} {top50Ranking && ( (top50Ranking.eu.size || 0) > 0 || (top50Ranking.uk.size || 0) > 0 ) && (
{showOnlyTop50 && (
)}
)} 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-48" />
{filteredAndSorted.map((row: any) => { const asin = row.asin.trim().toUpperCase(); const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = []; if (top50Ranking) { const euRank = top50Ranking.eu.get(asin); const ukRank = top50Ranking.uk.get(asin); if (euRank) ranks.push({ rank: euRank, label: 'EU', theme: 'indigo' }); if (ukRank) ranks.push({ rank: ukRank, label: 'UK', theme: 'blue' }); } return ( ); })}
PRODUCT
{ranks.map((r, i) => ( ))} {row.asin} {stockMap && ( )}
SKU: {row.sku} {row.title}
); }; const SummaryCard = ({ label, value, prevValue, format, prefix = "", decimals = 0, color = "text-white", inverse = false }: { label: string; value: number; prevValue?: number; format: 'currency' | 'number' | 'percent'; prefix?: string; decimals?: number; color?: string; inverse?: boolean }) => { const growth = prevValue !== undefined && prevValue > 0 ? ((value - prevValue) / prevValue) * 100 : null; const formatVal = (v: number) => { if (format === 'currency') return `$${Math.round(v).toLocaleString('de-DE')}`; if (format === 'percent') return v.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + "%"; return prefix + v.toLocaleString('de-DE', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); }; return (
{label}
{formatVal(value)} {prevValue !== undefined && (
Prev: {formatVal(prevValue)} {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; const formatVal = (v: number) => { if (format === 'currency') return Math.round(v).toLocaleString('de-DE'); if (format === 'number') { if (prefix === "$") { return "$" + v.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } else { return prefix + Math.round(v).toLocaleString('de-DE'); } } return v.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + "%"; }; return (
{formatVal(value)} {prevValue !== undefined && (
Prev: {formatVal(prevValue)} {growth !== null && ( 0 ? !inverse : inverse) ? 'text-emerald-400' : 'text-rose-400'}`}> {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;