Add complete Experiments tracking system

New Features:
- Experiments tab with full CRUD operations
- Create experiments by ASIN or product line groups
- Track pricing, advertising, content, and promotion experiments
- Performance analytics with baseline vs experiment comparison
- Visual experiment badges in Weekly Sales grid
- Experiment detail view with metrics and learnings

Technical Changes:
- Add Supabase experiments table migration
- New services/experiments.ts for CRUD + calculations
- New components: ExperimentsView, ExperimentDetail, ExperimentForm, ExperimentBadge
- Integrate experiment indicators in WeeklyGrid
- Add navigation tab (desktop + mobile)
- Type definitions in types.ts

Usage:
1. Run SQL migration in Supabase
2. Navigate to Experiments tab
3. Create experiments with ASINs, dates, hypothesis
4. View performance lift after completion
5. See active experiments marked in Weekly Sales

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Christian Vidal Wolf
2026-02-20 19:31:44 +01:00
co-authored by Qwen-Coder
parent a4919691b2
commit f93d92da0b
9 changed files with 1689 additions and 4 deletions
+410
View File
@@ -0,0 +1,410 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Experiment, ExperimentCreateInput, ExperimentType, ExperimentStatus, ExperimentMetric } from '../types';
import { getExperiment, updateExperiment, deleteExperiment, calculateExperimentPerformance } from '../services/experiments';
import { ExperimentStatusBadge, ExperimentTypeBadge } from './ExperimentBadge';
interface ExperimentDetailProps {
experimentId: string | null;
onClose: () => void;
salesData?: any[];
}
const ExperimentDetail: React.FC<ExperimentDetailProps> = ({ experimentId, onClose, salesData = [] }) => {
const [experiment, setExperiment] = useState<Experiment | null>(null);
const [loading, setLoading] = useState(false);
const [editing, setEditing] = useState(false);
const [formData, setFormData] = useState<Partial<Experiment>>({});
const loadExperiment = useCallback(async () => {
if (!experimentId) return;
setLoading(true);
try {
const data = await getExperiment(experimentId);
setExperiment(data);
setFormData(data || {});
} catch (e: any) {
console.error('Failed to load experiment:', e);
} finally {
setLoading(false);
}
}, [experimentId]);
useEffect(() => {
if (experimentId) {
loadExperiment();
}
}, [experimentId, loadExperiment]);
const handleSave = async () => {
if (!experimentId) return;
setLoading(true);
try {
const updated = await updateExperiment(experimentId, formData);
setExperiment(updated);
setEditing(false);
} catch (e: any) {
alert(`Error saving: ${e.message}`);
} finally {
setLoading(false);
}
};
const handleCalculateResults = async () => {
if (!experiment || !salesData.length) return;
setLoading(true);
try {
const perf = await calculateExperimentPerformance(experiment, salesData);
const updated = await updateExperiment(experiment.id, perf);
setExperiment(updated);
} catch (e: any) {
alert(`Error calculating results: ${e.message}`);
} finally {
setLoading(false);
}
};
if (!experimentId) return null;
return (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-slate-900 border border-slate-700 rounded-2xl w-full max-w-5xl max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-slate-700">
<div className="flex items-center gap-3">
{editing ? (
<input
type="text"
value={formData.name || ''}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="text-xl font-bold bg-slate-800 border border-slate-600 rounded-lg px-3 py-1 text-white"
/>
) : (
<h2 className="text-xl font-bold text-white">{experiment?.name || 'Loading...'}</h2>
)}
{experiment && (
<div className="flex items-center gap-2">
<ExperimentStatusBadge status={experiment.status} />
<ExperimentTypeBadge type={experiment.type} />
</div>
)}
</div>
<div className="flex items-center gap-2">
{editing ? (
<>
<button
onClick={() => { setEditing(false); setFormData(experiment || {}); }}
className="px-3 py-1.5 text-sm text-slate-400 hover:text-white transition-colors"
>
Cancel
</button>
<button
onClick={handleSave}
disabled={loading}
className="px-4 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors"
>
Save
</button>
</>
) : (
<>
<button
onClick={() => setEditing(true)}
className="px-3 py-1.5 text-sm text-slate-400 hover:text-white transition-colors"
>
Edit
</button>
<button
onClick={onClose}
className="p-2 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</>
)}
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
{loading && !experiment ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-indigo-500"></div>
</div>
) : experiment ? (
<div className="space-y-6">
{/* Performance Cards */}
{(experiment.experiment_units !== undefined || experiment.baseline_units !== undefined) && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<PerformanceCard
label="Baseline Units"
value={experiment.baseline_units?.toLocaleString() || '—'}
color="slate"
/>
<PerformanceCard
label="Experiment Units"
value={experiment.experiment_units?.toLocaleString() || '—'}
color="indigo"
/>
<PerformanceCard
label="Lift"
value={experiment.actual_lift_percent !== undefined
? `${experiment.actual_lift_percent >= 0 ? '+' : ''}${experiment.actual_lift_percent}%`
: '—'}
color={experiment.actual_lift_percent && experiment.actual_lift_percent >= 0 ? 'emerald' : 'red'}
/>
<PerformanceCard
label="Statistical Significance"
value={experiment.statistical_significance
? `${(experiment.statistical_significance * 100).toFixed(1)}%`
: '—'}
color="purple"
/>
</div>
)}
{/* Calculate Results Button */}
{experiment.status === 'completed' && experiment.experiment_units === undefined && salesData.length > 0 && (
<button
onClick={handleCalculateResults}
className="w-full py-3 bg-indigo-600/20 border border-indigo-500/30 hover:bg-indigo-600/30 text-indigo-400 rounded-xl font-medium transition-colors"
>
📊 Calculate Experiment Results
</button>
)}
{/* Details Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Left Column */}
<div className="space-y-4">
<DetailSection title="📋 Overview">
{editing ? (
<>
<textarea
value={formData.description || ''}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white"
rows={3}
placeholder="Experiment description..."
/>
</>
) : (
<p className="text-slate-300 text-sm">{experiment.description || 'No description'}</p>
)}
</DetailSection>
<DetailSection title="🎯 Hypothesis">
{editing ? (
<textarea
value={formData.hypothesis || ''}
onChange={(e) => setFormData({ ...formData, hypothesis: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white"
rows={3}
placeholder="What do you expect to happen?"
/>
) : (
<p className="text-slate-300 text-sm">{experiment.hypothesis || 'No hypothesis defined'}</p>
)}
</DetailSection>
<DetailSection title="📏 Metrics">
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-400">Primary Metric:</span>
<span className="text-white font-medium capitalize">{experiment.primary_metric}</span>
</div>
{experiment.target_lift_percent && (
<div className="flex justify-between">
<span className="text-slate-400">Target Lift:</span>
<span className="text-white font-medium">{experiment.target_lift_percent}%</span>
</div>
)}
</div>
</DetailSection>
</div>
{/* Right Column */}
<div className="space-y-4">
<DetailSection title="🎪 Experiment Details">
<div className="space-y-2 text-sm">
{editing ? (
<>
<div className="grid grid-cols-2 gap-2">
<select
value={formData.status || 'planned'}
onChange={(e) => setFormData({ ...formData, status: e.target.value as ExperimentStatus })}
className="bg-slate-800 border border-slate-600 rounded-lg px-2 py-1.5 text-sm text-white"
>
<option value="planned">Planned</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
<option value="paused">Paused</option>
</select>
<select
value={formData.type || 'pricing'}
onChange={(e) => setFormData({ ...formData, type: e.target.value as ExperimentType })}
className="bg-slate-800 border border-slate-600 rounded-lg px-2 py-1.5 text-sm text-white"
>
<option value="pricing">Pricing</option>
<option value="advertising">Advertising</option>
<option value="content">Content</option>
<option value="promotion">Promotion</option>
</select>
</div>
<input
type="text"
value={formData.marketplace || ''}
onChange={(e) => setFormData({ ...formData, marketplace: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-2 py-1.5 text-sm text-white"
placeholder="Marketplace (DE, UK, FR, IT, ES)"
/>
</>
) : (
<>
<div className="flex justify-between">
<span className="text-slate-400">Marketplace:</span>
<span className="text-white font-medium">{experiment.marketplace}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-400">Type:</span>
<span className="text-white font-medium capitalize">{experiment.type}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-400">Status:</span>
<span className="text-white font-medium capitalize">{experiment.status}</span>
</div>
</>
)}
</div>
</DetailSection>
<DetailSection title="📅 Timeline">
<div className="space-y-2 text-sm">
{editing ? (
<>
<div>
<label className="text-xs text-slate-400 block mb-1">Start Date</label>
<input
type="date"
value={formData.start_date || ''}
onChange={(e) => setFormData({ ...formData, start_date: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-2 py-1.5 text-sm text-white"
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">End Date</label>
<input
type="date"
value={formData.end_date || ''}
onChange={(e) => setFormData({ ...formData, end_date: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-2 py-1.5 text-sm text-white"
/>
</div>
</>
) : (
<>
<div className="flex justify-between">
<span className="text-slate-400">Start Date:</span>
<span className="text-white">{new Date(experiment.start_date).toLocaleDateString()}</span>
</div>
{experiment.end_date && (
<div className="flex justify-between">
<span className="text-slate-400">End Date:</span>
<span className="text-white">{new Date(experiment.end_date).toLocaleDateString()}</span>
</div>
)}
{experiment.end_date && (
<div className="flex justify-between">
<span className="text-slate-400">Duration:</span>
<span className="text-white">
{Math.ceil((new Date(experiment.end_date).getTime() - new Date(experiment.start_date).getTime()) / (1000 * 60 * 60 * 24))} days
</span>
</div>
)}
</>
)}
</div>
</DetailSection>
<DetailSection title="👤 Owner">
{editing ? (
<input
type="text"
value={formData.owner || ''}
onChange={(e) => setFormData({ ...formData, owner: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-2 py-1.5 text-sm text-white"
placeholder="Owner name"
/>
) : (
<p className="text-slate-300 text-sm">{experiment.owner || 'Not assigned'}</p>
)}
</DetailSection>
</div>
</div>
{/* ASINs */}
<DetailSection title="📦 Target ASINs">
<div className="flex flex-wrap gap-2">
{experiment.asins.map((asin, idx) => (
<span
key={idx}
className="px-2.5 py-1 bg-slate-800 border border-slate-600 rounded-lg text-sm text-slate-300 font-mono"
>
{asin}
</span>
))}
</div>
</DetailSection>
{/* Learnings */}
<DetailSection title="💡 Learnings & Conclusions">
{editing ? (
<textarea
value={formData.learnings || ''}
onChange={(e) => setFormData({ ...formData, learnings: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white"
rows={4}
placeholder="What did you learn from this experiment?"
/>
) : (
<p className="text-slate-300 text-sm whitespace-pre-wrap">
{experiment.learnings || 'No learnings recorded yet'}
</p>
)}
</DetailSection>
</div>
) : null}
</div>
</div>
</div>
);
};
// Performance Card Component
const PerformanceCard: React.FC<{ label: string; value: string; color: string }> = ({ label, value, color }) => {
const colorClasses: Record<string, string> = {
slate: 'bg-slate-500/10 border-slate-500/30 text-slate-400',
indigo: 'bg-indigo-500/10 border-indigo-500/30 text-indigo-400',
emerald: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400',
red: 'bg-red-500/10 border-red-500/30 text-red-400',
purple: 'bg-purple-500/10 border-purple-500/30 text-purple-400',
};
return (
<div className={`p-4 rounded-xl border ${colorClasses[color]}`}>
<div className="text-xs opacity-70 mb-1">{label}</div>
<div className="text-xl font-bold">{value}</div>
</div>
);
};
// Detail Section Component
const DetailSection: React.FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => (
<div className="bg-slate-800/30 border border-slate-700 rounded-xl p-4">
<h3 className="text-sm font-semibold text-slate-300 mb-3">{title}</h3>
{children}
</div>
);
export default ExperimentDetail;