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 Dropdown from '@/components/Dropdown';
import Menu from '@/components/menu/Menu'; import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem'; 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 = ({ const UniformityConfirmationPreview = ({
uniformity, uniformity,
@@ -314,6 +321,37 @@ const UniformityTable = () => {
} }
}, [projectFlockKandangLookup]); }, [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 ===== // ===== BUILD SWR KEY WITH FILTERS =====
const uniformitySwrKey = useMemo(() => { const uniformitySwrKey = useMemo(() => {
const basePath = UniformityApi.basePath; const basePath = UniformityApi.basePath;
@@ -370,29 +408,54 @@ const UniformityTable = () => {
const handleFilterLocationChange = useCallback( const handleFilterLocationChange = useCallback(
(val: OptionType | OptionType[] | null) => { (val: OptionType | OptionType[] | null) => {
const location = val as 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); setFilterLocation(location);
setFilterProjectFlock(null); setFilterProjectFlock(null);
setFilterKandang(null); setFilterKandang(null);
setFilterProjectFlockLocationId( setFilterProjectFlockLocationId(
location ? location.value.toString() : '' 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( const handleFilterProjectFlockChange = useCallback(
(val: OptionType | OptionType[] | null) => { (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); setFilterKandang(null);
filterFormik.setFieldValue('kandang', null);
filterFormik.setFieldValue('kandang_id', 0);
}, },
[] [filterFormik]
); );
const handleFilterKandangChange = useCallback( const handleFilterKandangChange = useCallback(
(val: OptionType | OptionType[] | null) => { (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(() => { const handleResetFilters = useCallback(() => {
@@ -403,41 +466,34 @@ const UniformityTable = () => {
setFilterProjectFlockKandangId(undefined); setFilterProjectFlockKandangId(undefined);
setFilterStartDate(''); setFilterStartDate('');
setFilterEndDate(''); 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 handleApplyFilters = useCallback(() => {
const errors: Record<string, string> = {}; handleFormSubmit(
new Event('submit') as unknown as React.FormEvent<HTMLFormElement>
if (!filterStartDate) { );
errors.start_date = 'Tanggal mulai wajib diisi'; }, [handleFormSubmit]);
}
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,
]);
const selectedRowIds = useMemo(() => { const selectedRowIds = useMemo(() => {
return Object.keys(rowSelection) return Object.keys(rowSelection)
@@ -1134,42 +1190,46 @@ const UniformityTable = () => {
<Icon icon='heroicons:x-mark' width={20} height={20} /> <Icon icon='heroicons:x-mark' width={20} height={20} />
</Button> </Button>
</div> </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='space-y-4 px-4'>
<div className='grid grid-cols-1 sm:grid-cols-2 sm:gap-4'> <div className='grid grid-cols-1 sm:grid-cols-2 sm:gap-4'>
<div> <div>
<DateInput <DateInput
label='Tanggal' label='Tanggal'
name='start_date' name='start_date'
value={filterStartDate} value={filterFormik.values.start_date}
onChange={(e) => { onChange={handleFilterStartDateChange}
setFilterStartDate(e.target.value); onBlur={filterFormik.handleBlur}
setFilterErrors((prev) => ({ ...prev, start_date: '' })); isError={
}} filterFormik.touched.start_date &&
Boolean(filterFormik.errors.start_date)
}
errorMessage={filterFormik.errors.start_date}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
/> />
{filterErrors.start_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.start_date}
</p>
)}
</div> </div>
<div> <div>
<DateInput <DateInput
label=' ' label=' '
name='end_date' name='end_date'
value={filterEndDate} value={filterFormik.values.end_date}
onChange={(e) => { onChange={handleFilterEndDateChange}
setFilterEndDate(e.target.value); onBlur={filterFormik.handleBlur}
setFilterErrors((prev) => ({ ...prev, end_date: '' })); isError={
}} filterFormik.touched.end_date &&
Boolean(filterFormik.errors.end_date)
}
errorMessage={filterFormik.errors.end_date}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
/> />
{filterErrors.end_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.end_date}
</p>
)}
</div> </div>
</div> </div>
@@ -1177,65 +1237,65 @@ const UniformityTable = () => {
<SelectInput <SelectInput
label='Lokasi' label='Lokasi'
placeholder='Pilih Lokasi...' placeholder='Pilih Lokasi...'
value={filterLocation} value={filterFormik.values.location}
onChange={(value) => { onChange={(value) => {
handleFilterLocationChange(value); handleFilterLocationChange(value);
setFilterErrors((prev) => ({ ...prev, location: '' }));
}} }}
options={filterLocationOptions} options={filterLocationOptions}
onInputChange={setFilterLocationInputValue} onInputChange={setFilterLocationInputValue}
isLoading={isLoadingFilterLocations} isLoading={isLoadingFilterLocations}
onMenuScrollToBottom={loadMoreFilterLocations} onMenuScrollToBottom={loadMoreFilterLocations}
isError={
filterFormik.touched.location &&
Boolean(filterFormik.errors.location)
}
errorMessage={filterFormik.errors.location}
isClearable
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
/> />
{filterErrors.location && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.location}
</p>
)}
</div> </div>
<div> <div>
<SelectInput <SelectInput
label='Project Flock' label='Project Flock'
placeholder='Pilih Project Flock...' placeholder='Pilih Project Flock...'
value={filterProjectFlock} value={filterFormik.values.project_flock}
onChange={(value) => { onChange={(value) => {
handleFilterProjectFlockChange(value); handleFilterProjectFlockChange(value);
setFilterErrors((prev) => ({ ...prev, project_flock: '' }));
}} }}
options={filterProjectFlockOptions} options={filterProjectFlockOptions}
onInputChange={setFilterProjectFlockSearchValue} onInputChange={setFilterProjectFlockSearchValue}
isLoading={isLoadingFilterProjectFlocks} isLoading={isLoadingFilterProjectFlocks}
onMenuScrollToBottom={loadMoreFilterProjectFlocks} 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' }} className={{ wrapper: 'w-full' }}
/> />
{filterErrors.project_flock && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.project_flock}
</p>
)}
</div> </div>
<div> <div>
<SelectInput <SelectInput
label='Kandang' label='Kandang'
placeholder='Pilih Kandang...' placeholder='Pilih Kandang...'
value={filterKandang} value={filterFormik.values.kandang}
onChange={(value) => { onChange={(value) => {
handleFilterKandangChange(value); handleFilterKandangChange(value);
setFilterErrors((prev) => ({ ...prev, kandang: '' }));
}} }}
options={filterKandangOptions} 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' }} className={{ wrapper: 'w-full' }}
/> />
{filterErrors.kandang && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.kandang}
</p>
)}
</div> </div>
</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
>;