- Implement PriceInput component tests to validate Persian and Arabic numeral handling, input formatting, and controlled behavior. - Create ServiceDetailPage component with detailed service information, including pricing, insurance coverage, and editing capabilities. - Add API tests for service item detail retrieval and coverage synchronization with insurance contracts. - Ensure proper error handling and user feedback for service item retrieval and coverage management.
99 lines
3.4 KiB
TypeScript
99 lines
3.4 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;
|
|
}
|
|
|
|
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,
|
|
}: 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"
|
|
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>
|
|
);
|
|
}
|