refactor(FE): Tighten product form validation and layout

This commit is contained in:
rstubryan
2026-01-07 14:21:37 +07:00
parent 8b7ed9e46b
commit d049f6c34f
2 changed files with 195 additions and 179 deletions
@@ -29,36 +29,38 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
sku: Yup.string().required('SKU wajib diisi!'), sku: Yup.string().required('SKU wajib diisi!'),
uom: Yup.object({ uom: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1, 'Satuan wajib dipilih!').required(),
label: Yup.string().required(), label: Yup.string().required(),
}) })
.nullable() .nullable()
.required('Satuan wajib diisi!'), .required('Satuan wajib diisi!'),
uom_id: Yup.number() uom_id: Yup.number()
.min(1, 'Satuan wajib dipilih!')
.required('Satuan wajib diisi!') .required('Satuan wajib diisi!')
.typeError('Satuan wajib diisi!'), .typeError('Satuan wajib diisi!'),
product_category: Yup.object({ product_category: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1, 'Kategori produk wajib dipilih!').required(),
label: Yup.string().required(), label: Yup.string().required(),
}) })
.nullable() .nullable()
.required('Kategori produk wajib diisi!'), .required('Kategori produk wajib diisi!'),
product_category_id: Yup.number() product_category_id: Yup.number()
.min(1, 'Kategori produk wajib dipilih!')
.required('Kategori produk wajib diisi!') .required('Kategori produk wajib diisi!')
.typeError('Kategori produk wajib diisi!'), .typeError('Kategori produk wajib diisi!'),
product_price: Yup.number() product_price: Yup.number()
.required('Harga produk wajib diisi!') .required('Harga produk wajib diisi!')
.typeError('Harga produk wajib diisi!') .typeError('Harga produk wajib diisi!')
.min(0, 'Harga produk tidak boleh kurang dari 0!'), .min(1, 'Harga produk tidak boleh kurang dari 1!'),
selling_price: Yup.number() selling_price: Yup.number()
.required('Harga jual wajib diisi!') .required('Harga jual wajib diisi!')
.typeError('Harga jual wajib diisi!') .typeError('Harga jual wajib diisi!')
.min(0, 'Harga jual tidak boleh kurang dari 0!'), .min(1, 'Harga jual tidak boleh kurang dari 1!'),
tax: Yup.number() tax: Yup.number()
.required('Pajak wajib diisi!') .required('Pajak wajib diisi!')
@@ -69,7 +71,7 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
expiry_period: Yup.number() expiry_period: Yup.number()
.required('Periode kadaluarsa wajib diisi!') .required('Periode kadaluarsa wajib diisi!')
.typeError('Periode kadaluarsa wajib diisi!') .typeError('Periode kadaluarsa wajib diisi!')
.min(0, 'Periode kadaluarsa tidak boleh kurang dari 0!'), .min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'),
supplier_ids: Yup.array() supplier_ids: Yup.array()
.of(Yup.number().required().typeError('Supplier tidak valid!')) .of(Yup.number().required().typeError('Supplier tidak valid!'))
@@ -234,7 +234,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
<span>{productFormErrorMessage}</span> <span>{productFormErrorMessage}</span>
</div> </div>
)} )}
<div className='flex flex-col gap-4'> <div className='grid grid-cols-1 gap-4'>
<TextInput <TextInput
required required
label='Nama' label='Nama'
@@ -247,179 +247,193 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
errorMessage={formik.errors.name} errorMessage={formik.errors.name}
readOnly={type === 'detail'} readOnly={type === 'detail'}
/> />
<TextInput <div className='grid grid-cols-2 gap-4'>
required <TextInput
label='Merek' required
name='brand' label='Merek'
placeholder='Masukkan merek...' name='brand'
value={formik.values.brand} placeholder='Masukkan merek...'
onChange={formik.handleChange} value={formik.values.brand}
onBlur={formik.handleBlur} onChange={formik.handleChange}
isError={formik.touched.brand && Boolean(formik.errors.brand)} onBlur={formik.handleBlur}
errorMessage={formik.errors.brand} isError={formik.touched.brand && Boolean(formik.errors.brand)}
readOnly={type === 'detail'} errorMessage={formik.errors.brand}
/> readOnly={type === 'detail'}
<TextInput />
required <TextInput
label='SKU' required
name='sku' label='SKU'
placeholder='Masukkan SKU...' name='sku'
value={formik.values.sku} placeholder='Masukkan SKU...'
onChange={formik.handleChange} value={formik.values.sku}
onBlur={formik.handleBlur} onChange={formik.handleChange}
isError={formik.touched.sku && Boolean(formik.errors.sku)} onBlur={formik.handleBlur}
errorMessage={formik.errors.sku} isError={formik.touched.sku && Boolean(formik.errors.sku)}
readOnly={type === 'detail'} errorMessage={formik.errors.sku}
/> readOnly={type === 'detail'}
<SelectInput />
required </div>
label='Satuan' <div className='grid grid-cols-2 gap-4'>
placeholder='Pilih satuan...' <SelectInput
value={formik.values.uom ?? undefined} required
onChange={uomChangeHandler} label='Satuan'
options={uomOptions} placeholder='Pilih satuan...'
onInputChange={setUomSelectInputValue} value={formik.values.uom ?? undefined}
isLoading={isLoadingUoms} onChange={uomChangeHandler}
isError={formik.touched.uom_id && Boolean(formik.errors.uom_id)} options={uomOptions}
errorMessage={formik.errors.uom_id as string} onInputChange={setUomSelectInputValue}
isDisabled={type === 'detail'} isLoading={isLoadingUoms}
isClearable isError={
/> (formik.touched.uom || formik.touched.uom_id) &&
<SelectInput Boolean(formik.errors.uom_id)
required }
label='Kategori Produk' errorMessage={formik.errors.uom_id as string}
placeholder='Pilih kategori produk...' isDisabled={type === 'detail'}
value={formik.values.product_category ?? undefined} isClearable
onChange={categoryChangeHandler} />
options={categoryOptions} <SelectInput
onInputChange={setCategorySelectInputValue} required
isLoading={isLoadingCategories} label='Kategori Produk'
isError={ placeholder='Pilih kategori produk...'
formik.touched.product_category_id && value={formik.values.product_category ?? undefined}
Boolean(formik.errors.product_category_id) onChange={categoryChangeHandler}
} options={categoryOptions}
errorMessage={formik.errors.product_category_id as string} onInputChange={setCategorySelectInputValue}
isDisabled={type === 'detail'} isLoading={isLoadingCategories}
isClearable isError={
/> (formik.touched.product_category ||
<NumberInput formik.touched.product_category_id) &&
required Boolean(formik.errors.product_category_id)
label='Harga Produk' }
name='product_price' errorMessage={formik.errors.product_category_id as string}
placeholder='Masukkan harga produk...' isDisabled={type === 'detail'}
value={formik.values.product_price} isClearable
onChange={formik.handleChange} />
onBlur={formik.handleBlur} </div>
decimalScale={2} <div className='grid grid-cols-2 gap-4'>
allowNegative={false} <NumberInput
thousandSeparator=',' required
decimalSeparator='.' label='Harga Produk'
inputPrefix='Rp ' name='product_price'
isError={ placeholder='Masukkan harga produk...'
formik.touched.product_price && value={formik.values.product_price}
Boolean(formik.errors.product_price) onChange={formik.handleChange}
} onBlur={formik.handleBlur}
errorMessage={formik.errors.product_price as string} decimalScale={2}
readOnly={type === 'detail'} allowNegative={false}
/> thousandSeparator=','
<NumberInput decimalSeparator='.'
required inputPrefix='Rp '
label='Harga Jual' isError={
name='selling_price' formik.touched.product_price &&
placeholder='Masukkan harga jual...' Boolean(formik.errors.product_price)
value={formik.values.selling_price} }
onChange={formik.handleChange} errorMessage={formik.errors.product_price as string}
onBlur={formik.handleBlur} readOnly={type === 'detail'}
decimalScale={2} />
allowNegative={false} <NumberInput
thousandSeparator=',' required
decimalSeparator='.' label='Harga Jual'
inputPrefix='Rp ' name='selling_price'
isError={ placeholder='Masukkan harga jual...'
formik.touched.selling_price && value={formik.values.selling_price}
Boolean(formik.errors.selling_price) onChange={formik.handleChange}
} onBlur={formik.handleBlur}
errorMessage={formik.errors.selling_price as string} decimalScale={2}
readOnly={type === 'detail'} allowNegative={false}
/> thousandSeparator=','
<NumberInput decimalSeparator='.'
required inputPrefix='Rp '
label='Pajak (%)' isError={
name='tax' formik.touched.selling_price &&
placeholder='Masukkan pajak...' Boolean(formik.errors.selling_price)
value={formik.values.tax} }
onChange={formik.handleChange} errorMessage={formik.errors.selling_price as string}
onBlur={formik.handleBlur} readOnly={type === 'detail'}
decimalScale={2} />
allowNegative={false} </div>
thousandSeparator=',' <div className='grid grid-cols-2 gap-4'>
decimalSeparator='.' <NumberInput
inputSuffix='%' required
isError={formik.touched.tax && Boolean(formik.errors.tax)} label='Pajak (%)'
errorMessage={formik.errors.tax as string} name='tax'
readOnly={type === 'detail'} placeholder='Masukkan pajak...'
/> value={formik.values.tax}
<NumberInput onChange={formik.handleChange}
required onBlur={formik.handleBlur}
label='Periode Kadaluarsa (hari)' decimalScale={2}
name='expiry_period' allowNegative={false}
placeholder='Masukkan periode kadaluarsa...' thousandSeparator=','
value={formik.values.expiry_period} decimalSeparator='.'
onChange={formik.handleChange} inputSuffix='%'
onBlur={formik.handleBlur} isError={formik.touched.tax && Boolean(formik.errors.tax)}
decimalScale={0} errorMessage={formik.errors.tax as string}
allowNegative={false} readOnly={type === 'detail'}
thousandSeparator=',' />
decimalSeparator='.' <NumberInput
inputSuffix='hari' required
isError={ label='Periode Kadaluarsa (hari)'
formik.touched.expiry_period && name='expiry_period'
Boolean(formik.errors.expiry_period) placeholder='Masukkan periode kadaluarsa...'
} value={formik.values.expiry_period}
errorMessage={formik.errors.expiry_period as string} onChange={formik.handleChange}
readOnly={type === 'detail'} onBlur={formik.handleBlur}
/> decimalScale={0}
<SelectInput allowNegative={false}
required thousandSeparator=','
label='Supplier' decimalSeparator='.'
placeholder='Pilih supplier...' inputSuffix='hari'
isMulti isError={
value={supplierOptions.filter((opt) => formik.touched.expiry_period &&
(formik.values.supplier_ids || []).includes(opt.value) Boolean(formik.errors.expiry_period)
)} }
onChange={supplierChangeHandler} errorMessage={formik.errors.expiry_period as string}
options={supplierOptions} readOnly={type === 'detail'}
onInputChange={setSupplierSelectInputValue} />
isLoading={isLoadingSuppliers} </div>
isError={ <div className='grid grid-cols-2 gap-4'>
formik.touched.supplier_ids && <SelectInput
Boolean(formik.errors.supplier_ids) required
} label='Supplier'
errorMessage={formik.errors.supplier_ids as string} placeholder='Pilih supplier...'
isDisabled={type === 'detail'} isMulti
isClearable value={supplierOptions.filter((opt) =>
/> (formik.values.supplier_ids || []).includes(opt.value)
<SelectInput )}
required onChange={supplierChangeHandler}
label='Flags' options={supplierOptions}
placeholder='Pilih flags...' onInputChange={setSupplierSelectInputValue}
isMulti isLoading={isLoadingSuppliers}
value={PRODUCT_FLAG_OPTIONS.filter((opt) => isError={
(formik.values.flags || []).includes(opt.value) formik.touched.supplier_ids &&
)} Boolean(formik.errors.supplier_ids)
onChange={(val) => { }
const arr = Array.isArray(val) ? val : val ? [val] : []; errorMessage={formik.errors.supplier_ids as string}
formik.setFieldValue( isDisabled={type === 'detail'}
'flags', isClearable
arr.map((v) => (v as OptionType).value) />
); <SelectInput
}} required
options={PRODUCT_FLAG_OPTIONS} label='Flags'
isError={formik.touched.flags && Boolean(formik.errors.flags)} placeholder='Pilih flags...'
errorMessage={formik.errors.flags as string} isMulti
isDisabled={type === 'detail'} value={PRODUCT_FLAG_OPTIONS.filter((opt) =>
isClearable (formik.values.flags || []).includes(opt.value)
/> )}
onChange={(val) => {
const arr = Array.isArray(val) ? val : val ? [val] : [];
formik.setFieldValue(
'flags',
arr.map((v) => (v as OptionType).value)
);
}}
options={PRODUCT_FLAG_OPTIONS}
isError={formik.touched.flags && Boolean(formik.errors.flags)}
errorMessage={formik.errors.flags as string}
isDisabled={type === 'detail'}
isClearable
/>
</div>
</div> </div>
<div className='flex flex-row justify-between gap-2 flex-wrap'> <div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && ( {type !== 'add' && (