import { useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { TrashIcon, EnvelopeIcon, ArrowPathIcon, NoSymbolIcon, EyeIcon, ShieldCheckIcon, } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse, PaginatedResponse } from '../lib/api'; import { formatNumber } from '../lib/utils'; import ConfirmDialog from './ui/ConfirmDialog'; import InviteDoctorModal from './ui/InviteDoctorModal'; import DoctorPermissionsModal from './ui/DoctorPermissionsModal'; const HUES_LIST = [256, 205, 162, 295, 272]; export interface ClinicDoctorItem { id: string; uuid: string; name: string; gender: string | null; degree: string | null; img: { url: string }[]; specialties: { id: string; name: string }[]; active: boolean; } export interface ClinicInvitation { uuid: string; mobile: string; invited_name: string | null; invited_specialty: string | null; status: 'pending' | 'accepted' | 'rejected' | 'suspended' | 'removed'; token_used: boolean; invited_at: number; expires_at: number; responded_at: number | null; doctor: { uuid: string; name: string } | null; } const INV_STATUS_MAP: Record = { pending: { label: 'در انتظار', cls: 'amber' }, accepted: { label: 'پذیرفته‌شده', cls: 'green' }, rejected: { label: 'رد شده', cls: 'gray' }, suspended: { label: 'تعلیق', cls: 'violet' }, removed: { label: 'حذف‌شده', cls: 'gray' }, }; /** * ClinicDoctorsManager — self-contained management of a clinic's doctors and * pending invitations (list, invite, resend, suspend, delete invitation, detach * doctor). Reused by both the admin ClinicDetailPage and the clinic-owner * settings tab (ClinicDoctorsPage). `readOnly` hides every mutating control. */ export default function ClinicDoctorsManager({ clinicUuid, readOnly = false }: { clinicUuid: string; readOnly?: boolean; }) { const navigate = useNavigate(); const qc = useQueryClient(); const [doctorsTab, setDoctorsTab] = useState<'doctors' | 'invitations'>('doctors'); const [inviteOpen, setInviteOpen] = useState(false); const [detachDoctorConfirm, setDetachDoctorConfirm] = useState(null); const [permissionsFor, setPermissionsFor] = useState(null); const doctorsQ = useQuery({ queryKey: ['clinic-doctors', clinicUuid], queryFn: () => api.get>(`/api/v1/clinic/doctor-list/${clinicUuid}`), enabled: !!clinicUuid, }); const invitationsQ = useQuery({ queryKey: ['clinic-invitations', clinicUuid], queryFn: () => api.get>(`/api/v1/admin/clinic/${clinicUuid}/invitations?limit=50`), enabled: !!clinicUuid, }); const doctorList: ClinicDoctorItem[] = useMemo(() => { const raw = doctorsQ.data?.data; return (raw as any)?.data ?? raw ?? []; }, [doctorsQ.data]); const invitationList: ClinicInvitation[] = invitationsQ.data?.data ?? []; const resendInvMut = useMutation({ mutationFn: (invUuid: string) => api.post>(`/api/v1/admin/clinic/invitation/${invUuid}/resend`, {}), onSuccess: () => { toast.success('پیامک مجدداً ارسال شد'); qc.invalidateQueries({ queryKey: ['clinic-invitations', clinicUuid] }); }, onError: (e: Error) => toast.error(e.message), }); const changeInvStatusMut = useMutation({ mutationFn: ({ invUuid, status }: { invUuid: string; status: string }) => api.patch>(`/api/v1/admin/clinic/invitation/${invUuid}/status`, { status }), onSuccess: (_d, v) => { toast.success(v.status === 'pending' ? 'دعوتنامه فعال و پیامک مجدداً ارسال شد' : 'دعوتنامه تعلیق شد'); qc.invalidateQueries({ queryKey: ['clinic-invitations', clinicUuid] }); }, onError: (e: Error) => toast.error(e.message), }); const deleteInvMut = useMutation({ mutationFn: (invUuid: string) => api.delete>(`/api/v1/admin/clinic/invitation/${invUuid}`), onSuccess: () => { toast.success('دعوتنامه حذف شد'); qc.invalidateQueries({ queryKey: ['clinic-invitations', clinicUuid] }); }, onError: (e: Error) => toast.error(e.message), }); const detachDoctorMut = useMutation({ mutationFn: (doctorUuid: string) => api.delete>(`/api/v1/admin/clinic/${clinicUuid}/doctor/${doctorUuid}`), onSuccess: () => { toast.success('پزشک از کلینیک جدا شد'); qc.invalidateQueries({ queryKey: ['clinic-doctors', clinicUuid] }); qc.invalidateQueries({ queryKey: ['clinic-detail', clinicUuid] }); }, onError: (e: Error) => toast.error(e.message), }); return ( <>
{/* Card header */}
{!readOnly && ( )}
{/* Doctors tab */} {doctorsTab === 'doctors' && ( doctorList.length === 0 ? (

هیچ پزشکی به این کلینیک متصل نیست

) : (
{doctorList.map(doc => { const dHue = HUES_LIST[(doc.uuid?.charCodeAt(0) ?? 0) % HUES_LIST.length]; const img = doc.img?.[0]?.url; return (
{img ? :
{doc.name?.[0] ?? '?'}
}
{doc.name}
{doc.specialties?.length > 0 && (
{doc.specialties.map(s => s.name).join('، ')}
)}
{doc.active ? 'فعال' : 'غیرفعال'} {!readOnly && ( <> )}
); })}
) )} {/* Invitations tab */} {doctorsTab === 'invitations' && ( invitationList.length === 0 ? (

هیچ دعوتنامه‌ای ارسال نشده

) : (
{invitationList.map(inv => { const statusInfo = INV_STATUS_MAP[inv.status] ?? { label: inv.status, cls: 'gray' }; const isExpired = !inv.token_used && inv.status === 'pending' && Date.now() / 1000 > inv.expires_at; return (
{inv.invited_name ?? inv.mobile}
{inv.mobile} {inv.invited_specialty && ( {inv.invited_specialty} )} {inv.doctor && ( )}
{isExpired ? 'منقضی' : statusInfo.label} {!readOnly && (
{inv.status === 'pending' && ( )} {inv.status !== 'removed' && inv.status !== 'accepted' && ( )}
)}
); })}
) )}
{/* Detach Doctor From Clinic Confirm */} { if (detachDoctorConfirm) detachDoctorMut.mutate(detachDoctorConfirm.uuid); setDetachDoctorConfirm(null); }} onCancel={() => setDetachDoctorConfirm(null)} /> {/* Per-doctor clinic permissions */} {permissionsFor && ( setPermissionsFor(null)} /> )} {/* Invite doctor modal */} {inviteOpen && clinicUuid && ( setInviteOpen(false)} onInvited={() => { qc.invalidateQueries({ queryKey: ['clinic-invitations', clinicUuid] }); setDoctorsTab('invitations'); }} /> )} ); }