diff --git a/assets/admin/App.tsx b/assets/admin/App.tsx index 0769cfb2..04093dd5 100644 --- a/assets/admin/App.tsx +++ b/assets/admin/App.tsx @@ -39,6 +39,7 @@ import MyPatientsPage from './pages/MyPatientsPage'; import NewSessionPage from './pages/NewSessionPage'; import InsurancePricingPage from './pages/InsurancePricingPage'; import ClaimsPage from './pages/ClaimsPage'; +import DoctorClaimsPage from './pages/DoctorClaimsPage'; import MyFinancialPage from './pages/MyFinancialPage'; import ClinicFormPage from './pages/ClinicFormPage'; import PreRegistrationsPage from './pages/PreRegistrationsPage'; @@ -177,6 +178,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/assets/admin/components/layout/Sidebar.tsx b/assets/admin/components/layout/Sidebar.tsx index 9e30a2c1..cf147b92 100644 --- a/assets/admin/components/layout/Sidebar.tsx +++ b/assets/admin/components/layout/Sidebar.tsx @@ -73,6 +73,7 @@ function buildSections( label: "کاربران", }, { to: "/admin/doctors", icon: HeartIcon, label: "پزشکان" }, + { to: "/admin/doctor-claims", icon: HeartIcon, label: "تصاحب پروفایل" }, { to: "/admin/clinics", icon: BuildingOffice2Icon, diff --git a/assets/admin/pages/DoctorClaimsPage.tsx b/assets/admin/pages/DoctorClaimsPage.tsx new file mode 100644 index 00000000..3d6eee32 --- /dev/null +++ b/assets/admin/pages/DoctorClaimsPage.tsx @@ -0,0 +1,186 @@ +import React, { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { ArrowPathIcon, UserPlusIcon } from '@heroicons/react/24/outline'; +import { toast } from 'sonner'; +import { api } from '../lib/api'; +import type { ApiResponse, PaginatedResponse } from '../lib/api'; +import { formatDate } from '../lib/utils'; +import DataTable, { Column } from '../components/ui/DataTable'; +import Pagination from '../components/ui/Pagination'; +import Modal from '../components/ui/Modal'; + +interface DoctorClaim { + uuid: string; + status: 'pending' | 'completed' | 'failed'; + doctor: { uuid: string; name: string }; + mobile_masked: string; + verification_method: string; + failure_reason: string | null; + created_at: number; + completed_at: number | null; +} + +const FILTERS = [ + { value: '', label: 'همه' }, + { value: 'completed', label: 'موفق' }, + { value: 'failed', label: 'ناموفق' }, + { value: 'pending', label: 'در جریان' }, +]; + +const STATUS_BADGE: Record = { + completed: { cls: 'green', label: 'موفق' }, + failed: { cls: 'red', label: 'ناموفق' }, + pending: { cls: 'amber', label: 'در جریان' }, +}; + +const METHOD_LABEL: Record = { + 'apiir_personinfo+shahkar': 'استعلام هویت + شاهکار', + 'apiir_personinfo': 'استعلام هویت', + 'admin_manual': 'انتقال دستی ادمین', +}; + +export default function DoctorClaimsPage() { + const qc = useQueryClient(); + const [page, setPage] = useState(1); + const [status, setStatus] = useState(''); + const [transferTarget, setTransferTarget] = useState(null); + const [transferMobile, setTransferMobile] = useState(''); + const limit = 20; + + const { data, isLoading, refetch, isFetching } = useQuery({ + queryKey: ['doctor-claims', page, status], + queryFn: () => { + const p = new URLSearchParams({ page: String(page), limit: String(limit) }); + if (status) p.set('status', status); + return api.get>(`/api/v1/admin/doctor-claims?${p}`); + }, + }); + + const transferMut = useMutation({ + mutationFn: ({ doctorUuid, mobile }: { doctorUuid: string; mobile: string }) => + api.post>(`/api/v1/admin/doctors/${doctorUuid}/transfer`, { mobile }), + onSuccess: () => { + toast.success('پروفایل با موفقیت منتقل شد'); + setTransferTarget(null); + setTransferMobile(''); + qc.invalidateQueries({ queryKey: ['doctor-claims'] }); + qc.invalidateQueries({ queryKey: ['admin-doctors'] }); + }, + onError: (err: Error) => toast.error(err.message), + }); + + const columns: Column[] = [ + { key: 'doctor', header: 'پزشک', render: (c) => {c.doctor?.name} }, + { + key: 'status', + header: 'وضعیت', + render: (c) => {STATUS_BADGE[c.status].label}, + }, + { key: 'mobile_masked', header: 'موبایل', render: (c) => {c.mobile_masked} }, + { + key: 'verification_method', + header: 'روش احراز', + render: (c) => METHOD_LABEL[c.verification_method] ?? c.verification_method, + }, + { + key: 'failure_reason', + header: 'علت شکست', + render: (c) => c.failure_reason + ? {c.failure_reason} + : '—', + }, + { key: 'created_at', header: 'تاریخ درخواست', render: (c) => formatDate(c.created_at) }, + { + key: 'completed_at', + header: 'پایان', + render: (c) => (c.completed_at ? formatDate(c.completed_at) : '—'), + }, + ]; + + const items = data?.data ?? []; + const total = data?.meta?.totalRecords ?? 0; + + return ( +
+
+
+

تصاحب پروفایل پزشکان

+
{total} درخواست — پروفایل‌های ایمپورت‌شده از نظام پزشکی
+
+
+ +
+
+
+
+ {FILTERS.map((f) => ( + + ))} +
+
+ +
+
+ + + columns={columns} + data={items} + loading={isLoading} + emptyMessage="هیچ درخواست تصاحبی ثبت نشده است" + actions={(claim) => ( + claim.status !== 'completed' ? ( + + ) : null + )} + /> + +
+ + setTransferTarget(null)} + title={`انتقال دستی پروفایل ${transferTarget?.doctor?.name ?? ''}`} + > +
+

+ مالکیت پروفایل بدون استعلام هویت به کاربرِ این شماره منتقل می‌شود + (اگر کاربری با این موبایل نباشد، ساخته می‌شود). فقط برای پشتیبانی استفاده کنید. +

+
+ setTransferMobile(e.target.value)} + placeholder="09xxxxxxxxx" + maxLength={11} + /> +
+
+ + +
+
+
+
+ ); +} diff --git a/assets/admin/pages/DoctorsPage.tsx b/assets/admin/pages/DoctorsPage.tsx index ea5cc2af..210dd650 100644 --- a/assets/admin/pages/DoctorsPage.tsx +++ b/assets/admin/pages/DoctorsPage.tsx @@ -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(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>(`${base}?${p}`); @@ -257,6 +261,13 @@ export default function DoctorsPage() {
+ {!isRepresentation && ( +
+ + + +
+ )}