From d0d323954bcdf7e2167e8f3a55665b60d9bd1716 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 23 Oct 2025 13:10:03 +0700 Subject: [PATCH 01/10] feat: install husky --- package-lock.json | 17 +++++++++++++++++ package.json | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 1aa69d33..a39060ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "daisyui": "^5.1.12", "eslint": "^9", "eslint-config-next": "15.5.3", + "husky": "^9.1.7", "tailwindcss": "^4", "typescript": "^5" } @@ -4176,6 +4177,22 @@ "react-is": "^16.7.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", diff --git a/package.json b/package.json index 8adf6787..e970499c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "eslint && next dev --turbopack", "build": "next build --turbopack", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "prepare": "husky" }, "dependencies": { "@tanstack/match-sorter-utils": "^8.19.4", @@ -36,6 +37,7 @@ "daisyui": "^5.1.12", "eslint": "^9", "eslint-config-next": "15.5.3", + "husky": "^9.1.7", "tailwindcss": "^4", "typescript": "^5" } From 70e1aca6c7ee48f1d7e3a31eb71060e3c08f7961 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 23 Oct 2025 13:13:34 +0700 Subject: [PATCH 02/10] feat: create husky pre-commit file --- .husky/pre-commit | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..66ff6a67 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +npm run lint +npm run build From 7e53743b070d0b68e876e5fe77baa6e2fc2dbdf1 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 23 Oct 2025 13:14:16 +0700 Subject: [PATCH 03/10] refactor(FE-114): remove FieldMessage component usage and streamline error message handling in form inputs --- src/components/input/CheckboxInput.tsx | 13 +- src/components/input/FileInput.tsx | 14 +- src/components/input/NumberInput.tsx | 376 +++++++----------- src/components/input/RadioInput.tsx | 17 +- src/components/input/SelectInput.tsx | 13 +- src/components/input/TagInput.tsx | 14 +- src/components/input/TextArea.tsx | 15 +- src/components/input/TextInput.tsx | 17 +- .../inventory/movement/form/MovementForm.tsx | 9 - .../recording/form/RecordingForm.tsx | 9 - src/styles/daisyui.css | 4 + 11 files changed, 196 insertions(+), 305 deletions(-) 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; + } } From 2de32dc944f29053e8895b5b6453831ecc8ab868 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 23 Oct 2025 13:49:43 +0700 Subject: [PATCH 04/10] refactor(FE-114): simplify CheckboxInput component and enhance styling options --- src/components/input/CheckboxInput.tsx | 294 +++--------------- .../inventory/movement/form/MovementForm.tsx | 44 ++- .../recording/form/RecordingForm.tsx | 123 +++++--- 3 files changed, 157 insertions(+), 304 deletions(-) diff --git a/src/components/input/CheckboxInput.tsx b/src/components/input/CheckboxInput.tsx index c3c5be0a..fb0c95c7 100644 --- a/src/components/input/CheckboxInput.tsx +++ b/src/components/input/CheckboxInput.tsx @@ -1,279 +1,87 @@ 'use client'; -import { ChangeEventHandler, FocusEventHandler, ReactNode, useId } from 'react'; - +import { HTMLProps, useEffect, useRef } from 'react'; import { cn } from '@/lib/helper'; -import FieldMessage from '@/components/helper/FieldMessage'; -export interface CheckboxInputProps { - // Basic Props +interface CheckboxInputProps extends HTMLProps { name: string; label?: string; - bottomLabel?: string; - checked?: boolean; - value?: string | number; indeterminate?: boolean; - naked?: boolean; // New prop for checkbox-only mode - - // Styling Props - className?: { + classNames?: { wrapper?: string; - label?: string; + inputWrapper?: string; checkbox?: string; - input?: string; + label?: 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; - - // Additional Props - tooltip?: string; - description?: string; - size?: 'sm' | 'md' | 'lg'; - variant?: - | 'default' - | 'primary' - | 'secondary' - | 'success' - | 'warning' - | 'info' - | 'error'; } const CheckboxInput = ({ + indeterminate, name, label, - bottomLabel, - checked = false, - value, - indeterminate = false, - naked = false, className, + classNames, + isValid, isError, errorMessage, - disabled = false, - readOnly = false, - required = false, - isLoading = false, - startAdornment, - endAdornment, - onChange, - onBlur, - onFocus, - tooltip, - description, - size = 'md', - variant = 'default', + ...rest }: CheckboxInputProps) => { - const showErrorMessage = Boolean(isError && errorMessage); - const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel; + const ref = useRef(null!); - // Size classes - const sizeClasses = { - sm: 'checkbox-sm', - md: 'checkbox-md', - lg: 'checkbox-lg', - }; - - // Variant classes - const variantClasses = { - default: '', - primary: 'checkbox-primary', - secondary: 'checkbox-secondary', - success: 'checkbox-success', - warning: 'checkbox-warning', - info: 'checkbox-info', - error: 'checkbox-error', - }; - - // Generate unique ID for accessibility using React's useId hook for SSR compatibility - const generatedId = useId(); - const checkboxId = `checkbox-${name}-${generatedId}`; - - // Naked mode - only checkbox, no wrapper structure - if (naked) { - return ( -
- { - if (input) { - input.indeterminate = indeterminate; - } - }} - /> - - {/* Loading State */} - {isLoading && ( -
- -
- )} -
- ); - } + useEffect(() => { + if (typeof indeterminate === 'boolean') { + ref.current.indeterminate = !rest.checked && indeterminate; + } + }, [ref, indeterminate]); return (
- {/* Label with Tooltip Support */} - {label && ( -
- - )} - - {/* Description */} - {description && ( -

- {description} -

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

{bottomLabel}

- )} - {isError && errorMessage && ( -

{errorMessage}

- )} + {errorMessage && {errorMessage}} ); }; diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 711b7d20..d2c91168 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -847,7 +847,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { selectedProducts.length && formik.values.products?.length > 0 } - onChange={(e) => { + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedProducts( formik.values.products?.map( @@ -858,8 +860,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { setSelectedProducts([]); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} /> @@ -890,11 +894,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {type !== 'detail' && ( -
+
{ + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedProducts([ ...selectedProducts, @@ -906,8 +912,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { ); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} />
@@ -1061,7 +1069,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { selectedDeliveries.length && formik.values.deliveries?.length > 0 } - onChange={(e) => { + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedDeliveries( formik.values.deliveries?.map( @@ -1072,8 +1082,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { setSelectedDeliveries([]); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} />
@@ -1150,11 +1162,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {type !== 'detail' && ( -
+
{ + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedDeliveries([ ...selectedDeliveries, @@ -1168,8 +1182,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { ); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} />
diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index c654f2ba..2735a7e9 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -760,29 +760,30 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {type !== 'detail' && ( -
- 0 + 0 + } + onChange={( + e: React.ChangeEvent + ) => { + if (e.target.checked) { + setSelectedFeed( + formik.values.feed_data?.map((_, idx) => idx) ?? + [] + ); + } else { + setSelectedFeed([]); } - onChange={(e) => { - if (e.target.checked) { - setSelectedFeed( - formik.values.feed_data?.map( - (_, idx) => idx - ) ?? [] - ); - } else { - setSelectedFeed([]); - } - }} - naked={true} - size='sm' - /> -
+ }} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + /> )} @@ -812,11 +813,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {type !== 'detail' && ( -
+
{ + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedFeed([...selectedFeed, idx]); } else { @@ -825,8 +828,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} />
@@ -1007,7 +1012,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { selectedWeight.length && formik.values.body_weight?.length > 0 } - onChange={(e) => { + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedWeight( formik.values.body_weight?.map( @@ -1018,8 +1025,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedWeight([]); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} />
@@ -1059,11 +1068,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {type !== 'detail' && ( -
+
{ + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedWeight([...selectedWeight, idx]); } else { @@ -1072,8 +1083,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} />
@@ -1248,7 +1261,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { selectedVaccine.length && formik.values.vaccination?.length > 0 } - onChange={(e) => { + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedVaccine( formik.values.vaccination?.map( @@ -1259,8 +1274,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedVaccine([]); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} />
@@ -1292,11 +1309,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {type !== 'detail' && ( -
+
{ + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedVaccine([...selectedVaccine, idx]); } else { @@ -1305,8 +1324,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} />
@@ -1503,7 +1524,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { selectedMortality.length && formik.values.mortality?.length > 0 } - onChange={(e) => { + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedMortality( formik.values.mortality?.map( @@ -1514,8 +1537,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedMortality([]); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} />
@@ -1546,11 +1571,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {type !== 'detail' && ( -
+
{ + onChange={( + e: React.ChangeEvent + ) => { if (e.target.checked) { setSelectedMortality([ ...selectedMortality, @@ -1562,8 +1589,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); } }} - naked={true} - size='sm' + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} />
From e6f5b2493bd13566fc11e2ab151625a1fd9b1b33 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 23 Oct 2025 13:51:34 +0700 Subject: [PATCH 05/10] refactor(FE-Storyless): update input components to include consistent background styling --- src/components/input/NumberInput.tsx | 2 +- src/components/input/TextArea.tsx | 2 +- src/components/input/TextInput.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/input/NumberInput.tsx b/src/components/input/NumberInput.tsx index 5710e932..2afe7e55 100644 --- a/src/components/input/NumberInput.tsx +++ b/src/components/input/NumberInput.tsx @@ -387,7 +387,7 @@ const NumberInput = ({
Date: Thu, 23 Oct 2025 15:09:39 +0700 Subject: [PATCH 06/10] chore(FE-114): add inputmask and its type definitions to package.json --- package-lock.json | 27 ++++++++++++++++++++++++++- package.json | 2 ++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index a39060ef..4a583dbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.12.2", "clsx": "^2.1.1", "formik": "^2.4.6", + "inputmask": "^5.0.9", "moment": "^2.30.1", "next": "15.5.3", "react": "19.1.0", @@ -29,6 +30,7 @@ "@eslint/eslintrc": "^3", "@iconify/react": "^6.0.2", "@tailwindcss/postcss": "^4", + "@types/inputmask": "^5.0.7", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -1640,6 +1642,13 @@ "@types/react": "*" } }, + "node_modules/@types/inputmask": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/inputmask/-/inputmask-5.0.7.tgz", + "integrity": "sha512-uojbVPWzBQ/n/0jc/d16fLqmGasFIptbrLD2WrCPWArlk+5PgblOqH4EDkI3AoobXLAlOK5yF01V8jMmvMG5qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1675,6 +1684,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1744,6 +1754,7 @@ "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", @@ -2261,6 +2272,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2829,7 +2841,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.1.12", @@ -3263,6 +3276,7 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3437,6 +3451,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4229,6 +4244,12 @@ "node": ">=0.8.19" } }, + "node_modules/inputmask": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/inputmask/-/inputmask-5.0.9.tgz", + "integrity": "sha512-s0lUfqcEbel+EQXtehXqwCJGShutgieOaIImFKC/r4reYNvX3foyrChl6LOEvaEgxEbesePIrw1Zi2jhZaDZbQ==", + "license": "MIT" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5766,6 +5787,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5775,6 +5797,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6591,6 +6614,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6758,6 +6782,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index e970499c..70e5737f 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "axios": "^1.12.2", "clsx": "^2.1.1", "formik": "^2.4.6", + "inputmask": "^5.0.9", "moment": "^2.30.1", "next": "15.5.3", "react": "19.1.0", @@ -31,6 +32,7 @@ "@eslint/eslintrc": "^3", "@iconify/react": "^6.0.2", "@tailwindcss/postcss": "^4", + "@types/inputmask": "^5.0.7", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", From ae967c5ddb863582c6ce7db8aa5b271c4df2050e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 23 Oct 2025 16:00:24 +0700 Subject: [PATCH 07/10] refactor(FE-114): integrate inputmask for enhanced numeric input handling and validation --- src/components/input/NumberInput.tsx | 617 ++++++++++++--------------- 1 file changed, 281 insertions(+), 336 deletions(-) diff --git a/src/components/input/NumberInput.tsx b/src/components/input/NumberInput.tsx index 2afe7e55..96c7f446 100644 --- a/src/components/input/NumberInput.tsx +++ b/src/components/input/NumberInput.tsx @@ -6,101 +6,47 @@ import { FocusEventHandler, ReactNode, useEffect, + useRef, useState, } from 'react'; -import { Icon } from '@iconify/react'; import { cn } from '@/lib/helper'; +import Inputmask from 'inputmask'; -// Utility Functions -const formatNumber = ( - value: number | string, - decimals: number = 0, - thousandSeparator: string = '.', - decimalSeparator: string = ',' -): string => { - if (value === '' || value === null || value === undefined) return ''; +const createInputMask = ( + maskType: MaskType, + decimals: number, + thousandSeparator: string, + decimalSeparator: string, + allowNegative: boolean, + oncomplete?: () => void, + onincomplete?: () => void, + oncleared?: () => void +): Inputmask.Instance => { + const options: Inputmask.Options = { + alias: 'numeric', + groupSeparator: thousandSeparator, + radixPoint: decimalSeparator, + digits: decimals, + allowMinus: allowNegative, + rightAlign: false, + insertMode: true, + autoUnmask: false, + clearMaskOnLostFocus: false, + digitsOptional: decimals > 0, + placeholder: '0', + numericInput: false, + positionCaretOnClick: 'radixFocus', + greedy: true, + oncomplete, + onincomplete, + oncleared + }; - 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; + return new Inputmask(options); }; -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 type MaskType = 'currency' | 'weight' | 'decimal' | 'number' | 'text'; export interface NumberInputProps { label?: string; @@ -108,252 +54,188 @@ export interface NumberInputProps { name: string; value?: number | string; placeholder?: string; + className?: { wrapper?: string; label?: string; inputWrapper?: string; input?: string; }; + isError?: boolean; isValid?: boolean; + errorMessage?: string; 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; + oncomplete?: () => void; + onincomplete?: () => void; + oncleared?: () => void; } 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(''); - - // 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 ''; + 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', + allowNegative = false, + oncomplete, + onincomplete, + oncleared, + }: NumberInputProps) => { + const inputRef = useRef(null); + const inputmaskRef = useRef(null); + const [maskComplete, setMaskComplete] = useState(false); + const [maskIncomplete, setMaskIncomplete] = useState(false); + const [maskCleared, setMaskCleared] = useState(false); + const getInputPrefix = (): string => { 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 - ); + return currencyPrefix; default: - return String(rawValue); + return ''; + } + }; + + const getInputSuffix = (): string => { + switch (maskType) { + case 'weight': + return weightUnit; + default: + return ''; } }; - // Initialize display value when value prop changes useEffect(() => { - setDisplayValue(getFormattedValue(value || '')); + if (inputRef.current && !readOnly && !disabled) { + if (inputmaskRef.current) { + try { + inputmaskRef.current.remove(); + } catch (error) { + console.warn('Error removing Inputmask:', error); + } + } + + const handleComplete = () => { + setMaskComplete(true); + setMaskIncomplete(false); + setMaskCleared(false); + if (oncomplete) oncomplete(); + }; + + const handleIncomplete = () => { + setMaskIncomplete(true); + setMaskComplete(false); + setMaskCleared(false); + if (onincomplete) onincomplete(); + }; + + const handleCleared = () => { + setMaskCleared(true); + setMaskComplete(false); + setMaskIncomplete(false); + if (oncleared) oncleared(); + }; + + const im = createInputMask( + maskType, + decimals, + thousandSeparator, + decimalSeparator, + allowNegative, + handleComplete, + handleIncomplete, + handleCleared + ); + + try { + im.mask(inputRef.current); + inputmaskRef.current = im; + } catch (error) { + console.warn('Error applying Inputmask:', error); + inputmaskRef.current = null; + } + } + + return () => { + if (inputmaskRef.current) { + try { + inputmaskRef.current.remove(); + } catch (error) { + console.warn('Error removing Inputmask on cleanup:', error); + } + } + }; + }, [maskType, decimals, thousandSeparator, decimalSeparator, allowNegative, readOnly, disabled, oncomplete, onincomplete, oncleared]); + + useEffect(() => { + if (inputRef.current && value !== undefined) { + if (value === null || value === '') { + inputRef.current.value = ''; + } else { + inputRef.current.value = String(value); + } + } }, [value]); - const handleInputChange = (e: ChangeEvent) => { - let inputValue = e.target.value; + const handleKeyUp = (e: React.KeyboardEvent) => { + const currentValue = (e.currentTarget as HTMLInputElement).value; + console.log('✅ After format:', currentValue); - // 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; - - 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(), + value: currentValue, }, } as ChangeEvent; - 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; - - onChange(syntheticEvent); - } - }; + const inputPrefix = getInputPrefix(); + const inputSuffix = getInputSuffix(); return (
)} -
- {/* Decrement Button */} - {showSteppers && ( - - )} - - {startAdornment && startAdornment} - - - - {(isLoading || endAdornment) && ( -
- {isLoading && } - - {endAdornment && endAdornment} + + {inputPrefix} +
)} - {/* Increment Button */} - {showSteppers && ( -
+ + {inputSuffix && ( +
- - + + {inputSuffix} + +
)}
+ {(maskType === 'text' || (oncomplete || onincomplete || oncleared)) && ( +
+ + Complete + + + Incomplete + + + Cleared + +
+ )} + {!isError && bottomLabel && (

{bottomLabel}

)} @@ -468,3 +412,4 @@ const NumberInput = ({ }; export default NumberInput; + From 90ae7c469a9cfbc81b071a03a26c82f4153d6ba4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 23 Oct 2025 16:48:55 +0700 Subject: [PATCH 08/10] refactor(FE-114): swap thousand and decimal separators for improved usability --- src/components/input/NumberInput.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/input/NumberInput.tsx b/src/components/input/NumberInput.tsx index 96c7f446..5b2188ee 100644 --- a/src/components/input/NumberInput.tsx +++ b/src/components/input/NumberInput.tsx @@ -114,8 +114,8 @@ const NumberInput = ({ isLoading = false, maskType = 'number', decimals = 0, - thousandSeparator = '.', - decimalSeparator = ',', + thousandSeparator = ',', + decimalSeparator = '.', currencyPrefix = 'Rp ', weightUnit = 'kg', allowNegative = false, @@ -178,11 +178,11 @@ const NumberInput = ({ if (oncleared) oncleared(); }; - const im = createInputMask( + const im = createInputMask( maskType, decimals, - thousandSeparator, - decimalSeparator, + ',', + '.', allowNegative, handleComplete, handleIncomplete, From 3cf8f4c89b35dd8b0ba52978e5eee5ea94bb7aba Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 23 Oct 2025 16:49:32 +0700 Subject: [PATCH 09/10] refactor(FE-114): enhance numeric input handling for chicken weight and count with improved formatting --- .../production/recording/form/RecordingForm.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 2735a7e9..120cddb8 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -487,7 +487,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { // Create wrapper handlers that match NumberInput's onChange signature const handleChickenWeightChangeWrapper = useCallback( (idx: number) => (e: React.ChangeEvent) => { - const value = parseFloat(e.target.value) || 0; + const value = parseFloat(e.target.value.replace(/[^\d,.-]/g, '').replace(/,/g, '')) || 0; handleChickenWeightChange(idx, value); }, [handleChickenWeightChange] @@ -495,7 +495,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const handleChickenCountChangeWrapper = useCallback( (idx: number) => (e: React.ChangeEvent) => { - const value = parseFloat(e.target.value) || 0; + const value = parseFloat(e.target.value.replace(/[^\d,.-]/g, '').replace(/,/g, '')) || 0; handleChickenCountChange(idx, value); }, [handleChickenCountChange] @@ -503,7 +503,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const handleAverageWeightChangeWrapper = useCallback( (idx: number) => (e: React.ChangeEvent) => { - const value = parseFloat(e.target.value) || 0; + const value = parseFloat(e.target.value.replace(/[^\d,.-]/g, '').replace(/,/g, '')) || 0; handleAverageWeightChange(idx, value); }, [handleAverageWeightChange] @@ -1100,8 +1100,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { onBlur={formik.handleBlur} maskType='weight' weightUnit='gram' - decimals={0} + decimals={2} min={0} + thousandSeparator=',' + decimalSeparator='.' isError={ isRepeaterInputError( 'body_weight', @@ -1162,8 +1164,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { onBlur={formik.handleBlur} maskType='weight' weightUnit='gram' - decimals={0} + decimals={2} min={0} + thousandSeparator=',' + decimalSeparator='.' isError={ isRepeaterInputError( 'body_weight', From 3f76cb58fe5edbf3edc62de8a8a9a911463e7e6a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 23 Oct 2025 17:15:17 +0700 Subject: [PATCH 10/10] refactor(FE-114): improve alignment and styling of checkbox inputs in RecordingForm --- .../recording/form/RecordingForm.tsx | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 120cddb8..45f0cbf5 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -812,19 +812,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {formik.values.feed_data?.map((feed, idx) => ( {type !== 'detail' && ( - -
+ + e: React.ChangeEvent, ) => { if (e.target.checked) { setSelectedFeed([...selectedFeed, idx]); } else { setSelectedFeed( - selectedFeed.filter((i) => i !== idx) + selectedFeed.filter((i) => i !== idx), ); } }} @@ -833,11 +832,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { checkbox: 'checkbox checkbox-sm', }} /> -
)} - { {type !== 'detail' && ( -
+
{ } }} classNames={{ - wrapper: 'flex justify-center', + wrapper: 'flex justify-center items-center h-full', checkbox: 'checkbox checkbox-sm', }} /> @@ -1067,8 +1065,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {formik.values.body_weight?.map((weight, idx) => ( {type !== 'detail' && ( - -
+ { } }} classNames={{ - wrapper: 'flex justify-center', + wrapper: 'flex justify-center items-center', checkbox: 'checkbox checkbox-sm', }} /> -
)} @@ -1257,7 +1253,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {type !== 'detail' && ( -
+
{ } }} classNames={{ - wrapper: 'flex justify-center', + wrapper: 'flex justify-center items-center h-full', checkbox: 'checkbox checkbox-sm', }} /> @@ -1312,19 +1308,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {formik.values.vaccination?.map((vaccine, idx) => ( {type !== 'detail' && ( - -
+ + e: React.ChangeEvent, ) => { if (e.target.checked) { setSelectedVaccine([...selectedVaccine, idx]); } else { setSelectedVaccine( - selectedVaccine.filter((i) => i !== idx) + selectedVaccine.filter((i) => i !== idx), ); } }} @@ -1333,11 +1328,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { checkbox: 'checkbox checkbox-sm', }} /> -
)} - { {type !== 'detail' && ( -
+
{ } }} classNames={{ - wrapper: 'flex justify-center', + wrapper: 'flex justify-center items-center h-full', checkbox: 'checkbox checkbox-sm', }} /> @@ -1574,13 +1568,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {formik.values.mortality?.map((mortality, idx) => ( {type !== 'detail' && ( - -
+ + e: React.ChangeEvent, ) => { if (e.target.checked) { setSelectedMortality([ @@ -1589,7 +1582,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ]); } else { setSelectedMortality( - selectedMortality.filter((i) => i !== idx) + selectedMortality.filter((i) => i !== idx), ); } }} @@ -1598,11 +1591,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { checkbox: 'checkbox checkbox-sm', }} /> -
)} - opt.value === mortality.condition