refactor(FE): Improve expense form validation messages

This commit is contained in:
rstubryan
2026-01-17 21:30:03 +07:00
parent df550abc46
commit ccf535cbd9
3 changed files with 107 additions and 9 deletions
@@ -41,11 +41,16 @@ type ExpenseFormSchemaType = {
export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
Yup.object({
category: Yup.object({
value: Yup.string().oneOf(['BOP', 'NON-BOP']).required(),
label: Yup.string().oneOf(['BOP', 'NON-BOP']).required(),
value: Yup.string()
.oneOf(['BOP', 'NON-BOP'])
.required('Kategori wajib diisi!'),
label: Yup.string()
.oneOf(['BOP', 'NON-BOP'])
.required('Kategori wajib diisi!'),
})
.nullable()
.optional(),
.required('Kategori wajib diisi!')
.typeError('Kategori wajib diisi!'),
location: Yup.object({
value: Yup.number().min(1).required(),
@@ -104,9 +109,12 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
.of(
Yup.object({
nonstock: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
value: Yup.number().min(1).required('Nonstock wajib diisi!'),
label: Yup.string().required('Nonstock wajib diisi!'),
})
.nullable()
.required('Nonstock wajib diisi!')
.typeError('Nonstock wajib diisi!'),
nonstock_id: Yup.number()
.required('Nonstock wajib diisi!')
.min(1, 'Nonstock wajib diisi!')
@@ -192,6 +192,7 @@ const ExpenseRequestForm = ({
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('location', true);
formik.setFieldTouched('location_id', true);
formik.setFieldValue('location', val);
const locationId = Array.isArray(val) ? val[0]?.value : val?.value;
@@ -268,6 +269,7 @@ const ExpenseRequestForm = ({
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('supplier', true);
formik.setFieldTouched('supplier_id', true);
formik.setFieldValue('supplier', val);
const supplierId = Array.isArray(val) ? val[0]?.value : val?.value;
@@ -407,6 +409,16 @@ const ExpenseRequestForm = ({
placeholder='Pilih Kategori'
value={formik.values.category}
onChange={categoryChangeHandler}
isError={
formik.touched.category && Boolean(formik.errors.category)
}
errorMessage={
formik.touched.category && formik.errors.category
? typeof formik.errors.category === 'object'
? 'Kategori wajib diisi!'
: (formik.errors.category as string)
: undefined
}
options={[
{
value: 'BOP',
@@ -426,6 +438,10 @@ const ExpenseRequestForm = ({
placeholder='Pilih Lokasi'
value={formik.values.location}
onChange={locationChangeHandler}
isError={
formik.touched.location_id && Boolean(formik.errors.location_id)
}
errorMessage={formik.errors.location_id as string}
options={locationOptions}
isLoading={isLoadingLocationOptions}
onInputChange={setLocationInputValue}
@@ -438,6 +454,12 @@ const ExpenseRequestForm = ({
required
value={formik.values.transaction_date}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
formik.touched.transaction_date &&
Boolean(formik.errors.transaction_date)
}
errorMessage={formik.errors.transaction_date as string}
className={{
wrapper: 'col-span-12 sm:col-span-4',
}}
@@ -459,6 +481,16 @@ const ExpenseRequestForm = ({
placeholder='Pilih Vendor'
value={formik.values.supplier}
onChange={supplierChangeHandler}
isError={
formik.touched.supplier_id && Boolean(formik.errors.supplier_id)
}
errorMessage={
formik.touched.supplier_id && formik.errors.supplier_id
? typeof formik.errors.supplier_id === 'object'
? 'Vendor wajib diisi!'
: (formik.errors.supplier_id as string)
: undefined
}
options={supplierOptions}
isLoading={isLoadingVendorOptions}
onInputChange={setVendorInputValue}
@@ -55,6 +55,10 @@ const ExpenseRequestKandangDetailExpense: React.FC<
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
true
);
formik.setFieldTouched(
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock_id`,
true
);
formik.setFieldValue(
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
val
@@ -105,11 +109,14 @@ const ExpenseRequestKandangDetailExpense: React.FC<
expenseIdx
]?.[column] &&
Boolean(
formik.errors.expense_nonstocks?.[kandangExpenseIdx] instanceof
Object &&
formik.errors.expense_nonstocks?.[kandangExpenseIdx] &&
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx] ===
'object' &&
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
expenseIdx
] instanceof Object &&
] &&
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx]
.cost_items?.[expenseIdx] === 'object' &&
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
expenseIdx
]?.[column]
@@ -117,6 +124,32 @@ const ExpenseRequestKandangDetailExpense: React.FC<
);
};
const getExpenseRepeaterErrorMessage = (
column: 'nonstock' | 'quantity' | 'price' | 'notes',
kandangExpenseIdx: number,
expenseIdx: number
): string => {
const kandangError = formik.errors.expense_nonstocks?.[kandangExpenseIdx];
if (!kandangError || typeof kandangError !== 'object') return '';
if (!('cost_items' in kandangError)) return '';
const costItemsError = kandangError.cost_items?.[expenseIdx];
if (!costItemsError || typeof costItemsError !== 'object') return '';
const fieldError = costItemsError[column as keyof typeof costItemsError];
if (!fieldError) return '';
if (typeof fieldError === 'object' && fieldError !== null) {
return 'Nonstock wajib diisi!';
}
return String(fieldError);
};
return (
<Card
className={{
@@ -202,6 +235,16 @@ const ExpenseRequestKandangDetailExpense: React.FC<
val
);
}}
isError={isExpenseRepeaterInputError(
'nonstock',
kandangExpenseIdx,
expenseIdx
)}
errorMessage={getExpenseRepeaterErrorMessage(
'nonstock',
kandangExpenseIdx,
expenseIdx
)}
options={nonstockOptions}
isLoading={isLoadingNonstockOptions}
onInputChange={setNonstockInputValue}
@@ -226,6 +269,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
kandangExpenseIdx,
expenseIdx
)}
errorMessage={getExpenseRepeaterErrorMessage(
'quantity',
kandangExpenseIdx,
expenseIdx
)}
className={{ wrapper: 'min-w-24' }}
/>
</td>
@@ -246,6 +294,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
kandangExpenseIdx,
expenseIdx
)}
errorMessage={getExpenseRepeaterErrorMessage(
'price',
kandangExpenseIdx,
expenseIdx
)}
inputPrefix={
<span className='text-gray-600 font-medium'>
Rp
@@ -271,6 +324,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
kandangExpenseIdx,
expenseIdx
)}
errorMessage={getExpenseRepeaterErrorMessage(
'notes',
kandangExpenseIdx,
expenseIdx
)}
className={{ wrapper: 'min-w-24' }}
/>
</td>