fix: resolve LINE level experiments to individual ASINs using salesData so calculation works for product lines

This commit is contained in:
Christian Vidal Wolf
2026-02-21 19:26:45 +01:00
parent c2d516dbfc
commit 249dc0c749
4 changed files with 63 additions and 36 deletions
+5 -3
View File
@@ -10,7 +10,10 @@ import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, For
import { queryGemini } from './services/geminiService'; import { queryGemini } from './services/geminiService';
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons'; import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons';
import { loadSalesData, saveSalesData, clearSalesData, loadAdsData, saveAdsData, clearAdsData } from './services/storage'; 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 // Lazy load heavy components for better initial performance
const DataGrid = lazy(() => import('./components/DataGrid')); const DataGrid = lazy(() => import('./components/DataGrid'));
@@ -258,8 +261,7 @@ const App: React.FC = () => {
const handleExperimentsFetch = useCallback(async () => { const handleExperimentsFetch = useCallback(async () => {
try { try {
const allAsins: string[] = Array.from(new Set(rawData.map(r => String(r.asin).toUpperCase()))); const expMap = await getActiveExperiments(rawData);
const expMap = await getActiveExperimentsForASINs(allAsins);
setExperimentMap(expMap); setExperimentMap(expMap);
console.log('[App] Loaded experiments for', expMap.size, 'ASINs'); console.log('[App] Loaded experiments for', expMap.size, 'ASINs');
} catch (error) { } catch (error) {
+2 -2
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Experiment, ExperimentCreateInput, ExperimentType, ExperimentStatus, ExperimentMetric, CombinedKPIs } from '../types'; 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 { ExperimentStatusBadge, ExperimentTypeBadge } from './ExperimentBadge';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, ReferenceLine } from 'recharts'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, ReferenceLine } from 'recharts';
@@ -67,7 +67,7 @@ const ExperimentDetail: React.FC<ExperimentDetailProps> = ({ experimentId, onClo
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!experiment || !salesData.length) return []; 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<string, any>(); const weeklyMap = new Map<string, any>();
salesData.forEach(r => { salesData.forEach(r => {
+2 -2
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react'; import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { ExperimentListItem, ExperimentType, ExperimentStatus, CombinedKPIs } from '../types'; 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 MultiSelectDropdown from './MultiSelectDropdown';
import { ExperimentBadge, ExperimentStatusBadge, ExperimentTypeBadge } from './ExperimentBadge'; import { ExperimentBadge, ExperimentStatusBadge, ExperimentTypeBadge } from './ExperimentBadge';
import { LineChart, Line, ResponsiveContainer, YAxis } from 'recharts'; import { LineChart, Line, ResponsiveContainer, YAxis } from 'recharts';
@@ -19,7 +19,7 @@ const ExperimentSparkline: React.FC<{ experiment: ExperimentListItem; salesData:
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!experiment?.asins || !salesData?.length) return []; 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<string, any>(); const weeklyMap = new Map<string, any>();
salesData.forEach(r => { salesData.forEach(r => {
+43 -18
View File
@@ -10,6 +10,34 @@ import { CombinedKPIs } from '../types';
const API_BASE = '/api/experiments'; const API_BASE = '/api/experiments';
// ============ Helper Functions ============
export const getExperimentAsins = (experimentAsins: string[], salesData: CombinedKPIs[]): Set<string> => {
const explicitAsins = new Set<string>();
const lines = new Set<string>();
(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<string>(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 ============ // ============ CRUD Operations ============
export const createExperiment = async (input: ExperimentCreateInput): Promise<Experiment> => { export const createExperiment = async (input: ExperimentCreateInput): Promise<Experiment> => {
@@ -135,13 +163,10 @@ export const listExperiments = async (
}); });
}; };
export const getActiveExperimentsForASINs = async ( export const getActiveExperiments = async (
asins: string[] salesData: CombinedKPIs[]
): Promise<Map<string, ActiveExperiment[]>> => { ): Promise<Map<string, ActiveExperiment[]>> => {
const params = new URLSearchParams(); const response = await fetch(`${API_BASE}?status=active`);
asins.forEach(asin => params.append('asin', asin));
const response = await fetch(`${API_BASE}?${params.toString()}&status=active`);
if (!response.ok) { if (!response.ok) {
const error = await response.json(); const error = await response.json();
@@ -160,17 +185,16 @@ export const getActiveExperimentsForASINs = async (
const map = new Map<string, ActiveExperiment[]>(); const map = new Map<string, ActiveExperiment[]>();
for (const asin of asins) { for (const exp of activeExperiments) {
const experiments = activeExperiments const expAsins = getExperimentAsins(exp.asins || [], salesData);
.filter((exp: any) => exp.asins?.includes(asin))
.map((exp: any) => {
const endDate = exp.end_date ? new Date(exp.end_date) : null; const endDate = exp.end_date ? new Date(exp.end_date) : null;
const daysRemaining = endDate const daysRemaining = endDate
? Math.ceil((endDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)) ? Math.ceil((endDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24))
: undefined; : undefined;
return { const activeExp: ActiveExperiment = {
asin, asin: '', // placeholder
experiment_id: exp.id, experiment_id: exp.id,
experiment_name: exp.name, experiment_name: exp.name,
type: exp.type, type: exp.type,
@@ -178,12 +202,13 @@ export const getActiveExperimentsForASINs = async (
start_date: exp.start_date, start_date: exp.start_date,
end_date: exp.end_date, end_date: exp.end_date,
days_remaining: daysRemaining, days_remaining: daysRemaining,
} as ActiveExperiment; };
});
if (experiments.length > 0) { expAsins.forEach(asin => {
map.set(asin, experiments); const asinList = map.get(asin) || [];
} asinList.push({ ...activeExp, asin });
map.set(asin, asinList);
});
} }
return map; return map;
@@ -216,7 +241,7 @@ export const calculateExperimentPerformance = async (
const baselineEnd = startDate; const baselineEnd = startDate;
// Filter sales data for experiment ASINs // 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 baselineData = salesData.filter(r => {
const recordDate = new Date(r.year, 0, 1 + (r.week - 1) * 7); const recordDate = new Date(r.year, 0, 1 + (r.week - 1) * 7);