diff --git a/App.tsx b/App.tsx index 5f00646..c84282f 100644 --- a/App.tsx +++ b/App.tsx @@ -1,5 +1,5 @@ -import React, { useState, useMemo, useEffect, useCallback, Suspense, lazy } from 'react'; +import React, { useState, useMemo, useEffect, useCallback, Suspense, lazy, useDeferredValue } from 'react'; import FileUpload from './components/FileUpload'; import Dashboard from './components/Dashboard'; import FilterBar from './components/FilterBar'; @@ -82,6 +82,7 @@ const App: React.FC = () => { woc: [], bulkSearch: '', }); + const deferredFilters = useDeferredValue(filters); const top50Mode = useMemo(() => { const hasUK = filters.customer.includes('Amazon UK'); @@ -525,8 +526,8 @@ const App: React.FC = () => { return metaMap; }, [rawData]); - 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 filteredData = useMemo(() => filterData(rawData, deferredFilters, stockMap, vendorStockMap, top50Mode), [rawData, deferredFilters, stockMap, vendorStockMap, top50Mode]); + const filteredAdsData = useMemo(() => filterAdsData(adsData, deferredFilters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode), [adsData, deferredFilters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode]); const aggregatedData = useMemo(() => aggregateData(filteredData), [filteredData]); // Calculate country-aware Top 50 Best Sellers for 2025 @@ -565,8 +566,8 @@ const App: React.FC = () => { // Derived Data for Vendor (BSR) const filteredBsrData = useMemo(() => { - return filterBsrData(bsrData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode); - }, [bsrData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode]); + return filterBsrData(bsrData, deferredFilters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode); + }, [bsrData, deferredFilters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode]); // Combine Sales & Ads Data dynamically based on current filters const combinedAdsData = useMemo(() => { @@ -595,7 +596,7 @@ const App: React.FC = () => { // Only apply the "Max Week" cutoff if the user has NOT explicitly selected a specific time period. // If Month or Week filters are active, we show exactly what was asked (e.g., Full Year 2024). // If no time filters are active, we default to YTD (Like-for-Like) comparison. - const isTimeFilterActive = filters.month.length > 0 || filters.week.length > 0; + const isTimeFilterActive = deferredFilters.month.length > 0 || deferredFilters.week.length > 0; if (!combinedAdsData) return []; @@ -607,25 +608,25 @@ const App: React.FC = () => { if (!item.week) return true; return item.week <= gridContext.maxWeek; }); - }, [combinedAdsData, gridContext.maxWeek, filters.month, filters.week]); + }, [combinedAdsData, gridContext.maxWeek, deferredFilters.month, deferredFilters.week]); // Derived Data for Dashboard (YTD Filtered - Isolated) // Dashboard needs aggregated data respecting the max week limit of current year. // Dashboard needs aggregated data respecting the max week limit of current year... // UNLESS the user explicitly filters for a period. const ytdFilteredData = useMemo(() => { - const isTimeFilterActive = filters.month.length > 0 || filters.week.length > 0; + const isTimeFilterActive = deferredFilters.month.length > 0 || deferredFilters.week.length > 0; if (isTimeFilterActive) return filteredData; return filteredData.filter(r => r.week <= gridContext.maxWeek); - }, [filteredData, gridContext.maxWeek, filters.month, filters.week]); + }, [filteredData, gridContext.maxWeek, deferredFilters.month, deferredFilters.week]); const ytdFilteredAdsData = useMemo(() => { - const isTimeFilterActive = filters.month.length > 0 || filters.week.length > 0; + const isTimeFilterActive = deferredFilters.month.length > 0 || deferredFilters.week.length > 0; if (isTimeFilterActive) return filteredAdsData; return filteredAdsData.filter(r => r.week <= gridContext.maxWeek); - }, [filteredAdsData, gridContext.maxWeek, filters.month, filters.week]); + }, [filteredAdsData, gridContext.maxWeek, deferredFilters.month, deferredFilters.week]); const ytdAggregatedData = useMemo(() => { return aggregateData(ytdFilteredData); @@ -637,7 +638,7 @@ const App: React.FC = () => { // Derive Context Data (Product Line Context when drilling down) const contextAggregatedData = useMemo(() => { // Check if we are filtering by specific items (SKU, ASIN, Title) - const hasItemFilters = filters.sku.length > 0 || filters.asin.length > 0 || filters.title.length > 0; + const hasItemFilters = deferredFilters.sku.length > 0 || deferredFilters.asin.length > 0 || deferredFilters.title.length > 0; if (!hasItemFilters || filteredData.length === 0) { return null; @@ -648,7 +649,7 @@ const App: React.FC = () => { // 2. Create a "broad" filter: Keep Year/Customer/Month, but CLEAR Item filters, and restrict to these Lines const contextFilters: FilterState = { - ...filters, + ...deferredFilters, line: activeLines, // Force these lines sku: [], // Clear specific item filters asin: [], @@ -660,7 +661,7 @@ const App: React.FC = () => { const broadData = filterData(rawData, contextFilters); return aggregateData(broadData); - }, [rawData, filters, filteredData]); + }, [rawData, deferredFilters, filteredData]); // Derive Options for Filter Dropdowns diff --git a/components/MultiSelectDropdown.tsx b/components/MultiSelectDropdown.tsx index 3d52790..66e55e2 100644 --- a/components/MultiSelectDropdown.tsx +++ b/components/MultiSelectDropdown.tsx @@ -15,9 +15,16 @@ const MultiSelectDropdown: React.FC = ({ label, select const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [rangeInput, setRangeInput] = useState(''); + // Local optimistic state so checkboxes respond instantly on click + const [localSelected, setLocalSelected] = useState(selected); const dropdownRef = useRef(null); const inputRef = useRef(null); + // Sync local state when committed prop changes (after deferred update settles) + useEffect(() => { + setLocalSelected(selected); + }, [selected]); + useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -52,36 +59,29 @@ const MultiSelectDropdown: React.FC = ({ label, select }, [options, searchTerm]); const toggleOption = (option: string) => { - if (selected.includes(option)) { - onChange(selected.filter((item) => item !== option)); - } else { - onChange([...selected, option]); - } + const next = localSelected.includes(option) + ? localSelected.filter((item) => item !== option) + : [...localSelected, option]; + setLocalSelected(next); + onChange(next); }; const handleSelectAll = () => { - // If searching, only select/deselect visible options + let next: string[]; if (searchTerm) { - const allFilteredSelected = filteredOptions.every(opt => selected.includes(opt)); - if (allFilteredSelected) { - // Deselect all filtered options - onChange(selected.filter(item => !filteredOptions.includes(item))); - } else { - // Select all filtered options (add unique ones) - const newSelected = Array.from(new Set([...selected, ...filteredOptions])); - onChange(newSelected); - } + const allFilteredSelected = filteredOptions.every(opt => localSelected.includes(opt)); + next = allFilteredSelected + ? localSelected.filter(item => !filteredOptions.includes(item)) + : Array.from(new Set([...localSelected, ...filteredOptions])); } else { - // Standard behavior - if (selected.length === options.length) { - onChange([]); // Deselect all - } else { - onChange([...options]); // Select all - } + next = localSelected.length === options.length ? [] : [...options]; } + setLocalSelected(next); + onChange(next); }; const handleClear = () => { + setLocalSelected([]); onChange([]); }; @@ -135,12 +135,12 @@ const MultiSelectDropdown: React.FC = ({ label, select