Optimize Forecast View performance and implement WOC filter across views

This commit is contained in:
Christian Vidal Wolf
2026-01-29 08:47:03 +01:00
parent 98e8bb3904
commit d19f0cddac
6 changed files with 169 additions and 12 deletions
+42
View File
@@ -21,6 +21,8 @@ interface AdsPerformanceProps {
vendorStockMap?: Map<string, { eu: number; uk: number }>;
vendorStockFilter: string[];
onVendorStockFilterChange: (newFilters: string[]) => void;
wocFilter: string[];
onWocFilterChange: (newFilters: string[]) => void;
}
type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits';
@@ -106,6 +108,8 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
onStockFilterChange,
vendorStockFilter,
onVendorStockFilterChange,
wocFilter,
onWocFilterChange,
top50Mode
}) => {
const [searchTerm, setSearchTerm] = useState('');
@@ -258,6 +262,33 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
);
}
// Apply WOC Filter
if (wocFilter && wocFilter.length > 0 && vendorStockMap) {
result = result.filter(item => {
const asin = item.asin.trim().toUpperCase();
const stockData = vendorStockMap.get(asin);
if (!stockData) return false;
const stock = top50Mode === 'uk' ? stockData.uk : stockData.eu;
const velocity = item.avgWeeklySales || 0;
let woc: number = 0;
if (velocity > 0) {
woc = stock / velocity;
} else if (stock > 0) {
woc = 999;
}
return wocFilter.some(f => {
if (f === '< 4 Weeks') return woc < 4;
if (f === '> 4 Weeks') return woc >= 4;
if (f === 'Out of Stock') return woc === 0;
if (f === 'Infinite Cover') return woc === 999;
return true;
});
});
}
if (showOnlyTop50 && top50Ranking) {
result.sort((a, b) => {
const asinA = a.asin.trim().toUpperCase();
@@ -408,6 +439,17 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
'Low Stock (<10)',
]}
/>
<InColumnStockFilter
currentFilters={wocFilter}
onFilterChange={onWocFilterChange}
title="Week Coverage"
options={[
'< 4 Weeks',
'> 4 Weeks',
'Out of Stock',
'Infinite Cover'
]}
/>
</div>
</div>
</th>
+58 -4
View File
@@ -23,6 +23,8 @@ interface ForecastViewProps {
onStockFilterChange: (newFilters: string[]) => void;
vendorStockFilter: string[];
onVendorStockFilterChange: (newFilters: string[]) => void;
wocFilter: string[];
onWocFilterChange: (newFilters: string[]) => void;
}
const MONTH_ORDER = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
@@ -169,10 +171,20 @@ const ForecastView: React.FC<ForecastViewProps> = ({
onStockFilterChange,
vendorStockFilter,
onVendorStockFilterChange,
wocFilter,
onWocFilterChange,
top50Mode
}) => {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useMemo(() => searchTerm.toLowerCase(), [searchTerm]);
const [debouncedSearch, setDebouncedSearch] = useState('');
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearch(searchTerm.toLowerCase());
}, 300);
return () => clearTimeout(timer);
}, [searchTerm]);
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
const [displayCount, setDisplayCount] = useState(50);
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'asc' | 'desc' }>({ key: 'actualUnits', direction: 'desc' });
@@ -232,8 +244,8 @@ const ForecastView: React.FC<ForecastViewProps> = ({
return Array.from(dataMap.values());
}, [baseFilteredData]);
const processedData = useMemo(() => {
let result = baseFilteredData.map(item => {
const calculatedData = useMemo(() => {
return baseFilteredData.map(item => {
const forecastPeriod = activeMonths.reduce((sum, month) => {
return sum + (item.monthlyData?.[month]?.forecastUnits || 0);
}, 0);
@@ -247,6 +259,10 @@ const ForecastView: React.FC<ForecastViewProps> = ({
fcAchievement
};
});
}, [baseFilteredData, activeMonths]);
const processedData = useMemo(() => {
let result = [...calculatedData];
if (showOnlyTop50 && top50Ranking) {
const currentRankMap = top50Mode === 'eu' ? top50Ranking.eu : top50Ranking.uk;
@@ -261,6 +277,33 @@ const ForecastView: React.FC<ForecastViewProps> = ({
);
}
// Apply WOC Filter
if (wocFilter && wocFilter.length > 0 && vendorStockMap) {
result = result.filter(p => {
const asin = p.asin.trim().toUpperCase();
const stockData = vendorStockMap.get(asin);
if (!stockData) return false;
const stock = top50Mode === 'uk' ? stockData.uk : stockData.eu;
const velocity = p.avgWeeklySales || 0;
let woc: number = 0;
if (velocity > 0) {
woc = stock / velocity;
} else if (stock > 0) {
woc = 999;
}
return wocFilter.some(f => {
if (f === '< 4 Weeks') return woc < 4;
if (f === '> 4 Weeks') return woc >= 4;
if (f === 'Out of Stock') return woc === 0;
if (f === 'Infinite Cover') return woc === 999;
return true;
});
});
}
// Apply Sorting
const { key, direction } = sortConfig;
result.sort((a: any, b: any) => {
@@ -276,7 +319,7 @@ const ForecastView: React.FC<ForecastViewProps> = ({
});
return result;
}, [baseFilteredData, debouncedSearch, showOnlyTop50, top50Ranking, top50Mode, sortConfig, activeMonths]);
}, [calculatedData, debouncedSearch, showOnlyTop50, top50Ranking, top50Mode, sortConfig]);
const paginatedData = useMemo(() => processedData.slice(0, displayCount), [processedData, displayCount]);
@@ -393,6 +436,17 @@ const ForecastView: React.FC<ForecastViewProps> = ({
className="bg-slate-950 border border-white/10 rounded-xl px-4 py-2 text-sm text-white focus:outline-none w-64"
/>
<button onClick={handleExportExcel} className="p-2 bg-slate-800 hover:bg-slate-700 text-white rounded-xl border border-white/5 transition-all"><DownloadIcon /></button>
<InColumnStockFilter
currentFilters={wocFilter}
onFilterChange={onWocFilterChange}
title="Week Coverage"
options={[
'< 4 Weeks',
'> 4 Weeks',
'Out of Stock',
'Infinite Cover'
]}
/>
</div>
</div>
+43 -1
View File
@@ -21,6 +21,8 @@ interface WeeklyGridProps {
vendorStockMap?: Map<string, { eu: number; uk: number }>;
vendorStockFilter: string[];
onVendorStockFilterChange: (newFilters: string[]) => void;
wocFilter: string[];
onWocFilterChange: (newFilters: string[]) => void;
velocityMap?: Map<string, number>;
}
@@ -152,6 +154,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
onStockFilterChange,
vendorStockFilter,
onVendorStockFilterChange,
wocFilter,
onWocFilterChange,
customerFilters,
top50Mode,
velocityMap
@@ -272,8 +276,35 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
}
}
// WOC Filter
if (wocFilter && wocFilter.length > 0 && velocityMap && vendorStockMap) {
result = result.filter(r => {
const asin = r.asin.trim().toUpperCase();
const stockData = vendorStockMap.get(asin);
if (!stockData) return false;
const stock = top50Mode === 'uk' ? stockData.uk : stockData.eu;
const velocity = velocityMap.get(asin) || 0;
let woc: number = 0;
if (velocity > 0) {
woc = stock / velocity;
} else if (stock > 0) {
woc = 999;
}
return wocFilter.some(f => {
if (f === '< 4 Weeks') return woc < 4;
if (f === '> 4 Weeks') return woc >= 4;
if (f === 'Out of Stock') return woc === 0;
if (f === 'Infinite Cover') return woc === 999;
return true;
});
});
}
return result;
}, [rows, debouncedSearch, showOnlyTop50, top50Ranking, growthFilterMode, growthThreshold, sortConfig, weeks, top50Mode]);
}, [rows, debouncedSearch, showOnlyTop50, top50Ranking, growthFilterMode, growthThreshold, sortConfig, weeks, top50Mode, wocFilter, velocityMap, vendorStockMap]);
// 2. Sort results
const sortedRows = useMemo(() => {
@@ -499,6 +530,17 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
'Low Stock (<10)',
]}
/>
<InColumnStockFilter
currentFilters={wocFilter}
onFilterChange={onWocFilterChange}
title="Week Coverage"
options={[
'< 4 Weeks',
'> 4 Weeks',
'Out of Stock',
'Infinite Cover'
]}
/>
</div>
{sortConfig?.metric === 'rank' && (
<span className="text-amber-500 font-black text-sm animate-bounce-subtle">