From 2a199d397e0900dbad6f68c518eb02314d6672ca Mon Sep 17 00:00:00 2001 From: "christian.vidal" Date: Fri, 30 Jan 2026 19:28:45 +0100 Subject: [PATCH] Implement Custom Numeric Filtering (>10, 5-10) for Stock/WOC columns in Weekly Sales --- components/InColumnStockFilter.tsx | 38 ++++++++++- components/WeeklyGrid.tsx | 10 +-- services/dataProcessor.ts | 106 ++++++++++++++--------------- 3 files changed, 89 insertions(+), 65 deletions(-) diff --git a/components/InColumnStockFilter.tsx b/components/InColumnStockFilter.tsx index 922c043..7c6ce11 100644 --- a/components/InColumnStockFilter.tsx +++ b/components/InColumnStockFilter.tsx @@ -22,7 +22,9 @@ export const InColumnStockFilter: React.FC = ({ icon }) => { const [isOpen, setIsOpen] = useState(false); + const [customInput, setCustomInput] = useState(''); const containerRef = useRef(null); + const inputRef = useRef(null); // Close when clicking outside useEffect(() => { @@ -50,6 +52,15 @@ export const InColumnStockFilter: React.FC = ({ setIsOpen(false); }; + const handleCustomAdd = () => { + if (customInput.trim()) { + if (!currentFilters.includes(customInput.trim())) { + onFilterChange([...currentFilters, customInput.trim()]); + } + setCustomInput(''); + } + }; + return (
{isOpen && ( -
-
+
+
{title} {currentFilters.length > 0 && ( )}
+ + {/* Custom Input */} +
+ setCustomInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleCustomAdd(); + e.stopPropagation(); // Prevent grid row clicks if any + }} + autoFocus + /> + +
{options.map(option => { const isSelected = currentFilters.includes(option); return ( diff --git a/components/WeeklyGrid.tsx b/components/WeeklyGrid.tsx index 81fcd8f..d0429a5 100644 --- a/components/WeeklyGrid.tsx +++ b/components/WeeklyGrid.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState, useEffect, useCallback, useRef } from 'react'; import * as XLSX from 'xlsx'; import { CombinedKPIs } from '../types'; -import { pivotWeeklySalesData, WeeklyPivotRow, PAN_EU_COUNTRIES } from '../services/dataProcessor'; +import { pivotWeeklySalesData, WeeklyPivotRow, PAN_EU_COUNTRIES, checkNumericConditions } from '../services/dataProcessor'; import { StockBadge } from './StockBadge'; import { InColumnStockFilter } from './InColumnStockFilter'; import { Top50Badge } from './Top50Badge'; @@ -307,13 +307,7 @@ const WeeklyGrid: React.FC = ({ 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 checkNumericConditions(woc, wocFilter); }); } diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 3669ae1..dc312e6 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -49,6 +49,55 @@ const parseUnits = (value: string): number => { const MONTH_ORDER = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +// Helper for numeric filtering (e.g. ">5", "10-20") +export const checkNumericConditions = (value: number, filters: string[]): boolean => { + if (!filters || filters.length === 0) return true; + + return filters.some(f => { + // Handle specific string labels + if (f.includes('Out of Stock') || f === 'Out of Stock') return value === 0; + if (f.includes('In Stock') && !f.includes('Low')) return value > 0; + if (f.includes('Low Stock')) return value < 10; + if (f === '< 4 Weeks') return value < 4; + if (f === '> 4 Weeks') return value >= 4; + if (f === 'Infinite Cover') return value === 999; + + const input = f.trim().toLowerCase(); + + // Range: 10-20 + if (input.includes('-') && !input.startsWith('-')) { // Avoid negative numbers confusion if possible, though simple range usually 10-20 + const parts = input.split('-').map(s => parseFloat(s.trim())); + if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) { + return value >= parts[0] && value <= parts[1]; + } + } + + // Expressions + if (input.startsWith('<=')) { + const val = parseFloat(input.substring(2)); + return !isNaN(val) && value <= val; + } + if (input.startsWith('>=')) { + const val = parseFloat(input.substring(2)); + return !isNaN(val) && value >= val; + } + if (input.startsWith('<')) { + const val = parseFloat(input.substring(1)); + return !isNaN(val) && value < val; + } + if (input.startsWith('>')) { + const val = parseFloat(input.substring(1)); + return !isNaN(val) && value > val; + } + + // Exact Match + const val = parseFloat(input); + if (!isNaN(val)) return value === val; + + return false; + }); +}; + // Comprehensive Month Mapping (English + Spanish + Short/Full) const MONTH_MAP: Record = { // English Short @@ -735,34 +784,7 @@ const checkStockFilter = (sku: string, filters: string[], stockMap?: Map { - // Smart Filters - if (f === 'Out of Stock (0)') return stockValue === 0; - if (f === 'In Stock (>0)') return stockValue > 0; - if (f === 'Low Stock (<10)') return stockValue < 10; - - // Numeric Range (e.g. ">10", "1-50") - const input = f.trim().toLowerCase(); - if (input.includes('-')) { - const [start, end] = input.split('-').map(s => parseFloat(s.trim())); - if (!isNaN(start) && !isNaN(end)) return stockValue >= start && stockValue <= end; - } else if (input.startsWith('<=')) { - const val = parseFloat(input.substring(2).trim()); - if (!isNaN(val)) return stockValue <= val; - } else if (input.startsWith('>=')) { - const val = parseFloat(input.substring(2).trim()); - if (!isNaN(val)) return stockValue >= val; - } else if (input.startsWith('<')) { - const val = parseFloat(input.substring(1).trim()); - if (!isNaN(val)) return stockValue < val; - } else if (input.startsWith('>')) { - const val = parseFloat(input.substring(1).trim()); - if (!isNaN(val)) return stockValue > val; - } - - // Exact Match - return stockValue.toString() === f; - }); + return checkNumericConditions(stockValue, filters); }; const checkVendorStockFilter = (asin: string, filters: string[], vendorStockMap?: Map, mode: 'eu' | 'uk' = 'eu'): boolean => { @@ -772,33 +794,7 @@ const checkVendorStockFilter = (asin: string, filters: string[], vendorStockMap? const data = vendorStockMap.get(asin); const stockValue = data ? (mode === 'uk' ? data.uk : data.eu) : 0; - return filters.some(f => { - if (f === 'Out of Stock (0)') return stockValue === 0; - if (f === 'In Stock (>20)') return stockValue > 20; - if (f === 'In Stock (>0)') return stockValue > 0; - if (f === 'Low Stock (<10)') return stockValue < 10; - - const input = f.trim().toLowerCase(); - if (input.includes('-')) { - const [start, end] = input.split('-').map(s => parseFloat(s.trim())); - if (!isNaN(start) && !isNaN(end)) return stockValue >= start && stockValue <= end; - } else if (input.startsWith('<=')) { - const val = parseFloat(input.substring(2).trim()); - if (!isNaN(val)) return stockValue <= val; - } else if (input.startsWith('>=')) { - const val = parseFloat(input.substring(2).trim()); - if (!isNaN(val)) return stockValue >= val; - } else if (input.startsWith('<')) { - const val = parseFloat(input.substring(1).trim()); - if (!isNaN(val)) return stockValue < val; - } else if (input.startsWith('>')) { - const val = parseFloat(input.substring(1).trim()); - if (!isNaN(val)) return stockValue > val; - } else if (!isNaN(parseFloat(input))) { - return stockValue === parseFloat(input); - } - return false; - }); + return checkNumericConditions(stockValue, filters); }; // Filter Ads Data by Country, Year, Week, ASIN, SKU, and Product Line