Added, beneficiaries, site visits and change management

This commit is contained in:
2026-09-27 11:40:28 +02:00
parent fb27cc8c92
commit 36e5d3d7c8
16 changed files with 3132 additions and 212 deletions
@@ -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>
);
};