feat(subscription): implement feature gating for subscription-based access in admin panel

This commit is contained in:
hamed
2026-06-15 12:00:36 +03:30
parent 3d33cf09b8
commit fa6656df48
11 changed files with 534 additions and 26 deletions
+28 -18
View File
@@ -13,6 +13,7 @@ import {
DocumentTextIcon,
HeartIcon,
KeyIcon,
LockClosedIcon,
StarIcon,
TagIcon,
UserCircleIcon,
@@ -26,8 +27,9 @@ import {
import { NavLink, useNavigate } from "react-router-dom";
import { useAuthStore } from "../../stores/authStore";
import { useUiStore } from "../../stores/uiStore";
import { useSubscription } from "../../hooks/useSubscription";
type SectionItem = { to: string; icon: React.ElementType; label: string };
type SectionItem = { to: string; icon: React.ElementType; label: string; feature?: string };
type Section = { label: string; items: SectionItem[] };
function buildSections(primaryRole: string | null, dbUuid: string | null): Section[] {
@@ -87,11 +89,11 @@ function buildSections(primaryRole: string | null, dbUuid: string | null): Secti
label: 'مدیریت',
items: [
{ to: '/admin/appointments', icon: CalendarDaysIcon, label: 'نوبت‌ها' },
{ to: '/admin/my-patients', icon: FolderOpenIcon, label: 'پرونده بیماران' },
{ to: '/admin/my-patients', icon: FolderOpenIcon, label: 'پرونده بیماران', feature: 'patient_records' },
{ to: '/admin/staff', icon: UserPlusIcon, label: 'پرسنل' },
{ to: '/admin/my-secretaries', icon: IdentificationIcon, label: 'منشیان' },
{ to: '/admin/clinic-services', icon: WrenchScrewdriverIcon, label: 'سرویس‌ها' },
{ to: '/admin/sms-wallet', icon: DevicePhoneMobileIcon, label: 'کیف پول پیامک' },
{ to: '/admin/clinic-services', icon: WrenchScrewdriverIcon, label: 'سرویس‌ها', feature: 'services' },
{ to: '/admin/sms-wallet', icon: DevicePhoneMobileIcon, label: 'کیف پول پیامک', feature: 'sms_panel' },
],
},
{
@@ -121,11 +123,11 @@ function buildSections(primaryRole: string | null, dbUuid: string | null): Secti
label: 'مدیریت',
items: [
{ to: '/admin/appointments', icon: CalendarDaysIcon, label: 'نوبت‌های من' },
{ to: '/admin/my-patients', icon: FolderOpenIcon, label: 'پرونده بیماران' },
{ to: '/admin/my-patients', icon: FolderOpenIcon, label: 'پرونده بیماران', feature: 'patient_records' },
{ to: '/admin/staff', icon: UserPlusIcon, label: 'پرسنل' },
{ to: '/admin/my-secretaries', icon: IdentificationIcon, label: 'منشیان' },
{ to: '/admin/clinic-services', icon: WrenchScrewdriverIcon, label: 'سرویس‌ها' },
{ to: '/admin/sms-wallet', icon: DevicePhoneMobileIcon, label: 'کیف پول پیامک' },
{ to: '/admin/clinic-services', icon: WrenchScrewdriverIcon, label: 'سرویس‌ها', feature: 'services' },
{ to: '/admin/sms-wallet', icon: DevicePhoneMobileIcon, label: 'کیف پول پیامک', feature: 'sms_panel' },
],
},
{
@@ -149,6 +151,7 @@ function buildSections(primaryRole: string | null, dbUuid: string | null): Secti
label: 'مدیریت',
items: [
{ to: '/admin/appointments', icon: CalendarDaysIcon, label: 'نوبت‌ها' },
{ to: '/admin/my-patients', icon: FolderOpenIcon, label: 'پرونده بیماران', feature: 'patient_records' },
],
},
];
@@ -185,6 +188,7 @@ interface Props {
export default function Sidebar({ mobileOpen: _m, onMobileClose: _c }: Props) {
const sidebarOpen = useUiStore((s) => s.sidebarOpen);
const { logout, primaryRole, userName, availableContexts, dbUuid } = useAuthStore();
const { hasFeature } = useSubscription();
const navigate = useNavigate();
const sections = buildSections(primaryRole, dbUuid);
@@ -208,17 +212,23 @@ export default function Sidebar({ mobileOpen: _m, onMobileClose: _c }: Props) {
{sections.map((section) => (
<div className="nav-group" key={section.label}>
<span className="nav-label">{section.label}</span>
{section.items.map(({ to, icon: Icon, label }) => (
<NavLink
key={to}
to={to}
title={!sidebarOpen ? label : undefined}
className={({ isActive }) => `nav-item${isActive ? ' active' : ''}`}
>
<Icon style={{ width: 19, height: 19, flexShrink: 0 }} />
<span>{label}</span>
</NavLink>
))}
{section.items.map(({ to, icon: Icon, label, feature }) => {
const isLocked = feature ? !hasFeature(feature) : false;
const dest = isLocked ? '/admin/subscription' : to;
return (
<NavLink
key={to}
to={dest}
title={isLocked ? 'نیاز به ارتقاء پنل' : (!sidebarOpen ? label : undefined)}
className={({ isActive }) => `nav-item${isActive && !isLocked ? ' active' : ''}${isLocked ? ' locked' : ''}`}
style={isLocked ? { opacity: 0.55 } : undefined}
>
<Icon style={{ width: 19, height: 19, flexShrink: 0 }} />
<span>{label}</span>
{isLocked && <LockClosedIcon style={{ width: 12, height: 12, marginRight: 'auto', flexShrink: 0 }} />}
</NavLink>
);
})}
</div>
))}