From 4f375a4f0bb254437fabc6518d985e88204753b7 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 24 Nov 2025 09:43:20 +0700 Subject: [PATCH] feat(FE-200,204): create Expense Realization Form --- .../expense/form/ExpenseRealizationForm.tsx | 410 ++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 src/components/pages/expense/form/ExpenseRealizationForm.tsx diff --git a/src/components/pages/expense/form/ExpenseRealizationForm.tsx b/src/components/pages/expense/form/ExpenseRealizationForm.tsx new file mode 100644 index 00000000..5baa2a45 --- /dev/null +++ b/src/components/pages/expense/form/ExpenseRealizationForm.tsx @@ -0,0 +1,410 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useFormik } from 'formik'; +import toast from 'react-hot-toast'; + +import Link from 'next/link'; +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import DropFileInput from '@/components/input/DropFileInput'; +import ExpenseKandangsTable from '@/components/pages/expense/form/ExpenseKandangsTable'; +import ExpenseRealizationKandangDetailExpense from '@/components/pages/expense/form/ExpenseRealizationKandangDetailExpense'; + +import { + CreateExpenseRealizationPayload, + Expense, + UpdateExpenseRealizationPayload, +} from '@/types/api/expense'; +import { + ExpenseRealizationFormSchema, + ExpenseRealizationFormValues, + getExpenseRealizationFormInitialValues, + UpdateExpenseRealizationFormSchema, +} from '@/components/pages/expense/form/ExpenseRealizationForm.schema'; +import { ExpenseApi } from '@/services/api/expense'; +import { isResponseError } from '@/lib/api-helper'; +import { LocationApi, SupplierApi } from '@/services/api/master-data'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { ACCEPTED_FILE_TYPE } from '@/config/constant'; +import { cn } from '@/lib/helper'; + +interface ExpenseRealizationFormProps { + type?: 'add' | 'edit' | 'detail'; + initialValues?: Expense; +} + +const ExpenseRealizationForm = ({ + type = 'add', + initialValues, +}: ExpenseRealizationFormProps) => { + const router = useRouter(); + + const [expenseFormErrorMessage, setExpenseFormErrorMessage] = useState(''); + + const createExpenseHandler = useCallback( + async (payload: CreateExpenseRealizationPayload) => { + const createExpenseRes = await ExpenseApi.createRealization( + initialValues?.id as number, + ExpenseApi.convertExpenseRealizationPayloadToFormData(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: UpdateExpenseRealizationPayload) => { + const updateExpenseRes = await ExpenseApi.updateRealization( + expenseId, + ExpenseApi.convertExpenseRealizationPayloadToFormData(payload) + ); + + if (updateExpenseRes?.status === 'error') { + setExpenseFormErrorMessage(updateExpenseRes.message); + return; + } + + toast.success(updateExpenseRes?.message as string); + router.refresh(); + router.push('/expense'); + }, + [router] + ); + + const formik = useFormik({ + initialValues: getExpenseRealizationFormInitialValues(initialValues), + validationSchema: + type === 'edit' + ? UpdateExpenseRealizationFormSchema + : ExpenseRealizationFormSchema, + onSubmit: async (values) => { + setExpenseFormErrorMessage(''); + + const realizations: CreateExpenseRealizationPayload['realizations'] = []; + + values.realizations.forEach((realization) => { + realization.cost_items.forEach((costItem) => { + const unitPrice = + parseFloat(String(costItem.total_cost)) / + parseFloat(String(costItem.quantity)); + + const realizationItem = { + expense_nonstock_id: costItem.nonstock?.value as number, + qty: parseFloat(String(costItem.quantity)) as number, + unit_price: unitPrice, + total_price: parseFloat(String(costItem.total_cost)) as number, + notes: costItem.notes ?? '', + }; + + realizations.push(realizationItem); + }); + }); + + const expensePayload: CreateExpenseRealizationPayload = { + realization_date: values.realization_date as string, + documents: values.documents as File[], + realizations, + }; + + switch (type) { + case 'add': + await createExpenseHandler(expensePayload); + break; + + case 'edit': + await updateExpenseHandler( + initialValues?.id as number, + expensePayload + ); + break; + } + }, + }); + + const { setValues: formikSetValues } = formik; + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const { + setInputValue: setVendorInputValue, + options: vendorOptions, + isLoadingOptions: isLoadingVendorOptions, + } = useSelect(SupplierApi.basePath, 'id', 'name'); + + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldTouched('location', true); + formik.setFieldValue('location', val); + + formik.setFieldValue('kandangs', []); + formik.setFieldValue('realizations', []); + }; + + const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => { + formik.setFieldTouched('kandangs', true); + formik.setFieldValue('kandangs', kandangs); + + const newRealizations = [...(formik.values.realizations ?? [])]; + + // add new realizations + kandangs.forEach((kandangItem) => { + const isKandangExistInRealization = newRealizations.find( + (realizationItem) => realizationItem.kandang_id === kandangItem.id + ); + + if (isKandangExistInRealization) return; + + newRealizations.push({ + kandang_id: kandangItem.id, + cost_items: [ + { + nonstock: undefined, + quantity: undefined, + total_cost: undefined, + notes: '', + }, + ], + }); + }); + + // prune realizations + const kandangIds = new Set(kandangs.map((kandang) => kandang.id)); + const deletedRealizationsIdx: number[] = []; + + newRealizations.forEach((realization, idx) => { + const isRealizationValid = kandangIds.has(realization.kandang_id); + + if (!isRealizationValid) { + deletedRealizationsIdx.push(idx); + } + }); + + deletedRealizationsIdx.forEach((deletedRealizationIdx) => { + newRealizations.splice(deletedRealizationIdx, 1); + }); + + formik.setFieldValue('realizations', newRealizations); + }; + + const vendorChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldTouched('vendor', true); + formik.setFieldValue('vendor', val); + }; + + const realizationDocumentsChangeHandler = (val: File[]) => { + formik.setFieldTouched('documents', true); + formik.setFieldValue('documents', val); + }; + + const realizationDocumentsDeleteHandler = (deletedFileIdx: number) => { + const newRequestDocuments = formik.values.documents; + + newRequestDocuments?.splice(deletedFileIdx, 1); + + formik.setFieldValue('documents', newRequestDocuments); + }; + + useEffect(() => { + formikSetValues(getExpenseRealizationFormInitialValues(initialValues)); + }, [formikSetValues, getExpenseRealizationFormInitialValues, initialValues]); + + return ( +
+
+ + +

+ Realisasi Biaya Operasional +

+
+ +
+
+ + + + + + + + + + + {formik.values.existing_documents && + formik.values.existing_documents.length > 0 && ( +
+
    + {formik.values.existing_documents.map( + (existingDocument, existingDocumentIdx) => ( +
  • + + {existingDocument.name}{' '} + + +
  • + ) + )} +
+
+ )} + + +
+ + {expenseFormErrorMessage && ( +
+ + {expenseFormErrorMessage} +
+ )} + +
+ {type !== 'add' && ( +
+ {type !== 'edit' && ( + + )} +
+ )} + + {type !== 'detail' && ( +
+ + + +
+ )} +
+
+
+ ); +}; + +export default ExpenseRealizationForm;