import React, { useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { PencilIcon, TrashIcon, CheckCircleIcon, XCircleIcon, UserGroupIcon, CalendarDaysIcon, ClockIcon, ReceiptPercentIcon, PlusIcon } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse, PaginatedResponse } from '../lib/api'; import type { Representation, City } from '../types'; import { formatDate, formatDateTime, formatNumber } from '../lib/utils'; import PageHeader from '../components/ui/PageHeader'; import { ActiveBadge } from '../components/ui/StatusBadge'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import Modal from '../components/ui/Modal'; import SearchableSelect from '../components/ui/SearchableSelect'; import { digitsOnly } from '../lib/utils'; interface RepDoctor { uuid: string; id: number; name: string; gender: string | null; medical_code: string | null; is_active: boolean; owner_status?: string; past_count: number; upcoming_count: number; created_at: string; } interface RepAppointment { uuid: string; slot_start: number; slot_end: number; status: string; patient_name: string | null; doctor_uuid: string; doctor_name: string; } interface UnassignedDoctor { uuid: string; name: string; medical_code: string | null; mobile: string | null; } function DetailRow({ label, value }: { label: string; value: React.ReactNode }) { return (
{label} {value ?? '—'}
); } function StatTile({ icon: Icon, label, value, tone }: { icon: React.ComponentType<{ className?: string }>; label: string; value: React.ReactNode; tone: 'violet' | 'green' | 'amber' | 'pink'; }) { return (
{label}
{value}
); } export default function RepresentationDetailPage() { const { uuid } = useParams<{ uuid: string }>(); const navigate = useNavigate(); const qc = useQueryClient(); const [deleteOpen, setDeleteOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); const [formData, setFormData] = useState({ full_name: '', city_id: '', mobile_number: '', commission_percent: '' }); const [attachOpen, setAttachOpen] = useState(false); const [attachSearch, setAttachSearch] = useState(''); const [apptScope, setApptScope] = useState<'upcoming' | 'past'>('upcoming'); const citiesQuery = useQuery({ queryKey: ['cities-select'], queryFn: () => api.get>('/api/v1/admin/cities?limit=200'), staleTime: 5 * 60_000, }); const cities: City[] = citiesQuery.data?.data ?? []; const { data, isLoading } = useQuery({ queryKey: ['representation', uuid], queryFn: () => api.get>(`/api/v1/representation/${uuid}`), enabled: !!uuid, }); const rep: Representation | undefined = (data?.data as any)?.data ?? data?.data; const doctorsQuery = useQuery({ queryKey: ['representation-doctors', uuid], queryFn: () => api.get>(`/api/v1/admin/representations/${uuid}/doctors?limit=100`), enabled: !!uuid, }); const repDoctors: RepDoctor[] = doctorsQuery.data?.data ?? []; const repDoctorsTotal = doctorsQuery.data?.meta?.totalRecords ?? repDoctors.length; const upcomingTotal = repDoctors.reduce((s, d) => s + (d.upcoming_count ?? 0), 0); const pastTotal = repDoctors.reduce((s, d) => s + (d.past_count ?? 0), 0); const appointmentsQuery = useQuery({ queryKey: ['representation-appointments', uuid, apptScope], queryFn: () => api.get>(`/api/v1/admin/representations/${uuid}/appointments?scope=${apptScope}&limit=50`), enabled: !!uuid, }); const repAppointments: RepAppointment[] = appointmentsQuery.data?.data ?? []; const repAppointmentsTotal = appointmentsQuery.data?.meta?.totalRecords ?? repAppointments.length; const unassignedQuery = useQuery({ queryKey: ['unassigned-doctors', attachSearch], queryFn: () => api.get>(`/api/v1/admin/doctors?unassigned=1&limit=20${attachSearch ? `&search=${encodeURIComponent(attachSearch)}` : ''}`), enabled: attachOpen, }); const unassignedDoctors: UnassignedDoctor[] = unassignedQuery.data?.data ?? []; const attachMutation = useMutation({ mutationFn: (doctorUuid: string) => api.post>(`/api/v1/admin/representations/${uuid}/doctors`, { doctor_uuid: doctorUuid }), onSuccess: () => { toast.success('پزشک به نماینده متصل شد'); setAttachOpen(false); setAttachSearch(''); qc.invalidateQueries({ queryKey: ['representation-doctors', uuid] }); qc.invalidateQueries({ queryKey: ['representations'] }); }, onError: (err: Error) => toast.error(err.message), }); const updateMutation = useMutation({ mutationFn: (d: Partial) => api.patch>(`/api/v1/representation/${uuid}`, d), onSuccess: () => { toast.success('اطلاعات بروزرسانی شد'); setEditOpen(false); qc.invalidateQueries({ queryKey: ['representation', uuid] }); qc.invalidateQueries({ queryKey: ['representations'] }); }, onError: (err: Error) => toast.error(err.message), }); const toggleActiveMutation = useMutation({ mutationFn: () => api.patch>(`/api/v1/representation/${uuid}`, { active: !(rep?.active ?? rep?.is_active), }), onSuccess: () => { toast.success('وضعیت تغییر کرد'); qc.invalidateQueries({ queryKey: ['representation', uuid] }); }, onError: (err: Error) => toast.error(err.message), }); const deleteMutation = useMutation({ mutationFn: () => api.delete>(`/api/v1/representation/${uuid}`), onSuccess: () => { toast.success('نماینده حذف شد'); navigate('/admin/representations'); }, onError: (err: Error) => toast.error(err.message), }); const openEdit = () => { if (!rep) return; setFormData({ full_name: rep.full_name ?? rep.domain ?? '', city_id: rep.city_id ? String(rep.city_id) : '', mobile_number: rep.mobile_number ?? '', commission_percent: String(rep.commission_percent), }); setEditOpen(true); }; const isActive = rep?.active ?? rep?.is_active ?? false; if (isLoading) { return (
{[1, 2, 3, 4, 5].map((i) => (
))}
); } if (!rep) { return (
نماینده‌ای با این شناسه یافت نشد.
); } const name = rep.full_name ?? rep.domain ?? '—'; return (
} /> {/* KPI tiles */}
{/* Basic Info */}

اطلاعات پایه

{rep.mobile_number} : null } /> } />
{/* Bank Account */}

