feat: redesign dashboard layout and header tabs to match CraftAlley V2 mockup

This commit is contained in:
Christian Vidal Wolf
2026-06-17 11:29:49 +02:00
parent 41ad8d3598
commit 3cb986bd90
3 changed files with 554 additions and 399 deletions
+472 -331
View File
@@ -445,21 +445,6 @@ const Dashboard: React.FC<DashboardProps> = ({
const [top10Metric, setTop10Metric] = useState<'sellOut' | 'units'>('sellOut');
const [cumulativeMetric, setCumulativeMetric] = useState<'sellOut' | 'units'>('sellOut');
const cumulativeData = useMemo(() => {
const source = cumulativeMetric === 'sellOut' ? data.seasonality : data.seasonalityUnits;
const years = data.availableYears;
const running: Record<string, number> = {};
years.forEach(y => { running[y] = 0; });
return source.map(point => {
const result: Record<string, number | string> = { name: point.name };
years.forEach(y => {
running[y] = (running[y] || 0) + ((point[y] as number) || 0);
result[y] = running[y];
});
return result;
});
}, [data.seasonality, data.seasonalityUnits, data.availableYears, cumulativeMetric]);
// Calculate Ads KPIs by Year
const adsKPIsByYear = useMemo(() => {
if (!adsData || adsData.length === 0) return null;
@@ -472,11 +457,6 @@ const Dashboard: React.FC<DashboardProps> = ({
yearMap.set(y, { cost: 0, attributedSales: 0, clicks: 0, impressions: 0 });
}
const t = yearMap.get(y)!;
if (ad.year === 2026) {
console.log("DASH 2026 W" + ad.week, ad.cost);
}
t.cost += ad.cost || 0;
t.attributedSales += ad.attributedSales30d;
t.clicks += ad.clicks || 0;
@@ -503,357 +483,518 @@ const Dashboard: React.FC<DashboardProps> = ({
}, [adsKPIsByYear]);
// Decide which data source to use for Product Line charts
// If contextData is provided (drill down), we use that to show the "Total Line" view.
// Otherwise we use the standard filtered data.
const displayData = contextData || data;
// Calculate dynamic height for the All Product Lines chart to enable scrolling
// Assume ~60px per product line to give it enough space, minimum 300px
const chartHeight = Math.max(displayData.topLinesSplit.length * 60, 300);
// Dynamic stats computation for KPI cards to match mockup V2 layout
const renderKPICardData = (metric: 'sellOut' | 'units') => {
const sorted = [...data.availableYears].sort((a, b) => parseInt(b) - parseInt(a));
const currentYear = sorted[0];
const prevYear = sorted[1];
const twoYearsPrior = sorted[2];
const currentVal = currentYear && data.totalsByYear[currentYear] ? data.totalsByYear[currentYear][metric] : 0;
const prevVal = prevYear && data.totalsByYear[prevYear] ? data.totalsByYear[prevYear][metric] : 0;
const priorVal = twoYearsPrior && data.totalsByYear[twoYearsPrior] ? data.totalsByYear[twoYearsPrior][metric] : 0;
const formatVal = (v: number) => {
if (metric === 'sellOut') {
return `${v.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
}
return v.toLocaleString('de-DE');
};
const formatDiff = (curr: number, prev: number) => {
const diff = curr - prev;
const sign = diff >= 0 ? '+' : '';
if (metric === 'sellOut') {
return `${sign}${diff.toLocaleString('de-DE', { maximumFractionDigits: 0 })}`;
}
return `${sign}${diff.toLocaleString('de-DE')}`;
};
let mainGrowthPercent = '';
if (prevVal > 0) {
const pct = ((currentVal - prevVal) / prevVal) * 100;
mainGrowthPercent = `${pct >= 0 ? '+' : ''}${pct.toFixed(1)}%`;
}
let prevGrowthPercent = '';
if (priorVal > 0) {
const pct = ((prevVal - priorVal) / priorVal) * 100;
prevGrowthPercent = `${pct >= 0 ? '+' : ''}${pct.toFixed(1)}%`;
}
return {
currentYear,
prevYear,
twoYearsPrior,
currentVal: formatVal(currentVal),
prevVal: formatVal(prevVal),
priorVal: formatVal(priorVal),
diffLabel: `${formatDiff(currentVal, prevVal)} vs año anterior`,
mainGrowthPercent,
prevGrowthPercent
};
};
const sellOutStats = renderKPICardData('sellOut');
const unitsStats = renderKPICardData('units');
// Dynamic color coding based on year relevance
const getYearColor = (year: string | number) => {
const sorted = [...data.availableYears].sort((a, b) => parseInt(b) - parseInt(a));
const index = sorted.indexOf(year.toString());
if (index === 0) return '#06b6d4'; // Cyan (current year, e.g., 2025/2026)
if (index === 1) return '#6366f1'; // Indigo (previous year, e.g., 2024/2025)
if (index === 2) return '#64748b'; // Muted Grey (two years prior)
return COLORS[index % COLORS.length] || '#64748b';
};
const cumulativeData = useMemo(() => {
const source = cumulativeMetric === 'sellOut' ? data.seasonality : data.seasonalityUnits;
const years = data.availableYears;
const running: Record<string, number> = {};
years.forEach(y => { running[y] = 0; });
return source.map(point => {
const result: Record<string, number | string> = { name: point.name };
years.forEach(y => {
running[y] = (running[y] || 0) + ((point[y] as number) || 0);
result[y] = running[y];
});
return result;
});
}, [data.seasonality, data.seasonalityUnits, data.availableYears, cumulativeMetric]);
return (
<div className="p-3 md:p-6 space-y-4 md:space-y-6 max-w-7xl mx-auto animate-fade-in pb-24">
<div className="p-3 md:p-6 space-y-6 max-w-7xl mx-auto animate-fade-in pb-24">
{/* Ads Performance Section - Overhauled Layout to match Mockup */}
{adsKPIsByYear && availableAdsYears.length > 0 && (
<div className="bg-slate-900/40 border border-slate-800 rounded-2xl p-5 animate-fade-in shadow-xl">
<div className="flex items-center gap-2 mb-4 border-b border-slate-800/80 pb-3">
<div className="relative">
<span className="absolute inset-0 bg-fuchsia-400 rounded-full animate-ping opacity-20"></span>
<span className="relative block w-2 h-2 rounded-full bg-fuchsia-400 shadow-[0_0_8px_rgba(232,121,249,0.8)]"></span>
{/* Ads Performance Section - Split Actual vs Anterior */}
{adsKPIsByYear && availableAdsYears.length > 0 && (() => {
const currentYear = availableAdsYears[0];
const prevYear = availableAdsYears[1];
const twoYearsPrior = availableAdsYears[2];
const currentKpi = adsKPIsByYear[currentYear];
const prevKpi = prevYear ? adsKPIsByYear[prevYear] : null;
const priorKpi = twoYearsPrior ? adsKPIsByYear[twoYearsPrior] : null;
const renderAdsGrowth = (currentVal: number, prevVal: number | undefined, isPercentage: boolean = false, inverse: boolean = false) => {
if (!prevVal || prevVal === 0) return <span className="text-slate-500 font-medium">-</span>;
const diff = currentVal - prevVal;
if (isPercentage) {
const pct = (diff / prevVal) * 100;
const isPos = pct >= 0;
const color = inverse ? (isPos ? 'text-red-400' : 'text-emerald-400') : (isPos ? 'text-emerald-400' : 'text-red-400');
return <span className={`text-[10px] font-bold ${color}`}>{isPos ? '+' : ''}{pct.toFixed(1)}%</span>;
} else {
// For ACOS diff in percentage points
const isPos = diff >= 0;
const color = inverse ? (isPos ? 'text-red-400' : 'text-emerald-400') : (isPos ? 'text-emerald-400' : 'text-red-400');
return <span className={`text-[10px] font-bold ${color}`}>{isPos ? '+' : ''}{diff.toFixed(2)}pp</span>;
}
};
const renderRoasDiff = (currentVal: number, prevVal: number | undefined) => {
if (!prevVal || prevVal === 0) return <span className="text-slate-500 font-medium">-</span>;
const diff = currentVal - prevVal;
const isPos = diff >= 0;
return <span className={`text-[10px] font-bold ${isPos ? 'text-emerald-400' : 'text-red-400'}`}>{isPos ? '+' : ''}{diff.toFixed(2)}</span>;
};
return (
<div className="bg-slate-900/40 border border-slate-800 rounded-2xl p-5 shadow-xl">
<div className="flex items-center gap-2 mb-4 border-b border-slate-800/80 pb-3">
<span className="w-2 h-2 rounded-full bg-cyan-400 shadow-[0_0_8px_rgba(34,211,238,0.8)]"></span>
<h3 className="text-xs font-black text-slate-300 uppercase tracking-widest">Advertising Performance</h3>
<div className="ml-auto text-[10px] text-slate-500 font-medium">
{adsData.length.toLocaleString('de-DE')} registros
</div>
</div>
<h3 className="text-xs font-black text-fuchsia-400 uppercase tracking-widest">Advertising Performance</h3>
<div className="ml-auto px-2.5 py-0.5 bg-slate-950 rounded border border-slate-800 text-[10px] text-slate-400 font-bold uppercase tracking-wider">
{adsData.length.toLocaleString('de-DE')} Records
</div>
</div>
<div className={`grid gap-4 ${availableAdsYears.length > 1 ? 'grid-cols-1 lg:grid-cols-2' : 'grid-cols-1'}`}>
{availableAdsYears.map((year, idx) => {
const kpi = adsKPIsByYear[year];
const prevYear = availableAdsYears[idx + 1];
const prevKpi = prevYear ? adsKPIsByYear[prevYear] : null;
const renderGrowth = (current: number, previous: number | undefined, inverse: boolean = false) => {
if (!previous || previous === 0) return null;
const pct = ((current - previous) / previous) * 100;
const isPositive = pct >= 0;
const colorClass = inverse
? (isPositive ? 'text-red-400' : 'text-emerald-400')
: (isPositive ? 'text-emerald-400' : 'text-red-400');
return (
<span className={`text-[10px] font-bold ml-1 ${colorClass} whitespace-nowrap`}>
{isPositive ? '▲' : '▼'}{Math.abs(pct).toFixed(0)}%
</span>
);
};
return (
<div key={year} className="relative bg-slate-950/30 rounded-xl p-4 border border-white/5">
<div className="flex items-center gap-2 mb-3">
<span className="text-[10px] font-black text-slate-400 font-mono bg-slate-900 px-2 py-0.5 rounded border border-slate-800">{year}</span>
<div className="h-[1px] flex-1 bg-gradient-to-r from-slate-800/80 to-transparent"></div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 divide-y lg:divide-y-0 lg:divide-x divide-slate-800">
{/* Período Actual */}
<div className="pb-4 lg:pb-0 lg:pr-6">
<div className="bg-slate-800/40 px-3 py-1.5 rounded text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-4">
Período Actual ({currentYear})
</div>
<div className="grid grid-cols-4 gap-2">
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Sell Out</span>
<span className="text-base font-bold text-white block">
{currentKpi.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{renderAdsGrowth(currentKpi.totalSpend, prevKpi?.totalSpend, true)}
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{/* Ad Spend */}
<div className="group">
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Spend</span>
<div className="flex items-baseline overflow-hidden">
<span className="text-base font-bold text-fuchsia-400 truncate">
{kpi.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{renderGrowth(kpi.totalSpend, prevKpi?.totalSpend)}
</div>
</div>
{/* Attributed Sales */}
<div className="group">
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Sales</span>
<div className="flex items-baseline overflow-hidden">
<span className="text-base font-bold text-emerald-400 truncate">
{kpi.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{renderGrowth(kpi.attributedSales, prevKpi?.attributedSales)}
</div>
</div>
{/* ACOS */}
<div className="group">
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">ACOS</span>
<div className="flex items-baseline overflow-hidden">
<span className="text-base font-bold text-emerald-400 truncate">
{kpi.acos.toFixed(1)}%
</span>
{renderGrowth(kpi.acos, prevKpi?.acos, true)}
</div>
</div>
{/* ROAS */}
<div className="group">
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">ROAS</span>
<div className="flex items-baseline overflow-hidden">
<span className="text-base font-bold text-emerald-400 truncate">
{kpi.roas.toFixed(2)}x
</span>
{renderGrowth(kpi.roas, prevKpi?.roas)}
</div>
</div>
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Spend</span>
<span className="text-base font-bold text-white block">
{currentKpi.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{renderAdsGrowth(currentKpi.attributedSales, prevKpi?.attributedSales, true)}
</div>
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Acos</span>
<span className="text-base font-bold text-white block">
{currentKpi.acos.toFixed(1)}%
</span>
{renderAdsGrowth(currentKpi.acos, prevKpi?.acos, false, true)}
</div>
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Roas</span>
<span className="text-base font-bold text-white block">
{currentKpi.roas.toFixed(2)}x
</span>
{renderRoasDiff(currentKpi.roas, prevKpi?.roas)}
</div>
</div>
);
})}
</div>
{/* Período Anterior */}
<div className="pt-4 lg:pt-0 lg:pl-6">
<div className="bg-slate-800/40 px-3 py-1.5 rounded text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-4">
Período Anterior ({prevYear || 'N/A'})
</div>
{prevKpi ? (
<div className="grid grid-cols-4 gap-2">
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Sell Out</span>
<span className="text-base font-bold text-white block">
{prevKpi.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{renderAdsGrowth(prevKpi.totalSpend, priorKpi?.totalSpend, true)}
</div>
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Spend</span>
<span className="text-base font-bold text-white block">
{prevKpi.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{renderAdsGrowth(prevKpi.attributedSales, priorKpi?.attributedSales, true)}
</div>
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Acos</span>
<span className="text-base font-bold text-white block">
{prevKpi.acos.toFixed(1)}%
</span>
{renderAdsGrowth(prevKpi.acos, priorKpi?.acos, false, true)}
</div>
<div>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Roas</span>
<span className="text-base font-bold text-white block">
{prevKpi.roas.toFixed(2)}x
</span>
{renderRoasDiff(prevKpi.roas, priorKpi?.roas)}
</div>
</div>
) : (
<div className="text-sm text-slate-500 italic py-2">No hay datos históricos disponibles</div>
)}
</div>
</div>
</div>
);
})()}
{/* KPI Section with CraftAlley Card Layout */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Sell Out Revenue Card */}
<div className="bg-slate-900/40 border border-slate-800 rounded-xl p-5 md:p-6 flex flex-col justify-between shadow-lg relative">
<div className="flex justify-between items-start mb-2">
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Sell Out Revenue</h3>
{sellOutStats.mainGrowthPercent && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
{sellOutStats.mainGrowthPercent}
</span>
)}
</div>
<div className="mb-4">
<span className="text-4xl font-extrabold text-white tracking-tight leading-none">
{sellOutStats.currentVal}
</span>
<p className="text-xs text-emerald-400 font-semibold mt-2">{sellOutStats.diffLabel}</p>
</div>
<div className="border-t border-slate-800/80 pt-4 mt-2">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-[10px] text-slate-500 uppercase tracking-wider block mb-1">Año Anterior ({sellOutStats.prevYear})</span>
<span className="text-lg font-bold text-slate-200 block">{sellOutStats.prevVal}</span>
<span className="text-xs text-emerald-400 font-bold">{sellOutStats.prevGrowthPercent}</span>
</div>
<div>
<span className="text-[10px] text-slate-500 uppercase tracking-wider block mb-1">Hace 2 Años ({sellOutStats.twoYearsPrior || 'N/A'})</span>
<span className="text-lg font-bold text-slate-200 block">{sellOutStats.priorVal}</span>
<span className="text-xs text-slate-500 font-medium">referencia</span>
</div>
</div>
</div>
</div>
)}
{/* KPI Section - Pass both specific data and context data */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<MultiYearKPICard
title="Sell Out Revenue"
metric="sellOut"
data={data.totalsByYear}
availableYears={data.availableYears}
contextData={contextData ? contextData.totalsByYear : undefined}
/>
<MultiYearKPICard
title="Units Sold"
metric="units"
data={data.totalsByYear}
availableYears={data.availableYears}
contextData={contextData ? contextData.totalsByYear : undefined}
/>
{/* Units Sold Card */}
<div className="bg-slate-900/40 border border-slate-800 rounded-xl p-5 md:p-6 flex flex-col justify-between shadow-lg relative">
<div className="flex justify-between items-start mb-2">
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Units Sold</h3>
{unitsStats.mainGrowthPercent && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
{unitsStats.mainGrowthPercent}
</span>
)}
</div>
<div className="mb-4">
<span className="text-4xl font-extrabold text-white tracking-tight leading-none">
{unitsStats.currentVal}
</span>
<p className="text-xs text-emerald-400 font-semibold mt-2">{unitsStats.diffLabel}</p>
</div>
<div className="border-t border-slate-800/80 pt-4 mt-2">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-[10px] text-slate-500 uppercase tracking-wider block mb-1">Año Anterior ({unitsStats.prevYear})</span>
<span className="text-lg font-bold text-slate-200 block">{unitsStats.prevVal}</span>
<span className="text-xs text-emerald-400 font-bold">{unitsStats.prevGrowthPercent}</span>
</div>
<div>
<span className="text-[10px] text-slate-500 uppercase tracking-wider block mb-1">Hace 2 Años ({unitsStats.twoYearsPrior || 'N/A'})</span>
<span className="text-lg font-bold text-slate-200 block">{unitsStats.priorVal}</span>
<span className="text-xs text-slate-500 font-medium">referencia</span>
</div>
</div>
</div>
</div>
</div>
{/* Main Grid matching Mockup Columns */}
{/* Row 1 Grid: Product Lines & Seasonality side-by-side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Column */}
<div className="space-y-6 flex flex-col">
{/* All Product Lines Revenue Chart */}
<ExpandableCard title={contextData ? "Total Product Line Performance (Context)" : "Product Lines (Revenue)"} className="h-96">
<div className="flex flex-col h-full">
<div className="flex justify-between items-center mb-2">
{contextData && <span className="text-xs text-indigo-400 font-semibold uppercase tracking-wider">Showing Full Product Line Data</span>}
<div className="bg-slate-900 p-1 rounded-lg border border-slate-800 flex text-xs ml-auto">
<button
onClick={(e) => { e.stopPropagation(); setTop10Metric('sellOut'); }}
className={`px-3 py-1 rounded-md transition-colors ${top10Metric === 'sellOut' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Sell Out ()
</button>
<button
onClick={(e) => { e.stopPropagation(); setTop10Metric('units'); }}
className={`px-3 py-1 rounded-md transition-colors ${top10Metric === 'units' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Units
</button>
</div>
</div>
{/* Scrollable Container */}
<div className="flex-1 min-h-0 overflow-y-auto pr-2 custom-scrollbar">
<div style={{ height: `${chartHeight}px` }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={displayData.topLinesSplit} layout="vertical" margin={{ left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" horizontal={false} />
<XAxis
type="number"
stroke="#64748b"
tickFormatter={(val) => top10Metric === 'sellOut' ? `${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')}
orientation='top'
/>
<YAxis dataKey="name" type="category" width={100} stroke="#94a3b8" tick={{ fontSize: 12 }} />
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric={top10Metric} />} cursor={{ fill: '#1e293b' }} />
{displayData.availableYears.map((year, index) => (
<Bar
key={year}
dataKey={`${year}_${top10Metric === 'sellOut' ? 'value' : 'units'}`}
name={year}
fill={getYearColor(year)}
radius={[0, 4, 4, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
{/* Horizontal Bar Chart (Product Lines Revenue) */}
<ExpandableCard title={contextData ? "Total Product Line Performance (Context)" : "Product Lines - Revenue"} className="h-96">
<div className="flex flex-col h-full">
<div className="flex justify-between items-center mb-2">
{contextData && <span className="text-xs text-indigo-400 font-semibold uppercase tracking-wider">Showing Full Product Line Data</span>}
<div className="bg-slate-900 p-1 rounded-lg border border-slate-800 flex text-xs ml-auto">
<button
onClick={(e) => { e.stopPropagation(); setTop10Metric('sellOut'); }}
className={`px-3 py-1 rounded-md transition-colors ${top10Metric === 'sellOut' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Sell Out ()
</button>
<button
onClick={(e) => { e.stopPropagation(); setTop10Metric('units'); }}
className={`px-3 py-1 rounded-md transition-colors ${top10Metric === 'units' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Units
</button>
</div>
</div>
</ExpandableCard>
{/* Growth Table */}
<div className="h-72">
<GrowthTable
title={contextData ? "Fastest Growing Lines (Full Line Context)" : "Fastest Growing Lines (YoY - €)"}
data={displayData.topMovers}
type="growth"
periods={displayData.comparisonPeriods}
/>
</div>
{/* Decline Table */}
<div className="h-72">
<GrowthTable
title={contextData ? "Declining Lines (Full Line Context)" : "Declining Lines (YoY - €)"}
data={displayData.bottomMovers}
type="decline"
periods={displayData.comparisonPeriods}
/>
</div>
</div>
{/* Right Column */}
<div className="space-y-6">
{/* Units Chart (Split by Year) */}
<ExpandableCard title={contextData ? "Total Product Line Units (Context)" : "Units Sold by Product Line (Overview)"} className="h-96">
<div className="flex flex-col h-full">
{contextData && <div className="text-xs text-indigo-400 font-semibold uppercase tracking-wider mb-2 text-right">Showing Full Product Line Data</div>}
<div className="flex-1">
<div className="flex-1 min-h-0 overflow-y-auto pr-2 custom-scrollbar">
<div style={{ height: `${chartHeight}px` }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={displayData.byLineOverviewSplit} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis dataKey="name" stroke="#64748b" tick={{ fontSize: 10 }} interval={0} angle={-15} textAnchor="end" height={60} />
<YAxis
<BarChart data={displayData.topLinesSplit} layout="vertical" margin={{ left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" horizontal={false} />
<XAxis
type="number"
stroke="#64748b"
tickFormatter={(val) => {
if (val >= 1000000) return `${(val / 1000000).toFixed(1)}M`;
if (val >= 1000) return `${(val / 1000).toFixed(0)}k`;
return val;
}}
tickFormatter={(val) => top10Metric === 'sellOut' ? `${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')}
orientation='top'
/>
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric="units" />} cursor={{ fill: '#1e293b' }} />
<YAxis dataKey="name" type="category" width={100} stroke="#94a3b8" tick={{ fontSize: 12 }} />
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric={top10Metric} />} cursor={{ fill: '#1e293b' }} />
{displayData.availableYears.map((year, index) => (
<Bar
key={year}
dataKey={year}
dataKey={`${year}_${top10Metric === 'sellOut' ? 'value' : 'units'}`}
name={year}
fill={getYearColor(year)}
radius={[4, 4, 0, 0]}
radius={[0, 4, 4, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
</div>
</ExpandableCard>
</div>
</ExpandableCard>
{/* Seasonality Chart - ALWAYS uses specific filtered data 'data' */}
<ExpandableCard title="Monthly Sales Seasonality (Selected Items)" className="h-96">
<div className="flex flex-col h-full">
<div className="flex justify-end mb-2">
<div className="bg-slate-900 p-1 rounded-lg border border-slate-800 flex text-xs">
<button
onClick={(e) => { e.stopPropagation(); setSeasonalityMetric('sellOut'); }}
className={`px-3 py-1 rounded-md transition-colors ${seasonalityMetric === 'sellOut' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Sell Out ()
</button>
<button
onClick={(e) => { e.stopPropagation(); setSeasonalityMetric('units'); }}
className={`px-3 py-1 rounded-md transition-colors ${seasonalityMetric === 'units' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Units
</button>
</div>
</div>
<div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={seasonalityMetric === 'sellOut' ? data.seasonality : data.seasonalityUnits}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="name" stroke="#64748b" />
<YAxis
stroke="#64748b"
tickFormatter={(val) => seasonalityMetric === 'sellOut' ? `${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')}
/>
<Tooltip content={(props: any) => <SeasonalityTooltip {...props} metric={seasonalityMetric} />} />
<Legend />
{data.availableYears.map((year, index) => (
<Line
key={year}
type="monotone"
dataKey={year}
name={year}
stroke={getYearColor(year)}
strokeWidth={3}
dot={{ r: 4, strokeWidth: 2 }}
activeDot={{ r: 6 }}
/>
))}
</LineChart>
</ResponsiveContainer>
{/* Monthly Seasonality Chart */}
<ExpandableCard title="Estacionalidad Mensual - Ventas" className="h-96">
<div className="flex flex-col h-full">
<div className="flex justify-end mb-2">
<div className="bg-slate-900 p-1 rounded-lg border border-slate-800 flex text-xs">
<button
onClick={(e) => { e.stopPropagation(); setSeasonalityMetric('sellOut'); }}
className={`px-3 py-1 rounded-md transition-colors ${seasonalityMetric === 'sellOut' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Sell Out ()
</button>
<button
onClick={(e) => { e.stopPropagation(); setSeasonalityMetric('units'); }}
className={`px-3 py-1 rounded-md transition-colors ${seasonalityMetric === 'units' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Units
</button>
</div>
</div>
</ExpandableCard>
{/* Cumulative Sales Chart - YoY comparison month by month */}
<ExpandableCard title="Cumulative Sales YoY (Month by Month)" className="h-96">
<div className="flex flex-col h-full">
<div className="flex justify-end mb-2">
<div className="bg-slate-900 p-1 rounded-lg border border-slate-800 flex text-xs">
<button
onClick={(e) => { e.stopPropagation(); setCumulativeMetric('sellOut'); }}
className={`px-3 py-1 rounded-md transition-colors ${cumulativeMetric === 'sellOut' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Sell Out ()
</button>
<button
onClick={(e) => { e.stopPropagation(); setCumulativeMetric('units'); }}
className={`px-3 py-1 rounded-md transition-colors ${cumulativeMetric === 'units' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Units
</button>
</div>
</div>
<div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={cumulativeData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="name" stroke="#64748b" />
<YAxis
stroke="#64748b"
tickFormatter={(val) => cumulativeMetric === 'sellOut' ? `${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')}
/>
<Tooltip content={(props: any) => <SeasonalityTooltip {...props} metric={cumulativeMetric} />} />
<Legend />
{data.availableYears.map((year, index) => (
<Line
key={year}
type="monotone"
dataKey={year}
name={year}
stroke={getYearColor(year)}
strokeWidth={3}
dot={{ r: 4, strokeWidth: 2 }}
activeDot={{ r: 6 }}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
</div>
</ExpandableCard>
{/* Country Chart - Uses Specific Data 'data' usually, unless we want to broaden it. Kept specific for now. */}
<ExpandableCard title="Revenue Distribution by Customer" className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data.byCustomerSplit}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis dataKey="name" stroke="#64748b" />
<YAxis stroke="#64748b" tickFormatter={(val) => `${(val / 1000).toFixed(0)}k`} />
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric="sellOut" />} cursor={{ fill: '#1e293b' }} />
{data.availableYears.map((year, index) => (
<Bar
key={year}
dataKey={year}
name={year}
fill={getYearColor(year)}
radius={[4, 4, 0, 0]}
<div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={seasonalityMetric === 'sellOut' ? data.seasonality : data.seasonalityUnits}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="name" stroke="#64748b" />
<YAxis
stroke="#64748b"
tickFormatter={(val) => seasonalityMetric === 'sellOut' ? `${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')}
/>
))}
</BarChart>
</ResponsiveContainer>
</ExpandableCard>
<Tooltip content={(props: any) => <SeasonalityTooltip {...props} metric={seasonalityMetric} />} />
<Legend />
{data.availableYears.map((year, index) => (
<Line
key={year}
type="monotone"
dataKey={year}
name={year}
stroke={getYearColor(year)}
strokeWidth={3}
dot={{ r: 4, strokeWidth: 2 }}
activeDot={{ r: 6 }}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
</div>
</ExpandableCard>
</div>
{/* Row 2 Grid: Growth Tables side-by-side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Growth Table (Fastest Growing) */}
<div className="h-80">
<GrowthTable
title={contextData ? "Fastest Growing (Full Line Context)" : "Fastest Growing - YoY (€)"}
data={displayData.topMovers}
type="growth"
periods={displayData.comparisonPeriods}
/>
</div>
{/* Decline Table (Declining Lines) */}
<div className="h-80">
<GrowthTable
title={contextData ? "Declining Lines (Full Line Context)" : "Declining Lines - YoY (€)"}
data={displayData.bottomMovers}
type="decline"
periods={displayData.comparisonPeriods}
/>
</div>
</div>
{/* Row 3: Full-width Cumulative Sales YoY Chart */}
<div className="w-full">
<ExpandableCard title="Cumulative Sales YoY - Mensual" className="h-96">
<div className="flex flex-col h-full">
<div className="flex justify-end mb-2">
<div className="bg-slate-900 p-1 rounded-lg border border-slate-800 flex text-xs">
<button
onClick={(e) => { e.stopPropagation(); setCumulativeMetric('sellOut'); }}
className={`px-3 py-1 rounded-md transition-colors ${cumulativeMetric === 'sellOut' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Sell Out ()
</button>
<button
onClick={(e) => { e.stopPropagation(); setCumulativeMetric('units'); }}
className={`px-3 py-1 rounded-md transition-colors ${cumulativeMetric === 'units' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'}`}
>
Units
</button>
</div>
</div>
<div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={cumulativeData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="name" stroke="#64748b" />
<YAxis
stroke="#64748b"
tickFormatter={(val) => cumulativeMetric === 'sellOut' ? `${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')}
/>
<Tooltip content={(props: any) => <SeasonalityTooltip {...props} metric={cumulativeMetric} />} />
<Legend />
{data.availableYears.map((year, index) => (
<Line
key={year}
type="monotone"
dataKey={year}
name={year}
stroke={getYearColor(year)}
strokeWidth={3}
dot={{ r: 4, strokeWidth: 2 }}
activeDot={{ r: 6 }}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
</div>
</ExpandableCard>
</div>
{/* Keep secondary charts/details below main redesigned mockup */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Units Chart (Split by Year) */}
<ExpandableCard title={contextData ? "Total Product Line Units (Context)" : "Units Sold by Product Line (Overview)"} className="h-96">
<div className="flex flex-col h-full">
{contextData && <div className="text-xs text-indigo-400 font-semibold uppercase tracking-wider mb-2 text-right">Showing Full Product Line Data</div>}
<div className="flex-1">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={displayData.byLineOverviewSplit} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis dataKey="name" stroke="#64748b" tick={{ fontSize: 10 }} interval={0} angle={-15} textAnchor="end" height={60} />
<YAxis
stroke="#64748b"
tickFormatter={(val) => {
if (val >= 1000000) return `${(val / 1000000).toFixed(1)}M`;
if (val >= 1000) return `${(val / 1000).toFixed(0)}k`;
return val;
}}
/>
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric="units" />} cursor={{ fill: '#1e293b' }} />
{displayData.availableYears.map((year, index) => (
<Bar
key={year}
dataKey={year}
name={year}
fill={getYearColor(year)}
radius={[4, 4, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
</div>
</ExpandableCard>
{/* Country Chart */}
<ExpandableCard title="Revenue Distribution by Customer" className="h-96">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data.byCustomerSplit}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis dataKey="name" stroke="#64748b" />
<YAxis stroke="#64748b" tickFormatter={(val) => `${(val / 1000).toFixed(0)}k`} />
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric="sellOut" />} cursor={{ fill: '#1e293b' }} />
{data.availableYears.map((year, index) => (
<Bar
key={year}
dataKey={year}
name={year}
fill={getYearColor(year)}
radius={[4, 4, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
</ExpandableCard>
</div>
{/* Top Movers Table - Full Width */}
+8 -8
View File
@@ -51,30 +51,30 @@ const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options
</button>
{/* Filter dropdowns - always visible on desktop, toggle on mobile */}
<div className={`max-w-7xl mx-auto flex-wrap gap-2 md:gap-4 items-end ${isExpanded ? 'flex flex-col md:flex-row mt-3' : 'hidden md:flex md:flex-row'}`}>
<div className={`max-w-7xl mx-auto flex-wrap gap-2 md:gap-3 items-end ${isExpanded ? 'flex flex-col md:flex-row mt-3' : 'hidden md:flex md:flex-row'}`}>
<MultiSelectDropdown
label="Customer"
label="CUSTOMER"
selected={filters.customer}
options={options.customer}
onChange={(v) => onFilterChange('customer', v)}
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Year"
label="YEAR"
selected={filters.year}
options={options.year}
onChange={(v) => onFilterChange('year', v)}
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Month"
label="MONTH"
selected={filters.month}
options={options.month}
onChange={(v) => onFilterChange('month', v)}
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Week"
label="WEEK"
selected={filters.week}
options={options.week}
onChange={(v) => onFilterChange('week', v)}
@@ -82,7 +82,7 @@ const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options
enableRangeSelect={true}
/>
<MultiSelectDropdown
label="Product Line"
label="PRODUCT LINE"
selected={filters.line}
options={options.line}
onChange={(v) => onFilterChange('line', v)}
@@ -96,7 +96,7 @@ const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Title"
label="TITLE"
selected={filters.title}
options={options.title}
onChange={(v) => onFilterChange('title', v)}
@@ -110,7 +110,7 @@ const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Stock"
label="STOCK"
selected={filters.stock}
options={options.stock}
onChange={(v) => onFilterChange('stock', v)}