mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:45:23 +02:00
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:
co-authored by
Qwen-Coder
parent
a4919691b2
commit
f93d92da0b
@@ -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 >
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user