mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 12:25:22 +02:00
feat: add Top 50 filter and sorting to Forecast View
This commit is contained in:
+82
-20
@@ -1,5 +1,5 @@
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import { ProductForecastData, FilterState } from '../types';
|
||||
import {
|
||||
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 [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
|
||||
const lastDataMonthIdx = useMemo(() => {
|
||||
@@ -84,18 +95,41 @@ const ForecastView: React.FC<ForecastViewProps> = ({ data, filters, top50Ranking
|
||||
}, [data]);
|
||||
|
||||
const filteredProducts = useMemo(() => {
|
||||
if (!searchTerm) return data;
|
||||
const s = searchTerm.toLowerCase();
|
||||
return data.filter(p =>
|
||||
p.asin.toLowerCase().includes(s) ||
|
||||
p.sku.toLowerCase().includes(s) ||
|
||||
p.title.toLowerCase().includes(s)
|
||||
);
|
||||
}, [data, searchTerm]);
|
||||
let result = data;
|
||||
|
||||
if (searchTerm) {
|
||||
const s = searchTerm.toLowerCase();
|
||||
result = result.filter(p =>
|
||||
p.asin.toLowerCase().includes(s) ||
|
||||
p.sku.toLowerCase().includes(s) ||
|
||||
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(() => {
|
||||
return [...filteredProducts].sort((a, b) => b.annualForecast - a.annualForecast);
|
||||
}, [filteredProducts]);
|
||||
const result = [...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 (
|
||||
<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="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>
|
||||
<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 className="flex flex-col md:flex-row items-center gap-4">
|
||||
{/* Top 50 Toggle */}
|
||||
<div className="flex items-center bg-slate-950 p-1 rounded-lg border border-slate-700">
|
||||
<button
|
||||
onClick={() => setShowOnlyTop50(!showOnlyTop50)}
|
||||
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'}`}
|
||||
>
|
||||
<span className="text-sm">🏆</span>
|
||||
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 className="overflow-x-auto">
|
||||
|
||||
Reference in New Issue
Block a user