309 lines
8.3 KiB
TypeScript
309 lines
8.3 KiB
TypeScript
// 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;
|
||
}; |