Files
CrazeAnalytix/components/mkt/WaterfallModal.tsx
T
Christian Vidal WolfandClaude Sonnet 4.6 845a8e0ae6 fix: resolve black screen caused by React hooks violation in WaterfallModal
Move useMemo before early return in WaterfallModal to comply with Rules of Hooks.
Wrap App with ErrorBoundary so future render crashes show an error panel instead of a silent black screen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 13:04:42 +01:00

166 lines
7.3 KiB
TypeScript

import React, { useMemo } from 'react';
import { Product } from './types';
import { calculateProductMetrics, formatCurrency, formatPercent } from './utils';
import { X } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell, ReferenceLine } from 'recharts';
interface WaterfallModalProps {
product: Product | null;
includeCOGS: boolean;
onClose: () => void;
}
export const WaterfallModal: React.FC<WaterfallModalProps> = ({ product, includeCOGS, onClose }) => {
const data = useMemo(() => {
if (!product) return [];
const metrics = calculateProductMetrics(product, includeCOGS);
let currentTotal = product.grossSales;
const steps = [
{ name: 'Gross Sales', value: product.grossSales, isTotal: true, color: '#6366f1' }, // Indigo
{ name: 'PPC', value: -product.ppcSpend, isTotal: false, color: '#ef4444' }, // Rose
{ name: 'Deals', value: -product.deals, isTotal: false, color: '#f97316' }, // Orange
{ name: 'Promos', value: -product.promos, isTotal: false, color: '#f59e0b' }, // Amber
{ name: 'Chargebacks', value: -product.chargebacks, isTotal: false, color: '#eab308' }, // Yellow
];
if (includeCOGS) {
steps.push({ name: 'COGS', value: -product.cogs, isTotal: false, color: '#64748b' }); // Slate
}
steps.push({ name: 'Net Margin', value: metrics.netMargin, isTotal: true, color: metrics.netMargin >= 0 ? '#10b981' : '#ef4444' });
const chartData = steps.map(step => {
if (step.isTotal) {
return {
name: step.name,
start: 0,
end: step.value,
val: step.value,
color: step.color,
isTotal: true
};
} else {
const start = currentTotal;
currentTotal += step.value; // value is negative
return {
name: step.name,
start: currentTotal, // The bottom of the visible bar
end: start, // The top of the visible bar
val: step.value,
color: step.color,
isTotal: false
};
}
});
// Transform for stacked bar chart: [bottomTransparent, visibleBar]
return chartData.map(d => ({
name: d.name,
transparent: d.start,
visible: Math.abs(d.end - d.start),
val: d.val,
color: d.color,
isTotal: d.isTotal
}));
}, [product, includeCOGS]);
if (!product) return null;
const metrics = calculateProductMetrics(product, includeCOGS);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<div className="bg-[#13161F] rounded-2xl shadow-2xl border border-[#1F2433] w-full max-w-4xl max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-[#13161F] border-b border-[#1F2433] px-6 py-4 flex items-center justify-between z-10">
<div className="flex items-center gap-4">
<div>
<h2 className="text-xl font-bold text-white">{product.name}</h2>
<p className="text-sm text-slate-400 font-mono mt-0.5">{product.sku} | {product.asin}</p>
</div>
</div>
<button onClick={onClose} className="p-2 text-slate-400 hover:text-white hover:bg-[#1F2433] rounded-full transition-colors">
<X className="w-6 h-6" />
</button>
</div>
<div className="p-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div className="bg-[#0A0C10] p-4 rounded-xl border border-[#1F2433]">
<p className="text-xs text-slate-400 font-medium mb-1 uppercase tracking-wider">Gross Sales</p>
<p className="text-lg font-bold text-white">{formatCurrency(product.grossSales)}</p>
</div>
<div className="bg-[#0A0C10] p-4 rounded-xl border border-[#1F2433]">
<p className="text-xs text-slate-400 font-medium mb-1 uppercase tracking-wider">Net Margin</p>
<p className={`text-lg font-bold ${metrics.netMargin >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>{formatCurrency(metrics.netMargin)}</p>
</div>
<div className="bg-[#0A0C10] p-4 rounded-xl border border-[#1F2433]">
<p className="text-xs text-slate-400 font-medium mb-1 uppercase tracking-wider">Margin %</p>
<p className={`text-lg font-bold ${metrics.marginPercent >= 0.2 ? 'text-emerald-400' : metrics.marginPercent >= 0.1 ? 'text-amber-400' : 'text-rose-400'}`}>
{formatPercent(metrics.marginPercent)}
</p>
</div>
<div className="bg-[#0A0C10] p-4 rounded-xl border border-[#1F2433]">
<p className="text-xs text-slate-400 font-medium mb-1 uppercase tracking-wider">ACOS</p>
<p className="text-lg font-bold text-amber-400">{formatPercent(metrics.acos)}</p>
</div>
</div>
<div className="mb-6">
<h3 className="text-lg font-semibold text-white">Profitability Analysis (Waterfall)</h3>
<p className="text-sm text-slate-400">Breakdown of deductions from gross sales to net margin.</p>
</div>
<div className="h-[400px] w-full bg-[#0A0C10] p-4 rounded-xl border border-[#1F2433]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data} margin={{ top: 20, right: 20, bottom: 40, left: 20 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#1F2433" />
<XAxis
dataKey="name"
tick={{ fontSize: 12, fill: '#8B949E' }}
axisLine={{ stroke: '#1F2433' }}
tickLine={false}
angle={-45}
textAnchor="end"
height={60}
/>
<YAxis
tickFormatter={(val) => `$${val / 1000}k`}
tick={{ fontSize: 12, fill: '#8B949E' }}
axisLine={false}
tickLine={false}
/>
<Tooltip
cursor={{ fill: '#13161F' }}
content={({ active, payload }) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="bg-[#13161F] p-3 border border-[#1F2433] shadow-xl rounded-lg text-sm">
<p className="font-medium text-white mb-1">{data.name}</p>
<p className={`font-bold ${data.val < 0 ? 'text-rose-400' : 'text-emerald-400'}`}>
{data.val > 0 && !data.isTotal ? '+' : ''}{formatCurrency(data.val)}
</p>
</div>
);
}
return null;
}}
/>
<ReferenceLine y={0} stroke="#475569" />
<Bar dataKey="transparent" stackId="a" fill="transparent" />
<Bar dataKey="visible" stackId="a" radius={[4, 4, 4, 4]}>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
</div>
);
};