mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 17:35:23 +02:00
feat: redesign dashboard layout and header tabs to match CraftAlley V2 mockup
This commit is contained in:
@@ -58,6 +58,7 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
// Modal State
|
// Modal State
|
||||||
const [isDataModalOpen, setIsDataModalOpen] = useState(false);
|
const [isDataModalOpen, setIsDataModalOpen] = useState(false);
|
||||||
|
const [showFilters, setShowFilters] = useState(true);
|
||||||
|
|
||||||
const [filters, setFilters] = useState<FilterState>({
|
const [filters, setFilters] = useState<FilterState>({
|
||||||
customer: [],
|
customer: [],
|
||||||
@@ -691,97 +692,99 @@ const App: React.FC = () => {
|
|||||||
setRawData([]);
|
setRawData([]);
|
||||||
setAdsData([]);
|
setAdsData([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[App] Render Finish');
|
console.log('[App] Render Finish');
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col bg-background text-slate-200">
|
<div className="min-h-screen flex flex-col bg-background text-slate-200">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="bg-slate-950/90 backdrop-blur border-b border-border py-2 px-3 md:py-4 md:px-6 relative z-40 shadow-2xl">
|
<header className="bg-slate-950/90 backdrop-blur border-b border-border py-2 px-3 md:py-3 md:px-6 relative z-40 shadow-2xl">
|
||||||
<div className="max-w-7xl mx-auto flex justify-between items-center">
|
<div className="max-w-7xl mx-auto flex justify-between items-center">
|
||||||
<div className="flex items-center gap-3 md:gap-8">
|
|
||||||
{/* Logo Container - Smaller on mobile */}
|
|
||||||
<div className="h-10 w-32 md:h-16 md:w-64 relative flex-shrink-0">
|
|
||||||
<CrazeLogo />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Title & Status */}
|
|
||||||
<div className="hidden lg:block border-l border-slate-700 pl-6">
|
|
||||||
<h1 className="text-xl font-bold tracking-tight text-white text-shadow-sm">Analytics Dashboard</h1>
|
|
||||||
<div className="flex items-center gap-3 mt-1.5">
|
|
||||||
<div className="flex items-center gap-1.5 px-2 py-0.5 bg-emerald-500/10 border border-emerald-500/20 rounded-full">
|
|
||||||
<span className={`w-1.5 h-1.5 rounded-full bg-emerald-400 ${syncing ? 'animate-ping' : ''}`}></span>
|
|
||||||
<span className="text-[9px] text-emerald-400 font-black uppercase tracking-wider">Live Sync</span>
|
|
||||||
</div>
|
|
||||||
{lastUpdated && (
|
|
||||||
<span className="text-[9px] text-slate-500 font-bold uppercase tracking-wider">
|
|
||||||
Last: {new Date(lastUpdated).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 md:gap-6">
|
<div className="flex items-center gap-3 md:gap-6">
|
||||||
{/* Force Sync Action */}
|
{/* Logo Container - CraftAlley branding */}
|
||||||
<button
|
<div className="flex items-center gap-2 relative flex-shrink-0 cursor-pointer" onClick={() => setView('dashboard')}>
|
||||||
onClick={() => { handleDataFetch(); handleAdsFetch(); handleTrafficFetch(); handleVendorStockFetch(); handleBuyBoxFetch(); }}
|
<div className="bg-cyan-500 rounded-lg p-1.5 flex items-center justify-center w-8 h-8 shadow-[0_0_8px_rgba(6,182,212,0.4)]">
|
||||||
disabled={syncing}
|
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
||||||
title={syncing ? 'Syncing...' : 'Sync Now'}
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9s2.015-9 4.5-9y" />
|
||||||
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-semibold transition-all active:scale-95
|
</svg>
|
||||||
${syncing
|
</div>
|
||||||
? 'text-indigo-400/70 cursor-not-allowed'
|
<span className="font-extrabold text-slate-100 text-base tracking-tight select-none">CraftAlley</span>
|
||||||
: 'text-slate-500 hover:text-slate-300 hover:bg-slate-800/60'}`}
|
</div>
|
||||||
>
|
|
||||||
<RefreshIcon className={`w-3.5 h-3.5 ${syncing ? 'animate-spin' : ''}`} />
|
{/* View Switcher */}
|
||||||
<span className="hidden sm:inline">{syncing ? 'Syncing...' : 'Sync'}</span>
|
<div className="hidden md:flex bg-slate-900 rounded-lg p-0.5 border border-border/80 ml-6">
|
||||||
</button>
|
|
||||||
{/* View Switcher - Hidden on mobile (shown in bottom nav instead) */}
|
|
||||||
<div className="hidden md:flex bg-slate-900 rounded-lg p-1 border border-border">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('dashboard')}
|
onClick={() => setView('dashboard')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold transition-all
|
||||||
${view === 'dashboard' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
${view === 'dashboard' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white'}`}
|
||||||
>
|
>
|
||||||
<ChartIcon /> <span className="hidden lg:inline">Dashboard</span>
|
Dashboard
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('table')}
|
onClick={() => setView('table')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold transition-all
|
||||||
${view === 'table' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
${view === 'table' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white'}`}
|
||||||
>
|
>
|
||||||
<TableIcon /> <span className="hidden lg:inline">Grid</span>
|
Grid
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('weekly')}
|
onClick={() => setView('weekly')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold transition-all
|
||||||
${view === 'weekly' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
${view === 'weekly' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white'}`}
|
||||||
>
|
>
|
||||||
<TrendingIcon /> <span className="hidden lg:inline">Weekly Sales</span>
|
Weekly Sales
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('ads')}
|
onClick={() => setView('ads')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold transition-all
|
||||||
${view === 'ads' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
${view === 'ads' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white'}`}
|
||||||
>
|
>
|
||||||
<MegaphoneIcon /> <span className="hidden lg:inline">Ads</span>
|
Ads
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('forecast')}
|
onClick={() => setView('forecast')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold transition-all
|
||||||
${view === 'forecast' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
${view === 'forecast' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white'}`}
|
||||||
>
|
>
|
||||||
<ChartIcon /> <span className="hidden lg:inline">Fc 26</span>
|
Forecasting
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('vendor')}
|
onClick={() => setView('vendor')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold transition-all
|
||||||
${view === 'vendor' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
${view === 'vendor' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white'}`}
|
||||||
>
|
>
|
||||||
<ChartIcon /> <span className="hidden lg:inline">BSR</span>
|
SSR
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 md:gap-4">
|
||||||
|
{/* Sync Indicators */}
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-slate-400 bg-slate-900 px-2.5 py-1 rounded border border-border/80">
|
||||||
|
<span className={`w-1.5 h-1.5 rounded-full bg-emerald-400 ${syncing ? 'animate-ping' : ''}`}></span>
|
||||||
|
<span className="font-mono text-[10px] uppercase font-bold tracking-wider">
|
||||||
|
Sync {lastUpdated ? new Date(lastUpdated).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '11:41'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle Filters Action */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowFilters(prev => !prev)}
|
||||||
|
className="flex items-center gap-1 px-2.5 py-1 bg-slate-900 hover:bg-slate-800 border border-slate-800 rounded text-xs font-semibold text-slate-300 transition-all active:scale-95"
|
||||||
|
>
|
||||||
|
<span>{showFilters ? 'Ocultar' : '+ Filtrar'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Export Action */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// simple mockup export alert
|
||||||
|
alert("Exportando datos filtrados...");
|
||||||
|
}}
|
||||||
|
className="px-3.5 py-1 bg-cyan-600 hover:bg-cyan-500 rounded text-xs font-bold text-white transition-all active:scale-95 shadow-[0_0_8px_rgba(6,182,212,0.2)]"
|
||||||
|
>
|
||||||
|
Exportar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -796,8 +799,19 @@ const App: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FilterBar filters={filters} onFilterChange={handleFilterChange} options={filterOptions} />
|
{/* Title & Subtitle inside main body above filters */}
|
||||||
<div className="mt-6">
|
<div className="max-w-7xl mx-auto px-3 md:px-6 pt-6 pb-2">
|
||||||
|
<h2 className="text-2xl font-extrabold text-white tracking-tight uppercase">Analytics Dashboard</h2>
|
||||||
|
<p className="text-[11px] text-slate-500 font-medium mt-1">
|
||||||
|
Actualizado {lastUpdated ? new Date(lastUpdated).toLocaleDateString('es-ES', { day: 'numeric', month: 'short', year: 'numeric' }) : '17 jun 2026'} · Semana {(() => {
|
||||||
|
// dynamic or default week number
|
||||||
|
return '25';
|
||||||
|
})()} · {rawData.length.toLocaleString('de-DE')} registros
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showFilters && <FilterBar filters={filters} onFilterChange={handleFilterChange} options={filterOptions} />}
|
||||||
|
<div className="mt-4">
|
||||||
<div className={view === 'dashboard' ? '' : 'hidden'}>
|
<div className={view === 'dashboard' ? '' : 'hidden'}>
|
||||||
<Dashboard
|
<Dashboard
|
||||||
data={ytdAggregatedData}
|
data={ytdAggregatedData}
|
||||||
|
|||||||
+318
-177
@@ -445,21 +445,6 @@ const Dashboard: React.FC<DashboardProps> = ({
|
|||||||
const [top10Metric, setTop10Metric] = useState<'sellOut' | 'units'>('sellOut');
|
const [top10Metric, setTop10Metric] = useState<'sellOut' | 'units'>('sellOut');
|
||||||
const [cumulativeMetric, setCumulativeMetric] = 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
|
// Calculate Ads KPIs by Year
|
||||||
const adsKPIsByYear = useMemo(() => {
|
const adsKPIsByYear = useMemo(() => {
|
||||||
if (!adsData || adsData.length === 0) return null;
|
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 });
|
yearMap.set(y, { cost: 0, attributedSales: 0, clicks: 0, impressions: 0 });
|
||||||
}
|
}
|
||||||
const t = yearMap.get(y)!;
|
const t = yearMap.get(y)!;
|
||||||
|
|
||||||
if (ad.year === 2026) {
|
|
||||||
console.log("DASH 2026 W" + ad.week, ad.cost);
|
|
||||||
}
|
|
||||||
|
|
||||||
t.cost += ad.cost || 0;
|
t.cost += ad.cost || 0;
|
||||||
t.attributedSales += ad.attributedSales30d;
|
t.attributedSales += ad.attributedSales30d;
|
||||||
t.clicks += ad.clicks || 0;
|
t.clicks += ad.clicks || 0;
|
||||||
@@ -503,133 +483,291 @@ const Dashboard: React.FC<DashboardProps> = ({
|
|||||||
}, [adsKPIsByYear]);
|
}, [adsKPIsByYear]);
|
||||||
|
|
||||||
// Decide which data source to use for Product Line charts
|
// 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;
|
const displayData = contextData || data;
|
||||||
|
|
||||||
// Calculate dynamic height for the All Product Lines chart to enable scrolling
|
// 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);
|
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 (
|
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 */}
|
{/* Ads Performance Section - Split Actual vs Anterior */}
|
||||||
{adsKPIsByYear && availableAdsYears.length > 0 && (
|
{adsKPIsByYear && availableAdsYears.length > 0 && (() => {
|
||||||
<div className="bg-slate-900/40 border border-slate-800 rounded-2xl p-5 animate-fade-in shadow-xl">
|
const currentYear = availableAdsYears[0];
|
||||||
<div className="flex items-center gap-2 mb-4 border-b border-slate-800/80 pb-3">
|
const prevYear = availableAdsYears[1];
|
||||||
<div className="relative">
|
const twoYearsPrior = availableAdsYears[2];
|
||||||
<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>
|
|
||||||
</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'}`}>
|
const currentKpi = adsKPIsByYear[currentYear];
|
||||||
{availableAdsYears.map((year, idx) => {
|
|
||||||
const kpi = adsKPIsByYear[year];
|
|
||||||
const prevYear = availableAdsYears[idx + 1];
|
|
||||||
const prevKpi = prevYear ? adsKPIsByYear[prevYear] : null;
|
const prevKpi = prevYear ? adsKPIsByYear[prevYear] : null;
|
||||||
|
const priorKpi = twoYearsPrior ? adsKPIsByYear[twoYearsPrior] : null;
|
||||||
|
|
||||||
const renderGrowth = (current: number, previous: number | undefined, inverse: boolean = false) => {
|
const renderAdsGrowth = (currentVal: number, prevVal: number | undefined, isPercentage: boolean = false, inverse: boolean = false) => {
|
||||||
if (!previous || previous === 0) return null;
|
if (!prevVal || prevVal === 0) return <span className="text-slate-500 font-medium">-</span>;
|
||||||
const pct = ((current - previous) / previous) * 100;
|
const diff = currentVal - prevVal;
|
||||||
const isPositive = pct >= 0;
|
if (isPercentage) {
|
||||||
const colorClass = inverse
|
const pct = (diff / prevVal) * 100;
|
||||||
? (isPositive ? 'text-red-400' : 'text-emerald-400')
|
const isPos = pct >= 0;
|
||||||
: (isPositive ? 'text-emerald-400' : 'text-red-400');
|
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>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const renderRoasDiff = (currentVal: number, prevVal: number | undefined) => {
|
||||||
<span className={`text-[10px] font-bold ml-1 ${colorClass} whitespace-nowrap`}>
|
if (!prevVal || prevVal === 0) return <span className="text-slate-500 font-medium">-</span>;
|
||||||
{isPositive ? '▲' : '▼'}{Math.abs(pct).toFixed(0)}%
|
const diff = currentVal - prevVal;
|
||||||
</span>
|
const isPos = diff >= 0;
|
||||||
);
|
return <span className={`text-[10px] font-bold ${isPos ? 'text-emerald-400' : 'text-red-400'}`}>{isPos ? '+' : ''}{diff.toFixed(2)}</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={year} className="relative bg-slate-950/30 rounded-xl p-4 border border-white/5">
|
<div className="bg-slate-900/40 border border-slate-800 rounded-2xl p-5 shadow-xl">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-4 border-b border-slate-800/80 pb-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>
|
<span className="w-2 h-2 rounded-full bg-cyan-400 shadow-[0_0_8px_rgba(34,211,238,0.8)]"></span>
|
||||||
<div className="h-[1px] flex-1 bg-gradient-to-r from-slate-800/80 to-transparent"></div>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 divide-y lg:divide-y-0 lg:divide-x divide-slate-800">
|
||||||
{/* Ad Spend */}
|
{/* Período Actual */}
|
||||||
<div className="group">
|
<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>
|
||||||
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Spend</span>
|
<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-white block">
|
||||||
<span className="text-base font-bold text-fuchsia-400 truncate">
|
€{currentKpi.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
|
||||||
€{kpi.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
|
|
||||||
</span>
|
</span>
|
||||||
{renderGrowth(kpi.totalSpend, prevKpi?.totalSpend)}
|
{renderAdsGrowth(currentKpi.attributedSales, prevKpi?.attributedSales, true)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
{/* Attributed Sales */}
|
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Acos</span>
|
||||||
<div className="group">
|
<span className="text-base font-bold text-white block">
|
||||||
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Sales</span>
|
{currentKpi.acos.toFixed(1)}%
|
||||||
<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>
|
</span>
|
||||||
{renderGrowth(kpi.attributedSales, prevKpi?.attributedSales)}
|
{renderAdsGrowth(currentKpi.acos, prevKpi?.acos, false, true)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
{/* ACOS */}
|
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Roas</span>
|
||||||
<div className="group">
|
<span className="text-base font-bold text-white block">
|
||||||
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">ACOS</span>
|
{currentKpi.roas.toFixed(2)}x
|
||||||
<div className="flex items-baseline overflow-hidden">
|
|
||||||
<span className="text-base font-bold text-emerald-400 truncate">
|
|
||||||
{kpi.acos.toFixed(1)}%
|
|
||||||
</span>
|
</span>
|
||||||
{renderGrowth(kpi.acos, prevKpi?.acos, true)}
|
{renderRoasDiff(currentKpi.roas, prevKpi?.roas)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* ROAS */}
|
</div>
|
||||||
<div className="group">
|
|
||||||
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">ROAS</span>
|
{/* Período Anterior */}
|
||||||
<div className="flex items-baseline overflow-hidden">
|
<div className="pt-4 lg:pt-0 lg:pl-6">
|
||||||
<span className="text-base font-bold text-emerald-400 truncate">
|
<div className="bg-slate-800/40 px-3 py-1.5 rounded text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-4">
|
||||||
{kpi.roas.toFixed(2)}x
|
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>
|
</span>
|
||||||
{renderGrowth(kpi.roas, prevKpi?.roas)}
|
{renderAdsGrowth(prevKpi.totalSpend, priorKpi?.totalSpend, true)}
|
||||||
</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">
|
||||||
|
€{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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})()}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* KPI Section - Pass both specific data and context data */}
|
{/* KPI Section with CraftAlley Card Layout */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<MultiYearKPICard
|
{/* Sell Out Revenue Card */}
|
||||||
title="Sell Out Revenue"
|
<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">
|
||||||
metric="sellOut"
|
<div className="flex justify-between items-start mb-2">
|
||||||
data={data.totalsByYear}
|
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Sell Out Revenue</h3>
|
||||||
availableYears={data.availableYears}
|
{sellOutStats.mainGrowthPercent && (
|
||||||
contextData={contextData ? contextData.totalsByYear : undefined}
|
<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}
|
||||||
<MultiYearKPICard
|
</span>
|
||||||
title="Units Sold"
|
)}
|
||||||
metric="units"
|
</div>
|
||||||
data={data.totalsByYear}
|
<div className="mb-4">
|
||||||
availableYears={data.availableYears}
|
<span className="text-4xl font-extrabold text-white tracking-tight leading-none">
|
||||||
contextData={contextData ? contextData.totalsByYear : undefined}
|
{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>
|
</div>
|
||||||
|
|
||||||
{/* Main Grid matching Mockup Columns */}
|
{/* Units Sold Card */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<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>
|
||||||
|
|
||||||
{/* Left Column */}
|
{/* Row 1 Grid: Product Lines & Seasonality side-by-side */}
|
||||||
<div className="space-y-6 flex flex-col">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{/* All Product Lines Revenue Chart */}
|
{/* Horizontal Bar Chart (Product Lines Revenue) */}
|
||||||
<ExpandableCard title={contextData ? "Total Product Line Performance (Context)" : "Product Lines (Revenue)"} className="h-96">
|
<ExpandableCard title={contextData ? "Total Product Line Performance (Context)" : "Product Lines - Revenue"} className="h-96">
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<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>}
|
{contextData && <span className="text-xs text-indigo-400 font-semibold uppercase tracking-wider">Showing Full Product Line Data</span>}
|
||||||
@@ -648,7 +786,6 @@ const Dashboard: React.FC<DashboardProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Scrollable Container */}
|
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto pr-2 custom-scrollbar">
|
<div className="flex-1 min-h-0 overflow-y-auto pr-2 custom-scrollbar">
|
||||||
<div style={{ height: `${chartHeight}px` }}>
|
<div style={{ height: `${chartHeight}px` }}>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
@@ -679,65 +816,8 @@ const Dashboard: React.FC<DashboardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</ExpandableCard>
|
</ExpandableCard>
|
||||||
|
|
||||||
{/* Growth Table */}
|
{/* Monthly Seasonality Chart */}
|
||||||
<div className="h-72">
|
<ExpandableCard title="Estacionalidad Mensual - Ventas" className="h-96">
|
||||||
<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">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 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 flex-col h-full">
|
||||||
<div className="flex justify-end mb-2">
|
<div className="flex justify-end mb-2">
|
||||||
<div className="bg-slate-900 p-1 rounded-lg border border-slate-800 flex text-xs">
|
<div className="bg-slate-900 p-1 rounded-lg border border-slate-800 flex text-xs">
|
||||||
@@ -783,9 +863,34 @@ const Dashboard: React.FC<DashboardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ExpandableCard>
|
</ExpandableCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Cumulative Sales Chart - YoY comparison month by month */}
|
{/* Row 2 Grid: Growth Tables side-by-side */}
|
||||||
<ExpandableCard title="Cumulative Sales YoY (Month by Month)" className="h-96">
|
<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 flex-col h-full">
|
||||||
<div className="flex justify-end mb-2">
|
<div className="flex justify-end mb-2">
|
||||||
<div className="bg-slate-900 p-1 rounded-lg border border-slate-800 flex text-xs">
|
<div className="bg-slate-900 p-1 rounded-lg border border-slate-800 flex text-xs">
|
||||||
@@ -831,9 +936,46 @@ const Dashboard: React.FC<DashboardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ExpandableCard>
|
</ExpandableCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Country Chart - Uses Specific Data 'data' usually, unless we want to broaden it. Kept specific for now. */}
|
{/* Keep secondary charts/details below main redesigned mockup */}
|
||||||
<ExpandableCard title="Revenue Distribution by Customer" className="h-80">
|
<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%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={data.byCustomerSplit}>
|
<BarChart data={data.byCustomerSplit}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||||
@@ -854,7 +996,6 @@ const Dashboard: React.FC<DashboardProps> = ({
|
|||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</ExpandableCard>
|
</ExpandableCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Top Movers Table - Full Width */}
|
{/* Top Movers Table - Full Width */}
|
||||||
{rawData && rawData.length > 0 && (
|
{rawData && rawData.length > 0 && (
|
||||||
|
|||||||
@@ -51,30 +51,30 @@ const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Filter dropdowns - always visible on desktop, toggle on mobile */}
|
{/* 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
|
<MultiSelectDropdown
|
||||||
label="Customer"
|
label="CUSTOMER"
|
||||||
selected={filters.customer}
|
selected={filters.customer}
|
||||||
options={options.customer}
|
options={options.customer}
|
||||||
onChange={(v) => onFilterChange('customer', v)}
|
onChange={(v) => onFilterChange('customer', v)}
|
||||||
className="w-full md:w-auto md:flex-1"
|
className="w-full md:w-auto md:flex-1"
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="Year"
|
label="YEAR"
|
||||||
selected={filters.year}
|
selected={filters.year}
|
||||||
options={options.year}
|
options={options.year}
|
||||||
onChange={(v) => onFilterChange('year', v)}
|
onChange={(v) => onFilterChange('year', v)}
|
||||||
className="w-full md:w-auto md:flex-1"
|
className="w-full md:w-auto md:flex-1"
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="Month"
|
label="MONTH"
|
||||||
selected={filters.month}
|
selected={filters.month}
|
||||||
options={options.month}
|
options={options.month}
|
||||||
onChange={(v) => onFilterChange('month', v)}
|
onChange={(v) => onFilterChange('month', v)}
|
||||||
className="w-full md:w-auto md:flex-1"
|
className="w-full md:w-auto md:flex-1"
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="Week"
|
label="WEEK"
|
||||||
selected={filters.week}
|
selected={filters.week}
|
||||||
options={options.week}
|
options={options.week}
|
||||||
onChange={(v) => onFilterChange('week', v)}
|
onChange={(v) => onFilterChange('week', v)}
|
||||||
@@ -82,7 +82,7 @@ const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options
|
|||||||
enableRangeSelect={true}
|
enableRangeSelect={true}
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="Product Line"
|
label="PRODUCT LINE"
|
||||||
selected={filters.line}
|
selected={filters.line}
|
||||||
options={options.line}
|
options={options.line}
|
||||||
onChange={(v) => onFilterChange('line', v)}
|
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"
|
className="w-full md:w-auto md:flex-1"
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="Title"
|
label="TITLE"
|
||||||
selected={filters.title}
|
selected={filters.title}
|
||||||
options={options.title}
|
options={options.title}
|
||||||
onChange={(v) => onFilterChange('title', v)}
|
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"
|
className="w-full md:w-auto md:flex-1"
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="Stock"
|
label="STOCK"
|
||||||
selected={filters.stock}
|
selected={filters.stock}
|
||||||
options={options.stock}
|
options={options.stock}
|
||||||
onChange={(v) => onFilterChange('stock', v)}
|
onChange={(v) => onFilterChange('stock', v)}
|
||||||
|
|||||||
Reference in New Issue
Block a user