feat(representation): expose doctor representation details and add counts in admin views
This commit is contained in:
@@ -57,6 +57,7 @@ interface DoctorDetail {
|
||||
state: { id: string; name: string }[];
|
||||
city: { id: string; name: string }[];
|
||||
clinics: { id: string; uuid: string; name: string; address: string | null; telephone: string | null }[];
|
||||
representation: { id: number; uuid: string; full_name: string | null } | null;
|
||||
}
|
||||
|
||||
interface SpecialtyOpt { id: number; uuid: string; name: string; parent_id: number | null; }
|
||||
@@ -2699,6 +2700,13 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
<InfoCard icon={AcademicCapIcon} label="کد نظام پزشکی" value={doctor.medical_system_code} mono copyable />
|
||||
<InfoCard icon={CalendarIcon} label="سابقه (سال)" value={doctor.experience > 0 ? String(doctor.experience) : null} />
|
||||
<InfoCard icon={StarIcon} label="امتیاز" value={<div dir="ltr"><StarRating rate={rateNum} /></div>} />
|
||||
<InfoCard
|
||||
icon={UserIcon}
|
||||
label="نماینده"
|
||||
value={doctor.representation
|
||||
? <button type="button" style={{ color: '#2563eb', textDecoration: 'underline', background: 'none', border: 0, padding: 0, cursor: 'pointer', font: 'inherit' }} onClick={() => navigate(`/admin/representations/${doctor.representation!.uuid}`)}>{doctor.representation.full_name ?? 'نماینده'}</button>
|
||||
: <span className="text-slate-400">بدون نماینده</span>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ interface AdminDoctor {
|
||||
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;
|
||||
@@ -294,6 +297,7 @@ export default function DoctorsPage() {
|
||||
<th>درجه</th>
|
||||
<th>امتیاز</th>
|
||||
<th>موبایل</th>
|
||||
{!isRepresentation && <th>نماینده</th>}
|
||||
<th>وضعیت</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -348,6 +352,21 @@ export default function DoctorsPage() {
|
||||
<td style={{ fontFamily: 'monospace', direction: 'ltr', textAlign: 'right' }} className="muted">
|
||||
{doc.mobile ?? '—'}
|
||||
</td>
|
||||
{!isRepresentation && (
|
||||
<td>
|
||||
{doc.representation_id ? (
|
||||
<span
|
||||
className="badge blue"
|
||||
style={{ cursor: doc.representation_uuid ? 'pointer' : 'default', fontSize: 11 }}
|
||||
onClick={(e) => { e.stopPropagation(); if (doc.representation_uuid) navigate(`/admin/representations/${doc.representation_uuid}`); }}
|
||||
>
|
||||
{doc.representation_name ?? 'نماینده'}
|
||||
</span>
|
||||
) : (
|
||||
<span className="muted" style={{ fontSize: 11 }}>بدون نماینده</span>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
{doc.owner_status === 'unclaimed' && <span className="badge amber" style={{ marginLeft: 6 }}>بدونمالک</span>}
|
||||
{doc.owner_status === 'pending_transfer' && <span className="badge violet" style={{ marginLeft: 6 }}>در انتظار انتقال</span>}
|
||||
|
||||
@@ -13,6 +13,18 @@ import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import Modal from '../components/ui/Modal';
|
||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||
|
||||
interface RepDoctor {
|
||||
uuid: string;
|
||||
id: number;
|
||||
name: string;
|
||||
gender: string | null;
|
||||
medical_code: string | null;
|
||||
is_active: boolean;
|
||||
owner_status?: string;
|
||||
appointment_count: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
function DetailRow({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
return (
|
||||
<div className="cp-info-row items-start gap-4">
|
||||
@@ -45,6 +57,14 @@ export default function RepresentationDetailPage() {
|
||||
|
||||
const rep: Representation | undefined = (data?.data as any)?.data ?? data?.data;
|
||||
|
||||
const doctorsQuery = useQuery({
|
||||
queryKey: ['representation-doctors', uuid],
|
||||
queryFn: () => api.get<PaginatedResponse<RepDoctor>>(`/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 updateMutation = useMutation({
|
||||
mutationFn: (d: Partial<Representation>) =>
|
||||
api.patch<ApiResponse<Representation>>(`/api/v1/representation/${uuid}`, d),
|
||||
@@ -214,6 +234,42 @@ export default function RepresentationDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Doctors of this representation */}
|
||||
<div className="cp-card p-6 mt-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-sm font-semibold text-gray-700">پزشکان این نماینده</h2>
|
||||
<span className="badge blue">{formatNumber(repDoctorsTotal)} پزشک</span>
|
||||
</div>
|
||||
{doctorsQuery.isLoading ? (
|
||||
<p className="text-sm text-gray-400 text-center py-6">در حال بارگذاری…</p>
|
||||
) : repDoctors.length === 0 ? (
|
||||
<p className="text-sm text-gray-400 text-center py-6">این نماینده پزشکی ندارد</p>
|
||||
) : (
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>نام</th>
|
||||
<th>کد نظام</th>
|
||||
<th>تعداد نوبت</th>
|
||||
<th>وضعیت</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{repDoctors.map((d) => (
|
||||
<tr key={d.uuid} style={{ cursor: 'pointer' }} onClick={() => navigate(`/admin/doctors/${d.uuid}`)}>
|
||||
<td><b>{d.name}</b></td>
|
||||
<td dir="ltr" style={{ fontFamily: 'monospace' }}>{d.medical_code ?? '—'}</td>
|
||||
<td>{formatNumber(d.appointment_count)}</td>
|
||||
<td><ActiveBadge active={d.is_active} /></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Edit Modal */}
|
||||
<Modal open={editOpen} title="ویرایش نماینده"
|
||||
onClose={() => setEditOpen(false)}
|
||||
|
||||
@@ -104,6 +104,8 @@ export default function RepresentationsPage() {
|
||||
{ key: 'domain', header: 'دامنه', render: (r) => r.domain ? <span dir="ltr">{r.domain}</span> : '—' },
|
||||
{ key: 'city', header: 'شهرها', render: (r) => r.city ?? '—' },
|
||||
{ key: 'commission_percent', header: 'کمیسیون', render: (r) => `${formatNumber(r.commission_percent)}٪` },
|
||||
{ key: 'doctor_count', header: 'تعداد پزشکان', render: (r) => formatNumber(r.doctor_count ?? 0) },
|
||||
{ key: 'appointment_count', header: 'تعداد نوبت', render: (r) => formatNumber(r.appointment_count ?? 0) },
|
||||
{ key: 'wallet_balance', header: 'موجودی کیفپول', render: (r) => formatRial(r.wallet_balance ?? 0) },
|
||||
{ key: 'is_active', header: 'وضعیت', render: (r) => <ActiveBadge active={r.is_active ?? r.active ?? false} /> },
|
||||
{ key: 'created_at', header: 'تاریخ ثبت', render: (r) => formatDate(r.created_at) },
|
||||
|
||||
@@ -157,6 +157,8 @@ export interface Representation {
|
||||
commission_percent: number;
|
||||
bank_account: { card?: string; bank_name?: string; iban?: string } | null;
|
||||
wallet_balance?: number;
|
||||
doctor_count?: number;
|
||||
appointment_count?: number;
|
||||
active?: boolean;
|
||||
is_active?: boolean;
|
||||
created_at: string;
|
||||
|
||||
Reference in New Issue
Block a user