import React, { useState, useEffect, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { MagnifyingGlassIcon, PlusIcon, EyeIcon, TrashIcon, ArrowPathIcon, CheckCircleIcon, XCircleIcon, TableCellsIcon, Squares2X2Icon, } from '@heroicons/react/24/outline'; import { XMarkIcon } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse, PaginatedResponse } from '../lib/api'; import { formatDate, formatNumber } from '../lib/utils'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import Pagination from '../components/ui/Pagination'; import SearchableSelect from '../components/ui/SearchableSelect'; import { useAuthStore } from '../stores/authStore'; // ── Types ───────────────────────────────────────────────────────────────── interface AdminDoctor { uuid: string; id: number; name: string; gender: string | null; degree: string | null; medical_code: string | null; mobile: string | null; email: string | null; is_active: boolean; owner_status?: string; source?: string; rate: number; representation_id?: number | null; representation_uuid?: string | null; representation_name?: string | null; specialties: { id: number; name: string }[]; profile_image: string | null; created_at: string; } interface DoctorStats { total: number; active: number; inactive: number; male: number; female: number; } interface SpecialtyOption { id: number; uuid: string; name: string; } // ── Helpers ─────────────────────────────────────────────────────────────── const HUES_LIST = [256, 205, 162, 295, 272]; const DEGREE_BADGE: Record = { general: 'gray', specialist: 'blue', expert: 'violet', subspecialistplus: 'amber', }; const DEGREE_LABEL: Record = { general: 'عمومی', specialist: 'متخصص', expert: 'فوق تخصص', subspecialistplus: 'فلوشیپ', }; function DoctorAvatar({ name, id, image, size = 'sm' }: { name: string; id: number; image: string | null; size?: 'sm' | 'lg'; }) { const initials = name.split(' ').filter(Boolean).map((w) => w[0]).join('').toUpperCase().slice(0, 2) || 'Dr'; const hue = HUES_LIST[id % HUES_LIST.length]; if (image) { return (
{name}
); } return (
{initials}
); } // ── Main Page ───────────────────────────────────────────────────────────── export default function DoctorsPage() { const navigate = useNavigate(); const qc = useQueryClient(); const primaryRole = useAuthStore(s => s.primaryRole); const isRepresentation = primaryRole === 'representation'; const [page, setPage] = useState(1); const [limit] = useState(25); const [searchInput, setSearchInput] = useState(''); const [search, setSearch] = useState(''); const [status, setStatus] = useState(''); const [ownerStatus, setOwnerStatus] = useState(''); const [specialtyId, setSpecialty] = useState(''); const [provinceId, setProvinceId] = useState(''); const [cityId, setCityId] = useState(''); const [view, setView] = useState<'table' | 'grid'>('table'); const [deleteTarget, setDeleteTarget] = useState(null); useEffect(() => { const t = setTimeout(() => { setSearch(searchInput); setPage(1); }, 350); return () => clearTimeout(t); }, [searchInput]); useEffect(() => { setPage(1); }, [status, ownerStatus, specialtyId, provinceId, cityId]); // ── Queries ── const statsQ = useQuery({ queryKey: ['doctors-stats', isRepresentation], queryFn: () => { const url = isRepresentation ? '/api/v1/representation/doctors/stats' : '/api/v1/admin/doctors/stats'; return api.get>(url); }, staleTime: 30_000, }); const specialtiesQ = useQuery({ queryKey: ['specialties-list'], queryFn: () => api.get>('/api/v1/specialties'), staleTime: 300_000, }); const provincesQ = useQuery({ queryKey: ['provinces-list'], queryFn: () => api.get>('/api/v1/provinces'), staleTime: 600_000, enabled: !isRepresentation, }); const citiesQ = useQuery({ queryKey: ['cities-list', provinceId], queryFn: () => api.get>(`/api/v1/cities${provinceId ? `?province_id=${provinceId}` : ''}`), staleTime: 600_000, enabled: !isRepresentation && !!provinceId, }); const doctorsQ = useQuery({ queryKey: ['admin-doctors', page, limit, search, status, ownerStatus, specialtyId, provinceId, cityId], 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); if (provinceId && !isRepresentation) p.set('state_id', provinceId); if (cityId && !isRepresentation) p.set('city_id', cityId); const base = isRepresentation ? '/api/v1/representation/doctors' : '/api/v1/admin/doctors'; return api.get>(`${base}?${p}`); }, }); const stats: DoctorStats | undefined = useMemo( () => (statsQ.data?.data as any)?.data ?? statsQ.data?.data, [statsQ.data] ); // هم endpoint ادمین و هم endpoint نماینده شکل AdminDoctor را برمی‌گردانند. const items: AdminDoctor[] = doctorsQ.data?.data ?? []; const total = doctorsQ.data?.meta?.totalRecords ?? 0; const specialties: SpecialtyOption[] = useMemo( () => (specialtiesQ.data?.data as any)?.data ?? specialtiesQ.data?.data ?? [], [specialtiesQ.data] ); const provinceOptions = useMemo( () => ((provincesQ.data?.data as any)?.data ?? provincesQ.data?.data ?? []).map((p: { id: number; name: string }) => ({ value: String(p.id), label: p.name })), [provincesQ.data] ); const cityOptions = useMemo( () => ((citiesQ.data?.data as any)?.data ?? citiesQ.data?.data ?? []).map((c: { id: number; name: string }) => ({ value: String(c.id), label: c.name })), [citiesQ.data] ); // ── Mutations ── const deleteMut = useMutation({ mutationFn: (uuid: string) => api.delete>(`/api/v1/doctor/${uuid}`), onSuccess: () => { toast.success('پزشک حذف شد'); setDeleteTarget(null); qc.invalidateQueries({ queryKey: ['admin-doctors'] }); qc.invalidateQueries({ queryKey: ['doctors-stats'] }); }, onError: (e: Error) => toast.error(e.message), }); const toggleMut = useMutation({ mutationFn: (uuid: string) => { const base = isRepresentation ? '/api/v1/representation/doctors' : '/api/v1/admin/doctors'; return api.post>(`${base}/${uuid}/status`, {}); }, onSuccess: () => { toast.success('وضعیت پزشک تغییر کرد'); qc.invalidateQueries({ queryKey: ['admin-doctors'] }); qc.invalidateQueries({ queryKey: ['doctors-stats'] }); }, onError: (e: Error) => toast.error(e.message), }); const kpiCards = [ { label: 'کل پزشکان', value: stats?.total, bg: 'var(--surface-3)', color: 'var(--text-2)' }, { label: 'فعال', value: stats?.active, bg: 'var(--success-bg)', color: 'var(--success)' }, { label: 'غیرفعال', value: stats?.inactive, bg: 'var(--surface-3)', color: 'var(--text-3)' }, { label: 'مرد', value: stats?.male, bg: 'var(--info-bg)', color: 'var(--info)' }, { label: 'زن', value: stats?.female, bg: 'var(--violet-bg)', color: 'var(--violet)' }, ]; return (
{/* Header */}

