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 (
);
}
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={
<>
>
}
>
deleteMutation.mutate()}
onCancel={() => setDeleteOpen(false)}
/>
);
}