feat(FE-65): enhance MovementForm with initial values handling and refactor components

This commit is contained in:
rstubryan
2025-10-10 08:40:17 +07:00
parent 7dbf880228
commit a9cdea7318
2 changed files with 78 additions and 182 deletions
@@ -1,4 +1,5 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
import { Movement } from '@/types/api/inventory/movement';
export const MovementFormSchema = Yup.object({ export const MovementFormSchema = Yup.object({
alasan_transfer: Yup.string().required('Alasan transfer wajib diisi!'), alasan_transfer: Yup.string().required('Alasan transfer wajib diisi!'),
@@ -66,3 +67,43 @@ export const MovementFormSchema = Yup.object({
export const UpdateMovementFormSchema = MovementFormSchema; export const UpdateMovementFormSchema = MovementFormSchema;
export type MovementFormValues = Yup.InferType<typeof MovementFormSchema>; export type MovementFormValues = Yup.InferType<typeof MovementFormSchema>;
export const getMovementFormInitialValues = (
initialValues?: Movement
): MovementFormValues => ({
alasan_transfer: initialValues?.alasan_transfer ?? '',
tanggal_transfer: initialValues?.tanggal_transfer ?? '',
warehouse_asal: initialValues?.warehouse_asal
? {
value: initialValues.warehouse_asal.id,
label: initialValues.warehouse_asal.name,
}
: null,
warehouse_asal_id: initialValues?.warehouse_asal?.id ?? 0,
warehouse_tujuan: initialValues?.warehouse_tujuan
? {
value: initialValues.warehouse_tujuan.id,
label: initialValues.warehouse_tujuan.name,
}
: null,
warehouse_tujuan_id: initialValues?.warehouse_tujuan?.id ?? 0,
product:
initialValues?.product?.map((p) => ({
product: { value: p.product.id, label: p.product.name },
product_id: p.product.id,
qty_product: p.qty_product,
})) ?? [],
ekspedisi:
initialValues?.ekspedisi?.map((e) => ({
product: { value: e.product_id, label: '' },
product_id: e.product_id,
qty: e.qty,
supplier: { value: e.supplier.id, label: e.supplier.name },
supplier_id: e.supplier.id,
plat_nomor: e.plat_nomor,
no_surat_jalan: e.no_surat_jalan,
dokumen: e.dokumen,
biaya_ekspedisi: e.biaya_ekspedisi,
nama_sopir: e.nama_sopir,
})) ?? [],
});
@@ -1,36 +1,33 @@
'use client'; 'use client';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { FieldArray, FormikProvider, useFormik } from 'formik'; import { FieldArray, FormikProvider, useFormik } from 'formik';
import { toast } from 'react-hot-toast';
import useSWR from 'swr'; import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, { OptionType } from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { FormHeader } from '@/components/helper/form/FormHeader';
import { FormActions } from '@/components/helper/form/FormActions';
import {
CreateMovementPayload,
Movement,
} from '@/types/api/inventory/movement';
import { isResponseSuccess } from '@/lib/api-helper';
import { import {
MovementFormSchema, MovementFormSchema,
MovementFormValues, MovementFormValues,
UpdateMovementFormSchema, UpdateMovementFormSchema,
getMovementFormInitialValues,
} from '@/components/pages/inventory/movement/form/MovementForm.schema'; } from '@/components/pages/inventory/movement/form/MovementForm.schema';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useMovementFormHandlers } from './useMovementFormHandlers';
import {
Movement,
CreateMovementPayload,
UpdateMovementPayload,
} from '@/types/api/inventory/movement';
import { import {
ProductApi, ProductApi,
WarehouseApi,
SupplierApi, SupplierApi,
WarehouseApi,
} from '@/services/api/master-data'; } from '@/services/api/master-data';
import { MovementApi } from '@/services/api/inventory';
import { cn } from '@/lib/helper';
interface MovementFormProps { interface MovementFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -38,77 +35,20 @@ interface MovementFormProps {
} }
const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
const router = useRouter(); const [, setMovementFormErrorMessage] = useState('');
const deleteModal = useModal();
const [movementFormErrorMessage, setMovementFormErrorMessage] = useState(''); const {
const [isDeleteLoading, setIsDeleteLoading] = useState(false); deleteModal,
movementFormErrorMessage,
const createMovementHandler = useCallback( isDeleteLoading,
async (payload: CreateMovementPayload) => { createMovementHandler,
const res = await MovementApi.create(payload); updateMovementHandler,
if (isResponseError(res)) { deleteMovementClickHandler,
setMovementFormErrorMessage(res.message); confirmationModalDeleteClickHandler,
return; } = useMovementFormHandlers(initialValues?.id);
}
toast.success(res?.message as string);
router.push('/inventory/movement');
},
[router]
);
const updateMovementHandler = useCallback(
async (movementId: number, payload: UpdateMovementPayload) => {
const res = await MovementApi.update(movementId, payload);
if (res?.status === 'error') {
setMovementFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.refresh();
router.push('/inventory/movement');
},
[router]
);
const formikInitialValues = useMemo<MovementFormValues>( const formikInitialValues = useMemo<MovementFormValues>(
() => ({ () => getMovementFormInitialValues(initialValues),
alasan_transfer: initialValues?.alasan_transfer ?? '',
tanggal_transfer: initialValues?.tanggal_transfer ?? '',
warehouse_asal: initialValues?.warehouse_asal
? {
value: initialValues.warehouse_asal.id,
label: initialValues.warehouse_asal.name,
}
: null,
warehouse_asal_id: initialValues?.warehouse_asal?.id ?? 0,
warehouse_tujuan: initialValues?.warehouse_tujuan
? {
value: initialValues.warehouse_tujuan.id,
label: initialValues.warehouse_tujuan.name,
}
: null,
warehouse_tujuan_id: initialValues?.warehouse_tujuan?.id ?? 0,
product:
initialValues?.product?.map((p) => ({
product: { value: p.product.id, label: p.product.name },
product_id: p.product.id,
qty_product: p.qty_product,
})) ?? [],
ekspedisi:
initialValues?.ekspedisi?.map((e) => ({
product: { value: e.product_id, label: '' }, // Need to fetch product details
product_id: e.product_id,
qty: e.qty,
supplier: { value: e.supplier.id, label: e.supplier.name },
supplier_id: e.supplier.id,
plat_nomor: e.plat_nomor,
no_surat_jalan: e.no_surat_jalan,
dokumen: e.dokumen,
biaya_ekspedisi: e.biaya_ekspedisi,
nama_sopir: e.nama_sopir,
})) ?? [],
}),
[initialValues] [initialValues]
); );
@@ -185,19 +125,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
? suppliers?.data.map((s) => ({ value: s.id, label: s.name })) ? suppliers?.data.map((s) => ({ value: s.id, label: s.name }))
: []; : [];
const deleteMovementClickHandler = () => {
deleteModal.openModal();
};
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
await MovementApi.delete(initialValues?.id as number);
deleteModal.closeModal();
toast.success('Successfully delete Movement!');
setIsDeleteLoading(false);
router.push('/inventory/movement');
};
const { setValues: formikSetValues } = formik; const { setValues: formikSetValues } = formik;
useEffect(() => { useEffect(() => {
@@ -207,22 +134,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return ( return (
<> <>
<section className='w-full max-w-xl'> <section className='w-full max-w-xl'>
<header className='flex flex-col gap-4'> <FormHeader
<Button type={type}
href='/inventory/movement' title='Movement'
variant='link' backUrl='/inventory/movement'
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'>
{type === 'add' && 'Tambah Movement'}
{type === 'edit' && 'Edit Movement'}
{type === 'detail' && 'Detail Movement'}
</h1>
</header>
<FormikProvider value={formik}> <FormikProvider value={formik}>
<form <form
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
@@ -330,11 +246,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<FieldArray name='product'> <FieldArray name='product'>
{({ push, remove }) => ( {({ push, remove }) => (
<> <>
{typeof formik.errors.product === 'string' && (
<div className='text-error'>
{formik.errors.product}
</div>
)}
<table className='table'> <table className='table'>
<thead> <thead>
<tr> <tr>
@@ -433,11 +344,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<FieldArray name='ekspedisi'> <FieldArray name='ekspedisi'>
{({ push, remove }) => ( {({ push, remove }) => (
<> <>
{typeof formik.errors.ekspedisi === 'string' && (
<div className='text-error'>
{formik.errors.ekspedisi}
</div>
)}
<table className='table'> <table className='table'>
<thead> <thead>
<tr> <tr>
@@ -652,67 +558,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
</div> </div>
{/* Action buttons */} {/* Action buttons */}
<div className='flex flex-row justify-between gap-2 flex-wrap'> <FormActions<MovementFormValues>
{type !== 'add' && ( type={type}
<div className='flex flex-row justify-start gap-2'> formik={formik}
<Button editUrl={
type='button' initialValues
color='error' ? `/inventory/movement/detail/edit/?movementId=${initialValues.id}`
onClick={deleteMovementClickHandler} : undefined
className='px-4' }
> onDelete={deleteMovementClickHandler}
<Icon />
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{type !== 'edit' && (
<Button
type='button'
color='warning'
href={`/inventory/movement/detail/edit/?movementId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
)}
</div>
)}
{type !== 'detail' && (
<div
className={cn('flex flex-row justify-end gap-2', {
'w-full': type === 'add',
})}
>
<Button
type='reset'
color='warning'
className='px-4'
onClick={formik.handleReset}
>
Reset
</Button>
<Button
type='submit'
color='primary'
className='px-4'
isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting}
>
Submit
</Button>
</div>
)}
</div>
{movementFormErrorMessage && ( {movementFormErrorMessage && (
<div role='alert' className='alert alert-error'> <div role='alert' className='alert alert-error'>