171 lines
4.4 KiB
TypeScript
171 lines
4.4 KiB
TypeScript
"use client";
|
|
import { useState, Ref, useImperativeHandle, useEffect, useCallback } from "react";
|
|
import SortableTable, { Pagination, RowsPerPage, SortableTableProps } from "./Table";
|
|
import { PrismaClient } from "@repo/db";
|
|
import { getData } from "./pagiantedTableActions";
|
|
import { cn, useDebounce } from "@repo/shared-components";
|
|
|
|
export interface PaginatedTableRef {
|
|
refresh: () => void;
|
|
}
|
|
|
|
interface PaginatedTableProps<TData, TWhere extends object>
|
|
extends Omit<SortableTableProps<TData>, "data"> {
|
|
prismaModel: keyof PrismaClient;
|
|
stickyHeaders?: boolean;
|
|
initialRowsPerPage?: number;
|
|
showSearch?: boolean;
|
|
getFilter?: (searchTerm: string) => TWhere;
|
|
include?: Record<string, boolean>;
|
|
strictQuery?: boolean;
|
|
leftOfSearch?: React.ReactNode;
|
|
rightOfSearch?: React.ReactNode;
|
|
leftOfPagination?: React.ReactNode;
|
|
rightOfPagination?: React.ReactNode;
|
|
supressQuery?: boolean;
|
|
ref?: Ref<PaginatedTableRef>;
|
|
}
|
|
|
|
export function PaginatedTable<TData, TWhere extends object>({
|
|
prismaModel,
|
|
initialRowsPerPage = 30,
|
|
getFilter,
|
|
showSearch = false,
|
|
include,
|
|
ref,
|
|
strictQuery = false,
|
|
stickyHeaders = false,
|
|
leftOfSearch,
|
|
rightOfSearch,
|
|
leftOfPagination,
|
|
rightOfPagination,
|
|
supressQuery,
|
|
...restProps
|
|
}: PaginatedTableProps<TData, TWhere>) {
|
|
const [data, setData] = useState<TData[]>([]);
|
|
const [rowsPerPage, setRowsPerPage] = useState(initialRowsPerPage);
|
|
const [page, setPage] = useState(0);
|
|
const [total, setTotal] = useState(0);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [orderBy, setOrderBy] = useState<Record<string, "asc" | "desc">>(
|
|
restProps.initialOrderBy
|
|
? restProps.initialOrderBy.reduce(
|
|
(acc, sort) => {
|
|
acc[sort.id] = sort.desc ? "desc" : "asc";
|
|
return acc;
|
|
},
|
|
{} as Record<string, "asc" | "desc">,
|
|
)
|
|
: {},
|
|
);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const refreshTableData = useCallback(async () => {
|
|
if (supressQuery) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
getData({
|
|
model: prismaModel,
|
|
limit: rowsPerPage,
|
|
offset: page * rowsPerPage,
|
|
where: getFilter ? getFilter(searchTerm) : undefined,
|
|
include,
|
|
orderBy,
|
|
select: strictQuery
|
|
? restProps.columns
|
|
.filter(
|
|
(col): col is { accessorKey: string } =>
|
|
typeof (col as { accessorKey?: unknown }).accessorKey === "string",
|
|
)
|
|
.map((col) => col.accessorKey)
|
|
.reduce<Record<string, boolean>>((acc, key) => {
|
|
acc[key] = true;
|
|
return acc;
|
|
}, {})
|
|
: undefined,
|
|
})
|
|
.then((result) => {
|
|
if (result) {
|
|
setData(result.data);
|
|
setTotal(result.total);
|
|
}
|
|
})
|
|
.finally(() => {
|
|
setLoading(false);
|
|
});
|
|
}, [
|
|
supressQuery,
|
|
prismaModel,
|
|
rowsPerPage,
|
|
page,
|
|
searchTerm,
|
|
getFilter,
|
|
include,
|
|
orderBy,
|
|
strictQuery,
|
|
restProps.columns,
|
|
]);
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
refresh: () => {
|
|
refreshTableData();
|
|
},
|
|
}));
|
|
|
|
// useEffect to show loading spinner
|
|
useEffect(() => {
|
|
if (supressQuery) return;
|
|
|
|
setLoading(true);
|
|
}, [searchTerm, page, rowsPerPage, orderBy, getFilter, setLoading, supressQuery]);
|
|
|
|
useDebounce(
|
|
() => {
|
|
refreshTableData();
|
|
},
|
|
500,
|
|
[searchTerm, page, rowsPerPage, orderBy, getFilter],
|
|
);
|
|
|
|
return (
|
|
<div className="m-4 space-y-4">
|
|
{(rightOfSearch || leftOfSearch || showSearch) && (
|
|
<div
|
|
className={cn(
|
|
"sticky z-20 flex items-center gap-2 py-2",
|
|
stickyHeaders && "bg-base-100/80 sticky top-0 border-b backdrop-blur",
|
|
)}
|
|
>
|
|
<div className="flex flex-1 gap-2">
|
|
<div>{leftOfSearch}</div>
|
|
<div>{loading && <span className="loading loading-dots loading-md" />}</div>
|
|
</div>
|
|
{showSearch && (
|
|
<input
|
|
type="text"
|
|
placeholder="Suchen..."
|
|
value={searchTerm}
|
|
onChange={(e) => {
|
|
setSearchTerm(e.target.value);
|
|
setPage(0); // Reset to first page on search
|
|
}}
|
|
className="input input-bordered w-full max-w-xs justify-end"
|
|
/>
|
|
)}
|
|
<div className="flex justify-center">{rightOfSearch}</div>
|
|
</div>
|
|
)}
|
|
|
|
<SortableTable data={data} prismaModel={prismaModel} setOrderBy={setOrderBy} {...restProps} />
|
|
<div className="items-between flex">
|
|
{leftOfPagination}
|
|
<RowsPerPage rowsPerPage={rowsPerPage} setRowsPerPage={setRowsPerPage} />
|
|
{rightOfPagination}
|
|
<Pagination totalPages={Math.ceil(total / rowsPerPage)} page={page} setPage={setPage} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|