feat: add mobile number change functionality for doctors and clinics
- Implemented PATCH endpoints for changing the login mobile number of doctors and clinics. - Added ChangeLoginMobileModal component for handling mobile number updates in the UI. - Updated ClinicsPage and DoctorsPage to include buttons for changing mobile numbers. - Enhanced AdminApiController to manage mobile number changes with validation. - Created tests to ensure proper functionality and validation for mobile number changes. - Updated API documentation to reflect new endpoints and their usage.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '../lib/api';
|
||||
import { digitsOnly } from '../lib/utils';
|
||||
import Modal from './ui/Modal';
|
||||
|
||||
interface Props {
|
||||
/** `null` یعنی مودال بسته است. */
|
||||
target: { uuid: string; name: string; mobile_number?: string | null } | null;
|
||||
/** `doctors` → /api/v1/admin/doctors/{uuid}/mobile · `clinic` → /api/v1/admin/clinic/{uuid}/mobile */
|
||||
resource: 'doctors' | 'clinic';
|
||||
/** کلید کوئریِ لیستی که بعد از تغییر باید invalidate شود. */
|
||||
queryKey: unknown[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* تغییر شمارهٔ **ورود** پزشک/کلینیک توسط مدیر کل. شماره هویتِ ورود کاربر است، پس
|
||||
* سرور یکتا بودن را کنترل میکند و اینجا فقط قالبِ ۰۹ + ۱۱ رقم اعتبارسنجی میشود.
|
||||
*/
|
||||
export default function ChangeLoginMobileModal({ target, resource, queryKey, onClose }: Props) {
|
||||
const qc = useQueryClient();
|
||||
const [mobile, setMobile] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setMobile(target?.mobile_number ?? '');
|
||||
}, [target]);
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: () => api.patch(`/api/v1/admin/${resource}/${target!.uuid}/mobile`, { mobile_number: mobile }),
|
||||
onSuccess: () => {
|
||||
toast.success('شماره موبایل تغییر کرد');
|
||||
qc.invalidateQueries({ queryKey });
|
||||
onClose();
|
||||
},
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
const invalid = !/^09\d{9}$/.test(mobile);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={target !== null}
|
||||
title={`تغییر شماره ورود — ${target?.name ?? ''}`}
|
||||
size="sm"
|
||||
onClose={onClose}
|
||||
footer={
|
||||
<>
|
||||
<button onClick={onClose} className="btn ghost sm">لغو</button>
|
||||
<button
|
||||
onClick={() => save.mutate()}
|
||||
disabled={save.isPending || invalid}
|
||||
className="btn primary sm"
|
||||
>
|
||||
{save.isPending ? 'در حال ذخیره...' : 'ذخیره'}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p style={{ margin: '0 0 14px', fontSize: 12, lineHeight: 1.9, color: 'var(--text-2)' }}>
|
||||
این شماره، شمارهٔ <b>ورود به پنل</b> است. پس از تغییر، ورود فقط با شمارهٔ جدید ممکن
|
||||
خواهد بود و شماره باید در کل سامانه یکتا باشد.
|
||||
</p>
|
||||
|
||||
<div className="form-row">
|
||||
<label>شماره موبایل</label>
|
||||
<input
|
||||
className="input"
|
||||
dir="ltr"
|
||||
inputMode="numeric"
|
||||
aria-label="شماره موبایل"
|
||||
placeholder="09xxxxxxxxx"
|
||||
value={mobile}
|
||||
onChange={(e) => setMobile(digitsOnly(e.target.value, 11))}
|
||||
/>
|
||||
{mobile !== '' && invalid && <p className="err-text">شماره باید با ۰۹ شروع شود و ۱۱ رقم باشد</p>}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MagnifyingGlassIcon,
|
||||
PlusIcon,
|
||||
BuildingOffice2Icon,
|
||||
DevicePhoneMobileIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { toast } from 'sonner';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -21,6 +22,7 @@ import Portal from '../components/ui/Portal';
|
||||
import { formatDate, formatNumber, iranMobileSchema } from '../lib/utils';
|
||||
import Pagination from '../components/ui/Pagination';
|
||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import ChangeLoginMobileModal from '../components/ChangeLoginMobileModal';
|
||||
import { latinDigitsField } from '../lib/forms';
|
||||
|
||||
const HUES_LIST = [256, 205, 162, 295, 272];
|
||||
@@ -41,6 +43,7 @@ export default function ClinicsPage() {
|
||||
const [search, setSearch] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('');
|
||||
const [deleteTarget, setDeleteTarget] = useState<Clinic | null>(null);
|
||||
const [mobileTarget, setMobileTarget] = useState<{ uuid: string; name: string; mobile_number?: string | null } | null>(null);
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const limit = 15;
|
||||
|
||||
@@ -217,6 +220,13 @@ export default function ClinicsPage() {
|
||||
>
|
||||
<EyeIcon style={{ width: 15, height: 15 }} />
|
||||
</button>
|
||||
<button
|
||||
className="mini-btn"
|
||||
title="تغییر شماره ورود"
|
||||
onClick={() => setMobileTarget({ uuid: c.uuid, name: c.name, mobile_number: c.owner_mobile })}
|
||||
>
|
||||
<DevicePhoneMobileIcon style={{ width: 15, height: 15 }} />
|
||||
</button>
|
||||
<button
|
||||
className={`mini-btn${c.is_active ? '' : ' active'}`}
|
||||
title={c.is_active ? 'غیرفعال کردن' : 'فعال کردن'}
|
||||
@@ -303,6 +313,13 @@ export default function ClinicsPage() {
|
||||
onConfirm={() => deleteTarget && deleteMutation.mutate(deleteTarget)}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
/>
|
||||
|
||||
<ChangeLoginMobileModal
|
||||
target={mobileTarget}
|
||||
resource="clinic"
|
||||
queryKey={['clinics']}
|
||||
onClose={() => setMobileTarget(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
MagnifyingGlassIcon, PlusIcon, EyeIcon, TrashIcon, ArrowPathIcon,
|
||||
CheckCircleIcon, XCircleIcon, TableCellsIcon, Squares2X2Icon,
|
||||
CheckCircleIcon, XCircleIcon, TableCellsIcon, Squares2X2Icon, DevicePhoneMobileIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { toast } from 'sonner';
|
||||
@@ -11,6 +11,7 @@ import { api } from '../lib/api';
|
||||
import type { ApiResponse, PaginatedResponse } from '../lib/api';
|
||||
import { formatDate, formatNumber, displayDoctorName } from '../lib/utils';
|
||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import ChangeLoginMobileModal from '../components/ChangeLoginMobileModal';
|
||||
import Pagination from '../components/ui/Pagination';
|
||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
@@ -111,6 +112,7 @@ export default function DoctorsPage() {
|
||||
const [cityId, setCityId] = useState('');
|
||||
const [view, setView] = useState<'table' | 'grid'>('table');
|
||||
const [deleteTarget, setDeleteTarget] = useState<AdminDoctor | null>(null);
|
||||
const [mobileTarget, setMobileTarget] = useState<{ uuid: string; name: string; mobile_number?: string | null } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => { setSearch(searchInput); setPage(1); }, 350);
|
||||
@@ -430,6 +432,10 @@ export default function DoctorsPage() {
|
||||
onClick={() => navigate(`/admin/doctors/${doc.uuid}`)}>
|
||||
<EyeIcon style={{ width: 16, height: 16 }} />
|
||||
</button>
|
||||
<button className="mini-btn" title="تغییر شماره ورود"
|
||||
onClick={() => setMobileTarget({ uuid: doc.uuid, name: doc.name, mobile_number: doc.mobile })}>
|
||||
<DevicePhoneMobileIcon style={{ width: 16, height: 16 }} />
|
||||
</button>
|
||||
<button className="mini-btn" title={doc.is_active ? 'غیرفعال کردن' : 'فعالسازی'}
|
||||
onClick={() => toggleMut.mutate(doc.uuid)} disabled={toggleMut.isPending}>
|
||||
{doc.is_active
|
||||
@@ -517,6 +523,13 @@ export default function DoctorsPage() {
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
/>
|
||||
|
||||
<ChangeLoginMobileModal
|
||||
target={mobileTarget}
|
||||
resource="doctors"
|
||||
queryKey={['doctors']}
|
||||
onClose={() => setMobileTarget(null)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user