'use client'; import { useCallback, useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useFormik } from 'formik'; import { toast } from 'react-hot-toast'; import { Icon } from '@iconify/react'; import Link from 'next/link'; import Button from '@/components/Button'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import SelectInput, { OptionType, useSelect, } from '@/components/input/SelectInput'; import DateInput from '@/components/input/DateInput'; import ExpenseKandangsTable from '@/components/pages/expense/form/ExpenseKandangsTable'; import DropFileInput from '@/components/input/DropFileInput'; import ExpenseRequestKandangDetailExpense from '@/components/pages/expense/form/ExpenseRequestKandangDetailExpense'; import RequirePermission from '@/components/helper/RequirePermission'; import { ExpenseRequestFormSchema, ExpenseRequestFormValues, getExpenseFormInitialValues, UpdateExpenseRequestFormSchema, } from '@/components/pages/expense/form/ExpenseRequestForm.schema'; import { isResponseError } from '@/lib/api-helper'; import { Expense, CreateExpensePayload, UpdateExpensePayload, } from '@/types/api/expense'; import { ExpenseApi } from '@/services/api/expense'; import { cn, sleep } from '@/lib/helper'; import { LocationApi, SupplierApi } from '@/services/api/master-data'; import { ACCEPTED_FILE_TYPE } from '@/config/constant'; import { Supplier } from '@/types/api/master-data/supplier'; import { getUniqueFormikErrors } from '@/lib/formik-helper'; import AlertErrorList from '@/components/helper/form/FormErrors'; interface ExpenseFormProps { type?: 'add' | 'edit' | 'detail'; initialValues?: Expense; } const ExpenseRequestForm = ({ type = 'add', initialValues, }: ExpenseFormProps) => { const router = useRouter(); // Modal hooks const deleteModal = useModal(); const approveModal = useModal(); const rejectModal = useModal(); const [expenseFormErrorMessage, setExpenseFormErrorMessage] = useState(''); const [formErrorList, setFormErrorList] = useState([]); const createExpenseHandler = useCallback( async (payload: CreateExpensePayload) => { const createExpenseRes = await ExpenseApi.create( ExpenseApi.convertExpenseRequestPayloadToFormData(payload) ); if (isResponseError(createExpenseRes)) { setExpenseFormErrorMessage(createExpenseRes.message); return; } toast.success(createExpenseRes?.message as string); router.push('/expense'); }, [router] ); const updateExpenseHandler = useCallback( async ( expenseId: number, payload: UpdateExpensePayload, deletedDocumentIds: number[] ) => { const updateExpenseRes = await ExpenseApi.update( expenseId, ExpenseApi.convertExpenseRequestUpdatePayloadToFormData(payload), deletedDocumentIds ); if (updateExpenseRes?.status === 'error') { setExpenseFormErrorMessage(updateExpenseRes.message); return; } toast.success(updateExpenseRes?.message as string); router.refresh(); router.push('/expense'); }, [router] ); const formik = useFormik({ initialValues: getExpenseFormInitialValues(initialValues), validationSchema: type === 'edit' ? UpdateExpenseRequestFormSchema : ExpenseRequestFormSchema, onSubmit: async (values) => { setExpenseFormErrorMessage(''); const expensePayload: CreateExpensePayload = { category: formik.values.category?.value as 'BOP' | 'NON-BOP', location_id: values.location_id as number, transaction_date: values?.transaction_date as string, supplier_id: values.supplier?.value as number, documents: values.documents as File[], expense_nonstocks: values.expense_nonstocks.map((expenseNonstock) => { const basePayload = { cost_items: expenseNonstock.cost_items.map((costItem) => ({ nonstock_id: costItem.nonstock?.value as number, quantity: parseFloat(String(costItem.quantity)) as number, price: parseFloat(String(costItem.price)) as number, notes: costItem.notes ?? '', })), }; return expenseNonstock.kandang_id ? { ...basePayload, kandang_id: expenseNonstock.kandang_id } : basePayload; }), }; switch (type) { case 'add': await createExpenseHandler(expensePayload); break; case 'edit': const expenseUpdatePayload: UpdateExpensePayload = { category: formik.values.category?.value as 'BOP' | 'NON-BOP', location_id: values.location_id as number, transaction_date: values?.transaction_date as string, supplier_id: values.supplier?.value as number, documents: values.documents as File[], expense_nonstocks: values.expense_nonstocks.map( (expenseNonstock) => { const basePayload = { cost_items: expenseNonstock.cost_items.map((costItem) => ({ nonstock_id: costItem.nonstock?.value as number, quantity: parseFloat(String(costItem.quantity)) as number, price: parseFloat(String(costItem.price)) as number, notes: costItem.notes ?? '', })), }; return expenseNonstock.kandang_id ? { ...basePayload, kandang_id: expenseNonstock.kandang_id } : basePayload; } ), }; await updateExpenseHandler( initialValues?.id as number, expenseUpdatePayload, formik.values.deleted_documents ?? [] ); break; } }, }); const { setValues: formikSetValues } = formik; const { setInputValue: setLocationInputValue, options: locationOptions, isLoadingOptions: isLoadingLocationOptions, } = useSelect(LocationApi.basePath, 'id', 'name'); const { setInputValue: setVendorInputValue, options: supplierOptions, isLoadingOptions: isLoadingVendorOptions, } = useSelect(SupplierApi.basePath, 'id', 'name'); const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldTouched('category', true); formik.setFieldValue('category', val); }; const locationChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { const location = val as OptionType | null; const locationId = location ? Number(location.value) : 0; formik.setFieldTouched('location', true); formik.setFieldValue('location', location); formik.setFieldTouched('location_id', true); formik.setFieldValue('location_id', locationId); }, [] ); const kandangsChangeHandler = ( kandangs: { id?: number; name?: string }[] ) => { formik.setFieldTouched('kandangs', true); formik.setFieldValue('kandangs', kandangs); // If no kandangs selected, create expense item for location if (kandangs.length === 0) { formik.setFieldValue('expense_nonstocks', [ { cost_items: [ { nonstock: null, nonstock_id: 0, quantity: undefined, price: undefined, notes: '', }, ], }, ]); return; } const newExpenseNonstocks: typeof formik.values.expense_nonstocks = []; kandangs.forEach((kandangItem) => { if (!kandangItem.id) return; const existingExpenseNonstock = formik.values.expense_nonstocks?.find( (expenseNonstockItem) => expenseNonstockItem.kandang_id === kandangItem.id ); newExpenseNonstocks.push({ kandang_id: kandangItem.id, cost_items: existingExpenseNonstock?.cost_items || [ { nonstock: null, nonstock_id: 0, quantity: undefined, price: undefined, notes: '', }, ], }); }); formik.setFieldValue('expense_nonstocks', newExpenseNonstocks); }; const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldTouched('supplier', true); formik.setFieldTouched('supplier_id', true); formik.setFieldValue('supplier', val); const supplierId = Array.isArray(val) ? val[0]?.value : val?.value; formik.setFieldValue('supplier_id', supplierId ?? 0); }; const requestDocumentsChangeHandler = (val: File[]) => { formik.setFieldTouched('documents', true); const invalidFiles = val.filter((file) => file.size > 5 * 1024 * 1024); if (invalidFiles.length > 0) { toast.error('Ukuran dokumen maksimal 5 MB!'); return; } formik.setFieldValue('documents', val); }; const requestDocumentsDeleteHandler = (deletedFileIdx: number) => { const newRequestDocuments = formik.values.documents; newRequestDocuments?.splice(deletedFileIdx, 1); formik.setFieldValue('documents', newRequestDocuments); }; const deleteDocumentClickHandler = ( deletedDocumentIdx: number, deletedDocumentId: number ) => { const newDeletedDocumentIds = [...(formik.values.deleted_documents ?? [])]; const newExistingDocuments = [ ...(formik.values.existing_documents ?? []), ].filter((_, idx) => idx !== deletedDocumentIdx); newDeletedDocumentIds.push(deletedDocumentId); formik.setFieldTouched('deleted_documents', true); formik.setFieldValue('deleted_documents', newDeletedDocumentIds); formik.setFieldTouched('existing_documents', true); formik.setFieldValue('existing_documents', newExistingDocuments); }; const deleteExpenseClickHandler = () => { deleteModal.openModal(); }; const confirmationModalRejectClickHandler = async () => { await sleep(750); rejectModal.closeModal(); toast.success('Berhasil melakukan reject biaya operasional!'); }; const confirmationModalApproveClickHandler = async () => { await sleep(750); approveModal.closeModal(); toast.success('Berhasil melakukan approve biaya operasional!'); }; const confirmationModalDeleteClickHandler = async () => { await ExpenseApi.delete(initialValues?.id as number); deleteModal.closeModal(); toast.success('Successfully delete Expense!'); router.push('/expense'); }; const handleValidateForm = async () => { const errors = await formik.validateForm(); if (Object.keys(errors).length > 0) { const errorMessages = getUniqueFormikErrors(errors); setFormErrorList(errorMessages); return; } }; const handleFormSubmit = (e: React.FormEvent) => { e.preventDefault(); handleValidateForm(); formik.handleSubmit(e); }; useEffect(() => { formikSetValues(getExpenseFormInitialValues(initialValues)); }, [formikSetValues, getExpenseFormInitialValues, initialValues]); return ( <>

{type === 'add' && 'Tambah Biaya Operasional'} {type === 'edit' && 'Edit Biaya Operasional'} {type === 'detail' && 'Detail Biaya Operasional'}

{expenseFormErrorMessage && (
{expenseFormErrorMessage}
)} {formErrorList.length > 0 && ( setFormErrorList([])} /> )}
{formik.values.existing_documents && formik.values.existing_documents.length > 0 && (
    {formik.values.existing_documents.map( (existingDocument, existingDocumentIdx) => (
  • {existingDocument.name}{' '}
  • ) )}
)}
{type !== 'add' && (
{type !== 'edit' && ( )}
)} {type !== 'detail' && (
)}
{type !== 'add' && ( )} {type === 'detail' && ( <> )} ); }; export default ExpenseRequestForm;