import React, { useMemo, useState, useCallback } from 'react'; import * as XLSX from 'xlsx'; import { CombinedKPIs, FilterState } from '../types'; import { DownloadIcon, FunnelIcon, TrendingIcon, ChartIcon } from './Icons'; import { StockBadge } from './StockBadge'; import { Top50Badge } from './Top50Badge'; import { InColumnStockFilter } from './InColumnStockFilter'; interface AdsPerformanceProps { data: CombinedKPIs[]; filters: FilterState; top50Ranking?: { eu: Map; uk: Map; }; stockMap?: Map; stockFilter: string[]; onStockFilterChange: (newFilters: string[]) => void; top50Mode: 'eu' | 'uk'; } type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits'; // Define AdsProductKPIs type for clarity type AdsProductKPIs = CombinedKPIs & { acos: number; tacos: number; roas: number; ctr: number; cpc: number; cvrUnits: number; prevData?: AdsProductKPIs; }; const AdsRow: React.FC<{ item: AdsProductKPIs; top50Ranking?: { eu: Map; uk: Map }; top50Mode: 'eu' | 'uk'; stockMap?: Map; }> = React.memo(({ item, top50Ranking, top50Mode, stockMap }) => { const asin = item.asin.trim().toUpperCase(); const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = []; if (top50Ranking) { if (top50Mode === 'eu') { const rankEU = top50Ranking.eu.get(asin); if (rankEU) ranks.push({ rank: rankEU, label: 'EU', theme: 'indigo' }); } else { const rankUK = top50Ranking.uk.get(asin); if (rankUK) ranks.push({ rank: rankUK, label: 'UK', theme: 'blue' }); } } return (
{ranks.map((r, i) => ( ))} {item.asin}
SKU: {item.sku}
{item.title} {stockMap && ( )}
); }); const AdsPerformance: React.FC = ({ data, filters, top50Ranking, stockMap, stockFilter, onStockFilterChange, top50Mode }) => { const [searchTerm, setSearchTerm] = useState(''); const [showOnlyTop50, setShowOnlyTop50] = useState(false); const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: 'asc' | 'desc' }>({ key: 'cost', direction: 'desc' }); 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) { 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]); 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)) { 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)!; if (isCurrent) { entry.curr.salesTotal += item.salesTotal; entry.curr.unitsTotal += item.unitsTotal; entry.curr.salesAds += item.salesAds; entry.curr.unitsAds += item.unitsAds; entry.curr.cost += item.cost; entry.curr.clicks += item.clicks; entry.curr.impressions += item.impressions; entry.curr.conversions += item.conversions; entry.curr.salesOrganic += item.salesOrganic; entry.curr.glanceViews = (entry.curr.glanceViews || 0) + (item.glanceViews || 0); } else if (isPrevious) { if (!entry.prev) { entry.prev = { ...item, salesTotal: 0, unitsTotal: 0, salesAds: 0, unitsAds: 0, cost: 0, clicks: 0, impressions: 0, conversions: 0, salesOrganic: 0 }; } entry.prev.salesTotal += item.salesTotal; entry.prev.unitsTotal += item.unitsTotal; entry.prev.salesAds += item.salesAds; entry.prev.unitsAds += item.unitsAds; entry.prev.cost += item.cost; entry.prev.clicks += item.clicks; entry.prev.impressions += item.impressions; entry.prev.conversions += item.conversions; entry.prev.salesOrganic += item.salesOrganic; entry.prev.glanceViews = (entry.prev.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 } as AdsProductKPIs; }); }, [data, comparisonConfig]); 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; const filteredAndSorted = useMemo(() => { let result = aggregatedByAsin; if (showOnlyTop50 && top50Ranking) { const currentRankMap = top50Mode === 'eu' ? top50Ranking.eu : top50Ranking.uk; result = result.filter(p => currentRankMap.has(p.asin.toUpperCase())); } if (searchTerm) { const lowSearch = searchTerm.toLowerCase(); result = result.filter(item => item.asin.toLowerCase().includes(lowSearch) || item.sku.toLowerCase().includes(lowSearch) || item.title.toLowerCase().includes(lowSearch) ); } 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['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]); return (

Performance Overview

{aggregatedByAsin.length} products • {comparisonConfig ? `Comparison: ${comparisonConfig.prev} vs ${comparisonConfig.curr}` : 'Direct Overview'}

Product Inventory

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

{top50Ranking && (top50Ranking.eu.size > 0 || top50Ranking.uk.size > 0) && (
)} 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 w-48" />
{filteredAndSorted.map((p) => ( ))}
PRODUCT
); }; const SummaryCard = ({ label, value, prevValue, format, prefix = "", decimals = 0, color = "text-white", inverse = false }: any) => { 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 }: any) => { 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 }); 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 }: any) => { 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;