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 { Filter, 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 [selectedCategory, setSelectedCategory] = useState('All'); 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 (all marketplaces summed) 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 marketplaces summed) const adsSpendMap = new Map(); adsData.forEach(r => { const asin = r.asin.trim().toUpperCase(); adsSpendMap.set(asin, (adsSpendMap.get(asin) || 0) + r.cost); }); return 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: dealsMap.get(asin) || 0, promos: promosMap.get(asin) || 0, chargebacks: chargebacksMap.get(asin) || 0, chargebacksPrevMonth: 0, cogs: 0, })) // Only show products that have actual sales .filter(p => p.grossSales > 0); }, [rawData, adsData, dealsMap, promosMap, chargebacksMap]); const categories = useMemo( () => ['All', ...Array.from(new Set(allProducts.map(p => p.category))).sort()], [allProducts] ); const filteredProducts = useMemo(() => { if (selectedCategory === 'All') return allProducts; return allProducts.filter(p => p.category === selectedCategory); }, [allProducts, selectedCategory]); // Reset category filter when product list changes (e.g. rawData load) useEffect(() => { setSelectedCategory('All'); }, [rawData]); // --------------------------------------------------------------------------- // 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.

{/* Category Filter */}
{/* COGS Toggle */}

Product Performance

Click on a product to view the waterfall breakdown.

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