feat(clinic): allow representation role to view clinic details without edit permissions
This commit is contained in:
@@ -165,7 +165,7 @@ export default function App() {
|
|||||||
<Route path="my-clinic" element={<RoleRoute roles={['clinic']}><MyClinicPage /></RoleRoute>} />
|
<Route path="my-clinic" element={<RoleRoute roles={['clinic']}><MyClinicPage /></RoleRoute>} />
|
||||||
|
|
||||||
{/* ادمین + کلینیک */}
|
{/* ادمین + کلینیک */}
|
||||||
<Route path="clinics/:uuid" element={<RoleRoute roles={['admin', 'clinic']}><ClinicDetailPage /></RoleRoute>} />
|
<Route path="clinics/:uuid" element={<RoleRoute roles={['admin', 'clinic', 'representation']}><ClinicDetailPage /></RoleRoute>} />
|
||||||
|
|
||||||
{/* فقط ادمین — کلینیک از طریق دعوتنامه در صفحه کلینیک خود دکتر اضافه میکند */}
|
{/* فقط ادمین — کلینیک از طریق دعوتنامه در صفحه کلینیک خود دکتر اضافه میکند */}
|
||||||
<Route path="representation-settlement" element={<RoleRoute roles={['representation']}><RepresentationSettlementPage /></RoleRoute>} />
|
<Route path="representation-settlement" element={<RoleRoute roles={['representation']}><RepresentationSettlementPage /></RoleRoute>} />
|
||||||
|
|||||||
@@ -480,6 +480,8 @@ export default function ClinicDetailPage() {
|
|||||||
const dbUuid = useAuthStore(s => s.dbUuid);
|
const dbUuid = useAuthStore(s => s.dbUuid);
|
||||||
const authToken = useAuthStore(s => s.token);
|
const authToken = useAuthStore(s => s.token);
|
||||||
const isOwner = primaryRole === 'clinic' && dbUuid === uuid;
|
const isOwner = primaryRole === 'clinic' && dbUuid === uuid;
|
||||||
|
// نماینده فقط مشاهده میکند؛ هیچ بخشی قابل ویرایش نیست.
|
||||||
|
const isReadOnly = primaryRole === 'representation';
|
||||||
|
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
@@ -759,9 +761,11 @@ export default function ClinicDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<button className="btn ghost sm" onClick={() => openEdit()}>
|
{!isReadOnly && (
|
||||||
<PencilIcon style={{ width: 15, height: 15 }} /> ویرایش
|
<button className="btn ghost sm" onClick={() => openEdit()}>
|
||||||
</button>
|
<PencilIcon style={{ width: 15, height: 15 }} /> ویرایش
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{primaryRole === 'admin' && (
|
{primaryRole === 'admin' && (
|
||||||
<>
|
<>
|
||||||
<button className={`btn sm ${clinic.is_active ? 'soft' : 'primary'}`}
|
<button className={`btn sm ${clinic.is_active ? 'soft' : 'primary'}`}
|
||||||
@@ -794,16 +798,20 @@ export default function ClinicDetailPage() {
|
|||||||
{clinic.name?.[0] ?? '?'}
|
{clinic.name?.[0] ?? '?'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button onClick={() => logoInputRef.current?.click()} disabled={logoUploading}
|
{!isReadOnly && (
|
||||||
style={{
|
<>
|
||||||
position: 'absolute', insetInlineEnd: -6, bottom: -6, width: 24, height: 24,
|
<button onClick={() => logoInputRef.current?.click()} disabled={logoUploading}
|
||||||
borderRadius: '50%', background: 'var(--primary)', border: '2px solid var(--surface)',
|
style={{
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
|
position: 'absolute', insetInlineEnd: -6, bottom: -6, width: 24, height: 24,
|
||||||
}}>
|
borderRadius: '50%', background: 'var(--primary)', border: '2px solid var(--surface)',
|
||||||
<CameraIcon style={{ width: 12, height: 12, color: '#fff' }} />
|
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
|
||||||
</button>
|
}}>
|
||||||
<input ref={logoInputRef} type="file" accept="image/*" style={{ display: 'none' }}
|
<CameraIcon style={{ width: 12, height: 12, color: '#fff' }} />
|
||||||
onChange={e => e.target.files?.[0] && handleLogoUpload(e.target.files[0])} />
|
</button>
|
||||||
|
<input ref={logoInputRef} type="file" accept="image/*" style={{ display: 'none' }}
|
||||||
|
onChange={e => e.target.files?.[0] && handleLogoUpload(e.target.files[0])} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div style={{ fontSize: 20, fontWeight: 700 }}>{clinic.name}</div>
|
<div style={{ fontSize: 20, fontWeight: 700 }}>{clinic.name}</div>
|
||||||
@@ -847,9 +855,11 @@ export default function ClinicDetailPage() {
|
|||||||
دعوتنامهها ({formatNumber(invitationList.length)})
|
دعوتنامهها ({formatNumber(invitationList.length)})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn primary sm" onClick={() => setInviteOpen(true)}>
|
{!isReadOnly && (
|
||||||
<EnvelopeIcon style={{ width: 14, height: 14 }} /> دعوت پزشک
|
<button className="btn primary sm" onClick={() => setInviteOpen(true)}>
|
||||||
</button>
|
<EnvelopeIcon style={{ width: 14, height: 14 }} /> دعوت پزشک
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Doctors tab */}
|
{/* Doctors tab */}
|
||||||
@@ -937,36 +947,38 @@ export default function ClinicDetailPage() {
|
|||||||
<span className={`badge ${isExpired ? 'gray' : statusInfo.cls}`} style={{ fontSize: 11 }}>
|
<span className={`badge ${isExpired ? 'gray' : statusInfo.cls}`} style={{ fontSize: 11 }}>
|
||||||
<span className="bdot" />{isExpired ? 'منقضی' : statusInfo.label}
|
<span className="bdot" />{isExpired ? 'منقضی' : statusInfo.label}
|
||||||
</span>
|
</span>
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
{!isReadOnly && (
|
||||||
{inv.status === 'pending' && (
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
|
{inv.status === 'pending' && (
|
||||||
|
<button
|
||||||
|
className="mini-btn"
|
||||||
|
title="ارسال مجدد"
|
||||||
|
disabled={resendInvMut.isPending}
|
||||||
|
onClick={() => resendInvMut.mutate(inv.uuid)}
|
||||||
|
>
|
||||||
|
<ArrowPathIcon style={{ width: 13, height: 13 }} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{inv.status !== 'removed' && inv.status !== 'accepted' && (
|
||||||
|
<button
|
||||||
|
className="mini-btn"
|
||||||
|
title="تعلیق"
|
||||||
|
disabled={changeInvStatusMut.isPending}
|
||||||
|
onClick={() => changeInvStatusMut.mutate({ invUuid: inv.uuid, status: inv.status === 'suspended' ? 'pending' : 'suspended' })}
|
||||||
|
>
|
||||||
|
<NoSymbolIcon style={{ width: 13, height: 13 }} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className="mini-btn"
|
className="mini-btn danger"
|
||||||
title="ارسال مجدد"
|
title="حذف"
|
||||||
disabled={resendInvMut.isPending}
|
disabled={deleteInvMut.isPending}
|
||||||
onClick={() => resendInvMut.mutate(inv.uuid)}
|
onClick={() => deleteInvMut.mutate(inv.uuid)}
|
||||||
>
|
>
|
||||||
<ArrowPathIcon style={{ width: 13, height: 13 }} />
|
<TrashIcon style={{ width: 13, height: 13 }} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
{inv.status !== 'removed' && inv.status !== 'accepted' && (
|
)}
|
||||||
<button
|
|
||||||
className="mini-btn"
|
|
||||||
title="تعلیق"
|
|
||||||
disabled={changeInvStatusMut.isPending}
|
|
||||||
onClick={() => changeInvStatusMut.mutate({ invUuid: inv.uuid, status: inv.status === 'suspended' ? 'pending' : 'suspended' })}
|
|
||||||
>
|
|
||||||
<NoSymbolIcon style={{ width: 13, height: 13 }} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
className="mini-btn danger"
|
|
||||||
title="حذف"
|
|
||||||
disabled={deleteInvMut.isPending}
|
|
||||||
onClick={() => deleteInvMut.mutate(inv.uuid)}
|
|
||||||
>
|
|
||||||
<TrashIcon style={{ width: 13, height: 13 }} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -982,13 +994,17 @@ export default function ClinicDetailPage() {
|
|||||||
<b style={{ fontSize: 14 }}>
|
<b style={{ fontSize: 14 }}>
|
||||||
گالری تصاویر ({formatNumber((clinic.images_clinic ?? []).filter(i => i?.url).length)}/{formatNumber(MAX_GALLERY_IMAGES)})
|
گالری تصاویر ({formatNumber((clinic.images_clinic ?? []).filter(i => i?.url).length)}/{formatNumber(MAX_GALLERY_IMAGES)})
|
||||||
</b>
|
</b>
|
||||||
<button className="btn ghost sm" onClick={() => galleryInputRef.current?.click()}
|
{!isReadOnly && (
|
||||||
disabled={galleryUploading || (clinic.images_clinic ?? []).filter(i => i?.url).length >= MAX_GALLERY_IMAGES}>
|
<>
|
||||||
<PlusIcon style={{ width: 14, height: 14 }} />
|
<button className="btn ghost sm" onClick={() => galleryInputRef.current?.click()}
|
||||||
{galleryUploading ? 'در حال آپلود...' : 'افزودن تصویر'}
|
disabled={galleryUploading || (clinic.images_clinic ?? []).filter(i => i?.url).length >= MAX_GALLERY_IMAGES}>
|
||||||
</button>
|
<PlusIcon style={{ width: 14, height: 14 }} />
|
||||||
<input ref={galleryInputRef} type="file" accept="image/*" multiple style={{ display: 'none' }}
|
{galleryUploading ? 'در حال آپلود...' : 'افزودن تصویر'}
|
||||||
onChange={e => e.target.files?.length && handleGalleryUpload(e.target.files)} />
|
</button>
|
||||||
|
<input ref={galleryInputRef} type="file" accept="image/*" multiple style={{ display: 'none' }}
|
||||||
|
onChange={e => e.target.files?.length && handleGalleryUpload(e.target.files)} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(clinic.images_clinic ?? []).length === 0 ? (
|
{(clinic.images_clinic ?? []).length === 0 ? (
|
||||||
<div className="empty" style={{ padding: '20px 0' }}>
|
<div className="empty" style={{ padding: '20px 0' }}>
|
||||||
@@ -999,15 +1015,17 @@ export default function ClinicDetailPage() {
|
|||||||
{clinic.images_clinic.filter(img => img?.url).map((img, i) => (
|
{clinic.images_clinic.filter(img => img?.url).map((img, i) => (
|
||||||
<div key={i} style={{ position: 'relative', borderRadius: 8, overflow: 'hidden', aspectRatio: '1', background: 'var(--surface-2, var(--bg))' }}>
|
<div key={i} style={{ position: 'relative', borderRadius: 8, overflow: 'hidden', aspectRatio: '1', background: 'var(--surface-2, var(--bg))' }}>
|
||||||
<img src={img.url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
<img src={img.url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||||
<button
|
{!isReadOnly && (
|
||||||
type="button"
|
<button
|
||||||
className="mini-btn danger"
|
type="button"
|
||||||
title="حذف تصویر"
|
className="mini-btn danger"
|
||||||
onClick={() => setDeleteImageConfirm(img.url)}
|
title="حذف تصویر"
|
||||||
style={{ position: 'absolute', top: 6, left: 6 }}
|
onClick={() => setDeleteImageConfirm(img.url)}
|
||||||
>
|
style={{ position: 'absolute', top: 6, left: 6 }}
|
||||||
<TrashIcon style={{ width: 14, height: 14 }} />
|
>
|
||||||
</button>
|
<TrashIcon style={{ width: 14, height: 14 }} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1036,9 +1054,11 @@ export default function ClinicDetailPage() {
|
|||||||
<div className="card card-pad">
|
<div className="card card-pad">
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||||
<b style={{ fontSize: 13 }}>تخصصها، بیمه و خدمات</b>
|
<b style={{ fontSize: 13 }}>تخصصها، بیمه و خدمات</b>
|
||||||
<button className="btn ghost sm" style={{ fontSize: 12 }} onClick={() => openEdit()}>
|
{!isReadOnly && (
|
||||||
<PencilIcon style={{ width: 13, height: 13 }} /> ویرایش
|
<button className="btn ghost sm" style={{ fontSize: 12 }} onClick={() => openEdit()}>
|
||||||
</button>
|
<PencilIcon style={{ width: 13, height: 13 }} /> ویرایش
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||||
@@ -1091,7 +1111,7 @@ export default function ClinicDetailPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Clinic Addresses Section */}
|
{/* Clinic Addresses Section */}
|
||||||
{(isOwner || primaryRole === 'admin') && (
|
{(isOwner || primaryRole === 'admin' || isReadOnly) && (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="toolbar" style={{ padding: '12px 16px' }}>
|
<div className="toolbar" style={{ padding: '12px 16px' }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14 }}>
|
<div style={{ fontWeight: 600, fontSize: 14 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user