Files
clinicpro/.claude/prompt/insurance-pages-subscription-gating.md
hamedandClaude Opus 4.8 15e7d92fc1 feat(subscription): gate insurance pages behind "insurance" plan feature
Wrap InsurancePricingPage and ClaimsPage in <FeatureGate feature="insurance">
so direct-URL access is blocked without an active subscription that enables
the feature. Add feature: "insurance" to the sidebar links (doctor/clinic/
secretary sections) so the menu items hide when the plan lacks it.

Make AdminSubscriptionPage feature controls dynamic: derive the plan feature
checkboxes from FEATURE_LABELS (now including "insurance") and switch the Zod
schema to z.record, so adding a feature only touches the label map.

No backend/migration change: plan.features is free-form JSON; existing plans
default to insurance:false and admins enable it per plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 03:21:52 +03:30

8.1 KiB

محدودسازی صفحات بیمه (قیمت‌گذاری و مطالبات) به خرید پلن اشتراکی

پروژه

clinicpro (admin frontend + UI ادمینِ پلن)

زمینه

پنل ادمین یک سیستم feature-gating اشتراکی از قبل دارد که کار می‌کند:

  • assets/admin/hooks/useSubscription.tsGET /api/v1/subscription/my را می‌خواند و hasFeature(key) می‌دهد (از subscription.plan.features).
  • assets/admin/components/ui/FeatureGate.tsx → اگر feature فعال نباشد، به‌جای محتوا یک قفل + دکمه‌ی «مشاهده/ارتقاء پنل اشتراکی» نشان می‌دهد.
  • Sidebar.tsx → آیتم‌های منو با feature: "..." در صورت نبودِ feature مخفی می‌شوند (!hasFeature(feature)).

دو صفحه با همین الگو درست محدود شده‌اند: ClinicServicesPage با <FeatureGate feature="services"> و SmsWalletPage با <FeatureGate feature="sms_panel">.

مشکل / هدف

صفحات «قیمت‌گذاری بیمه» (/admin/insurance-pricing) و «مطالبات بیمه» (/admin/claims) به‌تازگی اضافه شده‌اند ولی هیچ محدودسازی اشتراکی ندارند — نه در سطح صفحه و نه در منو:

  • InsurancePricingPage.tsx و ClaimsPage.tsx در <FeatureGate> پیچیده نشده‌اند → باز کردن مستقیم URL، صفحه را بدون اشتراک نمایش می‌دهد.
  • در Sidebar.tsx لینک این دو صفحه برخلاف my-patients کلید feature: ندارند → همیشه در منو دیده می‌شوند (در حالی‌که my-patients با feature: "patient_records" مخفی می‌شود).

هدف: این دو صفحه فقط برای دارندگانِ پلن اشتراکی فعال که feature insurance را دارند باز باشند (کلید جدید، طبق تصمیم کاربر). دقیقاً مثل services / sms_panel.

فایل‌های مرتبط

فایل نقش
assets/admin/pages/InsurancePricingPage.tsx باید در <FeatureGate feature="insurance"> پیچیده شود
assets/admin/pages/ClaimsPage.tsx باید در <FeatureGate feature="insurance"> پیچیده شود
assets/admin/components/layout/Sidebar.tsx افزودن feature: "insurance" به لینک هر دو صفحه (دو نقش: doctor و clinic)
assets/admin/pages/AdminSubscriptionPage.tsx افزودن insurance به FEATURE_LABELS + planSchema.features تا ادمین بتواند این feature را روی پلن‌ها فعال کند
assets/admin/components/ui/FeatureGate.tsx / hooks/useSubscription.ts فقط مصرف؛ تغییر نمی‌کنند

وضعیت فعلی

الگوی درستِ موجود (برای تقلید)

// assets/admin/pages/SmsWalletPage.tsx (انتها)
return (
  <FeatureGate feature="sms_panel">
    ...
  </FeatureGate>
);

صفحات محافظت‌نشده

// assets/admin/pages/InsurancePricingPage.tsx — هیچ گاردی ندارد
export default function InsurancePricingPage() {
  return (
    <div className="fade-in">
      <PageHeader title="بیمه و قیمت‌گذاری" description="..." />
      <FreeVisitPrice />
      <TenantInsuranceContracts />
    </div>
  );
}
// assets/admin/pages/ClaimsPage.tsx — return اصلی بدون FeatureGate
export default function ClaimsPage() {
  // ... useQuery / useMutation ...
  return ( /* جدول مطالبات + بدهی بیمه */ );
}

