feat: implement doctor invitation acceptance logic to add doctors to clinic_doctors
This commit is contained in:
@@ -835,7 +835,13 @@ export default function ClinicDetailPage() {
|
||||
<span className="muted" style={{ fontSize: 11 }}>{inv.invited_specialty}</span>
|
||||
)}
|
||||
{inv.doctor && (
|
||||
<span className="badge blue" style={{ fontSize: 11 }}><span className="bdot" />{inv.doctor.name}</span>
|
||||
<button
|
||||
className="badge blue"
|
||||
style={{ fontSize: 11, cursor: 'pointer', border: 'none', background: 'none', padding: 0 }}
|
||||
onClick={() => navigate(`/admin/doctors/${inv.doctor!.uuid}`)}
|
||||
>
|
||||
<span className="bdot" />{inv.doctor.name}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
UserGroupIcon, HeartIcon, BuildingOffice2Icon, CalendarDaysIcon,
|
||||
CreditCardIcon, ArrowPathIcon, BellAlertIcon, ChatBubbleLeftEllipsisIcon,
|
||||
ClockIcon, StarIcon, UserIcon,
|
||||
ClockIcon, StarIcon, UserIcon, CheckIcon, XMarkIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '../lib/api';
|
||||
import type { ApiResponse } from '../lib/api';
|
||||
import { formatNumber, formatRial, formatDateTime } from '../lib/utils';
|
||||
@@ -626,6 +627,103 @@ function ClinicDashboard() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Doctor Clinic Invitations Card ───────────────────────────────────────
|
||||
|
||||
interface ClinicInvitation {
|
||||
uuid: string;
|
||||
invited_specialty: string | null;
|
||||
invited_at: number;
|
||||
expires_at: number;
|
||||
clinic: { uuid: string; name: string; logo: string | null };
|
||||
}
|
||||
|
||||
function DoctorClinicInvitationsCard() {
|
||||
const qc = useQueryClient();
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['doctor-my-invitations'],
|
||||
queryFn: () => api.get<{ data: { data: ClinicInvitation[] } }>('/api/v1/doctor/invitations'),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
|
||||
const respondMut = useMutation({
|
||||
mutationFn: ({ uuid, action }: { uuid: string; action: 'accept' | 'reject' }) =>
|
||||
api.post(`/api/v1/doctor/invitation/${uuid}/respond`, { action }),
|
||||
onSuccess: (_res, { action }) => {
|
||||
toast.success(action === 'accept' ? 'دعوتنامه پذیرفته شد' : 'دعوتنامه رد شد');
|
||||
qc.invalidateQueries({ queryKey: ['doctor-my-invitations'] });
|
||||
qc.invalidateQueries({ queryKey: ['dashboard-doctor'] });
|
||||
},
|
||||
onError: (err: Error) => toast.error(err.message),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const invitations: ClinicInvitation[] = (data?.data as any) ?? [];
|
||||
|
||||
if (!isLoading && invitations.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="card card-pad" style={{ marginTop: 'var(--gap)' }}>
|
||||
<div className="card-title-row">
|
||||
<h3 style={{ fontSize: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<BellAlertIcon style={{ width: 18, height: 18, color: 'var(--danger)' }} />
|
||||
دعوتنامههای کلینیک
|
||||
</h3>
|
||||
<span className="badge red"><span className="bdot" />{invitations.length} در انتظار</span>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{[1, 2].map(i => (
|
||||
<div key={i} className="skeleton" style={{ height: 60, borderRadius: 8 }} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{invitations.map((inv) => (
|
||||
<div key={inv.uuid} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 12,
|
||||
padding: '12px 14px', borderRadius: 8,
|
||||
background: 'var(--surface-2, var(--primary-soft))',
|
||||
border: '1px solid var(--border)',
|
||||
}}>
|
||||
{inv.clinic.logo ? (
|
||||
<img src={inv.clinic.logo} alt="" style={{ width: 40, height: 40, borderRadius: 8, objectFit: 'cover', flexShrink: 0 }} />
|
||||
) : (
|
||||
<div style={{ width: 40, height: 40, borderRadius: 8, background: 'var(--primary)', color: 'var(--on-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700, fontSize: 16, flexShrink: 0 }}>
|
||||
{(inv.clinic.name ?? '?')[0]}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<b style={{ fontSize: 13.5, display: 'block' }}>{inv.clinic.name}</b>
|
||||
{inv.invited_specialty && <span className="muted" style={{ fontSize: 12 }}>{inv.invited_specialty}</span>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
|
||||
<button
|
||||
className="btn primary sm"
|
||||
style={{ padding: '5px 12px', fontSize: 12 }}
|
||||
disabled={respondMut.isPending}
|
||||
onClick={() => respondMut.mutate({ uuid: inv.uuid, action: 'accept' })}
|
||||
>
|
||||
<CheckIcon style={{ width: 14, height: 14 }} />
|
||||
پذیرفتن
|
||||
</button>
|
||||
<button
|
||||
className="btn ghost sm"
|
||||
style={{ padding: '5px 12px', fontSize: 12 }}
|
||||
disabled={respondMut.isPending}
|
||||
onClick={() => respondMut.mutate({ uuid: inv.uuid, action: 'reject' })}
|
||||
>
|
||||
<XMarkIcon style={{ width: 14, height: 14 }} />
|
||||
رد کردن
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Doctor Dashboard ──────────────────────────────────────────────────────
|
||||
|
||||
interface DoctorDashboardData {
|
||||
@@ -684,6 +782,8 @@ function DoctorDashboard() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DoctorClinicInvitationsCard />
|
||||
|
||||
<div className="grid-2" style={{ marginTop: 'var(--gap)' }}>
|
||||
<div className="card card-pad">
|
||||
<div className="card-title-row">
|
||||
|
||||
Reference in New Issue
Block a user