Skip to main content

filter-sort-handler Utility

filter-sort-handler adalah core utility yang menggabungkan filter dan sort handlers dalam satu factory function untuk menyediakan integrated solution untuk data processing dan query parameter generation.

📋 Overview

🎯 Fungsionalitas Utama

  • Factory Pattern: Membuat reusable handler instances dengan custom configuration
  • Combined Processing: Single function untuk process filters dan sort bersamaan
  • Query Building: Generate complete query parameters object
  • Flexible Configuration: Customizable processing options untuk filters dan sort
  • Integration Ready: Designed untuk API integration dan URL generation

🏗️ Architecture Pattern

🚀 Quick Start

Basic Factory Usage

import { createFilterSortHandler } from '@/utils/filter-sort-handler';

// Create handler instance
const handler = createFilterSortHandler(
// Filter options
{
encodeValues: true,
encodeFilters: true,
formatDateValues: true
},
// Sort options
{
encodeSort: true
}
);

// Process filters and sort
const { filter, sort } = handler.processFiltersAndSort(
{
search: 'john doe',
status: 'active',
created_at: { gte: '2024-01-01' }
},
{ columnAccessor: 'name', direction: 'asc' }
);

console.log(filter); // "search:john doe;status:active;created_at:gte:2024-01-01"
console.log(sort); // "name,asc"

// Build complete query parameters
const queryParams = handler.buildQueryParams(
{
search: 'john doe',
status: 'active'
},
{ columnAccessor: 'name', direction: 'asc' },
{ page: 1, perPage: 10 }
);

console.log(queryParams);
// {
// filter: "search:john doe;status:active",
// sort: "name,asc",
// page: 1,
// perPage: 10
// }

API Integration Example

import { createFilterSortHandler } from '@/utils/filter-sort-handler';

// Create API-specific handler
const apiHandler = createFilterSortHandler(
{
encodeValues: true, // URL encode values
encodeFilters: true, // URL encode entire filter string
formatDateValues: true // Format dates as YYYY-MM-DD
},
{
encodeSort: true // URL encode sort string
}
);

// API service function
async function fetchUsers(options) {
const { filters, sort, pagination } = options;

// Process and build query parameters
const { filter, sort: processedSort } = apiHandler.processFiltersAndSort(filters, sort);
const queryParams = apiHandler.buildQueryParams(filters, sort, pagination);

// Make API call
const response = await fetch(`/api/users?${new URLSearchParams(queryParams)}`);
return response.json();
}

// Usage in component
function UserList() {
const [users, setUsers] = useState([]);

const loadUsers = async () => {
const result = await fetchUsers({
filters: {
search: 'john',
role: 'admin',
created_at: { gte: '2024-01-01' }
},
sort: { columnAccessor: 'created_at', direction: 'desc' },
pagination: { page: 1, perPage: 20 }
});

setUsers(result.data);
};

return (
<div>
<button onClick={loadUsers}>Load Users</button>
{/* User list display */}
</div>
);
}

📝 API Reference

Factory Function

interface FilterSortOptions {
encodeValues?: boolean; // URL encode individual filter values
encodeFilters?: boolean; // URL encode entire filter string
formatDateValues?: boolean; // Format date values to YYYY-MM-DD
}

interface SortOptions {
encodeSort?: boolean; // URL encode sort string
}

function createFilterSortHandler(
filterOptions: FilterSortOptions = {},
sortOptions: SortOptions = {}
): {
processFiltersAndSort: (
filters: Record<string, unknown>,
sortStatus: DataTableSortStatus | SortCondition | null
) => { filter: string; sort: string };

buildQueryParams: (
filters: Record<string, unknown>,
sortStatus: DataTableSortStatus | SortCondition | null,
pagination: { page: number; perPage: number }
) => Record<string, unknown>;

filter: FilterHandler; // Individual filter processor
sort: SortHandler; // Individual sort processor
};

Return Methods

