feat: implement MobileInput component for standardized mobile number input and update related forms to use it
This commit is contained in:
@@ -6,9 +6,11 @@ import { XMarkIcon } from '@heroicons/react/24/outline';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '../../lib/api';
|
import { api } from '../../lib/api';
|
||||||
import type { ApiResponse } from '../../lib/api';
|
import type { ApiResponse } from '../../lib/api';
|
||||||
|
import MobileInput from './MobileInput';
|
||||||
|
import { iranMobileSchema } from '../../lib/utils';
|
||||||
|
|
||||||
const inviteSchema = z.object({
|
const inviteSchema = z.object({
|
||||||
mobile: z.string().regex(/^09\d{9}$/, 'شماره موبایل ۱۱ رقمی با 09 شروع میشود'),
|
mobile: iranMobileSchema,
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
specialty: z.string().optional(),
|
specialty: z.string().optional(),
|
||||||
});
|
});
|
||||||
@@ -47,7 +49,7 @@ export default function InviteDoctorModal({ clinicUuid, onClose, onInvited }: Pr
|
|||||||
<div className="modal-body" style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
<div className="modal-body" style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ fontSize: 13, fontWeight: 600, display: 'block', marginBottom: 6 }}>شماره موبایل پزشک *</label>
|
<label style={{ fontSize: 13, fontWeight: 600, display: 'block', marginBottom: 6 }}>شماره موبایل پزشک *</label>
|
||||||
<input className="input" dir="ltr" placeholder="09xxxxxxxxx" {...register('mobile')} />
|
<MobileInput className="input" hasError={!!errors.mobile} {...register('mobile')} />
|
||||||
{errors.mobile && <div className="err-text">{errors.mobile.message}</div>}
|
{errors.mobile && <div className="err-text">{errors.mobile.message}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { sanitizeMobileInput } from '../../lib/utils';
|
||||||
|
|
||||||
|
type Props = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||||
|
hasError?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* فیلد شماره موبایل ایران: ارقام فارسی/عربی را به انگلیسی تبدیل میکند، فقط رقم میپذیرد و حداکثر ۱۱ رقم.
|
||||||
|
* سازگار با react-hook-form `register` (event-based onChange) و حالت controlled.
|
||||||
|
* قبل از فراخوانی onChange، مقدار DOM پاکسازی میشود تا value در state هم تمیز ذخیره شود.
|
||||||
|
*/
|
||||||
|
export default React.forwardRef<HTMLInputElement, Props>(function MobileInput(
|
||||||
|
{ hasError, className, placeholder, style, onChange, ...rest },
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
const handle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
e.target.value = sanitizeMobileInput(e.target.value);
|
||||||
|
onChange?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
{...rest}
|
||||||
|
ref={ref}
|
||||||
|
type="tel"
|
||||||
|
inputMode="numeric"
|
||||||
|
dir="ltr"
|
||||||
|
maxLength={11}
|
||||||
|
placeholder={placeholder ?? '09xxxxxxxxx'}
|
||||||
|
className={className ?? 'cp-input'}
|
||||||
|
style={hasError ? { borderColor: 'var(--danger)', ...(style || {}) } : style}
|
||||||
|
onChange={handle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { DevicePhoneMobileIcon, CheckCircleIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
import { DevicePhoneMobileIcon, CheckCircleIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||||
import { api } from '../../lib/api';
|
import { api } from '../../lib/api';
|
||||||
import type { ApiResponse } from '../../lib/api';
|
import type { ApiResponse } from '../../lib/api';
|
||||||
|
import { sanitizeMobileInput } from '../../lib/utils';
|
||||||
|
|
||||||
interface NotificationMobileData {
|
interface NotificationMobileData {
|
||||||
notification_mobile: string | null;
|
notification_mobile: string | null;
|
||||||
@@ -126,9 +127,11 @@ export default function NotificationMobileCard({ target }: Props) {
|
|||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={newMobile}
|
value={newMobile}
|
||||||
onChange={e => setNewMobile(e.target.value)}
|
|
||||||
placeholder="09XXXXXXXXX"
|
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
|
inputMode="numeric"
|
||||||
|
maxLength={11}
|
||||||
|
onChange={e => setNewMobile(sanitizeMobileInput(e.target.value))}
|
||||||
|
placeholder="09XXXXXXXXX"
|
||||||
style={{
|
style={{
|
||||||
width: '100%', maxWidth: 220, height: 38, padding: '0 12px',
|
width: '100%', maxWidth: 220, height: 38, padding: '0 12px',
|
||||||
borderRadius: 'var(--r-sm)', border: '1px solid var(--border)',
|
borderRadius: 'var(--r-sm)', border: '1px solid var(--border)',
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
export function formatRial(amount: number): string {
|
export function formatRial(amount: number): string {
|
||||||
return new Intl.NumberFormat('fa-IR').format(amount) + ' تومان';
|
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 {
|
export function cn(...classes: (string | undefined | null | false)[]): string {
|
||||||
return classes.filter(Boolean).join(' ');
|
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), 'شماره موبایل نامعتبر است');
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import { ArrowRightIcon, BuildingOffice2Icon } from '@heroicons/react/24/outline
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { ApiResponse } from '../lib/api';
|
import type { ApiResponse } from '../lib/api';
|
||||||
|
import MobileInput from '../components/ui/MobileInput';
|
||||||
|
import { iranMobileSchema } from '../lib/utils';
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
owner_mobile: z.string().min(10, 'شماره موبایل معتبر نیست').max(15),
|
owner_mobile: iranMobileSchema,
|
||||||
name: z.string().min(2, 'نام کلینیک حداقل ۲ کاراکتر'),
|
name: z.string().min(2, 'نام کلینیک حداقل ۲ کاراکتر'),
|
||||||
telephone: z.string().max(20).optional().or(z.literal('')),
|
telephone: z.string().max(20).optional().or(z.literal('')),
|
||||||
address: z.string().max(500).optional().or(z.literal('')),
|
address: z.string().max(500).optional().or(z.literal('')),
|
||||||
@@ -49,7 +51,7 @@ export default function ClinicFormPage() {
|
|||||||
<form onSubmit={handleSubmit((v) => mutation.mutate(v))} style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
<form onSubmit={handleSubmit((v) => mutation.mutate(v))} style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
<label style={{ fontSize: 13, fontWeight: 600 }}>شماره موبایل صاحب کلینیک <span style={{ color: 'var(--red)' }}>*</span></label>
|
<label style={{ fontSize: 13, fontWeight: 600 }}>شماره موبایل صاحب کلینیک <span style={{ color: 'var(--red)' }}>*</span></label>
|
||||||
<input className="field" placeholder="09xxxxxxxxx" dir="ltr" {...register('owner_mobile')} />
|
<MobileInput className="field" hasError={!!errors.owner_mobile} {...register('owner_mobile')} />
|
||||||
{errors.owner_mobile && <span style={{ color: 'var(--red)', fontSize: 12 }}>{errors.owner_mobile.message}</span>}
|
{errors.owner_mobile && <span style={{ color: 'var(--red)', fontSize: 12 }}>{errors.owner_mobile.message}</span>}
|
||||||
<span style={{ fontSize: 11, color: 'var(--text-3)' }}>اگر این موبایل در سیستم نباشد، کاربر جدید ساخته میشود</span>
|
<span style={{ fontSize: 11, color: 'var(--text-3)' }}>اگر این موبایل در سیستم نباشد، کاربر جدید ساخته میشود</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,14 +16,15 @@ import { api } from '../lib/api';
|
|||||||
import type { ApiResponse, PaginatedResponse } from '../lib/api';
|
import type { ApiResponse, PaginatedResponse } from '../lib/api';
|
||||||
import type { Clinic } from '../types';
|
import type { Clinic } from '../types';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
import { formatDate, formatNumber } from '../lib/utils';
|
import MobileInput from '../components/ui/MobileInput';
|
||||||
|
import { formatDate, formatNumber, iranMobileSchema } from '../lib/utils';
|
||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||||
|
|
||||||
const HUES_LIST = [256, 205, 162, 295, 272];
|
const HUES_LIST = [256, 205, 162, 295, 272];
|
||||||
|
|
||||||
const addSchema = z.object({
|
const addSchema = z.object({
|
||||||
owner_mobile: z.string().min(10, 'شماره موبایل معتبر نیست'),
|
owner_mobile: iranMobileSchema,
|
||||||
name: z.string().min(2, 'نام الزامی است'),
|
name: z.string().min(2, 'نام الزامی است'),
|
||||||
telephone: z.string().optional(),
|
telephone: z.string().optional(),
|
||||||
});
|
});
|
||||||
@@ -254,7 +255,7 @@ export default function ClinicsPage() {
|
|||||||
<label style={{ fontSize: 13, fontWeight: 600, marginBottom: 6, display: 'block' }}>
|
<label style={{ fontSize: 13, fontWeight: 600, marginBottom: 6, display: 'block' }}>
|
||||||
شماره موبایل صاحب کلینیک <span style={{ color: 'var(--red)' }}>*</span>
|
شماره موبایل صاحب کلینیک <span style={{ color: 'var(--red)' }}>*</span>
|
||||||
</label>
|
</label>
|
||||||
<input className="input" placeholder="09xxxxxxxxx" dir="ltr" {...addForm.register('owner_mobile')} />
|
<MobileInput className="input" hasError={!!addForm.formState.errors.owner_mobile} {...addForm.register('owner_mobile')} />
|
||||||
{addForm.formState.errors.owner_mobile && (
|
{addForm.formState.errors.owner_mobile && (
|
||||||
<div className="err-text">{addForm.formState.errors.owner_mobile.message}</div>
|
<div className="err-text">{addForm.formState.errors.owner_mobile.message}</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import 'leaflet/dist/leaflet.css';
|
|||||||
import { api, ApiError } from '../lib/api';
|
import { api, ApiError } from '../lib/api';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
import type { ApiResponse } from '../lib/api';
|
import type { ApiResponse } from '../lib/api';
|
||||||
import { formatNumber } from '../lib/utils';
|
import { formatNumber, iranMobileOptionalSchema } from '../lib/utils';
|
||||||
|
import MobileInput from '../components/ui/MobileInput';
|
||||||
import Modal from '../components/ui/Modal';
|
import Modal from '../components/ui/Modal';
|
||||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||||
import NotificationMobileCard from '../components/ui/NotificationMobileCard';
|
import NotificationMobileCard from '../components/ui/NotificationMobileCard';
|
||||||
@@ -2105,7 +2106,7 @@ const editSchema = z.object({
|
|||||||
gender: z.enum(['man', 'woman']).optional().or(z.literal('')),
|
gender: z.enum(['man', 'woman']).optional().or(z.literal('')),
|
||||||
degree: z.string().optional().or(z.literal('')),
|
degree: z.string().optional().or(z.literal('')),
|
||||||
medical_system_code: z.string().max(30).optional().or(z.literal('')),
|
medical_system_code: z.string().max(30).optional().or(z.literal('')),
|
||||||
mobile_number: z.string().max(15).optional().or(z.literal('')),
|
mobile_number: iranMobileOptionalSchema.optional(),
|
||||||
info: z.string().max(2000).optional().or(z.literal('')),
|
info: z.string().max(2000).optional().or(z.literal('')),
|
||||||
specialties: z.array(z.number()).optional(),
|
specialties: z.array(z.number()).optional(),
|
||||||
services: z.array(z.number()).optional(),
|
services: z.array(z.number()).optional(),
|
||||||
@@ -2715,7 +2716,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
<input type="text" className="cp-input" placeholder="دکتر ..." {...register('name')} />
|
<input type="text" className="cp-input" placeholder="دکتر ..." {...register('name')} />
|
||||||
</EditField>
|
</EditField>
|
||||||
<EditField label="شماره موبایل مطب">
|
<EditField label="شماره موبایل مطب">
|
||||||
<input type="text" dir="ltr" className="cp-input" placeholder="09xxxxxxxxx" {...register('mobile_number')} />
|
<MobileInput className="cp-input" {...register('mobile_number')} />
|
||||||
</EditField>
|
</EditField>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { toast } from 'sonner';
|
|||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { ApiResponse } from '../lib/api';
|
import type { ApiResponse } from '../lib/api';
|
||||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||||
|
import MobileInput from '../components/ui/MobileInput';
|
||||||
|
import { iranMobileSchema } from '../lib/utils';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
|
|
||||||
// ── Types ────────────────────────────────────────────────────────────────────
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
@@ -22,7 +24,7 @@ interface SpecialtyOption { id: number; uuid: string; name: string; parent_id: n
|
|||||||
// ── Schema ───────────────────────────────────────────────────────────────────
|
// ── Schema ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
mobile: z.string().min(10, 'شماره موبایل معتبر نیست').max(15),
|
mobile: iranMobileSchema,
|
||||||
name: z.string().min(2, 'نام حداقل ۲ کاراکتر'),
|
name: z.string().min(2, 'نام حداقل ۲ کاراکتر'),
|
||||||
gender: z.enum(['man', 'woman']).optional().or(z.literal('')),
|
gender: z.enum(['man', 'woman']).optional().or(z.literal('')),
|
||||||
degree: z.string().optional().or(z.literal('')),
|
degree: z.string().optional().or(z.literal('')),
|
||||||
@@ -318,8 +320,7 @@ export default function DoctorFormPage() {
|
|||||||
<SectionHeader icon={<PhoneIcon style={{ width: 16, height: 16 }} />} title="اطلاعات حساب" />
|
<SectionHeader icon={<PhoneIcon style={{ width: 16, height: 16 }} />} title="اطلاعات حساب" />
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
||||||
<Field label="شماره موبایل" required error={errors.mobile?.message} hint="اگر کاربر با این شماره وجود دارد پروفایل به او متصل میشود">
|
<Field label="شماره موبایل" required error={errors.mobile?.message} hint="اگر کاربر با این شماره وجود دارد پروفایل به او متصل میشود">
|
||||||
<input type="tel" dir="ltr" className="cp-input" placeholder="09xxxxxxxxx" {...register('mobile')}
|
<MobileInput hasError={!!errors.mobile} {...register('mobile')} />
|
||||||
style={errors.mobile ? { borderColor: 'var(--danger)' } : {}} />
|
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="نام کامل" required error={errors.name?.message}>
|
<Field label="نام کامل" required error={errors.name?.message}>
|
||||||
<input type="text" className="cp-input" placeholder="دکتر ..." {...register('name')}
|
<input type="text" className="cp-input" placeholder="دکتر ..." {...register('name')}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { toast } from 'sonner';
|
|||||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
import PwaLoginCard from '../components/ui/PwaLoginCard';
|
import PwaLoginCard from '../components/ui/PwaLoginCard';
|
||||||
|
import { sanitizeMobileInput } from '../lib/utils';
|
||||||
|
|
||||||
type Mode = 'password' | 'sms' | 'forgot';
|
type Mode = 'password' | 'sms' | 'forgot';
|
||||||
type SmsStep = 1 | 2;
|
type SmsStep = 1 | 2;
|
||||||
@@ -220,7 +221,7 @@ export default function LoginPage() {
|
|||||||
<label>شماره موبایل</label>
|
<label>شماره موبایل</label>
|
||||||
<input className="input" type="tel" dir="ltr" placeholder="09xxxxxxxxx"
|
<input className="input" type="tel" dir="ltr" placeholder="09xxxxxxxxx"
|
||||||
autoComplete="username" style={{ textAlign: 'right' }}
|
autoComplete="username" style={{ textAlign: 'right' }}
|
||||||
value={pwMobile} onChange={(e) => setPwMobile(e.target.value)} />
|
value={pwMobile} onChange={(e) => setPwMobile(sanitizeMobileInput(e.target.value))} />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label>رمز عبور</label>
|
<label>رمز عبور</label>
|
||||||
@@ -257,7 +258,7 @@ export default function LoginPage() {
|
|||||||
<label>شماره موبایل</label>
|
<label>شماره موبایل</label>
|
||||||
<input className="input" type="tel" dir="ltr" placeholder="09xxxxxxxxx"
|
<input className="input" type="tel" dir="ltr" placeholder="09xxxxxxxxx"
|
||||||
style={{ textAlign: 'right' }}
|
style={{ textAlign: 'right' }}
|
||||||
value={smsMobile} onChange={(e) => setSmsMobile(e.target.value)}
|
value={smsMobile} onChange={(e) => setSmsMobile(sanitizeMobileInput(e.target.value))}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleSmsSend()} />
|
onKeyDown={(e) => e.key === 'Enter' && handleSmsSend()} />
|
||||||
</div>
|
</div>
|
||||||
<button className="btn primary block" disabled={smsLoading} onClick={handleSmsSend}
|
<button className="btn primary block" disabled={smsLoading} onClick={handleSmsSend}
|
||||||
@@ -305,7 +306,7 @@ export default function LoginPage() {
|
|||||||
<label>شماره موبایل</label>
|
<label>شماره موبایل</label>
|
||||||
<input className="input" type="tel" dir="ltr" placeholder="09xxxxxxxxx"
|
<input className="input" type="tel" dir="ltr" placeholder="09xxxxxxxxx"
|
||||||
style={{ textAlign: 'right' }}
|
style={{ textAlign: 'right' }}
|
||||||
value={forgotMobile} onChange={(e) => setForgotMobile(e.target.value)}
|
value={forgotMobile} onChange={(e) => setForgotMobile(sanitizeMobileInput(e.target.value))}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleForgotSend()} />
|
onKeyDown={(e) => e.key === 'Enter' && handleForgotSend()} />
|
||||||
</div>
|
</div>
|
||||||
<button className="btn primary block" disabled={forgotLoading} onClick={handleForgotSend}
|
<button className="btn primary block" disabled={forgotLoading} onClick={handleForgotSend}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import { ActiveBadge } from "../components/ui/StatusBadge";
|
|||||||
import { useSubscription } from "../hooks/useSubscription";
|
import { useSubscription } from "../hooks/useSubscription";
|
||||||
import type { ApiResponse } from "../lib/api";
|
import type { ApiResponse } from "../lib/api";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { formatDate, maskMobile } from "../lib/utils";
|
import { formatDate, maskMobile, iranMobileSchema } from "../lib/utils";
|
||||||
|
import MobileInput from "../components/ui/MobileInput";
|
||||||
import { useAuthStore } from "../stores/authStore";
|
import { useAuthStore } from "../stores/authStore";
|
||||||
import type { Secretary, SecretaryPermissions } from "../types";
|
import type { Secretary, SecretaryPermissions } from "../types";
|
||||||
|
|
||||||
@@ -244,7 +245,7 @@ function PermissionsMatrix({
|
|||||||
|
|
||||||
const createSchema = z.object({
|
const createSchema = z.object({
|
||||||
name: z.string().min(2, "نام الزامی است"),
|
name: z.string().min(2, "نام الزامی است"),
|
||||||
mobile_number: z.string().regex(/^09[0-9]{9}$/, "شماره موبایل معتبر نیست"),
|
mobile_number: iranMobileSchema,
|
||||||
});
|
});
|
||||||
type CreateForm = z.infer<typeof createSchema>;
|
type CreateForm = z.infer<typeof createSchema>;
|
||||||
|
|
||||||
@@ -694,28 +695,10 @@ export default function MySecretariesPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="field" style={{ marginTop: 16 }}>
|
<div className="field" style={{ marginTop: 16 }}>
|
||||||
<label>شماره موبایل *</label>
|
<label>شماره موبایل *</label>
|
||||||
<input
|
<MobileInput
|
||||||
{...createForm.register("mobile_number")}
|
{...createForm.register("mobile_number")}
|
||||||
placeholder="09123456789"
|
|
||||||
dir="ltr"
|
|
||||||
inputMode="numeric"
|
|
||||||
maxLength={11}
|
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onChange={(e) => {
|
hasError={!!createForm.formState.errors.mobile_number}
|
||||||
const digits = e.target.value.replace(
|
|
||||||
/\D/g,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
createForm.setValue("mobile_number", digits, {
|
|
||||||
shouldValidate:
|
|
||||||
createForm.formState.isSubmitted,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
style={
|
|
||||||
createForm.formState.errors.mobile_number
|
|
||||||
? { borderColor: "var(--danger)" }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
{createForm.formState.errors.mobile_number && (
|
{createForm.formState.errors.mobile_number && (
|
||||||
<span className="field-error">
|
<span className="field-error">
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { z } from 'zod';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { ApiResponse, PaginatedResponse } from '../lib/api';
|
import type { ApiResponse, PaginatedResponse } from '../lib/api';
|
||||||
|
import MobileInput from '../components/ui/MobileInput';
|
||||||
|
import { iranMobileSchema } from '../lib/utils';
|
||||||
import type { Representation, City } from '../types';
|
import type { Representation, City } from '../types';
|
||||||
import { formatDate, formatRial, formatNumber } from '../lib/utils';
|
import { formatDate, formatRial, formatNumber } from '../lib/utils';
|
||||||
import DataTable, { Column } from '../components/ui/DataTable';
|
import DataTable, { Column } from '../components/ui/DataTable';
|
||||||
@@ -19,7 +21,7 @@ import SearchableSelect from '../components/ui/SearchableSelect';
|
|||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
full_name: z.string().min(2, 'نام الزامی است'),
|
full_name: z.string().min(2, 'نام الزامی است'),
|
||||||
mobile_number: z.string().min(10, 'شماره موبایل معتبر نیست'),
|
mobile_number: iranMobileSchema,
|
||||||
city_id: z.number().nullable().optional(),
|
city_id: z.number().nullable().optional(),
|
||||||
commission_percent: z.coerce.number().min(0).max(100),
|
commission_percent: z.coerce.number().min(0).max(100),
|
||||||
});
|
});
|
||||||
@@ -177,7 +179,7 @@ export default function RepresentationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="form-row" style={{ marginTop: 12 }}>
|
<div className="form-row" style={{ marginTop: 12 }}>
|
||||||
<label>شماره موبایل</label>
|
<label>شماره موبایل</label>
|
||||||
<input {...register('mobile_number')} dir="ltr" placeholder="09xxxxxxxxx" className="input" />
|
<MobileInput className="input" hasError={!!errors.mobile_number} {...register('mobile_number')} />
|
||||||
{errors.mobile_number && <p className="err-text">{errors.mobile_number.message}</p>}
|
{errors.mobile_number && <p className="err-text">{errors.mobile_number.message}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row" style={{ marginTop: 12 }}>
|
<div className="form-row" style={{ marginTop: 12 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user