mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +00:00
Merge branch 'dev/randy' into 'feat/FE/US-34/stock-adjustment'
[FEAT/FE][US#33][TASK#51-54] Form Validation and UI/UX Adjustment See merge request mbugroup/lti-web-client!10
This commit is contained in:
@@ -1,28 +1,38 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ComponentType, ReactNode, useEffect, useMemo, useState } from 'react';
|
import {
|
||||||
import Select, { OptionProps, GroupBase, InputActionMeta } from 'react-select';
|
ComponentType,
|
||||||
|
ReactNode,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import Select, {
|
||||||
|
OptionProps,
|
||||||
|
GroupBase,
|
||||||
|
InputActionMeta,
|
||||||
|
MultiValue,
|
||||||
|
SingleValue,
|
||||||
|
} from 'react-select';
|
||||||
|
import CreatableSelect from 'react-select/creatable';
|
||||||
import makeAnimated from 'react-select/animated';
|
import makeAnimated from 'react-select/animated';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
export interface OptionType {
|
export interface OptionType {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
label: string;
|
label: string;
|
||||||
className?: string; // for multi select
|
className?: string;
|
||||||
labelClassName?: string; // for multi select
|
labelClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OptionComponent<T = OptionType> = ComponentType<
|
export type OptionComponent<T = OptionType> = ComponentType<
|
||||||
OptionProps<T, boolean, GroupBase<T>>
|
OptionProps<T, boolean, GroupBase<T>>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
interface SelectInputProps<T = OptionType> {
|
interface SelectInputBaseProps<T = OptionType> {
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
bottomLabel?: ReactNode;
|
bottomLabel?: ReactNode;
|
||||||
value?: T | T[];
|
|
||||||
onChange?: (val: T | T[] | null) => void;
|
|
||||||
options: T[];
|
options: T[];
|
||||||
optionComponent?: OptionComponent<T>;
|
optionComponent?: OptionComponent<T>;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
@@ -46,9 +56,16 @@ interface SelectInputProps<T = OptionType> {
|
|||||||
onInputChange?: (search: string) => void;
|
onInputChange?: (search: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
|
||||||
|
createables?: boolean;
|
||||||
|
value?: T | T[] | null;
|
||||||
|
onChange?: (val: T | T[] | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
const animatedComponents = makeAnimated();
|
const animatedComponents = makeAnimated();
|
||||||
|
|
||||||
const SelectInput = <T extends OptionType>({
|
const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||||
|
const {
|
||||||
label,
|
label,
|
||||||
bottomLabel,
|
bottomLabel,
|
||||||
value,
|
value,
|
||||||
@@ -69,29 +86,48 @@ const SelectInput = <T extends OptionType>({
|
|||||||
isAnimated = true,
|
isAnimated = true,
|
||||||
openMenu,
|
openMenu,
|
||||||
delay = 300,
|
delay = 300,
|
||||||
|
createables = false,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
}: SelectInputProps) => {
|
} = props;
|
||||||
const [internalInputValue, setInternalInputValue] = useState('');
|
|
||||||
|
|
||||||
const [debouncedInputValue] = useDebounce(internalInputValue, delay ?? 300);
|
const [internalInputValue, setInternalInputValue] = useState('');
|
||||||
|
const [debouncedInputValue] = useDebounce(internalInputValue, delay);
|
||||||
|
|
||||||
const components = useMemo(() => {
|
const components = useMemo(() => {
|
||||||
const base = isAnimated ? animatedComponents : {};
|
const base = isAnimated ? animatedComponents : {};
|
||||||
|
return { ...base, IndicatorSeparator: () => null };
|
||||||
return {
|
|
||||||
...base,
|
|
||||||
IndicatorSeparator: () => null,
|
|
||||||
};
|
|
||||||
}, [isAnimated]);
|
}, [isAnimated]);
|
||||||
|
|
||||||
const internalInputChangeHandler = (value: string, meta: InputActionMeta) => {
|
const internalInputChangeHandler = (
|
||||||
if (meta.action === 'input-change') setInternalInputValue(value);
|
val: string,
|
||||||
|
meta: InputActionMeta
|
||||||
|
) => {
|
||||||
|
if (meta.action === 'input-change') setInternalInputValue(val);
|
||||||
if (meta.action === 'menu-close') setInternalInputValue('');
|
if (meta.action === 'menu-close') setInternalInputValue('');
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onInputChange?.(debouncedInputValue);
|
onInputChange?.(debouncedInputValue);
|
||||||
}, [debouncedInputValue]);
|
}, [onInputChange, debouncedInputValue]);
|
||||||
|
|
||||||
|
const SelectComponent = createables ? CreatableSelect : Select;
|
||||||
|
|
||||||
|
/** 🎯 handleChange tanpa any */
|
||||||
|
const handleChange = (
|
||||||
|
val: MultiValue<T> | SingleValue<T>
|
||||||
|
): void => {
|
||||||
|
if (!val) {
|
||||||
|
onChange?.(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMulti) {
|
||||||
|
onChange?.(val as T[]);
|
||||||
|
} else {
|
||||||
|
onChange?.(val as T);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -103,28 +139,23 @@ const SelectInput = <T extends OptionType>({
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full text-sm font-normal leading-5',
|
'w-full text-sm font-normal leading-5',
|
||||||
{
|
{ 'text-error': isError },
|
||||||
'text-error': isError,
|
|
||||||
},
|
|
||||||
className?.label
|
className?.label
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{required && (
|
{required && (
|
||||||
<>
|
<span className="tooltip tooltip-error" data-tip="required">
|
||||||
{' '}
|
<span className="text-error"> *</span>
|
||||||
<span className='tooltip tooltip-error' data-tip='required'>
|
|
||||||
<span className='text-error'> *</span>
|
|
||||||
</span>
|
</span>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Select
|
<SelectComponent<T, boolean, GroupBase<T>>
|
||||||
instanceId='select'
|
instanceId="select"
|
||||||
value={value}
|
value={value ?? (isMulti ? [] : null)}
|
||||||
onChange={(val) => onChange?.(val as T)}
|
onChange={handleChange}
|
||||||
options={options}
|
options={options}
|
||||||
menuIsOpen={openMenu}
|
menuIsOpen={openMenu}
|
||||||
inputValue={internalInputValue}
|
inputValue={internalInputValue}
|
||||||
@@ -136,14 +167,13 @@ const SelectInput = <T extends OptionType>({
|
|||||||
isRtl={isRtl}
|
isRtl={isRtl}
|
||||||
isSearchable={isSearchable}
|
isSearchable={isSearchable}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={cn('w-full', className)}
|
className={cn('w-full', className?.select)}
|
||||||
classNames={{
|
classNames={{
|
||||||
control: ({ isFocused, isDisabled }) =>
|
control: ({ isFocused, isDisabled }) =>
|
||||||
cn(
|
cn(
|
||||||
'w-full min-h-12! rounded-lg! border bg-white transition-shadow cursor-pointer!',
|
'w-full min-h-12! rounded-lg! border bg-white transition-shadow cursor-pointer!',
|
||||||
{
|
{
|
||||||
'border-red-500! focus-within:border-red-500 focus-within:ring-2 focus-within:ring-red-200':
|
'border-red-500! ring-2 ring-red-200': isError,
|
||||||
isError,
|
|
||||||
'border-indigo-500 ring-2 ring-indigo-200': isFocused,
|
'border-indigo-500 ring-2 ring-indigo-200': isFocused,
|
||||||
'border-gray-300': !isError && !isFocused,
|
'border-gray-300': !isError && !isFocused,
|
||||||
'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled,
|
'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled,
|
||||||
@@ -156,8 +186,6 @@ const SelectInput = <T extends OptionType>({
|
|||||||
cn({ 'text-gray-900': !isError, 'text-error!': isError }),
|
cn({ 'text-gray-900': !isError, 'text-error!': isError }),
|
||||||
input: () => cn('text-gray-900'),
|
input: () => cn('text-gray-900'),
|
||||||
indicatorsContainer: () => cn('flex items-center gap-1 pr-2'),
|
indicatorsContainer: () => cn('flex items-center gap-1 pr-2'),
|
||||||
indicatorSeparator: () => cn('mx-1 h-4 w-px bg-gray-200'),
|
|
||||||
clearIndicator: () => cn('p-1 rounded-md hover:bg-gray-100'),
|
|
||||||
dropdownIndicator: ({ isFocused }) =>
|
dropdownIndicator: ({ isFocused }) =>
|
||||||
cn('p-1 rounded-md hover:bg-gray-100', {
|
cn('p-1 rounded-md hover:bg-gray-100', {
|
||||||
'text-gray-900': isFocused,
|
'text-gray-900': isFocused,
|
||||||
@@ -165,55 +193,41 @@ const SelectInput = <T extends OptionType>({
|
|||||||
'text-error!': isError,
|
'text-error!': isError,
|
||||||
}),
|
}),
|
||||||
menu: () =>
|
menu: () =>
|
||||||
cn(
|
cn('border border-gray-200 rounded-lg bg-white shadow-lg!'),
|
||||||
'border border-gray-200 rounded-lg bg-white shadow-lg rounded-lg!'
|
|
||||||
),
|
|
||||||
menuList: () => cn('p-2! max-h-60 overflow-auto'),
|
menuList: () => cn('p-2! max-h-60 overflow-auto'),
|
||||||
groupHeading: () =>
|
option: ({ isFocused, isSelected }) =>
|
||||||
cn('ml-2 mt-2 mb-1 text-xs font-medium text-gray-500'),
|
cn('mt-1 px-3 py-2 rounded-md cursor-pointer!', {
|
||||||
option: ({ isFocused, isSelected, isDisabled }) =>
|
|
||||||
cn('mt-1 px-3 py-2 rounded-md cursor-pointer! select-none', {
|
|
||||||
'text-gray-300': isDisabled,
|
|
||||||
'bg-indigo-600 text-white': isFocused,
|
'bg-indigo-600 text-white': isFocused,
|
||||||
'text-gray-700': !isDisabled && !isFocused,
|
|
||||||
'active:bg-indigo-50': !isDisabled,
|
|
||||||
'bg-blue-500!': isSelected,
|
'bg-blue-500!': isSelected,
|
||||||
|
'text-gray-700': !isFocused && !isSelected,
|
||||||
}),
|
}),
|
||||||
noOptionsMessage: () => cn('px-3 py-2 text-gray-500'),
|
|
||||||
loadingMessage: () => cn('px-3 py-2 text-gray-500'),
|
|
||||||
multiValue: ({ getValue, index }) => {
|
multiValue: ({ getValue, index }) => {
|
||||||
const selectedValues = getValue();
|
const selectedValues = getValue() as T[];
|
||||||
|
|
||||||
return cn(
|
return cn(
|
||||||
'bg-indigo-50 rounded-md py-0.5 pl-2 pr-1 flex items-center gap-1 rounded-md!',
|
'bg-indigo-50 rounded-md py-0.5 pl-2 pr-1 flex items-center gap-1!',
|
||||||
selectedValues[index]?.className
|
selectedValues[index]?.className
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
multiValueLabel: ({ getValue, index }) => {
|
multiValueLabel: ({ getValue, index }) => {
|
||||||
const selectedValues = getValue();
|
const selectedValues = getValue() as T[];
|
||||||
|
|
||||||
return cn('text-indigo-700', selectedValues[index]?.labelClassName);
|
return cn('text-indigo-700', selectedValues[index]?.labelClassName);
|
||||||
},
|
},
|
||||||
multiValueRemove: () =>
|
|
||||||
cn('p-1 rounded-sm! hover:bg-indigo-100 hover:text-indigo-800'),
|
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
...components,
|
...components,
|
||||||
...(optionComponent ? { Option: optionComponent } : {}),
|
...(optionComponent ? { Option: optionComponent } : {}),
|
||||||
}}
|
}}
|
||||||
// make the menu float above modals/etc.
|
|
||||||
menuPortalTarget={
|
menuPortalTarget={
|
||||||
typeof document !== 'undefined' ? document.body : undefined
|
typeof document !== 'undefined' ? document.body : undefined
|
||||||
}
|
}
|
||||||
styles={{
|
styles={{
|
||||||
// Tailwind can't set inline z-index on a portal; use styles here:
|
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
{isError && <p className="w-full text-sm text-error">{errorMessage}</p>}
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
<p className="w-full text-sm opacity-60">{bottomLabel}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export interface TextAreaProps {
|
|||||||
endAdornment?: ReactNode;
|
endAdornment?: ReactNode;
|
||||||
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
|
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
|
||||||
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
|
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
|
||||||
cols?: number;
|
rows?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextArea = ({
|
const TextArea = ({
|
||||||
@@ -52,7 +52,7 @@ const TextArea = ({
|
|||||||
onBlur,
|
onBlur,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
cols = 3
|
rows = 3
|
||||||
}: TextAreaProps) => {
|
}: TextAreaProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -87,7 +87,7 @@ const TextArea = ({
|
|||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all',
|
'input h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
@@ -98,7 +98,7 @@ const TextArea = ({
|
|||||||
name={name}
|
name={name}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
cols={cols}
|
rows={rows}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const InventoryAdjustmentFormSchema = Yup.object({
|
|||||||
|
|
||||||
transaction_type: Yup.string()
|
transaction_type: Yup.string()
|
||||||
.oneOf(['increase', 'decrease'], 'Tipe transaksi tidak valid')
|
.oneOf(['increase', 'decrease'], 'Tipe transaksi tidak valid')
|
||||||
|
.nullable()
|
||||||
.required('Tipe transaksi wajib diisi'),
|
.required('Tipe transaksi wajib diisi'),
|
||||||
|
|
||||||
quantity: Yup.number()
|
quantity: Yup.number()
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const InventoryAdjustmentForm = ({
|
|||||||
product: undefined,
|
product: undefined,
|
||||||
warehouse: undefined,
|
warehouse: undefined,
|
||||||
quantity: initialValues?.quantity ?? 0,
|
quantity: initialValues?.quantity ?? 0,
|
||||||
transaction_type: initialValues?.transaction_type ?? 'increase',
|
transaction_type: undefined,
|
||||||
note: initialValues?.note ?? '',
|
note: initialValues?.note ?? '',
|
||||||
};
|
};
|
||||||
}, [initialValues]);
|
}, [initialValues]);
|
||||||
@@ -153,13 +153,14 @@ const InventoryAdjustmentForm = ({
|
|||||||
formik.setFieldValue('product_category_id', (val as OptionType)?.value);
|
formik.setFieldValue('product_category_id', (val as OptionType)?.value);
|
||||||
|
|
||||||
formik.setFieldValue('product_category', val);
|
formik.setFieldValue('product_category', val);
|
||||||
|
|
||||||
setSelectedProductCategories((val as OptionType)?.value as string);
|
setSelectedProductCategories((val as OptionType)?.value as string);
|
||||||
const disabled = (val as OptionType)?.value == null;
|
const disabled = (val as OptionType)?.value == null;
|
||||||
setDisabledProduct(disabled);
|
setDisabledProduct(disabled);
|
||||||
if (disabled) {
|
|
||||||
formik.setFieldValue('product_id', 0);
|
formik.setFieldValue('product_id', 0);
|
||||||
formik.setFieldValue('product', null);
|
formik.setFieldValue('product', null);
|
||||||
}
|
formik.setFieldTouched('product', false);
|
||||||
|
formik.setFieldTouched('product_id', false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -340,30 +341,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Number Input Stock */}
|
|
||||||
<TextInput
|
|
||||||
required
|
|
||||||
label={quantityLabel}
|
|
||||||
name='quantity'
|
|
||||||
type='text'
|
|
||||||
value={formatNumber(String(formik.values.quantity))}
|
|
||||||
onChange={(e) => {
|
|
||||||
const rawValue = e.target.value.replace(/,/g, '');
|
|
||||||
const numericValue = parseFloat(rawValue);
|
|
||||||
if (!isNaN(numericValue)) {
|
|
||||||
formik.setFieldValue('quantity', numericValue);
|
|
||||||
} else {
|
|
||||||
formik.setFieldValue('quantity', 0);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={formik.handleBlur}
|
|
||||||
isError={
|
|
||||||
formik.touched.quantity && Boolean(formik.errors.quantity)
|
|
||||||
}
|
|
||||||
errorMessage={formik.errors.quantity as string}
|
|
||||||
readOnly={type === 'detail'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Radio Button Flag Stock */}
|
{/* Radio Button Flag Stock */}
|
||||||
<RadioInput
|
<RadioInput
|
||||||
name='transaction_type'
|
name='transaction_type'
|
||||||
@@ -387,10 +364,39 @@ const InventoryAdjustmentForm = ({
|
|||||||
errorMessage={formik.errors.transaction_type as string}
|
errorMessage={formik.errors.transaction_type as string}
|
||||||
variant='radio-primary'
|
variant='radio-primary'
|
||||||
required
|
required
|
||||||
bottomLabel='Pilih salah satu tipe transaksi'
|
bottomLabel={formik.values.transaction_type == undefined ? 'Pilih salah satu tipe transaksi' : undefined}
|
||||||
disabled={type === 'detail'}
|
disabled={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Number Input Stock */}
|
||||||
|
<TextInput
|
||||||
|
className={{
|
||||||
|
wrapper: `${formik.values.transaction_type != undefined ? '' : 'hidden'}`,
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
label={quantityLabel}
|
||||||
|
name='quantity'
|
||||||
|
type='text'
|
||||||
|
value={formatNumber(String(formik.values.quantity))}
|
||||||
|
onChange={(e) => {
|
||||||
|
const rawValue = e.target.value.replace(/,/g, '');
|
||||||
|
const numericValue = parseFloat(rawValue);
|
||||||
|
if (!isNaN(numericValue)) {
|
||||||
|
formik.setFieldValue('quantity', numericValue);
|
||||||
|
} else {
|
||||||
|
formik.setFieldValue('quantity', 0);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.quantity && Boolean(formik.errors.quantity)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.quantity as string}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Text Area Input Reason */}
|
{/* Text Area Input Reason */}
|
||||||
<TextArea
|
<TextArea
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -307,7 +307,6 @@ const CustomerForm = ({
|
|||||||
isError={formik.touched.address && Boolean(formik.errors.address)}
|
isError={formik.touched.address && Boolean(formik.errors.address)}
|
||||||
errorMessage={formik.errors.address}
|
errorMessage={formik.errors.address}
|
||||||
readOnly={formType === 'detail'}
|
readOnly={formType === 'detail'}
|
||||||
cols={8}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import * as Yup from 'yup';
|
|||||||
|
|
||||||
export const SupplierFormSchema = Yup.object({
|
export const SupplierFormSchema = Yup.object({
|
||||||
name: Yup.string().required('Nama wajib diisi!'),
|
name: Yup.string().required('Nama wajib diisi!'),
|
||||||
alias: Yup.string().required('Alias wajib diisi!'),
|
alias: Yup.string()
|
||||||
|
.matches(/^[A-Za-z0-9]+$/, 'Alias hanya boleh berisi huruf dan angka tanpa spasi atau simbol!')
|
||||||
|
.max(5, 'Alias maksimal 5 karakter!')
|
||||||
|
.required('Alias wajib diisi!'),
|
||||||
pic: Yup.string().required('PIC wajib diisi!'),
|
pic: Yup.string().required('PIC wajib diisi!'),
|
||||||
type: Yup.object({
|
type: Yup.object({
|
||||||
value: Yup.string().required(),
|
value: Yup.string().required(),
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|||||||
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 TagInput from '@/components/input/TagInput';
|
|
||||||
import TextArea from '@/components/input/TextArea';
|
import TextArea from '@/components/input/TextArea';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
@@ -42,7 +41,7 @@ const SupplierForm = ({
|
|||||||
// Setup State
|
// Setup State
|
||||||
const [supplierFormErrorMessage, setSupplierFormErrorMessage] = useState('');
|
const [supplierFormErrorMessage, setSupplierFormErrorMessage] = useState('');
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [hatcheryTagInputValue, setHatcheryTagInputValue] = useState('');
|
const [hatcheryOptionsValues, setHatcheryOptionValues] = useState<OptionType[]>([]);
|
||||||
|
|
||||||
// -- Options data mapping
|
// -- Options data mapping
|
||||||
const typeOptions = TYPE_OPTIONS;
|
const typeOptions = TYPE_OPTIONS;
|
||||||
@@ -168,8 +167,22 @@ const SupplierForm = ({
|
|||||||
// Initialize Formik
|
// Initialize Formik
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formikSetValues(formikInitialValues);
|
formikSetValues(formikInitialValues);
|
||||||
setHatcheryTagInputValue(formikInitialValues.hatchery);
|
if(formType != 'add'){
|
||||||
}, [formikSetValues, formikInitialValues, hatcheryTagInputValue]);
|
const hatcheryArrays = formikInitialValues.hatchery.split(',');
|
||||||
|
const hatcheryCreatedOptions = hatcheryArrays.map((item) => ({
|
||||||
|
value: item,
|
||||||
|
label: item,
|
||||||
|
}));
|
||||||
|
setHatcheryOptionValues(hatcheryCreatedOptions);
|
||||||
|
}
|
||||||
|
}, [formikSetValues, formikInitialValues, setHatcheryOptionValues]);
|
||||||
|
useEffect(() => {
|
||||||
|
const commaSeparatedValues = hatcheryOptionsValues.map((item) => item.value).join(',');
|
||||||
|
formikSetValues({
|
||||||
|
...formik.values,
|
||||||
|
hatchery: commaSeparatedValues,
|
||||||
|
})
|
||||||
|
}, [hatcheryOptionsValues, formikSetValues]);
|
||||||
|
|
||||||
// Option Handler
|
// Option Handler
|
||||||
const typeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const typeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -281,17 +294,25 @@ const SupplierForm = ({
|
|||||||
isClearable
|
isClearable
|
||||||
isSearchable={true}
|
isSearchable={true}
|
||||||
/>
|
/>
|
||||||
<TagInput
|
<SelectInput
|
||||||
name='hatchery'
|
isMulti
|
||||||
|
createables
|
||||||
|
required
|
||||||
|
placeholder='Pilih Hatchery'
|
||||||
label='Hatchery'
|
label='Hatchery'
|
||||||
value={hatcheryTagInputValue}
|
value={hatcheryOptionsValues}
|
||||||
onChange={(value) => formik.setFieldValue('hatchery', value)}
|
onChange={(val) => {
|
||||||
isError={
|
console.log(val); // pastikan val = array of { value, label }
|
||||||
formik.touched.hatchery && Boolean(formik.errors.hatchery)
|
setHatcheryOptionValues(val as OptionType[]);
|
||||||
}
|
}}
|
||||||
errorMessage={formik.errors.hatchery}
|
isError={formik.touched.hatchery && Boolean(formik.errors.hatchery)}
|
||||||
readOnly={formType === 'detail'}
|
errorMessage={formik.errors.hatchery as string}
|
||||||
|
isDisabled={formType === 'detail'}
|
||||||
|
isClearable
|
||||||
|
isSearchable={true}
|
||||||
|
options={[]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
label='Nomor Telepon'
|
label='Nomor Telepon'
|
||||||
@@ -327,7 +348,6 @@ const SupplierForm = ({
|
|||||||
isError={formik.touched.address && Boolean(formik.errors.address)}
|
isError={formik.touched.address && Boolean(formik.errors.address)}
|
||||||
errorMessage={formik.errors.address}
|
errorMessage={formik.errors.address}
|
||||||
readOnly={formType === 'detail'}
|
readOnly={formType === 'detail'}
|
||||||
cols={8}
|
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
|
|||||||
Reference in New Issue
Block a user