mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 15:45:24 +02:00
fix: resolve UTC date matching and implement per-product ASIN breakdown table for Line experiments
This commit is contained in:
@@ -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(() => {
|
const chartData = useMemo(() => {
|
||||||
if (!experiment || !salesData.length) return [];
|
if (!experiment || !salesData.length) return [];
|
||||||
|
|
||||||
const asinSet = getExperimentAsins(experiment.asins, salesData);
|
const asinSet = new Set(targetAsins);
|
||||||
const weeklyMap = new Map<string, any>();
|
const weeklyMap = new Map<string, any>();
|
||||||
|
|
||||||
salesData.forEach(r => {
|
salesData.forEach(r => {
|
||||||
@@ -488,19 +535,66 @@ const ExperimentDetail: React.FC<ExperimentDetailProps> = ({ experimentId, onClo
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ASINs */}
|
{/* ASINs */}
|
||||||
<DetailSection title="📦 Target ASINs">
|
<DetailSection title={`📦 Target ASINs (${targetAsins.length})`}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{(experiment.asins || []).map((asin, idx) => (
|
{targetAsinsOriginal.map((asin, idx) => (
|
||||||
<span
|
<span
|
||||||
key={idx}
|
key={`orig-${idx}`}
|
||||||
className="px-2.5 py-1 bg-slate-800 border border-slate-600 rounded-lg text-sm text-slate-300 font-mono"
|
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}
|
{asin}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
</DetailSection>
|
</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 */}
|
{/* Learnings */}
|
||||||
<DetailSection title="💡 Learnings & Conclusions">
|
<DetailSection title="💡 Learnings & Conclusions">
|
||||||
{editing ? (
|
{editing ? (
|
||||||
|
|||||||
@@ -232,8 +232,13 @@ export const calculateExperimentPerformance = async (
|
|||||||
experiment_acos: number;
|
experiment_acos: number;
|
||||||
actual_lift_percent: number;
|
actual_lift_percent: number;
|
||||||
}> => {
|
}> => {
|
||||||
const startDate = new Date(experiment.start_date);
|
const parseLocalDate = (dateStr: string) => {
|
||||||
const endDate = experiment.end_date ? new Date(experiment.end_date) : new Date();
|
const [y, m, d] = dateStr.split('T')[0].split('-');
|
||||||
|
return new Date(Number(y), Number(m) - 1, Number(d));
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDate = parseLocalDate(experiment.start_date);
|
||||||
|
const endDate = experiment.end_date ? parseLocalDate(experiment.end_date) : new Date();
|
||||||
|
|
||||||
// Calculate baseline period (same duration before experiment)
|
// Calculate baseline period (same duration before experiment)
|
||||||
const durationMs = endDate.getTime() - startDate.getTime();
|
const durationMs = endDate.getTime() - startDate.getTime();
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// read dummy data or mock
|
||||||
|
const mockSalesData = [
|
||||||
|
{
|
||||||
|
asin: 'B08F2J8S1Y',
|
||||||
|
line: 'LEGENDS',
|
||||||
|
year: 2026,
|
||||||
|
week: 1,
|
||||||
|
unitsTotal: 10,
|
||||||
|
salesTotal: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
asin: 'B08F2J8S1Y',
|
||||||
|
line: 'LEGENDS',
|
||||||
|
year: 2026,
|
||||||
|
week: 2,
|
||||||
|
unitsTotal: 15,
|
||||||
|
salesTotal: 150,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const experimentAsins = ['LINE:LEGENDS'];
|
||||||
|
|
||||||
|
const lines = new Set();
|
||||||
|
const explicitAsins = new Set();
|
||||||
|
experimentAsins.forEach(a => {
|
||||||
|
const val = (a || '').trim().toUpperCase();
|
||||||
|
if (val.startsWith('LINE:')) {
|
||||||
|
lines.add(val.substring(5).trim());
|
||||||
|
} else if (val) {
|
||||||
|
explicitAsins.add(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const asinSet = new Set(explicitAsins);
|
||||||
|
if (lines.size > 0 && mockSalesData) {
|
||||||
|
mockSalesData.forEach(r => {
|
||||||
|
const line = (r.line || '').trim().toUpperCase();
|
||||||
|
if (line && lines.has(line)) {
|
||||||
|
if (r.asin) asinSet.add(r.asin.toUpperCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Resolved ASINs:', Array.from(asinSet));
|
||||||
|
|
||||||
|
// Date test
|
||||||
|
const startDateStr = '2026-01-01';
|
||||||
|
const endDateStr = '2026-01-14';
|
||||||
|
|
||||||
|
const startDate = new Date(startDateStr);
|
||||||
|
const endDate = new Date(endDateStr);
|
||||||
|
|
||||||
|
console.log('Start Date UTC:', startDate.toISOString(), 'Local:', startDate.toString());
|
||||||
|
console.log('End Date UTC:', endDate.toISOString(), 'Local:', endDate.toString());
|
||||||
|
|
||||||
|
const recordDate1 = new Date(2026, 0, 1); // week 1
|
||||||
|
const recordDate2 = new Date(2026, 0, 8); // week 2
|
||||||
|
const recordDate3 = new Date(2026, 0, 15); // week 3
|
||||||
|
|
||||||
|
console.log('Week 1 Record Date Local:', recordDate1.toString());
|
||||||
|
console.log('Week 1 included in experiment?', recordDate1 >= startDate && recordDate1 <= endDate);
|
||||||
|
console.log('Week 2 Record Date Local:', recordDate2.toString());
|
||||||
|
console.log('Week 2 included in experiment?', recordDate2 >= startDate && recordDate2 <= endDate);
|
||||||
|
console.log('Week 3 Record Date Local:', recordDate3.toString());
|
||||||
|
console.log('Week 3 included in experiment?', recordDate3 >= startDate && recordDate3 <= endDate);
|
||||||
|
|
||||||
Reference in New Issue
Block a user