import React, { useState, useEffect, useRef, useMemo } from 'react'; import { FilterConfig } from '../../types'; import { ChevronDown, Search, Filter, X, Plus, Save, Trash } from '../../icons'; export interface FilterPreset { id: string; name: string; filters: Record; createdAt: number; } export interface AdvancedFilterPanelProps { configs: FilterConfig[]; values: Record; onFilterChange: (field: string, value: any) => void; onClearFilters?: () => void; allowPresets?: boolean; allowComplexFilters?: boolean; initialPresets?: FilterPreset[]; onPresetsChange?: (presets: FilterPreset[]) => void; expanded?: boolean; onExpandedChange?: (expanded: boolean) => void; persistKey?: string; labels?: { searchPlaceholder?: string; clearFiltersButton?: string; savePresetButton?: string; addFilterButton?: string; presetNamePlaceholder?: string; noFiltersMessage?: string; addGroupButton?: string; [key: string]: string | undefined; }; } const AdvancedFilterPanel: React.FC = ({ configs, values, onFilterChange, onClearFilters, allowPresets = false, allowComplexFilters = false, initialPresets = [], onPresetsChange, expanded = false, onExpandedChange, persistKey, labels = {} }) => { // State for panel controls const [isExpanded, setIsExpanded] = useState(expanded); const [searchTerm, setSearchTerm] = useState(''); const [presetName, setPresetName] = useState(''); const [presets, setPresets] = useState(initialPresets || []); const [activePresetId, setActivePresetId] = useState(null); // Refs for tracking DOM elements const panelRef = useRef(null); // Calculate active filters const activeFilters = useMemo(() => { return Object.entries(values).filter(([_, value]) => value !== undefined && value !== null && value !== '' ); }, [values]); const activeFilterCount = activeFilters.length; // Toggle panel expansion const toggleExpanded = () => { const newState = !isExpanded; setIsExpanded(newState); if (onExpandedChange) { onExpandedChange(newState); } }; // Effect to load presets from localStorage useEffect(() => { if (!allowPresets) return; try { const key = `rpt-filter-presets-${persistKey || 'default'}`; const storedPresets = localStorage.getItem(key); if (storedPresets) { const parsed = JSON.parse(storedPresets) as FilterPreset[]; if (Array.isArray(parsed)) { setPresets(parsed); } } else if (initialPresets?.length) { setPresets(initialPresets); } } catch (error) { console.error('Error loading filter presets:', error); } }, [allowPresets, persistKey, initialPresets]); // Effect to save presets to localStorage useEffect(() => { if (!allowPresets) return; try { const key = `rpt-filter-presets-${persistKey || 'default'}`; localStorage.setItem(key, JSON.stringify(presets)); // Notify parent component if (onPresetsChange) { onPresetsChange(presets); } } catch (error) { console.error('Error saving filter presets:', error); } }, [presets, allowPresets, persistKey, onPresetsChange]); // Save current filters as a preset const saveAsPreset = () => { if (!presetName.trim() || Object.keys(values).length === 0) return; const newPreset: FilterPreset = { id: `preset-${Date.now()}`, name: presetName.trim(), filters: { ...values }, // Clone the current filter values createdAt: Date.now() }; setPresets(prev => [...prev, newPreset]); setPresetName(''); setActivePresetId(newPreset.id); }; // Apply a preset const applyPreset = (presetId: string) => { const preset = presets.find(p => p.id === presetId); if (!preset) return; // Clear existing filters first if (onClearFilters) onClearFilters(); // Apply preset filters one by one Object.entries(preset.filters).forEach(([field, value]) => { onFilterChange(field, value); }); setActivePresetId(presetId); }; // Delete a preset const deletePreset = (e: React.MouseEvent, presetId: string) => { e.stopPropagation(); setPresets(prev => prev.filter(p => p.id !== presetId)); if (activePresetId === presetId) { setActivePresetId(null); } }; // Clear all filters const handleClearFilters = () => { if (onClearFilters) onClearFilters(); setActivePresetId(null); }; // Filter configs based on search term const filteredConfigs = useMemo(() => { if (!searchTerm.trim()) return configs; const lowercasedSearch = searchTerm.toLowerCase(); return configs.filter(config => config.label.toLowerCase().includes(lowercasedSearch) || config.key.toLowerCase().includes(lowercasedSearch) ); }, [configs, searchTerm]); // Handle remove filter const handleRemoveFilter = (field: string) => { onFilterChange(field, undefined); // When removing a filter, also clear active preset if set if (activePresetId) { setActivePresetId(null); } }; return (
Filters {activeFilterCount > 0 && ( {activeFilterCount} )}
{isExpanded && (
{/* Filter search */}
setSearchTerm(e.target.value)} />
{/* Filter presets section */} {allowPresets && (
Saved Filters
{presets.length > 0 && (
{presets.map(preset => (
))}
)}
setPresetName(e.target.value)} />
)} {/* Active filters section */}
Active Filters
{activeFilters.length > 0 ? (
{activeFilters.map(([field, value]) => { const config = configs.find(c => c.key === field); if (!config) return null; return (
{config.label}
onFilterChange(field, value)} />
); })}
) : (
{labels.noFiltersMessage || "No filters applied"}
)}
{/* Available filters list */}
Available Filters
{filteredConfigs.map(config => { // Skip if already active if (values[config.key] !== undefined && values[config.key] !== null && values[config.key] !== '') { return null; } return (
{config.label}
onFilterChange(config.key, value)} />
); })}
{/* Actions */}
{activeFilterCount > 0 && ( )}
)}
); }; // Component for rendering different filter control types const FilterControl: React.FC<{ config: FilterConfig; value: any; onChange: (value: any) => void; }> = ({ config, value, onChange }) => { const handleClear = (e: React.MouseEvent) => { e.stopPropagation(); onChange(''); }; switch (config.type) { case 'select': return (
{value && ( )}
); case 'multiselect': // Simplified multiselect - could be enhanced with a proper multiselect component return (
{value && ( )}
); case 'text': return (
onChange(e.target.value)} placeholder={config.placeholder || `Filter by ${config.label}`} /> {value && ( )}
); case 'number': return (
onChange(e.target.value === '' ? '' : Number(e.target.value))} placeholder={config.placeholder || `Filter by ${config.label}`} /> {value !== '' && value !== undefined && ( )}
); case 'date': return (
onChange(e.target.value)} /> {value && ( )}
); case 'dateRange': // For simplicity, we're just handling a single date value // A real dateRange would need more complex UI and state handling return (
onChange({ ...value, from: e.target.value })} placeholder="From" /> to onChange({ ...value, to: e.target.value })} placeholder="To" />
{value && (value.from || value.to) && ( )}
); case 'boolean': return (
{value !== undefined && ( )}
); default: return (
onChange(e.target.value)} placeholder={config.placeholder || `Filter by ${config.label}`} /> {value && ( )}
); } }; export default AdvancedFilterPanel;