diff --git a/src/components/input/CheckboxInput.tsx b/src/components/input/CheckboxInput.tsx index 8ee202db..c3c5be0a 100644 --- a/src/components/input/CheckboxInput.tsx +++ b/src/components/input/CheckboxInput.tsx @@ -267,12 +267,13 @@ const CheckboxInput = ({

)} - {/* Field Message */} - + {/* Error Message or Bottom Label */} + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + {isError && errorMessage && ( +

{errorMessage}

+ )} ); }; diff --git a/src/components/input/FileInput.tsx b/src/components/input/FileInput.tsx index 285a3f42..6218970c 100644 --- a/src/components/input/FileInput.tsx +++ b/src/components/input/FileInput.tsx @@ -2,7 +2,6 @@ import { Ref } from 'react'; import { cn } from '@/lib/helper'; import { TextInputProps } from '@/components/input/TextInput'; -import FieldMessage from '@/components/helper/FieldMessage'; interface FileInputProps extends Omit< @@ -38,9 +37,6 @@ const FileInput = ({ onBlur, readOnly = false, }: FileInputProps) => { - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; - return (
- + {bottomLabel && ( +

{bottomLabel}

+ )} + + {isError &&

{errorMessage}

}
); }; diff --git a/src/components/input/NumberInput.tsx b/src/components/input/NumberInput.tsx index 4d6a7393..5710e932 100644 --- a/src/components/input/NumberInput.tsx +++ b/src/components/input/NumberInput.tsx @@ -11,67 +11,8 @@ import { import { Icon } from '@iconify/react'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; -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 - */ +// Utility Functions const formatNumber = ( value: number | string, decimals: number = 0, @@ -95,10 +36,6 @@ const formatNumber = ( : integerPart; }; -/** - * Parse formatted string to number - * Converts formatted input back to raw number for processing - */ const parseNumber = ( value: string, thousandSeparator: string = '.', @@ -115,10 +52,26 @@ const parseNumber = ( return isNaN(parsed) ? 0 : parsed; }; -/** - * Clean and validate numeric input while typing - * Ensures only valid characters are allowed - */ +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, @@ -146,6 +99,56 @@ 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, @@ -179,35 +182,20 @@ const NumberInput = ({ }: NumberInputProps) => { const [displayValue, setDisplayValue] = useState(''); - // CONFIG & HELPERS + // Determine if decimals are allowed based on maskType const allowDecimal = maskType === 'decimal' || maskType === 'weight' || decimals > 0; - const getInputPrefix = (): string => { - switch (maskType) { - case 'currency': - return currencyPrefix; - default: - return ''; - } - }; - - const getInputSuffix = (): string => { - switch (maskType) { - case 'weight': - return weightUnit; - default: - return ''; - } - }; - + // 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( @@ -221,14 +209,21 @@ const NumberInput = ({ } }; - // EFFECTS + // Initialize display value when value prop changes useEffect(() => { setDisplayValue(getFormattedValue(value || '')); }, [value]); - // EVENT HANDLERS const handleInputChange = (e: ChangeEvent) => { - 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 const cleaned = cleanNumericInput( @@ -267,6 +262,7 @@ const NumberInput = ({ // Call onChange with modified event if (onChange) { + // Create a synthetic event with the numeric value const syntheticEvent = { ...e, target: { @@ -280,6 +276,7 @@ const NumberInput = ({ } }; + // Handle Increment const handleIncrement = () => { if (disabled || readOnly) return; @@ -317,6 +314,7 @@ const NumberInput = ({ } }; + // Handle Decrement const handleDecrement = () => { 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 (
)} - {/* Input Container */} -
- {/* Prefix Block */} - {inputPrefix && ( -
+ {/* Decrement Button */} + {showSteppers && ( + + )} + + {startAdornment && startAdornment} + + + + {(isLoading || endAdornment) && ( +
+ {isLoading && } + + {endAdornment && endAdornment}
)} - {/* 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 */} - + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + {isError && errorMessage && ( +

{errorMessage}

+ )}
); }; diff --git a/src/components/input/RadioInput.tsx b/src/components/input/RadioInput.tsx index 65589258..71a731aa 100644 --- a/src/components/input/RadioInput.tsx +++ b/src/components/input/RadioInput.tsx @@ -2,7 +2,6 @@ import { ChangeEventHandler, ReactNode } from 'react'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; export interface RadioOption { label: string; @@ -48,8 +47,6 @@ const RadioInput = ({ onChange, onBlur, }: RadioInputProps) => { - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; return (
{/* Label atas */} @@ -100,11 +97,15 @@ const RadioInput = ({ ))}
- + {/* Label bawah */} + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + + {/* Pesan error */} + {isError && errorMessage && ( +

{errorMessage}

+ )}
); }; diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index 9caf2e17..28eb9786 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -12,7 +12,6 @@ import CreatableSelect from 'react-select/creatable'; import makeAnimated from 'react-select/animated'; import { useDebounce } from 'use-debounce'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; export interface OptionType { value: string | number; @@ -118,9 +117,6 @@ const SelectInput = (props: SelectInputProps) => { } }; - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; - return (
(props: SelectInputProps) => { }} /> - + {isError &&

{errorMessage}

} + {!isError && bottomLabel && ( +

{bottomLabel}

+ )}
); }; diff --git a/src/components/input/TagInput.tsx b/src/components/input/TagInput.tsx index 11407ada..a14b2f63 100644 --- a/src/components/input/TagInput.tsx +++ b/src/components/input/TagInput.tsx @@ -2,7 +2,6 @@ import React, { useState, KeyboardEvent, ChangeEvent, useEffect } from 'react'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; export interface TagInputProps { label?: string; @@ -74,9 +73,6 @@ const TagInput: React.FC = ({ setInputValue(e.target.value); }; - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; - return (
= ({ )}
- + {/* Bottom label or error message */} + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + {isError &&

{errorMessage}

} ); }; diff --git a/src/components/input/TextArea.tsx b/src/components/input/TextArea.tsx index 34644021..6e93811c 100644 --- a/src/components/input/TextArea.tsx +++ b/src/components/input/TextArea.tsx @@ -3,7 +3,6 @@ import { ChangeEventHandler, FocusEventHandler, ReactNode } from 'react'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; export interface TextAreaProps { label?: string; @@ -51,9 +50,6 @@ const TextArea = ({ isLoading = false, rows = 3, }: TextAreaProps) => { - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; - return (
)} - + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + {isError &&

{errorMessage}

}
); }; diff --git a/src/components/input/TextInput.tsx b/src/components/input/TextInput.tsx index 1bd154b6..eec312c1 100644 --- a/src/components/input/TextInput.tsx +++ b/src/components/input/TextInput.tsx @@ -8,7 +8,6 @@ import { } from 'react'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; export interface TextInputProps { type?: HTMLInputTypeAttribute; @@ -56,9 +55,6 @@ const TextInput = ({ readOnly = false, isLoading = false, }: TextInputProps) => { - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; - return (
- + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + {isError && errorMessage && ( +

{errorMessage}

+ )}
); }; diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 57d0a585..711b7d20 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -29,7 +29,6 @@ import { SupplierApi, WarehouseApi } from '@/services/api/master-data'; import { ProductWarehouseApi } from '@/services/api/inventory'; import { toast } from 'react-hot-toast'; import FileInput from '@/components/input/FileInput'; -import FieldMessage from '@/components/helper/FieldMessage'; import CheckboxInput from '@/components/input/CheckboxInput'; interface MovementFormProps { @@ -910,7 +909,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { naked={true} size='sm' /> - )} @@ -1006,7 +1004,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { height={24} /> - )} @@ -1174,7 +1171,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { naked={true} size='sm' /> - )} @@ -1323,10 +1319,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { '-' )} - ) : ( @@ -1444,7 +1436,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { height={24} /> - )} diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 8c166700..c654f2ba 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -30,7 +30,6 @@ import { ProductWarehouseApi } from '@/services/api/inventory'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { Warehouse } from '@/types/api/master-data/warehouse'; import { LocationApi } from '@/services/api/master-data'; -import FieldMessage from '@/components/helper/FieldMessage'; import Card from '@/components/Card'; interface RecordingFormProps { @@ -829,7 +828,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { naked={true} size='sm' /> - )} @@ -948,7 +946,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { height={24} /> - )} @@ -1078,7 +1075,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { naked={true} size='sm' /> - )} @@ -1191,7 +1187,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { height={24} /> - )} @@ -1313,7 +1308,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { naked={true} size='sm' /> - )} @@ -1448,7 +1442,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { height={24} /> - )} @@ -1572,7 +1565,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { naked={true} size='sm' /> - )} @@ -1646,7 +1638,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { height={24} /> - )} diff --git a/src/styles/daisyui.css b/src/styles/daisyui.css index 9a148fb4..dadaa2fa 100644 --- a/src/styles/daisyui.css +++ b/src/styles/daisyui.css @@ -8,4 +8,8 @@ --step-bg: var(--color-error); --step-fg: var(--color-error-content); } + + .table :where(th, td) { + vertical-align: top; + } }