import { Experiment, ExperimentCreateInput, ExperimentListItem, ActiveExperiment, ExperimentType, ExperimentStatus, ExperimentVerdict, CombinedKPIs, } from '../types'; const API_BASE = '/api/experiments'; // ============ ASIN Resolution ============ export const getExperimentAsins = (experimentAsins: string[], salesData: CombinedKPIs[]): Set => { const explicitAsins = new Set(); const lines = new Set(); (experimentAsins || []).forEach(a => { const val = (a || '').trim().toUpperCase(); if (val.startsWith('LINE:')) { lines.add(val.substring(5).trim()); } else if (val) { explicitAsins.add(val); } }); const asinSet = new Set(explicitAsins); if (lines.size > 0 && salesData) { salesData.forEach(r => { const line = (r.line || '').trim().toUpperCase(); if (line && lines.has(line)) { if (r.asin) asinSet.add(r.asin.toUpperCase()); } }); } return asinSet; }; // ============ CRUD Operations ============ export const createExperiment = async (input: ExperimentCreateInput): Promise => { const response = await fetch(API_BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to create experiment'); } return response.json(); }; export const updateExperiment = async ( id: string, updates: Partial ): Promise => { const response = await fetch(`${API_BASE}?id=${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates), }); if (!response.ok) { let errText = await response.text(); try { const errJson = JSON.parse(errText); errText = errJson.error || errText; } catch (e) { } throw new Error(errText); } return response.json(); }; export const deleteExperiment = async (id: string): Promise => { const response = await fetch(`${API_BASE}?id=${id}`, { method: 'DELETE', }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to delete experiment'); } }; export const getExperiment = async (id: string): Promise => { const response = await fetch(`${API_BASE}?id=${id}`); if (!response.ok) return null; return response.json(); }; export const listExperiments = async ( filters?: { status?: ExperimentStatus[]; type?: ExperimentType[]; marketplace?: string[]; asin?: string; dateFrom?: string; dateTo?: string; } ): Promise => { const params = new URLSearchParams(); if (filters?.status?.length) { filters.status.forEach(s => params.append('status', s)); } if (filters?.type?.length) { filters.type.forEach(t => params.append('type', t)); } if (filters?.marketplace?.length) { filters.marketplace.forEach(m => params.append('marketplace', m)); } if (filters?.asin) { params.append('asin', filters.asin); } if (filters?.dateFrom) { params.append('dateFrom', filters.dateFrom); } if (filters?.dateTo) { params.append('dateTo', filters.dateTo); } const response = await fetch(`${API_BASE}?${params.toString()}`); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to list experiments'); } const data = await response.json(); return data.map((exp: any) => { const today = new Date(); const startDate = new Date(exp.start_date); const endDate = exp.end_date ? new Date(exp.end_date) : null; let progressPercent = 0; if (endDate) { const totalDays = endDate.getTime() - startDate.getTime(); const elapsedDays = today.getTime() - startDate.getTime(); progressPercent = Math.min(100, Math.max(0, (elapsedDays / totalDays) * 100)); } return { id: exp.id, name: exp.name, type: exp.type, status: exp.status, asin_count: exp.asins?.length || 0, asins: exp.asins || [], control_asin_count: exp.control_asins?.length || 0, marketplace: exp.marketplace, start_date: exp.start_date, end_date: exp.end_date, progress_percent: Math.round(progressPercent), primary_metric: exp.primary_metric, verdict: exp.verdict, verdict_probability: exp.verdict_probability, actual_lift_percent: exp.actual_lift_percent, owner: exp.owner, }; }); }; export const getActiveExperiments = async ( salesData: CombinedKPIs[] ): Promise> => { const response = await fetch(`${API_BASE}?status=active`); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to fetch active experiments'); } const data = await response.json(); const today = new Date().toISOString().split('T')[0]; const activeExperiments = data.filter((exp: any) => exp.status === 'active' && exp.start_date <= today && (!exp.end_date || exp.end_date >= today) ); const map = new Map(); for (const exp of activeExperiments) { const expAsins = getExperimentAsins(exp.asins || [], salesData); const endDate = exp.end_date ? new Date(exp.end_date) : null; const daysRemaining = endDate ? Math.ceil((endDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)) : undefined; const activeExp: ActiveExperiment = { asin: '', experiment_id: exp.id, experiment_name: exp.name, type: exp.type, status: exp.status, start_date: exp.start_date, end_date: exp.end_date, days_remaining: daysRemaining, }; expAsins.forEach(asin => { const asinList = map.get(asin) || []; asinList.push({ ...activeExp, asin }); map.set(asin, asinList); }); } return map; }; // ============ Display Helpers ============ export const getExperimentStatusColor = (status: ExperimentStatus): string => { switch (status) { case 'active': return 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30'; case 'planned': return 'bg-amber-500/20 text-amber-400 border-amber-500/30'; case 'completed': return 'bg-slate-500/20 text-slate-400 border-slate-500/30'; case 'paused': return 'bg-red-500/20 text-red-400 border-red-500/30'; default: return 'bg-slate-500/20 text-slate-400'; } }; export const getExperimentTypeColor = (type: ExperimentType): string => { switch (type) { case 'pricing': return 'bg-blue-500/20 text-blue-400 border-blue-500/30'; case 'advertising': return 'bg-purple-500/20 text-purple-400 border-purple-500/30'; case 'content': return 'bg-pink-500/20 text-pink-400 border-pink-500/30'; case 'promotion': return 'bg-orange-500/20 text-orange-400 border-orange-500/30'; case 'seo': return 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30'; default: return 'bg-slate-500/20 text-slate-400'; } }; export const getExperimentIcon = (type: ExperimentType): string => { switch (type) { case 'pricing': return '๐Ÿ’ฐ'; case 'advertising': return '๐Ÿ“ข'; case 'content': return '๐Ÿ“'; case 'promotion': return '๐Ÿท๏ธ'; case 'seo': return '๐Ÿ”'; default: return '๐Ÿงช'; } }; export const getVerdictColor = (verdict?: ExperimentVerdict): string => { switch (verdict) { case 'winner': return 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30'; case 'loser': return 'bg-red-500/15 text-red-400 border-red-500/30'; case 'inconclusive': return 'bg-slate-500/15 text-slate-400 border-slate-500/30'; default: return 'bg-slate-800/50 text-slate-500 border-slate-700'; } }; export const getVerdictLabel = (verdict?: ExperimentVerdict): string => { switch (verdict) { case 'winner': return 'Winner'; case 'loser': return 'Unsuccessful'; case 'inconclusive': return 'Unsuccessful'; default: return 'Pending'; } }; export const METRIC_LABELS: Record = { units: 'Units Sold', sessions: 'Sessions', cvr: 'Conversion Rate', ctr: 'Click-Through Rate', roas: 'ROAS', revenue: 'Revenue', acos: 'ACOS', }; export const formatMetricValue = (value: number, metric: string): string => { switch (metric) { case 'cvr': case 'ctr': case 'acos': return `${value.toFixed(1)}%`; case 'roas': return `${value.toFixed(2)}x`; case 'revenue': return `โ‚ฌ${value.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`; default: return value.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); } };