feat: move filtered totals to Data Grid table header

- DataGrid.tsx: Integrated aggregated totals into a new sticky header row
- DataGrid.tsx: Aligned Sell Out and Units totals directly above their respective columns
- DataGrid.tsx: Added YoY growth indicators to the header totals
- DataGrid.tsx: Improved performance with memoized total calculations
- DataGrid.tsx: Fixed syntax errors and TypeScript warnings
This commit is contained in:
Christian Vidal Wolf
2026-01-22 15:26:04 +01:00
parent 235928a588
commit aaca2ffe88
+98 -86
View File
@@ -282,7 +282,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData =
const { years } = useMemo(() => {
// We still need unique years for columns
const yearsSet = new Set(data.map(d => String((d as any).year)));
const yearsSet = new Set<string>(data.map(d => String(d.year)));
return { years: Array.from(yearsSet).sort((a, b) => parseInt(b) - parseInt(a)) };
}, [data]);
@@ -451,6 +451,57 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData =
return result;
}, [pivotRows, rowFilters, sortConfig, years]);
// Calculate aggregated totals for the header row
const filteredTotalsByYear = useMemo(() => {
const result: { [year: string]: { sellOut: number, units: number, sellOutGrowth?: number, unitsGrowth?: number } } = {};
years.forEach((year, index) => {
const totals = processedRows.reduce(
(acc, row) => {
const data = row.totalsByYear[year];
if (data) {
acc.sellOut += data.sellOut;
acc.units += data.units;
}
return acc;
},
{ sellOut: 0, units: 0 }
);
result[year] = totals;
// Calculate YoY growth if previous year exists
const prevYear = years[index + 1];
if (prevYear) {
const prevYearTotals = processedRows.reduce(
(acc, row) => {
const data = row.totalsByYear[prevYear];
if (data) {
acc.sellOut += data.sellOut;
acc.units += data.units;
}
return acc;
},
{ sellOut: 0, units: 0 }
);
if (prevYearTotals.sellOut > 0) {
result[year].sellOutGrowth = ((totals.sellOut - prevYearTotals.sellOut) / prevYearTotals.sellOut) * 100;
} else if (totals.sellOut > 0) {
result[year].sellOutGrowth = 100;
}
if (prevYearTotals.units > 0) {
result[year].unitsGrowth = ((totals.units - prevYearTotals.units) / prevYearTotals.units) * 100;
} else if (totals.units > 0) {
result[year].unitsGrowth = 100;
}
}
});
return result;
}, [processedRows, years]);
const paginatedRows = useMemo(() => {
const start = (currentPage - 1) * ROWS_PER_PAGE;
return processedRows.slice(start, start + ROWS_PER_PAGE);
@@ -765,89 +816,6 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData =
)}
</div>
)}
{/* Filter Totals Summary */}
<div className="bg-gradient-to-r from-indigo-900/20 to-purple-900/20 border-y border-indigo-500/30 px-4 py-3">
<div className="flex flex-wrap items-center gap-6">
<div className="flex items-center gap-2">
<svg className="w-5 h-5 text-indigo-400" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" /></svg>
<span className="text-xs font-bold text-indigo-300 uppercase tracking-wide">Filtered Totals:</span>
</div>
{years.map((year, index) => {
const yearTotals = processedRows.reduce(
(acc, row) => {
const data = row.totalsByYear[year];
if (data) {
acc.sellOut += data.sellOut;
acc.units += data.units;
}
return acc;
},
{ sellOut: 0, units: 0 }
);
// Calculate YoY growth if previous year exists
const prevYear = years[index + 1];
let sellOutGrowth = null;
let unitsGrowth = null;
if (prevYear) {
const prevYearTotals = processedRows.reduce(
(acc, row) => {
const data = row.totalsByYear[prevYear];
if (data) {
acc.sellOut += data.sellOut;
acc.units += data.units;
}
return acc;
},
{ sellOut: 0, units: 0 }
);
if (prevYearTotals.sellOut > 0) {
sellOutGrowth = ((yearTotals.sellOut - prevYearTotals.sellOut) / prevYearTotals.sellOut) * 100;
} else if (yearTotals.sellOut > 0) {
sellOutGrowth = 100;
}
if (prevYearTotals.units > 0) {
unitsGrowth = ((yearTotals.units - prevYearTotals.units) / prevYearTotals.units) * 100;
} else if (yearTotals.units > 0) {
unitsGrowth = 100;
}
}
return (
<div key={year} className="flex items-center gap-4 px-4 py-2 bg-slate-900/50 border border-slate-700/50 rounded-lg">
<span className="text-xs font-bold text-slate-400">{year}</span>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">S.O:</span>
<span className="text-sm font-bold text-emerald-400">
{yearTotals.sellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{sellOutGrowth !== null && (
<span className={`text-xs font-bold ${sellOutGrowth >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{sellOutGrowth >= 0 ? '↑' : '↓'} {Math.abs(sellOutGrowth).toFixed(1)}%
</span>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">Units:</span>
<span className="text-sm font-bold text-blue-400">
{yearTotals.units.toLocaleString('de-DE')}
</span>
{unitsGrowth !== null && (
<span className={`text-xs font-bold ${unitsGrowth >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{unitsGrowth >= 0 ? '↑' : '↓'} {Math.abs(unitsGrowth).toFixed(1)}%
</span>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
@@ -889,7 +857,52 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData =
{/* Data Table */}
<div className="overflow-x-auto min-h-[400px]">
<table className="w-full text-left text-sm border-collapse">
<thead className="bg-slate-950 text-slate-400 uppercase text-xs font-semibold tracking-wider sticky top-0 z-20 shadow-sm">
<thead className="bg-slate-950 text-slate-400 uppercase text-xs font-semibold tracking-wider sticky top-0 z-20 shadow-sm border-b border-white/10">
{/* NEW: Filtered Totals Header Row */}
<tr className="bg-indigo-950/20 border-b border-indigo-500/30">
<th colSpan={effectiveDimensions.length} className="px-4 py-3 bg-indigo-900/10">
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-indigo-400" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
</svg>
<span className="text-[10px] font-black text-indigo-300 tracking-widest uppercase">Filtered Totals</span>
</div>
</th>
{years.map((year, index) => {
const totals = filteredTotalsByYear[year];
const prevYear = years[index + 1];
return (
<React.Fragment key={`totals-${year}`}>
<th className="px-4 py-3 text-right border-x border-indigo-500/10 bg-indigo-900/5">
<div className="flex flex-col items-end">
<span className="text-emerald-400 font-bold text-sm">
{totals.sellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{totals.sellOutGrowth !== undefined && (
<span className={`text-[9px] font-black ${totals.sellOutGrowth >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{totals.sellOutGrowth >= 0 ? '↑' : '↓'} {Math.abs(totals.sellOutGrowth).toFixed(1)}%
</span>
)}
</div>
</th>
<th className="px-4 py-3 text-right border-r border-indigo-500/10 bg-indigo-900/5">
<div className="flex flex-col items-end">
<span className="text-blue-400 font-bold text-sm">
{totals.units.toLocaleString('de-DE')}
</span>
{totals.unitsGrowth !== undefined && (
<span className={`text-[9px] font-black ${totals.unitsGrowth >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{totals.unitsGrowth >= 0 ? '↑' : '↓'} {Math.abs(totals.unitsGrowth).toFixed(1)}%
</span>
)}
</div>
</th>
{prevYear && <th colSpan={2} className="bg-indigo-900/5 border-r border-indigo-500/10"></th>}
{showAdsMetrics && adsSummary && <th colSpan={2} className="bg-fuchsia-900/5 border-r border-fuchsia-500/10"></th>}
</React.Fragment>
);
})}
</tr>
<tr>
{/* Dynamic Dimension Headers */}
{effectiveDimensions.map(dim => {
@@ -1099,7 +1112,6 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData =
</div>
</div>
</div>
</div>
);
};