mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 18:55:24 +02:00
feat: add Top 50 filter and sorting to DataGrid + fix BuyBox badge hover
This commit is contained in:
@@ -35,7 +35,7 @@ export const BuyBoxWarningBadge: React.FC<BuyBoxWarningBadgeProps> = ({ asin, bu
|
|||||||
|
|
||||||
{/* Hover Popup */}
|
{/* Hover Popup */}
|
||||||
{isHovered && (
|
{isHovered && (
|
||||||
<div className="absolute z-[100] left-0 bottom-full mb-1 w-64 bg-slate-900 border border-amber-500/50 rounded-xl shadow-2xl shadow-black animate-fade-in overflow-hidden pointer-events-none">
|
<div className="absolute z-[100] left-0 bottom-full mb-1 w-64 bg-slate-900 border border-amber-500/50 rounded-xl shadow-2xl shadow-black animate-fade-in overflow-hidden">
|
||||||
<div className="bg-amber-500/20 px-3 py-2 border-b border-white/10">
|
<div className="bg-amber-500/20 px-3 py-2 border-b border-white/10">
|
||||||
<span className="text-amber-400 text-[10px] font-black uppercase tracking-widest flex items-center gap-2">
|
<span className="text-amber-400 text-[10px] font-black uppercase tracking-widest flex items-center gap-2">
|
||||||
<span>⚠️</span> Buy Box Lost Reason
|
<span>⚠️</span> Buy Box Lost Reason
|
||||||
|
|||||||
+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 DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, stockMap, vendorStockMap, top50Ranking, top50Mode, velocityMap, buyBoxLostMap, defaultSort }) => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [sortConfig, setSortConfig] = useState<SortConfig>(defaultSort || { key: null, direction: 'desc' });
|
const [sortConfig, setSortConfig] = useState<SortConfig>(defaultSort || { key: null, direction: 'desc' });
|
||||||
const [showChart, setShowChart] = useState(true);
|
const [showChart, setShowChart] = useState(true);
|
||||||
const [visibleMetrics, setVisibleMetrics] = useState<('sellOut' | 'units')[]>(['sellOut', 'units']);
|
const [visibleMetrics, setVisibleMetrics] = useState<('sellOut' | 'units')[]>(['sellOut', 'units']);
|
||||||
const [showAdsMetrics, setShowAdsMetrics] = useState(true);
|
const [showAdsMetrics, setShowAdsMetrics] = useState(true);
|
||||||
const [showAttributedSales, setShowAttributedSales] = useState(false);
|
const [showAttributedSales, setShowAttributedSales] = useState(false);
|
||||||
|
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
||||||
const [showDimensionMenu, setShowDimensionMenu] = useState(false);
|
const [showDimensionMenu, setShowDimensionMenu] = useState(false);
|
||||||
const dimensionRef = useRef<HTMLDivElement>(null);
|
const dimensionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -377,6 +379,29 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
|||||||
const processedRows = useMemo(() => {
|
const processedRows = useMemo(() => {
|
||||||
let result = pivotRows;
|
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
|
// 1. Filter
|
||||||
if (rowFilters.length > 0) {
|
if (rowFilters.length > 0) {
|
||||||
const latestYear = years[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;
|
if (valA > valB) return sortConfig.direction === 'asc' ? 1 : -1;
|
||||||
return 0;
|
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;
|
return result;
|
||||||
}, [pivotRows, rowFilters, sortConfig, years]);
|
}, [pivotRows, rowFilters, sortConfig, years, searchTerm, showOnlyTop50, top50Ranking, top50Mode]);
|
||||||
|
|
||||||
// Calculate aggregated totals for the header row
|
// Calculate aggregated totals for the header row
|
||||||
const filteredTotalsByYear = useMemo(() => {
|
const filteredTotalsByYear = useMemo(() => {
|
||||||
@@ -592,7 +625,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
|||||||
// Reset pagination when filters change
|
// Reset pagination when filters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}, [rowFilters, data, effectiveDimensions]);
|
}, [rowFilters, data, effectiveDimensions, searchTerm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-[95vw] mx-auto animate-fade-in pb-24">
|
<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>
|
</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 */}
|
{/* Attributed Sales Toggle - Only show when ads data is loaded and ads metrics are shown */}
|
||||||
{adsSummary && (
|
{adsSummary && (
|
||||||
<button
|
<button
|
||||||
@@ -1108,7 +1155,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
: dim === 'asin' || dim === 'sku'
|
: 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();
|
const asin = (row.asin || '').trim().toUpperCase();
|
||||||
if (asin && top50Ranking) {
|
if (asin && top50Ranking) {
|
||||||
@@ -1122,8 +1169,10 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})()}
|
})()}
|
||||||
<span>{(row[dim as keyof PivotRow] as string) || '-'}</span>
|
<span className="whitespace-nowrap">{(row[dim as keyof PivotRow] as string) || '-'}</span>
|
||||||
<BuyBoxWarningBadge asin={row.asin} buyBoxLostMap={buyBoxLostMap} />
|
<div className="shrink-0">
|
||||||
|
<BuyBoxWarningBadge asin={row.asin} buyBoxLostMap={buyBoxLostMap} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
: (row[dim as keyof PivotRow] as string) || '-'
|
: (row[dim as keyof PivotRow] as string) || '-'
|
||||||
}
|
}
|
||||||
@@ -1206,7 +1255,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
|||||||
|
|
||||||
{paginatedRows.length === 0 && (
|
{paginatedRows.length === 0 && (
|
||||||
<tr>
|
<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.
|
No data matches your filters.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1215,26 +1264,66 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination and Search */}
|
||||||
<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="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="text-sm text-slate-500">
|
<div className="flex flex-wrap items-center gap-4 w-full md:w-auto">
|
||||||
Page {currentPage} of {totalPages || 1}
|
{/* 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>
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
<div className="flex items-center gap-4 w-full md:w-auto justify-between md:justify-end">
|
||||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
<div className="flex items-center gap-2">
|
||||||
disabled={currentPage === 1}
|
<button
|
||||||
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"
|
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||||
>
|
disabled={currentPage === 1}
|
||||||
Previous
|
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"
|
||||||
</button>
|
>
|
||||||
<button
|
<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>
|
||||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
</button>
|
||||||
disabled={currentPage === totalPages || totalPages === 0}
|
<div className="bg-slate-950 border border-white/10 px-4 py-2 rounded-xl">
|
||||||
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"
|
<span className="text-xs font-black text-white">{currentPage} / {totalPages || 1}</span>
|
||||||
>
|
</div>
|
||||||
Next
|
<button
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user