feat: implement MobileInput component for standardized mobile number input and update related forms to use it
This commit is contained in:
@@ -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() {
|
||||
<form onSubmit={handleSubmit((v) => mutation.mutate(v))} style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<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>}
|
||||
<span style={{ fontSize: 11, color: 'var(--text-3)' }}>اگر این موبایل در سیستم نباشد، کاربر جدید ساخته میشود</span>
|
||||
</div>
|
||||
|
||||
@@ -16,14 +16,15 @@ import { api } from '../lib/api';
|
||||
import type { ApiResponse, PaginatedResponse } from '../lib/api';
|
||||
import type { Clinic } from '../types';
|
||||
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 ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
|
||||
const HUES_LIST = [256, 205, 162, 295, 272];
|
||||
|
||||
const addSchema = z.object({
|
||||
owner_mobile: z.string().min(10, 'شماره موبایل معتبر نیست'),
|
||||
owner_mobile: iranMobileSchema,
|
||||
name: z.string().min(2, 'نام الزامی است'),
|
||||
telephone: z.string().optional(),
|
||||
});
|
||||
@@ -254,7 +255,7 @@ export default function ClinicsPage() {
|
||||
<label style={{ fontSize: 13, fontWeight: 600, marginBottom: 6, display: 'block' }}>
|
||||
شماره موبایل صاحب کلینیک <span style={{ color: 'var(--red)' }}>*</span>
|
||||
</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 && (
|
||||
<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 { useAuthStore } from '../stores/authStore';
|
||||
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 ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import NotificationMobileCard from '../components/ui/NotificationMobileCard';
|
||||
@@ -2105,7 +2106,7 @@ const editSchema = z.object({
|
||||
gender: z.enum(['man', 'woman']).optional().or(z.literal('')),
|
||||
degree: z.string().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('')),
|
||||
specialties: 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')} />
|
||||
</EditField>
|
||||
<EditField label="شماره موبایل مطب">
|
||||
<input type="text" dir="ltr" className="cp-input" placeholder="09xxxxxxxxx" {...register('mobile_number')} />
|
||||
<MobileInput className="cp-input" {...register('mobile_number')} />
|
||||
</EditField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<SectionHeader icon={<PhoneIcon style={{ width: 16, height: 16 }} />} title="اطلاعات حساب" />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
||||
<Field label="شماره موبایل" required error={errors.mobile?.message} hint="اگر کاربر با این شماره وجود دارد پروفایل به او متصل میشود">
|
||||
<input type="tel" dir="ltr" className="cp-input" placeholder="09xxxxxxxxx" {...register('mobile')}
|
||||
style={errors.mobile ? { borderColor: 'var(--danger)' } : {}} />
|
||||
<MobileInput hasError={!!errors.mobile} {...register('mobile')} />
|
||||
</Field>
|
||||
<Field label="نام کامل" required error={errors.name?.message}>
|
||||
<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 { useAuthStore } from '../stores/authStore';
|
||||
import PwaLoginCard from '../components/ui/PwaLoginCard';
|
||||
import { sanitizeMobileInput } from '../lib/utils';
|
||||
|
||||
type Mode = 'password' | 'sms' | 'forgot';
|
||||
type SmsStep = 1 | 2;
|
||||
@@ -220,7 +221,7 @@ export default function LoginPage() {
|
||||
<label>شماره موبایل</label>
|
||||
<input className="input" type="tel" dir="ltr" placeholder="09xxxxxxxxx"
|
||||
autoComplete="username" style={{ textAlign: 'right' }}
|
||||
value={pwMobile} onChange={(e) => setPwMobile(e.target.value)} />
|
||||
value={pwMobile} onChange={(e) => setPwMobile(sanitizeMobileInput(e.target.value))} />
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label>رمز عبور</label>
|
||||
@@ -257,7 +258,7 @@ export default function LoginPage() {
|
||||
<label>شماره موبایل</label>
|
||||
<input className="input" type="tel" dir="ltr" placeholder="09xxxxxxxxx"
|
||||
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()} />
|
||||
</div>
|
||||
<button className="btn primary block" disabled={smsLoading} onClick={handleSmsSend}
|
||||
@@ -305,7 +306,7 @@ export default function LoginPage() {
|
||||
<label>شماره موبایل</label>
|
||||
<input className="input" type="tel" dir="ltr" placeholder="09xxxxxxxxx"
|
||||
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()} />
|
||||
</div>
|
||||
<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 type { ApiResponse } 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 type { Secretary, SecretaryPermissions } from "../types";
|
||||
|
||||
@@ -244,7 +245,7 @@ function PermissionsMatrix({
|
||||
|
||||
const createSchema = z.object({
|
||||
name: z.string().min(2, "نام الزامی است"),
|
||||
mobile_number: z.string().regex(/^09[0-9]{9}$/, "شماره موبایل معتبر نیست"),
|
||||
mobile_number: iranMobileSchema,
|
||||
});
|
||||
type CreateForm = z.infer<typeof createSchema>;
|
||||
|
||||
@@ -694,28 +695,10 @@ export default function MySecretariesPage() {
|
||||
</div>
|
||||
<div className="field" style={{ marginTop: 16 }}>
|
||||
<label>شماره موبایل *</label>
|
||||
<input
|
||||
<MobileInput
|
||||
{...createForm.register("mobile_number")}
|
||||
placeholder="09123456789"
|
||||
dir="ltr"
|
||||
inputMode="numeric"
|
||||
maxLength={11}
|
||||
autoComplete="off"
|
||||
onChange={(e) => {
|
||||
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
|
||||
}
|
||||
hasError={!!createForm.formState.errors.mobile_number}
|
||||
/>
|
||||
{createForm.formState.errors.mobile_number && (
|
||||
<span className="field-error">
|
||||
|
||||
@@ -8,6 +8,8 @@ import { z } from 'zod';
|
||||
import { toast } from 'sonner';
|
||||
import { api } 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 { formatDate, formatRial, formatNumber } from '../lib/utils';
|
||||
import DataTable, { Column } from '../components/ui/DataTable';
|
||||
@@ -19,7 +21,7 @@ import SearchableSelect from '../components/ui/SearchableSelect';
|
||||
|
||||
const schema = z.object({
|
||||
full_name: z.string().min(2, 'نام الزامی است'),
|
||||
mobile_number: z.string().min(10, 'شماره موبایل معتبر نیست'),
|
||||
mobile_number: iranMobileSchema,
|
||||
city_id: z.number().nullable().optional(),
|
||||
commission_percent: z.coerce.number().min(0).max(100),
|
||||
});
|
||||
@@ -177,7 +179,7 @@ export default function RepresentationsPage() {
|
||||
</div>
|
||||
<div className="form-row" style={{ marginTop: 12 }}>
|
||||
<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>}
|
||||
</div>
|
||||
<div className="form-row" style={{ marginTop: 12 }}>
|
||||
|
||||
Reference in New Issue
Block a user