mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:35:24 +02:00
feat: complete experiments tab analytics optimization with modular kpis and sparklines
This commit is contained in:
@@ -1,15 +1,92 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { ExperimentListItem, ExperimentType, ExperimentStatus } from '../types';
|
||||
import { ExperimentListItem, ExperimentType, ExperimentStatus, CombinedKPIs } from '../types';
|
||||
import { listExperiments, deleteExperiment, getExperimentStatusColor, getExperimentTypeColor, getExperimentIcon } from '../services/experiments';
|
||||
import MultiSelectDropdown from './MultiSelectDropdown';
|
||||
import { ExperimentBadge, ExperimentStatusBadge, ExperimentTypeBadge } from './ExperimentBadge';
|
||||
import { LineChart, Line, ResponsiveContainer, YAxis } from 'recharts';
|
||||
|
||||
const getChartMetricColor = (metric: string) => {
|
||||
switch (metric) {
|
||||
case 'cvr': return '#34d399'; // emerald
|
||||
case 'gv': return '#a78bfa'; // purple
|
||||
case 'acos': return '#fb7185'; // rose
|
||||
case 'revenue': return '#fbbf24'; // amber
|
||||
default: return '#818cf8'; // indigo for units
|
||||
}
|
||||
};
|
||||
|
||||
const ExperimentSparkline: React.FC<{ experiment: ExperimentListItem; salesData: CombinedKPIs[] }> = ({ experiment, salesData }) => {
|
||||
const chartData = useMemo(() => {
|
||||
if (!experiment.asins || !salesData.length) return [];
|
||||
|
||||
const asinSet = new Set(experiment.asins.map(a => a.toUpperCase()));
|
||||
const weeklyMap = new Map<string, any>();
|
||||
|
||||
salesData.forEach(r => {
|
||||
const asin = (r.asin || '').toUpperCase();
|
||||
if (!asinSet.has(asin)) return;
|
||||
|
||||
const weekNum = r.week || 1;
|
||||
const yearStr = r.year || new Date().getFullYear();
|
||||
const weekStr = `${yearStr}-W${String(weekNum).padStart(2, '0')}`;
|
||||
|
||||
if (!weeklyMap.has(weekStr)) {
|
||||
const d = new Date(yearStr, 0, 1 + (weekNum - 1) * 7);
|
||||
weeklyMap.set(weekStr, {
|
||||
name: weekStr,
|
||||
timestamp: d.getTime(),
|
||||
units: 0,
|
||||
revenue: 0,
|
||||
gv: 0,
|
||||
spend: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const w = weeklyMap.get(weekStr);
|
||||
w.units += r.unitsTotal || 0;
|
||||
w.revenue += r.salesTotal || 0;
|
||||
w.gv += r.glanceViews || 0;
|
||||
w.spend += r.cost || 0;
|
||||
});
|
||||
|
||||
const arr = Array.from(weeklyMap.values()).map(w => ({
|
||||
...w,
|
||||
cvr: w.gv > 0 ? (w.units / w.gv) * 100 : 0,
|
||||
acos: w.revenue > 0 ? (w.spend / w.revenue) * 100 : 0
|
||||
})).sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
return arr.slice(-12);
|
||||
}, [experiment, salesData]);
|
||||
|
||||
if (chartData.length < 2) return <div className="h-8 w-16 bg-slate-800/50 rounded animate-pulse" />;
|
||||
|
||||
const metric = experiment.primary_metric || 'units';
|
||||
|
||||
return (
|
||||
<div className="h-10 w-24">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={chartData}>
|
||||
<YAxis domain={['dataMin', 'dataMax']} hide />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey={metric === 'bsr' ? 'units' : metric === 'ctr' ? 'gv' : metric}
|
||||
stroke={getChartMetricColor(metric)}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ExperimentsViewProps {
|
||||
onOpenDetail: (experimentId: string) => void;
|
||||
onOpenCreate: () => void;
|
||||
salesData?: CombinedKPIs[];
|
||||
}
|
||||
|
||||
const ExperimentsView: React.FC<ExperimentsViewProps> = ({ onOpenDetail, onOpenCreate }) => {
|
||||
const ExperimentsView: React.FC<ExperimentsViewProps> = ({ onOpenDetail, onOpenCreate, salesData = [] }) => {
|
||||
const [experiments, setExperiments] = useState<ExperimentListItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -57,7 +134,7 @@ const ExperimentsView: React.FC<ExperimentsViewProps> = ({ onOpenDetail, onOpenC
|
||||
const handleDelete = async (id: string, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm('Are you sure you want to delete this experiment?')) return;
|
||||
|
||||
|
||||
try {
|
||||
await deleteExperiment(id);
|
||||
loadExperiments();
|
||||
@@ -78,7 +155,7 @@ const ExperimentsView: React.FC<ExperimentsViewProps> = ({ onOpenDetail, onOpenC
|
||||
active: experiments.filter(e => e.status === 'active').length,
|
||||
planned: experiments.filter(e => e.status === 'planned').length,
|
||||
avgLift: experiments.filter(e => e.actual_lift_percent !== undefined)
|
||||
.reduce((sum, e) => sum + (e.actual_lift_percent || 0), 0) /
|
||||
.reduce((sum, e) => sum + (e.actual_lift_percent || 0), 0) /
|
||||
Math.max(1, experiments.filter(e => e.actual_lift_percent !== undefined).length),
|
||||
}), [experiments]);
|
||||
|
||||
@@ -192,7 +269,7 @@ const ExperimentsView: React.FC<ExperimentsViewProps> = ({ onOpenDetail, onOpenC
|
||||
<th className="text-left text-xs font-medium text-slate-400 uppercase tracking-wider px-4 py-3">ASINs</th>
|
||||
<th className="text-left text-xs font-medium text-slate-400 uppercase tracking-wider px-4 py-3">Duration</th>
|
||||
<th className="text-left text-xs font-medium text-slate-400 uppercase tracking-wider px-4 py-3">Progress</th>
|
||||
<th className="text-left text-xs font-medium text-slate-400 uppercase tracking-wider px-4 py-3">Lift</th>
|
||||
<th className="text-left text-xs font-medium text-slate-400 uppercase tracking-wider px-4 py-3">Impact</th>
|
||||
<th className="text-left text-xs font-medium text-slate-400 uppercase tracking-wider px-4 py-3">Owner</th>
|
||||
<th className="text-right text-xs font-medium text-slate-400 uppercase tracking-wider px-4 py-3">Actions</th>
|
||||
</tr>
|
||||
@@ -241,13 +318,21 @@ const ExperimentsView: React.FC<ExperimentsViewProps> = ({ onOpenDetail, onOpenC
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{exp.actual_lift_percent !== undefined ? (
|
||||
<span className={`text-sm font-medium ${exp.actual_lift_percent >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{exp.actual_lift_percent >= 0 ? '+' : ''}{exp.actual_lift_percent}%
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-slate-500">—</span>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
{salesData.length > 0 && <ExperimentSparkline experiment={exp} salesData={salesData} />}
|
||||
<div className="w-16">
|
||||
{exp.actual_lift_percent !== undefined ? (
|
||||
<span className={`text-sm font-bold ${exp.actual_lift_percent >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{exp.actual_lift_percent >= 0 ? '+' : ''}{exp.actual_lift_percent}%
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-slate-500 font-medium">—</span>
|
||||
)}
|
||||
<div className="text-[10px] text-slate-500 capitalize leading-none mt-0.5" style={{ color: getChartMetricColor(exp.primary_metric || 'units') }}>
|
||||
{exp.primary_metric || 'units'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-slate-400">{exp.owner || '—'}</span>
|
||||
@@ -303,4 +388,6 @@ const StatCard: React.FC<{ label: string; value: string | number; color: string
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default ExperimentsView;
|
||||
|
||||
Reference in New Issue
Block a user