mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +00:00
feat: implement paid off expense feature
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { useSWRConfig } from 'swr';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton';
|
import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
@@ -26,7 +28,7 @@ import {
|
|||||||
UploadRequestDocumentsFormSchema,
|
UploadRequestDocumentsFormSchema,
|
||||||
UploadRequestDocumentsFormValues,
|
UploadRequestDocumentsFormValues,
|
||||||
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
@@ -46,6 +48,11 @@ const ExpenseRequestContent = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const returnTo = getExpenseListReturnTo(searchParams);
|
const returnTo = getExpenseListReturnTo(searchParams);
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
|
const refreshExpense = () => {
|
||||||
|
mutate((key) => Array.isArray(key) && key[0] === 'expense-detail');
|
||||||
|
};
|
||||||
|
|
||||||
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
|
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
|
||||||
useApprovalSteps({
|
useApprovalSteps({
|
||||||
@@ -95,17 +102,24 @@ const ExpenseRequestContent = ({
|
|||||||
!isLatestApprovalRejected &&
|
!isLatestApprovalRejected &&
|
||||||
initialValues?.latest_approval.step_number === 4;
|
initialValues?.latest_approval.step_number === 4;
|
||||||
|
|
||||||
|
const isExpensePaidOff = initialValues?.is_paid;
|
||||||
|
|
||||||
|
const showPaidOffButton =
|
||||||
|
!isExpensePaidOff && (initialValues?.latest_approval.step_number ?? 0) > 4;
|
||||||
|
|
||||||
// Modal hooks
|
// Modal hooks
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const completeModal = useModal();
|
const completeModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
|
const paidOffModal = useModal();
|
||||||
|
|
||||||
// Modal loading state
|
// Modal loading state
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
|
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
const [isPaidOffLoading, setIsPaidOffLoading] = useState(false);
|
||||||
const [, setApprovalNotes] = useState('');
|
const [, setApprovalNotes] = useState('');
|
||||||
|
|
||||||
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
||||||
@@ -146,7 +160,31 @@ const ExpenseRequestContent = ({
|
|||||||
rejectModal.openModal();
|
rejectModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const paidOffClickHandler = () => {
|
||||||
|
paidOffModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
// Modal confirm click handler
|
// Modal confirm click handler
|
||||||
|
const confirmationModalPaidOffClickHandler = async () => {
|
||||||
|
setIsPaidOffLoading(true);
|
||||||
|
|
||||||
|
const paidOffResponse = await ExpenseApi.setExpensePaidOff(
|
||||||
|
initialValues?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(paidOffResponse)) {
|
||||||
|
toast.success('Berhasil menandai biaya operasional sebagai lunas!');
|
||||||
|
refreshExpense();
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
'Gagal menandai biaya operasional sebagai lunas!: ' +
|
||||||
|
paidOffResponse?.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
paidOffModal.closeModal();
|
||||||
|
setIsPaidOffLoading(false);
|
||||||
|
};
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
@@ -388,6 +426,24 @@ const ExpenseRequestContent = ({
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showPaidOffButton && (
|
||||||
|
<RequirePermission permissions='lti.expense.create.realization'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='success'
|
||||||
|
onClick={paidOffClickHandler}
|
||||||
|
className='w-full sm:w-fit'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:check-circle-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
Tandai Lunas
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
||||||
{showEditButton && (
|
{showEditButton && (
|
||||||
<RequirePermission permissions='lti.expense.update'>
|
<RequirePermission permissions='lti.expense.update'>
|
||||||
@@ -533,6 +589,19 @@ const ExpenseRequestContent = ({
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Status Lunas</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>
|
||||||
|
<StatusBadge
|
||||||
|
color={initialValues?.is_paid ? 'primary' : 'warning'}
|
||||||
|
text={initialValues?.is_paid ? 'Lunas' : 'Belum Lunas'}
|
||||||
|
className={{
|
||||||
|
badge: 'w-fit whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Dokumen Pengajuan</th>
|
<th>Dokumen Pengajuan</th>
|
||||||
<th>:</th>
|
<th>:</th>
|
||||||
@@ -752,6 +821,21 @@ const ExpenseRequestContent = ({
|
|||||||
onClick: confirmationModalRejectClickHandler,
|
onClick: confirmationModalRejectClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={paidOffModal.ref}
|
||||||
|
type='success'
|
||||||
|
text='Apakah anda yakin ingin menandai biaya operasional ini sebagai lunas?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'success',
|
||||||
|
isLoading: isPaidOffLoading,
|
||||||
|
onClick: confirmationModalPaidOffClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import ButtonFilter from '@/components/helper/ButtonFilter';
|
|||||||
import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal';
|
import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal';
|
||||||
import ExpenseTableSkeleton from '@/components/pages/expense/skeleton/ExpenseTableSkeleton';
|
import ExpenseTableSkeleton from '@/components/pages/expense/skeleton/ExpenseTableSkeleton';
|
||||||
import Dropdown from '@/components/dropdown/Dropdown';
|
import Dropdown from '@/components/dropdown/Dropdown';
|
||||||
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
@@ -87,10 +88,12 @@ const RowOptionsMenu = ({
|
|||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
deleteClickHandler,
|
deleteClickHandler,
|
||||||
|
paidOffClickHandler,
|
||||||
}: {
|
}: {
|
||||||
popoverPosition: 'bottom' | 'top';
|
popoverPosition: 'bottom' | 'top';
|
||||||
props: CellContext<Expense, unknown>;
|
props: CellContext<Expense, unknown>;
|
||||||
deleteClickHandler: () => void;
|
deleteClickHandler: () => void;
|
||||||
|
paidOffClickHandler: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
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}`;
|
||||||
@@ -112,6 +115,11 @@ const RowOptionsMenu = ({
|
|||||||
props.row.original.latest_approval.step_number === 4
|
props.row.original.latest_approval.step_number === 4
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
|
const showPaidOffButton = props.row.original.latest_approval
|
||||||
|
? props.row.original.latest_approval.step_number > 4 &&
|
||||||
|
!props.row.original.is_paid
|
||||||
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
@@ -179,6 +187,28 @@ const RowOptionsMenu = ({
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showPaidOffButton && (
|
||||||
|
<RequirePermission permissions='lti.expense.create.realization'>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
paidOffClickHandler();
|
||||||
|
closePopover();
|
||||||
|
}}
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:check-circle-outline'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-success'
|
||||||
|
/>
|
||||||
|
Tandai Lunas
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
<RequirePermission permissions='lti.expense.delete'>
|
<RequirePermission permissions='lti.expense.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -264,6 +294,7 @@ const ExpensesTable = () => {
|
|||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
|
const paidOffModal = useModal();
|
||||||
const bulkApproveFormModal = useModal();
|
const bulkApproveFormModal = useModal();
|
||||||
const exportProgressInputModal = useModal();
|
const exportProgressInputModal = useModal();
|
||||||
|
|
||||||
@@ -276,6 +307,7 @@ const ExpensesTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
const [isPaidOffLoading, setIsPaidOffLoading] = useState(false);
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
||||||
@@ -432,6 +464,20 @@ const ExpensesTable = () => {
|
|||||||
<ExpenseStatusBadge approval={props.row.original.latest_approval} />
|
<ExpenseStatusBadge approval={props.row.original.latest_approval} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: 'Status Lunas',
|
||||||
|
cell: (props) => {
|
||||||
|
return (
|
||||||
|
<StatusBadge
|
||||||
|
color={props.row.original.is_paid ? 'primary' : 'warning'}
|
||||||
|
text={props.row.original.is_paid ? 'Lunas' : 'Belum Lunas'}
|
||||||
|
className={{
|
||||||
|
badge: 'w-fit whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
@@ -447,11 +493,17 @@ const ExpensesTable = () => {
|
|||||||
deleteModal.openModal();
|
deleteModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const paidOffClickHandler = () => {
|
||||||
|
setSelectedExpense(props.row.original);
|
||||||
|
paidOffModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenu
|
<RowOptionsMenu
|
||||||
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
props={props}
|
props={props}
|
||||||
deleteClickHandler={deleteClickHandler}
|
deleteClickHandler={deleteClickHandler}
|
||||||
|
paidOffClickHandler={paidOffClickHandler}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -593,6 +645,29 @@ const ExpensesTable = () => {
|
|||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmationModalPaidOffClickHandler = async () => {
|
||||||
|
setIsPaidOffLoading(true);
|
||||||
|
|
||||||
|
const paidOffResponse = await ExpenseApi.setExpensePaidOff(
|
||||||
|
selectedExpense?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(paidOffResponse)) {
|
||||||
|
refreshExpenses();
|
||||||
|
paidOffModal.closeModal();
|
||||||
|
toast.success('Berhasil menandai biaya operasional sebagai lunas!');
|
||||||
|
refreshExpenses();
|
||||||
|
} else {
|
||||||
|
paidOffModal.closeModal();
|
||||||
|
toast.error(
|
||||||
|
'Gagal menandai biaya operasional sebagai lunas!: ' +
|
||||||
|
paidOffResponse?.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsPaidOffLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
const confirmationModalApproveClickHandler = async (notes: string) => {
|
const confirmationModalApproveClickHandler = async (notes: string) => {
|
||||||
setIsApproveLoading(true);
|
setIsApproveLoading(true);
|
||||||
|
|
||||||
@@ -1105,6 +1180,21 @@ const ExpensesTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={paidOffModal.ref}
|
||||||
|
type='success'
|
||||||
|
text='Apakah anda yakin ingin menandai biaya operasional ini sebagai lunas?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'success',
|
||||||
|
isLoading: isPaidOffLoading,
|
||||||
|
onClick: confirmationModalPaidOffClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConfirmationModalWithNotes
|
<ConfirmationModalWithNotes
|
||||||
ref={approveModal.ref}
|
ref={approveModal.ref}
|
||||||
type='success'
|
type='success'
|
||||||
|
|||||||
Reference in New Issue
Block a user