import { useState } from 'react' import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react' import { cn } from '@/shared/lib/cn' type SortState = { key: string; desc: boolean } /** * Состояние сортировки таблицы. Клик по новому столбцу — по возрастанию; повторный клик по тому же — * переключает направление. Для серверных списков `sort`/`desc` передаются в API (и в queryKey), для * клиентских — в {@link sortRows}. */ export function useTableSort(defaultKey: string, defaultDesc = false) { const [sort, setSort] = useState({ key: defaultKey, desc: defaultDesc }) const toggle = (key: string) => setSort((s) => (s.key === key ? { key, desc: !s.desc } : { key, desc: false })) return { sort, toggle } } /** Заголовок-кнопка столбца со стрелкой сортировки. */ export function SortHeader({ label, sortKey, sort, onToggle, className, }: { label: string sortKey: string sort: SortState onToggle: (key: string) => void className?: string }) { const active = sort.key === sortKey const Icon = !active ? ChevronsUpDown : sort.desc ? ArrowDown : ArrowUp return ( // aria-sort — атрибут заголовка столбца, а не кнопки внутри него: у роли button его нет, // и скринридер там его просто не прочтёт. ) } type Comparable = string | number | boolean | null | undefined /** Клиентская сортировка строк по выбранному ключу (для непагинированных списков). nulls — в конец. */ export function sortRows( rows: T[], sort: SortState, accessors: Record Comparable>, ): T[] { const accessor = accessors[sort.key] if (!accessor) return rows const dir = sort.desc ? -1 : 1 return [...rows].sort((a, b) => { const av = accessor(a) const bv = accessor(b) if (av == null && bv == null) return 0 if (av == null) return 1 if (bv == null) return -1 if (typeof av === 'string' && typeof bv === 'string') return av.localeCompare(bv) * dir return (av < bv ? -1 : av > bv ? 1 : 0) * dir }) }