feat: Implement secretary permissions enforcement across multiple resources
- Added SecretaryAccessChecker to manage resource access for secretaries. - Integrated permission checks for payments, inventory, and tags in relevant controllers. - Updated PaymentController and PaymentMethodController to enforce secretary permissions. - Enhanced TenantTagController to check permissions for tag management actions. - Introduced tests for secretary resource enforcement, ensuring proper access control. - Updated DoctorSecretary entity to include inventory and tags permissions. - Created a comprehensive audit document for secretary permissions coverage and enforcement. - Fixed potential crashes in SecretaryDashboard when rendering without doctor data.
This commit is contained in:
@@ -869,7 +869,9 @@ function DoctorDashboard() {
|
||||
// ── Secretary Dashboard ───────────────────────────────────────────────────
|
||||
|
||||
interface SecretaryDashboardData {
|
||||
doctor: { uuid: string; name: string; degree: string | null };
|
||||
scope: 'doctor' | 'clinic';
|
||||
doctor?: { uuid: string; name: string; degree: string | null };
|
||||
clinic?: { uuid: string; name: string };
|
||||
permissions: Record<string, unknown>;
|
||||
stats: { today_appointments: number; tomorrow_appointments: number };
|
||||
today_appointments: ApptRow[];
|
||||
@@ -888,20 +890,37 @@ function SecretaryDashboard() {
|
||||
|
||||
if (q.isLoading) return <LoadingSkeleton />;
|
||||
|
||||
if (q.isError || !d) {
|
||||
return (
|
||||
<div className="card card-pad" style={{ marginTop: 'var(--gap)', textAlign: 'center', padding: '2rem' }}>
|
||||
<UserIcon style={{ width: 40, height: 40, color: 'var(--text-3)', margin: '0 auto 1rem' }} />
|
||||
<p className="muted" style={{ fontSize: 13.5 }}>در حال حاضر اطلاعات داشبورد در دسترس نیست.</p>
|
||||
<button className="btn ghost sm" style={{ marginTop: 12 }} onClick={() => q.refetch()}>
|
||||
<ArrowPathIcon style={{ width: 14, height: 14 }} />
|
||||
تلاش دوباره
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// منشیِ scope=clinic فیلد doctor ندارد و scope=doctor فیلد clinic؛ نام محیط بر اساس scope.
|
||||
const scopeName = d.scope === 'clinic' ? (d.clinic?.name ?? '') : displayDoctorName(d.doctor?.name);
|
||||
const scopeLabel = d.scope === 'clinic' ? 'کلینیک' : 'مطب';
|
||||
|
||||
const kpiCards = [
|
||||
{ label: 'نوبتهای امروز', value: formatNumber(d?.stats.today_appointments ?? 0), icon: CalendarDaysIcon, color: 'var(--warning)', bg: 'var(--warning-bg)' },
|
||||
{ label: 'نوبتهای فردا', value: formatNumber(d?.stats.tomorrow_appointments ?? 0), icon: ClockIcon, color: 'var(--info)', bg: 'var(--info-bg)' },
|
||||
{ label: 'نوبتهای امروز', value: formatNumber(d.stats?.today_appointments ?? 0), icon: CalendarDaysIcon, color: 'var(--warning)', bg: 'var(--warning-bg)' },
|
||||
{ label: 'نوبتهای فردا', value: formatNumber(d.stats?.tomorrow_appointments ?? 0), icon: ClockIcon, color: 'var(--info)', bg: 'var(--info-bg)' },
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const canViewAppts = (d?.permissions as any)?.resources?.appointments?.view ?? false;
|
||||
const canViewAppts = (d.permissions as any)?.resources?.appointments?.view ?? false;
|
||||
|
||||
return (
|
||||
<div className="fade-in">
|
||||
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
||||
<div>
|
||||
<h1 className="section-title">داشبورد منشی</h1>
|
||||
<div className="muted" style={{ fontSize: 13, marginTop: 2 }}>{today} · منشی {displayDoctorName(d?.doctor.name)}</div>
|
||||
<div className="muted" style={{ fontSize: 13, marginTop: 2 }}>{today} · منشی {scopeLabel} {scopeName}</div>
|
||||
</div>
|
||||
<button className="btn ghost sm" onClick={() => q.refetch()}>
|
||||
<ArrowPathIcon style={{ width: 14, height: 14 }} />
|
||||
@@ -910,10 +929,10 @@ function SecretaryDashboard() {
|
||||
</div>
|
||||
|
||||
<div className="card card-pad" style={{ marginBottom: 'var(--gap)', display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<AvatarEl initials={(d?.doctor.name ?? 'D').slice(0, 1)} hue={256} size="lg" />
|
||||
<AvatarEl initials={(scopeName || 'D').slice(0, 1)} hue={256} size="lg" />
|
||||
<div>
|
||||
<div style={{ fontWeight: 700, fontSize: 16 }}>{displayDoctorName(d?.doctor.name) || '—'}</div>
|
||||
{d?.doctor.degree && <div className="muted" style={{ fontSize: 13, marginTop: 3 }}>{d.doctor.degree}</div>}
|
||||
<div style={{ fontWeight: 700, fontSize: 16 }}>{scopeName || '—'}</div>
|
||||
{d.scope === 'doctor' && d.doctor?.degree && <div className="muted" style={{ fontSize: 13, marginTop: 3 }}>{d.doctor.degree}</div>}
|
||||
</div>
|
||||
<div style={{ marginRight: 'auto', display: 'flex', gap: 8 }}>
|
||||
<span className={`badge ${canViewAppts ? 'green' : 'gray'}`}>
|
||||
|
||||
Reference in New Issue
Block a user