// Table utilities for searching, filtering, sorting, and pagination export interface SortConfig { key: string; direction: 'asc' | 'desc'; } export interface FilterConfig { [key: string]: string | string[] | number | boolean; } export interface PaginationConfig { page: number; pageSize: number; } export interface TableState { search: string; filters: FilterConfig; sort: SortConfig | null; pagination: PaginationConfig; } // Search function with Arabic text support export function searchData>( data: T[], searchTerm: string, searchableFields: (keyof T)[] ): T[] { if (!searchTerm.trim()) return data; const normalizedSearch = normalizeArabicText(searchTerm.toLowerCase()); return data.filter(item => { return searchableFields.some(field => { const value = item[field]; if (value == null) return false; const normalizedValue = normalizeArabicText(String(value).toLowerCase()); return normalizedValue.includes(normalizedSearch); }); }); } // Filter data based on multiple criteria export function filterData>( data: T[], filters: FilterConfig ): T[] { return data.filter(item => { return Object.entries(filters).every(([key, filterValue]) => { if (!filterValue || filterValue === '' || (Array.isArray(filterValue) && filterValue.length === 0)) { return true; } const itemValue = item[key]; if (Array.isArray(filterValue)) { // Multi-select filter return filterValue.includes(String(itemValue)); } if (typeof filterValue === 'string') { // Text filter if (itemValue == null) return false; const normalizedItemValue = normalizeArabicText(String(itemValue).toLowerCase()); const normalizedFilterValue = normalizeArabicText(filterValue.toLowerCase()); return normalizedItemValue.includes(normalizedFilterValue); } if (typeof filterValue === 'number') { // Numeric filter return Number(itemValue) === filterValue; } if (typeof filterValue === 'boolean') { // Boolean filter return Boolean(itemValue) === filterValue; } return true; }); }); } // Sort data export function sortData>( data: T[], sortConfig: SortConfig | null ): T[] { if (!sortConfig) return data; return [...data].sort((a, b) => { const aValue = a[sortConfig.key]; const bValue = b[sortConfig.key]; // Handle null/undefined values if (aValue == null && bValue == null) return 0; if (aValue == null) return sortConfig.direction === 'asc' ? 1 : -1; if (bValue == null) return sortConfig.direction === 'asc' ? -1 : 1; // Handle different data types let comparison = 0; if (typeof aValue === 'string' && typeof bValue === 'string') { // String comparison with Arabic support comparison = normalizeArabicText(aValue).localeCompare(normalizeArabicText(bValue), 'ar'); } else if (typeof aValue === 'number' && typeof bValue === 'number') { // Numeric comparison comparison = aValue - bValue; } else if (aValue instanceof Date && bValue instanceof Date) { // Date comparison comparison = aValue.getTime() - bValue.getTime(); } else { // Fallback to string comparison comparison = String(aValue).localeCompare(String(bValue), 'ar'); } return sortConfig.direction === 'asc' ? comparison : -comparison; }); } // Paginate data export function paginateData( data: T[], pagination: PaginationConfig ): { data: T[]; totalItems: number; totalPages: number; currentPage: number; hasNextPage: boolean; hasPreviousPage: boolean; } { const { page, pageSize } = pagination; const totalItems = data.length; const totalPages = Math.ceil(totalItems / pageSize); const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedData = data.slice(startIndex, endIndex); return { data: paginatedData, totalItems, totalPages, currentPage: page, hasNextPage: page < totalPages, hasPreviousPage: page > 1, }; } // Process table data with all operations export function processTableData>( data: T[], state: TableState, searchableFields: (keyof T)[] ) { let processedData = [...data]; // Apply search if (state.search) { processedData = searchData(processedData, state.search, searchableFields); } // Apply filters if (Object.keys(state.filters).length > 0) { processedData = filterData(processedData, state.filters); } // Apply sorting if (state.sort) { processedData = sortData(processedData, state.sort); } // Apply pagination const paginationResult = paginateData(processedData, state.pagination); return { ...paginationResult, filteredCount: processedData.length, originalCount: data.length, }; } // Normalize Arabic text for better searching function normalizeArabicText(text: string): string { return text // Normalize Arabic characters .replace(/[أإآ]/g, 'ا') .replace(/[ة]/g, 'ه') .replace(/[ي]/g, 'ى') // Remove diacritics .replace(/[\u064B-\u065F\u0670\u06D6-\u06ED]/g, '') // Normalize whitespace .replace(/\s+/g, ' ') .trim(); } // Generate filter options from data export function generateFilterOptions>( data: T[], field: keyof T, labelFormatter?: (value: any) => string ): { value: string; label: string }[] { const uniqueValues = Array.from(new Set( data .map(item => item[field]) .filter(value => value != null && value !== '') )); return uniqueValues .sort((a, b) => { if (typeof a === 'string' && typeof b === 'string') { return normalizeArabicText(a).localeCompare(normalizeArabicText(b), 'ar'); } return String(a).localeCompare(String(b)); }) .map(value => ({ value: String(value), label: labelFormatter ? labelFormatter(value) : String(value), })); } // Create table state from URL search params export function createTableStateFromParams( searchParams: URLSearchParams, defaultPageSize = 10 ): TableState { return { search: searchParams.get('search') || '', filters: Object.fromEntries( Array.from(searchParams.entries()) .filter(([key]) => key.startsWith('filter_')) .map(([key, value]) => [key.replace('filter_', ''), value]) ), sort: searchParams.get('sort') && searchParams.get('sortDir') ? { key: searchParams.get('sort')!, direction: searchParams.get('sortDir') as 'asc' | 'desc', } : null, pagination: { page: parseInt(searchParams.get('page') || '1'), pageSize: parseInt(searchParams.get('pageSize') || String(defaultPageSize)), }, }; } // Convert table state to URL search params export function tableStateToParams(state: TableState): URLSearchParams { const params = new URLSearchParams(); if (state.search) { params.set('search', state.search); } Object.entries(state.filters).forEach(([key, value]) => { if (value && value !== '') { params.set(`filter_${key}`, String(value)); } }); if (state.sort) { params.set('sort', state.sort.key); params.set('sortDir', state.sort.direction); } if (state.pagination.page > 1) { params.set('page', String(state.pagination.page)); } if (state.pagination.pageSize !== 10) { params.set('pageSize', String(state.pagination.pageSize)); } return params; } // Debounce function for search input export function debounce any>( func: T, delay: number ): (...args: Parameters) => void { let timeoutId: NodeJS.Timeout; return (...args: Parameters) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func(...args), delay); }; } // Export utility types export type TableColumn = { key: keyof T; header: string; sortable?: boolean; filterable?: boolean; searchable?: boolean; render?: (value: any, item: T) => React.ReactNode; width?: string; align?: 'left' | 'center' | 'right'; }; export type TableAction = { label: string; icon?: React.ReactNode; onClick: (item: T) => void; variant?: 'primary' | 'secondary' | 'danger'; disabled?: (item: T) => boolean; hidden?: (item: T) => boolean; };