"use client"; import { ColumnDef, ColumnResizeDirection, ColumnResizeMode, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, SortingState, useReactTable, } from "@tanstack/react-table"; import React, { useEffect, useState } from "react"; import { Button, Table } from "flowbite-react"; import { HiChevronLeft, HiChevronRight, HiPlus } from "react-icons/hi"; import { useLocal } from "@/lib/utils/use-local"; 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 { 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 | string[]; placeholderFilter?: string | string[]; onLoadFilter?: (params?: any) => Promise | any; labelFilter?: string; 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 { autoPagination?: boolean; name?: string; column: Column[] | (() => Column[]); style?: "UI" | "Default"; align?: "center" | "left" | "right"; onLoad: | ((params: { search?: string; sort?: SortingState; take: number; paging: number; }) => Promise) | 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; fm?: any; mode?: "form" | "table"; feature?: string[]; onChange?: (data: any) => void; filter?: boolean; } export interface FilterProps { nameFilter?: string | string[]; placeholderFilter?: string | string[]; } export interface FieldFilterProps extends Omit, FilterProps, FilterProps { fm?: any; // Membuat `fm` nullable } export const TableList = ({ 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) => { const [show, setShow] = useState(false as boolean); const [data, setData] = useState([]); 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( <> {"Loading..."} , { duration: Infinity, } ); 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( <> {"Loading..."} , { duration: Infinity, } ); 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, ...local.filter, ...local.fieldResultFilter, }); if (!autoPagination) { res = paginateArray(res, take, local.paging); } local.data = res; cloneListFM(res); local.render(); setData(res); setTimeout(() => { toast.dismiss(); }, 100); } 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 { const col = typeof column === "function" ? column() : column; if (Array.isArray(col) && col?.length) { const dateIndices = Array.isArray(col) ? col .map((e, index) => (e.type === "date" ? index : -1)) .filter((index) => index !== -1) : []; const result: FieldFilterProps[] = col .filter( (e, index) => e.filter !== false && (e.type !== "date" || dateIndices.includes(index)) ) // Hapus jika `false` .map((e) => ({ nameFilter: e?.nameFilter, placeholderFilter: e?.placeholderFilter, name: Array.isArray(e?.nameFilter) && e?.nameFilter?.length ? e.nameFilter[0] : typeof e?.nameFilter === "string" ? e.nameFilter : e?.name, label: e?.labelFilter && typeof e?.labelFilter === "string" ? e.labelFilter // Default null jika tidak ada 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, })); console.log({ result }); local.fieldFilter = result; local.render(); } } catch (ex) {} const run = async () => { toast.info( <> {"Loading..."} , { duration: Infinity, } ); try { try { 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(); } } 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, ...local.filter, ...local.fieldResultFilter, }); 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); } } } catch (ex) {} setTimeout(() => { toast.dismiss(); }, 100); }; if (typeof onInit === "function") { onInit(local); } run(); }, []); const cols = typeof column === "function" ? column() : column; const defaultColumns: ColumnDef[] = init_column(cols); const [sorting, setSorting] = React.useState([]); const [columns] = React.useState(() => checkbox ? [ { id: "select", width: 10, header: ({ table }) => { return ( { 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 (
{ 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(); }} />
); }, sortable: false, }, ...defaultColumns, ] : [...defaultColumns] ); const [columnResizeMode, setColumnResizeMode] = React.useState("onChange"); const [columnResizeDirection, setColumnResizeDirection] = React.useState("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: take, }); const table = useReactTable({ data: data, columnResizeMode, pageCount: Math.ceil(local.count / take), manualPagination: true, columnResizeDirection, columns, enableRowSelection: true, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), onSortingChange: setSorting, initialState: { pagination: { pageIndex: 0, pageSize: take, //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, })); return ( <>
{!disabledHeader ? (
{sideLeft ? ( sideLeft(local) ) : ( <> )}
{ e.preventDefault(); await local.reload(); }} >
{ const value = e.target.value; local.search = value; local.render(); local.refresh(); }} />
{mode === "table" && filter && local?.fieldFilter?.length ? (
{ setShow(open); }} open={show} content={
{/*
Filter
*/}
{ 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 ( <>
{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 (
); } return (
); })}
{ e.preventDefault(); e.stopPropagation(); fm.data = {}; fm.render(); fm.submit(); }} > Clear Apply
); }} />
} > { setShow(true); }} >
) : ( <> )}
{sideRight ? sideRight(local) : <>}
) : ( <> )}
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 ? ( {table.getHeaderGroups().map((headerGroup) => ( {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 ( ); })} ))} ) : ( <> )} {table.getRowModel().rows.map((row, idx) => { const fm_row = mode === "form" ? local.dataForm?.[idx] : null; return ( 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 ( {renderData} ); })} ); })}
{ 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" : "" )} >
{header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )}
{isSort ? (
{local?.sort?.[name] ? ( <> {local?.sort?.[name] === "asc" ? ( ) : ( )} ) : ( )}
) : ( <> )}
{resize && headerGroup.headers.length !== index + 1 ? (
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)` : "", }, }} >
) : null}
{!hiddenNoRow && !table.getRowModel().rows?.length && (
No Data
)}
{ 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={local.paging} setPage={(page: any) => { setPagination({ pageIndex: page, pageSize: take, }); local.paging = page; local.render(); local.reload(); }} countPage={table.getPageCount()} countData={local.data.length} take={take} onChangePage={(page: number) => { table.setPageIndex(page); }} />
); }; export const Pagination: React.FC = ({ 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 (
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
{ 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" )} > {/* Previous */}
{ 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" )} > {/* Next */}
); }; export const PaginationPage: React.FC = ({ 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 (
{ 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 " )} >
{ 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 " )} >
); }; 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); }