mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +00:00
refactor(FE-Storyless): replace TextInput with NumberInput for price and tax fields, enhance form handling
This commit is contained in:
@@ -9,7 +9,11 @@ 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 NumberInput from '@/components/input/NumberInput';
|
||||||
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
|
||||||
@@ -24,7 +28,12 @@ import {
|
|||||||
CreateProductPayload,
|
CreateProductPayload,
|
||||||
UpdateProductPayload,
|
UpdateProductPayload,
|
||||||
} from '@/types/api/master-data/product';
|
} from '@/types/api/master-data/product';
|
||||||
import { UomApi, ProductCategoryApi, SupplierApi, ProductApi } from '@/services/api/master-data';
|
import {
|
||||||
|
UomApi,
|
||||||
|
ProductCategoryApi,
|
||||||
|
SupplierApi,
|
||||||
|
ProductApi,
|
||||||
|
} from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
|
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -67,30 +76,37 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
[router]
|
[router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const formikInitialValues = useMemo<ProductFormValues>(() => ({
|
const formikInitialValues = useMemo<ProductFormValues>(
|
||||||
name: initialValues?.name ?? '',
|
() => ({
|
||||||
brand: initialValues?.brand ?? '',
|
name: initialValues?.name ?? '',
|
||||||
sku: initialValues?.sku ?? '',
|
brand: initialValues?.brand ?? '',
|
||||||
uom: initialValues?.uom
|
sku: initialValues?.sku ?? '',
|
||||||
? { value: initialValues.uom.id, label: initialValues.uom.name }
|
uom: initialValues?.uom
|
||||||
: null,
|
? { value: initialValues.uom.id, label: initialValues.uom.name }
|
||||||
uom_id: initialValues?.uom?.id ?? 0,
|
: null,
|
||||||
product_category: initialValues?.product_category
|
uom_id: initialValues?.uom?.id ?? 0,
|
||||||
? { value: initialValues.product_category.id, label: initialValues.product_category.name }
|
product_category: initialValues?.product_category
|
||||||
: null,
|
? {
|
||||||
product_category_id: initialValues?.product_category?.id ?? 0,
|
value: initialValues.product_category.id,
|
||||||
product_price: initialValues?.product_price ?? 0,
|
label: initialValues.product_category.name,
|
||||||
selling_price: initialValues?.selling_price ?? 0,
|
}
|
||||||
tax: initialValues?.tax ?? 0,
|
: null,
|
||||||
expiry_period: initialValues?.expiry_period ?? 0,
|
product_category_id: initialValues?.product_category?.id ?? 0,
|
||||||
supplier: null, // not used for payload, just for UI
|
product_price: initialValues?.product_price ?? 0,
|
||||||
supplier_ids: initialValues?.suppliers?.map(s => s.id) ?? [],
|
selling_price: initialValues?.selling_price ?? 0,
|
||||||
flags: initialValues?.flags ?? [],
|
tax: initialValues?.tax ?? 0,
|
||||||
}), [initialValues]);
|
expiry_period: initialValues?.expiry_period ?? 0,
|
||||||
|
supplier: null, // not used for payload, just for UI
|
||||||
|
supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [],
|
||||||
|
flags: initialValues?.flags ?? [],
|
||||||
|
}),
|
||||||
|
[initialValues]
|
||||||
|
);
|
||||||
|
|
||||||
const formik = useFormik<ProductFormValues>({
|
const formik = useFormik<ProductFormValues>({
|
||||||
initialValues: formikInitialValues,
|
initialValues: formikInitialValues,
|
||||||
validationSchema: type === 'edit' ? UpdateProductFormSchema : ProductFormSchema,
|
validationSchema:
|
||||||
|
type === 'edit' ? UpdateProductFormSchema : ProductFormSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
setProductFormErrorMessage('');
|
setProductFormErrorMessage('');
|
||||||
const payload: CreateProductPayload = {
|
const payload: CreateProductPayload = {
|
||||||
@@ -99,12 +115,16 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
sku: values.sku,
|
sku: values.sku,
|
||||||
uom_id: values.uom_id,
|
uom_id: values.uom_id,
|
||||||
product_category_id: values.product_category_id,
|
product_category_id: values.product_category_id,
|
||||||
product_price: values.product_price,
|
product_price: parseInt(values.product_price.toString()) || 0,
|
||||||
selling_price: values.selling_price,
|
selling_price: parseInt(values.selling_price.toString()) || 0,
|
||||||
tax: values.tax,
|
tax: parseInt(values.tax.toString()) || 0,
|
||||||
expiry_period: values.expiry_period,
|
expiry_period: parseInt(values.expiry_period.toString()) || 0,
|
||||||
supplier_ids: (values.supplier_ids ?? []).filter((id): id is number => typeof id === 'number'),
|
supplier_ids: (values.supplier_ids ?? []).filter(
|
||||||
flags: (values.flags ?? []).filter((f): f is string => typeof f === 'string'),
|
(id): id is number => typeof id === 'number'
|
||||||
|
),
|
||||||
|
flags: (values.flags ?? []).filter(
|
||||||
|
(f): f is string => typeof f === 'string'
|
||||||
|
),
|
||||||
};
|
};
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'add':
|
case 'add':
|
||||||
@@ -120,12 +140,11 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
const { setValues: formikSetValues } = formik;
|
const { setValues: formikSetValues } = formik;
|
||||||
|
|
||||||
// UOM
|
// UOM
|
||||||
const [uomSelectInputValue, setUomSelectInputValue] = useState('');
|
const {
|
||||||
const uomsUrl = `${UomApi.basePath}?${new URLSearchParams({ search: uomSelectInputValue ?? '' }).toString()}`;
|
setInputValue: setUomSelectInputValue,
|
||||||
const { data: uoms, isLoading: isLoadingUoms } = useSWR(uomsUrl, UomApi.getAllFetcher);
|
options: uomOptions,
|
||||||
const uomOptions = isResponseSuccess(uoms)
|
isLoadingOptions: isLoadingUoms,
|
||||||
? uoms?.data.map((uom) => ({ value: uom.id, label: uom.name }))
|
} = useSelect(UomApi.basePath, 'id', 'name');
|
||||||
: [];
|
|
||||||
const uomChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const uomChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldTouched('uom', true);
|
formik.setFieldTouched('uom', true);
|
||||||
formik.setFieldValue('uom', val);
|
formik.setFieldValue('uom', val);
|
||||||
@@ -134,12 +153,11 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Product Category
|
// Product Category
|
||||||
const [categorySelectInputValue, setCategorySelectInputValue] = useState('');
|
const {
|
||||||
const categoriesUrl = `${ProductCategoryApi.basePath}?${new URLSearchParams({ search: categorySelectInputValue ?? '' }).toString()}`;
|
setInputValue: setCategorySelectInputValue,
|
||||||
const { data: categories, isLoading: isLoadingCategories } = useSWR(categoriesUrl, ProductCategoryApi.getAllFetcher);
|
options: categoryOptions,
|
||||||
const categoryOptions = isResponseSuccess(categories)
|
isLoadingOptions: isLoadingCategories,
|
||||||
? categories?.data.map((cat) => ({ value: cat.id, label: cat.name }))
|
} = useSelect(ProductCategoryApi.basePath, 'id', 'name');
|
||||||
: [];
|
|
||||||
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldTouched('product_category', true);
|
formik.setFieldTouched('product_category', true);
|
||||||
formik.setFieldValue('product_category', val);
|
formik.setFieldValue('product_category', val);
|
||||||
@@ -147,19 +165,25 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
formik.setFieldValue('product_category_id', (val as OptionType)?.value);
|
formik.setFieldValue('product_category_id', (val as OptionType)?.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Supplier (multi select)
|
// Supplier (multi select) - using SWR to filter by category
|
||||||
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
|
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
|
||||||
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue ?? '' }).toString()}`;
|
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue ?? '' }).toString()}`;
|
||||||
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(suppliersUrl, SupplierApi.getAllFetcher);
|
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
|
||||||
|
suppliersUrl,
|
||||||
|
SupplierApi.getAllFetcher
|
||||||
|
);
|
||||||
const supplierOptions = isResponseSuccess(suppliers)
|
const supplierOptions = isResponseSuccess(suppliers)
|
||||||
? suppliers?.data
|
? suppliers?.data
|
||||||
.filter((sup) => sup.category === 'SAPRONAK')
|
.filter((sup) => sup.category === 'SAPRONAK')
|
||||||
.map((sup) => ({ value: sup.id, label: sup.name }))
|
.map((sup) => ({ value: sup.id, label: sup.name }))
|
||||||
: [];
|
: [];
|
||||||
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||||
formik.setFieldTouched('supplier_ids', true);
|
formik.setFieldTouched('supplier_ids', true);
|
||||||
formik.setFieldValue('supplier_ids', arr.map((v) => (v as OptionType).value));
|
formik.setFieldValue(
|
||||||
|
'supplier_ids',
|
||||||
|
arr.map((v) => (v as OptionType).value)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteProductClickHandler = () => {
|
const deleteProductClickHandler = () => {
|
||||||
@@ -181,7 +205,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full max-w-xl'>
|
<section className='w-full max-w-2xl'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/product'
|
href='/master-data/product'
|
||||||
@@ -260,60 +284,88 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
options={categoryOptions}
|
options={categoryOptions}
|
||||||
onInputChange={setCategorySelectInputValue}
|
onInputChange={setCategorySelectInputValue}
|
||||||
isLoading={isLoadingCategories}
|
isLoading={isLoadingCategories}
|
||||||
isError={formik.touched.product_category_id && Boolean(formik.errors.product_category_id)}
|
isError={
|
||||||
|
formik.touched.product_category_id &&
|
||||||
|
Boolean(formik.errors.product_category_id)
|
||||||
|
}
|
||||||
errorMessage={formik.errors.product_category_id as string}
|
errorMessage={formik.errors.product_category_id as string}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Harga Produk'
|
label='Harga Produk'
|
||||||
name='product_price'
|
name='product_price'
|
||||||
type='number'
|
|
||||||
placeholder='Masukkan harga produk'
|
placeholder='Masukkan harga produk'
|
||||||
value={formik.values.product_price}
|
value={formik.values.product_price}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={formik.touched.product_price && Boolean(formik.errors.product_price)}
|
decimalScale={2}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
|
inputPrefix='Rp '
|
||||||
|
isError={
|
||||||
|
formik.touched.product_price &&
|
||||||
|
Boolean(formik.errors.product_price)
|
||||||
|
}
|
||||||
errorMessage={formik.errors.product_price as string}
|
errorMessage={formik.errors.product_price as string}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Harga Jual'
|
label='Harga Jual'
|
||||||
name='selling_price'
|
name='selling_price'
|
||||||
type='number'
|
|
||||||
placeholder='Masukkan harga jual'
|
placeholder='Masukkan harga jual'
|
||||||
value={formik.values.selling_price}
|
value={formik.values.selling_price}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={formik.touched.selling_price && Boolean(formik.errors.selling_price)}
|
decimalScale={2}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
|
inputPrefix='Rp '
|
||||||
|
isError={
|
||||||
|
formik.touched.selling_price &&
|
||||||
|
Boolean(formik.errors.selling_price)
|
||||||
|
}
|
||||||
errorMessage={formik.errors.selling_price as string}
|
errorMessage={formik.errors.selling_price as string}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Pajak (%)'
|
label='Pajak (%)'
|
||||||
name='tax'
|
name='tax'
|
||||||
type='number'
|
|
||||||
placeholder='Masukkan pajak'
|
placeholder='Masukkan pajak'
|
||||||
value={formik.values.tax}
|
value={formik.values.tax}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
decimalScale={2}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
|
inputSuffix='%'
|
||||||
isError={formik.touched.tax && Boolean(formik.errors.tax)}
|
isError={formik.touched.tax && Boolean(formik.errors.tax)}
|
||||||
errorMessage={formik.errors.tax as string}
|
errorMessage={formik.errors.tax as string}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Periode Kadaluarsa (hari)'
|
label='Periode Kadaluarsa (hari)'
|
||||||
name='expiry_period'
|
name='expiry_period'
|
||||||
type='number'
|
|
||||||
placeholder='Masukkan periode kadaluarsa'
|
placeholder='Masukkan periode kadaluarsa'
|
||||||
value={formik.values.expiry_period}
|
value={formik.values.expiry_period}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={formik.touched.expiry_period && Boolean(formik.errors.expiry_period)}
|
decimalScale={0}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
|
inputSuffix='hari'
|
||||||
|
isError={
|
||||||
|
formik.touched.expiry_period &&
|
||||||
|
Boolean(formik.errors.expiry_period)
|
||||||
|
}
|
||||||
errorMessage={formik.errors.expiry_period as string}
|
errorMessage={formik.errors.expiry_period as string}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
@@ -321,12 +373,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Supplier'
|
label='Supplier'
|
||||||
isMulti
|
isMulti
|
||||||
value={supplierOptions.filter(opt => formik.values.supplier_ids.includes(opt.value))}
|
value={supplierOptions.filter((opt) =>
|
||||||
|
formik.values.supplier_ids.includes(opt.value)
|
||||||
|
)}
|
||||||
onChange={supplierChangeHandler}
|
onChange={supplierChangeHandler}
|
||||||
options={supplierOptions}
|
options={supplierOptions}
|
||||||
onInputChange={setSupplierSelectInputValue}
|
onInputChange={setSupplierSelectInputValue}
|
||||||
isLoading={isLoadingSuppliers}
|
isLoading={isLoadingSuppliers}
|
||||||
isError={formik.touched.supplier_ids && Boolean(formik.errors.supplier_ids)}
|
isError={
|
||||||
|
formik.touched.supplier_ids &&
|
||||||
|
Boolean(formik.errors.supplier_ids)
|
||||||
|
}
|
||||||
errorMessage={formik.errors.supplier_ids as string}
|
errorMessage={formik.errors.supplier_ids as string}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
@@ -335,10 +392,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Flags'
|
label='Flags'
|
||||||
isMulti
|
isMulti
|
||||||
value={PRODUCT_FLAG_OPTIONS.filter(opt => formik.values.flags.includes(opt.value))}
|
value={PRODUCT_FLAG_OPTIONS.filter((opt) =>
|
||||||
onChange={val => {
|
formik.values.flags.includes(opt.value)
|
||||||
|
)}
|
||||||
|
onChange={(val) => {
|
||||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||||
formik.setFieldValue('flags', arr.map((v) => (v as OptionType).value));
|
formik.setFieldValue(
|
||||||
|
'flags',
|
||||||
|
arr.map((v) => (v as OptionType).value)
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
options={PRODUCT_FLAG_OPTIONS}
|
options={PRODUCT_FLAG_OPTIONS}
|
||||||
isError={formik.touched.flags && Boolean(formik.errors.flags)}
|
isError={formik.touched.flags && Boolean(formik.errors.flags)}
|
||||||
|
|||||||
Reference in New Issue
Block a user