mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-23 14:55:44 +00:00
refactor(FE-114): remove FieldMessage component usage and streamline error message handling in form inputs
This commit is contained in:
@@ -267,12 +267,13 @@ const CheckboxInput = ({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Field Message */}
|
{/* Error Message or Bottom Label */}
|
||||||
<FieldMessage
|
{!isError && bottomLabel && (
|
||||||
message={feedbackMessage}
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
tone={showErrorMessage ? 'error' : 'info'}
|
)}
|
||||||
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
{isError && errorMessage && (
|
||||||
/>
|
<p className='w-full text-sm text-error'>{errorMessage}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Ref } from 'react';
|
|||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { TextInputProps } from '@/components/input/TextInput';
|
import { TextInputProps } from '@/components/input/TextInput';
|
||||||
import FieldMessage from '@/components/helper/FieldMessage';
|
|
||||||
|
|
||||||
interface FileInputProps
|
interface FileInputProps
|
||||||
extends Omit<
|
extends Omit<
|
||||||
@@ -38,9 +37,6 @@ const FileInput = ({
|
|||||||
onBlur,
|
onBlur,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
}: FileInputProps) => {
|
}: FileInputProps) => {
|
||||||
const showErrorMessage = Boolean(isError && errorMessage);
|
|
||||||
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -80,11 +76,11 @@ const FileInput = ({
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldMessage
|
{bottomLabel && (
|
||||||
message={feedbackMessage}
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
tone={showErrorMessage ? 'error' : 'info'}
|
)}
|
||||||
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
|
||||||
/>
|
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,67 +11,8 @@ import {
|
|||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import FieldMessage from '@/components/helper/FieldMessage';
|
|
||||||
|
|
||||||
export type MaskType = 'currency' | 'weight' | 'decimal' | 'number';
|
// Utility Functions
|
||||||
|
|
||||||
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<HTMLInputElement>;
|
|
||||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
|
||||||
onFocus?: FocusEventHandler<HTMLInputElement>;
|
|
||||||
|
|
||||||
// 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 = (
|
const formatNumber = (
|
||||||
value: number | string,
|
value: number | string,
|
||||||
decimals: number = 0,
|
decimals: number = 0,
|
||||||
@@ -95,10 +36,6 @@ const formatNumber = (
|
|||||||
: integerPart;
|
: integerPart;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse formatted string to number
|
|
||||||
* Converts formatted input back to raw number for processing
|
|
||||||
*/
|
|
||||||
const parseNumber = (
|
const parseNumber = (
|
||||||
value: string,
|
value: string,
|
||||||
thousandSeparator: string = '.',
|
thousandSeparator: string = '.',
|
||||||
@@ -115,10 +52,26 @@ const parseNumber = (
|
|||||||
return isNaN(parsed) ? 0 : parsed;
|
return isNaN(parsed) ? 0 : parsed;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const formatCurrency = (
|
||||||
* Clean and validate numeric input while typing
|
value: number | string,
|
||||||
* Ensures only valid characters are allowed
|
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}` : '';
|
||||||
|
};
|
||||||
|
|
||||||
const cleanNumericInput = (
|
const cleanNumericInput = (
|
||||||
value: string,
|
value: string,
|
||||||
allowDecimal: boolean = false,
|
allowDecimal: boolean = false,
|
||||||
@@ -146,6 +99,56 @@ const cleanNumericInput = (
|
|||||||
return cleaned;
|
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<HTMLInputElement>;
|
||||||
|
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||||
|
onFocus?: FocusEventHandler<HTMLInputElement>;
|
||||||
|
|
||||||
|
// 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 = ({
|
const NumberInput = ({
|
||||||
label,
|
label,
|
||||||
bottomLabel,
|
bottomLabel,
|
||||||
@@ -179,35 +182,20 @@ const NumberInput = ({
|
|||||||
}: NumberInputProps) => {
|
}: NumberInputProps) => {
|
||||||
const [displayValue, setDisplayValue] = useState<string>('');
|
const [displayValue, setDisplayValue] = useState<string>('');
|
||||||
|
|
||||||
// CONFIG & HELPERS
|
// Determine if decimals are allowed based on maskType
|
||||||
const allowDecimal =
|
const allowDecimal =
|
||||||
maskType === 'decimal' || maskType === 'weight' || decimals > 0;
|
maskType === 'decimal' || maskType === 'weight' || decimals > 0;
|
||||||
|
|
||||||
const getInputPrefix = (): string => {
|
// Format value for display based on maskType
|
||||||
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 => {
|
const getFormattedValue = (rawValue: number | string): string => {
|
||||||
if (rawValue === '' || rawValue === null || rawValue === undefined)
|
if (rawValue === '' || rawValue === null || rawValue === undefined)
|
||||||
return '';
|
return '';
|
||||||
|
|
||||||
switch (maskType) {
|
switch (maskType) {
|
||||||
case 'currency':
|
case 'currency':
|
||||||
|
return formatCurrency(rawValue, currencyPrefix, decimals);
|
||||||
case 'weight':
|
case 'weight':
|
||||||
|
return formatWeight(rawValue, weightUnit, decimals);
|
||||||
case 'decimal':
|
case 'decimal':
|
||||||
case 'number':
|
case 'number':
|
||||||
return formatNumber(
|
return formatNumber(
|
||||||
@@ -221,14 +209,21 @@ const NumberInput = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// EFFECTS
|
// Initialize display value when value prop changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDisplayValue(getFormattedValue(value || ''));
|
setDisplayValue(getFormattedValue(value || ''));
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
// EVENT HANDLERS
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const inputValue = e.target.value;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// Clean input
|
// Clean input
|
||||||
const cleaned = cleanNumericInput(
|
const cleaned = cleanNumericInput(
|
||||||
@@ -267,6 +262,7 @@ const NumberInput = ({
|
|||||||
|
|
||||||
// Call onChange with modified event
|
// Call onChange with modified event
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
|
// Create a synthetic event with the numeric value
|
||||||
const syntheticEvent = {
|
const syntheticEvent = {
|
||||||
...e,
|
...e,
|
||||||
target: {
|
target: {
|
||||||
@@ -280,6 +276,7 @@ const NumberInput = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle Increment
|
||||||
const handleIncrement = () => {
|
const handleIncrement = () => {
|
||||||
if (disabled || readOnly) return;
|
if (disabled || readOnly) return;
|
||||||
|
|
||||||
@@ -317,6 +314,7 @@ const NumberInput = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle Decrement
|
||||||
const handleDecrement = () => {
|
const handleDecrement = () => {
|
||||||
if (disabled || readOnly) return;
|
if (disabled || readOnly) return;
|
||||||
|
|
||||||
@@ -357,12 +355,6 @@ const NumberInput = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// RENDER CALCULATIONS
|
|
||||||
const showErrorMessage = Boolean(isError && errorMessage);
|
|
||||||
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
|
||||||
const inputPrefix = getInputPrefix();
|
|
||||||
const inputSuffix = getInputSuffix();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -393,152 +385,84 @@ const NumberInput = ({
|
|||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Input Container */}
|
<div
|
||||||
<div className='relative flex'>
|
className={cn(
|
||||||
{/* Prefix Block */}
|
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200',
|
||||||
{inputPrefix && (
|
{
|
||||||
<div
|
'border-error': isError,
|
||||||
|
'border-success!': isValid,
|
||||||
|
},
|
||||||
|
className?.inputWrapper
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Decrement Button */}
|
||||||
|
{showSteppers && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={handleDecrement}
|
||||||
|
disabled={disabled || readOnly}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center px-4 py-2 border border-r-0 rounded-l-md transition-all duration-200',
|
'btn btn-ghost btn-sm h-8 w-8 min-h-0 p-0 rounded-md',
|
||||||
{
|
{
|
||||||
'bg-gray-100 border-gray-300': !disabled,
|
'opacity-50 cursor-not-allowed': disabled || readOnly,
|
||||||
'bg-gray-50 border-gray-200': disabled,
|
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<span
|
<Icon icon='ic:round-minus' width={20} height={20} />
|
||||||
className={cn(
|
</button>
|
||||||
'text-sm font-medium select-none whitespace-nowrap',
|
)}
|
||||||
{
|
|
||||||
'text-gray-600': !disabled,
|
{startAdornment && startAdornment}
|
||||||
'text-gray-400': disabled,
|
|
||||||
}
|
<input
|
||||||
)}
|
type='text'
|
||||||
>
|
id={name}
|
||||||
{inputPrefix}
|
name={name}
|
||||||
</span>
|
placeholder={placeholder}
|
||||||
|
value={displayValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn('grow', className?.input)}
|
||||||
|
readOnly={readOnly}
|
||||||
|
inputMode='decimal'
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(isLoading || endAdornment) && (
|
||||||
|
<div className='flex flex-row gap-2'>
|
||||||
|
{isLoading && <span className='loading loading-spinner' />}
|
||||||
|
|
||||||
|
{endAdornment && endAdornment}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Input Wrapper */}
|
{/* Increment Button */}
|
||||||
<div
|
{showSteppers && (
|
||||||
className={cn(
|
<button
|
||||||
'input h-12 text-base font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white',
|
type='button'
|
||||||
{
|
onClick={handleIncrement}
|
||||||
'border-error': isError,
|
disabled={disabled || readOnly}
|
||||||
'border-success!': isValid,
|
|
||||||
'rounded-l-none!': inputPrefix,
|
|
||||||
'rounded-r-none!': inputSuffix,
|
|
||||||
'input-disabled': disabled,
|
|
||||||
'cursor-not-allowed': disabled,
|
|
||||||
'bg-gray-50': disabled,
|
|
||||||
},
|
|
||||||
className?.inputWrapper
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Stepper Buttons */}
|
|
||||||
{showSteppers && (
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
onClick={handleDecrement}
|
|
||||||
disabled={disabled || readOnly}
|
|
||||||
className={cn(
|
|
||||||
'btn btn-ghost btn-sm h-8 w-8 min-h-0 p-0 rounded-md',
|
|
||||||
{
|
|
||||||
'opacity-50 cursor-not-allowed': disabled || readOnly,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
<Icon icon='ic:round-minus' width={20} height={20} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Start Adornment */}
|
|
||||||
{startAdornment && startAdornment}
|
|
||||||
|
|
||||||
{/* Main Input */}
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
id={name}
|
|
||||||
name={name}
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={displayValue}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
disabled={disabled}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'grow bg-transparent outline-none',
|
'btn btn-ghost btn-sm h-8 w-8 min-h-0 p-0 rounded-md',
|
||||||
{
|
{
|
||||||
'cursor-not-allowed': disabled,
|
'opacity-50 cursor-not-allowed': disabled || readOnly,
|
||||||
'text-gray-500': disabled,
|
|
||||||
},
|
|
||||||
className?.input
|
|
||||||
)}
|
|
||||||
readOnly={readOnly}
|
|
||||||
inputMode='decimal'
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* End Adornment & Loading */}
|
|
||||||
{(isLoading || endAdornment) && (
|
|
||||||
<div className='flex flex-row gap-2'>
|
|
||||||
{isLoading && <span className='loading loading-spinner' />}
|
|
||||||
{endAdornment && endAdornment}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Increment Button */}
|
|
||||||
{showSteppers && (
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
onClick={handleIncrement}
|
|
||||||
disabled={disabled || readOnly}
|
|
||||||
className={cn(
|
|
||||||
'btn btn-ghost btn-sm h-8 w-8 min-h-0 p-0 rounded-md',
|
|
||||||
{
|
|
||||||
'opacity-50 cursor-not-allowed': disabled || readOnly,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
<Icon icon='ic:round-plus' width={20} height={20} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Suffix Block */}
|
|
||||||
{inputSuffix && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'inline-flex items-center px-4 py-2 border border-l-0 rounded-r-md transition-all duration-200',
|
|
||||||
{
|
|
||||||
'bg-gray-100 border-gray-300': !disabled,
|
|
||||||
'bg-gray-50 border-gray-200': disabled,
|
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<span
|
<Icon icon='ic:round-plus' width={20} height={20} />
|
||||||
className={cn(
|
</button>
|
||||||
'text-sm font-medium select-none whitespace-nowrap',
|
|
||||||
{
|
|
||||||
'text-gray-600': !disabled,
|
|
||||||
'text-gray-400': disabled,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{inputSuffix}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Field Message */}
|
{!isError && bottomLabel && (
|
||||||
<FieldMessage
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
message={feedbackMessage}
|
)}
|
||||||
tone={showErrorMessage ? 'error' : 'info'}
|
{isError && errorMessage && (
|
||||||
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
<p className='w-full text-sm text-error'>{errorMessage}</p>
|
||||||
/>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { ChangeEventHandler, ReactNode } from 'react';
|
import { ChangeEventHandler, ReactNode } from 'react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import FieldMessage from '@/components/helper/FieldMessage';
|
|
||||||
|
|
||||||
export interface RadioOption {
|
export interface RadioOption {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -48,8 +47,6 @@ const RadioInput = ({
|
|||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
}: RadioInputProps) => {
|
}: RadioInputProps) => {
|
||||||
const showErrorMessage = Boolean(isError && errorMessage);
|
|
||||||
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('w-full flex flex-col gap-2', className?.wrapper)}>
|
<div className={cn('w-full flex flex-col gap-2', className?.wrapper)}>
|
||||||
{/* Label atas */}
|
{/* Label atas */}
|
||||||
@@ -100,11 +97,15 @@ const RadioInput = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FieldMessage
|
{/* Label bawah */}
|
||||||
message={feedbackMessage}
|
{!isError && bottomLabel && (
|
||||||
tone={showErrorMessage ? 'error' : 'info'}
|
<p className='text-sm opacity-60'>{bottomLabel}</p>
|
||||||
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
)}
|
||||||
/>
|
|
||||||
|
{/* Pesan error */}
|
||||||
|
{isError && errorMessage && (
|
||||||
|
<p className='text-sm text-error'>{errorMessage}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ 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';
|
||||||
import FieldMessage from '@/components/helper/FieldMessage';
|
|
||||||
|
|
||||||
export interface OptionType {
|
export interface OptionType {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
@@ -118,9 +117,6 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showErrorMessage = Boolean(isError && errorMessage);
|
|
||||||
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -218,11 +214,10 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldMessage
|
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
||||||
message={feedbackMessage}
|
{!isError && bottomLabel && (
|
||||||
tone={showErrorMessage ? 'error' : 'info'}
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
)}
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import React, { useState, KeyboardEvent, ChangeEvent, useEffect } from 'react';
|
import React, { useState, KeyboardEvent, ChangeEvent, useEffect } from 'react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import FieldMessage from '@/components/helper/FieldMessage';
|
|
||||||
|
|
||||||
export interface TagInputProps {
|
export interface TagInputProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -74,9 +73,6 @@ const TagInput: React.FC<TagInputProps> = ({
|
|||||||
setInputValue(e.target.value);
|
setInputValue(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showErrorMessage = Boolean(isError && errorMessage);
|
|
||||||
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -161,11 +157,11 @@ const TagInput: React.FC<TagInputProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FieldMessage
|
{/* Bottom label or error message */}
|
||||||
message={feedbackMessage}
|
{!isError && bottomLabel && (
|
||||||
tone={showErrorMessage ? 'error' : 'info'}
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
)}
|
||||||
/>
|
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { ChangeEventHandler, FocusEventHandler, ReactNode } from 'react';
|
import { ChangeEventHandler, FocusEventHandler, ReactNode } from 'react';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import FieldMessage from '@/components/helper/FieldMessage';
|
|
||||||
|
|
||||||
export interface TextAreaProps {
|
export interface TextAreaProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -51,9 +50,6 @@ const TextArea = ({
|
|||||||
isLoading = false,
|
isLoading = false,
|
||||||
rows = 3,
|
rows = 3,
|
||||||
}: TextAreaProps) => {
|
}: TextAreaProps) => {
|
||||||
const showErrorMessage = Boolean(isError && errorMessage);
|
|
||||||
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -87,7 +83,7 @@ const TextArea = ({
|
|||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all bg-white',
|
'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,
|
||||||
@@ -113,11 +109,10 @@ const TextArea = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FieldMessage
|
{!isError && bottomLabel && (
|
||||||
message={feedbackMessage}
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
tone={showErrorMessage ? 'error' : 'info'}
|
)}
|
||||||
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import FieldMessage from '@/components/helper/FieldMessage';
|
|
||||||
|
|
||||||
export interface TextInputProps {
|
export interface TextInputProps {
|
||||||
type?: HTMLInputTypeAttribute;
|
type?: HTMLInputTypeAttribute;
|
||||||
@@ -56,9 +55,6 @@ const TextInput = ({
|
|||||||
readOnly = false,
|
readOnly = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
}: TextInputProps) => {
|
}: TextInputProps) => {
|
||||||
const showErrorMessage = Boolean(isError && errorMessage);
|
|
||||||
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -91,7 +87,7 @@ const TextInput = ({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
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 duration-200 bg-white',
|
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
@@ -123,11 +119,12 @@ const TextInput = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FieldMessage
|
{!isError && bottomLabel && (
|
||||||
message={feedbackMessage}
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
tone={showErrorMessage ? 'error' : 'info'}
|
)}
|
||||||
isVisible={showErrorMessage || Boolean(bottomLabel)}
|
{isError && errorMessage && (
|
||||||
/>
|
<p className='w-full text-sm text-error'>{errorMessage}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import { SupplierApi, WarehouseApi } from '@/services/api/master-data';
|
|||||||
import { ProductWarehouseApi } from '@/services/api/inventory';
|
import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import FileInput from '@/components/input/FileInput';
|
import FileInput from '@/components/input/FileInput';
|
||||||
import FieldMessage from '@/components/helper/FieldMessage';
|
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
|
|
||||||
interface MovementFormProps {
|
interface MovementFormProps {
|
||||||
@@ -910,7 +909,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
naked={true}
|
naked={true}
|
||||||
size='sm'
|
size='sm'
|
||||||
/>
|
/>
|
||||||
<FieldMessage message={null} isVisible={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -1006,7 +1004,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
height={24}
|
height={24}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<FieldMessage message={null} isVisible={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -1174,7 +1171,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
naked={true}
|
naked={true}
|
||||||
size='sm'
|
size='sm'
|
||||||
/>
|
/>
|
||||||
<FieldMessage message={null} isVisible={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -1323,10 +1319,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
'-'
|
'-'
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<FieldMessage
|
|
||||||
message={null}
|
|
||||||
isVisible={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -1444,7 +1436,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
height={24}
|
height={24}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<FieldMessage message={null} isVisible={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import { ProductWarehouseApi } from '@/services/api/inventory';
|
|||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
import FieldMessage from '@/components/helper/FieldMessage';
|
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
|
|
||||||
interface RecordingFormProps {
|
interface RecordingFormProps {
|
||||||
@@ -829,7 +828,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
naked={true}
|
naked={true}
|
||||||
size='sm'
|
size='sm'
|
||||||
/>
|
/>
|
||||||
<FieldMessage message={null} isVisible={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -948,7 +946,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
height={24}
|
height={24}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<FieldMessage message={null} isVisible={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -1078,7 +1075,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
naked={true}
|
naked={true}
|
||||||
size='sm'
|
size='sm'
|
||||||
/>
|
/>
|
||||||
<FieldMessage message={null} isVisible={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -1191,7 +1187,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
height={24}
|
height={24}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<FieldMessage message={null} isVisible={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -1313,7 +1308,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
naked={true}
|
naked={true}
|
||||||
size='sm'
|
size='sm'
|
||||||
/>
|
/>
|
||||||
<FieldMessage message={null} isVisible={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -1448,7 +1442,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
height={24}
|
height={24}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<FieldMessage message={null} isVisible={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -1572,7 +1565,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
naked={true}
|
naked={true}
|
||||||
size='sm'
|
size='sm'
|
||||||
/>
|
/>
|
||||||
<FieldMessage message={null} isVisible={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -1646,7 +1638,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
height={24}
|
height={24}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<FieldMessage message={null} isVisible={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,4 +8,8 @@
|
|||||||
--step-bg: var(--color-error);
|
--step-bg: var(--color-error);
|
||||||
--step-fg: var(--color-error-content);
|
--step-fg: var(--color-error-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table :where(th, td) {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user