uup
This commit is contained in:
309
app/lib/table-utils.ts
Normal file
309
app/lib/table-utils.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
// 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<T extends Record<string, any>>(
|
||||
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<T extends Record<string, any>>(
|
||||
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<T extends Record<string, any>>(
|
||||
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<T>(
|
||||
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<T extends Record<string, any>>(
|
||||
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<T extends Record<string, any>>(
|
||||
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<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
delay: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => func(...args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
// Export utility types
|
||||
export type TableColumn<T> = {
|
||||
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<T> = {
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
onClick: (item: T) => void;
|
||||
variant?: 'primary' | 'secondary' | 'danger';
|
||||
disabled?: (item: T) => boolean;
|
||||
hidden?: (item: T) => boolean;
|
||||
};
|
||||
Reference in New Issue
Block a user