Files
lti-web-client/src/components/pages/master-data/supplier/form/SupplierForm.tsx
T

495 lines
16 KiB
TypeScript

'use client';
import { useModal } from '@/components/Modal';
import { CATEGORY_OPTIONS, TYPE_OPTIONS } from '@/config/constant';
import { isResponseError } from '@/lib/api-helper';
import { SupplierApi } from '@/services/api/master-data';
import {
CreateSupplierPayload,
Supplier,
} from '@/types/api/master-data/supplier';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import {
SupplierFormSchema,
SupplierFormValues,
UpdateSupplierFormSchema,
} from '@/components/pages/master-data/supplier/form/SupplierForm.schema';
import { useFormik } from 'formik';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import TextArea from '@/components/input/TextArea';
import { cn } from '@/lib/helper';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
interface SupplierCustomProps {
formType?: 'add' | 'edit' | 'detail';
initialValues?: Supplier;
}
const SupplierForm = ({
formType = 'add',
initialValues,
}: SupplierCustomProps) => {
// Setup Kebutuhan Form
const router = useRouter();
const deleteModal = useModal();
// Setup State
const [supplierFormErrorMessage, setSupplierFormErrorMessage] = useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [hatcheryOptionsValues, setHatcheryOptionValues] = useState<OptionType[]>([]);
// -- Options data mapping
const typeOptions = TYPE_OPTIONS;
const categoryOptions = CATEGORY_OPTIONS;
// Handler Event
const createSupplierHandler = useCallback(
async (payload: CreateSupplierPayload) => {
const createSupplierRes = await SupplierApi.create(payload);
if (isResponseError(createSupplierRes)) {
setSupplierFormErrorMessage(createSupplierRes.message);
return;
}
toast.success(createSupplierRes?.message as string);
router.push('/master-data/supplier');
},
[router]
);
const updateSupplierHandler = useCallback(
async (supplierId: number, payload: CreateSupplierPayload) => {
const updateSupplierRes = await SupplierApi.update(supplierId, payload);
if (isResponseError(updateSupplierRes)) {
setSupplierFormErrorMessage(updateSupplierRes.message);
return;
}
toast.success(updateSupplierRes?.message as string);
router.push('/master-data/supplier');
},
[router]
);
const deleteSupplierHandler = () => {
deleteModal.openModal();
};
const confirmationModalDeleteclickHandler = async () => {
setIsDeleteLoading(true);
await SupplierApi.delete(initialValues?.id as number);
deleteModal.closeModal();
setIsDeleteLoading(false);
router.push('/master-data/supplier');
};
// Utils Functions
const normalizeOptionValue = (
type?: string | { value: string; label: string },
options?: OptionType[]
): { value: string; label: string } => {
if (!type && !options) return { value: '', label: '' };
if (!type && options && options.length > 0)
return options[0] as { value: string; label: string };
if (typeof type === 'string') return { value: type, label: type };
return type ?? { value: '', label: '' };
};
// Memo
const formikInitialValues = useMemo<SupplierFormValues>(() => {
return {
name: initialValues?.name ?? '',
alias: initialValues?.alias ?? '',
pic: initialValues?.pic ?? '',
type: normalizeOptionValue(initialValues?.type, typeOptions),
category: normalizeOptionValue(initialValues?.category, categoryOptions),
hatchery: initialValues?.hatchery ?? '',
phone: initialValues?.phone ?? '',
email: initialValues?.email ?? '',
address: initialValues?.address ?? '',
npwp: initialValues?.npwp ?? '',
account_number: initialValues?.account_number ?? '',
due_date: initialValues?.due_date ?? 1,
};
}, [initialValues, typeOptions, categoryOptions]);
// Formik
const formik = useFormik<SupplierFormValues>({
initialValues: formikInitialValues,
enableReinitialize: true,
validationSchema:
formType === 'edit' ? UpdateSupplierFormSchema : SupplierFormSchema,
onSubmit: async (values) => {
// reset error message
setSupplierFormErrorMessage('');
// create payload
const payload: CreateSupplierPayload = {
name: values.name,
alias: values.alias,
pic: values.pic,
type: values.type.value,
category: values.category.value,
hatchery: values.hatchery,
phone: values.phone,
email: values.email,
address: values.address,
npwp: values.npwp,
account_number: values.account_number,
due_date: parseInt(values.due_date.toString()),
};
// cek type form yang disubmit
switch (formType) {
case 'add':
await createSupplierHandler(payload);
break;
case 'edit':
await updateSupplierHandler(initialValues?.id as number, payload);
break;
default:
break;
}
},
});
const { setValues: formikSetValues } = formik;
// Initialize Formik
useEffect(() => {
formikSetValues(formikInitialValues);
if(formType != 'add'){
const hatcheryArrays = formikInitialValues.hatchery.split(',');
const hatcheryCreatedOptions = hatcheryArrays.map((item) => ({
value: item,
label: item,
}));
setHatcheryOptionValues(hatcheryCreatedOptions);
}
}, [formikSetValues, formikInitialValues, setHatcheryOptionValues]);
useEffect(() => {
const commaSeparatedValues = hatcheryOptionsValues.map((item) => item.value).join(',');
formikSetValues({
...formik.values,
hatchery: commaSeparatedValues,
})
}, [hatcheryOptionsValues, formikSetValues]);
// Option Handler
const typeChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('type', true);
formik.setFieldValue('type', val);
};
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('category', true);
formik.setFieldValue('category', val);
};
// Render
return (
<>
<section className='w-full max-w-xl'>
<header className='flex flex-col gap-4'>
<Button
href='/master-data/supplier'
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 Supplier'}
{formType === 'edit' && 'Ubah Supplier'}
{formType === 'detail' && 'Detail Supplier'}
</h1>
</header>
<form
onSubmit={formik.handleSubmit}
onReset={formik.handleReset}
className='w-full mt-8 flex flex-col gap-6'
>
{/* Fields Form */}
<div className='flex flex-col gap-4'>
<TextInput
required
label='Nama Supplier'
name='name'
placeholder='Masukkan nama supplier'
value={formik.values.name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.name && Boolean(formik.errors.name)}
errorMessage={formik.errors.name}
readOnly={formType === 'detail'}
/>
<TextInput
required
label='Nama Alias'
name='alias'
placeholder='Masukkan alias supplier'
value={formik.values.alias}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.alias && Boolean(formik.errors.alias)}
errorMessage={formik.errors.alias}
readOnly={formType === 'detail'}
/>
<TextInput
required
label='Nama PIC'
name='pic'
placeholder='Masukkan PIC supplier'
value={formik.values.pic}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.pic && Boolean(formik.errors.pic)}
errorMessage={formik.errors.pic}
readOnly={formType === 'detail'}
/>
<SelectInput
required
placeholder='Pilih Tipe'
label='Tipe'
value={
typeOptions.find(
(item) => item.value === formik.values.type?.value
) ?? undefined
}
onChange={typeChangeHandler}
options={typeOptions}
isError={formik.touched.type && Boolean(formik.errors.type)}
errorMessage={formik.errors.type as string}
isDisabled={formType === 'detail'}
isClearable
isSearchable={true}
/>
<SelectInput
required
placeholder='Pilih Kategori'
label='Kategori'
value={
categoryOptions.find(
(item) => item.value === formik.values.category?.value
) ?? undefined
}
onChange={categoryChangeHandler}
options={categoryOptions}
isError={
formik.touched.category && Boolean(formik.errors.category)
}
errorMessage={formik.errors.category as string}
isDisabled={formType === 'detail'}
isClearable
isSearchable={true}
/>
<SelectInput
isMulti
createables
required
placeholder='Pilih Hatchery'
label='Hatchery'
value={hatcheryOptionsValues}
onChange={(val) => {
console.log(val); // pastikan val = array of { value, label }
setHatcheryOptionValues(val as OptionType[]);
}}
isError={formik.touched.hatchery && Boolean(formik.errors.hatchery)}
errorMessage={formik.errors.hatchery as string}
isDisabled={formType === 'detail'}
isClearable
isSearchable={true}
options={[]}
/>
<TextInput
required
label='Nomor Telepon'
name='phone'
placeholder='Masukkan nomor telepon supplier'
value={formik.values.phone}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.phone && Boolean(formik.errors.phone)}
errorMessage={formik.errors.phone}
readOnly={formType === 'detail'}
/>
<TextInput
required
label='Email'
name='email'
placeholder='Masukkan email supplier'
value={formik.values.email}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.email && Boolean(formik.errors.email)}
errorMessage={formik.errors.email}
readOnly={formType === 'detail'}
/>
<TextArea
required
label='Alamat'
name='address'
placeholder='Masukkan alamat supplier'
value={formik.values.address}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.address && Boolean(formik.errors.address)}
errorMessage={formik.errors.address}
readOnly={formType === 'detail'}
/>
<TextInput
required
label='NPWP'
name='npwp'
placeholder='Masukkan NPWP supplier'
value={formik.values.npwp}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.npwp && Boolean(formik.errors.npwp)}
errorMessage={formik.errors.npwp}
readOnly={formType === 'detail'}
/>
<TextInput
required
label='Nomor Rekening'
name='account_number'
placeholder='Masukkan nomor rekening supplier'
value={formik.values.account_number}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
formik.touched.account_number &&
Boolean(formik.errors.account_number)
}
errorMessage={formik.errors.account_number}
readOnly={formType === 'detail'}
/>
<TextInput
required
type='number'
className={{
wrapper: 'w-fit',
}}
label='Jatuh Tempo'
name='due_date'
placeholder='Masukkan tanggal pembayaran supplier'
value={formik.values.due_date}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
formik.touched.due_date && Boolean(formik.errors.due_date)
}
errorMessage={formik.errors.due_date}
readOnly={formType === 'detail'}
endAdornment={<div>Hari</div>}
/>
</div>
{/* Action Button */}
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{formType !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteSupplierHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{formType !== 'edit' && (
<Button
type='button'
color='warning'
href={`/master-data/supplier/detail/edit/?supplierId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
)}
</div>
)}
{formType !== 'detail' && (
<div
className={cn('flex flex-row justify-end gap-2', {
'w-full': formType === 'add',
})}
>
<Button type='reset' color='warning' className='px-4'>
Reset
</Button>
<Button
type='submit'
color='primary'
isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting}
className='px-4'
>
Submit
</Button>
</div>
)}
</div>
{supplierFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{supplierFormErrorMessage}</span>
</div>
)}
</form>
</section>
{formType !== 'add' && (
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Supplier ini (${initialValues?.name})?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
onClick: confirmationModalDeleteclickHandler,
isLoading: isDeleteLoading,
}}
/>
)}
</>
);
};
export default SupplierForm;