From f93d92da0b33f816c848999f7f5abacb0c620b50 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Fri, 20 Feb 2026 19:31:44 +0100 Subject: [PATCH] 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 --- App.tsx | 82 +++- components/ExperimentBadge.tsx | 115 +++++ components/ExperimentDetail.tsx | 410 ++++++++++++++++++ components/ExperimentForm.tsx | 338 +++++++++++++++ components/ExperimentsView.tsx | 306 +++++++++++++ components/WeeklyGrid.tsx | 10 + services/experiments.ts | 301 +++++++++++++ .../001_create_experiments_table.sql | 59 +++ types.ts | 72 +++ 9 files changed, 1689 insertions(+), 4 deletions(-) create mode 100644 components/ExperimentBadge.tsx create mode 100644 components/ExperimentDetail.tsx create mode 100644 components/ExperimentForm.tsx create mode 100644 components/ExperimentsView.tsx create mode 100644 services/experiments.ts create mode 100644 supabase/migrations/001_create_experiments_table.sql diff --git a/App.tsx b/App.tsx index 43dcf7a..0e9fc12 100644 --- a/App.tsx +++ b/App.tsx @@ -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([]); 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([]); const [isChatOpen, setIsChatOpen] = useState(false); const [activeUrl, setActiveUrl] = useState(() => localStorage.getItem('craze_csv_url') || PERMANENT_DROPBOX_URL); @@ -54,6 +58,13 @@ const App: React.FC = () => { const [cachedForecastRecords, setCachedForecastRecords] = useState([]); const [lastForecastFile, setLastForecastFile] = useState(null); const [buyBoxLostMap, setBuyBoxLostMap] = useState }>>(new Map()); + + // Experiments state + const [experimentMap, setExperimentMap] = useState>(new Map()); + const [selectedExperimentId, setSelectedExperimentId] = useState(null); + const [showCreateExperiment, setShowCreateExperiment] = useState(false); + const [preselectedAsins, setPreselectedAsins] = useState([]); + const [preselectedMarketplace, setPreselectedMarketplace] = useState(''); // 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 = () => { > Vendor + @@ -851,6 +884,8 @@ const App: React.FC = () => { top50Ranking={top50Ranking2025} velocityMap={velocityMap} buyBoxLostMap={buyBoxLostMap} + experimentMap={experimentMap} + onOpenExperiment={(id) => setSelectedExperimentId(id)} /> @@ -906,6 +941,19 @@ const App: React.FC = () => { + + }> +
+ setSelectedExperimentId(id)} + onOpenCreate={() => { + setPreselectedAsins([]); + setPreselectedMarketplace(filters.customer[0] || ''); + setShowCreateExperiment(true); + }} + /> +
+
)} @@ -916,7 +964,7 @@ const App: React.FC = () => { {/* Mobile Bottom Navigation */}