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>
This commit is contained in:
hamed
2026-06-24 03:21:52 +03:30
co-authored by Claude Opus 4.8
parent fc87e4a04e
commit 15e7d92fc1
5 changed files with 181 additions and 16 deletions
+10 -8
View File
@@ -34,11 +34,7 @@ const planSchema = z.object({
name: z.string().min(1, 'نام الزامی است'),
level: z.coerce.number().min(0),
max_secretaries: z.coerce.number().min(1),
features: z.object({
patient_records: z.boolean(),
services: z.boolean(),
sms_panel: z.boolean(),
}),
features: z.record(z.string(), z.boolean()),
active: z.boolean(),
});
type PlanForm = z.infer<typeof planSchema>;
@@ -60,8 +56,14 @@ const FEATURE_LABELS: Record<string, string> = {
patient_records: 'پرونده بیمار',
services: 'سرویس‌ها',
sms_panel: 'پنل پیامک',
insurance: 'بیمه و مطالبات',
};
const FEATURE_KEYS = Object.keys(FEATURE_LABELS);
const emptyFeatures = (): Record<string, boolean> =>
Object.fromEntries(FEATURE_KEYS.map((k) => [k, false]));
const PLAN_DISPLAY: Record<string, string> = {
free: 'رایگان',
basic: 'پایه',
@@ -86,7 +88,7 @@ function PlansTab() {
const planForm = useForm<PlanForm>({
resolver: zodResolver(planSchema),
defaultValues: { features: { patient_records: false, services: false, sms_panel: false }, active: true },
defaultValues: { features: emptyFeatures(), active: true },
});
const periodForm = useForm<PeriodForm>({
@@ -129,7 +131,7 @@ function PlansTab() {
name: plan.name,
level: plan.level,
max_secretaries: plan.max_secretaries,
features: { patient_records: plan.features.patient_records ?? false, services: plan.features.services ?? false, sms_panel: plan.features.sms_panel ?? false },
features: { ...emptyFeatures(), ...plan.features },
active: (plan as any).active ?? true,
});
setPlanModal(plan);
@@ -157,7 +159,7 @@ function PlansTab() {
return (
<>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
<button className="btn primary sm" onClick={() => { planForm.reset({ features: { patient_records: false, services: false, sms_panel: false }, active: true, level: 0, max_secretaries: 1 }); setPlanModal('create'); }}>
<button className="btn primary sm" onClick={() => { planForm.reset({ features: emptyFeatures(), active: true, level: 0, max_secretaries: 1 }); setPlanModal('create'); }}>
<PlusIcon style={{ width: 16 }} /> پنل جدید
</button>
</div>