This commit is contained in:
2026-01-23 20:35:40 +03:00
parent cf3b0e48ec
commit 66c151653e
137 changed files with 41495 additions and 0 deletions

309
app/lib/table-utils.ts Normal file
View 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;
};