- setMobile(e.target.value)} placeholder="شماره تماس مراجعه کننده را وارد نمایید" dir="ltr" />
+ setMobile(sanitizeMobileInput(e.target.value))} placeholder="شماره تماس مراجعه کننده را وارد نمایید" dir="ltr" inputMode="numeric" lang="en" maxLength={11} />
- setNationalCode(e.target.value.replace(/\D/g, '').slice(0, 10))}
- placeholder="کد ملی مراجعه کننده را وارد نمایید" dir="ltr" inputMode="numeric" maxLength={10} />
+ setNationalCode(toEnglishDigits(e.target.value).replace(/\D/g, '').slice(0, 10))}
+ placeholder="کد ملی مراجعه کننده را وارد نمایید" dir="ltr" inputMode="numeric" lang="en" maxLength={10} />
>
)}
diff --git a/assets/admin/components/ui/Input.tsx b/assets/admin/components/ui/Input.tsx
index 246996d8..91a9a71a 100644
--- a/assets/admin/components/ui/Input.tsx
+++ b/assets/admin/components/ui/Input.tsx
@@ -1,21 +1,37 @@
import React from 'react';
+import { toEnglishDigits } from '../../lib/utils';
type Props = React.InputHTMLAttributes & {
hasError?: boolean;
+ /** فیلد فقطعددی: کیبورد عددی، جهت LTR، و تبدیل زندهٔ ارقام فارسی/عربی به لاتین. */
+ numeric?: boolean;
};
/**
* Input متنی پایهٔ design system: کلاس `cp-input`، حالت خطا (قاب قرمز)،
* و forwardRef برای سازگاری با react-hook-form `register`.
+ * با `numeric`، ورودی به لاتین نرمال میشود تا از ثبت ارقام فارسی جلوگیری شود.
*/
export default React.forwardRef(function Input(
- { hasError, className, style, ...rest },
+ { hasError, numeric, className, style, onChange, inputMode, dir, lang, ...rest },
ref,
) {
+ const handleChange = numeric
+ ? (e: React.ChangeEvent) => {
+ const latin = toEnglishDigits(e.target.value);
+ if (latin !== e.target.value) e.target.value = latin;
+ onChange?.(e);
+ }
+ : onChange;
+
return (
diff --git a/assets/admin/components/ui/InputField.test.tsx b/assets/admin/components/ui/InputField.test.tsx
index 18ae62c4..7cd880d7 100644
--- a/assets/admin/components/ui/InputField.test.tsx
+++ b/assets/admin/components/ui/InputField.test.tsx
@@ -1,5 +1,5 @@
-import { describe, it, expect } from 'vitest';
-import { render, screen } from '@testing-library/react';
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
import Input from '@/components/ui/Input';
import Field from '@/components/ui/Field';
@@ -16,6 +16,31 @@ describe('Input', () => {
const el = screen.getByPlaceholderText('کدملی') as HTMLInputElement;
expect(el.style.borderColor).toBe('var(--danger)');
});
+
+ it('با numeric کیبورد عددی و جهت LTR میگیرد', () => {
+ render();
+ const el = screen.getByPlaceholderText('مبلغ') as HTMLInputElement;
+ expect(el.inputMode).toBe('numeric');
+ expect(el.getAttribute('dir')).toBe('ltr');
+ expect(el.getAttribute('lang')).toBe('en');
+ });
+
+ it('با numeric ارقام فارسی را قبل از onChange به لاتین تبدیل میکند', () => {
+ const onChange = vi.fn();
+ render();
+ const el = screen.getByPlaceholderText('مبلغ') as HTMLInputElement;
+ fireEvent.change(el, { target: { value: '۱۲۳۴' } });
+ expect(onChange).toHaveBeenCalledTimes(1);
+ expect(onChange.mock.calls[0][0].target.value).toBe('1234');
+ });
+
+ it('بدون numeric ورودی را دستنخورده میگذارد', () => {
+ const onChange = vi.fn();
+ render();
+ const el = screen.getByPlaceholderText('نام') as HTMLInputElement;
+ fireEvent.change(el, { target: { value: 'علی۱۲' } });
+ expect(onChange.mock.calls[0][0].target.value).toBe('علی۱۲');
+ });
});
describe('Field', () => {
diff --git a/assets/admin/components/ui/PriceInput.tsx b/assets/admin/components/ui/PriceInput.tsx
index 0f55f785..5d3ab30b 100644
--- a/assets/admin/components/ui/PriceInput.tsx
+++ b/assets/admin/components/ui/PriceInput.tsx
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
+import { toEnglishDigits } from '../../lib/utils';
interface PriceInputProps {
value: number | '';
@@ -10,17 +11,6 @@ interface PriceInputProps {
min?: number;
}
-const PERSIAN_TO_LATIN: Record = {
- '۰': '0', '۱': '1', '۲': '2', '۳': '3', '۴': '4',
- '۵': '5', '۶': '6', '۷': '7', '۸': '8', '۹': '9',
- '٠': '0', '١': '1', '٢': '2', '٣': '3', '٤': '4',
- '٥': '5', '٦': '6', '٧': '7', '٨': '8', '٩': '9',
-};
-
-function toLatinDigits(str: string): string {
- return str.replace(/[۰-۹٠-٩]/g, (ch) => PERSIAN_TO_LATIN[ch] ?? ch);
-}
-
function formatDisplay(num: number): string {
if (num === 0) return '';
return new Intl.NumberFormat('fa-IR').format(num);
@@ -42,7 +32,7 @@ export default function PriceInput({
}, [value]);
const handleChange = (e: React.ChangeEvent) => {
- const raw = toLatinDigits(e.target.value).replace(/[^0-9]/g, '');
+ const raw = toEnglishDigits(e.target.value).replace(/[^0-9]/g, '');
const num = raw === '' ? 0 : Math.max(min, parseInt(raw, 10));
onChange(num);
setDisplay(num > 0 ? formatDisplay(num) : '');
diff --git a/assets/admin/pages/AppointmentsPage.tsx b/assets/admin/pages/AppointmentsPage.tsx
index b21ddb11..9f5b224b 100644
--- a/assets/admin/pages/AppointmentsPage.tsx
+++ b/assets/admin/pages/AppointmentsPage.tsx
@@ -9,7 +9,7 @@ import { toast } from 'sonner';
import { api } from '../lib/api';
import type { PaginatedResponse, ApiResponse } from '../lib/api';
import type { Appointment } from '../types';
-import { formatDate, toGregorianDate, formatTime } from '../lib/utils';
+import { formatDate, toGregorianDate, formatTime, toEnglishDigits, sanitizeMobileInput } from '../lib/utils';
import { useAuthStore } from '../stores/authStore';
import Pagination from '../components/ui/Pagination';
import AppointmentFiltersModal, { applyAppointmentFilters, EMPTY_FILTERS } from '../components/AppointmentFiltersModal';
@@ -173,12 +173,7 @@ export function NewAppointmentModal({
// تغییر موبایل نتیجهی جستجوی قبلی را باطل میکند تا کاربر دوباره جستجو کند.
function onMobileChange(v: string) {
// ارقام فارسی/عربی → انگلیسی، فقط رقم، حداکثر ۱۱ رقم (کیبورد فارسی هم پذیرفته میشود).
- const normalized = v
- .replace(/[۰-۹]/g, d => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d)))
- .replace(/[٠-٩]/g, d => String('٠١٢٣٤٥٦٧٨٩'.indexOf(d)))
- .replace(/\D/g, '')
- .slice(0, 11);
- setMobile(normalized);
+ setMobile(sanitizeMobileInput(v));
if (lookup !== null) { setLookup(null); setPatientName(''); setNationalCode(''); }
}
@@ -265,9 +260,10 @@ export function NewAppointmentModal({
setNationalCode(e.target.value.replace(/\D/g, '').slice(0, 10))}
+ onChange={e => setNationalCode(toEnglishDigits(e.target.value).replace(/\D/g, '').slice(0, 10))}
placeholder="کد ملی ۱۰ رقمی"
style={{ ...inputSx, direction: 'ltr' }}
/>