From da57c554c12b51f60213a1e303b7037b93826f82 Mon Sep 17 00:00:00 2001
From: hamed <15238-genius.ha@users.noreply.drupalcode.org>
Date: Fri, 19 Jun 2026 15:28:40 +0330
Subject: [PATCH] feat: implement MobileInput component for standardized mobile
number input and update related forms to use it
---
.../admin/components/ui/InviteDoctorModal.tsx | 6 ++--
assets/admin/components/ui/MobileInput.tsx | 36 +++++++++++++++++++
.../components/ui/NotificationMobileCard.tsx | 7 ++--
assets/admin/lib/utils.ts | 34 ++++++++++++++++++
assets/admin/pages/ClinicFormPage.tsx | 6 ++--
assets/admin/pages/ClinicsPage.tsx | 7 ++--
assets/admin/pages/DoctorDetailPage.tsx | 7 ++--
assets/admin/pages/DoctorFormPage.tsx | 7 ++--
assets/admin/pages/LoginPage.tsx | 7 ++--
assets/admin/pages/MySecretariesPage.tsx | 27 +++-----------
assets/admin/pages/RepresentationsPage.tsx | 6 ++--
11 files changed, 108 insertions(+), 42 deletions(-)
create mode 100644 assets/admin/components/ui/MobileInput.tsx
diff --git a/assets/admin/components/ui/InviteDoctorModal.tsx b/assets/admin/components/ui/InviteDoctorModal.tsx
index 6b92aa13..97e87feb 100644
--- a/assets/admin/components/ui/InviteDoctorModal.tsx
+++ b/assets/admin/components/ui/InviteDoctorModal.tsx
@@ -6,9 +6,11 @@ import { XMarkIcon } from '@heroicons/react/24/outline';
import { toast } from 'sonner';
import { api } from '../../lib/api';
import type { ApiResponse } from '../../lib/api';
+import MobileInput from './MobileInput';
+import { iranMobileSchema } from '../../lib/utils';
const inviteSchema = z.object({
- mobile: z.string().regex(/^09\d{9}$/, 'شماره موبایل ۱۱ رقمی با 09 شروع میشود'),
+ mobile: iranMobileSchema,
name: z.string().optional(),
specialty: z.string().optional(),
});
@@ -47,7 +49,7 @@ export default function InviteDoctorModal({ clinicUuid, onClose, onInvited }: Pr
-
+
{errors.mobile &&
{errors.mobile.message}
}
diff --git a/assets/admin/components/ui/MobileInput.tsx b/assets/admin/components/ui/MobileInput.tsx
new file mode 100644
index 00000000..39e3a48d
--- /dev/null
+++ b/assets/admin/components/ui/MobileInput.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { sanitizeMobileInput } from '../../lib/utils';
+
+type Props = React.InputHTMLAttributes
& {
+ hasError?: boolean;
+};
+
+/**
+ * فیلد شماره موبایل ایران: ارقام فارسی/عربی را به انگلیسی تبدیل میکند، فقط رقم میپذیرد و حداکثر ۱۱ رقم.
+ * سازگار با react-hook-form `register` (event-based onChange) و حالت controlled.
+ * قبل از فراخوانی onChange، مقدار DOM پاکسازی میشود تا value در state هم تمیز ذخیره شود.
+ */
+export default React.forwardRef(function MobileInput(
+ { hasError, className, placeholder, style, onChange, ...rest },
+ ref,
+) {
+ const handle = (e: React.ChangeEvent) => {
+ e.target.value = sanitizeMobileInput(e.target.value);
+ onChange?.(e);
+ };
+
+ return (
+
+ );
+});
diff --git a/assets/admin/components/ui/NotificationMobileCard.tsx b/assets/admin/components/ui/NotificationMobileCard.tsx
index 1fb19ad7..b7e9e94a 100644
--- a/assets/admin/components/ui/NotificationMobileCard.tsx
+++ b/assets/admin/components/ui/NotificationMobileCard.tsx
@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { DevicePhoneMobileIcon, CheckCircleIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { api } from '../../lib/api';
import type { ApiResponse } from '../../lib/api';
+import { sanitizeMobileInput } from '../../lib/utils';
interface NotificationMobileData {
notification_mobile: string | null;
@@ -126,9 +127,11 @@ export default function NotificationMobileCard({ target }: Props) {
setNewMobile(e.target.value)}
- placeholder="09XXXXXXXXX"
dir="ltr"
+ inputMode="numeric"
+ maxLength={11}
+ onChange={e => setNewMobile(sanitizeMobileInput(e.target.value))}
+ placeholder="09XXXXXXXXX"
style={{
width: '100%', maxWidth: 220, height: 38, padding: '0 12px',
borderRadius: 'var(--r-sm)', border: '1px solid var(--border)',
diff --git a/assets/admin/lib/utils.ts b/assets/admin/lib/utils.ts
index e683b11a..ee23e552 100644
--- a/assets/admin/lib/utils.ts
+++ b/assets/admin/lib/utils.ts
@@ -1,3 +1,5 @@
+import { z } from 'zod';
+
export function formatRial(amount: number): string {
return new Intl.NumberFormat('fa-IR').format(amount) + ' تومان';
}
@@ -46,3 +48,35 @@ export function maskMobile(mobile: string): string {
export function cn(...classes: (string | undefined | null | false)[]): string {
return classes.filter(Boolean).join(' ');
}
+
+// تبدیل ارقام فارسی/عربی به انگلیسی + حذف هر کاراکتر غیرعددی.
+export function toEnglishDigits(input: string): string {
+ if (!input) return '';
+ return input
+ .replace(/[۰-۹]/g, (d) => String(d.charCodeAt(0) - 0x06f0))
+ .replace(/[٠-٩]/g, (d) => String(d.charCodeAt(0) - 0x0660));
+}
+
+// فقط ارقام انگلیسی، حداکثر ۱۱ رقم (برای فیلد موبایل).
+export function sanitizeMobileInput(input: string): string {
+ return toEnglishDigits(input).replace(/\D/g, '').slice(0, 11);
+}
+
+// regex شماره موبایل ایران
+export const IRAN_MOBILE_RE = /^09\d{9}$/;
+
+export function isValidIranMobile(input: string): boolean {
+ return IRAN_MOBILE_RE.test(toEnglishDigits(input || ''));
+}
+
+// schema قابلاستفادهی مشترک برای شماره موبایل ایران (ارقام فارسی/عربی را هم میپذیرد و نرمال میکند).
+export const iranMobileSchema = z
+ .string()
+ .transform((v) => toEnglishDigits(v).replace(/\D/g, ''))
+ .refine((v) => IRAN_MOBILE_RE.test(v), 'شماره موبایل باید ۱۱ رقم و با 09 شروع شود');
+
+// نسخهی اختیاری (خالی یا معتبر) برای فیلدهای غیرالزامی.
+export const iranMobileOptionalSchema = z
+ .string()
+ .transform((v) => toEnglishDigits(v).replace(/\D/g, ''))
+ .refine((v) => v === '' || IRAN_MOBILE_RE.test(v), 'شماره موبایل نامعتبر است');
diff --git a/assets/admin/pages/ClinicFormPage.tsx b/assets/admin/pages/ClinicFormPage.tsx
index a2db580d..baf41f64 100644
--- a/assets/admin/pages/ClinicFormPage.tsx
+++ b/assets/admin/pages/ClinicFormPage.tsx
@@ -8,9 +8,11 @@ import { ArrowRightIcon, BuildingOffice2Icon } from '@heroicons/react/24/outline
import { toast } from 'sonner';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
+import MobileInput from '../components/ui/MobileInput';
+import { iranMobileSchema } from '../lib/utils';
const schema = z.object({
- owner_mobile: z.string().min(10, 'شماره موبایل معتبر نیست').max(15),
+ owner_mobile: iranMobileSchema,
name: z.string().min(2, 'نام کلینیک حداقل ۲ کاراکتر'),
telephone: z.string().max(20).optional().or(z.literal('')),
address: z.string().max(500).optional().or(z.literal('')),
@@ -49,7 +51,7 @@ export default function ClinicFormPage() {
diff --git a/assets/admin/pages/DoctorFormPage.tsx b/assets/admin/pages/DoctorFormPage.tsx
index ab605c7c..990110ec 100644
--- a/assets/admin/pages/DoctorFormPage.tsx
+++ b/assets/admin/pages/DoctorFormPage.tsx
@@ -13,6 +13,8 @@ import { toast } from 'sonner';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import SearchableSelect from '../components/ui/SearchableSelect';
+import MobileInput from '../components/ui/MobileInput';
+import { iranMobileSchema } from '../lib/utils';
import { useAuthStore } from '../stores/authStore';
// ── Types ────────────────────────────────────────────────────────────────────
@@ -22,7 +24,7 @@ interface SpecialtyOption { id: number; uuid: string; name: string; parent_id: n
// ── Schema ───────────────────────────────────────────────────────────────────
const schema = z.object({
- mobile: z.string().min(10, 'شماره موبایل معتبر نیست').max(15),
+ mobile: iranMobileSchema,
name: z.string().min(2, 'نام حداقل ۲ کاراکتر'),
gender: z.enum(['man', 'woman']).optional().or(z.literal('')),
degree: z.string().optional().or(z.literal('')),
@@ -318,8 +320,7 @@ export default function DoctorFormPage() {
} title="اطلاعات حساب" />
-
+
شماره موبایل
setPwMobile(e.target.value)} />
+ value={pwMobile} onChange={(e) => setPwMobile(sanitizeMobileInput(e.target.value))} />
@@ -257,7 +258,7 @@ export default function LoginPage() {
setSmsMobile(e.target.value)}
+ value={smsMobile} onChange={(e) => setSmsMobile(sanitizeMobileInput(e.target.value))}
onKeyDown={(e) => e.key === 'Enter' && handleSmsSend()} />