fix: resolve UTC date matching and implement per-product ASIN breakdown table for Line experiments

This commit is contained in:
Christian Vidal Wolf
2026-02-21 19:37:07 +01:00
parent 8bd78c2e7f
commit aa9a7845b7
3 changed files with 181 additions and 13 deletions
+105 -11
View File
@@ -64,10 +64,57 @@ const ExperimentDetail: React.FC<ExperimentDetailProps> = ({ experimentId, onClo
}
};
const targetAsins = useMemo(() => {
if (!experiment) return [];
return Array.from(getExperimentAsins(experiment.asins, salesData));
}, [experiment, salesData]);
const productBreakdown = useMemo(() => {
if (!experiment || targetAsins.length <= 1 || !salesData.length) return null;
const parseDate = (d: string) => {
const [y, m, da] = d.split('T')[0].split('-');
return new Date(Number(y), Number(m) - 1, Number(da));
};
const startDate = parseDate(experiment.start_date);
const endDate = experiment.end_date ? parseDate(experiment.end_date) : new Date();
const asinMap = new Map<string, { units: number; revenue: number; gv: number; spend: number }>();
targetAsins.forEach(a => asinMap.set(a, { units: 0, revenue: 0, gv: 0, spend: 0 }));
salesData.forEach(r => {
const asin = (r.asin || '').toUpperCase();
if (!asinMap.has(asin)) return;
const recordDate = new Date(r.year, 0, 1 + ((r.week || 1) - 1) * 7);
if (recordDate >= startDate && recordDate <= endDate) {
const d = asinMap.get(asin)!;
d.units += r.unitsTotal || 0;
d.revenue += r.salesTotal || 0;
d.gv += r.glanceViews || 0;
d.spend += r.cost || 0;
}
});
return Array.from(asinMap.entries())
.map(([asin, metrics]) => ({
asin,
...metrics,
cvr: metrics.gv > 0 ? (metrics.units / metrics.gv) * 100 : 0,
acos: metrics.revenue > 0 ? (metrics.spend / metrics.revenue) * 100 : 0
}))
.filter(m => m.units > 0 || m.gv > 0 || m.spend > 0)
.sort((a, b) => b[experiment.primary_metric === 'bsr' ? 'units' : experiment.primary_metric as 'units' | 'revenue' | 'gv' | 'cvr' | 'acos' || 'units'] - a[experiment.primary_metric === 'bsr' ? 'units' : experiment.primary_metric as 'units' | 'revenue' | 'gv' | 'cvr' | 'acos' || 'units']);
}, [experiment, salesData, targetAsins]);
const targetAsinsOriginal = experiment?.asins || [];
const chartData = useMemo(() => {
if (!experiment || !salesData.length) return [];
const asinSet = getExperimentAsins(experiment.asins, salesData);
const asinSet = new Set(targetAsins);
const weeklyMap = new Map<string, any>();
salesData.forEach(r => {
@@ -488,19 +535,66 @@ const ExperimentDetail: React.FC<ExperimentDetailProps> = ({ experimentId, onClo
</div>
{/* ASINs */}
<DetailSection title="📦 Target ASINs">
<div className="flex flex-wrap gap-2">
{(experiment.asins || []).map((asin, idx) => (
<span
key={idx}
className="px-2.5 py-1 bg-slate-800 border border-slate-600 rounded-lg text-sm text-slate-300 font-mono"
>
{asin}
</span>
))}
<DetailSection title={`📦 Target ASINs (${targetAsins.length})`}>
<div className="flex flex-col gap-2">
<div className="flex flex-wrap gap-2">
{targetAsinsOriginal.map((asin, idx) => (
<span
key={`orig-${idx}`}
className="px-2.5 py-1 bg-indigo-900/40 border border-indigo-500/30 rounded-lg text-sm text-indigo-300 font-bold"
>
{asin}
</span>
))}
{targetAsins.map((asin, idx) => {
// Don't show again if it's the exact same as an explicit ASIN,
// but show if it was resolved from a LINE: prefix
if (targetAsinsOriginal.includes(asin)) return null;
return (
<span
key={`resolved-${idx}`}
className="px-2.5 py-1 bg-slate-800 border border-slate-600 rounded-lg text-sm text-slate-300 font-mono"
>
{asin}
</span>
);
})}
</div>
</div>
</DetailSection>
{/* Product Breakdown (Only shown if multiple ASINs, i.e. product line) */}
{productBreakdown && productBreakdown.length > 0 && (
<DetailSection title="📊 Product Performance Breakdown (During Experiment)">
<div className="overflow-x-auto rounded-xl border border-slate-700">
<table className="w-full text-left text-sm whitespace-nowrap">
<thead className="bg-slate-800/50 text-slate-400">
<tr>
<th className="px-4 py-3 font-medium">ASIN</th>
<th className="px-4 py-3 font-medium text-right">Units</th>
<th className="px-4 py-3 font-medium text-right">Revenue</th>
<th className="px-4 py-3 font-medium text-right">Glance Views</th>
<th className="px-4 py-3 font-medium text-right">CVR</th>
<th className="px-4 py-3 font-medium text-right">ACOS</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-700/50">
{productBreakdown.map(p => (
<tr key={p.asin} className="hover:bg-slate-800/30 transition-colors">
<td className="px-4 py-3 font-mono text-slate-300">{p.asin}</td>
<td className="px-4 py-3 text-right text-white font-medium">{p.units.toLocaleString()}</td>
<td className="px-4 py-3 text-right text-emerald-400">{p.revenue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
<td className="px-4 py-3 text-right text-slate-300">{p.gv.toLocaleString()}</td>
<td className="px-4 py-3 text-right text-amber-400">{p.cvr.toFixed(1)}%</td>
<td className="px-4 py-3 text-right text-rose-400">{p.acos.toFixed(1)}%</td>
</tr>
))}
</tbody>
</table>
</div>
</DetailSection>
)}
{/* Learnings */}
<DetailSection title="💡 Learnings & Conclusions">
{editing ? (