MethodParametersReturnsDescription
processFiltersAndSortfilters, sortStatus{ filter: string; sort: string }Process both filters and sort, returns query strings
buildQueryParamsfilters, sortStatus, paginationRecord<string, unknown>Build complete query parameters object
filterfiltersstringIndividual filter processor (from filter-handler)
sortsortStatusstringIndividual sort processor (from sort-handler)

🎨 Examples

Multiple Handler Instances

import { createFilterSortHandler } from '@/utils/filter-sort-handler';

// Different configurations for different use cases

// 1. API handler with full encoding
const apiHandler = createFilterSortHandler(
{
encodeValues: true,
encodeFilters: true,
formatDateValues: true
},
{
encodeSort: true
}
);

// 2. URL handler with minimal encoding
const urlHandler = createFilterSortHandler(
{
encodeValues: false,
encodeFilters: false,
formatDateValues: true
},
{
encodeSort: false
}
);

// 3. Internal processing handler
const internalHandler = createFilterSortHandler(
{
encodeValues: false,
encodeFilters: false,
formatDateValues: false
},
{
encodeSort: false
}
);

// Usage examples
function demonstrateHandlers() {
const filters = {
search: 'john doe',
status: 'active',
created_at: { gte: '2024-01-01' }
};
const sort = { columnAccessor: 'name', direction: 'asc' };

// API handler (full encoding)
const apiResult = apiHandler.processFiltersAndSort(filters, sort);
console.log('API Result:', apiResult);
// { filter: "search%3Ajohn%20doe%3Bstatus%3Aactive%3Bcreated_at%3Agte%3A2024-01-01", sort: "name%2Casc" }

// URL handler (minimal encoding)
const urlResult = urlHandler.processFiltersAndSort(filters, sort);
console.log('URL Result:', urlResult);
// { filter: "search:john doe;status:active;created_at:gte:2024-01-01", sort: "name,asc" }

// Internal handler (no encoding)
const internalResult = internalHandler.processFiltersAndSort(filters, sort);
console.log('Internal Result:', internalResult);
// { filter: "search:john doe;status:active;created_at:gte:2024-01-01", sort: "name,asc" }
}

E-commerce Product Filters

import { createFilterSortHandler } from '@/utils/filter-sort-handler';

// Create product-specific handler
const productHandler = createFilterSortHandler(
{
encodeValues: true,
encodeFilters: true,
formatDateValues: true
},
{
encodeSort: true
}
);

// Product service
class ProductService {
private handler = productHandler;

async getProducts(options: {
filters?: Record<string, unknown>;
sort?: DataTableSortStatus | SortCondition | null;
pagination?: { page: number; perPage: number };
}) {
const { filters = {}, sort, pagination = { page: 1, perPage: 20 } } = options;

// Build query parameters
const queryParams = this.handler.buildQueryParams(filters, sort, pagination);

// Make API call
const response = await fetch(`/api/products?${new URLSearchParams(queryParams)}`);
return response.json();
}

// Specific filter methods
async searchProducts(query: string, category?: string) {
const filters: Record<string, unknown> = { search: query };
if (category) filters.category = category;

return this.getProducts({
filters,
sort: { columnAccessor: 'relevance', direction: 'desc' }
});
}

async getProductsByPriceRange(minPrice: number, maxPrice: number) {
return this.getProducts({
filters: {
price: { gte: minPrice, lte: maxPrice }
},
sort: { columnAccessor: 'price', direction: 'asc' }
});
}

async getNewArrivals() {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

return this.getProducts({
filters: {
created_at: { gte: thirtyDaysAgo.toISOString().split('T')[0] }
},
sort: { columnAccessor: 'created_at', direction: 'desc' }
});
}
}

// Usage in React component
function ProductCatalog() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const productService = new ProductService();

const loadProducts = async (filters = {}) => {
setLoading(true);
try {
const result = await productService.getProducts({ filters });
setProducts(result.data);
} catch (error) {
console.error('Failed to load products:', error);
} finally {
setLoading(false);
}
};

