Added, beneficiaries, site visits and change management
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
|
||||
|
||||
export type SortDirection = 'asc' | 'desc' | null;
|
||||
|
||||
export interface SortConfig<T> {
|
||||
column: string | null;
|
||||
direction: SortDirection;
|
||||
customExtractors?: Record<string, (item: T) => any>;
|
||||
}
|
||||
|
||||
export function useTableSort<T>(
|
||||
data: T[],
|
||||
initialColumn: string | null = null,
|
||||
initialDirection: SortDirection = null,
|
||||
customExtractors?: Record<string, (item: T) => any>
|
||||
) {
|
||||
const [sortColumn, setSortColumn] = useState<string | null>(initialColumn);
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>(initialDirection);
|
||||
|
||||
const handleSort = useCallback((column: string) => {
|
||||
if (sortColumn !== column) {
|
||||
setSortColumn(column);
|
||||
setSortDirection('asc');
|
||||
} else {
|
||||
if (sortDirection === 'asc') {
|
||||
setSortDirection('desc');
|
||||
} else if (sortDirection === 'desc') {
|
||||
setSortDirection(null);
|
||||
setSortColumn(null);
|
||||
} else {
|
||||
setSortDirection('asc');
|
||||
}
|
||||
}
|
||||
}, [sortColumn, sortDirection]);
|
||||
|
||||
const sortedData = useMemo(() => {
|
||||
if (!sortColumn || !sortDirection || !Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const items = [...data];
|
||||
return items.sort((a: any, b: any) => {
|
||||
let valA = customExtractors && customExtractors[sortColumn]
|
||||
? customExtractors[sortColumn](a)
|
||||
: a?.[sortColumn];
|
||||
|
||||
let valB = customExtractors && customExtractors[sortColumn]
|
||||
? customExtractors[sortColumn](b)
|
||||
: b?.[sortColumn];
|
||||
|
||||
if (valA === undefined || valA === null) valA = '';
|
||||
if (valB === undefined || valB === null) valB = '';
|
||||
|
||||
// Number comparison
|
||||
if (typeof valA === 'number' && typeof valB === 'number') {
|
||||
return sortDirection === 'asc' ? valA - valB : valB - valA;
|
||||
}
|
||||
|
||||
// Numeric string comparison
|
||||
const numA = Number(valA);
|
||||
const numB = Number(valB);
|
||||
if (!isNaN(numA) && !isNaN(numB) && typeof valA !== 'boolean' && typeof valB !== 'boolean' && String(valA).trim() !== '' && String(valB).trim() !== '') {
|
||||
return sortDirection === 'asc' ? numA - numB : numB - numA;
|
||||
}
|
||||
|
||||
// Date comparison if valid ISO / date string
|
||||
const dateA = Date.parse(String(valA));
|
||||
const dateB = Date.parse(String(valB));
|
||||
if (!isNaN(dateA) && !isNaN(dateB) && (String(valA).includes('-') || String(valA).includes('/'))) {
|
||||
return sortDirection === 'asc' ? dateA - dateB : dateB - dateA;
|
||||
}
|
||||
|
||||
// String comparison
|
||||
const strA = String(valA).toLowerCase();
|
||||
const strB = String(valB).toLowerCase();
|
||||
if (strA < strB) return sortDirection === 'asc' ? -1 : 1;
|
||||
if (strA > strB) return sortDirection === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}, [data, sortColumn, sortDirection, customExtractors]);
|
||||
|
||||
return {
|
||||
sortedData,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
handleSort,
|
||||
};
|
||||
}
|
||||
|
||||
export interface SortableThProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
|
||||
column: string;
|
||||
currentColumn: string | null;
|
||||
direction: SortDirection;
|
||||
onSort: (column: string) => void;
|
||||
children: React.ReactNode;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
export const SortableTh: React.FC<SortableThProps> = ({
|
||||
column,
|
||||
currentColumn,
|
||||
direction,
|
||||
onSort,
|
||||
children,
|
||||
align = 'left',
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const isCurrent = currentColumn === column && direction !== null;
|
||||
|
||||
return (
|
||||
<th
|
||||
{...props}
|
||||
onClick={() => onSort(column)}
|
||||
className={`cursor-pointer select-none group transition-colors hover:text-white ${className}`}
|
||||
title={`Click to sort (Ascending, Descending, Clear)`}
|
||||
>
|
||||
<div
|
||||
className={`inline-flex items-center gap-1 ${
|
||||
align === 'right' ? 'justify-end w-full' : align === 'center' ? 'justify-center w-full' : 'justify-start'
|
||||
}`}
|
||||
>
|
||||
<span>{children}</span>
|
||||
{isCurrent && direction === 'asc' && (
|
||||
<ArrowUp className="w-3 h-3 text-blue-400 shrink-0" />
|
||||
)}
|
||||
{isCurrent && direction === 'desc' && (
|
||||
<ArrowDown className="w-3 h-3 text-blue-400 shrink-0" />
|
||||
)}
|
||||
{!isCurrent && (
|
||||
<ArrowUpDown className="w-3 h-3 text-slate-600 group-hover:text-slate-400 opacity-40 group-hover:opacity-100 shrink-0 transition" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user