mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 11:35:24 +02:00
feat: redesign dashboard layout and header tabs to match CraftAlley V2 mockup
This commit is contained in:
+472
-331
@@ -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 */}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user