const handleSearch = (query: string) => {
loadProducts({ search: query });
};

const handleCategoryFilter = (category: string) => {
loadProducts({ category });
};

const handlePriceFilter = (min: number, max: number) => {
loadProducts({ price: { gte: min, lte: max } });
};

return (
<div>
<SearchInput onSearch={handleSearch} />
<CategoryFilter onCategoryChange={handleCategoryFilter} />
<PriceRangeFilter onPriceChange={handlePriceFilter} />

{loading ? (
<div>Loading products...</div>
) : (
<ProductGrid products={products} />
)}
</div>
);
}

Admin Dashboard with Complex Filters

import { createFilterSortHandler } from '@/utils/filter-sort-handler';

// Admin-specific handler with custom configuration
const adminHandler = createFilterSortHandler(
{
encodeValues: true,
encodeFilters: true,
formatDateValues: true
},
{
encodeSort: true
}
);

// Admin service class
class AdminService {
private handler = adminHandler;

async getUsers(options: {
filters?: Record<string, unknown>;
sort?: DataTableSortStatus | SortCondition | null;
pagination?: { page: number; perPage: number };
}) {
const { filters = {}, sort, pagination = { page: 1, perPage: 50 } } = options;

const queryParams = this.handler.buildQueryParams(filters, sort, pagination);
const response = await fetch(`/api/admin/users?${new URLSearchParams(queryParams)}`);
return response.json();
}

async getUserActivity(userId: string, options: {
dateFrom?: Date;
dateTo?: Date;
action?: string;
}) {
const filters: Record<string, unknown> = { user_id: userId };

if (options.dateFrom) {
filters.created_at = { gte: options.dateFrom.toISOString().split('T')[0] };
}
if (options.dateTo) {
filters.created_at = {
...filters.created_at,
lte: options.dateTo.toISOString().split('T')[0]
};
}
if (options.action) {
filters.action = options.action;
}

return this.getUsers({
filters,
sort: { columnAccessor: 'created_at', direction: 'desc' }
});
}

async getSystemMetrics(options: {
period?: 'daily' | 'weekly' | 'monthly';
dateFrom?: Date;
dateTo?: Date;
}) {
const filters: Record<string, unknown> = {};

if (options.period) filters.period = options.period;
if (options.dateFrom) {
filters.date_from = options.dateFrom.toISOString().split('T')[0];
}
if (options.dateTo) {
filters.date_to = options.dateTo.toISOString().split('T')[0];
}

const queryParams = this.handler.buildQueryParams(
filters,
{ columnAccessor: 'date', direction: 'desc' },
{ page: 1, perPage: 100 }
);

const response = await fetch(`/api/admin/metrics?${new URLSearchParams(queryParams)}`);
return response.json();
}
}

// Usage in admin dashboard
function AdminDashboard() {
const [users, setUsers] = useState([]);
const [metrics, setMetrics] = useState([]);
const adminService = new AdminService();

const loadUsers = async (filters = {}) => {
const result = await adminService.getUsers({ filters });
setUsers(result.data);
};

const loadUserActivity = async (userId: string) => {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

const result = await adminService.getUserActivity(userId, {
dateFrom: thirtyDaysAgo
});
console.log('User activity:', result.data);
};

const loadMetrics = async (period: 'daily' | 'weekly' | 'monthly') => {
const lastMonth = new Date();
lastMonth.setMonth(lastMonth.getMonth() - 1);

const result = await adminService.getSystemMetrics({
period,
dateFrom: lastMonth
});
setMetrics(result.data);
};

return (
<div>
<h1>Admin Dashboard</h1>

{/* User Management */}
<section>
<h2>User Management</h2>
<UserFilters onFiltersChange={loadUsers} />
<UserTable users={users} />
</section>

{/* System Metrics */}
<section>
<h2>System Metrics</h2>
<MetricControls
onPeriodChange={loadMetrics}
periods={['daily', 'weekly', 'monthly']}
/>
<MetricsChart data={metrics} />
</section>
</div>
);
}

