Filter Components
The Filter Components provide comprehensive filtering functionality for the WorkPayCore Frontend application. These components handle data filtering, search operations, date range selection, and multi-criteria filtering with consistent UX patterns.
Overview
This document covers filter-related components that enable users to narrow down data views, search through datasets, and apply complex filtering criteria throughout the application.
Components Overview
Core Filter Components
- FilteringComponent - Main reusable filter wrapper with popover interface
- DatePickerFilterButton - Date range filter with calendar picker
- CheckboxFilterButton - Multi-select checkbox filter with search
- CustomFilter - Collapsible filter with search and checkbox options
Specialized Filter Components
- ExpensesFilters - Expense-specific filtering interface
- PaymentFilters - Payment batch filtering interface
- PayrollFilters - Payroll-specific filtering interface
- EmployeeHistoryFilter - Employee history filtering
Filter Utilities
- useFilterState - Custom hook for filter state management
- FilterDropdown - Simple dropdown filter component
FilteringComponent
Main reusable filter component that provides a popover interface for complex filtering operations.
Component Location
import { FilteringComponent, useFilterState } from 'components/ReusableFilter';
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| children | ReactNode | ✓ | - | Filter form content |
| handleSubmit | function | ✓ | - | Filter submit handler |
| showFilters | boolean | ✓ | - | Whether filters are visible |
| setShowFilters | function | ✓ | - | Filter visibility setter |
| isOpen | boolean | ✓ | - | Popover open state |
| onOpen | function | ✓ | - | Open handler |
| onClose | function | ✓ | - | Close handler |
| query | object | ✓ | - | Current filter state |
| setQuery | function | ✓ | - | Filter state setter |
| isLoading | boolean | ✓ | - | Loading state |
| isDisabled | boolean | - | false | Disabled state |
| handleReset | function | - | - | Reset handler |
Usage Examples
Basic Filter Implementation
import { FilteringComponent, useFilterState } from 'components/ReusableFilter';
import { useDisclosure } from '@chakra-ui/react';
function BasicFilters() {
const { isOpen, onClose, onOpen } = useDisclosure();
const [showFilters, setShowFilters] = useState(false);
const [query, setQuery] = useState({});
const [filterState, handleChange] = useFilterState({});
const onSubmit = event => {
event?.preventDefault();
setShowFilters(true);
setQuery({ ...filterState });
onClose();
};
const handleReset = () => {
setShowFilters(false);
setQuery({});
// Reset individual filter states
};
return (
<FilteringComponent
handleSubmit={onSubmit}
variant='filters'
showFilters={showFilters}
setShowFilters={setShowFilters}
isOpen={isOpen}
onClose={onClose}
onOpen={onOpen}
query={query}
setQuery={setQuery}
isLoading={false}
currentState={filterState}
handleReset={handleReset}
isDisabled={Object.keys(filterState).length === 0}
>
{/* Filter form content */}
<VStack spacing={4}>
<CustomSelectDropdown
options={statusOptions}
onChange={e => handleChange({ status: e.value })}
name='Status Filter'
/>
<DateRangeComponent
onChange={dates =>
handleChange({ startDate: dates[0], endDate: dates[1] })
}
/>
</VStack>
</FilteringComponent>
);
}
DatePickerFilterButton
A filter button component that provides date range selection with a calendar picker interface.
Component Location
import DatePickerFilterButton from 'components/Dropdown/DatePickerFilterButton';
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| selectedDate | [Date | null, Date | null] | ✓ | - | Selected date range |
| onDateChange | function | ✓ | - | Date change handler |
| clearFilter | function | ✓ | - | Filter clear handler |
| showComponent | boolean | ✓ | - | Component visibility |
| label | string | - | - | Filter label |
| isDisabled | boolean | - | false | Disabled state |
Usage Examples
Date Range Filter
import DatePickerFilterButton from 'components/Dropdown/DatePickerFilterButton';
import { format } from 'date-fns';
function DateRangeFilter() {
const [selectedDate, setSelectedDate] = useState([null, null]);
const [selectedFilters, setSelectedFilters] = useState({ date: false });
const handleDateChange = dates => {
setSelectedDate(dates);
const [startDate, endDate] = dates;
if (startDate && endDate) {
onFilterChange({
date_range: {
start: format(startDate, 'yyyy-MM-dd'),
end: format(endDate, 'yyyy-MM-dd'),
},
});
}
};
return (
<DatePickerFilterButton
selectedDate={selectedDate}
onDateChange={handleDateChange}
showComponent={selectedFilters.date}
clearFilter={() => {
setSelectedFilters(prev => ({ ...prev, date: false }));
onFilterChange({ date_range: { start: null, end: null } });
}}
label='Date Range'
/>
);
}
CheckboxFilterButton
A multi-select filter component with checkbox options and search functionality.
Component Location
import CheckboxFilterButton from 'components/Dropdown/CheckboxFilterButton';
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| items | array | ✓ | - | Array of filter items |
| onItemsChange | function | ✓ | - | Items change handler |
| showComponent | boolean | ✓ | - | Component visibility |
| hideComponent | function | ✓ | - | Hide component handler |
| label | string | - | - | Filter label |
| isDisabled | boolean | - | false | Disabled state |
| showAvatar | boolean | - | false | Show avatar for items |
TypeScript Interface
interface MenuItem {
label: string;
checked: boolean;
value: string | number;
profile_picture?: string;
}
Usage Examples
Project Filter
import CheckboxFilterButton from 'components/Dropdown/CheckboxFilterButton';
function ProjectFilter() {
const [projectItems, setProjectItems] = useState([]);
const [selectedFilters, setSelectedFilters] = useState({ project: false });
const handleProjectItemsChange = items => {
setProjectItems(items);
const checkedItems = items
.filter(item => item.checked)
.map(item => item.value);
onFilterChange({ project: checkedItems.join(',') });
};
useEffect(() => {
if (projectsData?.length) {
setProjectItems(
projectsData.map(project => ({
label: project.name,
value: project.id,
checked: false,
})),
);
}
}, [projectsData]);
return (
<CheckboxFilterButton
items={projectItems}
onItemsChange={handleProjectItemsChange}
showComponent={selectedFilters.project}
label='Project'
isDisabled={isProjectLoading}
hideComponent={() => {
setSelectedFilters(prev => ({ ...prev, project: false }));
onFilterChange(prevFilters => {
const { project, ...remainingFilters } = prevFilters;
return remainingFilters;
});
}}
/>
);
}
Employee Filter with Avatars
function EmployeeFilter() {
const [employeeItems, setEmployeeItems] = useState([]);
useEffect(() => {
if (employeesData?.length) {
setEmployeeItems(
employeesData.map(employee => ({
label: employee.employee_name,
value: employee.id,
profile_picture: employee.profile_picture,
checked: false,
})),
);
}
}, [employeesData]);
return (
<CheckboxFilterButton
items={employeeItems}
onItemsChange={handleEmployeeItemsChange}
showComponent={selectedFilters.employee}
label='Employee'
showAvatar
hideComponent={() => {
setSelectedFilters(prev => ({ ...prev, employee: false }));
}}
/>
);
}
useFilterState
Custom hook for managing filter state with update and reset functionality.
Usage
import { useFilterState } from 'components/ReusableFilter';
const [filterState, handleChange, handleResetFilterState] =
useFilterState(initialState);
Parameters
| Parameter | Type | Description |
|---|---|---|
| initialState | object | Initial filter state |
Return Values
| Value | Type | Description |
|---|---|---|
| filterState | object | Current filter state |
| handleChange | function | Update filter state |
| handleResetFilterState | function | Reset to initial state |
Usage Examples
function FilterExample() {
const [filterState, handleChange, handleResetFilterState] = useFilterState({
status: '',
dateRange: null,
department: '',
});
const updateStatus = (status) => {
handleChange({ status });
};
const resetFilters = () => {
handleResetFilterState();
};
return (
// Filter UI components
);
}
Filter Patterns
Complete Table Filter Pattern
import DatePickerFilterButton from 'components/Dropdown/DatePickerFilterButton';
import CheckboxFilterButton from 'components/Dropdown/CheckboxFilterButton';
import FilterDropdown from 'components/Dropdown/FilterDropdown';
function TableWithFilters() {
const [selectedFilters, setSelectedFilters] = useState({
date: false,
project: false,
client: false,
employee: false,
status: false,
});
const [selectedDate, setSelectedDate] = useState([null, null]);
const [projectItems, setProjectItems] = useState([]);
const [clientItems, setClientItems] = useState([]);
const [employeeItems, setEmployeeItems] = useState([]);
const [statusItems, setStatusItems] = useState([
{ label: 'Active', value: 'active', checked: false },
{ label: 'Inactive', value: 'inactive', checked: false },
]);
const handleDateChange = dates => {
setSelectedDate(dates);
const [startDate, endDate] = dates;
if (startDate && endDate) {
onFilterChange({
date_range: {
start: format(startDate, 'yyyy-MM-dd'),
end: format(endDate, 'yyyy-MM-dd'),
},
});
}
};
const resetFilters = () => {
setSelectedFilters({
date: false,
project: false,
client: false,
employee: false,
status: false,
});
setSelectedDate([null, null]);
setProjectItems(projectItems.map(item => ({ ...item, checked: false })));
setClientItems(clientItems.map(item => ({ ...item, checked: false })));
setEmployeeItems(employeeItems.map(item => ({ ...item, checked: false })));
setStatusItems(statusItems.map(item => ({ ...item, checked: false })));
setFilters({});
};
const selectedFiltersCount =
Object.values(selectedFilters).filter(Boolean).length;
const filterMenuItems = [
{
label: 'Date',
icon: <CalendarOutline boxSize={4} color='#003049' />,
onClick: () => {
setSelectedFilters(prevState => ({
...prevState,
date: !prevState.date,
}));
},
selected: selectedFilters.date,
},
{
label: 'Project',
icon: <ProjectOutline boxSize={4} color='#003049' />,
onClick: () => {
setSelectedFilters(prevState => ({
...prevState,
project: !prevState.project,
}));
},
selected: selectedFilters.project,
},
// ... more filter menu items
];
return (
<Box border='1px solid #E3E9EC' borderRadius='8px'>
<Wrap spacing={4} m={4} justify='space-between'>
<Wrap flex='1' spacing={4}>
<WrapItem>
<DatePickerFilterButton
selectedDate={selectedDate}
onDateChange={handleDateChange}
showComponent={selectedFilters.date}
clearFilter={() => {
setSelectedFilters(prev => ({ ...prev, date: false }));
onFilterChange({ date_range: { start: null, end: null } });
}}
label='Date'
/>
</WrapItem>
<WrapItem>
<CheckboxFilterButton
items={projectItems}
onItemsChange={handleProjectItemsChange}
showComponent={selectedFilters.project}
label='Project'
isDisabled={isProjectLoading}
hideComponent={() => {
setSelectedFilters(prev => ({ ...prev, project: false }));
}}
/>
</WrapItem>
<WrapItem>
<CheckboxFilterButton
items={clientItems}
onItemsChange={handleClientItemsChange}
showComponent={selectedFilters.client}
label='Client'
isDisabled={isClientLoading}
hideComponent={() => {
setSelectedFilters(prev => ({ ...prev, client: false }));
}}
/>
</WrapItem>
<WrapItem>
<CheckboxFilterButton
items={employeeItems}
onItemsChange={handleEmployeeItemsChange}
showComponent={selectedFilters.employee}
label='Employee'
showAvatar
isDisabled={isEmployeeLoading}
hideComponent={() => {
setSelectedFilters(prev => ({ ...prev, employee: false }));
}}
/>
</WrapItem>
{Object.values(selectedFilters).some(filter => filter) && (
<WrapItem>
<Button
onClick={resetFilters}
bg='#D6F1CA'
color='#006B3E'
_hover={{ bg: '#D6F1CA' }}
whiteSpace='nowrap'
px={4}
>
Reset filters
</Button>
</WrapItem>
)}
</Wrap>
<Wrap flex='none' spacing={4}>
<WrapItem>
<FilterDropdown
label='Filter'
icon={<FilterOutline />}
menuItems={filterMenuItems}
selectedFiltersCount={selectedFiltersCount}
/>
</WrapItem>
</Wrap>
</Wrap>
<WPDataTable
customColumn={columns}
tableData={data || []}
isLoading={isLoading}
/>
</Box>
);
}
Modal Filter Pattern
function ModalFilterPattern() {
const { isOpen, onClose, onOpen } = useDisclosure();
const [filterState, handleChange] = useFilterState({});
const [showFilters, setShowFilters] = useState(false);
return (
<FilteringComponent
handleSubmit={onSubmit}
variant='filters'
showFilters={showFilters}
setShowFilters={setShowFilters}
isOpen={isOpen}
onClose={onClose}
onOpen={onOpen}
query={query}
setQuery={setQuery}
isLoading={isLoading}
currentState={filterState}
handleReset={handleReset}
>
<VStack spacing={4} p={4}>
<CustomSelectDropdown
options={branchOptions}
onChange={e => handleChange({ branch_id: e.value })}
name='Branch'
/>
<CustomSelectDropdown
options={statusOptions}
onChange={e => handleChange({ status: e.value })}
name='Status'
/>
<DateRangeComponent
name='Date Range'
onChange={dates =>
handleChange({
startDate: dates[0],
endDate: dates[1],
})
}
/>
</VStack>
</FilteringComponent>
);
}
Best Practices
Filter State Management
-
Consistent State Structure
- Use standardized filter state objects
- Maintain filter state separately from UI state
- Implement proper state reset functionality
-
Performance Optimization
- Debounce filter changes to reduce API calls
- Use memoization for expensive filter operations
- Implement proper loading states
-
User Experience
- Provide clear filter indicators
- Show selected filter counts
- Enable easy filter reset
- Maintain filter state across navigation
Filter Component Design
-
Accessibility
- Ensure keyboard navigation support
- Provide proper ARIA labels
- Support screen readers
-
Responsive Design
- Handle mobile filter layouts
- Use appropriate breakpoints
- Ensure touch-friendly interactions
-
Visual Consistency
- Use consistent styling across filters
- Maintain clear visual hierarchy
- Provide loading and error states
Testing
Unit Tests
import { render, screen, fireEvent } from '@testing-library/react';
import CheckboxFilterButton from 'components/Dropdown/CheckboxFilterButton';
describe('CheckboxFilterButton', () => {
const mockItems = [
{ label: 'Project A', value: 1, checked: false },
{ label: 'Project B', value: 2, checked: false },
];
it('renders filter button with label', () => {
render(
<CheckboxFilterButton
items={mockItems}
onItemsChange={jest.fn()}
showComponent={true}
hideComponent={jest.fn()}
label='Projects'
/>,
);
expect(screen.getByText('Projects')).toBeInTheDocument();
});
it('handles checkbox selection', async () => {
const mockOnItemsChange = jest.fn();
const { user } = render(
<CheckboxFilterButton
items={mockItems}
onItemsChange={mockOnItemsChange}
showComponent={true}
hideComponent={jest.fn()}
label='Projects'
/>,
);
await user.click(screen.getByRole('button'));
await user.click(screen.getByRole('checkbox', { name: 'Project A' }));
expect(mockOnItemsChange).toHaveBeenCalledWith([
{ label: 'Project A', value: 1, checked: true },
{ label: 'Project B', value: 2, checked: false },
]);
});
it('filters items based on search', async () => {
const { user } = render(
<CheckboxFilterButton
items={mockItems}
onItemsChange={jest.fn()}
showComponent={true}
hideComponent={jest.fn()}
label='Projects'
/>,
);
await user.click(screen.getByRole('button'));
await user.type(screen.getByPlaceholderText('Search'), 'Project A');
expect(screen.getByText('Project A')).toBeInTheDocument();
expect(screen.queryByText('Project B')).not.toBeInTheDocument();
});
});
Integration Tests
describe('TableFilterIntegration', () => {
it('applies multiple filters correctly', async () => {
const { user } = render(<TableWithFilters />);
// Apply date filter
await user.click(screen.getByText('Date'));
// ... date selection logic
// Apply project filter
await user.click(screen.getByText('Project'));
await user.click(screen.getByText('Project A'));
// Verify API call with combined filters
expect(mockApiCall).toHaveBeenCalledWith({
date_range: { start: '2024-01-01', end: '2024-01-31' },
project: '1',
});
});
it('resets all filters correctly', async () => {
const { user } = render(<TableWithFilters />);
// Apply some filters
await user.click(screen.getByText('Date'));
await user.click(screen.getByText('Project'));
// Reset filters
await user.click(screen.getByText('Reset filters'));
expect(mockApiCall).toHaveBeenLastCalledWith({});
});
});
Migration Guide
From Legacy Filter Components
-
Update Import Paths
// Old
import FilterComponent from 'components/OldFilter';
// New
import { FilteringComponent } from 'components/ReusableFilter'; -
Update State Management
// Old
const [filters, setFilters] = useState({});
// New
const [filterState, handleChange] = useFilterState({}); -
Update Filter Structure
// Old
<FilterComponent onFilter={handleFilter}>
<FilterInput name="status" />
</FilterComponent>
// New
<FilteringComponent
handleSubmit={handleSubmit}
query={query}
setQuery={setQuery}
isOpen={isOpen}
onOpen={onOpen}
onClose={onClose}
>
<CustomSelectDropdown
options={statusOptions}
onChange={(e) => handleChange({ status: e.value })}
/>
</FilteringComponent>
Advanced Usage
Custom Filter Hook
function useAdvancedFilters(initialFilters = {}) {
const [filterState, handleChange] = useFilterState(initialFilters);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const applyFilters = useCallback(async filters => {
setIsLoading(true);
setError(null);
try {
const result = await filterAPI(filters);
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setIsLoading(false);
}
}, []);
const resetFilters = useCallback(() => {
handleChange(initialFilters);
}, [handleChange, initialFilters]);
return {
filterState,
handleChange,
applyFilters,
resetFilters,
isLoading,
error,
};
}
Filter Composition Pattern
function ComposedFilters({ children, onFiltersChange }) {
const [activeFilters, setActiveFilters] = useState(new Set());
const registerFilter = useCallback(
(filterId, filterData) => {
setActiveFilters(prev => new Set([...prev, filterId]));
onFiltersChange(filterId, filterData);
},
[onFiltersChange],
);
const unregisterFilter = useCallback(
filterId => {
setActiveFilters(prev => {
const newSet = new Set(prev);
newSet.delete(filterId);
return newSet;
});
onFiltersChange(filterId, null);
},
[onFiltersChange],
);
return (
<FilterContext.Provider value={{ registerFilter, unregisterFilter }}>
{children}
{activeFilters.size > 0 && (
<Button onClick={() => setActiveFilters(new Set())}>
Clear All Filters ({activeFilters.size})
</Button>
)}
</FilterContext.Provider>
);
}