import { Fragment, useMemo, useState } from 'react'; import { MoreVertical, GripVertical, ChevronRight, ChevronDown } from 'lucide-react'; import { useUi } from '../context/ui-context.jsx'; import { useDragReorder } from '../hooks/useDragReorder.js'; import { Flyout, FlyoutItem } from './Flyout.jsx'; // ListView/DataGrid: columnas ordenables, buscador ligero sobre el haystack completo, clic en // fila abre detalle, boton "..." abre el menu de acciones (equivalente al clic derecho). // `onReorder(idsEnNuevoOrden)` es opcional: si se pasa, habilita arrastrar filas con el mouse para // definir un orden manual (columna con icono de agarre a la izquierda) — solo mientras no haya un // ordenamiento por columna activo, para no mezclar los dos mecanismos de orden a la vez. // `getChildren(fila)` es opcional: si devuelve filas, la fila se pliega/despliega con su boton o con // un clic y las hijas se muestran debajo. Al buscar, solo quedan las hijas que coinciden y se abren. export function ListView({ columns, rows, searchValue, getSearchHaystack, onRowClick, rowActions, getRowKey, initialSort, onReorder, getChildren }) { const { t } = useUi(); const [sort, setSort] = useState(initialSort || { key: null, dir: 1 }); const [expanded, setExpanded] = useState(() => new Set()); const haystack = (r) => (getSearchHaystack ? getSearchHaystack(r) : JSON.stringify(r)).toLowerCase(); const toggleRow = (key) => setExpanded((prev) => { const next = new Set(prev); if (!next.delete(key)) next.add(key); return next; }); const filtered = useMemo(() => { if (!searchValue) return rows; const q = searchValue.toLowerCase(); return rows.filter((r) => haystack(r).includes(q) || getChildren?.(r)?.some((c) => haystack(c).includes(q))); // eslint-disable-next-line react-hooks/exhaustive-deps }, [rows, searchValue, getSearchHaystack, getChildren]); const sorted = useMemo(() => { if (!sort.key) return filtered; const col = columns.find((c) => c.key === sort.key); return [...filtered].sort((a, b) => { const va = col.sortValue ? col.sortValue(a) : a[sort.key]; const vb = col.sortValue ? col.sortValue(b) : b[sort.key]; if (va === vb) return 0; return (va > vb ? 1 : -1) * sort.dir; }); }, [filtered, sort, columns]); const dragEnabled = !!onReorder && !sort.key && !searchValue; const { order, getHandleProps, getRowProps, isDragging } = useDragReorder(sorted, (ids) => onReorder?.(ids), getRowKey); const displayRows = dragEnabled ? order : sorted; function toggleSort(key) { setSort((prev) => (prev.key === key ? { key, dir: prev.dir * -1 } : { key, dir: 1 })); } function renderRow(row, { isParent, open, onToggle, child } = {}) { const actions = rowActions?.(row) || []; const activate = isParent ? onToggle : onRowClick ? () => onRowClick(row) : null; return ( activate?.()} onContextMenu={actions.length ? (event) => { event.preventDefault(); event.currentTarget.querySelector('.row-menu-btn')?.click(); } : undefined} tabIndex={activate ? 0 : undefined} onKeyDown={(event) => { if (activate && event.target === event.currentTarget && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); activate(); } }} className={dragEnabled && isDragging(getRowKey(row)) ? 'dragging' : ''} {...(dragEnabled ? getRowProps(row) : {})} > {dragEnabled && ( e.stopPropagation()} {...getHandleProps(row)}> )} {columns.map((col, index) => ( {index === 0 && isParent ? ( {col.render ? col.render(row) : row[col.key]} ) : (col.render ? col.render(row) : row[col.key])} ))} {rowActions && ( e.stopPropagation()}> {actions.length > 0 && ( ( )}> {actions.map((action, i) => ( {action.label} ))} )} )} ); } return (
{dragEnabled && ))} {rowActions && {displayRows.map((row) => { const key = getRowKey(row); const allChildren = getChildren?.(row); const isParent = !!allChildren?.length; const q = searchValue?.toLowerCase(); const children = isParent && q ? allChildren.filter((c) => haystack(c).includes(q)) : allChildren; const open = isParent && (expanded.has(key) || (!!q && children.length > 0)); return ( {renderRow(row, { isParent, open, onToggle: () => toggleRow(key) })} {open && children.map((child) => renderRow(child, { child: true }))} ); })}
} {columns.map((col) => ( {col.sortable === false ? col.label : } }
{displayRows.length === 0 &&
{t('common_empty')}
}
); }