Files
formularios_front/src/components/table.tsx
T
2025-04-04 16:43:18 -06:00

198 lines
5.4 KiB
TypeScript

// @components/table.tsx
import React, { useState, useMemo } from "react";
import Pagination from "./pagination";
export interface Header<T> {
key: keyof T;
label: string;
render?: (value: T[keyof T], row: T) => React.ReactNode;
width?: string;
className?: string;
}
interface TableProps<T> {
headers: Header<T>[];
data: T[];
rowsPerPage?: number;
onRowClick?: (rowData: T) => void;
rowKey?: (row: T) => string | number;
className?: {
table?: string;
row?: string;
cell?: string;
header?: string;
};
controlledPage?: number;
onPageChange?: (page: number) => void;
}
export default function Table<T>({
headers,
data,
rowsPerPage = 10,
onRowClick,
rowKey,
className,
controlledPage,
onPageChange,
}: TableProps<T>) {
const [sortConfig, setSortConfig] = useState<{
key: keyof T;
direction: "asc" | "desc";
} | null>(null);
const [internalPage, setInternalPage] = useState(1);
const currentPage = controlledPage ?? internalPage;
const setPage = onPageChange ?? setInternalPage;
const handleSort = (key: keyof T) => {
let direction: "asc" | "desc" = "asc";
if (
sortConfig &&
sortConfig.key === key &&
sortConfig.direction === "asc"
) {
direction = "desc";
}
setSortConfig({ key, direction });
};
const sortedData = useMemo(() => {
if (!sortConfig) return data;
return [...data].sort((a, b) => {
if (a[sortConfig.key]! < b[sortConfig.key]!)
return sortConfig.direction === "asc" ? -1 : 1;
if (a[sortConfig.key]! > b[sortConfig.key]!)
return sortConfig.direction === "asc" ? 1 : -1;
return 0;
});
}, [data, sortConfig]);
const totalPages = Math.ceil(sortedData.length / rowsPerPage);
const paginatedData = sortedData.slice(
(currentPage - 1) * rowsPerPage,
currentPage * rowsPerPage
);
return (
<div className="table-container">
<table className={`e-table ${className?.table || ""}`}>
<colgroup>
{headers.map((header, index) => (
<col key={index} style={{ width: header.width }} />
))}
</colgroup>
<thead>
<tr>
{headers.map((header, index) => (
<th
key={index}
onClick={() => handleSort(header.key)}
tabIndex={0}
aria-sort={
sortConfig?.key === header.key
? sortConfig.direction === "asc"
? "ascending"
: "descending"
: "none"
}
style={{ width: header.width }}
className={`sortable ${className?.header || ""} ${
header.className || ""
} ${
sortConfig?.key === header.key
? `sorted-${sortConfig.direction}`
: ""
}`}
>
{header.label}
{sortConfig?.key === header.key && (
<i
className={`ms-1 bi bi-caret-${
sortConfig.direction === "asc" ? "up" : "down"
}`}
></i>
)}
</th>
))}
</tr>
</thead>
<tbody>
{paginatedData.length === 0 ? (
<tr>
<td colSpan={headers.length} className="text-center text-muted">
No hay datos disponibles
</td>
</tr>
) : (
paginatedData.map((row, rowIndex) => (
<tr
key={rowKey ? rowKey(row) : rowIndex}
onClick={() => onRowClick?.(row)}
className={`${onRowClick ? "clickable-row" : ""} ${
className?.row || ""
}`}
>
{headers.map((header, cellIndex) => (
<td
key={cellIndex}
className={`${className?.cell || ""} ${
header.className || ""
}`}
style={{ width: header.width }}
data-titulo={header.label}
>
{header.render
? header.render(row[header.key], row)
: (row[header.key] as React.ReactNode)}
</td>
))}
</tr>
))
)}
</tbody>
</table>
{totalPages > 1 && (
<Pagination
currentPage={currentPage}
onPageChange={setPage}
totalPages={totalPages}
/>
)}
</div>
);
}
interface TablaSkeletonProps {
rows?: number;
columns?: number;
}
export function TableSkeleton({ rows = 5, columns = 5 }: TablaSkeletonProps) {
return (
<table className="e-table placeholder-glow">
<thead>
<tr>
{Array.from({ length: columns }).map((_, index) => (
<th key={index}>
<span className="placeholder rounded col-12"></span>
</th>
))}
</tr>
</thead>
<tbody>
{Array.from({ length: rows }).map((_, rowIndex) => (
<tr key={rowIndex}>
{Array.from({ length: columns }).map((_, colIndex) => (
<td key={colIndex}>
<span className="placeholder rounded col-12"></span>
</td>
))}
</tr>
))}
</tbody>
</table>
);
}