Files
clinicpro/assets/admin/components/ui/PriceInput.tsx
T
hamed b49abee52f refactor: update pricing input handling to use PriceInput component
- Replaced raw input fields for pricing with PriceInput component across various forms and modals to ensure consistent formatting and accessibility.
- Updated tests to reflect changes in pricing input handling, ensuring values are displayed in toman with proper formatting.
- Enhanced accessibility by adding aria-labels and aria-invalid attributes to PriceInput components.
- Adjusted UI elements to improve layout and user experience, particularly in forms related to service items and scheduling.
- Changed labels from "نمایش در نوبت‌دهی" to "نمایش در نوبت‌دهی آنلاین" for clarity.
2026-08-09 08:38:23 +03:30

110 lines
3.8 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react';
import { digitsOnly, parseUserNumber } from '../../lib/utils';
interface PriceInputProps {
value: number | '';
onChange: (value: number) => void;
placeholder?: string;
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
min?: number;
max?: number;
/** نمایش ارقام لاتین با جداکنندهٔ کاما (پیش‌فرض: فارسی). */
latin?: boolean;
/** واحد نمایشی داخل فیلد، مثلاً «تومان». */
suffix?: string;
/** وقتی برچسبِ کنار فیلد `<label>` واقعی نیست (span/div)، نامِ دسترسی‌پذیر. */
ariaLabel?: string;
/** حالت خطا برای صفحه‌خوان — معادل `aria-invalid` روی input خام. */
ariaInvalid?: boolean;
id?: string;
}
function formatDisplay(num: number, latin: boolean): string {
return new Intl.NumberFormat(latin ? 'en-US' : 'fa-IR').format(num);
}
/** مقدار prop را به متن نمایشی تبدیل می‌کند؛ صفر و خالی هر دو فیلد خالی هستند. */
function textFromValue(value: number | '', latin: boolean): string {
const num = value === '' ? 0 : Number(value);
return Number.isFinite(num) && num !== 0 ? formatDisplay(num, latin) : '';
}
/**
* ورودی مبلغ: ارقام فارسی/عربی، جداکننده و واحد چسبیده به عدد را می‌پذیرد (پیست
* «۸۵,۰۰۰ تومان» ⇒ 85000) و همیشه یک عدد معتبر — هرگز NaN — به بالا می‌دهد.
*
* متن فیلد state مستقل است تا «۰ تایپ‌شده» از «خالی» تفکیک شود؛ محدودکردن به بازهٔ
* min/max فقط روی blur اعمال می‌شود تا رقم اول تایپ به مرز نپرد.
*/
export default function PriceInput({
value,
onChange,
placeholder = '0',
className,
style,
disabled,
min = 0,
max,
latin = false,
suffix,
ariaLabel,
ariaInvalid,
id,
}: PriceInputProps) {
const [text, setText] = useState(() => textFromValue(value, latin));
const textRef = useRef(text);
textRef.current = text;
// فقط وقتی مقدار از بیرون عوض شده باشد (reset فرم، بارگذاری داده) متن را بازنویسی کن.
useEffect(() => {
const shown = parseUserNumber(textRef.current) ?? 0;
const incoming = value === '' ? 0 : Number(value);
if (shown !== incoming) setText(textFromValue(value, latin));
}, [value, latin]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const raw = digitsOnly(e.target.value);
const num = parseUserNumber(raw) ?? 0;
setText(raw === '' ? '' : formatDisplay(num, latin));
onChange(num);
};
const handleBlur = () => {
if (text === '') return;
const num = parseUserNumber(text) ?? 0;
const clamped = Math.min(max ?? Infinity, Math.max(min, num));
if (clamped !== num) {
setText(formatDisplay(clamped, latin));
onChange(clamped);
}
};
const input = (
<input
type="text"
inputMode="numeric"
id={id}
aria-label={ariaLabel}
aria-invalid={ariaInvalid || undefined}
value={text}
onChange={handleChange}
onBlur={handleBlur}
placeholder={placeholder}
className={className}
style={suffix ? { textAlign: 'left', direction: 'ltr', border: 'none', background: 'transparent', outline: 'none', flex: 1, minWidth: 0, padding: 0, ...style } : { textAlign: 'left', direction: 'ltr', ...style }}
disabled={disabled}
/>
);
if (!suffix) return input;
return (
<span style={{ display: 'flex', alignItems: 'center', gap: 6, width: '100%' }}>
{input}
<span style={{ fontSize: 12, color: 'var(--text-3)', flexShrink: 0 }}>{suffix}</span>
</span>
);
}