feat: add clinic role support to doctor profile access and update API endpoint for clinic owners
This commit is contained in:
@@ -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>} />
|
||||
|
||||
{/* دکتر / منشی / کلینیک */}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user