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
+78 -4
View File
@@ -6,10 +6,11 @@ import FilterBar from './components/FilterBar';
import AIChat from './components/AIChat';
import CrazeLogo from './components/CrazeLogo';
import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, processUKInventoryExcel, calculateVelocityMap, getUniqueValues, processForecastExcel, processBuyBoxExcel, processVendorCSV } from './services/dataProcessor';
import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData } from './types';
import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData, ActiveExperiment } from './types';
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';
// Lazy load heavy components for better initial performance
const DataGrid = lazy(() => import('./components/DataGrid'));
@@ -18,6 +19,9 @@ const TopMovers = lazy(() => import('./components/TopMovers'));
const AdsPerformance = lazy(() => import('./components/AdsPerformance'));
const ForecastView = lazy(() => import('./components/ForecastView'));
const VendorDataView = lazy(() => import('./components/VendorDataView'));
const ExperimentsView = lazy(() => import('./components/ExperimentsView'));
const ExperimentDetail = lazy(() => import('./components/ExperimentDetail'));
const ExperimentForm = lazy(() => import('./components/ExperimentForm'));
// Loading fallback component
const LoadingSpinner = () => (
@@ -44,7 +48,7 @@ const App: React.FC = () => {
const [trafficData, setTrafficData] = useState<TrafficRecord[]>([]);
const [loading, setLoading] = useState(true);
const [syncing, setSyncing] = useState(false);
const [view, setView] = useState<'dashboard' | 'table' | 'weekly' | 'movers' | 'ads' | 'forecast' | 'vendor'>('dashboard');
const [view, setView] = useState<'dashboard' | 'table' | 'weekly' | 'movers' | 'ads' | 'forecast' | 'vendor' | 'experiments'>('dashboard');
const [forecastData, setForecastData] = useState<ProductForecastData[]>([]);
const [isChatOpen, setIsChatOpen] = useState(false);
const [activeUrl, setActiveUrl] = useState<string | null>(() => localStorage.getItem('craze_csv_url') || PERMANENT_DROPBOX_URL);
@@ -54,6 +58,13 @@ const App: React.FC = () => {
const [cachedForecastRecords, setCachedForecastRecords] = useState<ForecastRecord[]>([]);
const [lastForecastFile, setLastForecastFile] = useState<string | null>(null);
const [buyBoxLostMap, setBuyBoxLostMap] = useState<Map<string, { countries: string[]; reasons: Record<string, string> }>>(new Map());
// Experiments state
const [experimentMap, setExperimentMap] = useState<Map<string, ActiveExperiment[]>>(new Map());
const [selectedExperimentId, setSelectedExperimentId] = useState<string | null>(null);
const [showCreateExperiment, setShowCreateExperiment] = useState(false);
const [preselectedAsins, setPreselectedAsins] = useState<string[]>([]);
const [preselectedMarketplace, setPreselectedMarketplace] = useState<string>('');
// Modal State
const [isDataModalOpen, setIsDataModalOpen] = useState(false);
@@ -238,6 +249,17 @@ const App: React.FC = () => {
}
}, []);
const handleExperimentsFetch = useCallback(async () => {
try {
const allAsins = Array.from(new Set(rawData.map(r => r.asin.toUpperCase())));
const expMap = await getActiveExperimentsForASINs(allAsins);
setExperimentMap(expMap);
console.log('[App] Loaded experiments for', expMap.size, 'ASINs');
} catch (error) {
console.error("Failed to fetch experiments", error);
}
}, [rawData]);
const handleForecastFetch = useCallback(async (sales: SalesRecord[], globalSales: SalesRecord[], activeFilters: FilterState) => {
try {
@@ -379,7 +401,11 @@ const App: React.FC = () => {
handleVendorStockFetch();
handleBuyBoxFetch();
// 1e. Fetch Forecast data
// 1e. Fetch Experiments data
console.log("Fetching Experiments data...");
handleExperimentsFetch();
// 1f. Fetch Forecast data
handleForecastFetch(cachedData || [], cachedData || [], filters);
};
initApp();
@@ -793,6 +819,13 @@ const App: React.FC = () => {
>
<ChartIcon /> <span className="hidden lg:inline">Vendor</span>
</button>
<button
onClick={() => setView('experiments')}
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
${view === 'experiments' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
>
<span className="text-lg">🧪</span> <span className="hidden lg:inline">Experiments</span>
</button>
</div>
</div>
</div>
@@ -851,6 +884,8 @@ const App: React.FC = () => {
top50Ranking={top50Ranking2025}
velocityMap={velocityMap}
buyBoxLostMap={buyBoxLostMap}
experimentMap={experimentMap}
onOpenExperiment={(id) => setSelectedExperimentId(id)}
/>
</div>
</Suspense>
@@ -906,6 +941,19 @@ const App: React.FC = () => {
<VendorDataView />
</div>
</Suspense>
<Suspense fallback={<LoadingSpinner />}>
<div className={view === 'experiments' ? '' : 'hidden'}>
<ExperimentsView
onOpenDetail={(id) => setSelectedExperimentId(id)}
onOpenCreate={() => {
setPreselectedAsins([]);
setPreselectedMarketplace(filters.customer[0] || '');
setShowCreateExperiment(true);
}}
/>
</div>
</Suspense>
</div>
</>
)}
@@ -916,7 +964,7 @@ const App: React.FC = () => {
{/* Mobile Bottom Navigation */}
<nav className="fixed bottom-0 left-0 right-0 z-50 bg-slate-950/95 backdrop-blur border-t border-border md:hidden pb-safe">
<div className="grid grid-cols-7 gap-0">
<div className="grid grid-cols-8 gap-0">
{([
{ key: 'dashboard' as const, icon: <ChartIcon />, label: 'Home' },
{ key: 'table' as const, icon: <TableIcon />, label: 'Grid' },
@@ -925,6 +973,7 @@ const App: React.FC = () => {
{ key: 'ads' as const, icon: <MegaphoneIcon />, label: 'Ads' },
{ key: 'forecast' as const, icon: <ChartIcon />, label: 'Fc 26' },
{ key: 'vendor' as const, icon: <ChartIcon />, label: 'Vendor' },
{ key: 'experiments' as const, icon: <span className="text-lg">🧪</span>, label: 'Exp' },
]).map(({ key, icon, label }) => (
<button
key={key}
@@ -969,6 +1018,31 @@ const App: React.FC = () => {
)
}
{/* Experiment Detail Modal */}
{selectedExperimentId && (
<ExperimentDetail
experimentId={selectedExperimentId}
onClose={() => setSelectedExperimentId(null)}
salesData={rawData}
/>
)}
{/* Create Experiment Modal */}
{showCreateExperiment && (
<ExperimentForm
onClose={() => {
setShowCreateExperiment(false);
setPreselectedAsins([]);
setPreselectedMarketplace('');
}}
onSuccess={() => {
handleExperimentsFetch();
}}
initialAsins={preselectedAsins}
initialMarketplace={preselectedMarketplace}
/>
)}
</div >
);
};