From fffc0e759c75e95f2e864595c182c35ee3421ce1 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 13 Jan 2025 12:11:51 -0500 Subject: [PATCH] Enhance product page filtering --- inventory-server/src/routes/products.js | 252 ++++++------ .../components/products/ProductFilters.tsx | 365 +++++++++++++----- inventory/src/components/ui/command.tsx | 151 ++++++++ inventory/src/pages/Products.tsx | 78 +--- 4 files changed, 556 insertions(+), 290 deletions(-) create mode 100644 inventory/src/components/ui/command.tsx diff --git a/inventory-server/src/routes/products.js b/inventory-server/src/routes/products.js index bfc6588..5ab19e5 100755 --- a/inventory-server/src/routes/products.js +++ b/inventory-server/src/routes/products.js @@ -13,12 +13,6 @@ router.get('/', async (req, res) => { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 50; const offset = (page - 1) * limit; - const search = req.query.search || ''; - const category = req.query.category || 'all'; - const vendor = req.query.vendor || 'all'; - const stockStatus = req.query.stockStatus || 'all'; - const minPrice = parseFloat(req.query.minPrice) || 0; - const maxPrice = req.query.maxPrice ? parseFloat(req.query.maxPrice) : null; const sortColumn = req.query.sortColumn || 'title'; const sortDirection = req.query.sortDirection === 'desc' ? 'DESC' : 'ASC'; @@ -26,12 +20,19 @@ router.get('/', async (req, res) => { const conditions = ['p.visible = true']; const params = []; - if (search) { + // Handle text search filters + if (req.query.search) { conditions.push('(p.title LIKE ? OR p.SKU LIKE ?)'); - params.push(`%${search}%`, `%${search}%`); + params.push(`%${req.query.search}%`, `%${req.query.search}%`); } - if (category !== 'all') { + if (req.query.sku) { + conditions.push('p.SKU LIKE ?'); + params.push(`%${req.query.sku}%`); + } + + // Handle select filters + if (req.query.category && req.query.category !== 'all') { conditions.push(` p.product_id IN ( SELECT pc.product_id @@ -40,99 +41,144 @@ router.get('/', async (req, res) => { WHERE c.name = ? ) `); - params.push(category); + params.push(req.query.category); } - if (vendor !== 'all') { + if (req.query.vendor && req.query.vendor !== 'all') { conditions.push('p.vendor = ?'); - params.push(vendor); + params.push(req.query.vendor); } - if (stockStatus !== 'all') { - switch (stockStatus) { - case 'out_of_stock': - conditions.push('p.stock_quantity = 0'); - break; - case 'low_stock': - conditions.push('p.stock_quantity > 0 AND p.stock_quantity <= 5'); - break; - case 'in_stock': - conditions.push('p.stock_quantity > 5'); - break; - } + if (req.query.brand && req.query.brand !== 'all') { + conditions.push('p.brand = ?'); + params.push(req.query.brand); } - if (minPrice > 0) { + if (req.query.abcClass) { + conditions.push('pm.abc_class = ?'); + params.push(req.query.abcClass); + } + + // Handle numeric range filters + if (req.query.minStock) { + conditions.push('p.stock_quantity >= ?'); + params.push(parseFloat(req.query.minStock)); + } + + if (req.query.maxStock) { + conditions.push('p.stock_quantity <= ?'); + params.push(parseFloat(req.query.maxStock)); + } + + if (req.query.minPrice) { conditions.push('p.price >= ?'); - params.push(minPrice); + params.push(parseFloat(req.query.minPrice)); } - if (maxPrice) { + if (req.query.maxPrice) { conditions.push('p.price <= ?'); - params.push(maxPrice); + params.push(parseFloat(req.query.maxPrice)); + } + + if (req.query.minSalesAvg) { + conditions.push('pm.daily_sales_avg >= ?'); + params.push(parseFloat(req.query.minSalesAvg)); + } + + if (req.query.maxSalesAvg) { + conditions.push('pm.daily_sales_avg <= ?'); + params.push(parseFloat(req.query.maxSalesAvg)); + } + + if (req.query.minMargin) { + conditions.push('pm.avg_margin_percent >= ?'); + params.push(parseFloat(req.query.minMargin)); + } + + if (req.query.maxMargin) { + conditions.push('pm.avg_margin_percent <= ?'); + params.push(parseFloat(req.query.maxMargin)); + } + + if (req.query.minGMROI) { + conditions.push('pm.gmroi >= ?'); + params.push(parseFloat(req.query.minGMROI)); + } + + if (req.query.maxGMROI) { + conditions.push('pm.gmroi <= ?'); + params.push(parseFloat(req.query.maxGMROI)); + } + + // Handle status filters + if (req.query.stockStatus && req.query.stockStatus !== 'all') { + conditions.push('pm.stock_status = ?'); + params.push(req.query.stockStatus); } // Get total count for pagination const [countResult] = await pool.query( - `SELECT COUNT(*) as total FROM products p WHERE ${conditions.join(' AND ')}`, + `SELECT COUNT(DISTINCT p.product_id) as total + FROM products p + LEFT JOIN product_metrics pm ON p.product_id = pm.product_id + WHERE ${conditions.join(' AND ')}`, params ); const total = countResult[0].total; - // Get paginated results with metrics + // Get available filters + const [categories] = await pool.query( + 'SELECT name FROM categories ORDER BY name' + ); + const [vendors] = await pool.query( + 'SELECT DISTINCT vendor FROM products WHERE visible = true AND vendor IS NOT NULL AND vendor != "" ORDER BY vendor' + ); + + // Main query with all fields const query = ` + WITH product_thresholds AS ( + SELECT + p.product_id, + COALESCE( + (SELECT overstock_days FROM stock_thresholds st + JOIN product_categories pc ON st.category_id = pc.category_id + WHERE pc.product_id = p.product_id + AND st.vendor = p.vendor LIMIT 1), + (SELECT overstock_days FROM stock_thresholds st + JOIN product_categories pc ON st.category_id = pc.category_id + WHERE pc.product_id = p.product_id + AND st.vendor IS NULL LIMIT 1), + (SELECT overstock_days FROM stock_thresholds st + WHERE st.category_id IS NULL + AND st.vendor = p.vendor LIMIT 1), + (SELECT overstock_days FROM stock_thresholds st + WHERE st.category_id IS NULL + AND st.vendor IS NULL LIMIT 1), + 90 + ) as target_days + FROM products p + ) SELECT - p.product_id, - p.title, - p.SKU, - p.stock_quantity, - p.price, - p.regular_price, - p.cost_price, - p.landing_cost_price, - p.barcode, - p.vendor, - p.vendor_reference, - p.brand, - p.visible, - p.managing_stock, - p.replenishable, - p.moq, - p.uom, - p.image, + p.*, GROUP_CONCAT(DISTINCT c.name) as categories, - - -- Metrics from product_metrics pm.daily_sales_avg, pm.weekly_sales_avg, pm.monthly_sales_avg, - pm.avg_quantity_per_order, - pm.number_of_orders, - pm.first_sale_date, - pm.last_sale_date, - pm.days_of_inventory, - pm.weeks_of_inventory, - pm.reorder_point, - pm.safety_stock, pm.avg_margin_percent, - pm.total_revenue, - pm.inventory_value, - pm.cost_of_goods_sold, - pm.gross_profit, pm.gmroi, - pm.avg_lead_time_days, - pm.last_purchase_date, - pm.last_received_date, pm.abc_class, pm.stock_status, - pm.turnover_rate, + pm.avg_lead_time_days, pm.current_lead_time, pm.target_lead_time, - pm.lead_time_status + pm.lead_time_status, + pm.days_of_inventory as days_of_stock, + COALESCE(pm.days_of_inventory / NULLIF(pt.target_days, 0), 0) as stock_coverage_ratio FROM products p + LEFT JOIN product_metrics pm ON p.product_id = pm.product_id LEFT JOIN product_categories pc ON p.product_id = pc.product_id LEFT JOIN categories c ON pc.category_id = c.id - LEFT JOIN product_metrics pm ON p.product_id = pm.product_id + LEFT JOIN product_thresholds pt ON p.product_id = pt.product_id WHERE ${conditions.join(' AND ')} GROUP BY p.product_id ORDER BY ${sortColumn} ${sortDirection} @@ -141,64 +187,40 @@ router.get('/', async (req, res) => { const [rows] = await pool.query(query, [...params, limit, offset]); - // Transform the categories string into an array and parse numeric values - const productsWithCategories = rows.map(product => ({ - ...product, - categories: product.categories ? [...new Set(product.categories.split(','))] : [], - // Parse numeric values - price: parseFloat(product.price) || 0, - regular_price: parseFloat(product.regular_price) || 0, - cost_price: parseFloat(product.cost_price) || 0, - landing_cost_price: parseFloat(product.landing_cost_price) || 0, - stock_quantity: parseInt(product.stock_quantity) || 0, - moq: parseInt(product.moq) || 1, - uom: parseInt(product.uom) || 1, - // Parse metrics - daily_sales_avg: parseFloat(product.daily_sales_avg) || null, - weekly_sales_avg: parseFloat(product.weekly_sales_avg) || null, - monthly_sales_avg: parseFloat(product.monthly_sales_avg) || null, - avg_quantity_per_order: parseFloat(product.avg_quantity_per_order) || null, - number_of_orders: parseInt(product.number_of_orders) || null, - days_of_inventory: parseInt(product.days_of_inventory) || null, - weeks_of_inventory: parseInt(product.weeks_of_inventory) || null, - reorder_point: parseInt(product.reorder_point) || null, - safety_stock: parseInt(product.safety_stock) || null, - avg_margin_percent: parseFloat(product.avg_margin_percent) || null, - total_revenue: parseFloat(product.total_revenue) || null, - inventory_value: parseFloat(product.inventory_value) || null, - cost_of_goods_sold: parseFloat(product.cost_of_goods_sold) || null, - gross_profit: parseFloat(product.gross_profit) || null, - gmroi: parseFloat(product.gmroi) || null, - turnover_rate: parseFloat(product.turnover_rate) || null, - avg_lead_time_days: parseInt(product.avg_lead_time_days) || null, - current_lead_time: parseInt(product.current_lead_time) || null, - target_lead_time: parseInt(product.target_lead_time) || null + // Transform the results + const products = rows.map(row => ({ + ...row, + categories: row.categories ? row.categories.split(',') : [], + price: parseFloat(row.price), + cost_price: parseFloat(row.cost_price), + landing_cost_price: parseFloat(row.landing_cost_price), + stock_quantity: parseInt(row.stock_quantity), + daily_sales_avg: parseFloat(row.daily_sales_avg) || 0, + weekly_sales_avg: parseFloat(row.weekly_sales_avg) || 0, + monthly_sales_avg: parseFloat(row.monthly_sales_avg) || 0, + avg_margin_percent: parseFloat(row.avg_margin_percent) || 0, + gmroi: parseFloat(row.gmroi) || 0, + lead_time_days: parseInt(row.lead_time_days) || 0, + days_of_stock: parseFloat(row.days_of_stock) || 0, + stock_coverage_ratio: parseFloat(row.stock_coverage_ratio) || 0 })); - // Get unique categories and vendors for filters - const [categories] = await pool.query( - 'SELECT name FROM categories ORDER BY name' - ); - const [vendors] = await pool.query( - 'SELECT DISTINCT vendor FROM products WHERE visible = true AND vendor IS NOT NULL AND vendor != "" ORDER BY vendor' - ); - res.json({ - products: productsWithCategories, + products, pagination: { + page, + limit, total, - pages: Math.ceil(total / limit), - currentPage: page, - limit + totalPages: Math.ceil(total / limit) }, filters: { - categories: categories.map(c => c.name), - vendors: vendors.map(v => v.vendor) + categories: categories.map(category => category.name), + vendors: vendors.map(vendor => vendor.vendor) } }); } catch (error) { console.error('Error fetching products:', error); - res.status(500).json({ error: 'Failed to fetch products' }); + res.status(500).json({ error: 'Internal server error' }); } }); diff --git a/inventory/src/components/products/ProductFilters.tsx b/inventory/src/components/products/ProductFilters.tsx index 85f4d60..b2bf5f8 100644 --- a/inventory/src/components/products/ProductFilters.tsx +++ b/inventory/src/components/products/ProductFilters.tsx @@ -1,129 +1,282 @@ -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import * as React from "react"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { X, Plus } from "lucide-react"; +import { DialogTitle, DialogDescription } from "@/components/ui/dialog"; -interface ProductFilters { - search: string; - category: string; - vendor: string; - stockStatus: string; - minPrice: string; - maxPrice: string; +type FilterValue = string | number | boolean; + +interface FilterOption { + id: string; + label: string; + type: 'select' | 'number' | 'boolean' | 'text'; + options?: { label: string; value: string }[]; + group: string; } +interface ActiveFilter { + id: string; + label: string; + value: FilterValue; + displayValue: string; +} + +const FILTER_OPTIONS: FilterOption[] = [ + // Basic Info Group + { id: 'search', label: 'Search', type: 'text', group: 'Basic Info' }, + { id: 'sku', label: 'SKU', type: 'text', group: 'Basic Info' }, + { id: 'vendor', label: 'Vendor', type: 'select', group: 'Basic Info' }, + { id: 'brand', label: 'Brand', type: 'select', group: 'Basic Info' }, + { id: 'category', label: 'Category', type: 'select', group: 'Basic Info' }, + + // Inventory Group + { + id: 'stockStatus', + label: 'Stock Status', + type: 'select', + options: [ + { label: 'Critical', value: 'critical' }, + { label: 'Reorder', value: 'reorder' }, + { label: 'Healthy', value: 'healthy' }, + { label: 'Overstocked', value: 'overstocked' }, + { label: 'New', value: 'new' }, + ], + group: 'Inventory' + }, + { id: 'minStock', label: 'Min Stock', type: 'number', group: 'Inventory' }, + { id: 'maxStock', label: 'Max Stock', type: 'number', group: 'Inventory' }, + + // Pricing Group + { id: 'minPrice', label: 'Min Price', type: 'number', group: 'Pricing' }, + { id: 'maxPrice', label: 'Max Price', type: 'number', group: 'Pricing' }, + + // Sales Metrics Group + { id: 'minSalesAvg', label: 'Min Daily Sales Avg', type: 'number', group: 'Sales Metrics' }, + { id: 'maxSalesAvg', label: 'Max Daily Sales Avg', type: 'number', group: 'Sales Metrics' }, + + // Financial Metrics Group + { id: 'minMargin', label: 'Min Margin %', type: 'number', group: 'Financial Metrics' }, + { id: 'maxMargin', label: 'Max Margin %', type: 'number', group: 'Financial Metrics' }, + { id: 'minGMROI', label: 'Min GMROI', type: 'number', group: 'Financial Metrics' }, + { id: 'maxGMROI', label: 'Max GMROI', type: 'number', group: 'Financial Metrics' }, + + // Classification Group + { + id: 'abcClass', + label: 'ABC Class', + type: 'select', + options: [ + { label: 'A', value: 'A' }, + { label: 'B', value: 'B' }, + { label: 'C', value: 'C' }, + ], + group: 'Classification' + }, +]; + interface ProductFiltersProps { - filters: ProductFilters; categories: string[]; vendors: string[]; - onFilterChange: (filters: Partial) => void; + onFilterChange: (filters: Record) => void; onClearFilters: () => void; + activeFilters: Record; } -export function ProductFilters({ - filters, - categories, - vendors, - onFilterChange, - onClearFilters +export function ProductFilters({ + categories, + vendors, + onFilterChange, + onClearFilters, + activeFilters, }: ProductFiltersProps) { - const activeFilterCount = Object.values(filters).filter(Boolean).length; + const [open, setOpen] = React.useState(false); + const [selectedFilter, setSelectedFilter] = React.useState(null); + const [filterValue, setFilterValue] = React.useState(""); + + // Update filter options with dynamic data + const filterOptions = React.useMemo(() => { + return FILTER_OPTIONS.map(option => { + if (option.id === 'category') { + return { + ...option, + options: categories.map(cat => ({ label: cat, value: cat })) + }; + } + if (option.id === 'vendor') { + return { + ...option, + options: vendors.map(vendor => ({ label: vendor, value: vendor })) + }; + } + return option; + }); + }, [categories, vendors]); + + const activeFiltersList = React.useMemo(() => { + if (!activeFilters) return []; + + return Object.entries(activeFilters).map(([id, value]): ActiveFilter => { + const option = filterOptions.find(opt => opt.id === id); + let displayValue = String(value); + + if (option?.type === 'select' && option.options) { + const optionLabel = option.options.find(opt => opt.value === value)?.label; + if (optionLabel) displayValue = optionLabel; + } + + return { + id, + label: option?.label || id, + value, + displayValue + }; + }); + }, [activeFilters, filterOptions]); + + const handleSelectFilter = (filter: FilterOption) => { + setSelectedFilter(filter); + if (filter.type === 'select') { + setOpen(false); + // Open a new command dialog for selecting the value + // This will be handled in a separate component + } else { + // For other types, we'll show an input field + setFilterValue(""); + } + }; + + const handleRemoveFilter = (filterId: string) => { + const newFilters = { ...activeFilters }; + delete newFilters[filterId]; + onFilterChange(newFilters); + }; + + const handleApplyFilter = (value: FilterValue) => { + if (!selectedFilter) return; + + const newFilters = { + ...activeFilters, + [selectedFilter.id]: value + }; + + onFilterChange(newFilters); + setSelectedFilter(null); + setFilterValue(""); + setOpen(false); + }; return (
-
-

Filters

- {activeFilterCount > 0 && ( - + {activeFiltersList.length > 0 && ( + )}
-
-
- onFilterChange({ search: e.target.value })} - className="h-8 w-full" - /> + + {activeFiltersList.length > 0 && ( +
+ {activeFiltersList.map((filter) => ( + + + {filter.label}: {filter.displayValue} + + + + ))}
-
- - -
-
- -
- onFilterChange({ minPrice: e.target.value })} - className="h-8 w-full" - /> - - - onFilterChange({ maxPrice: e.target.value })} - className="h-8 w-full" - /> -
-
-
+ )} + + + + Search Filters + + Search and select filters to apply to the product list + + + + No filters found. + {Object.entries( + filterOptions.reduce>((acc, filter) => { + if (!acc[filter.group]) acc[filter.group] = []; + acc[filter.group].push(filter); + return acc; + }, {}) + ).map(([group, filters]) => ( + + {filters.map((filter) => ( + handleSelectFilter(filter)} + > + {filter.label} + + ))} + + ))} + + + + + {selectedFilter?.type === 'select' && ( + setSelectedFilter(null)}> + + Select {selectedFilter.label} + + Choose a value for the {selectedFilter.label.toLowerCase()} filter + + + + No options found. + + {selectedFilter.options?.map((option) => ( + handleApplyFilter(option.value)} + > + {option.label} + + ))} + + + + + )}
); } \ No newline at end of file diff --git a/inventory/src/components/ui/command.tsx b/inventory/src/components/ui/command.tsx new file mode 100644 index 0000000..0db642a --- /dev/null +++ b/inventory/src/components/ui/command.tsx @@ -0,0 +1,151 @@ +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +const CommandDialog = ({ children, ...props }: DialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/inventory/src/pages/Products.tsx b/inventory/src/pages/Products.tsx index db49b58..6c33c7b 100644 --- a/inventory/src/pages/Products.tsx +++ b/inventory/src/pages/Products.tsx @@ -164,14 +164,7 @@ const DEFAULT_VISIBLE_COLUMNS: (keyof Product)[] = [ export function Products() { const queryClient = useQueryClient(); const tableRef = useRef(null); - const [filters, setFilters] = useState({ - search: '', - category: 'all', - vendor: 'all', - stockStatus: 'all', - minPrice: '', - maxPrice: '', - }); + const [filters, setFilters] = useState>({}); const [sortColumn, setSortColumn] = useState('title'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const [page, setPage] = useState(1); @@ -215,49 +208,12 @@ export function Products() { } const result = await response.json(); - return { - ...result, - products: result.products.map((product: any) => ({ - ...product, - price: parseFloat(product.price) || 0, - regular_price: parseFloat(product.regular_price) || 0, - cost_price: parseFloat(product.cost_price) || 0, - stock_quantity: parseInt(product.stock_quantity) || 0 - })) - }; + return result; }; const { data, isLoading, isFetching } = useQuery({ queryKey: ['products', filters, sortColumn, sortDirection, page], - queryFn: async () => { - const searchParams = new URLSearchParams({ - page: page.toString(), - limit: '100', - sortColumn: sortColumn.toString(), - sortDirection, - ...filters, - }); - - const response = await fetch(`${config.apiUrl}/products?${searchParams}`); - if (!response.ok) { - throw new Error('Network response was not ok'); - } - const result = await response.json(); - - return { - ...result, - products: result.products.map((product: any) => ({ - ...product, - price: parseFloat(product.price) || 0, - regular_price: parseFloat(product.regular_price) || 0, - cost_price: parseFloat(product.cost_price) || 0, - stock_quantity: parseInt(product.stock_quantity) || 0, - sku: product.SKU || product.sku || '', - image: product.image || null, - categories: Array.isArray(product.categories) ? product.categories : [] - })) - }; - }, + queryFn: () => fetchProducts(page), placeholderData: keepPreviousData, staleTime: 30000, }); @@ -312,31 +268,14 @@ export function Products() { } }; - // Debounce the filter changes with a shorter delay - const debouncedFilterChange = useCallback( - debounce((newFilters: Partial) => { - setFilters(prev => ({ ...prev, ...newFilters })); - setPage(1); - }, 200), // Reduced debounce time - [] - ); - - const handleFilterChange = (newFilters: Partial) => { - // Update UI immediately for better responsiveness - setFilters(prev => ({ ...prev, ...newFilters })); - // Debounce the actual query - debouncedFilterChange(newFilters); + // Handle filter changes + const handleFilterChange = (newFilters: Record) => { + setFilters(newFilters); + setPage(1); }; const handleClearFilters = () => { - setFilters({ - search: '', - category: 'all', - vendor: 'all', - stockStatus: 'all', - minPrice: '', - maxPrice: '', - }); + setFilters({}); setPage(1); }; @@ -475,6 +414,7 @@ export function Products() { vendors={data?.filters.vendors ?? []} onFilterChange={handleFilterChange} onClearFilters={handleClearFilters} + activeFilters={filters} />