Sidebar — لینک‌ها بدون feature

// assets/admin/components/layout/Sidebar.tsx (نقش doctor و مشابهش در clinic)
{ to: "/admin/my-patients",       icon: FolderOpenIcon,  label: "پرونده بیماران", feature: "patient_records" },
{ to: "/admin/insurance-pricing", icon: ShieldCheckIcon, label: "قیمت‌گذاری بیمه" },   // ← feature ندارد
{ to: "/admin/claims",            icon: DocumentTextIcon, label: "مطالبات بیمه" },      // ← feature ندارد

AdminSubscriptionPage — featureهای هاردکد (فقط ۳ تا)

// assets/admin/pages/AdminSubscriptionPage.tsx
const FEATURE_LABELS: Record<string, string> = {
  patient_records: 'پرونده بیمار',
  services:        'سرویس‌ها',
  sms_panel:       'پنل پیامک',
};

const planSchema = z.object({
  ...
  features: z.object({
    patient_records: z.boolean(),
    services:        z.boolean(),
    sms_panel:       z.boolean(),
  }),
  ...
});

وظایف

۱. پیچیدن دو صفحه در FeatureGate

InsurancePricingPage و ClaimsPage را عیناً مثل SmsWalletPage/ClinicServicesPage در <FeatureGate feature="insurance"> بپیچ:

// InsurancePricingPage.tsx
import FeatureGate from '../components/ui/FeatureGate';

export default function InsurancePricingPage() {
  return (
    <FeatureGate feature="insurance">
      <div className="fade-in">
        <PageHeader title="بیمه و قیمت‌گذاری" description="..." />
        <FreeVisitPrice />
        <TenantInsuranceContracts />
      </div>
    </FeatureGate>
  );
}

برای ClaimsPage کل JSX داخل return ( ... ) را در <FeatureGate feature="insurance">...</FeatureGate> بگذار. (اگر pageدارای چند return شرطی است، فقط return نهاییِ محتوا را بپیچ؛ مراقب باش hookها بالای FeatureGate صدا زده شوند تا ترتیب hookها نشکند.)

۲. افزودن feature به لینک‌های Sidebar

به لینک‌های insurance-pricing و claims در هر دو بخش نقش (doctor و clinic) کلید feature: "insurance" اضافه کن تا وقتی پلن این feature را ندارد، از منو مخفی شوند (همان رفتار my-patients).

۳. افزودن insurance به UI ادمینِ پلن

تا ادمین بتواند feature insurance را روی پلن‌ها روشن/خاموش کند:

// AdminSubscriptionPage.tsx
const FEATURE_LABELS: Record<string, string> = {
  patient_records: 'پرونده بیمار',
  services:        'سرویس‌ها',
  sms_panel:       'پنل پیامک',
  insurance:       'بیمه و مطالبات',
};

و planSchema.features و defaultValues و openEditPlan را طوری اصلاح کن که insurance را هم شامل شود. (بهترین کار: featureها را از روی کلیدهای FEATURE_LABELS بساز تا دیگر هاردکد نباشد و افزودن feature بعدی فقط map را تغییر دهد.)

نکات مهم

  • backend نیازی به تغییر ندارد: SubscriptionPlan.features یک json آزاد است و hasFeature($key) هر کلیدی را می‌خواند؛ پلن‌های موجود به‌صورت پیش‌فرض insurance ندارند (یعنی false) و ادمین آن را روی پلن دلخواه فعال می‌کند. پس migration لازم نیست و قرارداد API تغییر نمی‌کند → docs نیاز به‌روزرسانی ندارد.
  • این محدودسازی فقط برای نقش‌هایی که useSubscription فعال دارد عمل می‌کند (doctor / clinic / secretary). برای admin و representation صفحات بیمه در منوی آن‌ها نیست؛ کاری لازم نیست.
  • ترتیب hookها: FeatureGate نباید باعث شود hookهای صفحه به‌صورت شرطی صدا زده شوند. در ClaimsPage همه‌ی useQuery/useMutation/useState بالای return می‌مانند و فقط JSX خروجی wrap می‌شود.
  • بعد از تغییرات: ddev exec yarn dev و ddev exec npx tsc --noEmit --project tsconfig.json برای چک تایپ.
  • گاردهای نقش (RoleRoute) را دست نزن — درست‌اند؛ این پرامپت فقط لایه‌ی feature اشتراکی را روی دو صفحه‌ی بیمه اضافه می‌کند.