diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx new file mode 100644 index 00000000..fa3f7d6d --- /dev/null +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -0,0 +1,650 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useFormik } from 'formik'; +import useSWR from 'swr'; +import toast from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Link from 'next/link'; +import Button from '@/components/Button'; +import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge'; +import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; +import DropFileInput from '@/components/input/DropFileInput'; +import ApprovalSteps, { + formatGroupedApprovalsToApprovalSteps, +} from '@/components/pages/ApprovalSteps'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; + +import { Expense } from '@/types/api/expense'; +import { formatCurrency, formatDate } from '@/lib/helper'; +import { + UploadRequestDocumentsFormSchema, + UploadRequestDocumentsFormValues, +} from '@/components/pages/expense/form/ExpenseRequestForm.schema'; +import { ACCEPTED_FILE_TYPE } from '@/config/constant'; +import { ExpenseApi } from '@/services/api/expense'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line'; +import { BaseApiResponse } from '@/types/api/api-general'; + +interface ExpenseRequestContentProps { + initialValues?: Expense; +} + +const ExpenseRequestContent = ({ + initialValues, +}: ExpenseRequestContentProps) => { + const router = useRouter(); + + const { data: approvalHistory, isLoading: isLoadingApprovalHistory } = useSWR( + initialValues ? [String(initialValues.id)] : null, + ([id]: string[]) => ExpenseApi.getApprovalHistory(Number(id)) + ); + + const isLatestApprovalRejected = + initialValues?.latest_approval.action === 'REJECTED'; + + const isLatestApprovalRejectedOrDone = + isLatestApprovalRejected || + initialValues?.latest_approval.step_number === 5; + + const isCurrentApprovalOnManager = + !isLatestApprovalRejected && + initialValues?.latest_approval.step_number === 1; + + const isCurrentApprovalOnFinance = + !isLatestApprovalRejected && + initialValues?.latest_approval.step_number === 2; + + const isCurrentApprovalOnRealization = + !isLatestApprovalRejected && + initialValues?.latest_approval.step_number === 4; + + const showEditButton = + initialValues?.latest_approval.step_number !== 5 && + (initialValues?.latest_approval.step_number === 1 || + initialValues?.latest_approval.step_number === 2 || + initialValues?.latest_approval.step_number === 3); + + const showRejectButton = + !isLatestApprovalRejected && + (initialValues?.latest_approval.step_number === 1 || + initialValues?.latest_approval.step_number === 2); + + const isExpenseCanBeRealized = + !isLatestApprovalRejected && + initialValues?.latest_approval.step_number === 3; + + // Modal hooks + const deleteModal = useModal(); + const completeModal = useModal(); + const approveModal = useModal(); + const rejectModal = useModal(); + + // Modal loading state + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [isCompleteLoading, setIsCompleteLoading] = useState(false); + const [isApproveLoading, setIsApproveLoading] = useState(false); + const [isRejectLoading, setIsRejectLoading] = useState(false); + + const formik = useFormik({ + initialValues: { + documents: [], + }, + validationSchema: UploadRequestDocumentsFormSchema, + onSubmit: async (values) => { + const addRequestDocumentsRes = await ExpenseApi.uploadRequestDocuments( + initialValues?.id as number, + values.documents + ); + + if (isResponseSuccess(addRequestDocumentsRes)) { + toast.success(addRequestDocumentsRes.message); + window.location.reload(); + } else { + toast.error(String(addRequestDocumentsRes?.message)); + } + }, + }); + + const deleteExpenseClickHandler = () => { + deleteModal.openModal(); + }; + + const completeExpenseClickHandler = () => { + completeModal.openModal(); + }; + + const approveClickHandler = () => { + approveModal.openModal(); + }; + + const rejectClickHandler = () => { + rejectModal.openModal(); + }; + + // Modal confirm click handler + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + try { + await ExpenseApi.delete(initialValues?.id as number); + + toast.success('Berhasil menghapus data biaya operasional!'); + router.push('/expense'); + } catch (error) { + toast.error('Gagal menghapus data biaya operasional!'); + } finally { + deleteModal.closeModal(); + setIsDeleteLoading(false); + } + }; + + const confirmationModalCompleteClickHandler = async () => { + setIsCompleteLoading(true); + + const completeRes = await ExpenseApi.complete(initialValues?.id as number); + + if (isResponseSuccess(completeRes)) { + toast.success(completeRes.message); + router.push('/expense'); + } else { + toast.error(completeRes?.message as string); + } + + completeModal.closeModal(); + setIsCompleteLoading(false); + }; + + const confirmationModalApproveClickHandler = async (notes: string) => { + setIsApproveLoading(true); + + let approveResponse: BaseApiResponse | undefined = undefined; + + if (isCurrentApprovalOnManager) { + approveResponse = await ExpenseApi.approveManager( + initialValues.id, + notes + ); + } + + if (isCurrentApprovalOnFinance) { + approveResponse = await ExpenseApi.approveFinance( + initialValues.id, + notes + ); + } + + if (isResponseSuccess(approveResponse)) { + approveModal.closeModal(); + + toast.success(approveResponse?.message); + router.push('/expense'); + } else { + approveModal.closeModal(); + + toast.error(approveResponse?.message as string); + } + + setIsApproveLoading(false); + }; + + const confirmationModalRejectClickHandler = async (notes: string) => { + setIsRejectLoading(true); + + let rejectResponse: BaseApiResponse | undefined = undefined; + + if (isCurrentApprovalOnManager) { + rejectResponse = await ExpenseApi.rejectManager(initialValues.id, notes); + } + + if (isCurrentApprovalOnFinance) { + rejectResponse = await ExpenseApi.rejectFinance(initialValues.id, notes); + } + + if (isResponseSuccess(rejectResponse)) { + rejectModal.closeModal(); + + toast.success(rejectResponse.message); + router.push('/expense'); + } else { + rejectModal.closeModal(); + + toast.error(rejectResponse?.message as string); + } + + setIsRejectLoading(false); + }; + + const requestDocumentsChangeHandler = (val: File[]) => { + formik.setFieldTouched('documents', true); + formik.setFieldValue('documents', val); + }; + + const requestDocumentsDeleteHandler = (deletedFileIdx: number) => { + const newRequestDocuments = formik.values.documents; + + newRequestDocuments?.splice(deletedFileIdx, 1); + + formik.setFieldValue('documents', newRequestDocuments); + }; + + return ( + <> +
+ {initialValues && + !isLoadingApprovalHistory && + isResponseSuccess(approvalHistory) && ( +
+ +
+ )} + +
+ {/* TODO: apply RBAC */} + +
+ {isCurrentApprovalOnManager && ( + + )} + + {isCurrentApprovalOnFinance && ( + + )} + + {isCurrentApprovalOnRealization && ( + + )} + + {showRejectButton && ( + + )} + + {isExpenseCanBeRealized && ( + + )} + +
+ {showEditButton && ( + + )} + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Nomor PO:{initialValues?.po_number ?? '-'}
Nomor Referensi:{initialValues?.reference_number}
Kategori: + {initialValues?.category === 'BOP' + ? 'Biaya Operasional' + : 'Non Biaya Operasional'} +
Lokasi:{initialValues?.location.name}
Kandang: + {initialValues?.kandangs + .map((item) => item.name) + .join(', ')} +
Vendor:{initialValues?.supplier.name}
Tanggal Transaksi: + {formatDate(initialValues?.expense_date, 'DD MMMM YYYY')} +
Tanggal Realisasi: + {initialValues?.realization_date + ? formatDate( + initialValues?.realization_date, + 'DD MMMM YYYY' + ) + : '-'} +
Nama Pengaju:{initialValues?.created_user.name}
Nominal Biaya:{formatCurrency(initialValues?.grand_total ?? 0)}
Status Pencairan: + +
Status Biaya: + +
Dokumen Pengajuan: +
+ {!initialValues?.documents || + (initialValues?.documents && + initialValues?.documents.length === 0 && + '-')} + + {initialValues?.documents && + initialValues?.documents.length > 0 && ( +
    + {initialValues?.documents.map( + (requestDocument, requestDocumentIdx) => ( +
  • + + {requestDocument.path}{' '} + + +
  • + ) + )} +
+ )} +
+ +
+ + + {formik.values.documents && + formik.values.documents.length > 0 && ( + + )} +
+
+
+
+
+

+ Rincian Pengajuan Biaya Operasional +

+ +
+ {initialValues?.kandangs.map( + (kandangExpense, kandangExpenseIdx) => { + let expenseGrandTotal = 0; + + kandangExpense.pengajuans?.forEach( + (item) => (expenseGrandTotal += item.total_price) + ); + + return ( +
+ + + + + + + + + + + + + + {kandangExpense.pengajuans?.map( + (pengajuanItem, pengajuanIdx) => ( + + + + + + + ) + )} + + + + + + + +
+ Biaya {kandangExpense.name} +
NonstockTotal KuantitasTotal BiayaCatatan
{pengajuanItem.nonstock.name}{pengajuanItem.qty} + {formatCurrency(pengajuanItem.total_price)} + + {pengajuanItem.note ?? '-'} +
+ Total Biaya Keseluruhan: + + {formatCurrency(expenseGrandTotal)} +
+
+ ); + } + )} +
+
+
+ + + + + + + + + + ); +}; + +export default ExpenseRequestContent;