diff --git a/src/components/input/NumberInput.tsx b/src/components/input/NumberInput.tsx index e5319e5b..231d8b24 100644 --- a/src/components/input/NumberInput.tsx +++ b/src/components/input/NumberInput.tsx @@ -13,7 +13,65 @@ import { Icon } from '@iconify/react'; import { cn } from '@/lib/helper'; import FieldMessage from '@/components/helper/FieldMessage'; -// Utility Functions +export type MaskType = 'currency' | 'weight' | 'decimal' | 'number'; + +export interface NumberInputProps { + // Basic Props + label?: string; + bottomLabel?: string; + name: string; + value?: number | string; + placeholder?: string; + + // Styling Props + className?: { + wrapper?: string; + label?: string; + inputWrapper?: string; + input?: string; + }; + + // State Props + isError?: boolean; + isValid?: boolean; + errorMessage?: string; + disabled?: boolean; + readOnly?: boolean; + required?: boolean; + isLoading?: boolean; + + // Adornment Props + startAdornment?: ReactNode; + endAdornment?: ReactNode; + + // Event Handlers + onChange?: ChangeEventHandler; + onBlur?: FocusEventHandler; + onFocus?: FocusEventHandler; + + // Masking Options + maskType?: MaskType; + decimals?: number; + thousandSeparator?: string; + decimalSeparator?: string; + currencyPrefix?: string; + weightUnit?: string; + + // Validation Props + min?: number; + max?: number; + allowNegative?: boolean; + + // Stepper Options + showSteppers?: boolean; + step?: number; +} + +// UTILITY FUNCTIONS +/** + * Core number formatting function + * Formats number with thousand separator and decimal separator + */ const formatNumber = ( value: number | string, decimals: number = 0, @@ -37,6 +95,10 @@ const formatNumber = ( : integerPart; }; +/** + * Parse formatted string to number + * Converts formatted input back to raw number for processing + */ const parseNumber = ( value: string, thousandSeparator: string = '.', @@ -53,26 +115,10 @@ const parseNumber = ( return isNaN(parsed) ? 0 : parsed; }; -const formatCurrency = ( - value: number | string, - prefix: string = 'Rp ', - decimals: number = 0 -): string => { - if (value === '' || value === null || value === undefined) return ''; - const formatted = formatNumber(value, decimals); - return formatted ? `${prefix}${formatted}` : ''; -}; - -const formatWeight = ( - value: number | string, - unit: string = 'kg', - decimals: number = 2 -): string => { - if (value === '' || value === null || value === undefined) return ''; - const formatted = formatNumber(value, decimals); - return formatted ? `${formatted} ${unit}` : ''; -}; - +/** + * Clean and validate numeric input while typing + * Ensures only valid characters are allowed + */ const cleanNumericInput = ( value: string, allowDecimal: boolean = false, @@ -100,56 +146,6 @@ const cleanNumericInput = ( return cleaned; }; -// Types -export type MaskType = 'currency' | 'weight' | 'decimal' | 'number'; - -export interface NumberInputProps { - label?: string; - bottomLabel?: string; - name: string; - value?: number | string; - placeholder?: string; - className?: { - wrapper?: string; - label?: string; - inputWrapper?: string; - input?: string; - }; - isError?: boolean; - isValid?: boolean; - disabled?: boolean; - readOnly?: boolean; - required?: boolean; - isLoading?: boolean; - errorMessage?: string; - startAdornment?: ReactNode; - endAdornment?: ReactNode; - onChange?: ChangeEventHandler; - onBlur?: FocusEventHandler; - onFocus?: FocusEventHandler; - - // Masking Options - maskType?: MaskType; - decimals?: number; - thousandSeparator?: string; - decimalSeparator?: string; - - // Currency specific - currencyPrefix?: string; - - // Weight specific - weightUnit?: string; - - // Validation - min?: number; - max?: number; - allowNegative?: boolean; - - // Stepper (Increment/Decrement buttons) - showSteppers?: boolean; - step?: number; -} - const NumberInput = ({ label, bottomLabel, @@ -183,20 +179,35 @@ const NumberInput = ({ }: NumberInputProps) => { const [displayValue, setDisplayValue] = useState(''); - // Determine if decimals are allowed based on maskType + // CONFIG & HELPERS const allowDecimal = maskType === 'decimal' || maskType === 'weight' || decimals > 0; - // Format value for display based on maskType + const getInputPrefix = (): string => { + switch (maskType) { + case 'currency': + return currencyPrefix; + default: + return ''; + } + }; + + const getInputSuffix = (): string => { + switch (maskType) { + case 'weight': + return weightUnit; + default: + return ''; + } + }; + const getFormattedValue = (rawValue: number | string): string => { if (rawValue === '' || rawValue === null || rawValue === undefined) return ''; switch (maskType) { case 'currency': - return formatCurrency(rawValue, currencyPrefix, decimals); case 'weight': - return formatWeight(rawValue, weightUnit, decimals); case 'decimal': case 'number': return formatNumber( @@ -210,21 +221,14 @@ const NumberInput = ({ } }; - // Initialize display value when value prop changes + // EFFECTS useEffect(() => { setDisplayValue(getFormattedValue(value || '')); }, [value]); + // EVENT HANDLERS const handleInputChange = (e: ChangeEvent) => { - let inputValue = e.target.value; - - // Remove prefix/suffix for editing - if (maskType === 'currency' && inputValue.startsWith(currencyPrefix)) { - inputValue = inputValue.slice(currencyPrefix.length); - } - if (maskType === 'weight' && inputValue.endsWith(` ${weightUnit}`)) { - inputValue = inputValue.slice(0, -weightUnit.length - 1); - } + const inputValue = e.target.value; // Clean input const cleaned = cleanNumericInput( @@ -263,7 +267,6 @@ const NumberInput = ({ // Call onChange with modified event if (onChange) { - // Create a synthetic event with the numeric value const syntheticEvent = { ...e, target: { @@ -277,7 +280,6 @@ const NumberInput = ({ } }; - // Handle Increment const handleIncrement = () => { if (disabled || readOnly) return; @@ -315,7 +317,6 @@ const NumberInput = ({ } }; - // Handle Decrement const handleDecrement = () => { if (disabled || readOnly) return; @@ -356,8 +357,11 @@ const NumberInput = ({ } }; + // RENDER CALCULATIONS const showErrorMessage = Boolean(isError && errorMessage); const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; + const inputPrefix = getInputPrefix(); + const inputSuffix = getInputSuffix(); return (
)} -
- {/* Decrement Button */} - {showSteppers && ( - - )} - - {startAdornment && startAdornment} - - - - {(isLoading || endAdornment) && ( -
- {isLoading && } - - {endAdornment && endAdornment} + {/* Input Container */} +
+ {/* Prefix Block */} + {inputPrefix && ( +
+ + {inputPrefix} +
)} - {/* Increment Button */} - {showSteppers && ( - + {/* Input Wrapper */} +
+ {/* Stepper Buttons */} + {showSteppers && ( + + )} + + {/* Start Adornment */} + {startAdornment && startAdornment} + + {/* Main Input */} + + + {/* End Adornment & Loading */} + {(isLoading || endAdornment) && ( +
+ {isLoading && } + {endAdornment && endAdornment} +
+ )} + + {/* Increment Button */} + {showSteppers && ( + + )} +
+ + {/* Suffix Block */} + {inputSuffix && ( +
+ + {inputSuffix} + +
)}
+ {/* Field Message */}