feat: implement dynamic YoY comparison columns and colors for Sell Out and Units in Grid view

This commit is contained in:
Christian Vidal Wolf
2026-06-17 13:50:36 +02:00
parent ce0d232fde
commit 434b1b5103
+113 -60
View File
@@ -1199,45 +1199,48 @@ const DataGrid: React.FC<DataGridProps> = ({ data, filters, hasCustomerFilter, a
<tr>
<th className="px-4 py-3 min-w-[80px]">SKU</th>
<th className="px-4 py-3 min-w-[280px]">Title</th>
{years.map((year, idx) => (
<React.Fragment key={year}>
<th className="px-4 py-3 text-right whitespace-nowrap">
{year} SO {idx === 0 && '↓'}
</th>
<th className="px-4 py-3 text-right whitespace-nowrap">
{year} U
</th>
</React.Fragment>
))}
<th className="px-4 py-3 text-center">S.O. Δ%</th>
<th className="px-4 py-3 text-right">S.O. Δ</th>
{years.map((year, idx) => {
const prevYear = years[idx + 1];
return (
<React.Fragment key={year}>
<th className="px-4 py-3 text-right whitespace-nowrap">
{year} SO {idx === 0 && '↓'}
</th>
<th className="px-4 py-3 text-right whitespace-nowrap">
{year} U
</th>
{prevYear && (
<>
<th className="px-4 py-3 text-center whitespace-nowrap text-slate-500 font-bold bg-slate-900/10">
Δ% {year}/{prevYear}
</th>
<th className="px-4 py-3 text-right whitespace-nowrap text-slate-500 font-bold bg-slate-900/10">
Δ {year}/{prevYear}
</th>
<th className="px-4 py-3 text-center whitespace-nowrap text-slate-500 font-bold bg-slate-950/20">
U Δ% {year}/{prevYear}
</th>
<th className="px-4 py-3 text-right whitespace-nowrap text-slate-500 font-bold bg-slate-950/20">
U Δ {year}/{prevYear}
</th>
</>
)}
</React.Fragment>
);
})}
</tr>
</thead>
<tbody className="divide-y divide-slate-900 text-slate-300">
{paginatedRows.map((row) => {
const latestYear = years[0] || '2026';
const prevYear = years[1] || '2025';
// Helper to get color code based on year comparison
const getYearCellColor = (year: string) => {
const sorted = [...years].sort((a, b) => parseInt(b) - parseInt(a));
const index = sorted.indexOf(year);
if (index === 0) return 'text-cyan-400'; // Current Year (Cyan)
if (index === 1) return 'text-indigo-400'; // Previous Year (Indigo)
return 'text-slate-400'; // Reference years
};
const currentData = row.totalsByYear[latestYear];
const prevData = row.totalsByYear[prevYear];
const currentSOVal = currentData?.sellOut || 0;
const currentUnitsVal = currentData?.units || 0;
const prevSOVal = prevData?.sellOut || 0;
const prevUnitsVal = prevData?.units || 0;
// Calculate Growth Values
let sellOutGrowthPct = 0;
if (prevSOVal > 0) {
sellOutGrowthPct = ((currentSOVal - prevSOVal) / prevSOVal) * 100;
} else if (currentSOVal > 0) {
sellOutGrowthPct = 100;
}
const sellOutDeltaVal = currentSOVal - prevSOVal;
// Calculate Weeks of Coverage (WOC)
const skuClean = row.sku?.replace(/(DE|EN)$/i, '');
const internalStockVal = stockMap?.get(skuClean) || 0;
const vendorStockVal = (top50Mode === 'uk' ? vendorStockMap?.get(row.asin?.trim().toUpperCase())?.uk : vendorStockMap?.get(row.asin?.trim().toUpperCase())?.eu) || 0;
@@ -1319,42 +1322,92 @@ const DataGrid: React.FC<DataGridProps> = ({ data, filters, hasCustomerFilter, a
</div>
</td>
{/* Metrics Data */}
{years.map((year) => {
{/* Metrics Data and Growth Columns */}
{years.map((year, idx) => {
const yData = row.totalsByYear[year];
const prevYear = years[idx + 1];
const prevData = prevYear ? row.totalsByYear[prevYear] : null;
const currentSOVal = yData?.sellOut || 0;
const currentUnitsVal = yData?.units || 0;
const prevSOVal = prevData?.sellOut || 0;
const prevUnitsVal = prevData?.units || 0;
// Sell Out Growth
let sellOutGrowthPct = 0;
if (prevSOVal > 0) {
sellOutGrowthPct = ((currentSOVal - prevSOVal) / prevSOVal) * 100;
} else if (currentSOVal > 0) {
sellOutGrowthPct = 100;
}
const sellOutDeltaVal = currentSOVal - prevSOVal;
// Units Growth
let unitsGrowthPct = 0;
if (prevUnitsVal > 0) {
unitsGrowthPct = ((currentUnitsVal - prevUnitsVal) / prevUnitsVal) * 100;
} else if (currentUnitsVal > 0) {
unitsGrowthPct = 100;
}
const unitsDeltaVal = currentUnitsVal - prevUnitsVal;
const cellColor = getYearCellColor(year);
return (
<React.Fragment key={year}>
<td className="px-4 py-4 text-right font-semibold text-slate-100 text-xs align-top">
<td className={`px-4 py-4 text-right font-semibold text-xs align-top ${cellColor}`}>
{yData ? `${yData.sellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}` : '-'}
</td>
<td className="px-4 py-4 text-right font-medium text-slate-300 text-xs align-top">
<td className={`px-4 py-4 text-right font-medium text-xs align-top ${cellColor}`}>
{yData ? yData.units.toLocaleString('de-DE') : '-'}
</td>
{prevYear && (
<>
{/* Sell Out Growth Pct */}
<td className="px-4 py-4 align-top text-center bg-slate-900/10">
{sellOutGrowthPct !== 0 ? (
<span className={`inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[9px] font-bold ${sellOutGrowthPct > 0 ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'}`}>
{sellOutGrowthPct > 0 ? '▲' : '▼'} {Math.round(Math.abs(sellOutGrowthPct))}%
</span>
) : (
<span className="text-slate-600">-</span>
)}
</td>
{/* Sell Out Delta Val */}
<td className={`px-4 py-4 text-right align-top font-bold text-xs bg-slate-900/10 ${sellOutDeltaVal >= 0 ? 'text-emerald-400' : 'text-rose-500'}`}>
{sellOutDeltaVal !== 0 ? (
<span>
{sellOutDeltaVal > 0 ? '+' : ''}{sellOutDeltaVal.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
) : (
<span className="text-slate-600">-</span>
)}
</td>
{/* Units Growth Pct */}
<td className="px-4 py-4 align-top text-center bg-slate-950/20">
{unitsGrowthPct !== 0 ? (
<span className={`inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[9px] font-bold ${unitsGrowthPct > 0 ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'}`}>
{unitsGrowthPct > 0 ? '▲' : '▼'} {Math.round(Math.abs(unitsGrowthPct))}%
</span>
) : (
<span className="text-slate-600">-</span>
)}
</td>
{/* Units Delta Val */}
<td className={`px-4 py-4 text-right align-top font-semibold text-xs bg-slate-950/20 ${unitsDeltaVal >= 0 ? 'text-emerald-400' : 'text-rose-500'}`}>
{unitsDeltaVal !== 0 ? (
<span>
{unitsDeltaVal > 0 ? '+' : ''}{unitsDeltaVal.toLocaleString('de-DE')}
</span>
) : (
<span className="text-slate-600">-</span>
)}
</td>
</>
)}
</React.Fragment>
);
})}
{/* Delta growth % */}
<td className="px-4 py-4 align-top text-center">
{sellOutGrowthPct !== 0 ? (
<span className="inline-flex items-center gap-0.5 px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
{Math.round(sellOutGrowthPct)}%
</span>
) : (
<span className="text-slate-600">-</span>
)}
</td>
{/* Delta growth € */}
<td className="px-4 py-4 text-right align-top font-bold text-emerald-400 text-xs">
{sellOutDeltaVal !== 0 ? (
<span>
{sellOutDeltaVal > 0 ? '+' : ''}{sellOutDeltaVal.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
) : (
<span className="text-slate-600">-</span>
)}
</td>
</tr>
);
})}