Report Generation with Dynamic Filters

import { createFilterSortHandler } from '@/utils/filter-sort-handler';

// Report-specific handler
const reportHandler = createFilterSortHandler(
{
encodeValues: true,
encodeFilters: true,
formatDateValues: true
},
{
encodeSort: true
}
);

// Report generator
class ReportGenerator {
private handler = reportHandler;

async generateReport(reportType: string, options: {
filters?: Record<string, unknown>;
sort?: DataTableSortStatus | SortCondition | null;
format?: 'json' | 'csv' | 'pdf';
}) {
const { filters = {}, sort, format = 'json' } = options;

const queryParams = this.handler.buildQueryParams(filters, sort, { page: 1, perPage: 10000 });
queryParams.format = format;
queryParams.type = reportType;

const response = await fetch(`/api/reports/generate?${new URLSearchParams(queryParams)}`);

if (format === 'csv' || format === 'pdf') {
// Handle file download
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `report-${reportType}-${Date.now()}.${format}`;
a.click();
window.URL.revokeObjectURL(url);
} else {
return response.json();
}
}

async getSalesReport(options: {
dateFrom: Date;
dateTo: Date;
region?: string;
productCategory?: string;
}) {
const filters: Record<string, unknown> = {
date_from: options.dateFrom.toISOString().split('T')[0],
date_to: options.dateTo.toISOString().split('T')[0]
};

if (options.region) filters.region = options.region;
if (options.productCategory) filters.product_category = options.productCategory;

return this.generateReport('sales', {
filters,
sort: { columnAccessor: 'total_amount', direction: 'desc' }
});
}

async getUserEngagementReport(options: {
dateFrom: Date;
dateTo: Date;
userType?: string;
}) {
const filters: Record<string, unknown> = {
date_from: options.dateFrom.toISOString().split('T')[0],
date_to: options.dateTo.toISOString().split('T')[0]
};

if (options.userType) filters.user_type = options.userType;

return this.generateReport('user_engagement', {
filters,
sort: { columnAccessor: 'last_active', direction: 'desc' }
});
}
}

// Usage in reporting interface
function ReportingInterface() {
const [generating, setGenerating] = useState(false);
const reportGenerator = new ReportGenerator();

const generateSalesReport = async () => {
setGenerating(true);
try {
await reportGenerator.getSalesReport({
dateFrom: new Date('2024-01-01'),
dateTo: new Date('2024-12-31'),
region: 'north-america'
});
} catch (error) {
console.error('Failed to generate report:', error);
} finally {
setGenerating(false);
}
};

return (
<div>
<h1>Report Generation</h1>

<button onClick={generateSalesReport} disabled={generating}>
{generating ? 'Generating...' : 'Generate Sales Report'}
</button>
</div>
);
}

🔧 Advanced Configuration

Custom Filter Processing

import { createFilterSortHandler } from '@/utils/filter-sort-handler';

// Handler with custom filter processing
const customHandler = createFilterSortHandler(
{
encodeValues: true,
encodeFilters: true,
formatDateValues: true
},
{
encodeSort: true
}
);

// Extend handler with custom methods
const extendedHandler = {
...customHandler,

// Custom method for complex date ranges
processDateRange(dateFrom: Date, dateTo: Date): string {
const filters = {
created_at: {
gte: dateFrom.toISOString().split('T')[0],
lte: dateTo.toISOString().split('T')[0]
}
};

return this.filter.process(filters);
},

// Custom method for location-based filters
processLocationFilter(latitude: number, longitude: number, radius: number): string {
const filters = {
location: {
lat: latitude,
lng: longitude,
radius: radius
}
};

return this.filter.process(filters);
},

// Custom method for multi-select filters
processMultiSelect(field: string, values: string[]): string {
const filters = {
[field]: values.length > 0 ? { in: values } : undefined
};

return this.filter.process(filters);
}
};

