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 DataGrid + fix BuyBox badge hover
This commit is contained in:
+114
-25
@@ -242,11 +242,13 @@ const ExpandableChartCard: React.FC<{ title: string; children: React.ReactNode;
|
||||
|
||||
const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, stockMap, vendorStockMap, top50Ranking, top50Mode, velocityMap, buyBoxLostMap, defaultSort }) => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [sortConfig, setSortConfig] = useState<SortConfig>(defaultSort || { key: null, direction: 'desc' });
|
||||
const [showChart, setShowChart] = useState(true);
|
||||
const [visibleMetrics, setVisibleMetrics] = useState<('sellOut' | 'units')[]>(['sellOut', 'units']);
|
||||
const [showAdsMetrics, setShowAdsMetrics] = useState(true);
|
||||
const [showAttributedSales, setShowAttributedSales] = useState(false);
|
||||
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
||||
const [showDimensionMenu, setShowDimensionMenu] = useState(false);
|
||||
const dimensionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -377,6 +379,29 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
||||
const processedRows = useMemo(() => {
|
||||
let result = pivotRows;
|
||||
|
||||
// NEW: Filter for Top 50
|
||||
if (showOnlyTop50 && top50Ranking) {
|
||||
const rankMap = top50Mode === 'eu' ? top50Ranking.eu : top50Ranking.uk;
|
||||
result = result.filter(row => {
|
||||
const asin = (row.asin || '').trim().toUpperCase();
|
||||
return asin && rankMap.has(asin);
|
||||
});
|
||||
}
|
||||
|
||||
// 0. Global Search Filter
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase().trim();
|
||||
result = result.filter(row => {
|
||||
return (
|
||||
(row.sku?.toLowerCase().includes(term)) ||
|
||||
(row.asin?.toLowerCase().includes(term)) ||
|
||||
(row.title?.toLowerCase().includes(term)) ||
|
||||
(row.line?.toLowerCase().includes(term)) ||
|
||||
(row.customer?.toLowerCase().includes(term))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 1. Filter
|
||||
if (rowFilters.length > 0) {
|
||||
const latestYear = years[0];
|
||||
@@ -473,10 +498,18 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
||||
if (valA > valB) return sortConfig.direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
} else if (showOnlyTop50 && top50Ranking) {
|
||||
// Default sort by Rank when Top 50 is active
|
||||
const rankMap = top50Mode === 'eu' ? top50Ranking.eu : top50Ranking.uk;
|
||||
result = [...result].sort((a, b) => {
|
||||
const rankA = a.asin ? rankMap.get(a.asin.toUpperCase()) || 999 : 999;
|
||||
const rankB = b.asin ? rankMap.get(b.asin.toUpperCase()) || 999 : 999;
|
||||
return rankA - rankB;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [pivotRows, rowFilters, sortConfig, years]);
|
||||
}, [pivotRows, rowFilters, sortConfig, years, searchTerm, showOnlyTop50, top50Ranking, top50Mode]);
|
||||
|
||||
// Calculate aggregated totals for the header row
|
||||
const filteredTotalsByYear = useMemo(() => {
|
||||
@@ -592,7 +625,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
||||
// Reset pagination when filters change
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [rowFilters, data, effectiveDimensions]);
|
||||
}, [rowFilters, data, effectiveDimensions, searchTerm]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-[95vw] mx-auto animate-fade-in pb-24">
|
||||
@@ -798,6 +831,20 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Top 50 Toggle */}
|
||||
{top50Ranking && (top50Ranking.eu.size > 0 || top50Ranking.uk.size > 0) && (
|
||||
<button
|
||||
onClick={() => setShowOnlyTop50(!showOnlyTop50)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-bold transition-all border shadow-sm ${showOnlyTop50 ? 'bg-gradient-to-r from-amber-600 to-orange-600 text-white border-amber-500 shadow-amber-500/20' : 'bg-slate-800 text-slate-400 border-slate-700 hover:text-amber-400 hover:bg-slate-700'}`}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-4 h-4">
|
||||
<path fillRule="evenodd" d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.007 5.404.433c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.433 2.082-5.006z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Top 50 ({top50Mode.toUpperCase()})</span>
|
||||
<span className="sm:hidden">Top 50</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Attributed Sales Toggle - Only show when ads data is loaded and ads metrics are shown */}
|
||||
{adsSummary && (
|
||||
<button
|
||||
@@ -1108,7 +1155,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
||||
)}
|
||||
</div>
|
||||
: dim === 'asin' || dim === 'sku'
|
||||
? <div className="flex items-center gap-1.5">
|
||||
? <div className="flex items-center gap-2 min-w-max">
|
||||
{(() => {
|
||||
const asin = (row.asin || '').trim().toUpperCase();
|
||||
if (asin && top50Ranking) {
|
||||
@@ -1122,8 +1169,10 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
<span>{(row[dim as keyof PivotRow] as string) || '-'}</span>
|
||||
<BuyBoxWarningBadge asin={row.asin} buyBoxLostMap={buyBoxLostMap} />
|
||||
<span className="whitespace-nowrap">{(row[dim as keyof PivotRow] as string) || '-'}</span>
|
||||
<div className="shrink-0">
|
||||
<BuyBoxWarningBadge asin={row.asin} buyBoxLostMap={buyBoxLostMap} />
|
||||
</div>
|
||||
</div>
|
||||
: (row[dim as keyof PivotRow] as string) || '-'
|
||||
}
|
||||
@@ -1206,7 +1255,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
||||
|
||||
{paginatedRows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={effectiveDimensions.length + (years.length * 2)} className="px-6 py-12 text-center text-slate-500 italic">
|
||||
<td colSpan={effectiveDimensions.length + (years.length * 2) + (showAdsMetrics && adsSummary ? years.length * 2 : 0)} className="px-6 py-12 text-center text-slate-500 italic">
|
||||
No data matches your filters.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1215,26 +1264,66 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="p-4 border-t border-border bg-slate-900/50 flex flex-col sm:flex-row justify-between items-center gap-4 rounded-b-xl">
|
||||
<div className="text-sm text-slate-500">
|
||||
Page {currentPage} of {totalPages || 1}
|
||||
{/* Pagination and Search */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-slate-900/50 p-4 rounded-xl border border-white/5 shadow-lg">
|
||||
<div className="flex flex-wrap items-center gap-4 w-full md:w-auto">
|
||||
{/* Search Bar */}
|
||||
<div className="relative w-full md:w-80">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search SKU, Title, or ASIN..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all pl-10"
|
||||
/>
|
||||
<svg className="absolute left-3 top-2.5 w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
{searchTerm && (
|
||||
<button
|
||||
onClick={() => setSearchTerm('')}
|
||||
className="absolute right-3 top-2.5 text-slate-500 hover:text-white"
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="h-8 w-px bg-white/10 hidden md:block" />
|
||||
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-xl text-sm font-black uppercase tracking-widest transition-all shadow-lg active:scale-95 border border-emerald-500/50"
|
||||
>
|
||||
<DownloadIcon />
|
||||
<span>Export Excel</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1 bg-slate-800 border border-slate-700 rounded text-sm text-slate-300 disabled:opacity-50 hover:bg-slate-700"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages || totalPages === 0}
|
||||
className="px-3 py-1 bg-slate-800 border border-slate-700 rounded text-sm text-slate-300 disabled:opacity-50 hover:bg-slate-700"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-4 w-full md:w-auto justify-between md:justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="p-2 bg-slate-800 hover:bg-slate-700 disabled:opacity-30 disabled:hover:bg-slate-800 rounded-lg transition-colors border border-white/5"
|
||||
>
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
|
||||
</button>
|
||||
<div className="bg-slate-950 border border-white/10 px-4 py-2 rounded-xl">
|
||||
<span className="text-xs font-black text-white">{currentPage} / {totalPages || 1}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
||||
disabled={currentPage >= totalPages}
|
||||
className="p-2 bg-slate-800 hover:bg-slate-700 disabled:opacity-30 disabled:hover:bg-slate-800 rounded-lg transition-colors border border-white/5"
|
||||
>
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:block text-xs font-bold text-slate-500 uppercase tracking-widest">
|
||||
{processedRows.length} Records found
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user