feat(clinic): allow representation role to view clinic details without edit permissions

This commit is contained in:
hamed
2026-06-25 09:42:41 +03:30
parent e65e506dae
commit 41b435bb88
2 changed files with 84 additions and 64 deletions
+1 -1
View File
@@ -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>} />
+21 -1
View File
@@ -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 }}>
{!isReadOnly && (
<button className="btn ghost sm" onClick={() => openEdit()}> <button className="btn ghost sm" onClick={() => openEdit()}>
<PencilIcon style={{ width: 15, height: 15 }} /> ویرایش <PencilIcon style={{ width: 15, height: 15 }} /> ویرایش
</button> </button>
)}
{primaryRole === 'admin' && ( {primaryRole === 'admin' && (
<> <>
<button className={`btn sm ${clinic.is_active ? 'soft' : 'primary'}`} <button className={`btn sm ${clinic.is_active ? 'soft' : 'primary'}`}
@@ -794,6 +798,8 @@ export default function ClinicDetailPage() {
{clinic.name?.[0] ?? '?'} {clinic.name?.[0] ?? '?'}
</div> </div>
)} )}
{!isReadOnly && (
<>
<button onClick={() => logoInputRef.current?.click()} disabled={logoUploading} <button onClick={() => logoInputRef.current?.click()} disabled={logoUploading}
style={{ style={{
position: 'absolute', insetInlineEnd: -6, bottom: -6, width: 24, height: 24, position: 'absolute', insetInlineEnd: -6, bottom: -6, width: 24, height: 24,
@@ -804,6 +810,8 @@ export default function ClinicDetailPage() {
</button> </button>
<input ref={logoInputRef} type="file" accept="image/*" style={{ display: 'none' }} <input ref={logoInputRef} type="file" accept="image/*" style={{ display: 'none' }}
onChange={e => e.target.files?.[0] && handleLogoUpload(e.target.files[0])} /> 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>
{!isReadOnly && (
<button className="btn primary sm" onClick={() => setInviteOpen(true)}> <button className="btn primary sm" onClick={() => setInviteOpen(true)}>
<EnvelopeIcon style={{ width: 14, height: 14 }} /> دعوت پزشک <EnvelopeIcon style={{ width: 14, height: 14 }} /> دعوت پزشک
</button> </button>
)}
</div> </div>
{/* Doctors tab */} {/* Doctors tab */}
@@ -937,6 +947,7 @@ 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>
{!isReadOnly && (
<div style={{ display: 'flex', gap: 4 }}> <div style={{ display: 'flex', gap: 4 }}>
{inv.status === 'pending' && ( {inv.status === 'pending' && (
<button <button
@@ -967,6 +978,7 @@ export default function ClinicDetailPage() {
<TrashIcon style={{ width: 13, height: 13 }} /> <TrashIcon style={{ width: 13, height: 13 }} />
</button> </button>
</div> </div>
)}
</div> </div>
</div> </div>
); );
@@ -982,6 +994,8 @@ 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>
{!isReadOnly && (
<>
<button className="btn ghost sm" onClick={() => galleryInputRef.current?.click()} <button className="btn ghost sm" onClick={() => galleryInputRef.current?.click()}
disabled={galleryUploading || (clinic.images_clinic ?? []).filter(i => i?.url).length >= MAX_GALLERY_IMAGES}> disabled={galleryUploading || (clinic.images_clinic ?? []).filter(i => i?.url).length >= MAX_GALLERY_IMAGES}>
<PlusIcon style={{ width: 14, height: 14 }} /> <PlusIcon style={{ width: 14, height: 14 }} />
@@ -989,6 +1003,8 @@ export default function ClinicDetailPage() {
</button> </button>
<input ref={galleryInputRef} type="file" accept="image/*" multiple style={{ display: 'none' }} <input ref={galleryInputRef} type="file" accept="image/*" multiple style={{ display: 'none' }}
onChange={e => e.target.files?.length && handleGalleryUpload(e.target.files)} /> 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,6 +1015,7 @@ 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' }} />
{!isReadOnly && (
<button <button
type="button" type="button"
className="mini-btn danger" className="mini-btn danger"
@@ -1008,6 +1025,7 @@ export default function ClinicDetailPage() {
> >
<TrashIcon style={{ width: 14, height: 14 }} /> <TrashIcon style={{ width: 14, height: 14 }} />
</button> </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>
{!isReadOnly && (
<button className="btn ghost sm" style={{ fontSize: 12 }} onClick={() => openEdit()}> <button className="btn ghost sm" style={{ fontSize: 12 }} onClick={() => openEdit()}>
<PencilIcon style={{ width: 13, height: 13 }} /> ویرایش <PencilIcon style={{ width: 13, height: 13 }} /> ویرایش
</button> </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 }}>