feat: add Top 50 filter and sorting to Forecast View

This commit is contained in:
Christian Vidal Wolf
2026-01-26 17:01:41 +01:00
parent 6379d4a37d
commit 7e2ed0955a
+82 -20
View File
@@ -1,5 +1,5 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState, useEffect } from 'react';
import { ProductForecastData, FilterState } from '../types'; import { ProductForecastData, FilterState } from '../types';
import { import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
@@ -30,6 +30,17 @@ const Top50Badge: React.FC<{ rank: number; label: string; theme?: 'amber' | 'ind
const ForecastView: React.FC<ForecastViewProps> = ({ data, filters, top50Ranking }) => { const ForecastView: React.FC<ForecastViewProps> = ({ data, filters, top50Ranking }) => {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu');
// Auto-detect best Top 50 mode based on data
useEffect(() => {
const hasUK = data.some(p => p.line.toLowerCase().includes('uk') || (p.monthlyData && p.actualUnits > 0 && filters.customer.some(c => c.toLowerCase().includes('uk'))));
const hasEU = data.some(p => !p.line.toLowerCase().includes('uk'));
// Simplified detection: if we have a lot of EU products, default EU, otherwise check UK
if (hasUK && !hasEU) setTop50Mode('uk');
}, [data, filters.customer]);
// Determine the "Current Month" based on available 2026 data // Determine the "Current Month" based on available 2026 data
const lastDataMonthIdx = useMemo(() => { const lastDataMonthIdx = useMemo(() => {
@@ -84,18 +95,41 @@ const ForecastView: React.FC<ForecastViewProps> = ({ data, filters, top50Ranking
}, [data]); }, [data]);
const filteredProducts = useMemo(() => { const filteredProducts = useMemo(() => {
if (!searchTerm) return data; let result = data;
const s = searchTerm.toLowerCase();
return data.filter(p => if (searchTerm) {
p.asin.toLowerCase().includes(s) || const s = searchTerm.toLowerCase();
p.sku.toLowerCase().includes(s) || result = result.filter(p =>
p.title.toLowerCase().includes(s) p.asin.toLowerCase().includes(s) ||
); p.sku.toLowerCase().includes(s) ||
}, [data, searchTerm]); p.title.toLowerCase().includes(s)
);
}
if (showOnlyTop50 && top50Ranking) {
const currentRankMap = top50Mode === 'eu' ? top50Ranking.eu : top50Ranking.uk;
result = result.filter(p => currentRankMap.has(p.asin.toUpperCase()));
}
return result;
}, [data, searchTerm, showOnlyTop50, top50Mode, top50Ranking]);
const sortedProducts = useMemo(() => { const sortedProducts = useMemo(() => {
return [...filteredProducts].sort((a, b) => b.annualForecast - a.annualForecast); const result = [...filteredProducts];
}, [filteredProducts]);
if (showOnlyTop50 && top50Ranking) {
const currentRankMap = top50Mode === 'eu' ? top50Ranking.eu : top50Ranking.uk;
result.sort((a, b) => {
const rankA = currentRankMap.get(a.asin.toUpperCase()) || 999;
const rankB = currentRankMap.get(b.asin.toUpperCase()) || 999;
return rankA - rankB;
});
} else {
result.sort((a, b) => b.annualForecast - a.annualForecast);
}
return result;
}, [filteredProducts, showOnlyTop50, top50Mode, top50Ranking]);
return ( return (
<div className="p-6 space-y-6 max-w-7xl mx-auto animate-fade-in pb-24"> <div className="p-6 space-y-6 max-w-7xl mx-auto animate-fade-in pb-24">
@@ -167,15 +201,43 @@ const ForecastView: React.FC<ForecastViewProps> = ({ data, filters, top50Ranking
<div className="bg-slate-900 border border-border rounded-2xl shadow-xl overflow-hidden"> <div className="bg-slate-900 border border-border rounded-2xl shadow-xl overflow-hidden">
<div className="p-6 border-b border-border flex flex-col md:flex-row justify-between gap-4"> <div className="p-6 border-b border-border flex flex-col md:flex-row justify-between gap-4">
<h3 className="text-lg font-bold text-white uppercase tracking-tight">Product Performance Comparison</h3> <h3 className="text-lg font-bold text-white uppercase tracking-tight">Product Performance Comparison</h3>
<div className="relative"> <div className="flex flex-col md:flex-row items-center gap-4">
<input {/* Top 50 Toggle */}
type="text" <div className="flex items-center bg-slate-950 p-1 rounded-lg border border-slate-700">
placeholder="Search by ASIN, SKU or Title..." <button
value={searchTerm} onClick={() => setShowOnlyTop50(!showOnlyTop50)}
onChange={(e) => setSearchTerm(e.target.value)} className={`px-3 py-1.5 rounded-md text-xs font-black transition-all flex items-center gap-2 ${showOnlyTop50 ? 'bg-indigo-600 text-white shadow-lg shadow-indigo-500/20' : 'text-slate-500 hover:text-slate-300'}`}
className="bg-slate-950 border border-slate-700 rounded-lg px-10 py-2 text-sm text-slate-200 focus:outline-none focus:border-indigo-500 w-full md:w-80" >
/> <span className="text-sm">🏆</span>
<svg className="absolute left-3 top-2.5 w-4 h-4 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg> Top 50
</button>
{showOnlyTop50 && (
<div className="flex items-center ml-1 border-l border-slate-700 pl-1">
<button
onClick={() => setTop50Mode('eu')}
className={`px-2 py-1 rounded text-[10px] font-bold transition-colors ${top50Mode === 'eu' ? 'text-indigo-400 bg-indigo-500/10' : 'text-slate-600 hover:text-slate-400'}`}
>
EU
</button>
<button
onClick={() => setTop50Mode('uk')}
className={`px-2 py-1 rounded text-[10px] font-bold transition-colors ${top50Mode === 'uk' ? 'text-indigo-400 bg-indigo-500/10' : 'text-slate-600 hover:text-slate-400'}`}
>
UK
</button>
</div>
)}
</div>
<div className="relative">
<input
type="text"
placeholder="Search by ASIN, SKU or Title..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-slate-950 border border-slate-700 rounded-lg px-10 py-2 text-sm text-slate-200 focus:outline-none focus:border-indigo-500 w-full md:w-80"
/>
<svg className="absolute left-3 top-2.5 w-4 h-4 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
</div>
</div> </div>
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">