1347 lines
49 KiB
TypeScript
1347 lines
49 KiB
TypeScript
"use client";
|
|
import {
|
|
ColumnDef,
|
|
ColumnResizeDirection,
|
|
ColumnResizeMode,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
getPaginationRowModel,
|
|
getSortedRowModel,
|
|
SortingState,
|
|
useReactTable,
|
|
} from "@tanstack/react-table";
|
|
import React, { useCallback, useEffect, useState } from "react";
|
|
import { Button, Label, Table } from "flowbite-react";
|
|
import { HiChevronLeft, HiChevronRight, HiPlus } from "react-icons/hi";
|
|
import { useLocal } from "@/lib/utils/use-local";
|
|
import { debouncedHandler } from "@/lib/utils/debounceHandler";
|
|
import { FaSort, FaSortDown, FaSortUp } from "react-icons/fa6";
|
|
import Link from "next/link";
|
|
import { init_column } from "./lib/column";
|
|
import { toast } from "sonner";
|
|
import { Loader2, Sticker } from "lucide-react";
|
|
import { InputSearch } from "../ui/input-search";
|
|
import get from "lodash.get";
|
|
import { Checkbox } from "../ui/checkbox";
|
|
import { getNumber } from "@/lib/utils/getNumber";
|
|
import { formatMoney } from "@/lib/components/form/field/TypeInput";
|
|
import { events } from "@/lib/utils/event";
|
|
import { Popover } from "../Popover/Popover";
|
|
import { ButtonBetter, ButtonContainer } from "../ui/button";
|
|
import { Filter } from "@/lib/svg/Filter";
|
|
import { Form } from "../form/Form";
|
|
import { Field, FieldProps } from "../form/Field";
|
|
export interface Column<T = any> {
|
|
name: string; // Tetap string karena ini adalah identifier kolom
|
|
header: (() => JSX.Element) | string;
|
|
filter?: boolean;
|
|
resize?: boolean;
|
|
width?: number;
|
|
type?: "text" | "date" | "time" | "money" | "file";
|
|
nameFilter?: string;
|
|
onLoadFilter?: (params?: any) => Promise<any> | any;
|
|
onLabel?: string | ((item: any) => any);
|
|
onValue?: string | ((item: any) => any);
|
|
pagination?: boolean;
|
|
search?: "api" | "local";
|
|
renderCell: (params: {
|
|
row: T; // row tetap `T` agar lebih fleksibel
|
|
name: string; // name tetap string
|
|
cell: any;
|
|
idx: number;
|
|
tbl: any;
|
|
fm_row?: any;
|
|
onChange?: (data: any) => void;
|
|
render: () => void;
|
|
}) => JSX.Element | any;
|
|
sortable?: boolean;
|
|
}
|
|
export interface TableListProps<T extends object> {
|
|
autoPagination?: boolean;
|
|
name?: string;
|
|
column: Column<T>[] | (() => Column<T>[]);
|
|
style?: "UI" | "Default";
|
|
align?: "center" | "left" | "right";
|
|
onLoad:
|
|
| ((params: {
|
|
search?: string;
|
|
sort?: SortingState;
|
|
take: number;
|
|
paging: number;
|
|
}) => Promise<any[]>)
|
|
| any[];
|
|
take?: number;
|
|
header?: {
|
|
sideLeft?: (local: any) => React.ReactNode;
|
|
sideRight?: (local: any) => React.ReactNode;
|
|
};
|
|
disabledPagination?: boolean;
|
|
disabledHeader?: boolean;
|
|
disabledHeadTable?: boolean;
|
|
hiddenNoRow?: boolean;
|
|
disabledHoverRow?: boolean;
|
|
onInit?: (local: any) => void;
|
|
onCount?: (
|
|
params?:
|
|
| {
|
|
search?: string;
|
|
take: number;
|
|
paging: number;
|
|
}
|
|
| string
|
|
) => Promise<number>;
|
|
fm?: any;
|
|
mode?: "form" | "table";
|
|
feature?: string[];
|
|
onChange?: (data: any) => void;
|
|
filter?: boolean;
|
|
}
|
|
|
|
export interface FieldFilterProps extends Omit<FieldProps, "fm"> {
|
|
fm?: any; // Membuat `fm` nullable
|
|
}
|
|
|
|
export const TableList = <T extends object>({
|
|
autoPagination = true,
|
|
name = "table",
|
|
column,
|
|
style = "UI",
|
|
align = "center",
|
|
onLoad,
|
|
take = 20,
|
|
header,
|
|
disabledPagination,
|
|
disabledHeader,
|
|
disabledHeadTable,
|
|
hiddenNoRow,
|
|
disabledHoverRow,
|
|
onInit,
|
|
onCount,
|
|
fm,
|
|
mode = "table",
|
|
feature,
|
|
onChange,
|
|
filter = true,
|
|
}: TableListProps<T>) => {
|
|
const [show, setShow] = useState(false as boolean);
|
|
const [data, setData] = useState<any[]>([]);
|
|
const sideLeft =
|
|
typeof header?.sideLeft === "function" ? header.sideLeft : null;
|
|
const sideRight =
|
|
typeof header?.sideRight === "function" ? header.sideRight : null;
|
|
type Person = {
|
|
firstName: string;
|
|
lastName: string;
|
|
age: number;
|
|
visits: number;
|
|
status: string;
|
|
progress: number;
|
|
};
|
|
const checkbox =
|
|
Array.isArray(feature) && feature?.length
|
|
? feature.includes("checkbox")
|
|
: false;
|
|
|
|
const local = useLocal({
|
|
fieldFilter: [] as FieldFilterProps[],
|
|
fieldResultFilter: {} as any,
|
|
table: null as any,
|
|
data: [] as any[],
|
|
dataForm: [] as any[],
|
|
listData: [] as any[],
|
|
sort: {} as any,
|
|
filter: {} as any,
|
|
search: null as any,
|
|
paging: 1,
|
|
count: 0 as any,
|
|
addRow: (row: any) => {
|
|
setData((prev) => [...prev, row]);
|
|
local.data.push(row);
|
|
local.render();
|
|
},
|
|
selection: {
|
|
all: false,
|
|
partial: [] as any[],
|
|
data: [] as any[],
|
|
},
|
|
renderRow: (row: any) => {
|
|
setData((prev) => [...prev, row]);
|
|
local.data = data;
|
|
local.render();
|
|
},
|
|
removeRow: (row: any) => {
|
|
setData((prev) => prev.filter((item) => item !== row)); // Update state lokal
|
|
local.data = local.data.filter((item: any) => item !== row); // Hapus row dari local.data
|
|
local.render(); // Panggil render untuk memperbarui UI
|
|
},
|
|
refresh: async () => {
|
|
toast.info(
|
|
<>
|
|
<Loader2
|
|
className={cx(
|
|
"h-4 w-4 animate-spin-important",
|
|
css`
|
|
animation: spin 1s linear infinite !important;
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
`
|
|
)}
|
|
/>
|
|
{"Loading..."}
|
|
</>
|
|
);
|
|
console.log(local.fieldResultFilter);
|
|
if (typeof onCount === "function") {
|
|
const params = await events("onload-param", {
|
|
take: 1,
|
|
paging: 1,
|
|
search: local.search,
|
|
...local.filter,
|
|
...local.fieldResultFilter,
|
|
});
|
|
const res = await onCount(params);
|
|
local.count = res;
|
|
local.render();
|
|
}
|
|
if (Array.isArray(onLoad)) {
|
|
let res = onLoad;
|
|
if (!autoPagination) {
|
|
res = paginateArray(res, take, 1);
|
|
}
|
|
local.data = res;
|
|
local.render();
|
|
setData(res);
|
|
} else {
|
|
let res: any = await onLoad({
|
|
search: local.search,
|
|
sort: local.sort,
|
|
take,
|
|
paging: 1,
|
|
...local.fieldResultFilter,
|
|
});
|
|
if (!autoPagination) {
|
|
res = paginateArray(res, take, 1);
|
|
}
|
|
local.data = res;
|
|
cloneListFM(res);
|
|
local.render();
|
|
setData(res);
|
|
setTimeout(() => {
|
|
toast.dismiss();
|
|
}, 100);
|
|
}
|
|
},
|
|
reload: async () => {
|
|
toast.info(
|
|
<>
|
|
<Loader2
|
|
className={cx(
|
|
"h-4 w-4 animate-spin-important",
|
|
css`
|
|
animation: spin 1s linear infinite !important;
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
`
|
|
)}
|
|
/>
|
|
{"Loading..."}
|
|
</>
|
|
);
|
|
if (Array.isArray(onLoad)) {
|
|
let res = onLoad;
|
|
if (!autoPagination) {
|
|
res = paginateArray(res, take, local.paging);
|
|
}
|
|
local.data = res;
|
|
local.render();
|
|
setData(res);
|
|
} else {
|
|
let res: any = await onLoad({
|
|
search: local.search,
|
|
sort: local.sort,
|
|
take,
|
|
paging: local.paging,
|
|
});
|
|
if (!autoPagination) {
|
|
res = paginateArray(res, take, local.paging);
|
|
}
|
|
local.data = res;
|
|
cloneListFM(res);
|
|
local.render();
|
|
setData(res);
|
|
setTimeout(() => {
|
|
toast.dismiss();
|
|
}, 100);
|
|
}
|
|
},
|
|
});
|
|
const cloneListFM = (data: any[]) => {
|
|
if (mode === "form") {
|
|
local.dataForm = data.map((e: any) => {
|
|
return {
|
|
...fm,
|
|
data: e,
|
|
render: () => {
|
|
local.render();
|
|
fm.data[name] = local.data;
|
|
fm.render();
|
|
},
|
|
};
|
|
});
|
|
local.render();
|
|
}
|
|
};
|
|
useEffect(() => {
|
|
try {
|
|
if (Array.isArray(column) && column?.length) {
|
|
const dateIndex = column.findIndex((e) => e.type === "date");
|
|
const result: FieldFilterProps[] = column
|
|
.filter(
|
|
(e, index) =>
|
|
e.filter !== false && (e.type !== "date" || index === dateIndex)
|
|
) // Hapus jika `false`
|
|
.map((e) => ({
|
|
name: e?.nameFilter || e.name,
|
|
label: e?.header && typeof e?.header === "string" ? e.header : "", // Default null jika tidak ada label
|
|
type:
|
|
typeof e.onLoadFilter === "function"
|
|
? "dropdown-async"
|
|
: e.type === "file"
|
|
? "text"
|
|
: e.type ?? "text", // Default null jika tidak ada type
|
|
onLoad: e.onLoadFilter, // Jika `undefined`, `null`, atau bukan boolean, default `true`
|
|
onValue: e?.onValue,
|
|
onLabel: e?.onLabel,
|
|
pagination: e?.pagination,
|
|
search: e?.search,
|
|
}));
|
|
local.fieldFilter = result;
|
|
local.render();
|
|
}
|
|
} catch (ex) {}
|
|
const run = async () => {
|
|
toast.info(
|
|
<>
|
|
<Loader2
|
|
className={cx(
|
|
"h-4 w-4 animate-spin-important",
|
|
css`
|
|
animation: spin 1s linear infinite !important;
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
`
|
|
)}
|
|
/>
|
|
{"Loading..."}
|
|
</>
|
|
);
|
|
try {
|
|
if (typeof onCount === "function") {
|
|
const params = await events("onload-param", {
|
|
take: 1,
|
|
paging: 1,
|
|
search: local.search,
|
|
...local.filter,
|
|
});
|
|
const res = await onCount(params);
|
|
local.count = res;
|
|
local.render();
|
|
}
|
|
} catch (ex) {}
|
|
if (mode === "form") {
|
|
local.data = fm.data?.[name] || [];
|
|
cloneListFM(fm.data?.[name] || []);
|
|
local.render();
|
|
setData(fm.data?.[name] || []);
|
|
} else {
|
|
if (Array.isArray(onLoad)) {
|
|
local.data = onLoad;
|
|
cloneListFM(onLoad);
|
|
local.render();
|
|
setData(onLoad);
|
|
} else if (typeof onLoad === "function") {
|
|
let res: any = await onLoad({
|
|
search: local.search,
|
|
sort: local.sort,
|
|
take,
|
|
paging: 1,
|
|
});
|
|
if (!autoPagination) {
|
|
res = paginateArray(res, take, 1);
|
|
}
|
|
if (!local.count) {
|
|
local.count = res?.length;
|
|
}
|
|
local.data = res;
|
|
local.render();
|
|
setData(local.data);
|
|
} else {
|
|
let res: any[] = onLoad;
|
|
if (!autoPagination) {
|
|
res = paginateArray(res, take, 1);
|
|
}
|
|
local.data = res;
|
|
local.render();
|
|
setData(local.data);
|
|
}
|
|
}
|
|
setTimeout(() => {
|
|
toast.dismiss();
|
|
}, 100);
|
|
};
|
|
if (typeof onInit === "function") {
|
|
onInit(local);
|
|
}
|
|
run();
|
|
}, []);
|
|
const cols = typeof column === "function" ? column() : column;
|
|
const defaultColumns: ColumnDef<Person>[] = init_column(cols);
|
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
|
const [columns] = React.useState<typeof defaultColumns>(() =>
|
|
checkbox
|
|
? [
|
|
{
|
|
id: "select",
|
|
width: 10,
|
|
header: ({ table }) => {
|
|
return (
|
|
<Checkbox
|
|
id="terms"
|
|
className="text-primary bg-white data-[state=checked]:bg-white data-[state=checked]:text-primary"
|
|
checked={table.getIsAllRowsSelected()}
|
|
onClick={(e) => {
|
|
table.getToggleAllRowsSelectedHandler();
|
|
const handler = table.getToggleAllRowsSelectedHandler();
|
|
handler(e); // Pastikan ini memanggil fungsi handler yang benar
|
|
local.selection.all = !local.selection.all;
|
|
local.render();
|
|
setTimeout(() => {
|
|
if (!table.getIsAllRowsSelected()) {
|
|
local.selection.all = false;
|
|
local.selection.partial = [];
|
|
local.selection.data = [];
|
|
local.render();
|
|
} else {
|
|
local.selection.partial = local.data.map((e) => e?.id);
|
|
local.selection.data = local.data;
|
|
local.render();
|
|
}
|
|
}, 25);
|
|
}}
|
|
/>
|
|
);
|
|
},
|
|
cell: ({ row }) => {
|
|
const findCheck = (row: any) => {
|
|
if (row.getIsSelected()) return true;
|
|
const data: any = row.original;
|
|
const res = local.selection.partial.find((e) => e === data?.id);
|
|
return res ? true : false;
|
|
};
|
|
return (
|
|
<div className="px-0.5 items-center justify-center flex flex-row">
|
|
<Checkbox
|
|
id="terms"
|
|
className="border border-primary"
|
|
checked={findCheck(row)}
|
|
onClick={(e) => {
|
|
const handler = row.getToggleSelectedHandler();
|
|
handler(e); // Pastikan ini memanggil fungsi handler yang benar
|
|
const data: any = row.original;
|
|
const checked = local.selection.all
|
|
? true
|
|
: local.selection.partial.find((e) => e === data?.id);
|
|
if (!checked) {
|
|
local.selection.partial.push(data?.id);
|
|
local.selection.data.push(data);
|
|
} else {
|
|
if (
|
|
local.selection.partial.find((e) => e === data?.id)
|
|
) {
|
|
local.selection.partial =
|
|
local.selection.partial.filter(
|
|
(e: any) => e !== data?.id
|
|
);
|
|
local.selection.data = local.selection.data.filter(
|
|
(e: any) => e?.id !== data?.id
|
|
);
|
|
}
|
|
local.selection.all = false;
|
|
}
|
|
local.render();
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
},
|
|
sortable: false,
|
|
},
|
|
...defaultColumns,
|
|
]
|
|
: [...defaultColumns]
|
|
);
|
|
|
|
const [columnResizeMode, setColumnResizeMode] =
|
|
React.useState<ColumnResizeMode>("onChange");
|
|
|
|
const [columnResizeDirection, setColumnResizeDirection] =
|
|
React.useState<ColumnResizeDirection>("ltr");
|
|
// Create the table and pass your options
|
|
useEffect(() => {
|
|
setData(local.data);
|
|
}, [local.data.length]);
|
|
const paginationConfig = disabledPagination
|
|
? {}
|
|
: {
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
};
|
|
const [pagination, setPagination] = React.useState({
|
|
pageIndex: 0,
|
|
pageSize: 20,
|
|
});
|
|
const table = useReactTable({
|
|
data: data,
|
|
columnResizeMode,
|
|
pageCount: Math.ceil(local.count / 20),
|
|
manualPagination: true,
|
|
columnResizeDirection,
|
|
columns,
|
|
enableRowSelection: true,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
onSortingChange: setSorting,
|
|
initialState: {
|
|
pagination: {
|
|
pageIndex: 0,
|
|
pageSize: 20, //custom default page size
|
|
},
|
|
},
|
|
state: {
|
|
pagination,
|
|
sorting,
|
|
},
|
|
...paginationConfig,
|
|
});
|
|
local.table = table;
|
|
|
|
// Manage your own state
|
|
const [state, setState] = React.useState(table.initialState);
|
|
|
|
// Override the state managers for the table to your own
|
|
table.setOptions((prev) => ({
|
|
...prev,
|
|
state,
|
|
onStateChange: setState,
|
|
debugTable: state.pagination.pageIndex > 2,
|
|
}));
|
|
const handleSearch = useCallback(
|
|
debouncedHandler(() => {
|
|
local.refresh();
|
|
}, 1000),
|
|
[]
|
|
);
|
|
return (
|
|
<>
|
|
<div className="tbl-wrapper flex flex-grow flex-col">
|
|
{!disabledHeader ? (
|
|
<div className="head-tbl-list block items-start justify-between px-4 py-4 sm:flex">
|
|
<div className="flex flex-row items-end">
|
|
<div className="sm:flex flex flex-col space-y-2">
|
|
<div className="flex">
|
|
{sideLeft ? (
|
|
sideLeft(local)
|
|
) : (
|
|
<>
|
|
<Link href={"/new"}>
|
|
<Button className="bg-primary">
|
|
<div className="flex items-center gap-x-0.5">
|
|
<HiPlus className="text-xl" />
|
|
<span className="capitalize">Add {name}</span>
|
|
</div>
|
|
</Button>
|
|
</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="ml-auto flex items-center flex-row gap-x-1">
|
|
<div className="tbl-search hidden items-center sm:mb-0 sm:flex sm:divide-x sm:divide-gray-100">
|
|
<form
|
|
onSubmit={async (e) => {
|
|
e.preventDefault();
|
|
await local.reload();
|
|
}}
|
|
>
|
|
<Label htmlFor="users-search" className="sr-only">
|
|
Search
|
|
</Label>
|
|
<div className="relative lg:w-56">
|
|
<InputSearch
|
|
// className="bg-white search text-xs "
|
|
id="users-search"
|
|
name="users-search"
|
|
placeholder={`Search`}
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
local.search = value;
|
|
local.render();
|
|
handleSearch();
|
|
}}
|
|
/>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{mode === "table" && filter && local?.fieldFilter?.length ? (
|
|
<div className="flex flex-row items-center">
|
|
<Popover
|
|
classNameTrigger={""}
|
|
arrow={show}
|
|
className="rounded-md"
|
|
onOpenChange={(open: any) => {
|
|
setShow(open);
|
|
}}
|
|
open={show}
|
|
content={
|
|
<div className="flex flex-col p-4 w-80 gap-y-1">
|
|
{/* <div className="text-md font-semibold">Filter</div> */}
|
|
|
|
<Form
|
|
toastMessage="Filter"
|
|
onSubmit={async (fm: any) => {
|
|
local.fieldResultFilter = fm?.data;
|
|
local.render();
|
|
local.refresh();
|
|
setShow(false);
|
|
}}
|
|
onLoad={async () => {
|
|
return { ...local.fieldResultFilter };
|
|
}}
|
|
showResize={false}
|
|
header={(fm: any) => {
|
|
return <></>;
|
|
}}
|
|
children={(fm: any) => {
|
|
return (
|
|
<>
|
|
<div
|
|
className={cx(
|
|
"flex flex-col flex-wrap rounded-sm"
|
|
)}
|
|
>
|
|
<div className="flex-grow grid gap-y-2">
|
|
{local.fieldFilter?.map((e, idx) => {
|
|
if (e?.type === "date") {
|
|
const fm_row = {
|
|
data: fm?.data?.[e?.name],
|
|
...fm,
|
|
render: () => {
|
|
fm.render();
|
|
fm.data[e?.name] = fm_row.data;
|
|
fm.render();
|
|
},
|
|
};
|
|
return (
|
|
<div
|
|
key={`field-filter-${idx}`}
|
|
className=" grid grid-cols-2 gap-2"
|
|
>
|
|
<Field
|
|
fm={fm}
|
|
name={"start_date"}
|
|
label={e?.label}
|
|
type={"date"}
|
|
placeholder={`From`}
|
|
/>
|
|
<Field
|
|
fm={fm}
|
|
name={"end_date"}
|
|
label={e?.label}
|
|
type={"date"}
|
|
placeholder={`To`}
|
|
visibleLabel={true}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
return (
|
|
<div key={`field-filter-${idx}`}>
|
|
<Field
|
|
fm={fm}
|
|
label={e?.label}
|
|
type={e?.type}
|
|
placeholder={`Search${
|
|
e?.label ? ` ${e?.label}` : "..."
|
|
}`}
|
|
{...e}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="flex flex-row items-center justify-end py-2 w-full gap-x-2">
|
|
<ButtonBetter
|
|
className="rounded-full w-full px-6 "
|
|
variant={"outline"}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
fm.data = {};
|
|
fm.render();
|
|
fm.submit();
|
|
}}
|
|
>
|
|
Clear
|
|
</ButtonBetter>
|
|
<ButtonBetter className="rounded-full w-full px-6 ">
|
|
Apply
|
|
</ButtonBetter>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}}
|
|
/>
|
|
</div>
|
|
}
|
|
>
|
|
<ButtonContainer
|
|
onClick={() => {
|
|
setShow(true);
|
|
}}
|
|
>
|
|
<Filter />
|
|
</ButtonContainer>
|
|
</Popover>
|
|
</div>
|
|
) : (
|
|
<></>
|
|
)}
|
|
|
|
<div className="flex">{sideRight ? sideRight(local) : <></>}</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<></>
|
|
)}
|
|
|
|
<div className="flex flex-col flex-grow">
|
|
<div className="overflow-auto relative flex-grow flex-row">
|
|
<div className="tbl absolute top-0 left-0 inline-block flex-grow w-full h-full align-middle">
|
|
<div className="relative">
|
|
<Table
|
|
className={cx(
|
|
"min-w-full divide-y divide-gray-200 table-bg",
|
|
css`
|
|
// thead th:first-child {
|
|
// overflow: hidden;
|
|
// border-top-left-radius: 10px; /* Sudut kiri atas */
|
|
// border-bottom-left-radius: 10px;
|
|
// }
|
|
// thead th:last-child {
|
|
// overflow: hidden;
|
|
// border-top-right-radius: 10px; /* Sudut kiri atas */
|
|
// border-bottom-right-radius: 10px;
|
|
// }
|
|
// tbody td:first-child {
|
|
// overflow: hidden;
|
|
// border-top-left-radius: 10px; /* Sudut kiri atas */
|
|
// border-bottom-left-radius: 10px;
|
|
// }
|
|
// tbody td:last-child {
|
|
// overflow: hidden;
|
|
// border-top-right-radius: 10px; /* Sudut kiri atas */
|
|
// border-bottom-right-radius: 10px;
|
|
// }
|
|
`,
|
|
checkbox &&
|
|
css`
|
|
.table-header-tbl > th:first-child {
|
|
width: 20px !important; /* Atur lebar sesuai kebutuhan */
|
|
text-align: center;
|
|
min-width: 40px;
|
|
max-width: 40px;
|
|
}
|
|
.table-row-element > td:first-child {
|
|
width: 20px !important; /* Atur lebar sesuai kebutuhan */
|
|
text-align: center;
|
|
min-width: 40px;
|
|
max-width: 40px;
|
|
}
|
|
`
|
|
)}
|
|
>
|
|
{!disabledHeadTable ? (
|
|
<thead
|
|
className={cx(
|
|
"table-head-list overflow-hidden text-md bg-primary text-white group/head text-md uppercase sticky top-0",
|
|
css`
|
|
z-index: 1;
|
|
`
|
|
)}
|
|
>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<tr
|
|
key={`${headerGroup.id}`}
|
|
className={"table-header-tbl"}
|
|
>
|
|
{headerGroup.headers.map((header, index) => {
|
|
const name = header.column.id;
|
|
const col = cols.find((e: any) => e?.name === name);
|
|
const isSort =
|
|
name === "select"
|
|
? false
|
|
: typeof col?.sortable === "boolean"
|
|
? col.sortable
|
|
: true;
|
|
const resize =
|
|
name === "select"
|
|
? false
|
|
: typeof col?.resize === "boolean"
|
|
? col.resize
|
|
: true;
|
|
return (
|
|
<th
|
|
{...{
|
|
style: {
|
|
width: !resize
|
|
? `${col?.width}px`
|
|
: name === "select"
|
|
? `${5}px`
|
|
: col?.width
|
|
? header.getSize() < col?.width
|
|
? `${col.width}px`
|
|
: header.getSize()
|
|
: header.getSize(),
|
|
},
|
|
}}
|
|
key={header.id}
|
|
colSpan={header.colSpan}
|
|
className={cx(
|
|
"relative px-2 py-2 text-sm py-1 uppercase",
|
|
name === "select" &&
|
|
css`
|
|
max-width: 5px;
|
|
`
|
|
)}
|
|
>
|
|
<div
|
|
key={`${header.id}-label`}
|
|
{...{
|
|
style: col?.width
|
|
? {
|
|
minWidth: `${col.width}px`,
|
|
}
|
|
: {},
|
|
}}
|
|
onClick={() => {
|
|
if (isSort) {
|
|
const sort = local?.sort?.[name];
|
|
const mode =
|
|
sort === "desc"
|
|
? null
|
|
: sort === "asc"
|
|
? "desc"
|
|
: "asc";
|
|
local.sort = mode
|
|
? {
|
|
[name]: mode,
|
|
}
|
|
: {};
|
|
local.render();
|
|
|
|
local.reload();
|
|
}
|
|
}}
|
|
className={cx(
|
|
"flex flex-grow flex-row flex-grow select-none items-center flex-row text-base text-nowrap",
|
|
isSort ? " cursor-pointer" : ""
|
|
)}
|
|
>
|
|
<div
|
|
className={cx(
|
|
"flex flex-row items-center flex-grow text-sm capitalize",
|
|
name === "select" ? "justify-center" : ""
|
|
)}
|
|
>
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(
|
|
header.column.columnDef.header,
|
|
header.getContext()
|
|
)}
|
|
</div>
|
|
{isSort ? (
|
|
<div className="flex flex-col items-center">
|
|
{local?.sort?.[name] ? (
|
|
<>
|
|
{local?.sort?.[name] === "asc" ? (
|
|
<FaSortUp
|
|
className={cx(
|
|
"text-xs",
|
|
local?.sort?.[name] === "asc"
|
|
? "text-white"
|
|
: "text-primary"
|
|
)}
|
|
/>
|
|
) : (
|
|
<FaSortDown
|
|
className={cx(
|
|
"text-xs",
|
|
local?.sort?.[name] === "desc"
|
|
? "text-white"
|
|
: "text-primary"
|
|
)}
|
|
/>
|
|
)}
|
|
</>
|
|
) : (
|
|
<FaSort className=" text-xs text-white" />
|
|
)}
|
|
</div>
|
|
) : (
|
|
<></>
|
|
)}
|
|
</div>
|
|
{resize &&
|
|
headerGroup.headers.length !== index + 1 ? (
|
|
<div
|
|
key={`${header.id}-resizer`} // Tambahkan key unik
|
|
{...{
|
|
onDoubleClick: () =>
|
|
header.column.resetSize(),
|
|
onMouseDown: header.getResizeHandler(),
|
|
onTouchStart: header.getResizeHandler(),
|
|
className: cx(
|
|
`resizer hover:bg-second cursor-e-resize ${
|
|
table.options.columnResizeDirection
|
|
} ${
|
|
header.column.getIsResizing()
|
|
? "isResizing bg-second"
|
|
: " bg-primary"
|
|
}`,
|
|
css`
|
|
width: 5px;
|
|
cursor: e-resize !important;
|
|
`
|
|
),
|
|
style: {
|
|
transform:
|
|
columnResizeMode === "onEnd" &&
|
|
header.column.getIsResizing()
|
|
? `translateX(${
|
|
(table.options
|
|
.columnResizeDirection ===
|
|
"rtl"
|
|
? -1
|
|
: 1) *
|
|
(table.getState()
|
|
.columnSizingInfo
|
|
.deltaOffset ?? 0)
|
|
}px)`
|
|
: "",
|
|
},
|
|
}}
|
|
></div>
|
|
) : null}
|
|
</th>
|
|
);
|
|
})}
|
|
</tr>
|
|
))}
|
|
</thead>
|
|
) : (
|
|
<></>
|
|
)}
|
|
|
|
<Table.Body className="divide-y border-none ">
|
|
{table.getRowModel().rows.map((row, idx) => {
|
|
const fm_row =
|
|
mode === "form" ? local.dataForm?.[idx] : null;
|
|
return (
|
|
<Table.Row
|
|
key={row.id}
|
|
className={cx(
|
|
disabledHoverRow ? "" : "hover:bg-gray-100",
|
|
css`
|
|
height: 44px;
|
|
> td {
|
|
vertical-align: ${align};
|
|
}
|
|
`,
|
|
"border-none ",
|
|
style === "UI" ? "even:bg-linear-blue " : ""
|
|
)}
|
|
>
|
|
{row.getVisibleCells().map((cell: any) => {
|
|
const ctx = cell.getContext();
|
|
const param = {
|
|
row: row.original,
|
|
name: get(ctx, "column.columnDef.accessorKey"),
|
|
cell,
|
|
idx,
|
|
tbl: local,
|
|
fm_row: fm_row,
|
|
onChange,
|
|
render: () => {
|
|
local.render();
|
|
setData(local.data);
|
|
},
|
|
};
|
|
const head = cols.find(
|
|
(e: any) =>
|
|
e?.name ===
|
|
get(ctx, "column.columnDef.accessorKey")
|
|
);
|
|
const renderData =
|
|
typeof head?.renderCell === "function"
|
|
? head.renderCell(param)
|
|
: flexRender(
|
|
cell.column.columnDef.cell,
|
|
cell.getContext()
|
|
);
|
|
return (
|
|
<Table.Cell
|
|
className={cx(
|
|
"text-md px-2 py-1 whitespace-nowrap text-gray-900 items-start",
|
|
name === "select"
|
|
? css`
|
|
width: 5px;
|
|
`
|
|
: ``
|
|
)}
|
|
key={cell.id}
|
|
>
|
|
{renderData}
|
|
</Table.Cell>
|
|
);
|
|
})}
|
|
</Table.Row>
|
|
);
|
|
})}
|
|
</Table.Body>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
{!hiddenNoRow && !table.getRowModel().rows?.length && (
|
|
<div
|
|
className={cx(
|
|
"flex-1 w-full absolute inset-0 flex flex-col items-center justify-center",
|
|
css`
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
`
|
|
)}
|
|
>
|
|
<div className="max-w-[15%] flex flex-col items-center">
|
|
<Sticker size={35} strokeWidth={1} />
|
|
<div className="pt-1 text-center">No Data</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Pagination
|
|
list={local}
|
|
count={local.count}
|
|
onNextPage={() => {
|
|
table.nextPage();
|
|
local.paging = local.paging + 1;
|
|
local.render();
|
|
local.reload();
|
|
}}
|
|
onPrevPage={() => {
|
|
table.previousPage();
|
|
local.paging = local.paging - 1;
|
|
local.render();
|
|
local.reload();
|
|
}}
|
|
disabledNextPage={!table.getCanNextPage()}
|
|
disabledPrevPage={!table.getCanPreviousPage()}
|
|
page={table.getState().pagination.pageIndex + 1}
|
|
setPage={(page: any) => {
|
|
setPagination({
|
|
pageIndex: page,
|
|
pageSize: 20,
|
|
});
|
|
local.paging = page;
|
|
local.render();
|
|
local.reload();
|
|
}}
|
|
countPage={table.getPageCount()}
|
|
countData={local.data.length}
|
|
take={take}
|
|
onChangePage={(page: number) => {
|
|
table.setPageIndex(page);
|
|
}}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const Pagination: React.FC<any> = ({
|
|
onNextPage,
|
|
onPrevPage,
|
|
disabledNextPage,
|
|
disabledPrevPage,
|
|
page,
|
|
count,
|
|
list,
|
|
setPage,
|
|
onChangePage,
|
|
}) => {
|
|
const local = useLocal({
|
|
page: 1 as any,
|
|
pagination: [] as any,
|
|
});
|
|
useEffect(() => {
|
|
local.page = page;
|
|
local.pagination = getPagination(page, Math.ceil(count / 20));
|
|
local.render();
|
|
}, [page, count]);
|
|
return (
|
|
<div className=" border-t border-gray-300 p-2 tbl-pagination sticky text-sm bottom-0 right-0 w-full grid grid-cols-3 gap-4 justify-end text-sm ">
|
|
<div className="flex flex-row items-center text-gray-600">
|
|
Showing {local.page * 20 - 19} to{" "}
|
|
{list.data?.length >= 20
|
|
? local.page * 20
|
|
: local.page === 1 && Math.ceil(count / 20) === 1
|
|
? list.data?.length
|
|
: local.page * 20 - 19 + list.data?.length}{" "}
|
|
of {formatMoney(getNumber(count))} results
|
|
</div>
|
|
<div className="flex flex-row justify-center">
|
|
<div>
|
|
<nav
|
|
className="isolate inline-flex -space-x-px flex flex-row items-center gap-x-2"
|
|
aria-label="Pagination"
|
|
>
|
|
{local.pagination.map((e: any, idx: number) => {
|
|
return (
|
|
<div
|
|
key={"page_" + idx}
|
|
onClick={() => {
|
|
if (e?.label !== "...") {
|
|
local.page = getNumber(e?.label);
|
|
local.render();
|
|
onChangePage(local.page - 1);
|
|
setPage(local.page - 1);
|
|
list.reload();
|
|
}
|
|
}}
|
|
className={cx(
|
|
"text-sm px-2 py-1",
|
|
e.active
|
|
? "relative z-10 inline-flex items-center bg-primary font-semibold text-white rounded-md"
|
|
: e?.label === "..."
|
|
? "relative z-10 inline-flex items-center font-semibold text-gray-800 rounded-md"
|
|
: "cursor-pointer relative z-10 inline-flex items-center hover:bg-gray-100 font-semibold text-gray-800 rounded-md"
|
|
)}
|
|
>
|
|
{e?.label}
|
|
</div>
|
|
);
|
|
})}
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-row items-center justify-end">
|
|
<div className="flex items-center flex-row gap-x-2 sm:mb-0 text-sm">
|
|
<div
|
|
onClick={() => {
|
|
if (!disabledPrevPage) {
|
|
onPrevPage();
|
|
}
|
|
}}
|
|
className={cx(
|
|
"flex flex-row items-center gap-x-2 justify-center rounded p-1 ",
|
|
disabledPrevPage
|
|
? "text-gray-200 border-gray-200 border px-2"
|
|
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900 border-gray-500 border px-2"
|
|
)}
|
|
>
|
|
<HiChevronLeft className="text-sm" />
|
|
{/* <span>Previous</span> */}
|
|
</div>
|
|
<div
|
|
onClick={() => {
|
|
if (!disabledNextPage) {
|
|
onNextPage();
|
|
}
|
|
}}
|
|
className={cx(
|
|
"flex flex-row items-center gap-x-2 justify-center rounded p-1 ",
|
|
disabledNextPage
|
|
? "text-gray-200 border-gray-200 border px-2"
|
|
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900 border-gray-500 border px-2"
|
|
)}
|
|
>
|
|
{/* <span>Next</span> */}
|
|
<HiChevronRight className="text-sm" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
export const PaginationPage: React.FC<any> = ({
|
|
onNextPage,
|
|
onPrevPage,
|
|
disabledNextPage,
|
|
disabledPrevPage,
|
|
page,
|
|
count,
|
|
list,
|
|
take,
|
|
setPage,
|
|
onChangePage,
|
|
}) => {
|
|
const local = useLocal({
|
|
page: 1 as any,
|
|
pagination: [] as any,
|
|
});
|
|
useEffect(() => {
|
|
local.page = page;
|
|
local.pagination = getPagination(page, Math.ceil(count / take));
|
|
local.render();
|
|
}, [page, count]);
|
|
return (
|
|
<div className=" tbl-pagination text-sm bottom-0 right-0 w-full grid grid-cols-1 gap-4 justify-center text-sm pt-2">
|
|
<div className="flex flex-row items-center justify-center">
|
|
<div className="flex items-center flex-row gap-x-2 sm:mb-0 text-sm">
|
|
<div
|
|
onClick={() => {
|
|
if (!disabledPrevPage) {
|
|
onPrevPage();
|
|
}
|
|
}}
|
|
className={cx(
|
|
"flex flex-row items-center gap-x-2 justify-center rounded-full p-2 text-md",
|
|
disabledPrevPage
|
|
? "text-gray-200 border-gray-200 border "
|
|
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900 border-gray-500 border "
|
|
)}
|
|
>
|
|
<HiChevronLeft />
|
|
</div>
|
|
<div className="flex flex-row justify-center">
|
|
<div>
|
|
<nav
|
|
className="isolate inline-flex -space-x-px flex flex-row items-center gap-x-2"
|
|
aria-label="Pagination"
|
|
>
|
|
{local.pagination.map((e: any, idx: number) => {
|
|
return (
|
|
<div
|
|
key={"page_" + idx}
|
|
onClick={() => {
|
|
if (e?.label !== "...") {
|
|
local.page = getNumber(e?.label);
|
|
local.render();
|
|
onChangePage(local.page);
|
|
setPage(local.page);
|
|
}
|
|
}}
|
|
className={cx(
|
|
"text-md px-2.5 py-1",
|
|
e.active
|
|
? "relative z-10 inline-flex items-center bg-primary font-semibold text-white rounded-full"
|
|
: e?.label === "..."
|
|
? "relative z-10 inline-flex items-center font-semibold text-gray-800 rounded-full"
|
|
: "cursor-pointer relative z-10 inline-flex items-center hover:bg-gray-100 font-semibold text-gray-800 rounded-full"
|
|
)}
|
|
>
|
|
{e?.label}
|
|
</div>
|
|
);
|
|
})}
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
<div
|
|
onClick={() => {
|
|
if (!disabledNextPage) {
|
|
onNextPage();
|
|
}
|
|
}}
|
|
className={cx(
|
|
"flex flex-row items-center gap-x-2 justify-center rounded-full p-2 ",
|
|
disabledNextPage
|
|
? "text-gray-200 border-gray-200 border"
|
|
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900 border-gray-500 border "
|
|
)}
|
|
>
|
|
<HiChevronRight className="text-md" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const getPagination = (currentPage: number, totalPages: number) => {
|
|
const pagination: { label: string; active: boolean }[] = [];
|
|
const maxVisible = 5; // Jumlah maksimal elemen yang ditampilkan
|
|
const halfRange = Math.floor((maxVisible - 3) / 2);
|
|
|
|
if (totalPages <= maxVisible) {
|
|
// Jika total halaman lebih kecil dari batas, tampilkan semua halaman
|
|
for (let i = 1; i <= totalPages; i++) {
|
|
pagination.push({ label: i.toString(), active: i === currentPage });
|
|
}
|
|
} else {
|
|
pagination.push({ label: "1", active: currentPage === 1 }); // Halaman pertama selalu ada
|
|
|
|
if (currentPage > halfRange + 2) {
|
|
pagination.push({ label: "...", active: false }); // Awal titik-titik
|
|
}
|
|
|
|
const startPage = Math.max(2, currentPage - halfRange);
|
|
const endPage = Math.min(totalPages - 1, currentPage + halfRange);
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
pagination.push({ label: i.toString(), active: i === currentPage });
|
|
}
|
|
|
|
if (currentPage < totalPages - halfRange - 1) {
|
|
pagination.push({ label: "...", active: false }); // Akhir titik-titik
|
|
}
|
|
|
|
pagination.push({
|
|
label: totalPages.toString(),
|
|
active: currentPage === totalPages,
|
|
}); // Halaman terakhir selalu ada
|
|
}
|
|
|
|
return pagination;
|
|
};
|
|
|
|
function paginateArray(array: any[], take: number, paging: number) {
|
|
if (!Array.isArray(array) || !array?.length) {
|
|
return [];
|
|
}
|
|
const startIndex = (paging - 1) * take;
|
|
const endIndex = startIndex + take;
|
|
return array.slice(startIndex, endIndex);
|
|
}
|