From d40740fce7ac720f5d684e2544cec3007b0803f6 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Wed, 28 Jan 2026 15:11:47 +0100 Subject: [PATCH] Optimize performance (persistent tabs, pagination) and add sortable columns + stock badges to ForecastView --- App.tsx | 49 ++++++++------ components/ForecastView.tsx | 131 ++++++++++++++++++++++++++---------- 2 files changed, 125 insertions(+), 55 deletions(-) diff --git a/App.tsx b/App.tsx index 786df01..ba00992 100644 --- a/App.tsx +++ b/App.tsx @@ -646,9 +646,12 @@ const App: React.FC = () => { <>
- {view === 'dashboard' && } - {view === 'table' && ( - }> +
+ +
+ + }> +
0} @@ -658,10 +661,11 @@ const App: React.FC = () => { top50Ranking={top50Ranking2025} top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'} /> - - )} - {view === 'weekly' && ( - }> +
+
+ + }> +
{ top50Ranking={top50Ranking2025} velocityMap={velocityMap} /> - - )} - {view === 'movers' && ( - }> +
+
+ + }> +
- - )} - {view === 'ads' && ( - }> +
+
+ + }> +
{ onVendorStockFilterChange={(s) => setFilters(prev => ({ ...prev, vendorStock: s }))} top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'} /> - - )} - {view === 'forecast' && ( - }> +
+
+ + }> +
{ onVendorStockFilterChange={(s) => setFilters(prev => ({ ...prev, vendorStock: s }))} top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'} /> - - )} +
+
)} diff --git a/components/ForecastView.tsx b/components/ForecastView.tsx index 86d5273..492f2b9 100644 --- a/components/ForecastView.tsx +++ b/components/ForecastView.tsx @@ -96,6 +96,10 @@ const ForecastRow: React.FC<{ {item.line} + {/* Own Stock Indicator */} + {stockMap && ( + + )} {/* WOC Indicator preserved */} @@ -161,30 +165,26 @@ const ForecastView: React.FC = ({ 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 = ({ 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 ; + return {sortConfig.direction === 'desc' ? '↓' : '↑'}; + }; return (
@@ -318,12 +359,13 @@ const ForecastView: React.FC = ({
-
+

- Product Performance Comparison (v2.4 Final) + Product Performance Comparison (v2.5 optimized)

+
Showing {paginatedData.length} of {processedData.length}
{top50Ranking && (top50Ranking.eu.size > 0 || top50Ranking.uk.size > 0) && (
+
+ )} + + {processedData.length === 0 && (

No forecast data found for current filters