mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +00:00
feat(FE-114): add NumberInput component and integrate into RecordingForm for enhanced numeric input handling
This commit is contained in:
@@ -0,0 +1,444 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
ChangeEventHandler,
|
||||||
|
FocusEventHandler,
|
||||||
|
ReactNode,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
// Utility Functions
|
||||||
|
const formatNumber = (
|
||||||
|
value: number | string,
|
||||||
|
decimals: number = 0,
|
||||||
|
thousandSeparator: string = '.',
|
||||||
|
decimalSeparator: string = ','
|
||||||
|
): string => {
|
||||||
|
if (value === '' || value === null || value === undefined) return '';
|
||||||
|
|
||||||
|
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
||||||
|
if (isNaN(numValue)) return '';
|
||||||
|
|
||||||
|
const parts = numValue.toFixed(decimals).split('.');
|
||||||
|
const integerPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator);
|
||||||
|
const decimalPart = parts[1];
|
||||||
|
|
||||||
|
return decimals > 0 && decimalPart
|
||||||
|
? `${integerPart}${decimalSeparator}${decimalPart}`
|
||||||
|
: integerPart;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseNumber = (
|
||||||
|
value: string,
|
||||||
|
thousandSeparator: string = '.',
|
||||||
|
decimalSeparator: string = ','
|
||||||
|
): number => {
|
||||||
|
if (!value) return 0;
|
||||||
|
|
||||||
|
// Remove thousand separators and replace decimal separator with dot
|
||||||
|
const cleaned = value
|
||||||
|
.replace(new RegExp(`\\${thousandSeparator}`, 'g'), '')
|
||||||
|
.replace(decimalSeparator, '.');
|
||||||
|
|
||||||
|
const parsed = parseFloat(cleaned);
|
||||||
|
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}` : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanNumericInput = (
|
||||||
|
value: string,
|
||||||
|
allowDecimal: boolean = false,
|
||||||
|
decimalSeparator: string = ','
|
||||||
|
): string => {
|
||||||
|
// Only allow numbers, decimal separator (if allowed), and minus sign at the start
|
||||||
|
let cleaned = value.replace(/[^\d,.-]/g, '');
|
||||||
|
|
||||||
|
// Handle decimal separator
|
||||||
|
if (allowDecimal) {
|
||||||
|
const parts = cleaned.split(decimalSeparator);
|
||||||
|
if (parts.length > 2) {
|
||||||
|
// Keep only first decimal separator
|
||||||
|
cleaned = parts[0] + decimalSeparator + parts.slice(1).join('');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cleaned = cleaned.replace(new RegExp(decimalSeparator, 'g'), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle minus sign (only at start)
|
||||||
|
const hasMinusAtStart = cleaned.startsWith('-');
|
||||||
|
cleaned = cleaned.replace(/-/g, '');
|
||||||
|
if (hasMinusAtStart) cleaned = '-' + 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 = ({
|
||||||
|
label,
|
||||||
|
bottomLabel,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
isError,
|
||||||
|
isValid,
|
||||||
|
errorMessage,
|
||||||
|
startAdornment,
|
||||||
|
endAdornment,
|
||||||
|
disabled = false,
|
||||||
|
required = false,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
onFocus,
|
||||||
|
readOnly = false,
|
||||||
|
isLoading = false,
|
||||||
|
maskType = 'number',
|
||||||
|
decimals = 0,
|
||||||
|
thousandSeparator = '.',
|
||||||
|
decimalSeparator = ',',
|
||||||
|
currencyPrefix = 'Rp ',
|
||||||
|
weightUnit = 'kg',
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
allowNegative = false,
|
||||||
|
showSteppers = false,
|
||||||
|
step = 1,
|
||||||
|
}: NumberInputProps) => {
|
||||||
|
const [displayValue, setDisplayValue] = useState<string>('');
|
||||||
|
|
||||||
|
// Determine if decimals are allowed based on maskType
|
||||||
|
const allowDecimal = maskType === 'decimal' || maskType === 'weight' || decimals > 0;
|
||||||
|
|
||||||
|
// Format value for display based on maskType
|
||||||
|
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(rawValue, decimals, thousandSeparator, decimalSeparator);
|
||||||
|
default:
|
||||||
|
return String(rawValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize display value when value prop changes
|
||||||
|
useEffect(() => {
|
||||||
|
setDisplayValue(getFormattedValue(value || ''));
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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
|
||||||
|
const cleaned = cleanNumericInput(inputValue, allowDecimal, decimalSeparator);
|
||||||
|
|
||||||
|
// Parse to number
|
||||||
|
let numericValue = parseNumber(cleaned, thousandSeparator, decimalSeparator);
|
||||||
|
|
||||||
|
// Apply validation
|
||||||
|
if (!allowNegative && numericValue < 0) {
|
||||||
|
numericValue = 0;
|
||||||
|
}
|
||||||
|
if (min !== undefined && numericValue < min) {
|
||||||
|
numericValue = min;
|
||||||
|
}
|
||||||
|
if (max !== undefined && numericValue > max) {
|
||||||
|
numericValue = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update display value
|
||||||
|
const formattedForDisplay = formatNumber(
|
||||||
|
numericValue,
|
||||||
|
decimals,
|
||||||
|
thousandSeparator,
|
||||||
|
decimalSeparator
|
||||||
|
);
|
||||||
|
|
||||||
|
setDisplayValue(formattedForDisplay);
|
||||||
|
|
||||||
|
// Call onChange with modified event
|
||||||
|
if (onChange) {
|
||||||
|
// Create a synthetic event with the numeric value
|
||||||
|
const syntheticEvent = {
|
||||||
|
...e,
|
||||||
|
target: {
|
||||||
|
...e.target,
|
||||||
|
name,
|
||||||
|
value: numericValue.toString(),
|
||||||
|
},
|
||||||
|
} as ChangeEvent<HTMLInputElement>;
|
||||||
|
|
||||||
|
onChange(syntheticEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle Increment
|
||||||
|
const handleIncrement = () => {
|
||||||
|
if (disabled || readOnly) return;
|
||||||
|
|
||||||
|
const currentValue = parseNumber(displayValue, thousandSeparator, decimalSeparator);
|
||||||
|
let newValue = currentValue + step;
|
||||||
|
|
||||||
|
// Apply max validation
|
||||||
|
if (max !== undefined && newValue > max) {
|
||||||
|
newValue = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update display
|
||||||
|
const formattedForDisplay = formatNumber(
|
||||||
|
newValue,
|
||||||
|
decimals,
|
||||||
|
thousandSeparator,
|
||||||
|
decimalSeparator
|
||||||
|
);
|
||||||
|
setDisplayValue(formattedForDisplay);
|
||||||
|
|
||||||
|
// Call onChange with synthetic event
|
||||||
|
if (onChange) {
|
||||||
|
const syntheticEvent = {
|
||||||
|
target: {
|
||||||
|
name,
|
||||||
|
value: newValue.toString(),
|
||||||
|
},
|
||||||
|
} as ChangeEvent<HTMLInputElement>;
|
||||||
|
|
||||||
|
onChange(syntheticEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle Decrement
|
||||||
|
const handleDecrement = () => {
|
||||||
|
if (disabled || readOnly) return;
|
||||||
|
|
||||||
|
const currentValue = parseNumber(displayValue, thousandSeparator, decimalSeparator);
|
||||||
|
let newValue = currentValue - step;
|
||||||
|
|
||||||
|
// Apply min validation (prevent negative if not allowed)
|
||||||
|
if (!allowNegative && newValue < 0) {
|
||||||
|
newValue = 0;
|
||||||
|
}
|
||||||
|
if (min !== undefined && newValue < min) {
|
||||||
|
newValue = min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update display
|
||||||
|
const formattedForDisplay = formatNumber(
|
||||||
|
newValue,
|
||||||
|
decimals,
|
||||||
|
thousandSeparator,
|
||||||
|
decimalSeparator
|
||||||
|
);
|
||||||
|
setDisplayValue(formattedForDisplay);
|
||||||
|
|
||||||
|
// Call onChange with synthetic event
|
||||||
|
if (onChange) {
|
||||||
|
const syntheticEvent = {
|
||||||
|
target: {
|
||||||
|
name,
|
||||||
|
value: newValue.toString(),
|
||||||
|
},
|
||||||
|
} as ChangeEvent<HTMLInputElement>;
|
||||||
|
|
||||||
|
onChange(syntheticEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-full flex flex-col gap-2 text-start',
|
||||||
|
className?.wrapper
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label && (
|
||||||
|
<label
|
||||||
|
htmlFor={name}
|
||||||
|
className={cn(
|
||||||
|
'w-full text-sm font-normal leading-5',
|
||||||
|
{
|
||||||
|
'text-error': isError,
|
||||||
|
},
|
||||||
|
className?.label
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{required && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<span className='tooltip tooltip-error' data-tip='required'>
|
||||||
|
<span className='text-error'> *</span>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
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',
|
||||||
|
{
|
||||||
|
'border-error': isError,
|
||||||
|
'border-success!': isValid,
|
||||||
|
},
|
||||||
|
className?.inputWrapper
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Decrement Button */}
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{startAdornment && startAdornment}
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id={name}
|
||||||
|
name={name}
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
|
{!isError && bottomLabel && (
|
||||||
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
|
)}
|
||||||
|
{isError && errorMessage && (
|
||||||
|
<p className='w-full text-sm text-error'>{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NumberInput;
|
||||||
@@ -6,6 +6,7 @@ import { Icon } from '@iconify/react';
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
@@ -589,13 +590,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
type='number'
|
|
||||||
name={`feed_data.${idx}.feed_stock`}
|
name={`feed_data.${idx}.feed_stock`}
|
||||||
value={feed.feed_stock}
|
value={feed.feed_stock}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
maskType='number'
|
||||||
|
decimals={0}
|
||||||
|
min={0}
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError(
|
isRepeaterInputError(
|
||||||
'feed_data',
|
'feed_data',
|
||||||
@@ -728,13 +731,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
type='number'
|
|
||||||
name={`body_weight.${idx}.chicken_weight`}
|
name={`body_weight.${idx}.chicken_weight`}
|
||||||
value={weight.chicken_weight}
|
value={weight.chicken_weight}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
maskType='weight'
|
||||||
|
weightUnit='gram'
|
||||||
|
decimals={0}
|
||||||
|
min={0}
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError(
|
isRepeaterInputError(
|
||||||
'body_weight',
|
'body_weight',
|
||||||
@@ -750,16 +756,21 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
).errorMessage
|
).errorMessage
|
||||||
}
|
}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full min-w-24',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
type='number'
|
|
||||||
name={`body_weight.${idx}.chicken_count`}
|
name={`body_weight.${idx}.chicken_count`}
|
||||||
value={weight.chicken_count}
|
value={weight.chicken_count}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
maskType='number'
|
||||||
|
decimals={0}
|
||||||
|
min={0}
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError(
|
isRepeaterInputError(
|
||||||
'body_weight',
|
'body_weight',
|
||||||
@@ -775,16 +786,22 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
).errorMessage
|
).errorMessage
|
||||||
}
|
}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full min-w-24',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
type='number'
|
|
||||||
name={`body_weight.${idx}.average_chicken_weight`}
|
name={`body_weight.${idx}.average_chicken_weight`}
|
||||||
value={weight.average_chicken_weight}
|
value={weight.average_chicken_weight}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
maskType='weight'
|
||||||
|
weightUnit='gram'
|
||||||
|
decimals={0}
|
||||||
|
min={0}
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError(
|
isRepeaterInputError(
|
||||||
'body_weight',
|
'body_weight',
|
||||||
@@ -800,6 +817,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
).errorMessage
|
).errorMessage
|
||||||
}
|
}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full min-w-24',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
@@ -995,13 +1015,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
type='number'
|
|
||||||
name={`vaccination.${idx}.used_stock`}
|
name={`vaccination.${idx}.used_stock`}
|
||||||
value={vaccine.used_stock}
|
value={vaccine.used_stock}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
maskType='number'
|
||||||
|
decimals={0}
|
||||||
|
min={0}
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError(
|
isRepeaterInputError(
|
||||||
'vaccination',
|
'vaccination',
|
||||||
|
|||||||
Reference in New Issue
Block a user