feat: add marketplace differentiation to Experiment Tracker

Each experiment now tracks which marketplace (Amazon DE, UK, etc.) it
applies to. The form defaults to the currently active marketplace filter,
and the delta % calculation filters by both product line and marketplace.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Vidal Wolf
2026-02-20 11:07:34 +01:00
co-authored by Claude Opus 4.6
parent 9a115ff5be
commit 812425a1ef
2 changed files with 45 additions and 7 deletions
+44 -6
View File
@@ -11,6 +11,7 @@ export interface Experiment {
id: string;
week: string; // "2026-08" matching YYYY-WW format used in pivot data
product_line: string;
marketplace: string; // e.g. "Amazon DE", "Amazon UK"
action_type: 'SEO' | 'Price' | 'Advertising' | 'Images' | 'A+ Content' | 'Variants' | 'Stock' | 'Other';
description: string;
expected_impact: 'More Sales' | 'Better Visibility' | 'Lower Costs';
@@ -93,6 +94,7 @@ interface ExperimentTrackerProps {
rows: WeeklyPivotRow[];
weeks: string[]; // sorted descending (most recent first)
primaryMetric: 'units' | 'revenue';
customerFilters: string[]; // active marketplace filters from parent
}
// --- Custom dot for experiment markers ---
@@ -150,6 +152,7 @@ const ChartTooltipContent: React.FC<any> = ({ active, payload, label, experiment
{exp.action_type}
</span>
<span className="text-[10px] text-white/70 ml-1.5">{exp.product_line}</span>
{exp.marketplace && <span className="text-[9px] text-slate-500 ml-1">({exp.marketplace})</span>}
<p className="text-[10px] text-slate-400 mt-0.5 leading-tight">{exp.description}</p>
</div>
</div>
@@ -162,7 +165,7 @@ const ChartTooltipContent: React.FC<any> = ({ active, payload, label, experiment
// --- Main Component ---
const ExperimentTracker: React.FC<ExperimentTrackerProps> = ({ rows, weeks, primaryMetric }) => {
const ExperimentTracker: React.FC<ExperimentTrackerProps> = ({ rows, weeks, primaryMetric, customerFilters }) => {
const [experiments, setExperiments] = useState<Experiment[]>(loadExperiments);
const [showForm, setShowForm] = useState(false);
const [showPanel, setShowPanel] = useState(true);
@@ -171,10 +174,24 @@ const ExperimentTracker: React.FC<ExperimentTrackerProps> = ({ rows, weeks, prim
// Form state
const [formWeek, setFormWeek] = useState(getCurrentWeek);
const [formLine, setFormLine] = useState('');
const [formMarketplace, setFormMarketplace] = useState('');
const [formAction, setFormAction] = useState<ActionType>('SEO');
const [formDesc, setFormDesc] = useState('');
const [formImpact, setFormImpact] = useState<ExpectedImpact>('More Sales');
// Available marketplaces from data
const marketplaces = useMemo(() => {
const mks = new Set(rows.map(r => r.customer).filter(Boolean));
return Array.from(mks).sort();
}, [rows]);
// Default marketplace to active filter when opening form
const defaultMarketplace = useMemo(() => {
if (customerFilters.length === 1) return customerFilters[0];
if (customerFilters.length > 0) return customerFilters[0];
return marketplaces[0] || '';
}, [customerFilters, marketplaces]);
// Available product lines from data
const productLines = useMemo(() => {
const lines = new Set(rows.map(r => r.line).filter(Boolean));
@@ -218,6 +235,7 @@ const ExperimentTracker: React.FC<ExperimentTrackerProps> = ({ rows, weeks, prim
let total = 0;
for (const row of rows) {
if (row.line !== exp.product_line) continue;
if (exp.marketplace && row.customer !== exp.marketplace) continue;
total += primaryMetric === 'units'
? (row.unitsByWeek[w] || 0)
: (row.revenueByWeek[w] || 0);
@@ -252,12 +270,13 @@ const ExperimentTracker: React.FC<ExperimentTrackerProps> = ({ rows, weeks, prim
}, []);
const handleSubmit = useCallback(() => {
if (!formLine || !formDesc.trim()) return;
if (!formLine || !formMarketplace || !formDesc.trim()) return;
const newExp: Experiment = {
id: `exp_${Date.now()}`,
week: formWeek,
product_line: formLine,
marketplace: formMarketplace,
action_type: formAction,
description: formDesc.trim(),
expected_impact: formImpact,
@@ -269,7 +288,7 @@ const ExperimentTracker: React.FC<ExperimentTrackerProps> = ({ rows, weeks, prim
setShowForm(false);
setFormDesc('');
setFormWeek(getCurrentWeek());
}, [formWeek, formLine, formAction, formDesc, formImpact, experiments, updateExperiments]);
}, [formWeek, formLine, formMarketplace, formAction, formDesc, formImpact, experiments, updateExperiments]);
const handleStatusChange = useCallback((id: string, newStatus: ExperimentStatus) => {
updateExperiments(experiments.map(e => e.id === id ? { ...e, status: newStatus } : e));
@@ -299,7 +318,7 @@ const ExperimentTracker: React.FC<ExperimentTrackerProps> = ({ rows, weeks, prim
</div>
</div>
<button
onClick={() => setShowForm(true)}
onClick={() => { setFormMarketplace(defaultMarketplace); 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">
@@ -417,6 +436,21 @@ const ExperimentTracker: React.FC<ExperimentTrackerProps> = ({ rows, weeks, prim
</select>
</div>
{/* Marketplace */}
<div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1.5">Marketplace</label>
<select
value={formMarketplace}
onChange={e => setFormMarketplace(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 marketplace...</option>
{marketplaces.map(mk => (
<option key={mk} value={mk}>{mk}</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>
@@ -478,7 +512,7 @@ const ExperimentTracker: React.FC<ExperimentTrackerProps> = ({ rows, weeks, prim
</button>
<button
onClick={handleSubmit}
disabled={!formLine || !formDesc.trim()}
disabled={!formLine || !formMarketplace || !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
@@ -507,10 +541,11 @@ const ExperimentTracker: React.FC<ExperimentTrackerProps> = ({ rows, weeks, prim
{showPanel && (
<div className="overflow-x-auto border-t border-white/5">
<table className="w-full text-left border-collapse min-w-[800px]">
<table className="w-full text-left border-collapse min-w-[900px]">
<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">Marketplace</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>
@@ -528,6 +563,9 @@ const ExperimentTracker: React.FC<ExperimentTrackerProps> = ({ rows, weeks, prim
<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-sky-400 font-bold">{exp.marketplace || '—'}</span>
</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>