276 lines
12 KiB
TypeScript
276 lines
12 KiB
TypeScript
import {
|
|
ColumnDef,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
useReactTable,
|
|
getPaginationRowModel,
|
|
SortingState,
|
|
getSortedRowModel,
|
|
ColumnFiltersState,
|
|
getFilteredRowModel,
|
|
VisibilityState,
|
|
} from "@tanstack/react-table";
|
|
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { Button } from "./ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuCheckboxItem,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuSub,
|
|
DropdownMenuSubContent,
|
|
DropdownMenuSubTrigger,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import React from "react";
|
|
import { Input } from "./ui/input";
|
|
import { Settings2, ChevronDown, ListFilter, Trash2 } from "lucide-react";
|
|
|
|
interface DataTableFilterOption {
|
|
columnId: string;
|
|
title: string;
|
|
options: { label: string; value: string }[];
|
|
}
|
|
|
|
export interface DataTableBulkAction<TData> {
|
|
label: string;
|
|
onClick: (rows: TData[]) => void;
|
|
icon?: React.ElementType;
|
|
variant?: "default" | "destructive";
|
|
}
|
|
|
|
interface DataTableProps<TData, TValue> {
|
|
columns: ColumnDef<TData, TValue>[];
|
|
data: TData[];
|
|
searchKey?: string;
|
|
filters?: DataTableFilterOption[];
|
|
bulkActions?: DataTableBulkAction<TData>[];
|
|
}
|
|
|
|
export function DataTable<TData, TValue>({
|
|
columns,
|
|
data,
|
|
filters,
|
|
bulkActions,
|
|
}: DataTableProps<TData, TValue>) {
|
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
|
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
|
const [rowSelection, setRowSelection] = React.useState({});
|
|
const [globalFilter, setGlobalFilter] = React.useState("");
|
|
|
|
const table = useReactTable({
|
|
data,
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
onSortingChange: setSorting,
|
|
getSortedRowModel: getSortedRowModel(),
|
|
onColumnFiltersChange: setColumnFilters,
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
onColumnVisibilityChange: setColumnVisibility,
|
|
onRowSelectionChange: setRowSelection,
|
|
onGlobalFilterChange: setGlobalFilter,
|
|
state: {
|
|
sorting,
|
|
columnFilters,
|
|
columnVisibility,
|
|
rowSelection,
|
|
globalFilter,
|
|
},
|
|
});
|
|
|
|
const selectedRows = table.getFilteredSelectedRowModel().rows;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="flex flex-1 items-center gap-2">
|
|
<Input
|
|
placeholder="Cari..."
|
|
value={globalFilter ?? ""}
|
|
onChange={(event) => setGlobalFilter(event.target.value)}
|
|
className="max-w-sm"
|
|
/>
|
|
|
|
{filters && filters.length > 0 && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" className="gap-2">
|
|
<ListFilter className="h-4 w-4" />
|
|
Filter
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-48">
|
|
<DropdownMenuLabel>Filter Berdasarkan</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
{filters.map((filter) => {
|
|
const column = table.getColumn(filter.columnId);
|
|
if (!column) return null;
|
|
|
|
return (
|
|
<DropdownMenuSub key={filter.columnId}>
|
|
<DropdownMenuSubTrigger>
|
|
<span>{filter.title}</span>
|
|
</DropdownMenuSubTrigger>
|
|
<DropdownMenuSubContent>
|
|
<DropdownMenuItem
|
|
onClick={() => column.setFilterValue(undefined)}
|
|
>
|
|
Semua
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
{filter.options.map((option) => (
|
|
<DropdownMenuItem
|
|
key={option.value}
|
|
onClick={() => {
|
|
if (option.value === "true") column.setFilterValue(true);
|
|
else if (option.value === "false") column.setFilterValue(false);
|
|
else column.setFilterValue(option.value);
|
|
}}
|
|
>
|
|
{option.label}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuSubContent>
|
|
</DropdownMenuSub>
|
|
);
|
|
})}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
|
|
{selectedRows.length > 0 && bulkActions && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="secondary" className="gap-2 animate-in fade-in slide-in-from-left-2 transition-all">
|
|
Aksi Massal ({selectedRows.length})
|
|
<ChevronDown className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start">
|
|
{bulkActions.map((action, i) => (
|
|
<DropdownMenuItem
|
|
key={i}
|
|
onClick={() => action.onClick(selectedRows.map((r) => r.original))}
|
|
className={action.variant === "destructive" ? "text-red-600 focus:text-red-600" : ""}
|
|
>
|
|
{action.icon && <action.icon className="mr-2 h-4 w-4" />}
|
|
{action.label}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" className="ml-auto gap-2">
|
|
<Settings2 className="h-4 w-4" />
|
|
Kolom
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-48">
|
|
<DropdownMenuLabel>Tampilan Kolom</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
{table
|
|
.getAllColumns()
|
|
.filter((column) => column.getCanHide())
|
|
.map((column) => {
|
|
return (
|
|
<DropdownMenuCheckboxItem
|
|
key={column.id}
|
|
className="capitalize"
|
|
checked={column.getIsVisible()}
|
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
|
onSelect={(e) => e.preventDefault()}
|
|
>
|
|
{(column.columnDef.meta as any)?.title ||
|
|
(typeof column.columnDef.header === "string"
|
|
? column.columnDef.header
|
|
: String(column.id).replace(/_/g, " "))}
|
|
</DropdownMenuCheckboxItem>
|
|
);
|
|
})}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
<div className="overflow-hidden rounded-md border bg-background">
|
|
<Table>
|
|
<TableHeader>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<TableRow key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => {
|
|
return (
|
|
<TableHead key={header.id}>
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
</TableHead>
|
|
);
|
|
})}
|
|
</TableRow>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{table.getRowModel().rows?.length ? (
|
|
table.getRowModel().rows.map((row) => (
|
|
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
|
{row.getVisibleCells().map((cell) => (
|
|
<TableCell key={cell.id}>
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
|
Tidak ada data yang ditemukan.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-4">
|
|
<div className="text-sm text-muted-foreground">
|
|
{table.getFilteredSelectedRowModel().rows.length} dari{" "}
|
|
{table.getFilteredRowModel().rows.length} baris dipilih.
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => table.previousPage()}
|
|
disabled={!table.getCanPreviousPage()}
|
|
>
|
|
Previous
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => table.nextPage()}
|
|
disabled={!table.getCanNextPage()}
|
|
>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|