feat(admin): doctor claims audit page + owner_status visibility in doctors list
- DoctorClaimsPage: paginated claim requests (status filter, verification
method, masked mobile, human-readable failure reason, Jalali dates) +
manual-transfer modal calling POST /api/v1/admin/doctors/{uuid}/transfer
- DoctorsPage: owner_status filter segment (بدونمالک/تصاحبشده) + badges
for unclaimed / pending_transfer rows
- doctorsList API mapping now returns owner_status + source
- route /admin/doctor-claims (admin-only) + sidebar entry
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,8 @@ interface AdminDoctor {
|
||||
mobile: string | null;
|
||||
email: string | null;
|
||||
is_active: boolean;
|
||||
owner_status?: string;
|
||||
source?: string;
|
||||
rate: number;
|
||||
specialties: { id: number; name: string }[];
|
||||
profile_image: string | null;
|
||||
@@ -100,6 +102,7 @@ export default function DoctorsPage() {
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const [search, setSearch] = useState('');
|
||||
const [status, setStatus] = useState('');
|
||||
const [ownerStatus, setOwnerStatus] = useState('');
|
||||
const [specialtyId, setSpecialty] = useState('');
|
||||
const [view, setView] = useState<'table' | 'grid'>('table');
|
||||
const [deleteTarget, setDeleteTarget] = useState<AdminDoctor | null>(null);
|
||||
@@ -109,7 +112,7 @@ export default function DoctorsPage() {
|
||||
return () => clearTimeout(t);
|
||||
}, [searchInput]);
|
||||
|
||||
useEffect(() => { setPage(1); }, [status, specialtyId]);
|
||||
useEffect(() => { setPage(1); }, [status, ownerStatus, specialtyId]);
|
||||
|
||||
// ── Queries ──
|
||||
|
||||
@@ -131,11 +134,12 @@ export default function DoctorsPage() {
|
||||
});
|
||||
|
||||
const doctorsQ = useQuery({
|
||||
queryKey: ['admin-doctors', page, limit, search, status, specialtyId],
|
||||
queryKey: ['admin-doctors', page, limit, search, status, ownerStatus, specialtyId],
|
||||
queryFn: () => {
|
||||
const p = new URLSearchParams({ page: String(page), limit: String(limit) });
|
||||
if (search) p.set('search', search);
|
||||
if (status) p.set('status', status);
|
||||
if (ownerStatus && !isRepresentation) p.set('owner_status', ownerStatus);
|
||||
if (specialtyId) p.set('specialty_id', specialtyId);
|
||||
const base = isRepresentation ? '/api/v1/representation/doctors' : '/api/v1/admin/doctors';
|
||||
return api.get<PaginatedResponse<AdminDoctor>>(`${base}?${p}`);
|
||||
@@ -257,6 +261,13 @@ export default function DoctorsPage() {
|
||||
<button className={status === 'active' ? 'on' : ''} onClick={() => setStatus('active')}>فعال</button>
|
||||
<button className={status === 'inactive' ? 'on' : ''} onClick={() => setStatus('inactive')}>غیرفعال</button>
|
||||
</div>
|
||||
{!isRepresentation && (
|
||||
<div className="seg" title="مالکیت پروفایل (ایمپورت نظام پزشکی)">
|
||||
<button className={!ownerStatus ? 'on' : ''} onClick={() => setOwnerStatus('')}>همه</button>
|
||||
<button className={ownerStatus === 'unclaimed' ? 'on' : ''} onClick={() => setOwnerStatus('unclaimed')}>بدونمالک</button>
|
||||
<button className={ownerStatus === 'claimed' ? 'on' : ''} onClick={() => setOwnerStatus('claimed')}>تصاحبشده</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="spacer" />
|
||||
<div className="seg">
|
||||
<button className={view === 'table' ? 'on' : ''} onClick={() => setView('table')} title="جدول">
|
||||
@@ -338,6 +349,8 @@ export default function DoctorsPage() {
|
||||
{doc.mobile ?? '—'}
|
||||
</td>
|
||||
<td>
|
||||
{doc.owner_status === 'unclaimed' && <span className="badge amber" style={{ marginLeft: 6 }}>بدونمالک</span>}
|
||||
{doc.owner_status === 'pending_transfer' && <span className="badge violet" style={{ marginLeft: 6 }}>در انتظار انتقال</span>}
|
||||
<span className={`badge ${doc.is_active ? 'green' : 'gray'}`}>
|
||||
<span className="bdot" />
|
||||
{doc.is_active ? 'فعال' : 'غیرفعال'}
|
||||
|
||||
Reference in New Issue
Block a user