Performance optimizations: lazy rendering, row memoization, and data processing loops

This commit is contained in:
Christian Vidal Wolf
2026-01-27 21:43:25 +01:00
parent f521dd2921
commit 6b4bef66ec
4 changed files with 284 additions and 194 deletions
+131 -86
View File
@@ -34,10 +34,105 @@ const Top50Badge: React.FC<{ rank: number; label: string; theme?: 'amber' | 'ind
);
};
const ForecastRow: React.FC<{
item: ForecastViewData;
activeMonths: string[];
top50Ranking?: { eu: Map<string, number>; uk: Map<string, number> };
stockMap?: Map<string, number>;
}> = React.memo(({ item, activeMonths, top50Ranking, stockMap }) => {
const asin = item.asin.toUpperCase();
// Top 50 Badges logic
const ranks: { rank: number; label: string; theme: 'blue' | 'indigo' }[] = [];
if (top50Ranking) {
const rankEU = top50Ranking.eu.get(asin);
const rankUK = top50Ranking.uk.get(asin);
if (rankEU) ranks.push({ rank: rankEU, label: 'EU', theme: 'indigo' });
if (rankUK) ranks.push({ rank: rankUK, label: 'UK', theme: 'blue' });
}
// Monthly Forecast Calculation
const filteredForecast = item.monthlyData
.filter(md => activeMonths.includes(md.month))
.reduce((acc, md) => acc + md.forecastUnits, 0);
// Actual Sales for 2026 (Selected Period)
const actualPeriod = item.monthlyData
.filter(md => activeMonths.includes(md.month))
.reduce((acc, md) => acc + md.actualUnits, 0);
// Total annual actual for achievement percentage
const actualTotal = item.monthlyData.reduce((acc, md) => acc + md.actualUnits, 0);
const totalAchievement = item.annualForecast > 0 ? (actualTotal / item.annualForecast) * 100 : 0;
return (
<tr key={item.asin} className="hover:bg-indigo-500/5 transition-colors group">
<td className="px-6 py-4">
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-3">
{ranks.map((r, i) => (
<Top50Badge key={i} rank={r.rank} label={r.label} theme={r.theme} />
))}
<span className="font-black text-sm text-[#818cf8] uppercase tracking-tight">{item.sku}</span>
<div className="px-2 py-0.5 rounded bg-slate-800/80 border border-white/5 text-[10px] font-bold text-slate-400 uppercase tracking-wider">
{item.asin}
</div>
</div>
<div className="flex items-start gap-2 text-[13px] font-medium text-slate-100 leading-snug group-hover:text-white transition-colors">
<span className="truncate max-w-[170px]">{item.title}</span>
{stockMap && (
<StockBadge stock={stockMap.get(item.sku?.replace(/(DE|EN)$/i, ''))} />
)}
</div>
<div className="text-[10px] font-black text-[#ec4899] uppercase tracking-[0.15em]">
{item.line}
</div>
</div>
</td>
<td className="px-6 py-4 text-right font-mono font-bold text-slate-400">
{filteredForecast.toLocaleString('de-DE')}
</td>
<td className="px-6 py-4 text-right font-mono font-bold text-emerald-400">
{actualPeriod.toLocaleString('de-DE')}
</td>
<td className="px-6 py-4 text-right">
<div className="flex flex-col items-end gap-1">
<span className={`px-2 py-1 rounded text-xs font-black ${totalAchievement >= 50 ? 'bg-emerald-500/10 text-emerald-400' : totalAchievement >= 10 ? 'bg-indigo-500/10 text-indigo-400' : 'bg-slate-800 text-slate-500'}`}>
{totalAchievement.toFixed(1)}%
</span>
<span className="text-[9px] text-slate-500 font-bold uppercase">of Annual {item.annualForecast.toLocaleString('de-DE')}</span>
</div>
</td>
<td className="px-6 py-4">
<div className="flex flex-col items-center gap-2">
<div className="w-24 h-1.5 bg-slate-800 rounded-full overflow-hidden border border-white/5">
<div
className={`h-full transition-all duration-500 rounded-full ${totalAchievement >= 100 ? 'bg-emerald-500' : totalAchievement >= 50 ? 'bg-indigo-500' : 'bg-amber-500/50'}`}
style={{ width: `${Math.min(100, totalAchievement)}%` }}
/>
</div>
<div className="flex items-center gap-1.5">
{totalAchievement >= 100 ? (
<div className="px-1.5 py-0.5 rounded text-[8px] font-black bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 uppercase tracking-tighter animate-pulse">
Target Hit
</div>
) : totalAchievement > 0 ? (
<div className="text-[9px] font-bold text-slate-500 italic">
{totalAchievement.toFixed(0)}% Done
</div>
) : null}
</div>
</div>
</td>
</tr>
);
});
const ForecastView: React.FC<ForecastViewProps> = ({ data, filters, top50Ranking, stockMap, stockFilter, onStockFilterChange }) => {
const [searchTerm, setSearchTerm] = useState('');
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu');
const [displayCount, setDisplayCount] = useState(50);
// Auto-detect best Top 50 mode based on data
useEffect(() => {
@@ -376,92 +471,42 @@ const ForecastView: React.FC<ForecastViewProps> = ({ data, filters, top50Ranking
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{sortedProducts.map((p) => {
const asin = p.asin.toUpperCase();
// Top 50 Badges logic
const ranks = [];
if (top50Ranking) {
const rankEU = top50Ranking.eu.get(asin);
const rankUK = top50Ranking.uk.get(asin);
if (rankEU) ranks.push({ rank: rankEU, label: 'EU', theme: 'indigo' as const });
if (rankUK) ranks.push({ rank: rankUK, label: 'UK', theme: 'blue' as const });
}
// Monthly Forecast Calculation
const filteredForecast = p.monthlyData
.filter(md => activeMonths.includes(md.month))
.reduce((acc, md) => acc + md.forecastUnits, 0);
// Actual Sales for 2026 (Selected Period)
const actualPeriod = p.monthlyData
.filter(md => activeMonths.includes(md.month))
.reduce((acc, md) => acc + md.actualUnits, 0);
// Total annual actual for achievement percentage
const actualTotal = p.monthlyData.reduce((acc, md) => acc + md.actualUnits, 0);
const totalAchievement = p.annualForecast > 0 ? (actualTotal / p.annualForecast) * 100 : 0;
// YTD Achievement (Achievement of the period)
const periodAchievement = filteredForecast > 0 ? (actualPeriod / filteredForecast) * 100 : 0;
return (
<tr key={p.asin} className="hover:bg-indigo-500/5 transition-colors group">
<td className="px-6 py-4">
<div className="flex flex-col gap-1.5">
{/* ID Row */}
<div className="flex items-center gap-3">
{ranks.map((r, i) => (
<Top50Badge key={i} rank={r.rank} label={r.label} />
))}
<span className="font-black text-sm text-[#818cf8] uppercase tracking-tight">{p.sku}</span>
<div className="px-2 py-0.5 rounded bg-slate-800/80 border border-white/5 text-[10px] font-bold text-slate-400 uppercase tracking-wider">
{p.asin}
</div>
</div>
{/* Title Row */}
<div className="flex items-start gap-2 text-[13px] font-medium text-slate-100 leading-snug group-hover:text-white transition-colors">
<span className="truncate max-w-[170px]">{p.title}</span>
{stockMap && (
<StockBadge stock={stockMap.get(p.sku?.replace(/(DE|EN)$/i, ''))} />
)}
</div>
{/* Line Row */}
<div className="text-[10px] font-black text-[#ec4899] uppercase tracking-[0.15em]">
{p.line}
</div>
</div>
</td>
<td className="px-6 py-4 text-right font-mono font-bold text-slate-400">
{filteredForecast.toLocaleString('de-DE')}
</td>
<td className="px-6 py-4 text-right font-mono font-bold text-emerald-400">
{actualPeriod.toLocaleString('de-DE')}
</td>
<td className="px-6 py-4 text-right">
<div className="flex flex-col items-end gap-1">
<span className={`px-2 py-1 rounded text-xs font-black ${totalAchievement >= 50 ? 'bg-emerald-500/10 text-emerald-400' : totalAchievement >= 10 ? 'bg-indigo-500/10 text-indigo-400' : 'bg-slate-800 text-slate-500'}`}>
{totalAchievement.toFixed(1)}%
</span>
<span className="text-[9px] text-slate-500 font-bold uppercase">of Annual {p.annualForecast.toLocaleString('de-DE')}</span>
</div>
</td>
<td className="px-6 py-4">
<div className="flex flex-col items-center gap-2">
<div className="w-32 h-2 bg-slate-800 rounded-full overflow-hidden border border-white/5">
<div
className={`h-full transition-all duration-1000 ${periodAchievement >= 100 ? 'bg-emerald-500' : periodAchievement >= 50 ? 'bg-indigo-500' : 'bg-amber-500'}`}
style={{ width: `${Math.min(100, periodAchievement)}%` }}
></div>
</div>
<span className={`text-[11px] font-black ${periodAchievement >= 100 ? 'text-emerald-400' : periodAchievement >= 50 ? 'text-indigo-400' : 'text-amber-400'}`}>
{periodAchievement.toFixed(1)}%
</span>
</div>
</td>
</tr>
);
})}
{sortedAndFiltered.length > 0 ? (
(() => {
const displayItems = sortedAndFiltered.slice(0, displayCount);
return (
<>
{displayItems.map(p => (
<ForecastRow
key={p.asin}
item={p}
activeMonths={activeMonths}
top50Ranking={top50Ranking}
stockMap={stockMap}
/>
))}
{displayCount < sortedAndFiltered.length && (
<tr>
<td colSpan={5} className="px-6 py-6 text-center bg-slate-900/40 backdrop-blur-sm border-t border-white/5">
<button
onClick={() => setDisplayCount(prev => prev + 100)}
className="px-6 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-xs font-black uppercase tracking-widest shadow-xl transition-all active:scale-95 border border-indigo-400/30"
>
Load More SKUs ({sortedAndFiltered.length - displayCount} remaining)
</button>
</td>
</tr>
)}
</>
);
})()
) : (
<tr>
<td colSpan={5} className="px-6 py-10 text-center text-slate-500 italic">
No products found matching your filters...
</td>
</tr>
)}
</tbody>
</table>
</div>