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:
hamed
2026-07-11 11:49:04 +03:30
co-authored by Claude Opus 4.8
parent af125572c9
commit 958105694a
5 changed files with 206 additions and 2 deletions
+15 -2
View File
@@ -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 ? 'فعال' : 'غیرفعال'}