import React, { useState, useMemo, useCallback } from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceDot } from 'recharts'; import { WeeklyPivotRow } from '../services/dataProcessor'; // --- Types --- export interface Experiment { id: string; week: string; // "2026-08" matching YYYY-WW format used in pivot data product_line: string; action_type: 'SEO' | 'Price' | 'Advertising' | 'Images' | 'A+ Content' | 'Variants' | 'Stock' | 'Other'; description: string; expected_impact: 'More Sales' | 'Better Visibility' | 'Lower Costs'; status: 'in_progress' | 'success' | 'failed'; created_at: string; } type ActionType = Experiment['action_type']; type ExpectedImpact = Experiment['expected_impact']; type ExperimentStatus = Experiment['status']; const ACTION_TYPES: ActionType[] = ['SEO', 'Price', 'Advertising', 'Images', 'A+ Content', 'Variants', 'Stock', 'Other']; const EXPECTED_IMPACTS: ExpectedImpact[] = ['More Sales', 'Better Visibility', 'Lower Costs']; const STATUSES: ExperimentStatus[] = ['in_progress', 'success', 'failed']; const ACTION_COLORS: Record = { 'SEO': '#3b82f6', 'Price': '#f59e0b', 'Advertising': '#f43f5e', 'Images': '#a855f7', 'A+ Content': '#10b981', 'Variants': '#14b8a6', 'Stock': '#f97316', 'Other': '#64748b', }; const ACTION_BG_CLASSES: Record = { 'SEO': 'bg-blue-500/20 text-blue-400 border-blue-500/30', 'Price': 'bg-amber-500/20 text-amber-400 border-amber-500/30', 'Advertising': 'bg-rose-500/20 text-rose-400 border-rose-500/30', 'Images': 'bg-purple-500/20 text-purple-400 border-purple-500/30', 'A+ Content': 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', 'Variants': 'bg-teal-500/20 text-teal-400 border-teal-500/30', 'Stock': 'bg-orange-500/20 text-orange-400 border-orange-500/30', 'Other': 'bg-slate-500/20 text-slate-400 border-slate-500/30', }; const STATUS_CLASSES: Record = { 'in_progress': 'bg-blue-500/20 text-blue-400 border-blue-500/30', 'success': 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', 'failed': 'bg-red-500/20 text-red-400 border-red-500/30', }; const STATUS_LABELS: Record = { 'in_progress': 'In Progress', 'success': 'Success', 'failed': 'Failed', }; const STORAGE_KEY = 'craze_experiments'; // --- Storage helpers --- const loadExperiments = (): Experiment[] => { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; } }; const saveExperiments = (experiments: Experiment[]) => { localStorage.setItem(STORAGE_KEY, JSON.stringify(experiments)); }; // --- Current ISO week helper --- const getCurrentWeek = (): string => { const now = new Date(); const jan1 = new Date(now.getFullYear(), 0, 1); const dayOfYear = Math.floor((now.getTime() - jan1.getTime()) / 86400000) + 1; const weekNum = Math.ceil((dayOfYear + jan1.getDay()) / 7); return `${now.getFullYear()}-${String(weekNum).padStart(2, '0')}`; }; // --- Props --- interface ExperimentTrackerProps { rows: WeeklyPivotRow[]; weeks: string[]; // sorted descending (most recent first) primaryMetric: 'units' | 'revenue'; } // --- Custom dot for experiment markers --- const ExperimentDot: React.FC = (props) => { const { cx, cy, experiments } = props; if (!experiments || experiments.length === 0 || !cx || !cy) return null; const color = ACTION_COLORS[experiments[0].action_type as ActionType] || '#64748b'; return ( {experiments.length > 1 && ( +{experiments.length - 1} )} ); }; // --- Custom chart tooltip --- const ChartTooltipContent: React.FC = ({ active, payload, label, experiments }) => { if (!active || !payload || !payload.length) return null; const weekExperiments = experiments?.filter((e: Experiment) => e.week === label) || []; return (
Week {label?.split('-')[1]}/{label?.split('-')[0]?.slice(-2)}
{payload.map((entry: any, i: number) => (
{entry.name} {entry.name === 'Revenue' ? `€${entry.value?.toLocaleString('de-DE')}` : entry.value?.toLocaleString('de-DE')}
))} {weekExperiments.length > 0 && (
Experiments
{weekExperiments.map((exp: Experiment) => (
{exp.action_type} {exp.product_line}

{exp.description}

))}
)}
); }; // --- Main Component --- const ExperimentTracker: React.FC = ({ rows, weeks, primaryMetric }) => { const [experiments, setExperiments] = useState(loadExperiments); const [showForm, setShowForm] = useState(false); const [showPanel, setShowPanel] = useState(true); const [editingStatus, setEditingStatus] = useState(null); // Form state const [formWeek, setFormWeek] = useState(getCurrentWeek); const [formLine, setFormLine] = useState(''); const [formAction, setFormAction] = useState('SEO'); const [formDesc, setFormDesc] = useState(''); const [formImpact, setFormImpact] = useState('More Sales'); // Available product lines from data const productLines = useMemo(() => { const lines = new Set(rows.map(r => r.line).filter(Boolean)); return Array.from(lines).sort(); }, [rows]); // Chart data: aggregate totals by week (ascending order for chart) const chartData = useMemo(() => { const reversedWeeks = [...weeks].reverse(); // ascending for chart return reversedWeeks.map(week => { let units = 0; let revenue = 0; let spend = 0; for (const row of rows) { units += row.unitsByWeek[week] || 0; revenue += row.revenueByWeek[week] || 0; spend += row.spendByWeek[week] || 0; } return { week, units, revenue, spend }; }); }, [rows, weeks]); // Weeks with experiments (for ReferenceDots) const weekExperimentMap = useMemo(() => { const map = new Map(); for (const exp of experiments) { const existing = map.get(exp.week) || []; existing.push(exp); map.set(exp.week, existing); } return map; }, [experiments]); // Compute delta % for an experiment const computeDelta = useCallback((exp: Experiment): { baseline: number | null; post: number | null; delta: number | null } => { const ascWeeks = [...weeks].reverse(); const weekIdx = ascWeeks.indexOf(exp.week); if (weekIdx === -1) return { baseline: null, post: null, delta: null }; const getWeekTotal = (w: string) => { let total = 0; for (const row of rows) { if (row.line !== exp.product_line) continue; total += primaryMetric === 'units' ? (row.unitsByWeek[w] || 0) : (row.revenueByWeek[w] || 0); } return total; }; // 3 weeks before const beforeWeeks = ascWeeks.slice(Math.max(0, weekIdx - 3), weekIdx); // 3 weeks after const afterWeeks = ascWeeks.slice(weekIdx + 1, weekIdx + 4); if (beforeWeeks.length === 0) return { baseline: null, post: null, delta: null }; const baselineSum = beforeWeeks.reduce((sum, w) => sum + getWeekTotal(w), 0); const baselineAvg = baselineSum / beforeWeeks.length; if (afterWeeks.length === 0) return { baseline: baselineAvg, post: null, delta: null }; const postSum = afterWeeks.reduce((sum, w) => sum + getWeekTotal(w), 0); const postAvg = postSum / afterWeeks.length; const delta = baselineAvg > 0 ? ((postAvg - baselineAvg) / baselineAvg) * 100 : null; return { baseline: baselineAvg, post: postAvg, delta }; }, [rows, weeks, primaryMetric]); // Persist experiments const updateExperiments = useCallback((updated: Experiment[]) => { setExperiments(updated); saveExperiments(updated); }, []); const handleSubmit = useCallback(() => { if (!formLine || !formDesc.trim()) return; const newExp: Experiment = { id: `exp_${Date.now()}`, week: formWeek, product_line: formLine, action_type: formAction, description: formDesc.trim(), expected_impact: formImpact, status: 'in_progress', created_at: new Date().toISOString(), }; updateExperiments([...experiments, newExp]); setShowForm(false); setFormDesc(''); setFormWeek(getCurrentWeek()); }, [formWeek, formLine, formAction, formDesc, formImpact, experiments, updateExperiments]); const handleStatusChange = useCallback((id: string, newStatus: ExperimentStatus) => { updateExperiments(experiments.map(e => e.id === id ? { ...e, status: newStatus } : e)); setEditingStatus(null); }, [experiments, updateExperiments]); const handleDelete = useCallback((id: string) => { updateExperiments(experiments.filter(e => e.id !== id)); }, [experiments, updateExperiments]); // Available weeks for the form dropdown const availableWeeks = useMemo(() => [...weeks].slice(0, 20), [weeks]); return (
{/* Header + Log Action Button */}

Experiment Tracker

{experiments.length} experiments logged

{/* Chart */} {chartData.length > 0 && (
Total {primaryMetric === 'units' ? 'Units' : 'Revenue'} by Week {experiments.length > 0 && ( — {experiments.length} experiment{experiments.length !== 1 ? 's' : ''} marked )}
`${w.split('-')[1]}/${w.split('-')[0].slice(-2)}`} /> primaryMetric === 'revenue' ? `€${(v / 1000).toFixed(0)}k` : v.toLocaleString('de-DE')} /> } cursor={{ stroke: '#6366f1', strokeWidth: 1, strokeDasharray: '4 4' }} /> {/* Experiment markers */} {chartData.map((point) => { const exps = weekExperimentMap.get(point.week); if (!exps || exps.length === 0) return null; return ( } /> ); })}
)} {/* Log Action Modal */} {showForm && (
setShowForm(false)}>
e.stopPropagation()}>

Log Experiment

{/* Week */}
{/* Product Line */}
{/* Action Type */}
{ACTION_TYPES.map(type => ( ))}
{/* Description */}