From b0bd2bd8a515a5316266b623a559c13b35e3f9d3 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 24 Nov 2025 09:35:30 +0700 Subject: [PATCH] chore(FE-196,205): refactor ExpenseDetail component --- .../pages/expense/ExpenseDetail.tsx | 499 ++---------------- 1 file changed, 34 insertions(+), 465 deletions(-) diff --git a/src/components/pages/expense/ExpenseDetail.tsx b/src/components/pages/expense/ExpenseDetail.tsx index af7ec7c7..859b19ce 100644 --- a/src/components/pages/expense/ExpenseDetail.tsx +++ b/src/components/pages/expense/ExpenseDetail.tsx @@ -1,157 +1,45 @@ 'use client'; -import { useState } from 'react'; -import toast from 'react-hot-toast'; -import { useRouter } from 'next/navigation'; -import { useFormik } from 'formik'; +import { useMemo, useState } from 'react'; -import Link from 'next/link'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; -import { useModal } from '@/components/Modal'; -import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; -import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge'; -import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; -import DropFileInput from '@/components/input/DropFileInput'; +import Tabs from '@/components/Tabs'; +import ExpenseRequestContent from '@/components/pages/expense/ExpenseRequestContent'; +import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent'; import { Expense } from '@/types/api/expense'; -import { formatCurrency, formatDate } from '@/lib/helper'; -import { ExpenseApi } from '@/services/api/expense'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { ACCEPTED_FILE_TYPE } from '@/config/constant'; -import { - UploadRequestDocumentsFormSchema, - UploadRequestDocumentsFormValues, -} from '@/components/pages/expense/form/ExpenseRequestForm.schema'; interface ExpenseDetailProps { initialValues?: Expense; } const ExpenseDetail: React.FC = ({ initialValues }) => { - const router = useRouter(); + const [activeTab, setActiveTab] = useState('request'); - // Modal hooks - const deleteModal = useModal(); - const approveModal = useModal(); - const rejectModal = useModal(); + const expenseDetailTabs = useMemo(() => { + const validTabs = [ + { + id: 'request', + label: 'Pengajuan', + content: , + }, + ]; - // Modal loading state - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [isApproveLoading, setIsApproveLoading] = useState(false); - const [isRejectLoading, setIsRejectLoading] = useState(false); - - const isLatestApprovalRejectedOrDone = - initialValues?.approval && - (initialValues.approval.action === 'REJECTED' || - initialValues.approval.step_number === 5); - - const formik = useFormik({ - initialValues: { - request_documents: [], - }, - validationSchema: UploadRequestDocumentsFormSchema, - onSubmit: async (values) => { - const addRequestDocumentsRes = await ExpenseApi.uploadRequestDocuments( - initialValues?.id as number, - values.request_documents - ); - - if (isResponseSuccess(addRequestDocumentsRes)) { - toast.success(addRequestDocumentsRes.message); - window.location.reload(); - } else { - toast.error(String(addRequestDocumentsRes?.message)); - } - }, - }); - - const deleteExpenseClickHandler = () => { - deleteModal.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 confirmationModalApproveClickHandler = async (notes: string) => { - setIsApproveLoading(true); - - const approveResponse = await ExpenseApi.approve( - initialValues?.id as number, - notes - ); - - if (isResponseSuccess(approveResponse)) { - approveModal.closeModal(); - - toast.success('Berhasil approve pengajuan biaya operasional!'); - router.push('/expense'); - } else { - approveModal.closeModal(); - - toast.error('Gagal approve pengajuan biaya operasional!'); + if ( + initialValues?.latest_approval && + initialValues?.latest_approval.step_number >= 4 && + initialValues.latest_approval.action !== 'REJECTED' + ) { + validTabs.push({ + id: 'realization', + label: 'Realisasi', + content: , + }); } - setIsApproveLoading(false); - }; - - const confirmationModalRejectClickHandler = async (notes: string) => { - setIsRejectLoading(true); - - const rejectResponse = await ExpenseApi.reject( - initialValues?.id as number, - notes - ); - - if (isResponseSuccess(rejectResponse)) { - rejectModal.closeModal(); - - toast.success('Berhasil reject pengajuan biaya operasional!'); - router.push('/expense'); - } else { - rejectModal.closeModal(); - - toast.error('Gagal reject pengajuan biaya operasional!'); - } - - setIsRejectLoading(false); - }; - - const requestDocumentsChangeHandler = (val: File[]) => { - formik.setFieldTouched('request_documents', true); - formik.setFieldValue('request_documents', val); - }; - - const requestDocumentsDeleteHandler = (deletedFileIdx: number) => { - const newRequestDocuments = formik.values.request_documents; - - newRequestDocuments?.splice(deletedFileIdx, 1); - - formik.setFieldValue('request_documents', newRequestDocuments); - }; + return validTabs; + }, [initialValues]); return ( <> @@ -171,335 +59,16 @@ const ExpenseDetail: React.FC = ({ initialValues }) => { -
- {/* TODO: apply RBAC */} - {!isLatestApprovalRejectedOrDone && ( -
- - - - - - - -
- )} - - {/* TODO: add and integrate ApprovalSteps component with API */} - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Nomor PO:{initialValues?.po_number ?? '-'}
Nomor Referensi:{initialValues?.reference_number}
Lokasi:{initialValues?.location.name}
Kandang: - {initialValues?.kandangs - .map((item) => item.name) - .join(', ')} -
Vendor:{initialValues?.vendor.name}
Tanggal Transaksi: - {formatDate( - initialValues?.transaction_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?.nominal ?? 0)}
Nominal Sudah Bayar:{formatCurrency(initialValues?.paid ?? 0)}
Nominal Sisa Bayar:{formatCurrency(initialValues?.remaining_cost ?? 0)}
Status Pencairan: - -
Status Biaya: - -
Dokumen Pengajuan: -
- {initialValues?.request_documents.length === 0 && '-'} - - {initialValues?.request_documents && - initialValues?.request_documents.length > 0 && ( -
    - {initialValues?.request_documents.map( - (requestDocument, requestDocumentIdx) => ( -
  • - - {requestDocument.name}{' '} - - -
  • - ) - )} -
- )} -
- -
- - - {formik.values.request_documents && - formik.values.request_documents.length > 0 && ( - - )} -
-
-
-
- -
-

- Rincian Pengajuan Biaya Operasional -

- -
- {initialValues?.kandang_expenses.map( - (kandangExpense, kandangExpenseIdx) => { - let expenseGrandTotal = 0; - - kandangExpense.expenses.forEach( - (item) => (expenseGrandTotal += item.total_expense) - ); - - return ( -
- - - - - - - - - - - - - - {kandangExpense.expenses.map( - (expenseItem, expenseIdx) => ( - - - - - - - ) - )} - - - - - - - -
- Biaya {kandangExpense.kandang.name} -
NonstockTotal KuantitasTotal BiayaCatatan
{expenseItem.nonstock.name}{expenseItem.total_quantity} - {formatCurrency(expenseItem.total_expense)} - - {expenseItem.notes ?? '-'} -
- Total Biaya Keseluruhan: - - {formatCurrency(expenseGrandTotal)} -
-
- ); - } - )} -
-
+ - - - - - - ); };