import React, { useState, useMemo } from 'react'; import { Product } from './types'; import { formatCurrency, formatPercent, calculateProductMetrics, cn } from './utils'; import { ArrowUpDown, AlertCircle, ChevronUp, ChevronDown } from 'lucide-react'; interface MasterTableProps { products: Product[]; includeCOGS: boolean; onProductClick: (product: Product) => void; } type SortKey = 'name' | 'grossSales' | 'ppcSpend' | 'deals' | 'promos' | 'chargebacks' | 'netMargin' | 'marginPercent'; type SortOrder = 'asc' | 'desc'; export const MasterTable: React.FC = ({ products, includeCOGS, onProductClick }) => { const [sortKey, setSortKey] = useState('marginPercent'); const [sortOrder, setSortOrder] = useState('asc'); const handleSort = (key: SortKey) => { if (sortKey === key) { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { setSortKey(key); setSortOrder('desc'); // Default to desc for new sort } }; const totals = useMemo(() => { return products.reduce((acc, product) => { const metrics = calculateProductMetrics(product, includeCOGS); acc.grossSales += product.grossSales; acc.ppcSpend += product.ppcSpend; acc.deals += product.deals; acc.promos += product.promos; acc.chargebacks += product.chargebacks; acc.netMargin += metrics.netMargin; return acc; }, { grossSales: 0, ppcSpend: 0, deals: 0, promos: 0, chargebacks: 0, netMargin: 0 }); }, [products, includeCOGS]); const totalMarginPercent = totals.grossSales > 0 ? totals.netMargin / totals.grossSales : 0; const totalAcos = totals.grossSales > 0 ? totals.ppcSpend / totals.grossSales : 0; const sortedProducts = useMemo(() => { return [...products].sort((a, b) => { const metricsA = calculateProductMetrics(a, includeCOGS); const metricsB = calculateProductMetrics(b, includeCOGS); let valA: number | string; let valB: number | string; switch (sortKey) { case 'name': valA = a.name; valB = b.name; break; case 'grossSales': valA = a.grossSales; valB = b.grossSales; break; case 'ppcSpend': valA = a.ppcSpend; valB = b.ppcSpend; break; case 'deals': valA = a.deals; valB = b.deals; break; case 'promos': valA = a.promos; valB = b.promos; break; case 'chargebacks': valA = a.chargebacks; valB = b.chargebacks; break; case 'netMargin': valA = metricsA.netMargin; valB = metricsB.netMargin; break; case 'marginPercent': valA = metricsA.marginPercent; valB = metricsB.marginPercent; break; default: valA = 0; valB = 0; } if (valA < valB) return sortOrder === 'asc' ? -1 : 1; if (valA > valB) return sortOrder === 'asc' ? 1 : -1; return 0; }); }, [products, includeCOGS, sortKey, sortOrder]); const SortIcon = ({ columnKey }: { columnKey: SortKey }) => { if (sortKey !== columnKey) return ; return sortOrder === 'asc' ? : ; }; return (
{/* Totals Row */} {/* Product Rows */} {sortedProducts.map((product) => { const metrics = calculateProductMetrics(product, includeCOGS); // Data bar width calculation (relative to max sales) const maxSales = Math.max(...products.map(p => p.grossSales)); const salesBarWidth = `${(product.grossSales / maxSales) * 100}%`; return ( onProductClick(product)} > ); })}
handleSort('name')}> Product handleSort('grossSales')}> SELL OUT handleSort('ppcSpend')}> PPC Spend (ACOS) handleSort('deals')}> Deals handleSort('promos')}> Promos handleSort('chargebacks')}> Op. Chargebacks handleSort('netMargin')}> Est. Margin ($) handleSort('marginPercent')}> Margin (%)
TOTALS {formatCurrency(totals.grossSales)} {formatCurrency(totals.ppcSpend)}
ACOS: {formatPercent(totalAcos)}
{formatCurrency(totals.deals)} {formatCurrency(totals.promos)} {formatCurrency(totals.chargebacks)} {formatCurrency(totals.netMargin)} = 0.20 ? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20" : totalMarginPercent >= 0.10 ? "bg-amber-500/10 text-amber-400 border border-amber-500/20" : "bg-rose-500/10 text-rose-400 border border-rose-500/20" )}> {formatPercent(totalMarginPercent)}
{product.name}
{product.sku} | {product.asin}
{formatCurrency(product.grossSales)}
{formatCurrency(product.ppcSpend)}
ACOS: {formatPercent(metrics.acos)}
{formatCurrency(product.deals)}
{formatCurrency(product.promos)}
{metrics.hasChargebackAnomaly && (
)} {formatCurrency(product.chargebacks)}
{formatCurrency(metrics.netMargin)}
= 0.20 ? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20" : metrics.marginPercent >= 0.10 ? "bg-amber-500/10 text-amber-400 border border-amber-500/20" : "bg-rose-500/10 text-rose-400 border border-rose-500/20" )}> {metrics.marginPercent > 0 ? '▲' : '▼'} {formatPercent(Math.abs(metrics.marginPercent))}
); };