Files
clinicpro/assets/admin/pages/SelectContextPage.tsx
T
hamedandClaude Opus 5 1ce9957538 fix(admin): drop cached environment data when switching context
A doctor who also owns a clinic runs two environments, and a subscription
belongs to an environment, not to the user. The panel treated it as the
user's: the subscription query was keyed ['subscription-my'] with no
context, and switching environments never touched the react-query cache. So
upgrading the clinic left the personal practice showing the clinic's plan
with its feature-gated menu items unlocked, and vice versa.

The query key now carries the active dbUuid, and the context switch clears
the whole cache — every cached response belongs to the environment it was
fetched in, not just this one.

The API was already correct: DualEnvironmentSubscriptionTest pins that
granting one environment leaves the other on free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 22:15:56 +03:30

98 lines
3.8 KiB
TypeScript

import { useNavigate } from 'react-router';
import { useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useAuthStore, ContextItem } from '../stores/authStore';
const ROLE_LABELS: Record<string, string> = {
admin: 'مدیر کل',
clinic: 'مالک کلینیک',
doctor: 'پزشک',
secretary: 'منشی',
user: 'کاربر',
};
const TYPE_ICONS: Record<string, string> = {
doctor: '🏥',
clinic: '🏢',
};
export default function SelectContextPage() {
const { availableContexts, switchContext } = useAuthStore();
const navigate = useNavigate();
const qc = useQueryClient();
const [loading, setLoading] = useState<string | null>(null);
const handleSelect = async (ctx: ContextItem) => {
setLoading(ctx.db_uuid);
await switchContext(ctx.db_uuid);
// هر پاسخِ cache‌شده متعلق به محیط قبلی است — از اشتراک و پلن گرفته تا بیماران و
// نوبت‌ها. بدون پاک کردن، محیط تازه داده و دسترسی‌های محیط قبلی را نشان می‌دهد.
qc.clear();
navigate('/admin/dashboard', { replace: true });
};
return (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--surface-alt, #f4f6f9)',
padding: '2rem',
}}>
<div style={{ width: '100%', maxWidth: 480 }}>
<div className="card card-pad" style={{ textAlign: 'center' }}>
<h2 style={{ marginBottom: '.25rem', fontSize: '1.25rem', fontWeight: 600 }}>
انتخاب محیط کاری
</h2>
<p className="muted" style={{ marginBottom: '1.5rem', fontSize: '.875rem' }}>
لطفاً محیط کاری مورد نظر خود را انتخاب کنید
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '.75rem' }}>
{availableContexts.map((ctx) => (
<button
key={ctx.db_uuid}
onClick={() => handleSelect(ctx)}
disabled={loading !== null}
style={{
display: 'flex',
alignItems: 'center',
gap: '1rem',
padding: '.875rem 1rem',
border: '1.5px solid var(--border, #e2e8f0)',
borderRadius: '0.625rem',
background: loading === ctx.db_uuid ? 'var(--surface-alt, #f4f6f9)' : 'var(--surface)',
cursor: loading !== null ? 'wait' : 'pointer',
textAlign: 'right',
transition: 'border-color .15s, box-shadow .15s',
opacity: loading !== null && loading !== ctx.db_uuid ? 0.5 : 1,
}}
onMouseEnter={e => (e.currentTarget.style.borderColor = 'var(--primary, #6366f1)')}
onMouseLeave={e => (e.currentTarget.style.borderColor = 'var(--border, #e2e8f0)')}
>
<span style={{ fontSize: '1.5rem', lineHeight: 1 }}>
{TYPE_ICONS[ctx.type] ?? '👤'}
</span>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: '.9375rem', marginBottom: '.125rem' }}>
{ctx.name}
</div>
<div style={{ fontSize: '.8125rem' }}>
<span className="badge blue" style={{ fontSize: '.75rem' }}>
{ROLE_LABELS[ctx.role] ?? ctx.role}
</span>
</div>
</div>
{loading === ctx.db_uuid && (
<span className="skeleton" style={{ width: 20, height: 20, borderRadius: '50%' }} />
)}
</button>
))}
</div>
</div>
</div>
</div>
);
}