feat(subscription): implement feature gating for subscription-based access in admin panel
This commit is contained in:
@@ -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>
|
||||
))}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { LockClosedIcon } from '@heroicons/react/24/outline';
|
||||
import { useSubscription } from '../../hooks/useSubscription';
|
||||
|
||||
interface FeatureGateProps {
|
||||
feature: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function FeatureGate({ feature, children }: FeatureGateProps) {
|
||||
const { hasFeature, hasPlan } = useSubscription();
|
||||
|
||||
if (hasFeature(feature)) return <>{children}</>;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
minHeight: 320, gap: 16, padding: 40, textAlign: 'center',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 64, height: 64, borderRadius: '50%',
|
||||
background: 'oklch(0.97 0.01 256)',
|
||||
display: 'grid', placeItems: 'center',
|
||||
border: '1px solid var(--border)',
|
||||
}}>
|
||||
<LockClosedIcon style={{ width: 28, color: 'var(--text-3)' }} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700, fontSize: 16, marginBottom: 6 }}>
|
||||
این قابلیت در پنل فعلی شما فعال نیست
|
||||
</div>
|
||||
<div style={{ color: 'var(--text-3)', fontSize: 13.5, maxWidth: 360, lineHeight: 1.6 }}>
|
||||
{hasPlan
|
||||
? 'برای استفاده از این قابلیت پنل خود را ارتقاء دهید'
|
||||
: 'برای استفاده از این قابلیت یک پنل اشتراکی فعال کنید'}
|
||||
</div>
|
||||
</div>
|
||||
<Link to="/admin/subscription" className="btn primary sm" style={{ textDecoration: 'none' }}>
|
||||
{hasPlan ? 'ارتقاء پنل' : 'مشاهده پنلهای اشتراکی'}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user