From 249dc0c74999c552992b882c9d206ceb1361611f Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Sat, 21 Feb 2026 19:26:45 +0100 Subject: [PATCH] fix: resolve LINE level experiments to individual ASINs using salesData so calculation works for product lines --- App.tsx | 8 ++-- components/ExperimentDetail.tsx | 4 +- components/ExperimentsView.tsx | 4 +- services/experiments.ts | 83 +++++++++++++++++++++------------ 4 files changed, 63 insertions(+), 36 deletions(-) diff --git a/App.tsx b/App.tsx index fcebcdb..b8d4748 100644 --- a/App.tsx +++ b/App.tsx @@ -10,7 +10,10 @@ import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, For import { queryGemini } from './services/geminiService'; import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons'; import { loadSalesData, saveSalesData, clearSalesData, loadAdsData, saveAdsData, clearAdsData } from './services/storage'; -import { getActiveExperimentsForASINs } from './services/experiments'; +import { + listExperiments, + getActiveExperiments, +} from './services/experiments'; // Lazy load heavy components for better initial performance const DataGrid = lazy(() => import('./components/DataGrid')); @@ -258,8 +261,7 @@ const App: React.FC = () => { const handleExperimentsFetch = useCallback(async () => { try { - const allAsins: string[] = Array.from(new Set(rawData.map(r => String(r.asin).toUpperCase()))); - const expMap = await getActiveExperimentsForASINs(allAsins); + const expMap = await getActiveExperiments(rawData); setExperimentMap(expMap); console.log('[App] Loaded experiments for', expMap.size, 'ASINs'); } catch (error) { diff --git a/components/ExperimentDetail.tsx b/components/ExperimentDetail.tsx index 53cd3d9..bcb8fa2 100644 --- a/components/ExperimentDetail.tsx +++ b/components/ExperimentDetail.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Experiment, ExperimentCreateInput, ExperimentType, ExperimentStatus, ExperimentMetric, CombinedKPIs } from '../types'; -import { getExperiment, updateExperiment, deleteExperiment, calculateExperimentPerformance } from '../services/experiments'; +import { getExperiment, updateExperiment, deleteExperiment, calculateExperimentPerformance, getExperimentAsins } from '../services/experiments'; import { ExperimentStatusBadge, ExperimentTypeBadge } from './ExperimentBadge'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, ReferenceLine } from 'recharts'; @@ -67,7 +67,7 @@ const ExperimentDetail: React.FC = ({ experimentId, onClo const chartData = useMemo(() => { if (!experiment || !salesData.length) return []; - const asinSet = new Set((experiment.asins || []).map(a => (a || '').toUpperCase())); + const asinSet = getExperimentAsins(experiment.asins, salesData); const weeklyMap = new Map(); salesData.forEach(r => { diff --git a/components/ExperimentsView.tsx b/components/ExperimentsView.tsx index 2e67193..202a117 100644 --- a/components/ExperimentsView.tsx +++ b/components/ExperimentsView.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { ExperimentListItem, ExperimentType, ExperimentStatus, CombinedKPIs } from '../types'; -import { listExperiments, deleteExperiment, getExperimentStatusColor, getExperimentTypeColor, getExperimentIcon } from '../services/experiments'; +import { listExperiments, deleteExperiment, getExperimentStatusColor, getExperimentTypeColor, getExperimentIcon, getExperimentAsins } from '../services/experiments'; import MultiSelectDropdown from './MultiSelectDropdown'; import { ExperimentBadge, ExperimentStatusBadge, ExperimentTypeBadge } from './ExperimentBadge'; import { LineChart, Line, ResponsiveContainer, YAxis } from 'recharts'; @@ -19,7 +19,7 @@ const ExperimentSparkline: React.FC<{ experiment: ExperimentListItem; salesData: const chartData = useMemo(() => { if (!experiment?.asins || !salesData?.length) return []; - const asinSet = new Set((experiment.asins || []).map(a => (a || '').toUpperCase())); + const asinSet = getExperimentAsins(experiment.asins, salesData); const weeklyMap = new Map(); salesData.forEach(r => { diff --git a/services/experiments.ts b/services/experiments.ts index 6daf827..5c5e041 100644 --- a/services/experiments.ts +++ b/services/experiments.ts @@ -10,6 +10,34 @@ import { CombinedKPIs } from '../types'; const API_BASE = '/api/experiments'; +// ============ Helper Functions ============ + +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 => { @@ -135,13 +163,10 @@ export const listExperiments = async ( }); }; -export const getActiveExperimentsForASINs = async ( - asins: string[] +export const getActiveExperiments = async ( + salesData: CombinedKPIs[] ): Promise> => { - const params = new URLSearchParams(); - asins.forEach(asin => params.append('asin', asin)); - - const response = await fetch(`${API_BASE}?${params.toString()}&status=active`); + const response = await fetch(`${API_BASE}?status=active`); if (!response.ok) { const error = await response.json(); @@ -160,30 +185,30 @@ export const getActiveExperimentsForASINs = async ( const map = new Map(); - for (const asin of asins) { - const experiments = activeExperiments - .filter((exp: any) => exp.asins?.includes(asin)) - .map((exp: any) => { - 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; + for (const exp of activeExperiments) { + const expAsins = getExperimentAsins(exp.asins || [], salesData); - return { - 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, - } as ActiveExperiment; - }); + 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; - if (experiments.length > 0) { - map.set(asin, experiments); - } + const activeExp: ActiveExperiment = { + asin: '', // placeholder + 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; @@ -216,7 +241,7 @@ export const calculateExperimentPerformance = async ( const baselineEnd = startDate; // Filter sales data for experiment ASINs - const asinSet = new Set(experiment.asins.map(a => a.toUpperCase())); + const asinSet = getExperimentAsins(experiment.asins, salesData); const baselineData = salesData.filter(r => { const recordDate = new Date(r.year, 0, 1 + (r.week - 1) * 7);