diff --git a/components/AdvertisingDashboard.tsx b/components/AdvertisingDashboard.tsx index 081926e..f9aa9fb 100644 --- a/components/AdvertisingDashboard.tsx +++ b/components/AdvertisingDashboard.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; -import { CombinedKPIs } from './types'; +import { CombinedKPIs } from '../types'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, Legend, ComposedChart, Area diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx index 6ddb097..2807f3c 100644 --- a/components/ErrorBoundary.tsx +++ b/components/ErrorBoundary.tsx @@ -47,7 +47,7 @@ class ErrorBoundary extends Component { ); } - return this.props.children; + return (this as any).props.children; } } diff --git a/components/ExperimentDetail.tsx b/components/ExperimentDetail.tsx deleted file mode 100644 index 33f2b19..0000000 --- a/components/ExperimentDetail.tsx +++ /dev/null @@ -1,712 +0,0 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { Experiment, ExperimentCreateInput, ExperimentType, ExperimentStatus, ExperimentMetric, CombinedKPIs } from '../types'; -import { getExperiment, updateExperiment, deleteExperiment, calculateExperimentPerformance, getExperimentAsins } from '../services/experiments'; -import { ExperimentStatusBadge, ExperimentTypeBadge } from './ExperimentBadge'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, ReferenceLine } from 'recharts'; - -interface ExperimentDetailProps { - experimentId: string | null; - onClose: () => void; - salesData?: any[]; -} - -const ExperimentDetail: React.FC = ({ experimentId, onClose, salesData = [] }) => { - const [experiment, setExperiment] = useState(null); - const [loading, setLoading] = useState(false); - const [editing, setEditing] = useState(false); - const [formData, setFormData] = useState>({}); - - const loadExperiment = useCallback(async () => { - if (!experimentId) return; - setLoading(true); - try { - const data = await getExperiment(experimentId); - setExperiment(data); - setFormData(data || {}); - } catch (e: any) { - console.error('Failed to load experiment:', e); - } finally { - setLoading(false); - } - }, [experimentId]); - - useEffect(() => { - if (experimentId) { - loadExperiment(); - } - }, [experimentId, loadExperiment]); - - const handleSave = async () => { - if (!experimentId) return; - setLoading(true); - try { - const updated = await updateExperiment(experimentId, formData); - setExperiment(updated); - setEditing(false); - } catch (e: any) { - alert(`Error saving: ${e.message}`); - } finally { - setLoading(false); - } - }; - - const handleCalculateResults = async () => { - if (!experiment || !salesData.length) return; - setLoading(true); - try { - const perf = await calculateExperimentPerformance(experiment, salesData); - console.log('Calculation yielded performance delta payload:', perf); - const updated = await updateExperiment(experiment.id, perf); - console.log('Update return from backend:', updated); - setExperiment(updated); - } catch (e: any) { - console.error('Full calculation error:', e); - alert(`Error calculating results: ${e.message}`); - } finally { - setLoading(false); - } - }; - - 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) => { - if (!d) return new Date(); - 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(); - - 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 mkt = (r.marketplace || (r as any).customer || '').toLowerCase(); - const isMarket = !experiment.marketplace || experiment.marketplace === 'All' || mkt.includes(experiment.marketplace.toLowerCase()); - if (!isMarket) 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 ?? (r as any).units ?? 0); - d.revenue += (r.salesTotal ?? (r as any).sellOut ?? 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 = new Set(targetAsins); - const weeklyMap = new Map(); - - salesData.forEach(r => { - const asin = (r.asin || '').toUpperCase(); - if (!asinSet.has(asin)) return; - - const mkt = (r.marketplace || (r as any).customer || '').toLowerCase(); - const isMarket = !experiment.marketplace || experiment.marketplace === 'All' || mkt.includes(experiment.marketplace.toLowerCase()); - if (!isMarket) 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 ?? (r as any).units ?? 0); - w.revenue += (r.salesTotal ?? (r as any).sellOut ?? 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(-16); // Let's keep the last 16 weeks to ensure enough window before and during - }, [experiment, salesData]); - - 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 formatChartMetric = (val: number, metric: string) => { - if (metric === 'cvr' || metric === 'acos' || metric === 'ctr') return `${val.toFixed(1)}%`; - if (metric === 'revenue' || metric === 'cost') return `€${Math.round(val).toLocaleString('de-DE')}`; - return Math.round(val).toLocaleString('de-DE'); - }; - - if (!experimentId) return null; - - return ( -
-
- {/* Header */} -
-
- {editing ? ( - setFormData({ ...formData, name: e.target.value })} - className="text-xl font-bold bg-slate-800 border border-slate-600 rounded-lg px-3 py-1 text-white" - /> - ) : ( -

{experiment?.name || 'Loading...'}

- )} - {experiment && ( -
- - -
- )} -
-
- {editing ? ( - <> - - - - ) : ( - <> - - - - )} -
-
- - {/* Content */} -
- {loading && !experiment ? ( -
-
-
- ) : experiment ? ( -
- {/* Performance Cards */} - {(experiment.experiment_units != null) && ( - <> -
-
- Período Baseline (Referencia) - - {(() => { - const sd = new Date(experiment.start_date); - const ed = experiment.end_date ? new Date(experiment.end_date) : new Date(); - const durationMs = ed.getTime() - sd.getTime(); - const bd = new Date(sd.getTime() - durationMs); - return `${bd.toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' })} — ${sd.toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' })}`; - })()} - -
-
VS
-
- Período del Experimento - - {(() => { - const sd = new Date(experiment.start_date); - const ed = experiment.end_date ? new Date(experiment.end_date) : new Date(); - return `${sd.toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' })} — ${ed.toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' })}`; - })()} - -
-
- -
- - - - - - - = 0 ? '+' : ''}${experiment.actual_lift_percent.toFixed(1)}%` - : '—'} - color={experiment.actual_lift_percent != null && experiment.actual_lift_percent >= 0 ? 'emerald' : 'red'} - /> - -
- - )} - - {/* Calculate Results Button */} - {salesData.length > 0 && ( - - )} - - {/* Dynamic Metric Chart */} - {chartData.length > 0 && ( -
-
-

- Timeline Impact ({(experiment.primary_metric || 'units').toUpperCase()}) -

-
-
- - - - val.split('-')[1]} // Just show W12 - /> - formatChartMetric(val, experiment.primary_metric)} - width={60} - /> - { - if (experiment.primary_metric === 'cvr') return [`${value.toFixed(1)}%`, 'CVR']; - if (experiment.primary_metric === 'acos') return [`${value.toFixed(1)}%`, 'ACOS']; - if (experiment.primary_metric === 'revenue') return [`€${value.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`, 'Rev']; - return [value.toLocaleString('de-DE'), 'Units']; - }} - /> - {/* Reference Line for Start Date */} - { - const dDate = new Date(experiment.start_date); - const jan1 = new Date(dDate.getFullYear(), 0, 1); - const dayOfYear = Math.floor((dDate.getTime() - jan1.getTime()) / 86400000) + 1; - const wN = Math.ceil((dayOfYear + jan1.getDay()) / 7); - return `${dDate.getFullYear()}-W${String(wN).padStart(2, '0')}`; - })()} - stroke="#e2e8f0" - strokeDasharray="3 3" - label={{ position: 'top', value: 'Started', fill: '#e2e8f0', fontSize: 10 }} - /> - - - -
-
- )} - - {/* Details Grid */} -
- {/* Left Column */} -
- - {editing ? ( - <> -