car_mms/app/lib/table-utils.ts
2025-09-11 14:22:27 +03:00

309 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
};