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>
8.1 KiB
محدودسازی صفحات بیمه (قیمتگذاری و مطالبات) به خرید پلن اشتراکی
پروژه
clinicpro (admin frontend + UI ادمینِ پلن)
زمینه
پنل ادمین یک سیستم feature-gating اشتراکی از قبل دارد که کار میکند:
assets/admin/hooks/useSubscription.ts→GET /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 اشتراکی را روی دو صفحهی بیمه اضافه میکند.