feat: add Experiment Tracker to Weekly Sales tab

Log weekly actions (SEO, pricing, ads, etc.) per product line and visualize
their impact with a Recharts line chart showing experiment markers. Includes
a collapsible experiments panel with auto-computed delta % (3-week before vs
after comparison) and editable status tracking. Data persisted in localStorage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Vidal Wolf
2026-02-20 10:58:51 +01:00
co-authored by Claude Opus 4.6
parent 557703c5ea
commit 9a115ff5be
2 changed files with 604 additions and 0 deletions
+600
View File
@@ -0,0 +1,600 @@
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<ActionType, string> = {
'SEO': '#3b82f6',
'Price': '#f59e0b',
'Advertising': '#f43f5e',
'Images': '#a855f7',
'A+ Content': '#10b981',
'Variants': '#14b8a6',
'Stock': '#f97316',
'Other': '#64748b',
};
const ACTION_BG_CLASSES: Record<ActionType, string> = {
'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<ExperimentStatus, string> = {
'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<ExperimentStatus, string> = {
'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<any> = (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 (
<g>
<circle cx={cx} cy={cy} r={10} fill={color} fillOpacity={0.25} stroke={color} strokeWidth={1.5} />
<circle cx={cx} cy={cy} r={4} fill={color} />
{experiments.length > 1 && (
<text x={cx + 12} y={cy - 4} fill={color} fontSize={9} fontWeight="bold">
+{experiments.length - 1}
</text>
)}
</g>
);
};
// --- Custom chart tooltip ---
const ChartTooltipContent: React.FC<any> = ({ active, payload, label, experiments }) => {
if (!active || !payload || !payload.length) return null;
const weekExperiments = experiments?.filter((e: Experiment) => e.week === label) || [];
return (
<div className="bg-slate-950 border border-white/20 rounded-xl shadow-2xl p-3 max-w-xs">
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 pb-2 border-b border-white/10">
Week {label?.split('-')[1]}/{label?.split('-')[0]?.slice(-2)}
</div>
{payload.map((entry: any, i: number) => (
<div key={i} className="flex justify-between items-center gap-4 mb-1">
<span className="text-[10px] text-slate-500 font-bold">{entry.name}</span>
<span className="text-xs font-black" style={{ color: entry.color }}>
{entry.name === 'Revenue' ? `${entry.value?.toLocaleString('de-DE')}` : entry.value?.toLocaleString('de-DE')}
</span>
</div>
))}
{weekExperiments.length > 0 && (
<div className="mt-2 pt-2 border-t border-white/10 space-y-1.5">
<div className="text-[9px] font-black text-fuchsia-400 uppercase tracking-widest">Experiments</div>
{weekExperiments.map((exp: Experiment) => (
<div key={exp.id} className="flex items-start gap-2">
<span
className="w-2 h-2 rounded-full mt-1 flex-shrink-0"
style={{ backgroundColor: ACTION_COLORS[exp.action_type] }}
/>
<div>
<span className={`text-[9px] font-bold px-1.5 py-0.5 rounded border ${ACTION_BG_CLASSES[exp.action_type]}`}>
{exp.action_type}
</span>
<span className="text-[10px] text-white/70 ml-1.5">{exp.product_line}</span>
<p className="text-[10px] text-slate-400 mt-0.5 leading-tight">{exp.description}</p>
</div>
</div>
))}
</div>
)}
</div>
);
};
// --- Main Component ---
const ExperimentTracker: React.FC<ExperimentTrackerProps> = ({ rows, weeks, primaryMetric }) => {
const [experiments, setExperiments] = useState<Experiment[]>(loadExperiments);
const [showForm, setShowForm] = useState(false);
const [showPanel, setShowPanel] = useState(true);
const [editingStatus, setEditingStatus] = useState<string | null>(null);
// Form state
const [formWeek, setFormWeek] = useState(getCurrentWeek);
const [formLine, setFormLine] = useState('');
const [formAction, setFormAction] = useState<ActionType>('SEO');
const [formDesc, setFormDesc] = useState('');
const [formImpact, setFormImpact] = useState<ExpectedImpact>('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<string, Experiment[]>();
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 (
<div className="flex flex-col gap-3">
{/* Header + Log Action Button */}
<div className="flex items-center justify-between bg-slate-900 border border-white/10 p-3 px-4 rounded-xl">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-fuchsia-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-fuchsia-500/20">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
</div>
<div>
<h3 className="text-sm font-black text-white uppercase tracking-wider">Experiment Tracker</h3>
<p className="text-[10px] text-slate-500 font-bold">{experiments.length} experiments logged</p>
</div>
</div>
<button
onClick={() => setShowForm(true)}
className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 rounded-lg text-xs font-bold transition-colors border border-indigo-500/50 shadow-lg shadow-indigo-500/20"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Log Action
</button>
</div>
{/* Chart */}
{chartData.length > 0 && (
<div className="bg-slate-900 border border-white/10 rounded-xl p-4 shadow-lg">
<div className="text-[10px] font-black text-slate-500 uppercase tracking-widest mb-3">
Total {primaryMetric === 'units' ? 'Units' : 'Revenue'} by Week
{experiments.length > 0 && (
<span className="ml-2 text-fuchsia-400">
{experiments.length} experiment{experiments.length !== 1 ? 's' : ''} marked
</span>
)}
</div>
<ResponsiveContainer width="100%" height={260}>
<LineChart data={chartData} margin={{ top: 10, right: 20, left: 10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis
dataKey="week"
stroke="#64748b"
tick={{ fontSize: 9, fontWeight: 'bold' }}
tickFormatter={(w: string) => `${w.split('-')[1]}/${w.split('-')[0].slice(-2)}`}
/>
<YAxis
stroke="#64748b"
tick={{ fontSize: 9 }}
tickFormatter={(v: number) => primaryMetric === 'revenue' ? `${(v / 1000).toFixed(0)}k` : v.toLocaleString('de-DE')}
/>
<Tooltip
content={<ChartTooltipContent experiments={experiments} />}
cursor={{ stroke: '#6366f1', strokeWidth: 1, strokeDasharray: '4 4' }}
/>
<Line
type="monotone"
dataKey={primaryMetric}
stroke="#6366f1"
strokeWidth={2.5}
dot={{ fill: '#6366f1', r: 3, strokeWidth: 0 }}
activeDot={{ r: 5, fill: '#818cf8', strokeWidth: 2, stroke: '#6366f1' }}
name={primaryMetric === 'units' ? 'Units' : 'Revenue'}
/>
<Line
type="monotone"
dataKey="spend"
stroke="#f43f5e"
strokeWidth={1.5}
strokeDasharray="4 3"
dot={false}
name="Ads Spend"
/>
{/* Experiment markers */}
{chartData.map((point) => {
const exps = weekExperimentMap.get(point.week);
if (!exps || exps.length === 0) return null;
return (
<ReferenceDot
key={point.week}
x={point.week}
y={point[primaryMetric]}
shape={<ExperimentDot experiments={exps} />}
/>
);
})}
</LineChart>
</ResponsiveContainer>
</div>
)}
{/* Log Action Modal */}
{showForm && (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={() => setShowForm(false)}>
<div className="bg-slate-900 border border-white/15 rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between mb-5">
<h3 className="text-base font-black text-white uppercase tracking-wider">Log Experiment</h3>
<button onClick={() => setShowForm(false)} className="text-slate-500 hover:text-white transition-colors">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-4">
{/* Week */}
<div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1.5">Week</label>
<select
value={formWeek}
onChange={e => setFormWeek(e.target.value)}
className="w-full bg-slate-950 border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
{availableWeeks.map(w => (
<option key={w} value={w}>Week {w.split('-')[1]}/{w.split('-')[0]} {w === getCurrentWeek() ? '(Current)' : ''}</option>
))}
</select>
</div>
{/* Product Line */}
<div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1.5">Product Line</label>
<select
value={formLine}
onChange={e => setFormLine(e.target.value)}
className="w-full bg-slate-950 border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<option value="">Select product line...</option>
{productLines.map(line => (
<option key={line} value={line}>{line}</option>
))}
</select>
</div>
{/* Action Type */}
<div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1.5">Action Type</label>
<div className="flex flex-wrap gap-1.5">
{ACTION_TYPES.map(type => (
<button
key={type}
onClick={() => setFormAction(type)}
className={`px-3 py-1.5 rounded-lg text-xs font-bold border transition-all ${formAction === type
? ACTION_BG_CLASSES[type] + ' ring-1 ring-white/20'
: 'bg-slate-800 text-slate-500 border-white/5 hover:border-white/15'
}`}
>
{type}
</button>
))}
</div>
</div>
{/* Description */}
<div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1.5">Description</label>
<textarea
value={formDesc}
onChange={e => setFormDesc(e.target.value)}
placeholder="e.g., Added keyword 'caja musical' to title..."
rows={3}
className="w-full bg-slate-950 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 resize-none"
/>
</div>
{/* Expected Impact */}
<div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1.5">Expected Impact</label>
<div className="flex gap-2">
{EXPECTED_IMPACTS.map(impact => (
<button
key={impact}
onClick={() => setFormImpact(impact)}
className={`flex-1 px-3 py-2 rounded-lg text-xs font-bold border transition-all ${formImpact === impact
? 'bg-indigo-600/20 text-indigo-400 border-indigo-500/30 ring-1 ring-indigo-500/20'
: 'bg-slate-800 text-slate-500 border-white/5 hover:border-white/15'
}`}
>
{impact}
</button>
))}
</div>
</div>
</div>
{/* Submit */}
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-white/10">
<button
onClick={() => setShowForm(false)}
className="px-4 py-2 rounded-lg text-xs font-bold text-slate-400 bg-slate-800 hover:bg-slate-700 border border-white/5 transition-colors"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={!formLine || !formDesc.trim()}
className="px-5 py-2 rounded-lg text-xs font-bold text-white bg-indigo-600 hover:bg-indigo-500 border border-indigo-500/50 shadow-lg shadow-indigo-500/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
>
Save Experiment
</button>
</div>
</div>
</div>
)}
{/* Experiments Panel */}
{experiments.length > 0 && (
<div className="bg-slate-900 border border-white/10 rounded-xl shadow-lg overflow-hidden">
<button
onClick={() => setShowPanel(!showPanel)}
className="w-full flex items-center justify-between p-3 px-4 hover:bg-white/[0.02] transition-colors"
>
<div className="flex items-center gap-2">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
Experiments Log ({experiments.length})
</span>
</div>
<svg className={`w-4 h-4 text-slate-500 transition-transform ${showPanel ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showPanel && (
<div className="overflow-x-auto border-t border-white/5">
<table className="w-full text-left border-collapse min-w-[800px]">
<thead>
<tr className="border-b border-white/10">
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Week</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Product Line</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Action</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Description</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Expected</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Status</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest text-right">Delta %</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest w-10"></th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{[...experiments].sort((a, b) => b.week.localeCompare(a.week)).map(exp => {
const { delta } = computeDelta(exp);
return (
<tr key={exp.id} className="hover:bg-white/[0.02] transition-colors">
<td className="p-2.5 px-4 text-xs font-bold text-white whitespace-nowrap">
W{exp.week.split('-')[1]}/{exp.week.split('-')[0].slice(-2)}
</td>
<td className="p-2.5 px-4">
<span className="text-[10px] text-fuchsia-400 font-bold uppercase tracking-widest">{exp.product_line}</span>
</td>
<td className="p-2.5 px-4">
<span className={`text-[10px] font-bold px-2 py-1 rounded border ${ACTION_BG_CLASSES[exp.action_type]}`}>
{exp.action_type}
</span>
</td>
<td className="p-2.5 px-4 text-xs text-white/70 max-w-[250px] truncate" title={exp.description}>
{exp.description}
</td>
<td className="p-2.5 px-4 text-[10px] text-slate-400 font-bold whitespace-nowrap">
{exp.expected_impact}
</td>
<td className="p-2.5 px-4">
{editingStatus === exp.id ? (
<select
value={exp.status}
onChange={e => handleStatusChange(exp.id, e.target.value as ExperimentStatus)}
onBlur={() => setEditingStatus(null)}
autoFocus
className="bg-slate-950 border border-white/10 rounded px-2 py-1 text-[10px] text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
{STATUSES.map(s => (
<option key={s} value={s}>{STATUS_LABELS[s]}</option>
))}
</select>
) : (
<button
onClick={() => setEditingStatus(exp.id)}
className={`text-[10px] font-bold px-2 py-1 rounded border cursor-pointer hover:ring-1 hover:ring-white/20 transition-all ${STATUS_CLASSES[exp.status]}`}
>
{STATUS_LABELS[exp.status]}
</button>
)}
</td>
<td className="p-2.5 px-4 text-right">
{delta !== null ? (
<span className={`text-xs font-black ${delta >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{delta >= 0 ? '+' : ''}{delta.toFixed(1)}%
</span>
) : (
<span className="text-[10px] text-slate-600 italic">Pending</span>
)}
</td>
<td className="p-2.5 px-4">
<button
onClick={() => handleDelete(exp.id)}
className="text-slate-600 hover:text-red-400 transition-colors"
title="Delete experiment"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
)}
</div>
);
};
export default ExperimentTracker;
+4
View File
@@ -10,6 +10,7 @@ import { VendorStockBadge } from './VendorStockBadge';
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge'; import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
import { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons'; import { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons';
import { ExcelFilter } from './ExcelFilter'; import { ExcelFilter } from './ExcelFilter';
import ExperimentTracker from './ExperimentTracker';
interface WeeklyGridProps { interface WeeklyGridProps {
data: CombinedKPIs[]; data: CombinedKPIs[];
@@ -664,6 +665,9 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
return ( return (
<div className="flex flex-col gap-3 md:gap-4 animate-fade-in px-1 md:px-0"> <div className="flex flex-col gap-3 md:gap-4 animate-fade-in px-1 md:px-0">
{/* Experiment Tracker */}
<ExperimentTracker rows={rows} weeks={weeks} primaryMetric={primaryMetric} />
{/* Toolbar: Search & Pagination */} {/* Toolbar: Search & Pagination */}
<div className="flex flex-col lg:flex-row justify-between items-center gap-3 md:gap-4 bg-slate-900 border border-white/10 p-2 md:p-4 rounded-xl shadow-lg"> <div className="flex flex-col lg:flex-row justify-between items-center gap-3 md:gap-4 bg-slate-900 border border-white/10 p-2 md:p-4 rounded-xl shadow-lg">
<div className="flex flex-wrap items-center gap-4 w-full lg:w-auto"> <div className="flex flex-wrap items-center gap-4 w-full lg:w-auto">