// Usage
function demonstrateExtendedHandler() {
// Date range filter
const dateFilter = extendedHandler.processDateRange(
new Date('2024-01-01'),
new Date('2024-12-31')
);

// Location filter
const locationFilter = extendedHandler.processLocationFilter(40.7128, -74.0060, 10);

// Multi-select filter
const categoryFilter = extendedHandler.processMultiSelect('category', ['electronics', 'books']);

console.log('Date Filter:', dateFilter);
console.log('Location Filter:', locationFilter);
console.log('Category Filter:', categoryFilter);
}

Configuration Variants

// Different configurations for different environments

// Development configuration (no encoding for debugging)
const devHandler = createFilterSortHandler(
{
encodeValues: false,
encodeFilters: false,
formatDateValues: true
},
{
encodeSort: false
}
);

// Production configuration (full encoding)
const prodHandler = createFilterSortHandler(
{
encodeValues: true,
encodeFilters: true,
formatDateValues: true
},
{
encodeSort: true
}
);

// Testing configuration (minimal processing)
const testHandler = createFilterSortHandler(
{
encodeValues: false,
encodeFilters: false,
formatDateValues: false
},
{
encodeSort: false
}
);

// Environment-based handler selection
function getHandler() {
const env = process.env.NODE_ENV;

switch (env) {
case 'development':
return devHandler;
case 'production':
return prodHandler;
case 'test':
return testHandler;
default:
return devHandler;
}
}

// Usage
const handler = getHandler();
const { filter } = handler.processFiltersAndSort(
{ search: 'test query' },
{ columnAccessor: 'name', direction: 'asc' }
);

🔧 Dependencies

Required Dependencies

npm install mantine-datatable

Internal Dependencies

  • @/utils/filter-handler - Individual filter processing
  • @/utils/sort-handler - Individual sort processing

Type Dependencies

import type { DataTableSortStatus } from 'mantine-datatable';

interface SortCondition {
key: string;
direction: 'asc' | 'desc';
}

interface PaginationOptions {
page: number;
perPage: number;
}

🚨 Error Handling

Error Recovery

import { createFilterSortHandler } from '@/utils/filter-sort-handler';

const safeHandler = createFilterSortHandler(
{
encodeValues: true,
encodeFilters: true,
formatDateValues: true
},
{
encodeSort: true
}
);

// Wrapper with error handling
function safeProcessFiltersAndSort(filters, sort) {
try {
return safeHandler.processFiltersAndSort(filters, sort);
} catch (error) {
console.error('Error processing filters and sort:', error);

// Return fallback values
return {
filter: '',
sort: ''
};
}
}

// Usage in API service
class SafeApiService {
async fetchData(options) {
try {
const { filter, sort } = safeProcessFiltersAndSort(options.filters, options.sort);
const queryParams = { ...options.pagination, filter, sort };

const response = await fetch(`/api/data?${new URLSearchParams(queryParams)}`);
return response.json();
} catch (error) {
console.error('API call failed:', error);
throw error;
}
}
}

Validation

// Handler with validation
const validatedHandler = createFilterSortHandler(
{
encodeValues: true,
encodeFilters: true,
formatDateValues: true
},
{
encodeSort: true
}
);

// Validation wrapper
function validateAndProcess(filters, sort) {
// Validate filters
if (typeof filters !== 'object' || filters === null) {
throw new Error('Filters must be an object');
}

// Validate sort
if (sort && (!sort.columnAccessor || !sort.direction)) {
throw new Error('Sort must have columnAccessor and direction');
}

// Validate filter values
for (const [key, value] of Object.entries(filters)) {
if (value === null || value === undefined) {
delete filters[key];
}
}

return validatedHandler.processFiltersAndSort(filters, sort);
}

