mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 23:35:45 +00:00
feat: create FinanceForm component
This commit is contained in:
@@ -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;
|
||||||
Reference in New Issue
Block a user