mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14:35:22 +02:00
Redesign ForecastView UI: Add Actuals, Achievement columns and Dark Theme
This commit is contained in:
+108
-80
@@ -48,69 +48,94 @@ const ForecastRow: React.FC<{
|
||||
}
|
||||
}
|
||||
|
||||
const monthlySales = activeMonths.map(m => (item.monthlyData && item.monthlyData[m]?.units) || 0);
|
||||
const avgMonthly = monthlySales.length > 0 ? monthlySales.reduce((a, b) => a + b, 0) / activeMonths.length : 0;
|
||||
const peakMonthly = monthlySales.length > 0 ? Math.max(...monthlySales) : 0;
|
||||
// Calculations for new columns
|
||||
const forecastPeriod = item.forecastUnits || 0;
|
||||
const actualUnits = item.actualUnits || 0;
|
||||
const annualForecast = item.annualForecast || 0;
|
||||
|
||||
// FC 26 Achievement: Actual / Annual Forecast
|
||||
const fcAchievement = annualForecast > 0 ? (actualUnits / annualForecast) * 100 : 0;
|
||||
|
||||
// YTD Achievement: Actual / Period Forecast
|
||||
// If activeMonths is basically "Year To Date" (which it usually is in this view unless filtered), this logic holds.
|
||||
// If user selects specific months, it becomes "Period Achievement".
|
||||
const ytdAchievement = forecastPeriod > 0 ? (actualUnits / forecastPeriod) * 100 : 0;
|
||||
|
||||
let achievementColor = 'bg-slate-700';
|
||||
if (ytdAchievement >= 100) achievementColor = 'bg-emerald-500';
|
||||
else if (ytdAchievement >= 80) achievementColor = 'bg-emerald-400';
|
||||
else if (ytdAchievement >= 50) achievementColor = 'bg-amber-400';
|
||||
else achievementColor = 'bg-rose-500';
|
||||
|
||||
return (
|
||||
<tr key={item.asin} className="hover:bg-indigo-500/5 transition-colors group">
|
||||
<tr key={item.asin} className="hover:bg-indigo-500/5 transition-colors group border-b border-slate-800/50 last:border-0">
|
||||
{/* Product Info */}
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{ranks.map((r, i) => (
|
||||
<Top50Badge key={i} rank={r.rank} label={r.label} theme={r.theme} />
|
||||
))}
|
||||
<span className="text-xs font-black text-indigo-400 tracking-tighter uppercase">
|
||||
{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">
|
||||
<div className="px-1.5 py-0.5 rounded bg-slate-800 border border-slate-700 text-[9px] font-bold text-slate-400 uppercase tracking-wider">
|
||||
{item.asin}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-white/70 line-clamp-1 group-hover:line-clamp-none transition-all" title={item.title}>{item.title}</span>
|
||||
<span className="text-sm font-medium text-slate-200 line-clamp-1 group-hover:line-clamp-none transition-all" title={item.title}>
|
||||
{item.title}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="px-2 py-0.5 rounded bg-indigo-500/10 text-[9px] font-black text-indigo-300 uppercase tracking-widest border border-indigo-500/20">{item.line}</span>
|
||||
{stockMap && (
|
||||
<StockBadge stock={stockMap.get(item.sku?.replace(/(DE|EN)$/i, ''))} />
|
||||
)}
|
||||
<span className="px-2 py-0.5 rounded bg-purple-500/10 text-[9px] font-black text-purple-300 uppercase tracking-widest border border-purple-500/20">
|
||||
{item.line}
|
||||
</span>
|
||||
{/* WOC Indicator preserved */}
|
||||
<VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={item.avgWeeklySales} />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span className="text-sm font-black text-white">{(item.actualUnits || 0).toLocaleString('de-DE')}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span className="text-sm font-black text-emerald-400">{(item.forecastUnits || 0).toLocaleString('de-DE')}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-sm font-black text-indigo-400">{(Math.round(avgMonthly) || 0).toLocaleString('de-DE')}</span>
|
||||
<span className="text-[9px] text-slate-500 font-bold uppercase tracking-tighter">Peak: {(Math.round(peakMonthly) || 0).toLocaleString('de-DE')}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="h-10 w-full min-w-[120px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={activeMonths.map(m => ({ name: m, units: (item.monthlyData && item.monthlyData[m]?.units) || 0 }))}>
|
||||
<Bar dataKey="units" fill="#6366f1" radius={[2, 2, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className={`text-sm font-black ${item.accuracy >= 80 ? 'text-emerald-400' : item.accuracy >= 50 ? 'text-amber-400' : 'text-rose-400'}`}>
|
||||
{item.accuracy}%
|
||||
{/* Forecast (Period) */}
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span className="text-sm font-bold text-slate-400">
|
||||
{forecastPeriod.toLocaleString('de-DE')}
|
||||
</span>
|
||||
<div className="w-16 h-1.5 bg-slate-800 rounded-full mt-1 overflow-hidden border border-white/5">
|
||||
<div
|
||||
className={`h-full transition-all duration-1000 ${item.accuracy >= 80 ? 'bg-emerald-500' : item.accuracy >= 50 ? 'bg-amber-500' : 'bg-rose-500'}`}
|
||||
style={{ width: `${item.accuracy}%` }}
|
||||
/>
|
||||
</td>
|
||||
|
||||
{/* Actual Units Sales 2026 */}
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span className="text-base font-black text-emerald-400">
|
||||
{actualUnits.toLocaleString('de-DE')}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* FC 26 Achievement (Annual %) */}
|
||||
<td className="px-6 py-4 text-center">
|
||||
<div className="flex flex-col items-center justify-center gap-1">
|
||||
<div className="px-3 py-1 rounded bg-slate-800 border border-slate-700 text-xs font-bold text-white shadow-sm">
|
||||
{fcAchievement.toFixed(1)}%
|
||||
</div>
|
||||
<span className="text-[9px] font-bold text-slate-500 uppercase tracking-tight">
|
||||
OF ANNUAL {Math.round(annualForecast / 1000)}K
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
|
||||
{/* YTD Achievement (Period %) */}
|
||||
<td className="px-6 py-4 align-middle">
|
||||
<div className="flex flex-col gap-1 w-full max-w-[200px] mx-auto">
|
||||
<div className="flex justify-between items-end mb-1">
|
||||
<span className={`text-xs font-black ${ytdAchievement >= 100 ? 'text-emerald-400' : 'text-slate-300'}`}>
|
||||
{ytdAchievement.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-slate-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${achievementColor}`}
|
||||
style={{ width: `${Math.min(100, ytdAchievement)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
@@ -257,40 +282,43 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
||||
'In Stock (>20)',
|
||||
'Low Stock (<10)',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-4 font-black text-slate-400 uppercase tracking-widest text-[10px] text-center">Actuals</th>
|
||||
<th className="px-6 py-4 font-black text-slate-400 uppercase tracking-widest text-[10px] text-center">Forecast</th>
|
||||
<th className="px-6 py-4 font-black text-slate-400 uppercase tracking-widest text-[10px] text-center">Avg/Mo</th>
|
||||
<th className="px-6 py-4 font-black text-slate-400 uppercase tracking-widest text-[10px]">Trend</th>
|
||||
<th className="px-6 py-4 font-black text-slate-400 uppercase tracking-widest text-[10px] text-center">Accuracy</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{finalFilteredData.slice(0, displayCount).map(p => (
|
||||
<ForecastRow
|
||||
key={p.asin}
|
||||
item={p}
|
||||
activeMonths={activeMonths}
|
||||
top50Ranking={top50Ranking}
|
||||
top50Mode={top50Mode}
|
||||
stockMap={stockMap}
|
||||
vendorStockMap={vendorStockMap}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{displayCount < finalFilteredData.length && (
|
||||
<div className="p-6 text-center bg-slate-800/20">
|
||||
<button onClick={() => setDisplayCount(prev => prev + 50)} className="px-6 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-xs font-black uppercase tracking-widest">Load More</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="flex-1 overflow-hidden flex flex-col min-h-0 bg-slate-950/50">
|
||||
<div className="flex-1 overflow-auto min-h-0 custom-scrollbar relative">
|
||||
<table className="w-full text-left border-collapse relative">
|
||||
<thead className="sticky top-0 z-20 bg-slate-950 shadow-sm text-xs font-bold text-slate-400 uppercase tracking-wider">
|
||||
<tr>
|
||||
<th className="px-6 py-4 font-black text-white w-[35%]">Product Info</th>
|
||||
<th className="px-6 py-4 text-center w-[12%]">Forecast (Period)</th>
|
||||
<th className="px-6 py-4 text-center w-[12%]">Actual Units Sales 2026</th>
|
||||
<th className="px-6 py-4 text-center w-[15%]">FC 26 Achievement</th>
|
||||
<th className="px-6 py-4 text-center w-[26%]">YTD Achievement</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
{data.map(item => (
|
||||
<ForecastRow
|
||||
key={item.asin}
|
||||
item={item}
|
||||
activeMonths={filters.month.length > 0 ? filters.month : MONTH_ORDER}
|
||||
top50Ranking={top50Ranking}
|
||||
top50Mode={top50Mode}
|
||||
stockMap={stockMap}
|
||||
vendorStockMap={vendorStockMap}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{data.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-slate-500">
|
||||
<p className="text-lg">No forecast data found for current filters</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Filter Panel (In-Column Stock Filter is handled inside components if needed, or we can add a toolbar later) */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForecastView;
|
||||
export default ForecastView;
|
||||
|
||||
Reference in New Issue
Block a user