mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'feat/bulk-approve-expense' into 'development'
[FEAT/FE] Bulk Approve Expense See merge request mbugroup/lti-web-client!414
This commit is contained in:
@@ -7,8 +7,6 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import {
|
import {
|
||||||
CellContext,
|
CellContext,
|
||||||
@@ -21,8 +19,11 @@ import toast from 'react-hot-toast';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import TextArea from '@/components/input/TextArea';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
@@ -37,7 +38,6 @@ import ExpenseTableSkeleton from '@/components/pages/expense/skeleton/ExpenseTab
|
|||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
import { buildExpenseActionHref } from '@/lib/expense-list-navigation';
|
|
||||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -53,16 +53,34 @@ type ExpenseTableFilters = {
|
|||||||
userId: string;
|
userId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const approvalStatusOptions = [
|
||||||
|
{ value: 'HEAD_AREA', label: 'Approval Head Area' },
|
||||||
|
{ value: 'UNIT_VICE_PRESIDENT', label: 'Approval Unit Vice President' },
|
||||||
|
{ value: 'FINANCE', label: 'Approval Finance' },
|
||||||
|
{ value: 'REALISASI', label: 'Realisasi' },
|
||||||
|
{ value: 'SELESAI', label: 'Selesai' },
|
||||||
|
] as const satisfies OptionType<
|
||||||
|
'HEAD_AREA' | 'UNIT_VICE_PRESIDENT' | 'FINANCE' | 'REALISASI' | 'SELESAI'
|
||||||
|
>[];
|
||||||
|
|
||||||
|
type ApprovalStatusValue =
|
||||||
|
| 'HEAD_AREA'
|
||||||
|
| 'UNIT_VICE_PRESIDENT'
|
||||||
|
| 'FINANCE'
|
||||||
|
| 'REALISASI'
|
||||||
|
| 'SELESAI';
|
||||||
|
|
||||||
|
const isApprovalDateRequired = (status?: ApprovalStatusValue) =>
|
||||||
|
status === 'REALISASI' || status === 'SELESAI';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
deleteClickHandler,
|
deleteClickHandler,
|
||||||
returnToSearchParams,
|
|
||||||
}: {
|
}: {
|
||||||
popoverPosition: 'bottom' | 'top';
|
popoverPosition: 'bottom' | 'top';
|
||||||
props: CellContext<Expense, unknown>;
|
props: CellContext<Expense, unknown>;
|
||||||
deleteClickHandler: () => void;
|
deleteClickHandler: () => void;
|
||||||
returnToSearchParams: URLSearchParams;
|
|
||||||
}) => {
|
}) => {
|
||||||
const popoverId = `expense#${props.row.original.id}`;
|
const popoverId = `expense#${props.row.original.id}`;
|
||||||
const popoverAnchorName = `--anchor-expense#${props.row.original.id}`;
|
const popoverAnchorName = `--anchor-expense#${props.row.original.id}`;
|
||||||
@@ -105,11 +123,7 @@ const RowOptionsMenu = ({
|
|||||||
<div className='flex flex-col bg-base-100 rounded-xl'>
|
<div className='flex flex-col bg-base-100 rounded-xl'>
|
||||||
<RequirePermission permissions='lti.expense.detail'>
|
<RequirePermission permissions='lti.expense.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={buildExpenseActionHref(
|
href={`/expense/detail/?expenseId=${props.row.original.id}`}
|
||||||
'/expense/detail/',
|
|
||||||
props.row.original.id,
|
|
||||||
returnToSearchParams
|
|
||||||
)}
|
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='none'
|
color='none'
|
||||||
className='p-3 justify-start text-sm font-semibold w-full'
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
@@ -123,11 +137,7 @@ const RowOptionsMenu = ({
|
|||||||
{showEditButton && (
|
{showEditButton && (
|
||||||
<RequirePermission permissions='lti.expense.update'>
|
<RequirePermission permissions='lti.expense.update'>
|
||||||
<Button
|
<Button
|
||||||
href={buildExpenseActionHref(
|
href={`/expense/detail/edit/?expenseId=${props.row.original.id}`}
|
||||||
'/expense/detail/edit/',
|
|
||||||
props.row.original.id,
|
|
||||||
returnToSearchParams
|
|
||||||
)}
|
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='none'
|
color='none'
|
||||||
className='p-3 justify-start text-sm font-semibold w-full'
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
@@ -142,11 +152,7 @@ const RowOptionsMenu = ({
|
|||||||
{showRealizationButton && (
|
{showRealizationButton && (
|
||||||
<RequirePermission permissions='lti.expense.create.realization'>
|
<RequirePermission permissions='lti.expense.create.realization'>
|
||||||
<Button
|
<Button
|
||||||
href={buildExpenseActionHref(
|
href={`/expense/realization/?expenseId=${props.row.original.id}`}
|
||||||
'/expense/realization/',
|
|
||||||
props.row.original.id,
|
|
||||||
returnToSearchParams
|
|
||||||
)}
|
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='none'
|
color='none'
|
||||||
className='p-3 justify-start text-sm font-semibold w-full'
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
@@ -184,11 +190,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ExpensesTable = () => {
|
const ExpensesTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -197,9 +198,9 @@ const ExpensesTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter<ExpenseTableFilters>({
|
} = useTableFilter<ExpenseTableFilters>({
|
||||||
initial: {
|
initial: {
|
||||||
page: Number(searchParams.get('page')) || 1,
|
page: 1,
|
||||||
pageSize: Number(searchParams.get('limit')) || 10,
|
pageSize: 10,
|
||||||
search: searchValue,
|
search: '',
|
||||||
nameSort: '',
|
nameSort: '',
|
||||||
transactionDate: '',
|
transactionDate: '',
|
||||||
realizationDate: '',
|
realizationDate: '',
|
||||||
@@ -217,6 +218,9 @@ const ExpensesTable = () => {
|
|||||||
vendorId: 'vendor_id',
|
vendorId: 'vendor_id',
|
||||||
userId: 'user_id',
|
userId: 'user_id',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
persist: true,
|
||||||
|
storeName: 'expense-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -228,57 +232,10 @@ const ExpensesTable = () => {
|
|||||||
ExpenseApi.getAllFetcher
|
ExpenseApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const syncPaginationToUrl = useCallback(
|
|
||||||
(page: number, pageSize: number) => {
|
|
||||||
const nextQueryString = new URLSearchParams({
|
|
||||||
page: String(page),
|
|
||||||
limit: String(pageSize),
|
|
||||||
}).toString();
|
|
||||||
|
|
||||||
router.replace(
|
|
||||||
nextQueryString ? `${pathname}?${nextQueryString}` : pathname,
|
|
||||||
{
|
|
||||||
scroll: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[pathname, router]
|
|
||||||
);
|
|
||||||
|
|
||||||
const pageChangeHandler = useCallback(
|
|
||||||
(page: number) => {
|
|
||||||
setPage(page);
|
|
||||||
syncPaginationToUrl(page, tableFilterState.pageSize);
|
|
||||||
},
|
|
||||||
[setPage, syncPaginationToUrl, tableFilterState.pageSize]
|
|
||||||
);
|
|
||||||
|
|
||||||
const pageSizeChangeHandler = useCallback(
|
|
||||||
(pageSize: number) => {
|
|
||||||
setPageSize(pageSize);
|
|
||||||
syncPaginationToUrl(1, pageSize);
|
|
||||||
},
|
|
||||||
[setPageSize, syncPaginationToUrl]
|
|
||||||
);
|
|
||||||
|
|
||||||
const returnToSearchParams = useMemo(() => {
|
|
||||||
const returnToParams = new URLSearchParams();
|
|
||||||
const queryString = new URLSearchParams({
|
|
||||||
page: String(tableFilterState.page),
|
|
||||||
limit: String(tableFilterState.pageSize),
|
|
||||||
}).toString();
|
|
||||||
|
|
||||||
returnToParams.set(
|
|
||||||
'returnTo',
|
|
||||||
queryString ? `${pathname}?${queryString}` : pathname
|
|
||||||
);
|
|
||||||
|
|
||||||
return returnToParams;
|
|
||||||
}, [pathname, tableFilterState.page, tableFilterState.pageSize]);
|
|
||||||
|
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
|
const bulkApproveFormModal = useModal();
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
@@ -290,6 +247,10 @@ const ExpensesTable = () => {
|
|||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
const [, setApprovalNotes] = useState('');
|
const [, setApprovalNotes] = useState('');
|
||||||
|
const [bulkApprovalStatus, setBulkApprovalStatus] =
|
||||||
|
useState<OptionType<ApprovalStatusValue> | null>(null);
|
||||||
|
const [bulkApprovalDate, setBulkApprovalDate] = useState('');
|
||||||
|
const [bulkApprovalNotes, setBulkApprovalNotes] = useState('');
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
@@ -456,7 +417,6 @@ const ExpensesTable = () => {
|
|||||||
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
props={props}
|
props={props}
|
||||||
deleteClickHandler={deleteClickHandler}
|
deleteClickHandler={deleteClickHandler}
|
||||||
returnToSearchParams={returnToSearchParams}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -474,17 +434,45 @@ const ExpensesTable = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// const bulkApproveClickHandler = () => {
|
const resetBulkApproveForm = useCallback(() => {
|
||||||
// approveModal.openModal();
|
setBulkApprovalStatus(null);
|
||||||
// };
|
setBulkApprovalDate('');
|
||||||
|
setBulkApprovalNotes('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
// const bulkRejectClickHandler = () => {
|
const openBulkApproveForm = useCallback(
|
||||||
// rejectModal.openModal();
|
(presetStatus?: ApprovalStatusValue) => {
|
||||||
// };
|
resetBulkApproveForm();
|
||||||
|
|
||||||
|
if (presetStatus) {
|
||||||
|
const selectedStatus = approvalStatusOptions.find(
|
||||||
|
(option) => option.value === presetStatus
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedStatus) {
|
||||||
|
setBulkApprovalStatus(selectedStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkApproveFormModal.openModal();
|
||||||
|
},
|
||||||
|
[bulkApproveFormModal, resetBulkApproveForm]
|
||||||
|
);
|
||||||
|
|
||||||
const bulkApproveClickHandler = () => {
|
const bulkApproveClickHandler = () => {
|
||||||
setApprovalNotes('');
|
openBulkApproveForm();
|
||||||
approveModal.openModal();
|
};
|
||||||
|
|
||||||
|
const bulkApproveHeadAreaClickHandler = () => {
|
||||||
|
openBulkApproveForm('HEAD_AREA');
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkApproveUnitVicePresidentClickHandler = () => {
|
||||||
|
openBulkApproveForm('UNIT_VICE_PRESIDENT');
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkApproveFinanceClickHandler = () => {
|
||||||
|
openBulkApproveForm('FINANCE');
|
||||||
};
|
};
|
||||||
|
|
||||||
const bulkRejectClickHandler = () => {
|
const bulkRejectClickHandler = () => {
|
||||||
@@ -492,6 +480,18 @@ const ExpensesTable = () => {
|
|||||||
rejectModal.openModal();
|
rejectModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bulkApprovalDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
||||||
|
e
|
||||||
|
) => {
|
||||||
|
setBulkApprovalDate(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkApprovalNotesChangeHandler: ChangeEventHandler<
|
||||||
|
HTMLTextAreaElement
|
||||||
|
> = (e) => {
|
||||||
|
setBulkApprovalNotes(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
@@ -554,6 +554,48 @@ const ExpensesTable = () => {
|
|||||||
setIsApproveLoading(false);
|
setIsApproveLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bulkApproveSubmitHandler = async () => {
|
||||||
|
if (!bulkApprovalStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isApprovalDateRequired(bulkApprovalStatus.value) && !bulkApprovalDate) {
|
||||||
|
toast.error('Tanggal realisasi wajib diisi.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bulkApprovalNotes.trim()) {
|
||||||
|
toast.error('Catatan wajib diisi.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsApproveLoading(true);
|
||||||
|
|
||||||
|
const bulkApproveResponse = await ExpenseApi.bulkApprovals(
|
||||||
|
selectedRowIds,
|
||||||
|
bulkApprovalStatus.value,
|
||||||
|
isApprovalDateRequired(bulkApprovalStatus.value) ? bulkApprovalDate : '',
|
||||||
|
bulkApprovalNotes
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(bulkApproveResponse)) {
|
||||||
|
refreshExpenses();
|
||||||
|
bulkApproveFormModal.closeModal();
|
||||||
|
toast.success(
|
||||||
|
`Berhasil approve ${selectedRowIds.length} data biaya operasional!`
|
||||||
|
);
|
||||||
|
resetBulkApproveForm();
|
||||||
|
setRowSelection({});
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
bulkApproveResponse?.message ??
|
||||||
|
`Gagal approve ${selectedRowIds.length} data biaya operasional!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsApproveLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
const confirmationModalRejectClickHandler = async (notes: string) => {
|
const confirmationModalRejectClickHandler = async (notes: string) => {
|
||||||
setIsRejectLoading(true);
|
setIsRejectLoading(true);
|
||||||
|
|
||||||
@@ -596,12 +638,7 @@ const ExpensesTable = () => {
|
|||||||
setIsRejectLoading(false);
|
setIsRejectLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('expense-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -645,7 +682,7 @@ const ExpensesTable = () => {
|
|||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
<div className='w-fit flex flex-col gap-3 flex-wrap'>
|
||||||
<RequirePermission permissions='lti.expense.create'>
|
<RequirePermission permissions='lti.expense.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense/add'
|
href='/expense/add'
|
||||||
@@ -658,14 +695,36 @@ const ExpensesTable = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
|
||||||
{selectedRowIds.length > 0 && (
|
{selectedRowIds.length > 0 && (
|
||||||
<>
|
<div className='flex flex-row gap-3 flex-wrap'>
|
||||||
<hr className='w-px h-full border-none bg-base-content/10 sm:block hidden' />
|
<RequirePermission
|
||||||
|
permissions={[
|
||||||
|
'lti.expense.approve.head_area',
|
||||||
|
'lti.expense.approve.unit_vice_president',
|
||||||
|
'lti.expense.approve.finance',
|
||||||
|
'lti.expense.create.realization',
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
onClick={bulkApproveClickHandler}
|
||||||
|
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='lucide-lab:farm'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-success'
|
||||||
|
/>
|
||||||
|
Bulk Approve
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
<RequirePermission permissions='lti.expense.approve.head_area'>
|
<RequirePermission permissions='lti.expense.approve.head_area'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='none'
|
color='none'
|
||||||
onClick={bulkApproveClickHandler}
|
onClick={bulkApproveHeadAreaClickHandler}
|
||||||
disabled={!isAllSelectedRowLatestApprovalOnHeadArea}
|
disabled={!isAllSelectedRowLatestApprovalOnHeadArea}
|
||||||
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
>
|
>
|
||||||
@@ -683,7 +742,7 @@ const ExpensesTable = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='none'
|
color='none'
|
||||||
onClick={bulkApproveClickHandler}
|
onClick={bulkApproveUnitVicePresidentClickHandler}
|
||||||
disabled={
|
disabled={
|
||||||
!isAllSelectedRowLatestApprovalOnUnitVicePresident
|
!isAllSelectedRowLatestApprovalOnUnitVicePresident
|
||||||
}
|
}
|
||||||
@@ -703,7 +762,7 @@ const ExpensesTable = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='none'
|
color='none'
|
||||||
onClick={bulkApproveClickHandler}
|
onClick={bulkApproveFinanceClickHandler}
|
||||||
disabled={!isAllSelectedRowLatestApprovalOnFinance}
|
disabled={!isAllSelectedRowLatestApprovalOnFinance}
|
||||||
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
>
|
>
|
||||||
@@ -744,12 +803,12 @@ const ExpensesTable = () => {
|
|||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search and Filter */}
|
{/* Search and Filter */}
|
||||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-start gap-3 flex-wrap'>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
placeholder='Search'
|
placeholder='Search'
|
||||||
@@ -814,8 +873,8 @@ const ExpensesTable = () => {
|
|||||||
totalItems={
|
totalItems={
|
||||||
isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0
|
isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0
|
||||||
}
|
}
|
||||||
onPageChange={pageChangeHandler}
|
onPageChange={setPage}
|
||||||
onPageSizeChange={pageSizeChangeHandler}
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
sorting={sorting}
|
sorting={sorting}
|
||||||
setSorting={setSorting}
|
setSorting={setSorting}
|
||||||
@@ -884,6 +943,100 @@ const ExpensesTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
ref={bulkApproveFormModal.ref}
|
||||||
|
className={{
|
||||||
|
modalBox: 'max-w-lg rounded-lg p-0',
|
||||||
|
}}
|
||||||
|
closeOnBackdrop
|
||||||
|
>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
||||||
|
<h4 className='text-sm font-semibold text-base-content'>
|
||||||
|
Bulk Approve Expense
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
bulkApproveFormModal.closeModal();
|
||||||
|
resetBulkApproveForm();
|
||||||
|
}}
|
||||||
|
className='p-1'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:close' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
|
<SelectInput
|
||||||
|
label='Status Approval'
|
||||||
|
options={approvalStatusOptions as OptionType[]}
|
||||||
|
value={bulkApprovalStatus}
|
||||||
|
onChange={(val) => {
|
||||||
|
const nextValue = val as OptionType<ApprovalStatusValue> | null;
|
||||||
|
setBulkApprovalStatus(nextValue);
|
||||||
|
|
||||||
|
if (!isApprovalDateRequired(nextValue?.value)) {
|
||||||
|
setBulkApprovalDate('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder='Pilih status approval'
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isApprovalDateRequired(bulkApprovalStatus?.value) && (
|
||||||
|
<DateInput
|
||||||
|
name='bulk_approval_date'
|
||||||
|
label='Tanggal Realisasi'
|
||||||
|
value={bulkApprovalDate}
|
||||||
|
onChange={bulkApprovalDateChangeHandler}
|
||||||
|
isNestedModal
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
name='bulk_approval_notes'
|
||||||
|
label='Catatan'
|
||||||
|
value={bulkApprovalNotes}
|
||||||
|
onChange={bulkApprovalNotesChangeHandler}
|
||||||
|
placeholder='Masukkan catatan approval...'
|
||||||
|
rows={4}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
bulkApproveFormModal.closeModal();
|
||||||
|
resetBulkApproveForm();
|
||||||
|
}}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='success'
|
||||||
|
onClick={bulkApproveSubmitHandler}
|
||||||
|
isLoading={isApproveLoading}
|
||||||
|
disabled={
|
||||||
|
!bulkApprovalStatus ||
|
||||||
|
!bulkApprovalNotes.trim() ||
|
||||||
|
(isApprovalDateRequired(bulkApprovalStatus.value) &&
|
||||||
|
!bulkApprovalDate)
|
||||||
|
}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<ExpensesFilterModal
|
<ExpensesFilterModal
|
||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
onSubmit={handleFilterSubmit}
|
onSubmit={handleFilterSubmit}
|
||||||
|
|||||||
@@ -330,6 +330,41 @@ export class ExpenseApiService extends BaseApiService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async bulkApprovals(
|
||||||
|
ids: number[],
|
||||||
|
status:
|
||||||
|
| 'HEAD_AREA'
|
||||||
|
| 'UNIT_VICE_PRESIDENT'
|
||||||
|
| 'FINANCE'
|
||||||
|
| 'REALISASI'
|
||||||
|
| 'SELESAI',
|
||||||
|
date: string, // YYYY-MM-DD
|
||||||
|
notes: string
|
||||||
|
): Promise<BaseApiResponse<Expense> | undefined> {
|
||||||
|
try {
|
||||||
|
const bulkApproveRes = await httpClient<BaseApiResponse<Expense>>(
|
||||||
|
`${this.basePath}/approvals/bulk`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
approvable_ids: ids,
|
||||||
|
status: status,
|
||||||
|
date: date,
|
||||||
|
notes: notes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return bulkApproveRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<Expense>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async rejectHeadArea(
|
async rejectHeadArea(
|
||||||
id: number,
|
id: number,
|
||||||
notes?: string
|
notes?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user