147 lines
3.6 KiB
TypeScript
147 lines
3.6 KiB
TypeScript
"use client";
|
|
import { useEffect, useState } from "react";
|
|
import {
|
|
useReactTable,
|
|
getCoreRowModel,
|
|
getSortedRowModel,
|
|
ColumnDef,
|
|
SortingState,
|
|
flexRender,
|
|
} from "@tanstack/react-table";
|
|
import { ArrowLeft, ArrowRight, ChevronDown, ChevronUp } from "lucide-react"; // Icons for sorting
|
|
import { PrismaClient } from "@repo/db";
|
|
|
|
export interface SortableTableProps<TData> {
|
|
data: TData[];
|
|
columns: ColumnDef<TData>[];
|
|
prismaModel?: keyof PrismaClient;
|
|
setOrderBy?: (orderBy: Record<string, "asc" | "desc">) => void;
|
|
initialOrderBy?: SortingState;
|
|
}
|
|
|
|
export default function SortableTable<TData>({
|
|
data,
|
|
columns,
|
|
initialOrderBy = [],
|
|
prismaModel,
|
|
setOrderBy,
|
|
}: SortableTableProps<TData>) {
|
|
const [sorting, setSorting] = useState<SortingState>(initialOrderBy);
|
|
|
|
const table = useReactTable({
|
|
data,
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
onSortingChange: setSorting,
|
|
state: { sorting },
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (prismaModel) {
|
|
const orderBy: Record<string, "asc" | "desc"> = {};
|
|
sorting.forEach((sort) => {
|
|
orderBy[sort.id] = sort.desc ? "desc" : "asc";
|
|
});
|
|
setOrderBy?.(orderBy);
|
|
}
|
|
}, [sorting, prismaModel, setOrderBy]);
|
|
|
|
return (
|
|
<div className="overflow-x-auto">
|
|
<table className="table table-zebra w-full">
|
|
<thead>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<tr key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => (
|
|
<th key={header.id} onClick={header.column.getToggleSortingHandler()}>
|
|
<div className="flex items-center gap-1 cursor-pointer">
|
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
{header.column.getIsSorted() === "asc" && <ChevronUp size={16} />}
|
|
{header.column.getIsSorted() === "desc" && <ChevronDown size={16} />}
|
|
</div>
|
|
</th>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</thead>
|
|
<tbody>
|
|
{table.getRowModel().rows.map((row) => (
|
|
<tr key={row.id}>
|
|
{row.getVisibleCells().map((cell) => (
|
|
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
{table.getRowModel().rows.length === 0 && (
|
|
<tr>
|
|
<td colSpan={columns.length} className="text-center font-bold text-sm text-gray-500">
|
|
Keine Daten gefunden
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export const RowsPerPage = ({
|
|
rowsPerPage,
|
|
setRowsPerPage,
|
|
}: {
|
|
rowsPerPage: number;
|
|
setRowsPerPage: (rowsPerPage: number) => void;
|
|
}) => {
|
|
return (
|
|
<select
|
|
className="select w-32"
|
|
value={rowsPerPage}
|
|
onChange={(e) => setRowsPerPage(Number(e.target.value))}
|
|
>
|
|
<option value={10}>10</option>
|
|
<option value={30}>30</option>
|
|
<option value={50}>50</option>
|
|
<option value={100}>100</option>
|
|
<option value={300}>300</option>
|
|
</select>
|
|
);
|
|
};
|
|
|
|
export const Pagination = ({
|
|
page,
|
|
totalPages,
|
|
setPage,
|
|
}: {
|
|
page: number;
|
|
totalPages: number;
|
|
setPage: (page: number) => void;
|
|
}) => {
|
|
if (totalPages === 0) return null;
|
|
return (
|
|
<div className="join w-full justify-end">
|
|
<button className="join-item btn" disabled={page === 0} onClick={() => setPage(page - 1)}>
|
|
<ArrowLeft size={16} />
|
|
</button>
|
|
<select
|
|
className="select join-item w-16"
|
|
value={page}
|
|
onChange={(e) => setPage(Number(e.target.value))}
|
|
>
|
|
{Array.from({ length: totalPages }).map((_, i) => (
|
|
<option key={i} value={i}>
|
|
{i + 1}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<button
|
|
className="join-item btn"
|
|
disabled={page === totalPages - 1}
|
|
onClick={() => page < totalPages && setPage(page + 1)}
|
|
>
|
|
<ArrowRight size={16} />
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|