mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:15:24 +02:00
Optimize performance (persistent tabs, pagination) and add sortable columns + stock badges to ForecastView
This commit is contained in:
+97
-34
@@ -96,6 +96,10 @@ const ForecastRow: React.FC<{
|
||||
<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>
|
||||
{/* Own Stock Indicator */}
|
||||
{stockMap && (
|
||||
<StockBadge stock={stockMap.get(item.sku?.replace(/(DE|EN)$/i, ''))} />
|
||||
)}
|
||||
{/* WOC Indicator preserved */}
|
||||
<VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={item.avgWeeklySales} />
|
||||
</div>
|
||||
@@ -161,30 +165,26 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
||||
top50Mode
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const debouncedSearch = useMemo(() => searchTerm.toLowerCase(), [searchTerm]);
|
||||
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
||||
const [displayCount, setDisplayCount] = useState(50);
|
||||
|
||||
const lastDataMonthIdx = useMemo(() => {
|
||||
let maxDataMonth = 0;
|
||||
for (let i = MONTH_ORDER.length - 1; i >= 0; i--) {
|
||||
if (data.some(p => (p.monthlyData?.[MONTH_ORDER[i]]?.actualUnits || 0) > 0)) {
|
||||
maxDataMonth = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const currentMonth = new Date().getMonth(); // 0 for Jan
|
||||
return Math.min(maxDataMonth, currentMonth);
|
||||
}, [data]);
|
||||
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'asc' | 'desc' }>({ key: 'actualUnits', direction: 'desc' });
|
||||
|
||||
const activeMonths = useMemo(() => {
|
||||
if (filters.month && filters.month.length > 0) {
|
||||
return filters.month.map(m => m.split('-')[0]);
|
||||
}
|
||||
// Default to YTD: Current month and all before it this year
|
||||
const currentMonthIdx = new Date().getMonth();
|
||||
return MONTH_ORDER.slice(0, currentMonthIdx + 1);
|
||||
}, [filters.month]);
|
||||
|
||||
const handleSort = (key: string) => {
|
||||
setSortConfig(prev => ({
|
||||
key,
|
||||
direction: prev.key === key && prev.direction === 'desc' ? 'asc' : 'desc'
|
||||
}));
|
||||
};
|
||||
|
||||
const baseFilteredData = useMemo(() => {
|
||||
let result = data;
|
||||
if (filters.line && filters.line.length > 0) result = result.filter(p => filters.line.includes(p.line));
|
||||
@@ -225,34 +225,75 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
||||
return Array.from(dataMap.values());
|
||||
}, [baseFilteredData]);
|
||||
|
||||
const finalFilteredData = useMemo(() => {
|
||||
let result = baseFilteredData;
|
||||
const processedData = useMemo(() => {
|
||||
let result = baseFilteredData.map(item => {
|
||||
const forecastPeriod = activeMonths.reduce((sum, month) => {
|
||||
return sum + (item.monthlyData?.[month]?.forecastUnits || 0);
|
||||
}, 0);
|
||||
const ytdAchievement = forecastPeriod > 0 ? ((item.actualUnits || 0) / forecastPeriod) * 100 : 0;
|
||||
const fcAchievement = (item.annualForecast || 0) > 0 ? ((item.actualUnits || 0) / item.annualForecast) * 100 : 0;
|
||||
|
||||
return {
|
||||
...item,
|
||||
forecastPeriod,
|
||||
ytdAchievement,
|
||||
fcAchievement
|
||||
};
|
||||
});
|
||||
|
||||
if (showOnlyTop50 && top50Ranking) {
|
||||
const currentRankMap = top50Mode === 'eu' ? top50Ranking.eu : top50Ranking.uk;
|
||||
result = result.filter(p => currentRankMap.has(p.asin.toUpperCase()));
|
||||
}
|
||||
if (searchTerm) {
|
||||
const s = searchTerm.toLowerCase();
|
||||
result = result.filter(p => p.sku.toLowerCase().includes(s) || p.asin.toLowerCase().includes(s) || p.title.toLowerCase().includes(s));
|
||||
|
||||
if (debouncedSearch) {
|
||||
result = result.filter(p =>
|
||||
p.sku.toLowerCase().includes(debouncedSearch) ||
|
||||
p.asin.toLowerCase().includes(debouncedSearch) ||
|
||||
p.title.toLowerCase().includes(debouncedSearch)
|
||||
);
|
||||
}
|
||||
|
||||
// Apply Sorting
|
||||
const { key, direction } = sortConfig;
|
||||
result.sort((a: any, b: any) => {
|
||||
let valA = a[key];
|
||||
let valB = b[key];
|
||||
|
||||
if (typeof valA === 'string') valA = valA.toLowerCase();
|
||||
if (typeof valB === 'string') valB = valB.toLowerCase();
|
||||
|
||||
if (valA < valB) return direction === 'asc' ? -1 : 1;
|
||||
if (valA > valB) return direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [baseFilteredData, searchTerm, showOnlyTop50, top50Ranking, top50Mode]);
|
||||
}, [baseFilteredData, debouncedSearch, showOnlyTop50, top50Ranking, top50Mode, sortConfig, activeMonths]);
|
||||
|
||||
const paginatedData = useMemo(() => processedData.slice(0, displayCount), [processedData, displayCount]);
|
||||
|
||||
const handleExportExcel = useCallback(() => {
|
||||
const exportData = finalFilteredData.map(p => ({
|
||||
const exportData = processedData.map(p => ({
|
||||
SKU: p.sku,
|
||||
ASIN: p.asin,
|
||||
Title: p.title,
|
||||
Line: p.line,
|
||||
'Actual Units (2025)': p.actualUnits,
|
||||
'Forecast Units (2025)': p.forecastUnits,
|
||||
'Accuracy (%)': p.accuracy
|
||||
'Forecast (Period)': p.forecastPeriod,
|
||||
'Actual Units (2026)': p.actualUnits,
|
||||
'FC 26 Achievement (%)': p.fcAchievement,
|
||||
'YTD Achievement (%)': p.ytdAchievement
|
||||
}));
|
||||
const ws = XLSX.utils.json_to_sheet(exportData);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Forecast');
|
||||
XLSX.writeFile(wb, `Forecast_Export_${new Date().toISOString().slice(0, 10)}.xlsx`);
|
||||
}, [finalFilteredData]);
|
||||
}, [processedData]);
|
||||
|
||||
const SortIndicator = ({ column }: { column: string }) => {
|
||||
if (sortConfig.key !== column) return <span className="ml-1 opacity-20">↕</span>;
|
||||
return <span className="ml-1 text-indigo-400">{sortConfig.direction === 'desc' ? '↓' : '↑'}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 animate-fade-in p-6 h-full overflow-hidden">
|
||||
@@ -318,12 +359,13 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900 border border-white/10 rounded-2xl overflow-hidden shadow-2xl flex-1 flex flex-col min-h-0">
|
||||
<div className="bg-slate-900 border border-white/10 rounded-2xl overflow-hidden shadow-2xl flex-1 flex flex-col min-h-0 relative">
|
||||
<div className="p-4 border-b border-white/5 flex flex-col md:flex-row justify-between gap-4 bg-slate-800/20 shrink-0">
|
||||
<h3 className="text-lg font-bold text-white uppercase tracking-tight">
|
||||
Product Performance Comparison <span className="text-xs text-slate-500 font-normal normal-case ml-2">(v2.4 Final)</span>
|
||||
Product Performance Comparison <span className="text-xs text-slate-500 font-normal normal-case ml-2">(v2.5 optimized)</span>
|
||||
</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-[10px] font-bold text-slate-500 uppercase">Showing {paginatedData.length} of {processedData.length}</div>
|
||||
{top50Ranking && (top50Ranking.eu.size > 0 || top50Ranking.uk.size > 0) && (
|
||||
<div className="flex bg-slate-950/50 p-1 rounded-xl border border-white/10 shadow-sm">
|
||||
<button
|
||||
@@ -348,17 +390,27 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
||||
|
||||
<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">
|
||||
<thead className="sticky top-0 z-20 bg-slate-950 shadow-sm text-[10px] font-black text-slate-500 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>
|
||||
<th className="px-6 py-4 text-white w-[35%] cursor-pointer hover:bg-white/5 transition-colors" onClick={() => handleSort('sku')}>
|
||||
Product Info <SortIndicator column="sku" />
|
||||
</th>
|
||||
<th className="px-6 py-4 text-center w-[12%] cursor-pointer hover:bg-white/5 transition-colors" onClick={() => handleSort('forecastPeriod')}>
|
||||
Forecast (Period) <SortIndicator column="forecastPeriod" />
|
||||
</th>
|
||||
<th className="px-6 py-4 text-center w-[12%] cursor-pointer hover:bg-white/5 transition-colors" onClick={() => handleSort('actualUnits')}>
|
||||
Actual Units 2026 <SortIndicator column="actualUnits" />
|
||||
</th>
|
||||
<th className="px-6 py-4 text-center w-[15%] cursor-pointer hover:bg-white/5 transition-colors" onClick={() => handleSort('fcAchievement')}>
|
||||
FC 26 Achievement <SortIndicator column="fcAchievement" />
|
||||
</th>
|
||||
<th className="px-6 py-4 text-center w-[26%] cursor-pointer hover:bg-white/5 transition-colors" onClick={() => handleSort('ytdAchievement')}>
|
||||
YTD Achievement <SortIndicator column="ytdAchievement" />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
{finalFilteredData.map(item => (
|
||||
{paginatedData.map(item => (
|
||||
<ForecastRow
|
||||
key={item.asin}
|
||||
item={item}
|
||||
@@ -372,7 +424,18 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{finalFilteredData.length === 0 && (
|
||||
{displayCount < processedData.length && (
|
||||
<div className="p-8 flex justify-center">
|
||||
<button
|
||||
onClick={() => setDisplayCount(prev => prev + 50)}
|
||||
className="px-8 py-3 bg-indigo-500 hover:bg-indigo-600 text-white text-sm font-black uppercase tracking-widest rounded-xl shadow-lg transition-all active:scale-95"
|
||||
>
|
||||
Load More Products
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{processedData.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>
|
||||
|
||||
Reference in New Issue
Block a user