mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 12:25:22 +02:00
Performance optimizations: lazy rendering, row memoization, and data processing loops
This commit is contained in:
+131
-86
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user