feat: create FinanceForm component

This commit is contained in:
ValdiANS
2025-12-15 13:06:24 +07:00
parent 0ced3f3bac
commit cadd5b09ba
@@ -0,0 +1,386 @@
'use client';
import { useModal } from '@/components/Modal';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { FinanceApi } from '@/services/api/finance';
import {
Finance,
CreateFinancePayload,
UpdateFinancePayload,
} from '@/types/api/finance';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import Button from '@/components/Button';
import { Icon } from '@iconify/react';
import TextInput from '@/components/input/TextInput';
import NumberInput from '@/components/input/NumberInput';
import { cn } from '@/lib/helper';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import TextArea from '@/components/input/TextArea';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import DateInput from '@/components/input/DateInput';
import { BankApi, CustomerApi } from '@/services/api/master-data';
import { Customer } from '@/types/api/master-data/customer';
import { Bank } from '@/types/api/master-data/bank';
import {
FINANCE_PAYMENT_METHOD_OPTIONS,
FINANCE_TRANSACTION_TYPE_OPTIONS,
} from '@/config/constant';
import { FinanceFormSchema, FinanceFormValues } from './FinanceForm.schema';
import Link from 'next/link';
interface FinanceFormProps {
formType?: 'add' | 'edit' | 'detail';
initialValues?: Finance;
}
const FinanceForm = ({ formType = 'add', initialValues }: FinanceFormProps) => {
const router = useRouter();
const deleteModal = useModal();
const [financeFormErrorMessage, fileFormErrorMessage] = useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
// API Options
const {
options: customerOptions,
isLoadingOptions: isLoadingCustomerOptions,
setInputValue: setCustomerInputValue,
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
const {
options: bankOptions,
isLoadingOptions: isLoadingBankOptions,
setInputValue: setBankInputValue,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
const createFinanceHandler = useCallback(
async (payload: CreateFinancePayload) => {
const createFinanceRes = await FinanceApi.create(payload);
if (isResponseError(createFinanceRes)) {
fileFormErrorMessage(createFinanceRes.message);
return;
}
toast.success(createFinanceRes?.message as string);
router.push('/finance');
},
[router]
);
// Note: Update and Delete handlers are not strictly needed for "add" page but good practice to structure for future
const deleteFinanceHandler = () => {
deleteModal.openModal();
};
const confirmationModalDeleteclickHandler = async () => {
// Implement delete logic if needed
setIsDeleteLoading(true);
// await FinanceApi.delete(initialValues?.id as number);
deleteModal.closeModal();
setIsDeleteLoading(false);
router.push('/finance');
};
const formikInitialValues = useMemo<FinanceFormValues>(() => {
return {
transactionType: initialValues
? {
value: initialValues.transaction_type,
label: initialValues.transaction_type,
} // Should map properly if labels differ
: null,
customerId: initialValues ? null : null, // ID not directly in BaseFinance without processing, assume add mode mostly
customer: initialValues
? { value: 0, label: initialValues.customer_name }
: null,
paymentDate: initialValues?.payment_date ?? '',
paymentMethod: প্রিMethods(initialValues?.payment_method),
bankId: null,
bank: initialValues ? { value: 0, label: initialValues.bank_name } : null,
supplierBankAccountNumber: '',
referenceNumber: initialValues?.reference_number ?? '',
amount: initialValues
? String(initialValues.revenue_amount || initialValues.expense_amount)
: '',
notes: '',
};
}, [initialValues]);
function িMethods(method?: string) {
if (!method) return null;
return (
FINANCE_PAYMENT_METHOD_OPTIONS.find((o) => o.value === method) ?? {
value: method,
label: method,
}
);
}
const formik = useFormik<FinanceFormValues>({
initialValues: formikInitialValues,
validationSchema: FinanceFormSchema,
onSubmit: async (values) => {
fileFormErrorMessage('');
const payload: CreateFinancePayload = {
transaction_type: (values.transactionType as OptionType)
.value as string,
customer_id: values.customer ? (values.customer.value as number) : 0,
payment_date: values.paymentDate,
payment_method: (values.paymentMethod as OptionType).value as string,
bank_id: values.bank ? (values.bank.value as number) : 0,
supplier_bank_account_number: values.supplierBankAccountNumber,
reference_number: values.referenceNumber,
amount: Number(values.amount),
notes: values.notes,
};
if (formType === 'add') {
await createFinanceHandler(payload);
}
},
});
const { setValues: formikSetValues } = formik;
useEffect(() => {
if (formType !== 'add' && initialValues) {
// Hydrate logic would go here if editing
}
}, [formType, initialValues]);
// Helper for select changes
const handleSelectChange = (
fieldName: keyof FinanceFormValues,
val: OptionType | null
) => {
formik.setFieldValue(fieldName, val);
};
return (
<>
<section className='w-full max-w-xl'>
<header className='flex flex-col gap-4'>
<Button
href='/finance'
variant='link'
className='w-fit p-0 text-primary'
>
<Icon icon='uil:arrow-left' width={24} height={24} />
Kembali
</Button>
<h1 className='text-2xl font-bold text-center'>
{formType === 'add' && 'Tambah Keuangan'}
{formType === 'edit' && 'Ubah Keuangan'}
{formType === 'detail' && 'Detail Keuangan'}
</h1>
</header>
<form
onSubmit={formik.handleSubmit}
className='w-full mt-8 flex flex-col gap-6'
>
<div className='flex flex-col gap-4'>
<SelectInput
required
label='Pilih Jenis Transaksi'
placeholder='Pilih Jenis Transaksi'
options={FINANCE_TRANSACTION_TYPE_OPTIONS}
value={formik.values.transactionType}
onChange={(val) =>
handleSelectChange('transactionType', val as OptionType)
}
isError={
formik.touched.transactionType &&
Boolean(formik.errors.transactionType)
}
errorMessage={formik.errors.transactionType as string}
isDisabled={formType === 'detail'}
/>
<SelectInput
required
label='Customer'
placeholder='Pilih Customer'
options={customerOptions}
isLoading={isLoadingCustomerOptions}
onInputChange={setCustomerInputValue}
value={formik.values.customer}
onChange={(val) =>
handleSelectChange('customer', val as OptionType)
}
isError={
formik.touched.customer && Boolean(formik.errors.customer)
}
errorMessage={formik.errors.customer as string} // Schema logic handles this validation conditionally
isDisabled={formType === 'detail'}
/>
<DateInput
required
label='Tanggal Pembayaran'
name='paymentDate'
placeholder='Pilih Tanggal Pembayaran'
value={formik.values.paymentDate}
onChange={(e) =>
formik.setFieldValue('paymentDate', e.target.value)
}
isError={
formik.touched.paymentDate && Boolean(formik.errors.paymentDate)
}
errorMessage={formik.errors.paymentDate}
readOnly={formType === 'detail'}
/>
<SelectInput
required
label='Metode Pembayaran'
placeholder='Pilih Metode Pembayaran'
options={FINANCE_PAYMENT_METHOD_OPTIONS}
value={formik.values.paymentMethod}
onChange={(val) =>
handleSelectChange('paymentMethod', val as OptionType)
}
isError={
formik.touched.paymentMethod &&
Boolean(formik.errors.paymentMethod)
}
errorMessage={formik.errors.paymentMethod as string}
isDisabled={formType === 'detail'}
/>
<SelectInput
label='Nomor Rekening'
placeholder='Pilih Bank'
options={bankOptions}
isLoading={isLoadingBankOptions}
onInputChange={setBankInputValue}
value={formik.values.bank}
onChange={(val) => handleSelectChange('bank', val as OptionType)}
isError={formik.touched.bank && Boolean(formik.errors.bank)} // Actually controlled by bankId in schema but logic applies
errorMessage={formik.errors.bankId as string} // bankId error mapping
isDisabled={formType === 'detail'}
/>
<TextInput
required
label='Nomor Rekening Customer' // As per screenshot
name='supplierBankAccountNumber'
placeholder='Nomor Rekening'
value={formik.values.supplierBankAccountNumber}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
formik.touched.supplierBankAccountNumber &&
Boolean(formik.errors.supplierBankAccountNumber)
}
errorMessage={formik.errors.supplierBankAccountNumber}
readOnly={formType === 'detail'}
/>
<TextInput
label='Nomor Referensi'
name='referenceNumber'
placeholder='Nomor Referensi'
value={formik.values.referenceNumber}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
formik.touched.referenceNumber &&
Boolean(formik.errors.referenceNumber)
}
errorMessage={formik.errors.referenceNumber}
readOnly={formType === 'detail'}
/>
<NumberInput
required
label='Nominal'
name='amount'
placeholder='Nominal'
value={Number(formik.values.amount)}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.amount && Boolean(formik.errors.amount)}
errorMessage={formik.errors.amount}
readOnly={formType === 'detail'}
inputPrefix={
<span className='text-gray-600 font-medium'>Rp</span>
}
/>
<TextArea
required
label='Catatan'
name='notes'
placeholder='Catatan'
value={formik.values.notes}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.notes && Boolean(formik.errors.notes)}
errorMessage={formik.errors.notes}
readOnly={formType === 'detail'}
/>
</div>
<div className='flex flex-row gap-2'>
<Button
type='submit'
color='primary'
isLoading={formik.isSubmitting}
disabled={!formik.isValid || !formik.dirty || formik.isSubmitting}
className='w-24'
>
Simpan
</Button>
<Button
type='button'
variant='outline'
color='warning'
href='/finance'
className='w-24'
>
Batal
</Button>
</div>
{financeFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{financeFormErrorMessage}</span>
</div>
)}
</form>
</section>
{formType !== 'add' && (
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
onClick: confirmationModalDeleteclickHandler,
isLoading: isDeleteLoading,
}}
/>
)}
</>
);
};
export default FinanceForm;