feat: implement server-side sorting

This commit is contained in:
ValdiANS
2026-05-13 10:51:35 +07:00
parent b9ef0fa338
commit 3a2e74b559
2 changed files with 103 additions and 40 deletions
+45 -24
View File
@@ -1,18 +1,13 @@
'use client'; 'use client';
import { import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
ChangeEventHandler,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { import {
CellContext, CellContext,
ColumnDef, ColumnDef,
Row, Row,
SortingState, SortingState,
Updater,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@@ -47,7 +42,8 @@ import { BaseApiResponse } from '@/types/api/api-general';
type ExpenseTableFilters = { type ExpenseTableFilters = {
search: string; search: string;
nameSort: string; sort_by: string;
order_by: string;
transactionDate: string; transactionDate: string;
realizationDate: string; realizationDate: string;
locationId: string; locationId: string;
@@ -242,7 +238,8 @@ const ExpensesTable = () => {
page: 1, page: 1,
pageSize: 10, pageSize: 10,
search: '', search: '',
nameSort: '', sort_by: '',
order_by: '',
transactionDate: '', transactionDate: '',
realizationDate: '', realizationDate: '',
locationId: '', locationId: '',
@@ -261,7 +258,8 @@ const ExpensesTable = () => {
paramMap: { paramMap: {
page: 'page', page: 'page',
pageSize: 'limit', pageSize: 'limit',
nameSort: 'sort_name', sort_by: 'sort_by',
order_by: 'sort_order',
transactionDate: 'transaction_date', transactionDate: 'transaction_date',
realizationDate: 'realization_date', realizationDate: 'realization_date',
locationId: 'location_id', locationId: 'location_id',
@@ -319,7 +317,26 @@ const ExpensesTable = () => {
const [exportProgressStartDate, setExportProgressStartDate] = useState(''); const [exportProgressStartDate, setExportProgressStartDate] = useState('');
const [exportProgressEndDate, setExportProgressEndDate] = useState(''); const [exportProgressEndDate, setExportProgressEndDate] = useState('');
const [sorting, setSorting] = useState<SortingState>([]); const sorting: SortingState = tableFilterState.sort_by
? [
{
id: tableFilterState.sort_by,
desc: tableFilterState.order_by === 'desc',
},
]
: [];
const handleSortingChange = (updater: Updater<SortingState>) => {
const next = typeof updater === 'function' ? updater(sorting) : updater;
if (next.length > 0) {
updateFilter('sort_by', next[0].id, true);
updateFilter('order_by', next[0].desc ? 'desc' : 'asc', true);
} else {
updateFilter('sort_by', '', true);
updateFilter('order_by', '', true);
}
};
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({}); const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const selectedRowIds = Object.keys(rowSelection).map((item) => const selectedRowIds = Object.keys(rowSelection).map((item) =>
parseInt(item) parseInt(item)
@@ -437,10 +454,12 @@ const ExpensesTable = () => {
cell: (props) => props.row.original.location?.name ?? '-', cell: (props) => props.row.original.location?.name ?? '-',
}, },
{ {
accessorKey: 'created_user',
accessorFn: (row) => row.created_user.name ?? '-', accessorFn: (row) => row.created_user.name ?? '-',
header: 'Nama Pengaju', header: 'Nama Pengaju',
}, },
{ {
accessorKey: 'supplier',
accessorFn: (row) => row.supplier.name ?? '-', accessorFn: (row) => row.supplier.name ?? '-',
header: 'Uraian', header: 'Uraian',
}, },
@@ -454,17 +473,20 @@ const ExpensesTable = () => {
}, },
{ {
header: 'Status Pencairan', header: 'Status Pencairan',
enableSorting: false,
cell: (props) => ( cell: (props) => (
<RealizationStatusBadge approval={props.row.original.latest_approval} /> <RealizationStatusBadge approval={props.row.original.latest_approval} />
), ),
}, },
{ {
header: 'Status BOP', header: 'Status BOP',
enableSorting: false,
cell: (props) => ( cell: (props) => (
<ExpenseStatusBadge approval={props.row.original.latest_approval} /> <ExpenseStatusBadge approval={props.row.original.latest_approval} />
), ),
}, },
{ {
accessorKey: 'is_paid',
header: 'Status Lunas', header: 'Status Lunas',
cell: (props) => { cell: (props) => {
return ( return (
@@ -478,6 +500,14 @@ const ExpensesTable = () => {
); );
}, },
}, },
{
accessorKey: 'created_at',
header: 'Tanggal Dibuat',
cell: (props) =>
props.row.original.created_at
? formatDate(props.row.original.created_at, 'DD MMM YYYY')
: '-',
},
{ {
header: 'Aksi', header: 'Aksi',
cell: (props) => { cell: (props) => {
@@ -882,17 +912,6 @@ const ExpensesTable = () => {
} }
}, [getTableFilterQueryString]); }, [getTableFilterQueryString]);
// track sorting
useEffect(() => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
if (!isNameSorted) {
updateFilter('nameSort', '', false);
} else {
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
}
}, [sorting, updateFilter]);
return ( return (
<> <>
<div className='w-full'> <div className='w-full'>
@@ -1051,7 +1070,8 @@ const ExpensesTable = () => {
'page', 'page',
'pageSize', 'pageSize',
'search', 'search',
'nameSort', 'sort_by',
'order_by',
'userId', 'userId',
'locationName', 'locationName',
'vendorName', 'vendorName',
@@ -1152,7 +1172,8 @@ const ExpensesTable = () => {
onPageSizeChange={setPageSize} onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={handleSortingChange}
manualSorting
rowSelection={rowSelection} rowSelection={rowSelection}
setRowSelection={setRowSelection} setRowSelection={setRowSelection}
enableRowSelection={tableEnableRowSelectionHandler} enableRowSelection={tableEnableRowSelectionHandler}
+58 -16
View File
@@ -2,7 +2,12 @@
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import {
CellContext,
ColumnDef,
SortingState,
Updater,
} from '@tanstack/react-table';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import Link from 'next/link'; import Link from 'next/link';
@@ -34,6 +39,8 @@ import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
type PurchaseTableFilters = { type PurchaseTableFilters = {
search: string; search: string;
sort_by: string;
order_by: string;
po_date: string; po_date: string;
approval_status: string; approval_status: string;
product_category_id: string; product_category_id: string;
@@ -157,18 +164,6 @@ const RowOptionsMenu = ({
}; };
const PurchaseTable = () => { const PurchaseTable = () => {
// ===== STATE MANAGEMENT =====
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
useState(false);
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
null
);
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
const [sorting, setSorting] = useState<SortingState>([]);
// ===== TABLE FILTER STATE ===== // ===== TABLE FILTER STATE =====
const { const {
state: tableFilterState, state: tableFilterState,
@@ -180,6 +175,8 @@ const PurchaseTable = () => {
} = useTableFilter<PurchaseTableFilters>({ } = useTableFilter<PurchaseTableFilters>({
initial: { initial: {
search: '', search: '',
sort_by: '',
order_by: '',
po_date: '', po_date: '',
approval_status: '', approval_status: '',
product_category_id: '', product_category_id: '',
@@ -198,6 +195,8 @@ const PurchaseTable = () => {
paramMap: { paramMap: {
page: 'page', page: 'page',
pageSize: 'limit', pageSize: 'limit',
sort_by: 'sort_by',
order_by: 'sort_order',
po_date: 'po_date', po_date: 'po_date',
approval_status: 'approval_status', approval_status: 'approval_status',
product_category_id: 'product_category_id', product_category_id: 'product_category_id',
@@ -219,6 +218,36 @@ const PurchaseTable = () => {
storeName: 'purchase-table', storeName: 'purchase-table',
}); });
// ===== STATE MANAGEMENT =====
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
useState(false);
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
null
);
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
const sorting: SortingState = tableFilterState.sort_by
? [
{
id: tableFilterState.sort_by,
desc: tableFilterState.order_by === 'desc',
},
]
: [];
const handleSortingChange = (updater: Updater<SortingState>) => {
const next = typeof updater === 'function' ? updater(sorting) : updater;
if (next.length > 0) {
updateFilter('sort_by', next[0].id, true);
updateFilter('order_by', next[0].desc ? 'desc' : 'asc', true);
} else {
updateFilter('sort_by', '', true);
updateFilter('order_by', '', true);
}
};
// ===== MODAL HOOKS ===== // ===== MODAL HOOKS =====
const filterModal = useModal(); const filterModal = useModal();
const deleteModal = useModal(); const deleteModal = useModal();
@@ -238,6 +267,7 @@ const PurchaseTable = () => {
const purchaseColumns: ColumnDef<Purchase>[] = [ const purchaseColumns: ColumnDef<Purchase>[] = [
{ {
header: 'No. PR/PO', header: 'No. PR/PO',
enableSorting: false,
cell: (props) => { cell: (props) => {
const { pr_number, po_number } = props.row.original; const { pr_number, po_number } = props.row.original;
return po_number ? po_number : pr_number; return po_number ? po_number : pr_number;
@@ -278,7 +308,7 @@ const PurchaseTable = () => {
cell: (props) => props.row.original.requester_name || '-', cell: (props) => props.row.original.requester_name || '-',
}, },
{ {
accessorKey: 'products.name', accessorKey: 'products',
header: 'Produk', header: 'Produk',
cell: (props) => { cell: (props) => {
const products = props.row.original.products; const products = props.row.original.products;
@@ -293,7 +323,7 @@ const PurchaseTable = () => {
}, },
}, },
{ {
accessorKey: 'location.name', accessorKey: 'location',
header: 'Lokasi', header: 'Lokasi',
cell: (props) => props.row.original.location?.name || '-', cell: (props) => props.row.original.location?.name || '-',
}, },
@@ -323,6 +353,7 @@ const PurchaseTable = () => {
}, },
{ {
header: 'Aging', header: 'Aging',
enableSorting: false,
cell: (props) => { cell: (props) => {
const purchase = props.row.original; const purchase = props.row.original;
if (!purchase.po_date) return '-'; if (!purchase.po_date) return '-';
@@ -334,6 +365,7 @@ const PurchaseTable = () => {
}, },
}, },
{ {
accessorKey: 'status',
header: 'Status Approval', header: 'Status Approval',
cell: (props) => { cell: (props) => {
const approval = props.row.original.latest_approval; const approval = props.row.original.latest_approval;
@@ -378,6 +410,14 @@ const PurchaseTable = () => {
); );
}, },
}, },
{
accessorKey: 'created_at',
header: 'Tanggal Dibuat',
cell: (props) =>
props.row.original.created_at
? formatDate(props.row.original.created_at, 'DD MMM YYYY')
: '-',
},
{ {
header: 'Aksi', header: 'Aksi',
cell: (props) => { cell: (props) => {
@@ -658,6 +698,7 @@ const PurchaseTable = () => {
'search', 'search',
'filter_by', 'filter_by',
'sort_by', 'sort_by',
'order_by',
'product_category_name', 'product_category_name',
'supplier_name', 'supplier_name',
'area_name', 'area_name',
@@ -771,7 +812,8 @@ const PurchaseTable = () => {
onPageSizeChange={setPageSize} onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={handleSortingChange}
manualSorting
className={{ className={{
containerClassName: cn('p-3 mb-0'), containerClassName: cn('p-3 mb-0'),
headerColumnClassName: 'text-nowrap', headerColumnClassName: 'text-nowrap',