اطلاعات بانکی

{rep.bank_account ? ( <> {rep.bank_account.card} : null } /> {rep.bank_account.iban} : null } /> ) : (

اطلاعات بانکی ثبت نشده

)}
{/* Doctors of this representation */}

پزشکان این نماینده

{formatNumber(repDoctorsTotal)}
{doctorsQuery.isLoading ? (

در حال بارگذاری…

) : repDoctors.length === 0 ? (

این نماینده هنوز پزشکی ندارد

) : (
{repDoctors.map((d) => ( navigate(`/admin/doctors/${d.uuid}`)}> ))}
نام پزشک کد نظام نوبت گذشته نوبت آینده وضعیت
{d.name} {d.medical_code ?? '—'} {formatNumber(d.past_count)} {d.upcoming_count > 0 ? {formatNumber(d.upcoming_count)} : ۰}
)}
{/* Appointments of this representation */}

نوبت‌های این نماینده

{!appointmentsQuery.isLoading && {formatNumber(repAppointmentsTotal)}}
{appointmentsQuery.isLoading ? (

در حال بارگذاری…

) : repAppointments.length === 0 ? (

{apptScope === 'upcoming' ? 'نوبت آینده‌ای ثبت نشده است' : 'نوبت گذشته‌ای وجود ندارد'}

) : (
{repAppointments.map((a) => ( navigate(`/admin/doctors/${a.doctor_uuid}`)}> ))}
پزشک بیمار زمان وضعیت
{a.doctor_name} {a.patient_name ?? '—'} {formatDateTime(a.slot_start)} {a.status}
)}
{/* Attach Doctor Modal */} { setAttachOpen(false); setAttachSearch(''); }} footer={} > setAttachSearch(e.target.value)} /> {unassignedQuery.isLoading ? (

در حال بارگذاری…

) : unassignedDoctors.length === 0 ? (

پزشکِ بدون نماینده‌ای یافت نشد

) : (
{unassignedDoctors.map((d) => (
{d.name}
{d.medical_code ?? d.mobile ?? '—'}
))}
)}
{/* Edit Modal */} setEditOpen(false)} footer={ <> } >
setFormData((p) => ({ ...p, full_name: e.target.value }))} className="cp-input h-11" />
({ value: String(c.id), label: c.name }))} value={formData.city_id || null} onChange={(v) => setFormData(p => ({ ...p, city_id: v ? String(v) : '' }))} placeholder="انتخاب شهر" isClearable />
setFormData((p) => ({ ...p, mobile_number: e.target.value }))} dir="ltr" className="cp-input h-11" />
setFormData((p) => ({ ...p, commission_percent: digitsOnly(e.target.value, 3) }))} type="text" inputMode="numeric" dir="ltr" className="cp-input h-11" />
deleteMutation.mutate()} onCancel={() => setDeleteOpen(false)} />
); }