mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-208): enhance PurchaseTable with improved state management and memoization
This commit is contained in:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user