refactor(FE): Add Formik-based filter with validation

This commit is contained in:
rstubryan
2026-01-23 21:32:10 +07:00
parent 9f6fec5a3c
commit b046b64ed2
2 changed files with 207 additions and 81 deletions
@@ -51,6 +51,13 @@ import { generateUniformityExcel } from '@/components/pages/production/uniformit
import Dropdown from '@/components/Dropdown';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
import { useFormik } from 'formik';
import {
UniformityTableFilterSchema,
type UniformityTableFilterValues,
} from '@/components/pages/production/uniformity/UniformityTableFilter.schema';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
const UniformityConfirmationPreview = ({
uniformity,
@@ -314,6 +321,37 @@ const UniformityTable = () => {
}
}, [projectFlockKandangLookup]);
// ===== FORMIK FILTER =====
const filterFormik = useFormik<UniformityTableFilterValues>({
initialValues: {
start_date: filterStartDate,
end_date: filterEndDate,
location: filterLocation,
location_id: Number(filterLocation?.value) || 0,
project_flock: filterProjectFlock,
project_flock_id: Number(filterProjectFlock?.value) || 0,
project_flock_kandang_id: filterProjectFlockKandangId,
kandang: filterKandang,
kandang_id: Number(filterKandang?.value) || 0,
},
validationSchema: UniformityTableFilterSchema,
enableReinitialize: true,
onSubmit: async (values) => {
setFilterStartDate(values.start_date);
setFilterEndDate(values.end_date);
setFilterLocation(values.location ?? null);
setFilterProjectFlock(values.project_flock ?? null);
setFilterKandang(values.kandang ?? null);
setIsSubmitted(true);
filterModal.closeModal();
},
});
// ===== FORMIK ERROR LIST =====
const { formErrorList, close, handleFormSubmit } =
useFormikErrorList(filterFormik);
// ===== BUILD SWR KEY WITH FILTERS =====
const uniformitySwrKey = useMemo(() => {
const basePath = UniformityApi.basePath;
@@ -370,29 +408,54 @@ const UniformityTable = () => {
const handleFilterLocationChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const location = val as OptionType | null;
const locationId = Number(location?.value) || 0;
filterFormik.setFieldValue('location', location);
filterFormik.setFieldValue('location_id', locationId);
setFilterLocation(location);
setFilterProjectFlock(null);
setFilterKandang(null);
setFilterProjectFlockLocationId(
location ? location.value.toString() : ''
);
filterFormik.setFieldValue('project_flock', null);
filterFormik.setFieldValue('project_flock_id', 0);
filterFormik.setFieldValue('kandang', null);
filterFormik.setFieldValue('kandang_id', 0);
},
[]
[filterFormik]
);
const handleFilterProjectFlockChange = useCallback(
(val: OptionType | OptionType[] | null) => {
setFilterProjectFlock(val as OptionType | null);
const projectFlock = val as OptionType | null;
const projectFlockId = Number(projectFlock?.value) || 0;
filterFormik.setFieldValue('project_flock', projectFlock);
filterFormik.setFieldValue('project_flock_id', projectFlockId);
setFilterProjectFlock(projectFlock);
setFilterKandang(null);
filterFormik.setFieldValue('kandang', null);
filterFormik.setFieldValue('kandang_id', 0);
},
[]
[filterFormik]
);
const handleFilterKandangChange = useCallback(
(val: OptionType | OptionType[] | null) => {
setFilterKandang(val as OptionType | null);
const kandang = val as OptionType | null;
const kandangId = Number(kandang?.value) || 0;
filterFormik.setFieldValue('kandang', kandang);
filterFormik.setFieldValue('kandang_id', kandangId);
setFilterKandang(kandang);
},
[]
[filterFormik]
);
const handleResetFilters = useCallback(() => {
@@ -403,41 +466,34 @@ const UniformityTable = () => {
setFilterProjectFlockKandangId(undefined);
setFilterStartDate('');
setFilterEndDate('');
}, []);
setFilterErrors({});
filterFormik.resetForm();
}, [filterFormik]);
const handleFilterStartDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setFilterStartDate(value);
filterFormik.setFieldValue('start_date', value);
},
[filterFormik]
);
const handleFilterEndDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setFilterEndDate(value);
filterFormik.setFieldValue('end_date', value);
},
[filterFormik]
);
const handleApplyFilters = useCallback(() => {
const errors: Record<string, string> = {};
if (!filterStartDate) {
errors.start_date = 'Tanggal mulai wajib diisi';
}
if (!filterEndDate) {
errors.end_date = 'Tanggal akhir wajib diisi';
}
if (!filterLocation) {
errors.location = 'Lokasi wajib dipilih';
}
if (!filterProjectFlock) {
errors.project_flock = 'Project Flock wajib dipilih';
}
if (!filterKandang) {
errors.kandang = 'Kandang wajib dipilih';
}
setFilterErrors(errors);
if (Object.keys(errors).length === 0) {
setIsSubmitted(true);
filterModal.closeModal();
}
}, [
filterModal,
filterStartDate,
filterEndDate,
filterLocation,
filterProjectFlock,
filterKandang,
]);
handleFormSubmit(
new Event('submit') as unknown as React.FormEvent<HTMLFormElement>
);
}, [handleFormSubmit]);
const selectedRowIds = useMemo(() => {
return Object.keys(rowSelection)
@@ -1134,42 +1190,46 @@ const UniformityTable = () => {
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
{/* Error List Alert */}
{formErrorList.length > 0 && (
<div className='w-full px-4'>
<AlertErrorList formErrorList={formErrorList} onClose={close} />
</div>
)}
<div className='space-y-4 px-4'>
<div className='grid grid-cols-1 sm:grid-cols-2 sm:gap-4'>
<div>
<DateInput
label='Tanggal'
name='start_date'
value={filterStartDate}
onChange={(e) => {
setFilterStartDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, start_date: '' }));
}}
value={filterFormik.values.start_date}
onChange={handleFilterStartDateChange}
onBlur={filterFormik.handleBlur}
isError={
filterFormik.touched.start_date &&
Boolean(filterFormik.errors.start_date)
}
errorMessage={filterFormik.errors.start_date}
className={{ wrapper: 'w-full' }}
/>
{filterErrors.start_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.start_date}
</p>
)}
</div>
<div>
<DateInput
label=' '
name='end_date'
value={filterEndDate}
onChange={(e) => {
setFilterEndDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, end_date: '' }));
}}
value={filterFormik.values.end_date}
onChange={handleFilterEndDateChange}
onBlur={filterFormik.handleBlur}
isError={
filterFormik.touched.end_date &&
Boolean(filterFormik.errors.end_date)
}
errorMessage={filterFormik.errors.end_date}
className={{ wrapper: 'w-full' }}
/>
{filterErrors.end_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.end_date}
</p>
)}
</div>
</div>
@@ -1177,65 +1237,65 @@ const UniformityTable = () => {
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi...'
value={filterLocation}
value={filterFormik.values.location}
onChange={(value) => {
handleFilterLocationChange(value);
setFilterErrors((prev) => ({ ...prev, location: '' }));
}}
options={filterLocationOptions}
onInputChange={setFilterLocationInputValue}
isLoading={isLoadingFilterLocations}
onMenuScrollToBottom={loadMoreFilterLocations}
isError={
filterFormik.touched.location &&
Boolean(filterFormik.errors.location)
}
errorMessage={filterFormik.errors.location}
isClearable
className={{ wrapper: 'w-full' }}
/>
{filterErrors.location && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.location}
</p>
)}
</div>
<div>
<SelectInput
label='Project Flock'
placeholder='Pilih Project Flock...'
value={filterProjectFlock}
value={filterFormik.values.project_flock}
onChange={(value) => {
handleFilterProjectFlockChange(value);
setFilterErrors((prev) => ({ ...prev, project_flock: '' }));
}}
options={filterProjectFlockOptions}
onInputChange={setFilterProjectFlockSearchValue}
isLoading={isLoadingFilterProjectFlocks}
onMenuScrollToBottom={loadMoreFilterProjectFlocks}
isDisabled={!filterLocation}
isDisabled={!filterFormik.values.location}
isError={
filterFormik.touched.project_flock &&
Boolean(filterFormik.errors.project_flock)
}
errorMessage={filterFormik.errors.project_flock}
isClearable
className={{ wrapper: 'w-full' }}
/>
{filterErrors.project_flock && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.project_flock}
</p>
)}
</div>
<div>
<SelectInput
label='Kandang'
placeholder='Pilih Kandang...'
value={filterKandang}
value={filterFormik.values.kandang}
onChange={(value) => {
handleFilterKandangChange(value);
setFilterErrors((prev) => ({ ...prev, kandang: '' }));
}}
options={filterKandangOptions}
isDisabled={!filterProjectFlock}
isDisabled={!filterFormik.values.project_flock}
isError={
filterFormik.touched.kandang &&
Boolean(filterFormik.errors.kandang)
}
errorMessage={filterFormik.errors.kandang}
isClearable
className={{ wrapper: 'w-full' }}
/>
{filterErrors.kandang && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.kandang}
</p>
)}
</div>
</div>
@@ -0,0 +1,66 @@
import { OptionType } from '@/components/input/SelectInput';
import * as yup from 'yup';
export type UniformityTableFilterType = {
start_date: string;
end_date: string;
location: OptionType | null;
location_id: number;
project_flock: OptionType | null;
project_flock_id: number;
project_flock_kandang_id: number | undefined;
kandang: OptionType | null;
kandang_id: number;
};
export const UniformityTableFilterSchema = yup.object({
start_date: yup.string().required('Tanggal mulai wajib diisi'),
end_date: yup.string().required('Tanggal akhir wajib diisi'),
location: yup
.mixed<OptionType>()
.required('Lokasi wajib dipilih')
.test('is-not-empty', 'Lokasi wajib dipilih', (value) => {
if (Array.isArray(value)) {
return value.length > 0;
}
return !!value;
}),
location_id: yup
.number()
.min(1, 'Location wajib diisi!')
.required('Location wajib diisi!')
.typeError('Location wajib diisi!'),
project_flock: yup
.mixed<OptionType>()
.required('Project Flock wajib dipilih')
.test('is-not-empty', 'Project Flock wajib dipilih', (value) => {
if (Array.isArray(value)) {
return value.length > 0;
}
return !!value;
}),
project_flock_id: yup
.number()
.min(1, 'Project flock wajib diisi!')
.required('Project flock wajib diisi!')
.typeError('Project flock wajib diisi!'),
project_flock_kandang_id: yup.number().optional(),
kandang: yup
.mixed<OptionType>()
.required('Kandang wajib dipilih')
.test('is-not-empty', 'Kandang wajib dipilih', (value) => {
if (Array.isArray(value)) {
return value.length > 0;
}
return !!value;
}),
kandang_id: yup
.number()
.min(1, 'Kandang wajib diisi!')
.required('Kandang wajib diisi!')
.typeError('Kandang wajib diisi!'),
}) as yup.ObjectSchema<UniformityTableFilterType>;
export type UniformityTableFilterValues = yup.InferType<
typeof UniformityTableFilterSchema
>;