import React, { useState, useMemo, useEffect } from 'react'; import * as XLSX from 'xlsx'; import { SalesRecord, AdsRecord } from '../../types'; import { Product } from './types'; import { KPICards } from './KPICards'; import { MasterTable } from './MasterTable'; import { WaterfallModal } from './WaterfallModal'; import { Settings2 } from 'lucide-react'; // --------------------------------------------------------------------------- // Excel parsing helpers (all files use raw: true so numbers come back as JS numbers) // --------------------------------------------------------------------------- function parseNumericCell(val: unknown): number { if (typeof val === 'number') return isNaN(val) ? 0 : val; if (typeof val === 'string') { const clean = val.replace(/[€$£\s]/g, '').trim(); if (!clean) return 0; // EU format: "1.234,56" — comma is decimal, dot is thousands if (clean.includes(',') && clean.includes('.') && clean.indexOf(',') > clean.indexOf('.')) { return parseFloat(clean.replace(/\./g, '').replace(',', '.')) || 0; } // EU format: "263,83" — only comma, treat as decimal if (clean.includes(',') && !clean.includes('.')) { return parseFloat(clean.replace(',', '.')) || 0; } return parseFloat(clean.replace(/,/g, '')) || 0; } return 0; } // Deals file: ASIN = col A (idx 0), total deal cost = col C (idx 2) function parseDealsExcel(buffer: ArrayBuffer): Map { const wb = XLSX.read(buffer, { type: 'array' }); const ws = wb.Sheets[wb.SheetNames[0]]; const rows = XLSX.utils.sheet_to_json(ws, { header: 1, raw: true }); const map = new Map(); for (let i = 1; i < rows.length; i++) { const row = rows[i] as unknown[]; const asin = row[0]; const cost = row[2]; if (asin && typeof asin === 'string' && asin.trim()) { const key = asin.trim().toUpperCase(); map.set(key, (map.get(key) || 0) + parseNumericCell(cost)); } } return map; } // Promos file: ASIN = col E (idx 4), promo cost = col K (idx 10) function parsePromosExcel(buffer: ArrayBuffer): Map { const wb = XLSX.read(buffer, { type: 'array' }); const ws = wb.Sheets[wb.SheetNames[0]]; const rows = XLSX.utils.sheet_to_json(ws, { header: 1, raw: true }); const map = new Map(); for (let i = 1; i < rows.length; i++) { const row = rows[i] as unknown[]; const asin = row[4]; const cost = row[10]; if (asin && typeof asin === 'string' && asin.trim()) { const key = asin.trim().toUpperCase(); map.set(key, (map.get(key) || 0) + parseNumericCell(cost)); } } return map; } // Chargebacks file: ASIN = col AN (idx 39), chargeback cost = col B (idx 1) function parseChargebacksExcel(buffer: ArrayBuffer): Map { const wb = XLSX.read(buffer, { type: 'array' }); const ws = wb.Sheets[wb.SheetNames[0]]; const rows = XLSX.utils.sheet_to_json(ws, { header: 1, raw: true }); const map = new Map(); for (let i = 1; i < rows.length; i++) { const row = rows[i] as unknown[]; const asin = row[39]; const cost = row[1]; if (asin && typeof asin === 'string' && asin.trim()) { const key = asin.trim().toUpperCase(); map.set(key, (map.get(key) || 0) + parseNumericCell(cost)); } } return map; } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- interface MktDataViewProps { rawData: SalesRecord[]; adsData: AdsRecord[]; } export default function MktDataView({ rawData, adsData }: MktDataViewProps) { const [includeCOGS, setIncludeCOGS] = useState(true); const [selectedProduct, setSelectedProduct] = useState(null); const [mktLoading, setMktLoading] = useState(true); const [dealsMap, setDealsMap] = useState>(new Map()); const [promosMap, setPromosMap] = useState>(new Map()); const [chargebacksMap, setChargebacksMap] = useState>(new Map()); // Fetch the 3 new marketing data files once on mount useEffect(() => { let cancelled = false; const fetchAll = async () => { setMktLoading(true); try { const [dealsRes, promosRes, chargesRes] = await Promise.all([ fetch('/api/fetch-mkt-data?file=deals'), fetch('/api/fetch-mkt-data?file=promos'), fetch('/api/fetch-mkt-data?file=chargebacks'), ]); if (cancelled) return; if (dealsRes.ok) { const buf = await dealsRes.arrayBuffer(); if (!cancelled) setDealsMap(parseDealsExcel(buf)); } else { console.warn('[MktDataView] fetch-mkt-data?file=deals failed:', dealsRes.status); } if (promosRes.ok) { const buf = await promosRes.arrayBuffer(); if (!cancelled) setPromosMap(parsePromosExcel(buf)); } else { console.warn('[MktDataView] fetch-mkt-data?file=promos failed:', promosRes.status); } if (chargesRes.ok) { const buf = await chargesRes.arrayBuffer(); if (!cancelled) setChargebacksMap(parseChargebacksExcel(buf)); } else { console.warn('[MktDataView] fetch-mkt-data?file=chargebacks failed:', chargesRes.status); } } catch (e) { console.error('[MktDataView] Error fetching marketing data:', e); } finally { if (!cancelled) setMktLoading(false); } }; fetchAll(); return () => { cancelled = true; }; }, []); // Build Product[] from real data sources const allProducts = useMemo((): Product[] => { if (rawData.length === 0) return []; // ASIN → best metadata (longest title wins) const metaMap = new Map(); rawData.forEach(r => { const asin = r.asin.trim().toUpperCase(); const existing = metaMap.get(asin); if (!existing || (r.title && r.title.length > (existing.title?.length || 0))) { metaMap.set(asin, { sku: r.sku || '', title: r.title || '', line: r.line || 'Other' }); } }); // ASIN → global sell-out and units const sellOutMap = new Map(); const unitsMap = new Map(); rawData.forEach(r => { const asin = r.asin.trim().toUpperCase(); sellOutMap.set(asin, (sellOutMap.get(asin) || 0) + r.sellOut); unitsMap.set(asin, (unitsMap.get(asin) || 0) + r.units); }); // ASIN → global ad spend (all filtered data, no hardcoded year) // Include ALL records (even empty ASIN) so total matches ADs Weekly tab const adsSpendMap = new Map(); adsData.forEach(r => { const asin = r.asin.trim().toUpperCase(); adsSpendMap.set(asin, (adsSpendMap.get(asin) || 0) + r.cost); }); // Deals/Promos/Chargebacks are 2025-only data — only apply when 2025 is in scope const has2025 = rawData.some(r => r.year === 2025) || adsData.some(r => r.year === 2025); const getDeals = (asin: string) => has2025 ? (dealsMap.get(asin) || 0) : 0; const getPromos = (asin: string) => has2025 ? (promosMap.get(asin) || 0) : 0; const getChargebacks = (asin: string) => has2025 ? (chargebacksMap.get(asin) || 0) : 0; // Build products from sales ASINs const products = Array.from(metaMap.entries()) .map(([asin, meta]): Product => ({ id: asin, asin, sku: meta.sku, name: meta.title || asin, image: '', category: meta.line, brand: '', grossSales: sellOutMap.get(asin) || 0, unitsSold: unitsMap.get(asin) || 0, ppcSpend: adsSpendMap.get(asin) || 0, deals: getDeals(asin), promos: getPromos(asin), chargebacks: getChargebacks(asin), chargebacksPrevMonth: 0, cogs: 0, })); // Add ASINs (or unassigned spend) that have ad spend but no sales records adsSpendMap.forEach((spend, asin) => { if (!metaMap.has(asin) && spend > 0) { products.push({ id: asin || '__unassigned__', asin: asin || '—', sku: '', name: asin ? asin : 'Unassigned Ad Spend', image: '', category: 'Other', brand: '', grossSales: 0, unitsSold: 0, ppcSpend: spend, deals: asin ? getDeals(asin) : 0, promos: asin ? getPromos(asin) : 0, chargebacks: asin ? getChargebacks(asin) : 0, chargebacksPrevMonth: 0, cogs: 0, }); } }); return products; }, [rawData, adsData, dealsMap, promosMap, chargebacksMap]); const filteredProducts = allProducts; // --------------------------------------------------------------------------- // Loading skeleton — shown while MKT files are being fetched or rawData is empty // --------------------------------------------------------------------------- if (rawData.length === 0 || mktLoading) { return (

Profitability Dashboard

Overview of product performance and margins.

{/* Skeleton KPI cards */}
{[...Array(4)].map((_, i) => (
))}
{/* Skeleton table */}
{[...Array(8)].map((_, i) => (
))}
); } return (
{/* Header Controls */}

Profitability Dashboard

Overview of product performance and margins.

{/* COGS Toggle */}

Product Performance

Click on a product to view the waterfall breakdown.

{/* Modal */} setSelectedProduct(null)} />
); }