chore: estructura inicial del MVP
Backend (Express + MariaDB) y frontend (React + Vite, identidad Fluent/Windows) con el nucleo funcional: seguridad, organizacion, proyectos, actividades, subtareas, dependencias, dashboard, calendario, gantt, comentarios, adjuntos, notificaciones in-app y catalogos. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { MoreVertical } from 'lucide-react';
|
||||
import { useUi } from '../context/ui-context.jsx';
|
||||
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).
|
||||
export function ListView({ columns, rows, searchValue, getSearchHaystack, onRowClick, rowActions, getRowKey }) {
|
||||
const { t } = useUi();
|
||||
const [sort, setSort] = useState({ key: null, dir: 1 });
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!searchValue) return rows;
|
||||
const q = searchValue.toLowerCase();
|
||||
return rows.filter((r) => (getSearchHaystack ? getSearchHaystack(r) : JSON.stringify(r)).toLowerCase().includes(q));
|
||||
}, [rows, searchValue, getSearchHaystack]);
|
||||
|
||||
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]);
|
||||
|
||||
function toggleSort(key) {
|
||||
setSort((prev) => (prev.key === key ? { key, dir: prev.dir * -1 } : { key, dir: 1 }));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="list-card">
|
||||
<table className="list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((col) => (
|
||||
<th key={col.key} onClick={() => col.sortable !== false && toggleSort(col.key)}>
|
||||
{col.label}{sort.key === col.key ? (sort.dir === 1 ? ' ▲' : ' ▼') : ''}
|
||||
</th>
|
||||
))}
|
||||
{rowActions && <th style={{ width: 40 }} />}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sorted.map((row) => (
|
||||
<tr key={getRowKey(row)} onClick={() => onRowClick?.(row)}>
|
||||
{columns.map((col) => (
|
||||
<td key={col.key}>{col.render ? col.render(row) : row[col.key]}</td>
|
||||
))}
|
||||
{rowActions && (
|
||||
<td onClick={(e) => e.stopPropagation()}>
|
||||
<Flyout trigger={({ onClick }) => (
|
||||
<button className="icon-btn row-menu-btn" onClick={onClick}><MoreVertical /></button>
|
||||
)}>
|
||||
{rowActions(row).map((action, i) => (
|
||||
<FlyoutItem key={i} icon={action.icon} danger={action.danger} onClick={action.onClick}>
|
||||
{action.label}
|
||||
</FlyoutItem>
|
||||
))}
|
||||
</Flyout>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{sorted.length === 0 && <div className="list-empty">{t('common_empty')}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user