feat(FE-166-169): Slicing UI Penjualan Form dan Client side validation

This commit is contained in:
randy-ar
2025-11-06 13:45:52 +07:00
parent d8637923bd
commit 158971d904
13 changed files with 1344 additions and 152 deletions
+90
View File
@@ -0,0 +1,90 @@
'use client';
import { ChangeEvent } from 'react';
import {
PatternFormat,
NumberFormatBase,
NumberFormatBaseProps,
OnValueChange,
} from 'react-number-format';
import TextInput, { TextInputProps } from '@/components/input/TextInput';
interface PatternInputProps extends Omit<TextInputProps, 'type'> {
/**
* Format pattern, contoh: "##/##/####", "(###) ###-####", "####-####-####"
*/
format: string;
/** Mask karakter kosong, misal "_" */
mask?: string;
/** Menampilkan mask walau value kosong */
allowEmptyFormatting?: boolean;
/** Placeholder karakter format, default: "#" */
patternChar?: string;
/** Jika true, izinkan huruf (A-Z) selain angka */
inputVehicleNumber?: boolean;
type?: 'text' | 'password' | 'tel';
}
/**
* PatternInput tetap backward-compatible dengan Storybook
* tapi bisa menerima huruf jika `allowCharacters={true}`
*/
const PatternInput = ({
type = 'text',
format,
mask = '_',
allowEmptyFormatting = false,
patternChar = '#',
inputVehicleNumber = false,
onChange,
...restProps
}: PatternInputProps) => {
const handleValueChange: OnValueChange = (values, { event }) => {
const newEvent = event as ChangeEvent<HTMLInputElement> | undefined;
if (newEvent) {
newEvent.target.value = values.value.toUpperCase();
onChange?.(newEvent);
}
};
if (inputVehicleNumber) {
return (
<NumberFormatBase
{...restProps}
type={type}
customInput={TextInput}
format={(value) => {
const clean = value.replace(/[^a-z0-9]/gi, '').toUpperCase();
const match = clean.match(/^([A-Z]{0,2})(\d{0,4})([A-Z]{0,3})$/);
if (!match) return clean;
const [, prefix, number, suffix] = match;
return [prefix, number, suffix].filter(Boolean).join(' ');
}}
removeFormatting={(val) => val.replace(/\s+/g, '')}
isValidInputCharacter={(char) => /^[a-z0-9]$/i.test(char)}
getCaretBoundary={(val) =>
Array(val.length + 1)
.fill(true)
.map(Boolean)
}
onValueChange={handleValueChange}
/>
);
}
return (
<PatternFormat
{...restProps}
type={type}
format={format}
mask={mask}
allowEmptyFormatting={allowEmptyFormatting}
patternChar={patternChar}
customInput={TextInput}
onValueChange={handleValueChange}
/>
);
};
export default PatternInput;
+10 -6
View File
@@ -55,6 +55,7 @@ interface SelectInputBaseProps<T = OptionType> {
delay?: number;
onInputChange?: (search: string) => void;
startAdornment?: ReactNode;
menuPortalTarget?: HTMLElement | null;
}
interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
@@ -68,7 +69,7 @@ const animatedComponents = makeAnimated();
const CustomControl = <
Option,
IsMulti extends boolean,
Group extends GroupBase<Option>
Group extends GroupBase<Option>,
>(
props: ControlProps<Option, IsMulti, Group>
) => {
@@ -117,6 +118,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
createables = false,
onInputChange,
startAdornment,
menuPortalTarget,
} = props;
const [internalInputValue, setInternalInputValue] = useState('');
@@ -186,7 +188,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
<SelectComponent<T, boolean, GroupBase<T>>
instanceId='select'
value={value ?? (isMulti ? [] : undefined)}
value={value ?? (isMulti ? [] : null)}
onChange={onChange ? handleChange : undefined}
options={options}
menuIsOpen={openMenu}
@@ -257,7 +259,9 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
startAdornment,
})}
menuPortalTarget={
typeof document !== 'undefined' ? document.body : undefined
typeof document !== 'undefined'
? (menuPortalTarget ?? document.body)
: undefined
}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
@@ -274,8 +278,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
const useSelect = <T,>(
basePath: string,
valueKey: keyof T,
labelKey: keyof T,
valueKey: keyof T | string,
labelKey: keyof T | string,
searchKey: string = 'search',
params?: { [key: string]: string }
) => {
@@ -286,7 +290,7 @@ const useSelect = <T,>(
[searchKey]: inputValue ?? '',
...params,
}).toString();
}, [inputValue, searchKey]);
}, [inputValue, searchKey, params]);
const optionsUrl = `${basePath}?${optionsUrlParams}`;