diff --git a/components/WeeklyGrid.tsx b/components/WeeklyGrid.tsx
index a0670c8..a2adf45 100644
--- a/components/WeeklyGrid.tsx
+++ b/components/WeeklyGrid.tsx
@@ -68,7 +68,9 @@ const MetricDetailTooltip: React.FC<{
metricName: string;
metricColor: string;
formatValue?: (val: number) => string;
-}> = ({ children, currentValue, previousValue, yoyValue, currentWeekLabel, previousWeekLabel, yoyWeekLabel, metricName, metricColor, formatValue }) => {
+ experimentDelta?: number | null;
+ baselineValue?: number | null;
+}> = ({ children, currentValue, previousValue, yoyValue, currentWeekLabel, previousWeekLabel, yoyWeekLabel, metricName, metricColor, formatValue, experimentDelta, baselineValue }) => {
const [isVisible, setIsVisible] = useState(false);
const wowGrowth = previousValue > 0 ? ((currentValue - previousValue) / previousValue) * 100 : (currentValue > 0 ? 100 : 0);
@@ -135,6 +137,22 @@ const MetricDetailTooltip: React.FC<{
No data
)}
+
+ {/* Experiment Baseline Delta (If active) */}
+ {typeof experimentDelta === 'number' && typeof baselineValue === 'number' && (
+
+
+ vs Pre-Experiment Baseline
+ = 0 ? 'text-emerald-400' : 'text-red-400'}`}>
+ {experimentDelta >= 0 ? '▲' : '▼'} {Math.abs(experimentDelta).toFixed(1)}%
+
+
+
+ Baseline Avg (4w)
+ {format(baselineValue)}
+
+
+ )}
{/* Arrow */}
@@ -145,6 +163,58 @@ const MetricDetailTooltip: React.FC<{
);
};
+// Helper to get 4-week baseline prior to the start date
+const getBaselineMetrics = (row: WeeklyPivotRow, weeks: string[], expStartDate: string) => {
+ const startDate = new Date(expStartDate);
+ const jan1 = new Date(startDate.getFullYear(), 0, 1);
+ const dayOfYear = Math.floor((startDate.getTime() - jan1.getTime()) / 86400000) + 1;
+ const weekNum = Math.ceil((dayOfYear + jan1.getDay()) / 7);
+ const startWeekStr = `${startDate.getFullYear()}-${String(weekNum).padStart(2, '0')}`;
+
+ // Find index of startWeekStr in weeks array
+ const startIdx = weeks.indexOf(startWeekStr);
+
+ if (startIdx === -1) return null; // Can't compute baseline if start week is not in our data view
+
+ // Weeks before the start date are at startIdx + 1 to startIdx + 4
+ let u = 0, r = 0, g = 0, weeksCount = 0;
+ for (let i = 1; i <= 4; i++) {
+ const w = weeks[startIdx + i];
+ if (w) {
+ u += (row.unitsByWeek[w] || 0);
+ r += (row.revenueByWeek[w] || 0);
+ g += (row.gvByWeek?.[w] || 0);
+ weeksCount++;
+ }
+ }
+
+ if (weeksCount === 0) return null;
+ return {
+ units: u / weeksCount,
+ revenue: r / weeksCount,
+ gv: g / weeksCount,
+ cvr: g > 0 ? (u / g) * 100 : 0
+ };
+};
+
+// Helper to check if a week overlaps with an experiment's date range
+const isWeekInExperiment = (weekKey: string, exp: ActiveExperiment): boolean => {
+ if (!weekKey || !weekKey.includes('-')) return false;
+ const [yearStr, weekStr] = weekKey.split('-');
+ const year = parseInt(yearStr);
+ const week = parseInt(weekStr);
+
+ // Basic approximation of week start (Jan 1 + (week-1)*7 days)
+ const weekStart = new Date(year, 0, 1 + (week - 1) * 7);
+ const weekEnd = new Date(weekStart.getTime() + 6 * 86400000); // add 6 days
+
+ const expStart = new Date(exp.start_date);
+ const expEnd = exp.end_date ? new Date(exp.end_date) : new Date(); // If no end date, it's ongoing
+
+ // Overlap logic: weekStart <= expEnd && weekEnd >= expStart
+ return weekStart <= expEnd && weekEnd >= expStart;
+};
+
const WeeklyRow: React.FC<{
row: WeeklyPivotRow;
weeks: string[];
@@ -236,9 +306,38 @@ const WeeklyRow: React.FC<{
const yoyRevenue = row.revenueByWeek[lastYearWeek] || 0;
const yoyGv = row.gvByWeek?.[lastYearWeek] || 0;
+ const cvr = gv > 0 ? (val / gv) * 100 : 0;
+ const prevCvr = prevGv > 0 ? (prevVal / prevGv) * 100 : 0;
+ const yoyCvr = yoyGv > 0 ? (yoyUnits / yoyGv) * 100 : 0;
+
+ // Determine if this cell should be highlighted due to an active experiment
+ const experimentsInWeek = experimentMap?.get(asin)?.filter(exp => isWeekInExperiment(week, exp)) || [];
+ const hasActiveExperiment = experimentsInWeek.length > 0;
+
+ let highlightClass = '';
+ let experimentDelta: number | null = null;
+ let baselineValue: number | null = null;
+
+ if (hasActiveExperiment) {
+ const primaryExp = experimentsInWeek[0];
+ if (primaryExp.type === 'pricing') highlightClass = 'bg-amber-500/10 border-t border-amber-500/20';
+ else if (primaryExp.type === 'advertising') highlightClass = 'bg-rose-500/10 border-t border-rose-500/20';
+ else if (primaryExp.type === 'content') highlightClass = 'bg-emerald-500/10 border-t border-emerald-500/20';
+ else highlightClass = 'bg-indigo-500/10 border-t border-indigo-500/20';
+
+ const baseline = getBaselineMetrics(row, weeks, primaryExp.start_date);
+ if (baseline) {
+ const myMetric = primaryMetric === 'units' ? val : revenue;
+ const baseMetric = primaryMetric === 'units' ? baseline.units : baseline.revenue;
+ if (baseMetric > 0) {
+ experimentDelta = ((myMetric - baseMetric) / baseMetric) * 100;
+ baselineValue = baseMetric;
+ }
+ }
+ }
return (
-
+ |
{/* Primary Metric based on toggle */}
{primaryMetric === 'units' ? (
0 ? (sortConfig?.key === week && sortConfig.metric === 'units' ? 'text-indigo-400' : 'text-white') : 'text-slate-700'}`}>
@@ -269,6 +370,8 @@ const WeeklyRow: React.FC<{
metricName="Revenue"
metricColor="text-white"
formatValue={(v) => `€${v.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`}
+ experimentDelta={experimentDelta}
+ baselineValue={baselineValue}
>
0 ? (sortConfig?.key === week && sortConfig.metric === 'spend' ? 'text-amber-400' : 'text-white') : 'text-slate-700'}`}>
@@ -305,7 +408,7 @@ const WeeklyRow: React.FC<{
)}
- {/* GV with tooltip - Always small/bottom */}
+ {/* GV and CVR with tooltip - Always small/bottom */}
{gv > 0 && (
-
-
- GV: {gv.toLocaleString('de-DE')}
-
- {renderGrowth(gv, prevGv)}
+
+
+
+ GV: {gv.toLocaleString('de-DE')}
+
+ {renderGrowth(gv, prevGv)}
+
+
+
+ CVR: {cvr.toFixed(1)}%
+
+ {renderGrowth(cvr, prevCvr)}
+
)}
@@ -364,6 +475,7 @@ const WeeklyGrid: React.FC = ({
const [growthFilterMode, setGrowthFilterMode] = useState<'all' | 'up' | 'down' | 'stable'>('all');
const [growthThreshold, setGrowthThreshold] = useState(10);
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
+ const [showActiveExperiments, setShowActiveExperiments] = useState(false);
const [displayCount, setDisplayCount] = useState(50);
const [primaryMetric, setPrimaryMetric] = useState<'units' | 'revenue'>('units');
const scrollContainerRef = useRef(null);
@@ -466,6 +578,15 @@ const WeeklyGrid: React.FC = ({
});
}
+ // Active Experiments Filter
+ if (showActiveExperiments && experimentMap) {
+ result = result.filter(r => {
+ const asin = r.asin.trim().toUpperCase();
+ const experiments = experimentMap.get(asin);
+ return experiments && experiments.length > 0;
+ });
+ }
+
// Column Filters (Excel-style) - using logic adapted from filterData
if (Object.keys(columnFilters).length > 0) {
(Object.entries(columnFilters) as [string, ColumnFilterCondition][]).forEach(([key, condition]) => {
@@ -585,7 +706,7 @@ const WeeklyGrid: React.FC = ({
}
return result;
- }, [rows, debouncedSearch, showOnlyTop50, top50Ranking, growthFilterMode, growthThreshold, sortConfig, weeks, top50Mode, wocFilter, velocityMap, vendorStockMap, numericFilters, columnFilters]);
+ }, [rows, debouncedSearch, showOnlyTop50, showActiveExperiments, experimentMap, top50Ranking, growthFilterMode, growthThreshold, sortConfig, weeks, top50Mode, wocFilter, velocityMap, vendorStockMap, numericFilters, columnFilters]);
// 2. Sort results
const sortedRows = useMemo(() => {
@@ -749,6 +870,25 @@ const WeeklyGrid: React.FC = ({
)}
+ {/* Active Experiments Toggle */}
+ {experimentMap && experimentMap.size > 0 && (
+
+
+
+ )}
+
{/* Metric Toggle */}
|