From 7178b57073caa088dcc7a3b80c5fdfacc16c1953 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Tue, 27 Jan 2026 15:34:06 +0100 Subject: [PATCH] Feature: Added Excel export to Ads and Forecast views --- components/AdsPerformance.tsx | 54 +++++++++++++++++++++++++++++++++-- components/ForecastView.tsx | 54 +++++++++++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/components/AdsPerformance.tsx b/components/AdsPerformance.tsx index 3920c17..c140bd5 100644 --- a/components/AdsPerformance.tsx +++ b/components/AdsPerformance.tsx @@ -1,5 +1,7 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; +import * as XLSX from 'xlsx'; import { CombinedKPIs, FilterState } from '../types'; +import { DownloadIcon } from './Icons'; interface AdsPerformanceProps { data: CombinedKPIs[]; @@ -215,6 +217,44 @@ const AdsPerformance: React.FC = ({ data, filters, top50Ran })); }; + 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')}`; @@ -306,11 +346,19 @@ const AdsPerformance: React.FC = ({ data, filters, top50Ran )} 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" + 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" /> + diff --git a/components/ForecastView.tsx b/components/ForecastView.tsx index 71e28cf..fbee0d9 100644 --- a/components/ForecastView.tsx +++ b/components/ForecastView.tsx @@ -1,6 +1,7 @@ - -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useState, useEffect, useCallback } from 'react'; +import * as XLSX from 'xlsx'; import { ProductForecastData, FilterState } from '../types'; +import { DownloadIcon } from './Icons'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, Legend, ComposedChart, Area @@ -168,6 +169,47 @@ const ForecastView: React.FC = ({ data, filters, top50Ranking return result; }, [filteredProducts, showOnlyTop50, top50Mode, top50Ranking]); + const handleExportExcel = useCallback(() => { + const exportData = sortedProducts.map(p => { + const rowData: any = { + ASIN: p.asin, + SKU: p.sku, + Title: p.title, + 'Product Line': p.line, + 'Annual Forecast': Math.round(p.annualForecast), + 'Annual Actual': Math.round(p.actualUnits), + 'Annual Fulfillment (%)': p.annualForecast > 0 ? Number(((p.actualUnits / p.annualForecast) * 100).toFixed(1)) : 0, + 'Period Forecast': 0, + 'Period Actual': 0, + }; + + p.monthlyData.forEach(md => { + if (activeMonths.includes(md.month)) { + rowData['Period Forecast'] += md.forecastUnits; + rowData['Period Actual'] += md.actualUnits; + } + }); + + rowData['Period Fulfillment (%)'] = rowData['Period Forecast'] > 0 + ? Number(((rowData['Period Actual'] / rowData['Period Forecast']) * 100).toFixed(1)) + : 0; + + // Add monthly details + MONTH_ORDER.forEach(m => { + const md = p.monthlyData.find(d => d.month === m); + rowData[`${m} FC`] = md ? Math.round(md.forecastUnits) : 0; + rowData[`${m} ACT`] = md ? Math.round(md.actualUnits) : 0; + }); + + return rowData; + }); + + const ws = XLSX.utils.json_to_sheet(exportData); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, 'Forecast Overview'); + XLSX.writeFile(wb, `Forecast_Export_${new Date().toISOString().slice(0, 10)}.xlsx`); + }, [sortedProducts, activeMonths]); + return (
@@ -299,6 +341,14 @@ const ForecastView: React.FC = ({ data, filters, top50Ranking />
+