From 510d10270e601466c8789bfe24738e74efd6e767 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 24 Nov 2025 09:42:14 +0700 Subject: [PATCH] feat(FE-195): implement bulk approve/reject in Expense list page --- .../pages/expense/ExpensesTable.tsx | 280 +++++++++++------- 1 file changed, 177 insertions(+), 103 deletions(-) diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index fc6f6d13..01573a31 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import useSWR from 'swr'; import { CellContext, @@ -31,13 +31,14 @@ import DateInput from '@/components/input/DateInput'; import { Expense } from '@/types/api/expense'; import { ExpenseApi } from '@/services/api/expense'; -import { cn, formatCurrency } from '@/lib/helper'; +import { cn, formatCurrency, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; import { LocationApi, SupplierApi } from '@/services/api/master-data'; import { Location } from '@/types/api/master-data/location'; import { Supplier } from '@/types/api/master-data/supplier'; +import { BaseApiResponse } from '@/types/api/api-general'; const RowOptionsMenu = ({ type = 'dropdown', @@ -53,66 +54,57 @@ const RowOptionsMenu = ({ deleteClickHandler: () => void; }) => { const showEditButton = - props.row.original.approval.action !== 'REJECTED' && - props.row.original.approval.step_number !== 5 && - props.row.original.approval.action !== 'APPROVED'; - - const showDeleteButton = showEditButton; + props.row.original.latest_approval.step_number !== 5 && + (props.row.original.latest_approval.step_number === 1 || + props.row.original.latest_approval.step_number === 2 || + props.row.original.latest_approval.step_number === 3); // TODO: apply RBAC - const showApproveButton = showEditButton; - const showRejectButton = showEditButton; + const showRealizationButton = + props.row.original.latest_approval.action !== 'REJECTED' && + props.row.original.latest_approval.step_number === 3; return ( - - - {showEditButton && ( +
- )} - {/* TODO: apply RBAC */} - {showApproveButton && ( - - )} + {showEditButton && ( + + )} - {showRejectButton && ( - - )} + {showRealizationButton && ( + + )} - {showDeleteButton && ( - )} +
); }; @@ -178,6 +170,7 @@ const ExpensesTable = () => { undefined ); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [isCompleteLoading, setIsCompleteLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); @@ -187,6 +180,57 @@ const ExpensesTable = () => { parseInt(item) ); + const isAllSelectedRowLatestApprovalOnManager = useMemo(() => { + return selectedRowIds.every((rowId) => { + if (!isResponseSuccess(expenses)) return false; + + const expenseItem = expenses.data.find((item) => item.id === rowId); + + const isLatestApprovalRejected = + expenseItem?.latest_approval.action === 'REJECTED'; + + const isCurrentApprovalOnManager = + !isLatestApprovalRejected && + expenseItem?.latest_approval.step_number === 1; + + return isCurrentApprovalOnManager; + }); + }, [expenses, selectedRowIds]); + + const isAllSelectedRowLatestApprovalOnFinance = useMemo(() => { + return selectedRowIds.every((rowId) => { + if (!isResponseSuccess(expenses)) return false; + + const expenseItem = expenses.data.find((item) => item.id === rowId); + + const isLatestApprovalRejected = + expenseItem?.latest_approval.action === 'REJECTED'; + + const isCurrentApprovalOnFinance = + !isLatestApprovalRejected && + expenseItem?.latest_approval.step_number === 2; + + return isCurrentApprovalOnFinance; + }); + }, [expenses, selectedRowIds]); + + const isAllSelectedRowLatestApprovalOnRealization = useMemo(() => { + return selectedRowIds.every((rowId) => { + if (!isResponseSuccess(expenses)) return false; + + const expenseItem = expenses.data.find((item) => item.id === rowId); + + const isLatestApprovalRejected = + expenseItem?.latest_approval.action === 'REJECTED'; + + const isCurrentApprovalOnRealization = + !isLatestApprovalRejected && + expenseItem?.latest_approval.step_number === 4; + + return isCurrentApprovalOnRealization; + }); + }, [expenses, selectedRowIds]); + const expensesColumns: ColumnDef[] = [ { id: 'select', @@ -202,7 +246,8 @@ const ExpensesTable = () => { ), cell: ({ row }) => { const isCheckboxDisabled = - !row.getCanSelect() || row.original.approval.action === 'REJECTED'; + !row.getCanSelect() || + row.original.latest_approval.action === 'REJECTED'; return (
@@ -218,61 +263,52 @@ const ExpensesTable = () => { }, }, { - accessorKey: 'transaction_date', + accessorKey: 'expense_date', header: 'Tanggal Pengajuan', + cell: (props) => + props.row.original.expense_date + ? formatDate(props.row.original.expense_date, 'DD MMM YYYY') + : '-', }, { accessorKey: 'realization_date', header: 'Tanggal Realisasi', - cell: (props) => props.getValue() ?? '-', + cell: (props) => + props.row.original.realization_date + ? formatDate(props.row.original.realization_date, 'DD MMM YYYY') + : '-', }, { accessorKey: 'location', header: 'Lokasi', - cell: (props) => props.row.original.location.name ?? '-', + cell: (props) => props.row.original.location?.name ?? '-', }, { accessorFn: (row) => row.created_user.name ?? '-', header: 'Nama Pengaju', }, { - accessorFn: (row) => row.vendor.name ?? '-', + accessorFn: (row) => row.supplier.name ?? '-', header: 'Vendor', }, { - accessorKey: 'nominal', + accessorKey: 'grand_total', header: 'Nominal', cell: (props) => - props.row.original.nominal - ? `Rp${formatCurrency(props.row.original.nominal)}` - : '-', - }, - { - accessorKey: 'paid', - header: 'Sudah Bayar', - cell: (props) => - props.row.original.paid - ? `Rp${formatCurrency(props.row.original.paid)}` - : '-', - }, - { - accessorKey: 'remaining_cost', - header: 'Sisa Bayar', - cell: (props) => - props.row.original.remaining_cost - ? `Rp${formatCurrency(props.row.original.remaining_cost)}` + props.row.original.grand_total + ? formatCurrency(props.row.original.grand_total) : '-', }, { header: 'Status Pencairan', cell: (props) => ( - + ), }, { header: 'Status BOP', cell: (props) => ( - + ), }, { @@ -283,7 +319,7 @@ const ExpensesTable = () => { const currentRowRelativeIndex = currentPageRows.findIndex((r) => r.id === props.row.id) + 1; - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3; const approveClickHandler = () => { setSelectedExpense(props.row.original); @@ -314,7 +350,7 @@ const ExpensesTable = () => { return ( <> - {currentPageSize > 2 && ( + {currentPageSize > 3 && ( { )} - {currentPageSize <= 2 && ( + {currentPageSize <= 3 && ( { const tableEnableRowSelectionHandler: (row: Row) => boolean = ( row ) => { - return row.original.approval.action !== 'REJECTED'; + return ( + row.original.latest_approval.action !== 'REJECTED' && + row.original.latest_approval.step_number !== 5 + ); }; + // const bulkApproveClickHandler = () => { + // approveModal.openModal(); + // }; + + // const bulkRejectClickHandler = () => { + // rejectModal.openModal(); + // }; + const bulkApproveClickHandler = () => { approveModal.openModal(); }; @@ -371,17 +418,26 @@ const ExpensesTable = () => { const confirmationModalApproveClickHandler = async (notes: string) => { setIsApproveLoading(true); - const bulkApproveResponse = await ExpenseApi.bulkApprove( - selectedRowIds, - notes - ); + let bulkApproveResponse: BaseApiResponse | undefined = undefined; + + if (isAllSelectedRowLatestApprovalOnManager) { + bulkApproveResponse = await ExpenseApi.bulkApproveManager( + selectedRowIds, + notes + ); + } else if (isAllSelectedRowLatestApprovalOnFinance) { + bulkApproveResponse = await ExpenseApi.bulkApproveFinance( + selectedRowIds, + notes + ); + } if (isResponseSuccess(bulkApproveResponse)) { refreshExpenses(); approveModal.closeModal(); toast.success( - `Berhasil approve ${selectedRowIds.length} data transfer ke laying!` + `Berhasil approve ${selectedRowIds.length} data biaya operasional!` ); setRowSelection({}); @@ -389,7 +445,7 @@ const ExpensesTable = () => { approveModal.closeModal(); toast.error( - `Gagal approve ${selectedRowIds.length} data transfer ke laying!` + `Gagal approve ${selectedRowIds.length} data biaya operasional!` ); } @@ -399,24 +455,33 @@ const ExpensesTable = () => { const confirmationModalRejectClickHandler = async (notes: string) => { setIsRejectLoading(true); - const bulkRejectResponse = await ExpenseApi.bulkReject( - selectedRowIds, - notes - ); + let bulkRejectResponse: BaseApiResponse | undefined = undefined; + + if (isAllSelectedRowLatestApprovalOnManager) { + bulkRejectResponse = await ExpenseApi.bulkRejectManager( + selectedRowIds, + notes + ); + } else if (isAllSelectedRowLatestApprovalOnFinance) { + bulkRejectResponse = await ExpenseApi.bulkRejectFinance( + selectedRowIds, + notes + ); + } if (isResponseSuccess(bulkRejectResponse)) { refreshExpenses(); rejectModal.closeModal(); toast.success( - `Berhasil reject ${selectedRowIds.length} data transfer ke laying!` + `Berhasil reject ${selectedRowIds.length} data biaya operasional!` ); setRowSelection({}); } else { rejectModal.closeModal(); toast.error( - `Gagal reject ${selectedRowIds.length} data transfer ke laying!` + `Gagal reject ${selectedRowIds.length} data biaya operasional!` ); } @@ -506,27 +571,36 @@ const ExpensesTable = () => { {selectedRowIds.length > 0 && ( <> - {/* TODO: apply RBAC */} + +