پزشکان

مدیریت پزشکان، تخصص‌ها و وضعیت همکاری
{/* KPI cards */}
{kpiCards.map((c) => (
{c.label}
{c.value === undefined ? : formatNumber(c.value)}
))}
{/* Main card */}
{/* Toolbar */}
setSearchInput(e.target.value)} placeholder="نام پزشک یا موبایل..." />
{specialties.length > 0 && (
({ value: String(s.id), label: s.name }))} value={specialtyId || null} onChange={(v) => setSpecialty(v ? String(v) : '')} placeholder="همه تخصص‌ها" isClearable height={36} />
)} {!isRepresentation && (
{ setProvinceId(v ? String(v) : ''); setCityId(''); }} placeholder="همه استان‌ها" isClearable height={36} />
)} {!isRepresentation && provinceId && (
setCityId(v ? String(v) : '')} placeholder="همه شهرها" isClearable height={36} />
)}
{!isRepresentation && (
)}
{/* Table view */} {view === 'table' && (
{!isRepresentation && } {doctorsQ.isLoading && Array.from({ length: 6 }).map((_, i) => ( {Array.from({ length: 7 }).map((_, j) => ( ))} ))} {!doctorsQ.isLoading && items.length === 0 && ( )} {!doctorsQ.isLoading && items.map((doc) => ( navigate(`/admin/doctors/${doc.uuid}`)}> {!isRepresentation && ( )} ))}
پزشک تخصص درجه امتیاز موبایلنمایندهوضعیت
هیچ پزشکی یافت نشد
دکتر {doc.name}
{doc.gender ?? '—'}
{doc.specialties.slice(0, 2).map((s) => ( {s.name} ))} {doc.specialties.length > 2 && ( +{doc.specialties.length - 2} )} {doc.degree && ( {DEGREE_LABEL[doc.degree] ?? doc.degree} )} {(doc.rate ?? 0).toFixed(1)} {doc.mobile ?? '—'} {doc.representation_id ? ( { e.stopPropagation(); if (doc.representation_uuid) navigate(`/admin/representations/${doc.representation_uuid}`); }} > {doc.representation_name ?? 'نماینده'} ) : ( بدون نماینده )} {doc.owner_status === 'unclaimed' && بدون‌مالک} {doc.owner_status === 'pending_transfer' && در انتظار انتقال} {doc.is_active ? 'فعال' : 'غیرفعال'} e.stopPropagation()}>
)} {/* Grid view */} {view === 'grid' && (
{doctorsQ.isLoading && Array.from({ length: 8 }).map((_, i) => (
))} {!doctorsQ.isLoading && items.map((doc) => (
navigate(`/admin/doctors/${doc.uuid}`)} >
دکتر {doc.name} {doc.specialties[0]?.name ?? '—'}
{doc.is_active ? 'فعال' : 'غیرفعال'}
{(doc.rate ?? 0).toFixed(1)} {formatDate(doc.created_at)}
))} {!doctorsQ.isLoading && items.length === 0 && (
هیچ پزشکی یافت نشد
)}
)}
{/* Dialogs */} deleteTarget && deleteMut.mutate(deleteTarget.uuid)} onCancel={() => setDeleteTarget(null)} />
); }