mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14:05:24 +02:00
feat: add growth filtering in Weekly Sales grid
- Added Gaining/Dropping/Stable filter modes with configurable threshold - Filter compares unit volume between active week and previous week - Updated toolbar UI with growth filter dropdown and threshold input
This commit is contained in:
+95
-28
@@ -19,6 +19,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
|
|||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [growthFilterMode, setGrowthFilterMode] = useState<'all' | 'up' | 'down' | 'stable'>('all');
|
||||||
|
const [growthThreshold, setGrowthThreshold] = useState(10);
|
||||||
|
|
||||||
// Default sort: most recent week, descending, units
|
// Default sort: most recent week, descending, units
|
||||||
const [sortConfig, setSortConfig] = useState<SortConfig>(() => {
|
const [sortConfig, setSortConfig] = useState<SortConfig>(() => {
|
||||||
@@ -46,15 +48,50 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
|
|||||||
|
|
||||||
// 1. Filter by search term
|
// 1. Filter by search term
|
||||||
const filteredRows = useMemo(() => {
|
const filteredRows = useMemo(() => {
|
||||||
if (!searchTerm) return rows;
|
let result = rows;
|
||||||
const lowSearch = searchTerm.toLowerCase();
|
|
||||||
return rows.filter(r =>
|
// 1. Search Filter
|
||||||
r.sku.toLowerCase().includes(lowSearch) ||
|
if (searchTerm) {
|
||||||
r.asin.toLowerCase().includes(lowSearch) ||
|
const lowSearch = searchTerm.toLowerCase();
|
||||||
r.title.toLowerCase().includes(lowSearch) ||
|
result = result.filter(r =>
|
||||||
r.line.toLowerCase().includes(lowSearch)
|
r.sku.toLowerCase().includes(lowSearch) ||
|
||||||
);
|
r.asin.toLowerCase().includes(lowSearch) ||
|
||||||
}, [rows, searchTerm]);
|
r.title.toLowerCase().includes(lowSearch) ||
|
||||||
|
r.line.toLowerCase().includes(lowSearch)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Growth Filter (if active)
|
||||||
|
if (growthFilterMode !== 'all' && sortConfig) {
|
||||||
|
const currentWeek = sortConfig.key;
|
||||||
|
const currentWeekIdx = weeks.indexOf(currentWeek);
|
||||||
|
const prevWeek = weeks[currentWeekIdx + 1];
|
||||||
|
|
||||||
|
if (prevWeek) {
|
||||||
|
result = result.filter(r => {
|
||||||
|
const currentUnits = r.unitsByWeek[currentWeek] || 0;
|
||||||
|
const prevUnits = r.unitsByWeek[prevWeek] || 0;
|
||||||
|
|
||||||
|
// Handle edge case: units were 0 in previous week
|
||||||
|
if (prevUnits === 0) {
|
||||||
|
if (currentUnits === 0) return growthFilterMode === 'stable';
|
||||||
|
return growthFilterMode === 'up'; // Gained from 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate percentage growth
|
||||||
|
const growth = ((currentUnits - prevUnits) / prevUnits) * 100;
|
||||||
|
|
||||||
|
// For 'down', we use the absolute threshold to check if it dropped BY at least that amount
|
||||||
|
if (growthFilterMode === 'up') return growth >= growthThreshold;
|
||||||
|
if (growthFilterMode === 'down') return growth <= -Math.abs(growthThreshold);
|
||||||
|
if (growthFilterMode === 'stable') return Math.abs(growth) < growthThreshold;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [rows, searchTerm, growthFilterMode, growthThreshold, sortConfig, weeks]);
|
||||||
|
|
||||||
// 2. Sort results
|
// 2. Sort results
|
||||||
const sortedRows = useMemo(() => {
|
const sortedRows = useMemo(() => {
|
||||||
@@ -109,41 +146,71 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 animate-fade-in">
|
<div className="flex flex-col gap-4 animate-fade-in">
|
||||||
{/* Toolbar: Search & Pagination */}
|
{/* Toolbar: Search & Pagination */}
|
||||||
<div className="flex flex-col lg:flex-row justify-between items-center gap-4 bg-slate-900/50 p-4 border border-white/10 rounded-xl">
|
<div className="flex flex-col lg:flex-row justify-between items-center gap-4 bg-slate-900 border border-white/10 p-4 rounded-xl shadow-lg">
|
||||||
<div className="relative w-full md:w-80">
|
<div className="flex flex-wrap items-center gap-4 w-full lg:w-auto">
|
||||||
<input
|
{/* Search Input */}
|
||||||
type="text"
|
<div className="relative w-full md:w-80">
|
||||||
placeholder="Search SKU, Title, or Line..."
|
<input
|
||||||
value={searchTerm}
|
type="text"
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
placeholder="Search SKU, Title, or ASIN..."
|
||||||
className="w-full bg-slate-950 border border-white/10 rounded-lg px-4 py-2 text-base text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all pl-10"
|
value={searchTerm}
|
||||||
/>
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
<svg className="absolute left-3 top-3 w-5 h-5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
className="w-full bg-slate-950 border border-white/10 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all pl-10"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
/>
|
||||||
</svg>
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Growth Filter */}
|
||||||
|
<div className="flex items-center gap-2 bg-slate-950/50 p-1.5 px-3 rounded-lg border border-white/5">
|
||||||
|
<span className="text-[11px] font-black text-slate-500 uppercase tracking-widest">Growth Filter</span>
|
||||||
|
<select
|
||||||
|
value={growthFilterMode}
|
||||||
|
onChange={(e) => setGrowthFilterMode(e.target.value as any)}
|
||||||
|
className="bg-slate-900 border border-white/10 rounded px-2 py-1 text-xs text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<option value="all">All Products</option>
|
||||||
|
<option value="up">Gaining</option>
|
||||||
|
<option value="down">Dropping</option>
|
||||||
|
<option value="stable">Stable</option>
|
||||||
|
</select>
|
||||||
|
{growthFilterMode !== 'all' && (
|
||||||
|
<div className="flex items-center gap-1.5 ml-1 border-l border-white/10 pl-3">
|
||||||
|
<span className="text-[10px] text-slate-500 font-bold">Thresh:</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={growthThreshold}
|
||||||
|
onChange={(e) => setGrowthThreshold(Number(e.target.value))}
|
||||||
|
className="w-12 bg-slate-900 border border-white/10 rounded px-1.5 py-1 text-xs text-white text-center focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
<span className="text-[10px] text-slate-500">%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-sm text-slate-500">
|
<span className="text-sm text-slate-500 whitespace-nowrap">
|
||||||
Showing {Math.min(sortedRows.length, (currentPage - 1) * ROWS_PER_PAGE + 1)}-{Math.min(sortedRows.length, currentPage * ROWS_PER_PAGE)} of {sortedRows.length}
|
Showing {Math.min(filteredRows.length, (currentPage - 1) * ROWS_PER_PAGE + 1)}-{Math.min(filteredRows.length, currentPage * ROWS_PER_PAGE)} of {filteredRows.length}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
className="p-2.5 bg-slate-800 hover:bg-slate-700 disabled:opacity-30 disabled:hover:bg-slate-800 rounded-lg transition-colors border border-white/5"
|
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-5 h-5 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>
|
<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>
|
</button>
|
||||||
<span className="bg-slate-950 border border-white/10 px-4 py-2 rounded-lg text-sm font-bold text-white min-w-[70px] text-center">
|
<span className="bg-slate-950 border border-white/10 px-3 py-1.5 rounded-lg text-xs font-bold text-white min-w-[60px] text-center">
|
||||||
{currentPage} / {Math.max(1, totalPages)}
|
{currentPage} / {Math.max(1, totalPages)}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||||
disabled={currentPage >= totalPages}
|
disabled={currentPage >= totalPages}
|
||||||
className="p-2.5 bg-slate-800 hover:bg-slate-700 disabled:opacity-30 disabled:hover:bg-slate-800 rounded-lg transition-colors border border-white/5"
|
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-5 h-5 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>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user