mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
chore(FE-196,205): refactor ExpenseDetail component
This commit is contained in:
@@ -1,157 +1,45 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useFormik } from 'formik';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import Tabs from '@/components/Tabs';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ExpenseRequestContent from '@/components/pages/expense/ExpenseRequestContent';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent';
|
||||||
import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge';
|
|
||||||
import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
|
|
||||||
import DropFileInput from '@/components/input/DropFileInput';
|
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
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 {
|
interface ExpenseDetailProps {
|
||||||
initialValues?: Expense;
|
initialValues?: Expense;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
||||||
const router = useRouter();
|
const [activeTab, setActiveTab] = useState<string>('request');
|
||||||
|
|
||||||
// Modal hooks
|
const expenseDetailTabs = useMemo(() => {
|
||||||
const deleteModal = useModal();
|
const validTabs = [
|
||||||
const approveModal = useModal();
|
{
|
||||||
const rejectModal = useModal();
|
id: 'request',
|
||||||
|
label: 'Pengajuan',
|
||||||
|
content: <ExpenseRequestContent initialValues={initialValues} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Modal loading state
|
if (
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
initialValues?.latest_approval &&
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
initialValues?.latest_approval.step_number >= 4 &&
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
initialValues.latest_approval.action !== 'REJECTED'
|
||||||
|
) {
|
||||||
const isLatestApprovalRejectedOrDone =
|
validTabs.push({
|
||||||
initialValues?.approval &&
|
id: 'realization',
|
||||||
(initialValues.approval.action === 'REJECTED' ||
|
label: 'Realisasi',
|
||||||
initialValues.approval.step_number === 5);
|
content: <ExpenseRealizationContent initialValues={initialValues} />,
|
||||||
|
});
|
||||||
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
|
||||||
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!');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsApproveLoading(false);
|
return validTabs;
|
||||||
};
|
}, [initialValues]);
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -171,335 +59,16 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
|||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className='w-full mt-4 flex flex-col gap-4'>
|
<Tabs
|
||||||
{/* TODO: apply RBAC */}
|
activeTabId={activeTab}
|
||||||
{!isLatestApprovalRejectedOrDone && (
|
onTabChange={setActiveTab}
|
||||||
<div className='w-full max-w-3xl mx-auto flex flex-row justify-end gap-2'>
|
tabs={expenseDetailTabs}
|
||||||
<Button
|
variant='lifted'
|
||||||
variant='outline'
|
className={{
|
||||||
color='success'
|
wrapper: 'max-w-5xl mx-auto mt-4',
|
||||||
onClick={approveClickHandler}
|
}}
|
||||||
className='w-full sm:w-fit'
|
/>
|
||||||
>
|
|
||||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
|
||||||
Approve
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='error'
|
|
||||||
onClick={rejectClickHandler}
|
|
||||||
className='w-full sm:w-fit'
|
|
||||||
>
|
|
||||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
|
||||||
Reject
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
color='warning'
|
|
||||||
href={`/expense/detail/edit/?expenseId=${initialValues?.id}`}
|
|
||||||
className='px-4 ml-2'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
color='error'
|
|
||||||
onClick={deleteExpenseClickHandler}
|
|
||||||
className='px-4'
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:delete-outline-rounded'
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
className='justify-start text-sm'
|
|
||||||
/>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* TODO: add and integrate ApprovalSteps component with API */}
|
|
||||||
|
|
||||||
<div className='overflow-x-auto w-full max-w-3xl mx-auto'>
|
|
||||||
<table className='table table-sm table-zebra'>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th>Nomor PO</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>{initialValues?.po_number ?? '-'}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Nomor Referensi</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>{initialValues?.reference_number}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Lokasi</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>{initialValues?.location.name}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Kandang</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>
|
|
||||||
{initialValues?.kandangs
|
|
||||||
.map((item) => item.name)
|
|
||||||
.join(', ')}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Vendor</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>{initialValues?.vendor.name}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Tanggal Transaksi</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>
|
|
||||||
{formatDate(
|
|
||||||
initialValues?.transaction_date,
|
|
||||||
'DD MMMM YYYY'
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Tanggal Realisasi</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>
|
|
||||||
{initialValues?.realization_date
|
|
||||||
? formatDate(
|
|
||||||
initialValues?.realization_date,
|
|
||||||
'DD MMMM YYYY'
|
|
||||||
)
|
|
||||||
: '-'}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Nama Pengaju</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>{initialValues?.created_user.name}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Nominal Biaya</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>{formatCurrency(initialValues?.nominal ?? 0)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Nominal Sudah Bayar</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>{formatCurrency(initialValues?.paid ?? 0)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Nominal Sisa Bayar</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>{formatCurrency(initialValues?.remaining_cost ?? 0)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Status Pencairan</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>
|
|
||||||
<RealizationStatusBadge
|
|
||||||
approval={initialValues?.approval}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Status Biaya</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>
|
|
||||||
<ExpenseStatusBadge approval={initialValues?.approval} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Dokumen Pengajuan</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>
|
|
||||||
<div>
|
|
||||||
{initialValues?.request_documents.length === 0 && '-'}
|
|
||||||
|
|
||||||
{initialValues?.request_documents &&
|
|
||||||
initialValues?.request_documents.length > 0 && (
|
|
||||||
<ul className='list-disc'>
|
|
||||||
{initialValues?.request_documents.map(
|
|
||||||
(requestDocument, requestDocumentIdx) => (
|
|
||||||
<li key={requestDocumentIdx}>
|
|
||||||
<Link
|
|
||||||
href={requestDocument.url}
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
className='text-blue-500 underline'
|
|
||||||
>
|
|
||||||
{requestDocument.name}{' '}
|
|
||||||
<Icon
|
|
||||||
icon='cuida:open-in-new-tab-outline'
|
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
className='inline'
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<DropFileInput
|
|
||||||
name='request_documents'
|
|
||||||
values={formik.values.request_documents}
|
|
||||||
onChange={requestDocumentsChangeHandler}
|
|
||||||
onDelete={requestDocumentsDeleteHandler}
|
|
||||||
accept={{
|
|
||||||
...ACCEPTED_FILE_TYPE.PDF,
|
|
||||||
...ACCEPTED_FILE_TYPE.IMAGE,
|
|
||||||
}}
|
|
||||||
maxFiles={10}
|
|
||||||
className={{
|
|
||||||
wrapper: 'mt-2',
|
|
||||||
inputWrapper: 'flex items-center',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{formik.values.request_documents &&
|
|
||||||
formik.values.request_documents.length > 0 && (
|
|
||||||
<Button
|
|
||||||
onClick={formik.submitForm}
|
|
||||||
disabled={formik.isSubmitting}
|
|
||||||
isLoading={formik.isSubmitting}
|
|
||||||
className='w-fit self-end'
|
|
||||||
>
|
|
||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
|
||||||
Tambah
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
|
||||||
<h2 className='font-bold text-xl text-center'>
|
|
||||||
Rincian Pengajuan Biaya Operasional
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className='w-full mt-2 flex flex-col gap-4'>
|
|
||||||
{initialValues?.kandang_expenses.map(
|
|
||||||
(kandangExpense, kandangExpenseIdx) => {
|
|
||||||
let expenseGrandTotal = 0;
|
|
||||||
|
|
||||||
kandangExpense.expenses.forEach(
|
|
||||||
(item) => (expenseGrandTotal += item.total_expense)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={kandangExpenseIdx}
|
|
||||||
className='overflow-x-auto w-full mx-auto'
|
|
||||||
>
|
|
||||||
<table className='table table-sm table-zebra'>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
colSpan={5}
|
|
||||||
className='font-bold text-center text-base-content text-lg'
|
|
||||||
>
|
|
||||||
Biaya {kandangExpense.kandang.name}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Nonstock</th>
|
|
||||||
<th>Total Kuantitas</th>
|
|
||||||
<th>Total Biaya</th>
|
|
||||||
<th>Catatan</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{kandangExpense.expenses.map(
|
|
||||||
(expenseItem, expenseIdx) => (
|
|
||||||
<tr key={expenseIdx}>
|
|
||||||
<td>{expenseItem.nonstock.name}</td>
|
|
||||||
<td>{expenseItem.total_quantity}</td>
|
|
||||||
<td>
|
|
||||||
{formatCurrency(expenseItem.total_expense)}
|
|
||||||
</td>
|
|
||||||
<td className='w-xs'>
|
|
||||||
{expenseItem.notes ?? '-'}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr className='border-y'>
|
|
||||||
<th colSpan={2} className='text-right'>
|
|
||||||
Total Biaya Keseluruhan:
|
|
||||||
</th>
|
|
||||||
<th colSpan={2}>
|
|
||||||
{formatCurrency(expenseGrandTotal)}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<ConfirmationModal
|
|
||||||
ref={deleteModal.ref}
|
|
||||||
type='error'
|
|
||||||
text='Apakah anda yakin ingin menghapus data transfer ke laying ini?'
|
|
||||||
secondaryButton={{
|
|
||||||
text: 'Tidak',
|
|
||||||
}}
|
|
||||||
primaryButton={{
|
|
||||||
text: 'Ya',
|
|
||||||
color: 'error',
|
|
||||||
isLoading: isDeleteLoading,
|
|
||||||
onClick: confirmationModalDeleteClickHandler,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmationModalWithNotes
|
|
||||||
ref={approveModal.ref}
|
|
||||||
type='success'
|
|
||||||
text='Apakah anda yakin ingin approve data transfer ke laying ini?'
|
|
||||||
secondaryButton={{
|
|
||||||
text: 'Tidak',
|
|
||||||
}}
|
|
||||||
primaryButton={{
|
|
||||||
text: 'Ya',
|
|
||||||
color: 'success',
|
|
||||||
isLoading: isApproveLoading,
|
|
||||||
onClick: confirmationModalApproveClickHandler,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmationModalWithNotes
|
|
||||||
ref={rejectModal.ref}
|
|
||||||
type='error'
|
|
||||||
text='Apakah anda yakin ingin reject data transfer ke laying ini?'
|
|
||||||
secondaryButton={{
|
|
||||||
text: 'Tidak',
|
|
||||||
}}
|
|
||||||
primaryButton={{
|
|
||||||
text: 'Ya',
|
|
||||||
color: 'error',
|
|
||||||
isLoading: isRejectLoading,
|
|
||||||
onClick: confirmationModalRejectClickHandler,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user