Skip to main content

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

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

PropTypeRequiredDefaultDescription
childrenReactNode-Filter form content
handleSubmitfunction-Filter submit handler
showFiltersboolean-Whether filters are visible
setShowFiltersfunction-Filter visibility setter
isOpenboolean-Popover open state
onOpenfunction-Open handler
onClosefunction-Close handler
queryobject-Current filter state
setQueryfunction-Filter state setter
isLoadingboolean-Loading state
isDisabledboolean-falseDisabled state
handleResetfunction--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

PropTypeRequiredDefaultDescription
selectedDate[Date | null, Date | null]-Selected date range
onDateChangefunction-Date change handler
clearFilterfunction-Filter clear handler
showComponentboolean-Component visibility
labelstring--Filter label
isDisabledboolean-falseDisabled 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

PropTypeRequiredDefaultDescription
itemsarray-Array of filter items
onItemsChangefunction-Items change handler
showComponentboolean-Component visibility
hideComponentfunction-Hide component handler
labelstring--Filter label
isDisabledboolean-falseDisabled state
showAvatarboolean-falseShow 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

ParameterTypeDescription
initialStateobjectInitial filter state

Return Values

ValueTypeDescription
filterStateobjectCurrent filter state
handleChangefunctionUpdate filter state
handleResetFilterStatefunctionReset 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}>
&lt;WrapItem&gt;
<DatePickerFilterButton
selectedDate={selectedDate}
onDateChange={handleDateChange}
showComponent={selectedFilters.date}
clearFilter={() => {
setSelectedFilters(prev => ({ ...prev, date: false }));
onFilterChange({ date_range: { start: null, end: null } });
}}
label='Date'
/>
</WrapItem>

&lt;WrapItem&gt;
<CheckboxFilterButton
items={projectItems}
onItemsChange={handleProjectItemsChange}
showComponent={selectedFilters.project}
label='Project'
isDisabled={isProjectLoading}
hideComponent={() => {
setSelectedFilters(prev => ({ ...prev, project: false }));
}}
/>
</WrapItem>

&lt;WrapItem&gt;
<CheckboxFilterButton
items={clientItems}
onItemsChange={handleClientItemsChange}
showComponent={selectedFilters.client}
label='Client'
isDisabled={isClientLoading}
hideComponent={() => {
setSelectedFilters(prev => ({ ...prev, client: false }));
}}
/>
</WrapItem>

&lt;WrapItem&gt;
<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) && (
&lt;WrapItem&gt;
<Button
onClick={resetFilters}
bg='#D6F1CA'
color='#006B3E'
_hover={{ bg: '#D6F1CA' }}
whiteSpace='nowrap'
px={4}
>
Reset filters
</Button>
</WrapItem>
)}
</Wrap>

<Wrap flex='none' spacing={4}>
&lt;WrapItem&gt;
<FilterDropdown
label='Filter'
icon={<FilterOutline />}
menuItems={filterMenuItems}
selectedFiltersCount={selectedFiltersCount}
/>
</WrapItem>
</Wrap>
</Wrap>

<WPDataTable
customColumn={columns}
tableData={data || []}
isLoading={isLoading}
/>
</Box>
);
}
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

  1. Consistent State Structure

    • Use standardized filter state objects
    • Maintain filter state separately from UI state
    • Implement proper state reset functionality
  2. Performance Optimization

    • Debounce filter changes to reduce API calls
    • Use memoization for expensive filter operations
    • Implement proper loading states
  3. User Experience

    • Provide clear filter indicators
    • Show selected filter counts
    • Enable easy filter reset
    • Maintain filter state across navigation

Filter Component Design

  1. Accessibility

    • Ensure keyboard navigation support
    • Provide proper ARIA labels
    • Support screen readers
  2. Responsive Design

    • Handle mobile filter layouts
    • Use appropriate breakpoints
    • Ensure touch-friendly interactions
  3. 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

  1. Update Import Paths

    // Old
    import FilterComponent from 'components/OldFilter';

    // New
    import { FilteringComponent } from 'components/ReusableFilter';
  2. Update State Management

    // Old
    const [filters, setFilters] = useState({});

    // New
    const [filterState, handleChange] = useFilterState({});
  3. 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>
);
}