feat: add clinic role support to doctor profile access and update API endpoint for clinic owners

This commit is contained in:
hamed
2026-06-13 18:10:44 +03:30
parent 5c2e9fae3f
commit c1c57df4ff
5 changed files with 253 additions and 21 deletions
+1 -1
View File
@@ -138,7 +138,7 @@ export default function App() {
{/* فقط ادمین — کلینیک از طریق دعوتنامه در صفحه کلینیک خود دکتر اضافه می‌کند */}
<Route path="doctors" element={<RoleRoute roles={['admin']}><DoctorsPage /></RoleRoute>} />
<Route path="doctors/new" element={<RoleRoute roles={['admin']}><DoctorFormPage /></RoleRoute>} />
<Route path="doctors/:uuid" element={<RoleRoute roles={['admin', 'doctor']}><DoctorDetailPage /></RoleRoute>} />
<Route path="doctors/:uuid" element={<RoleRoute roles={['admin', 'doctor', 'clinic']}><DoctorDetailPage /></RoleRoute>} />
<Route path="profile" element={<RoleRoute roles={['doctor']}><DoctorProfilePage /></RoleRoute>} />
{/* دکتر / منشی / کلینیک */}
+34 -20
View File
@@ -358,24 +358,31 @@ async function uploadDoctorImage(file: File): Promise<ImageFileData> {
function DoctorAvatar({ name, img, idx, onUpload, uploading }: {
name: string; img: string | null; idx: number;
onUpload: (f: File) => void; uploading: boolean;
onUpload?: (f: File) => void; uploading: boolean;
}) {
const ref = useRef<HTMLInputElement>(null);
const initials = name.split(' ').filter(Boolean).map(w => w[0]).join('').toUpperCase().slice(0, 2) || 'Dr';
return (
<div className="relative shrink-0 group cursor-pointer" onClick={() => ref.current?.click()}>
<div
className={`relative shrink-0 group ${onUpload ? 'cursor-pointer' : ''}`}
onClick={() => onUpload && ref.current?.click()}
>
{img
? <img src={img} alt={name} className="w-24 h-24 rounded-2xl object-cover shadow-xl ring-4 ring-white dark:ring-gray-900" />
: <div className={`w-24 h-24 rounded-2xl bg-gradient-to-br ${AVATAR_COLORS[idx % AVATAR_COLORS.length]} flex items-center justify-center text-white text-3xl font-bold shadow-xl ring-4 ring-white dark:ring-gray-900`}>{initials}</div>
}
<div className="absolute inset-0 rounded-2xl bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
{uploading
? <span className="w-6 h-6 border-2 border-white/40 border-t-white rounded-full animate-spin" />
: <CameraIcon className="w-7 h-7 text-white" />
}
</div>
<input ref={ref} type="file" accept="image/*" className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) onUpload(f); e.target.value = ''; }} />
{onUpload && (
<div className="absolute inset-0 rounded-2xl bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
{uploading
? <span className="w-6 h-6 border-2 border-white/40 border-t-white rounded-full animate-spin" />
: <CameraIcon className="w-7 h-7 text-white" />
}
</div>
)}
{onUpload && (
<input ref={ref} type="file" accept="image/*" className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) onUpload(f); e.target.value = ''; }} />
)}
</div>
);
}
@@ -2148,8 +2155,11 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
// ── Queries ──
const { data, isLoading, isError } = useQuery({
queryKey: ['doctor-detail', uuid],
queryFn: () => api.get<ApiResponse<any>>(`/api/v1/doctor/${uuid}`),
queryKey: ['doctor-detail', uuid, primaryRole],
queryFn: () =>
primaryRole === 'clinic'
? api.get<ApiResponse<any>>(`/api/v1/clinic/my-doctor/${uuid}`)
: api.get<ApiResponse<any>>(`/api/v1/doctor/${uuid}`),
enabled: !!uuid,
});
const specialtiesQ = useQuery({
@@ -2332,9 +2342,9 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
</>
) : (
<>
<button onClick={() => navigate('/admin/doctors')}
<button onClick={() => navigate(primaryRole === 'clinic' ? '/admin/dashboard' : '/admin/doctors')}
className="flex items-center gap-1.5 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
<ArrowRightIcon className="w-4 h-4" />پزشکان
<ArrowRightIcon className="w-4 h-4" />{primaryRole === 'clinic' ? 'داشبورد' : 'پزشکان'}
</button>
<span>/</span>
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل پزشک</span>
@@ -2349,8 +2359,10 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
<div className="flex flex-col items-center gap-1.5">
<DoctorAvatar name={doctor.name} img={mainImage} idx={idNum}
onUpload={handleImageUpload} uploading={uploadingImg} />
<span className="text-[10px] text-slate-400 dark:text-slate-500">کلیک برای تغییر عکس</span>
onUpload={primaryRole !== 'clinic' ? handleImageUpload : undefined} uploading={uploadingImg} />
{primaryRole !== 'clinic' && (
<span className="text-[10px] text-slate-400 dark:text-slate-500">کلیک برای تغییر عکس</span>
)}
</div>
<div className="flex-1 min-w-0 sm:mb-1">
@@ -2389,12 +2401,14 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
</div>
<div className="flex items-center gap-2 sm:mb-1 flex-wrap">
<button onClick={() => setEditOpen(true)} className="cp-btn-primary text-sm">
<PencilIcon className="w-4 h-4" />ویرایش
</button>
{primaryRole !== 'clinic' && (
<button onClick={() => setEditOpen(true)} className="cp-btn-primary text-sm">
<PencilIcon className="w-4 h-4" />ویرایش
</button>
)}
{/* Toggle active / Delete — فقط ادمین */}
{!isOwnProfile && (
{!isOwnProfile && primaryRole !== 'clinic' && (
<>
<button
onClick={() => setToggleConfirm(true)}