diff --git a/App.tsx b/App.tsx index 7822498..18874a6 100644 --- a/App.tsx +++ b/App.tsx @@ -54,7 +54,6 @@ const App: React.FC = () => { // Modal State const [isDataModalOpen, setIsDataModalOpen] = useState(false); - // Filters State const [filters, setFilters] = useState({ customer: [], year: [], @@ -63,7 +62,9 @@ const App: React.FC = () => { asin: [], sku: [], title: [], + week: [], stock: [], + vendorStock: [], }); // Handle Data Fetch (Simplified) @@ -229,6 +230,7 @@ const App: React.FC = () => { title: [], week: [], stock: [], + vendorStock: [], }); }; @@ -405,8 +407,15 @@ const App: React.FC = () => { return metaMap; }, [rawData]); - const filteredData = useMemo(() => filterData(rawData, filters, stockMap), [rawData, filters, stockMap]); - const filteredAdsData = useMemo(() => filterAdsData(adsData, filters, globalAsinMetadata, stockMap), [adsData, filters, globalAsinMetadata, stockMap]); + const top50Mode = useMemo(() => { + const hasUK = filters.customer.includes('Amazon UK'); + const hasEU = filters.customer.some(c => ['Amazon DE', 'Amazon IT', 'Amazon FR', 'Amazon ES'].includes(c)); + if (hasUK) return 'uk'; + return 'eu'; + }, [filters.customer]); + + const filteredData = useMemo(() => filterData(rawData, filters, stockMap, vendorStockMap, top50Mode), [rawData, filters, stockMap, vendorStockMap, top50Mode]); + const filteredAdsData = useMemo(() => filterAdsData(adsData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode), [adsData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode]); const aggregatedData = useMemo(() => aggregateData(filteredData), [filteredData]); // Calculate country-aware Top 50 Best Sellers for 2025 @@ -641,6 +650,8 @@ const App: React.FC = () => { vendorStockMap={vendorStockMap} stockFilter={filters.stock} onStockFilterChange={(s) => setFilters(prev => ({ ...prev, stock: s }))} + vendorStockFilter={filters.vendorStock} + onVendorStockFilterChange={(s) => setFilters(prev => ({ ...prev, vendorStock: s }))} customerFilters={filters.customer} top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'} top50Ranking={top50Ranking2025} @@ -662,6 +673,8 @@ const App: React.FC = () => { vendorStockMap={vendorStockMap} stockFilter={filters.stock} onStockFilterChange={(s) => setFilters(prev => ({ ...prev, stock: s }))} + vendorStockFilter={filters.vendorStock} + onVendorStockFilterChange={(s) => setFilters(prev => ({ ...prev, vendorStock: s }))} top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'} /> @@ -676,6 +689,8 @@ const App: React.FC = () => { vendorStockMap={vendorStockMap} stockFilter={filters.stock} onStockFilterChange={(s) => setFilters(prev => ({ ...prev, stock: s }))} + vendorStockFilter={filters.vendorStock} + onVendorStockFilterChange={(s) => setFilters(prev => ({ ...prev, vendorStock: s }))} top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'} /> diff --git a/components/AdsPerformance.tsx b/components/AdsPerformance.tsx index 477a81b..0476e42 100644 --- a/components/AdsPerformance.tsx +++ b/components/AdsPerformance.tsx @@ -19,6 +19,8 @@ interface AdsPerformanceProps { onStockFilterChange: (newFilters: string[]) => void; top50Mode: 'eu' | 'uk'; vendorStockMap?: Map; + vendorStockFilter: string[]; + onVendorStockFilterChange: (newFilters: string[]) => void; } type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits'; @@ -64,7 +66,9 @@ const AdsRow: React.FC<{ ))} {item.asin} - SKU: {item.sku} +
+ SKU: {item.sku} +
{item.title} {stockMap && ( @@ -92,7 +96,18 @@ const AdsRow: React.FC<{ ); }); -const AdsPerformance: React.FC = ({ data, filters, top50Ranking, stockMap, vendorStockMap, stockFilter, onStockFilterChange, top50Mode }) => { +const AdsPerformance: React.FC = ({ + data, + filters, + top50Ranking, + stockMap, + vendorStockMap, + stockFilter, + onStockFilterChange, + vendorStockFilter, + onVendorStockFilterChange, + top50Mode +}) => { const [searchTerm, setSearchTerm] = useState(''); const [showOnlyTop50, setShowOnlyTop50] = useState(false); const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: 'asc' | 'desc' }>({ @@ -376,7 +391,24 @@ const AdsPerformance: React.FC = ({ data, filters, top50Ran
PRODUCT - +
+ + 0)', + 'In Stock (>20)', + 'Low Stock (<10)', + ]} + /> +
diff --git a/components/ForecastView.tsx b/components/ForecastView.tsx index cd6853e..0bfce8f 100644 --- a/components/ForecastView.tsx +++ b/components/ForecastView.tsx @@ -12,18 +12,10 @@ import { LineChart, Line, Legend, ComposedChart, Area } from 'recharts'; -interface ForecastViewProps { - data: ProductForecastData[]; - filters: FilterState; // Restored filters prop - top50Ranking?: { - eu: Map; - uk: Map; - }; - stockMap?: Map; - vendorStockMap?: Map; - stockFilter: string[]; - onStockFilterChange: (newFilters: string[]) => void; - top50Mode: 'eu' | 'uk'; +top50Mode: 'eu' | 'uk'; +vendorStockMap ?: Map; +vendorStockFilter: string[]; +onVendorStockFilterChange: (newFilters: string[]) => void; } const MONTH_ORDER = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; @@ -116,7 +108,18 @@ const ForecastRow: React.FC<{ ); }); -const ForecastView: React.FC = ({ data, filters, top50Ranking, stockMap, vendorStockMap, stockFilter, onStockFilterChange, top50Mode }) => { +const ForecastView: React.FC = ({ + data, + filters, + top50Ranking, + stockMap, + vendorStockMap, + stockFilter, + onStockFilterChange, + vendorStockFilter, + onVendorStockFilterChange, + top50Mode +}) => { const [searchTerm, setSearchTerm] = useState(''); const [showOnlyTop50, setShowOnlyTop50] = useState(false); const [displayCount, setDisplayCount] = useState(50); @@ -231,7 +234,24 @@ const ForecastView: React.FC = ({ data, filters, top50Ranking
Product Details - +
+ + 0)', + 'In Stock (>20)', + 'Low Stock (<10)', + ]} + /> +
Actuals diff --git a/components/InColumnStockFilter.tsx b/components/InColumnStockFilter.tsx index 9350502..d301347 100644 --- a/components/InColumnStockFilter.tsx +++ b/components/InColumnStockFilter.tsx @@ -3,15 +3,22 @@ import React, { useState, useRef, useEffect } from 'react'; interface InColumnStockFilterProps { currentFilters: string[]; onFilterChange: (newFilters: string[]) => void; + title?: string; + options?: string[]; } -const STOCK_OPTIONS = [ +const DEFAULT_STOCK_OPTIONS = [ 'Out of Stock (0)', 'In Stock (>0)', 'Low Stock (<10)', ]; -export const InColumnStockFilter: React.FC = ({ currentFilters, onFilterChange }) => { +export const InColumnStockFilter: React.FC = ({ + currentFilters, + onFilterChange, + title = "Stock Filter", + options = DEFAULT_STOCK_OPTIONS +}) => { const [isOpen, setIsOpen] = useState(false); const containerRef = useRef(null); @@ -59,12 +66,12 @@ export const InColumnStockFilter: React.FC = ({ curren {isOpen && (
- Stock Filter + {title} {currentFilters.length > 0 && ( )}
- {STOCK_OPTIONS.map(option => { + {options.map(option => { const isSelected = currentFilters.includes(option); return (
void; customerFilters: string[]; vendorStockMap?: Map; + vendorStockFilter: string[]; + onVendorStockFilterChange: (newFilters: string[]) => void; } type SortConfig = { @@ -133,7 +135,19 @@ const WeeklyRow: React.FC<{ ); }); -const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown, stockMap, vendorStockMap, stockFilter, onStockFilterChange, customerFilters, top50Mode }) => { +const WeeklyGrid: React.FC = ({ + data, + top50Ranking, + onDrillDown, + stockMap, + vendorStockMap, + stockFilter, + onStockFilterChange, + vendorStockFilter, + onVendorStockFilterChange, + customerFilters, + top50Mode +}) => { // Pivot data - memoized const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]); @@ -460,10 +474,24 @@ const WeeklyGrid: React.FC = ({ da >
Product Details - +
+ + 0)', + 'In Stock (>20)', + 'Low Stock (<10)', + ]} + /> +
{sortConfig?.metric === 'rank' && ( {sortConfig.direction === 'asc' ? '↑' : '↓'} diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 9a1e959..9618d3d 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -743,12 +743,50 @@ const checkStockFilter = (sku: string, filters: string[], stockMap?: Map, mode: 'eu' | 'uk' = 'eu'): boolean => { + if (!filters || !Array.isArray(filters) || filters.length === 0) return true; + if (!vendorStockMap) return true; + + 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; + }); +}; + // Filter Ads Data by Country, Year, Week, ASIN, SKU, and Product Line export const filterAdsData = ( adsData: AdsRecord[], filters: FilterState, asinMetadata?: Map, - stockMap?: Map + stockMap?: Map, + vendorStockMap?: Map, + top50Mode: 'eu' | 'uk' = 'eu' ): AdsRecord[] => { return adsData.filter(ad => { const asin = ad.asin.trim().toUpperCase(); @@ -785,12 +823,19 @@ export const filterAdsData = ( // Stock match const stockMatch = checkStockFilter(meta?.sku || '', filters.stock, stockMap); + const vendorStockMatch = checkVendorStockFilter(asin, filters.vendorStock, vendorStockMap, top50Mode); - return countryMatch && yearMatch && weekMatch && asinMatch && skuMatch && lineMatch && stockMatch; + return countryMatch && yearMatch && weekMatch && asinMatch && skuMatch && lineMatch && stockMatch && vendorStockMatch; }); }; -export const filterData = (data: SalesRecord[], filters: FilterState, stockMap?: Map): SalesRecord[] => { +export const filterData = ( + data: SalesRecord[], + filters: FilterState, + stockMap?: Map, + vendorStockMap?: Map, + top50Mode: 'eu' | 'uk' = 'eu' +): SalesRecord[] => { return data.filter(item => { // 1. Month Logic: Handle "Apr-23" matching "Apr" filter const recordMonth = item.month; // e.g. "Apr-23" @@ -817,8 +862,9 @@ export const filterData = (data: SalesRecord[], filters: FilterState, stockMap?: // Stock match const stockMatch = checkStockFilter(item.sku, filters.stock, stockMap); + const vendorStockMatch = checkVendorStockFilter(item.asin, filters.vendorStock, vendorStockMap, top50Mode); - return customerMatch && yearMatch && monthMatch && lineMatch && asinMatch && skuMatch && titleMatch && weekMatch && stockMatch; + return customerMatch && yearMatch && monthMatch && lineMatch && asinMatch && skuMatch && titleMatch && weekMatch && stockMatch && vendorStockMatch; }); }; diff --git a/types.ts b/types.ts index 2b76f8d..95ceff3 100644 --- a/types.ts +++ b/types.ts @@ -24,6 +24,7 @@ export interface FilterState { title: string[]; // Added Title filter week: string[]; // Added Week filter stock: string[]; // Added Stock filter + vendorStock: string[]; // Added Vendor Stock filter } export interface GrowthMetric {