diff --git a/inventory/src/components/products/ProductFilters.tsx b/inventory/src/components/products/ProductFilters.tsx index 0e5e739..82e3cb3 100644 --- a/inventory/src/components/products/ProductFilters.tsx +++ b/inventory/src/components/products/ProductFilters.tsx @@ -18,13 +18,10 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { Input } from "@/components/ui/input"; -import { - ToggleGroup, - ToggleGroupItem, -} from "@/components/ui/toggle-group"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; type FilterValue = string | number | boolean; -type ComparisonOperator = '=' | '>' | '>=' | '<' | '<=' | 'between'; +type ComparisonOperator = "=" | ">" | ">=" | "<" | "<=" | "between"; interface FilterValueWithOperator { value: FilterValue | [number, number]; @@ -43,7 +40,7 @@ interface ActiveFilter { interface FilterOption { id: string; label: string; - type: 'select' | 'number' | 'boolean' | 'text'; + type: "select" | "number" | "boolean" | "text"; options?: { label: string; value: string }[]; group: string; operators?: ComparisonOperator[]; @@ -51,163 +48,163 @@ interface FilterOption { 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' }, - + { 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', + { + id: "stockStatus", + label: "Stock Status", + type: "select", options: [ - { label: 'Critical', value: 'critical' }, - { label: 'At Risk', value: 'at-risk' }, - { label: 'Reorder', value: 'reorder' }, - { label: 'Healthy', value: 'healthy' }, - { label: 'Overstocked', value: 'overstocked' }, - { label: 'New', value: 'new' } + { label: "Critical", value: "critical" }, + { label: "At Risk", value: "at-risk" }, + { label: "Reorder", value: "reorder" }, + { label: "Healthy", value: "healthy" }, + { label: "Overstocked", value: "overstocked" }, + { label: "New", value: "new" }, ], - group: 'Inventory' + group: "Inventory", }, - { - id: 'stock', - label: 'Stock Quantity', - type: 'number', - group: 'Inventory', - operators: ['=', '>', '>=', '<', '<=', 'between'] + { + id: "stock", + label: "Stock Quantity", + type: "number", + group: "Inventory", + operators: ["=", ">", ">=", "<", "<=", "between"], }, - { - id: 'daysOfStock', - label: 'Days of Stock', - type: 'number', - group: 'Inventory', - operators: ['=', '>', '>=', '<', '<=', 'between'] + { + id: "daysOfStock", + label: "Days of Stock", + type: "number", + group: "Inventory", + operators: ["=", ">", ">=", "<", "<=", "between"], }, - { - id: 'replenishable', - label: 'Replenishable', - type: 'select', + { + id: "replenishable", + label: "Replenishable", + type: "select", options: [ - { label: 'Yes', value: 'true' }, - { label: 'No', value: 'false' } + { label: "Yes", value: "true" }, + { label: "No", value: "false" }, ], - group: 'Inventory' + group: "Inventory", }, - + // Pricing Group - { - id: 'price', - label: 'Price', - type: 'number', - group: 'Pricing', - operators: ['=', '>', '>=', '<', '<=', 'between'] + { + id: "price", + label: "Price", + type: "number", + group: "Pricing", + operators: ["=", ">", ">=", "<", "<=", "between"], }, - { - id: 'costPrice', - label: 'Cost Price', - type: 'number', - group: 'Pricing', - operators: ['=', '>', '>=', '<', '<=', 'between'] + { + id: "costPrice", + label: "Cost Price", + type: "number", + group: "Pricing", + operators: ["=", ">", ">=", "<", "<=", "between"], }, - { - id: 'landingCost', - label: 'Landing Cost', - type: 'number', - group: 'Pricing', - operators: ['=', '>', '>=', '<', '<=', 'between'] + { + id: "landingCost", + label: "Landing Cost", + type: "number", + group: "Pricing", + operators: ["=", ">", ">=", "<", "<=", "between"], }, - + // Sales Metrics Group - { - id: 'dailySalesAvg', - label: 'Daily Sales Avg', - type: 'number', - group: 'Sales Metrics', - operators: ['=', '>', '>=', '<', '<=', 'between'] + { + id: "dailySalesAvg", + label: "Daily Sales Avg", + type: "number", + group: "Sales Metrics", + operators: ["=", ">", ">=", "<", "<=", "between"], }, - { - id: 'weeklySalesAvg', - label: 'Weekly Sales Avg', - type: 'number', - group: 'Sales Metrics', - operators: ['=', '>', '>=', '<', '<=', 'between'] + { + id: "weeklySalesAvg", + label: "Weekly Sales Avg", + type: "number", + group: "Sales Metrics", + operators: ["=", ">", ">=", "<", "<=", "between"], }, - { - id: 'monthlySalesAvg', - label: 'Monthly Sales Avg', - type: 'number', - group: 'Sales Metrics', - operators: ['=', '>', '>=', '<', '<=', 'between'] + { + id: "monthlySalesAvg", + label: "Monthly Sales Avg", + type: "number", + group: "Sales Metrics", + operators: ["=", ">", ">=", "<", "<=", "between"], }, - + // Financial Metrics Group - { - id: 'margin', - label: 'Margin %', - type: 'number', - group: 'Financial Metrics', - operators: ['=', '>', '>=', '<', '<=', 'between'] + { + id: "margin", + label: "Margin %", + type: "number", + group: "Financial Metrics", + operators: ["=", ">", ">=", "<", "<=", "between"], }, - { - id: 'gmroi', - label: 'GMROI', - type: 'number', - group: 'Financial Metrics', - operators: ['=', '>', '>=', '<', '<=', 'between'] + { + id: "gmroi", + label: "GMROI", + type: "number", + group: "Financial Metrics", + operators: ["=", ">", ">=", "<", "<=", "between"], }, - + // Lead Time & Stock Coverage Group - { - id: 'leadTime', - label: 'Lead Time (Days)', - type: 'number', - group: 'Lead Time & Coverage', - operators: ['=', '>', '>=', '<', '<=', 'between'] + { + id: "leadTime", + label: "Lead Time (Days)", + type: "number", + group: "Lead Time & Coverage", + operators: ["=", ">", ">=", "<", "<=", "between"], }, - { - id: 'leadTimeStatus', - label: 'Lead Time Status', - type: 'select', + { + id: "leadTimeStatus", + label: "Lead Time Status", + type: "select", options: [ - { label: 'On Target', value: 'on_target' }, - { label: 'Warning', value: 'warning' }, - { label: 'Critical', value: 'critical' } + { label: "On Target", value: "on_target" }, + { label: "Warning", value: "warning" }, + { label: "Critical", value: "critical" }, ], - group: 'Lead Time & Coverage' + group: "Lead Time & Coverage", }, - { - id: 'stockCoverage', - label: 'Stock Coverage Ratio', - type: 'number', - group: 'Lead Time & Coverage', - operators: ['=', '>', '>=', '<', '<=', 'between'] + { + id: "stockCoverage", + label: "Stock Coverage Ratio", + type: "number", + group: "Lead Time & Coverage", + operators: ["=", ">", ">=", "<", "<=", "between"], }, - + // Classification Group - { - id: 'abcClass', - label: 'ABC Class', - type: 'select', + { + id: "abcClass", + label: "ABC Class", + type: "select", options: [ - { label: 'A', value: 'A' }, - { label: 'B', value: 'B' }, - { label: 'C', value: 'C' } + { label: "A", value: "A" }, + { label: "B", value: "B" }, + { label: "C", value: "C" }, ], - group: 'Classification' + group: "Classification", }, - { - id: 'managingStock', - label: 'Managing Stock', - type: 'select', + { + id: "managingStock", + label: "Managing Stock", + type: "select", options: [ - { label: 'Yes', value: 'true' }, - { label: 'No', value: 'false' } + { label: "Yes", value: "true" }, + { label: "No", value: "false" }, ], - group: 'Classification' - } + group: "Classification", + }, ]; interface ProductFiltersProps { @@ -229,16 +226,21 @@ export function ProductFilters({ }: ProductFiltersProps) { const [showCommand, setShowCommand] = React.useState(false); const [selectedFilter, setSelectedFilter] = React.useState(null); - const [selectedOperator, setSelectedOperator] = React.useState('='); + const [selectedOperator, setSelectedOperator] = React.useState("="); const [inputValue, setInputValue] = React.useState(""); const [inputValue2, setInputValue2] = React.useState(""); const [searchValue, setSearchValue] = React.useState(""); + + // Add refs for the inputs + const numberInputRef = React.useRef(null); + const selectInputRef = React.useRef(null); + const textInputRef = React.useRef(null); // Reset states when popup closes const handlePopoverClose = () => { setShowCommand(false); setSelectedFilter(null); - setSelectedOperator('='); + setSelectedOperator("="); setInputValue(""); setInputValue2(""); setSearchValue(""); @@ -248,7 +250,7 @@ export function ProductFilters({ React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Command/Ctrl + K to toggle filter - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); if (!showCommand) { setShowCommand(true); @@ -256,35 +258,31 @@ export function ProductFilters({ handlePopoverClose(); } } - // Only handle Escape at the root level - if (e.key === 'Escape' && !selectedFilter) { - handlePopoverClose(); - } }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [selectedFilter, showCommand]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [showCommand]); // Update filter options with dynamic data const filterOptions = React.useMemo(() => { - return FILTER_OPTIONS.map(option => { - if (option.id === 'category') { + return FILTER_OPTIONS.map((option) => { + if (option.id === "category") { return { ...option, - options: categories.map(cat => ({ label: cat, value: cat })) + options: categories.map((cat) => ({ label: cat, value: cat })), }; } - if (option.id === 'vendor') { + if (option.id === "vendor") { return { ...option, - options: vendors.map(vendor => ({ label: vendor, value: vendor })) + options: vendors.map((vendor) => ({ label: vendor, value: vendor })), }; } - if (option.id === 'brand') { + if (option.id === "brand") { return { ...option, - options: brands.map(brand => ({ label: brand, value: brand })) + options: brands.map((brand) => ({ label: brand, value: brand })), }; } return option; @@ -294,50 +292,64 @@ export function ProductFilters({ // Filter options based on search const filteredOptions = React.useMemo(() => { if (!searchValue) return filterOptions; - + const search = searchValue.toLowerCase(); - return filterOptions.filter(option => - option.label.toLowerCase().includes(search) || - option.group.toLowerCase().includes(search) + return filterOptions.filter( + (option) => + option.label.toLowerCase().includes(search) || + option.group.toLowerCase().includes(search) ); }, [filterOptions, searchValue]); const handleSelectFilter = React.useCallback((filter: FilterOption) => { setSelectedFilter(filter); setInputValue(""); + + // Focus the appropriate input after state updates + requestAnimationFrame(() => { + if (filter.type === "number") { + numberInputRef.current?.focus(); + } else if (filter.type === "select") { + selectInputRef.current?.focus(); + } else { + textInputRef.current?.focus(); + } + }); }, []); const handleApplyFilter = (value: FilterValue | [number, number]) => { if (!selectedFilter) return; - + const newFilters = { ...activeFilters, [selectedFilter.id]: { value, - operator: selectedOperator - } + operator: selectedOperator, + }, }; - + onFilterChange(newFilters as Record); handlePopoverClose(); }; const handleBackToFilters = () => { setSelectedFilter(null); - setSelectedOperator('='); + setSelectedOperator("="); setInputValue(""); setInputValue2(""); }; const activeFiltersList = React.useMemo(() => { if (!activeFilters) return []; - + return Object.entries(activeFilters).map(([id, value]): ActiveFilter => { - const option = filterOptions.find(opt => opt.id === id); + 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 (option?.type === "select" && option.options) { + const optionLabel = option.options.find( + (opt) => opt.value === value + )?.label; if (optionLabel) displayValue = optionLabel; } @@ -345,7 +357,7 @@ export function ProductFilters({ id, label: option?.label || id, value, - displayValue + displayValue, }; }); }, [activeFilters, filterOptions]); @@ -354,15 +366,29 @@ export function ProductFilters({ value && setSelectedOperator(value)} + onValueChange={(value: ComparisonOperator) => + value && setSelectedOperator(value) + } className="flex-wrap" > - = - {'>'} - - {'<'} - - Between + + = + + + {">"} + + + ≥ + + + {"<"} + + + ≤ + + + Between + ); @@ -380,13 +406,14 @@ export function ProductFilters({ {renderOperatorSelect()}
setInputValue(e.target.value)} onKeyDown={(e) => { - if (e.key === 'Enter') { - if (selectedOperator === 'between') { + if (e.key === "Enter") { + if (selectedOperator === "between") { if (inputValue2) { const val1 = parseFloat(inputValue); const val2 = parseFloat(inputValue2); @@ -400,14 +427,14 @@ export function ProductFilters({ handleApplyFilter(val); } } - } else if (e.key === 'Escape') { + } else if (e.key === "Escape") { e.stopPropagation(); handleBackToFilters(); } }} className="w-[120px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" /> - {selectedOperator === 'between' && ( + {selectedOperator === "between" && ( <> and setInputValue2(e.target.value)} onKeyDown={(e) => { - if (e.key === 'Enter') { + if (e.key === "Enter") { const val1 = parseFloat(inputValue); const val2 = parseFloat(inputValue2); if (!isNaN(val1) && !isNaN(val2)) { handleApplyFilter([val1, val2]); } - } else if (e.key === 'Escape') { + } else if (e.key === "Escape") { e.stopPropagation(); handleBackToFilters(); } @@ -433,7 +460,7 @@ export function ProductFilters({ )} - { - if (e.key === 'Escape') { - e.preventDefault(); - e.stopPropagation(); - if (selectedFilter) { - handleBackToFilters(); - } else { - handlePopoverClose(); - } + onEscapeKeyDown={(event) => { + console.log('Escape pressed, selectedFilter:', selectedFilter); // Debug log + if (selectedFilter) { + event.preventDefault(); + event.stopPropagation(); + handleBackToFilters(); } }} > - + {!selectedFilter ? ( <> - { - if (e.key === 'Escape') { + if (e.key === "Escape") { e.preventDefault(); handlePopoverClose(); } @@ -545,11 +569,14 @@ export function ProductFilters({ No filters found. {Object.entries( - filteredOptions.reduce>((acc, filter) => { - if (!acc[filter.group]) acc[filter.group] = []; - acc[filter.group].push(filter); - return acc; - }, {}) + filteredOptions.reduce>( + (acc, filter) => { + if (!acc[filter.group]) acc[filter.group] = []; + acc[filter.group].push(filter); + return acc; + }, + {} + ) ).map(([group, filters]) => ( @@ -559,7 +586,7 @@ export function ProductFilters({ value={`${filter.id} ${filter.label}`} onSelect={() => { handleSelectFilter(filter); - if (filter.type !== 'select') { + if (filter.type !== "select") { setInputValue(""); } }} @@ -582,7 +609,7 @@ export function ProductFilters({ ))} - ) : selectedFilter.type === 'number' ? ( + ) : selectedFilter.type === "number" ? (
@@ -597,13 +624,14 @@ export function ProductFilters({ {renderOperatorSelect()}
setInputValue(e.target.value)} onKeyDown={(e) => { - if (e.key === 'Enter') { - if (selectedOperator === 'between') { + if (e.key === "Enter") { + if (selectedOperator === "between") { if (inputValue2) { const val1 = parseFloat(inputValue); const val2 = parseFloat(inputValue2); @@ -617,14 +645,14 @@ export function ProductFilters({ handleApplyFilter(val); } } - } else if (e.key === 'Escape') { + } else if (e.key === "Escape") { e.preventDefault(); handleBackToFilters(); } }} className="w-[120px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" /> - {selectedOperator === 'between' && ( + {selectedOperator === "between" && ( <> and setInputValue2(e.target.value)} onKeyDown={(e) => { - if (e.key === 'Enter') { + if (e.key === "Enter") { const val1 = parseFloat(inputValue); const val2 = parseFloat(inputValue2); if (!isNaN(val1) && !isNaN(val2)) { handleApplyFilter([val1, val2]); } - } else if (e.key === 'Escape') { + } else if (e.key === "Escape") { e.preventDefault(); handleBackToFilters(); } @@ -650,7 +678,7 @@ export function ProductFilters({ )}
- ) : selectedFilter.type === 'select' ? ( + ) : selectedFilter.type === "select" ? ( <> - { - if (e.key === 'Backspace' && !inputValue) { + if (e.key === "Backspace" && !inputValue) { e.preventDefault(); handleBackToFilters(); - } else if (e.key === 'Escape') { + } else if (e.key === "Escape") { e.preventDefault(); handleBackToFilters(); } @@ -696,8 +725,10 @@ export function ProductFilters({ {selectedFilter.options - ?.filter(option => - option.label.toLowerCase().includes(inputValue.toLowerCase()) + ?.filter((option) => + option.label + .toLowerCase() + .includes(inputValue.toLowerCase()) ) .map((option) => ( {option.label} - )) - } + ))} ) : ( <> - { - if (e.key === 'Enter' && inputValue.trim()) { + if (e.key === "Enter" && inputValue.trim()) { handleApplyFilter(inputValue.trim()); - } else if (e.key === 'Escape') { + } else if (e.key === "Escape") { e.preventDefault(); handleBackToFilters(); } @@ -786,4 +817,4 @@ export function ProductFilters({
); -} \ No newline at end of file +}