// Usage
function safeApiCall(filters, sort) {
try {
const { filter, sort: processedSort } = validateAndProcess(filters, sort);

// Proceed with API call
return apiCall({ filter, sort: processedSort });
} catch (error) {
console.error('Validation failed:', error);
// Handle validation error
return null;
}
}

📚 Best Practices

1. Handler Configuration Management

// Centralized handler configuration
class HandlerManager {
private static instance: HandlerManager;
private handlers: Map<string, any> = new Map();

static getInstance(): HandlerManager {
if (!HandlerManager.instance) {
HandlerManager.instance = new HandlerManager();
}
return HandlerManager.instance;
}

createHandler(name: string, filterOptions: FilterSortOptions, sortOptions: SortOptions) {
const handler = createFilterSortHandler(filterOptions, sortOptions);
this.handlers.set(name, handler);
return handler;
}

getHandler(name: string) {
const handler = this.handlers.get(name);
if (!handler) {
throw new Error(`Handler '${name}' not found`);
}
return handler;
}
}

// Usage
const handlerManager = HandlerManager.getInstance();

// Create handlers
const apiHandler = handlerManager.createHandler('api', {
encodeValues: true,
encodeFilters: true,
formatDateValues: true
}, { encodeSort: true });

const urlHandler = handlerManager.createHandler('url', {
encodeValues: false,
encodeFilters: false,
formatDateValues: true
}, { encodeSort: false });

// Use handlers
const apiResult = handlerManager.getHandler('api').processFiltersAndSort(filters, sort);
const urlResult = handlerManager.getHandler('url').processFiltersAndSort(filters, sort);

2. Performance Optimization

// Memoized handler instance
const memoizedHandler = useMemo(() =>
createFilterSortHandler(
{
encodeValues: true,
encodeFilters: true,
formatDateValues: true
},
{
encodeSort: true
}
),
[]); // Empty dependency array - create once

// Usage in component
function OptimizedComponent({ filters, sort }) {
const { filter, sort: processedSort } = useMemo(() =>
memoizedHandler.processFiltersAndSort(filters, sort),
[filters, sort]
);

return <div>{/* Component content */}</div>;
}

3. Type Safety

// Strictly typed handler configuration
interface StrictFilterOptions extends FilterSortOptions {
encodeValues: boolean;
encodeFilters: boolean;
formatDateValues: boolean;
}

interface StrictSortOptions extends SortOptions {
encodeSort: boolean;
}

// Type-safe handler creation
function createStrictFilterSortHandler(
filterOptions: StrictFilterOptions,
sortOptions: StrictSortOptions
) {
return createFilterSortHandler(filterOptions, sortOptions);
}

// Usage with explicit types
const strictHandler = createStrictFilterSortHandler(
{
encodeValues: true,
encodeFilters: true,
formatDateValues: true
},
{
encodeSort: true
}
);

🔍 Troubleshooting

Common Issues

Problem: Filters not encoding correctly Solution: Check encodeValues and encodeFilters configuration

Problem: Sort not working with DataTable Solution: Ensure sort object has correct columnAccessor and direction properties

Problem: Date values not formatting Solution: Set formatDateValues: true in filter options

Problem: Query parameters not building correctly Solution: Validate filter and sort objects before processing

Debug Mode

import { createFilterSortHandler } from '@/utils/filter-sort-handler';

// Debug handler with logging
const debugHandler = createFilterSortHandler(
{
encodeValues: true,
encodeFilters: true,
formatDateValues: true
},
{
encodeSort: true
}
);

// Debug wrapper
function debugProcessFiltersAndSort(filters, sort) {
console.log('Input filters:', filters);
console.log('Input sort:', sort);

const result = debugHandler.processFiltersAndSort(filters, sort);

console.log('Output filter:', result.filter);
console.log('Output sort:', result.sort);

return result;
}

// Usage
const debugResult = debugProcessFiltersAndSort(
{ search: 'test query', status: 'active' },
{ columnAccessor: 'name', direction: 'asc' }
);