feat(FE-208): enhance PurchaseTable with improved state management and memoization

This commit is contained in:
rstubryan
2025-11-18 13:35:58 +07:00
parent 2f2c1fca07
commit 9a650a130d
+172 -146
View File
@@ -1,6 +1,6 @@
'use client';
import { ChangeEventHandler, useState } from 'react';
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast';
@@ -10,7 +10,6 @@ import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button';
import { useModal } from '@/components/Modal';
import Modal from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import SelectInput, {
OptionType,
@@ -23,20 +22,25 @@ import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { cn, formatDate, formatCurrency } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
import { Purchase } from '@/types/api/purchase/purchase';
import { PurchaseRequestApi } from '@/services/api/purchase';
import { SupplierApi } from '@/services/api/master-data';
// ===== INTERFACES =====
interface RowOptionsMenuProps {
type: 'dropdown' | 'collapse';
props: CellContext<Purchase, unknown>;
deleteClickHandler: () => void;
}
const RowOptionsMenu = ({
type = 'dropdown',
props,
deleteClickHandler,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Purchase, unknown>;
deleteClickHandler: () => void;
}) => {
}: RowOptionsMenuProps) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
@@ -78,6 +82,18 @@ const RowOptionsMenu = ({
};
const PurchaseTable = () => {
// ===== STATE MANAGEMENT =====
const [selectedSupplier, setSelectedSupplier] = useState<OptionType | null>(
null
);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
null
);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
// ===== TABLE FILTER STATE =====
const {
state: tableFilterState,
updateFilter,
@@ -98,6 +114,17 @@ const PurchaseTable = () => {
},
});
// ===== MODAL HOOKS =====
const deleteModal = useModal();
// ===== USE SELECT HOOKS =====
const {
setInputValue: setSupplierInputValue,
options: supplierOptions,
isLoadingOptions: isLoadingSupplierOptions,
} = useSelect(SupplierApi.basePath, 'id', 'name');
// ===== API DATA FETCHING =====
const {
data: purchaseRequests,
isLoading,
@@ -107,133 +134,117 @@ const PurchaseTable = () => {
PurchaseRequestApi.getAllFetcher
);
// Modal hooks
const deleteModal = useModal();
// Supplier modal
const {
setInputValue: setSupplierInputValue,
options: supplierOptions,
isLoadingOptions: isLoadingSupplierOptions,
} = useSelect('/suppliers', 'id', 'name');
// Supplier value
const [selectedSupplier, setSelectedSupplier] = useState<OptionType | null>(
null
// ===== COMPUTED VALUES =====
const selectedRowIds = useMemo(
() => Object.keys(rowSelection).map((item) => parseInt(item)),
[rowSelection]
);
// Modal loading state
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
// ===== TABLE COLUMNS DEFINITION =====
const purchaseColumns: ColumnDef<Purchase>[] = useMemo(
() => [
{
header: 'No. PR/PO',
cell: (props) => {
const { pr_number, po_number } = props.row.original;
return po_number ? po_number : pr_number;
},
},
{
accessorKey: 'supplier',
header: 'Vendor',
cell: (props) => props.row.original.supplier.name,
},
{
accessorKey: 'created_by',
header: 'Nama Pengaju',
cell: (props) => {
const purchase = props.row.original;
return (
purchase.created_user?.name || `User ID: ${purchase.created_by}`
);
},
},
{
accessorKey: 'po_date',
header: 'Tgl. PO',
cell: (props) =>
props.row.original.po_date
? formatDate(props.row.original.po_date, 'DD-MMM-YYYY')
: '-',
},
{
accessorKey: 'due_date',
header: 'Jatuh Tempo',
cell: (props) =>
props.row.original.due_date
? formatDate(props.row.original.due_date, 'DD-MMM-YYYY')
: '-',
},
{
header: 'Aging',
cell: (props) => {
const purchase = props.row.original;
if (!purchase.po_date) return '-';
const poDate = new Date(purchase.po_date);
const today = new Date();
const diffTime = Math.abs(today.getTime() - poDate.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return `${diffDays} hari`;
},
},
{
accessorKey: 'grand_total',
header: 'Total (Rp.)',
cell: (props) => formatCurrency(props.row.original.grand_total),
},
{
header: 'Aksi',
cell: (props) => {
const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
// Selected purchase request for operations
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
null
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {
setSelectedPurchase(props.row.original);
deleteModal.openModal();
};
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
],
[]
);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const selectedRowIds = Object.keys(rowSelection).map((item) =>
parseInt(item)
);
const purchaseColumns: ColumnDef<Purchase>[] = [
{
header: 'No. PR/PO',
cell: (props) => {
const { pr_number, po_number } = props.row.original;
return po_number ? po_number : pr_number;
},
},
{
accessorKey: 'supplier',
header: 'Vendor',
cell: (props) => props.row.original.supplier.name,
},
{
accessorKey: 'created_by',
header: 'Nama Pengaju',
cell: (props) => {
const purchase = props.row.original;
return purchase.created_user?.name || `User ID: ${purchase.created_by}`;
},
},
{
accessorKey: 'po_date',
header: 'Tgl. PO',
cell: (props) =>
props.row.original.po_date
? formatDate(props.row.original.po_date, 'DD-MMM-YYYY')
: '-',
},
{
accessorKey: 'due_date',
header: 'Jatuh Tempo',
cell: (props) =>
props.row.original.due_date
? formatDate(props.row.original.due_date, 'DD-MMM-YYYY')
: '-',
},
{
header: 'Aging',
cell: (props) => {
const purchase = props.row.original;
if (!purchase.po_date) return '-';
const poDate = new Date(purchase.po_date);
const today = new Date();
const diffTime = Math.abs(today.getTime() - poDate.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return `${diffDays} hari`;
},
},
{
accessorKey: 'grand_total',
header: 'Total (Rp.)',
cell: (props) => formatCurrency(props.row.original.grand_total),
},
{
header: 'Aksi',
cell: (props) => {
const currentPageSize = props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {
setSelectedPurchase(props.row.original);
deleteModal.openModal();
};
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
];
// Modal confirm click handler
const confirmationModalDeleteClickHandler = async () => {
// ===== EVENT HANDLERS =====
const confirmationModalDeleteClickHandler = useCallback(async () => {
setIsDeleteLoading(true);
try {
@@ -241,31 +252,45 @@ const PurchaseTable = () => {
refreshPurchaseRequests();
deleteModal.closeModal();
toast.success('Berhasil menghapus data permintaan pembelian!');
} catch (error) {
} catch {
toast.error('Gagal menghapus data permintaan pembelian!');
}
setIsDeleteLoading(false);
};
}, [selectedPurchase?.id, refreshPurchaseRequests, deleteModal]);
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
updateFilter('search', e.target.value);
},
[updateFilter]
);
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
const pageSizeChangeHandler = useCallback(
(val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
},
[setPageSize]
);
setPageSize(newVal.value as number);
};
const poDateChangeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
updateFilter('poDate', e.target.value);
},
[updateFilter]
);
const poDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('poDate', e.target.value);
};
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedSupplier(val as OptionType);
updateFilter('supplier', val ? ((val as OptionType).value as string) : '');
};
const supplierChangeHandler = useCallback(
(val: OptionType | OptionType[] | null) => {
setSelectedSupplier(val as OptionType);
updateFilter(
'supplier',
val ? ((val as OptionType).value as string) : ''
);
},
[updateFilter]
);
return (
<>
@@ -405,6 +430,7 @@ const PurchaseTable = () => {
/>
</div>
{/* ===== MODAL COMPONENTS ===== */}
<ConfirmationModal
ref={deleteModal.ref}
type='error'