Files
clinicpro/assets/admin/pages/SubscriptionPage.tsx
T
hamed 55ab2f5dfc Implement comprehensive dark/light mode overhaul for admin panel
- Refactor color palette in `ui-design-spec.md` to utilize CSS variables exclusively, eliminating fixed hex values and Tailwind utility classes.
- Complete dark mode implementation in `uiStore.ts`, ensuring proper theme application via `applyTheme()` and `applyBrand()`.
- Create `admin-theme-dark-light-audit.md` to document the transition process, outlining issues with inline styles and fixed colors.
- Introduce `theme-tokens.test.ts` to enforce rules against fixed hex colors and ensure compliance with the design system.
- Update various components and styles to replace inline styles and fixed colors with CSS variables, ensuring consistent theming across light and dark modes.
- Ensure all changes maintain visual integrity in both light and dark modes, with a focus on accessibility and contrast standards.
2026-07-27 16:41:52 +03:30

554 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { CreditCardIcon } from '@heroicons/react/24/outline';
import { CheckCircleIcon } from '@heroicons/react/24/solid';
import { toast } from 'sonner';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import type { SubscriptionPlan, MySubscriptionData, SubscriptionPeriod } from '../types';
import { usePaymentConfig } from '../hooks/usePaymentConfig';
import { formatRial, formatNumber, formatDate } from '../lib/utils';
import Modal from '../components/ui/Modal';
import SettingsLayout from '../components/layout/SettingsLayout';
import {
SparkleIcon, PlanCardPerson, CloseCircleFilled, GiftIcon, InfoCircleRedFilled,
} from './subscriptionIcons';
// ── Constants ─────────────────────────────────────────────────────────────
/** Shared feature labels (also consumed by PaymentSuccessPage). */
export const PLAN_FEATURE_LABELS: Record<string, string> = {
patient_records: 'پرونده بیمار',
services: 'مدیریت سرویس‌ها',
sms_panel: 'پنل پیامک',
insurance: 'مدیریت بیمه',
};
/** Feature labels used on the plan cards — wording copied from clinic-pro-tauri. */
const CARD_FEATURE_LABELS: Record<string, string> = {
patient_records: 'مدیریت پرونده بیمار',
services: 'مدیریت سرویس ها',
sms_panel: 'پنل پیامک',
insurance: 'مدیریت بیمه',
};
/** The plan highlighted as "most popular" in the UI (no backend flag exists);
* matches clinic-pro-tauri, which flags the professional (top) plan. */
const POPULAR_PLAN_NAME = 'professional';
const PLAN_META: Record<string, { label: string; desc: string; tint: string }> = {
free: { label: 'رایگان', desc: 'مناسب برای شروع کار', tint: 'var(--surface-2)' },
basic: { label: 'پایه', desc: 'مناسب برای مطب‌های کوچک و متوسط', tint: 'var(--primary-soft)' },
professional: { label: 'حرفه‌ای', desc: 'مناسب برای کلینیک‌های بزرگ', tint: 'var(--violet-bg)' },
};
/** Gradient blur behind each plan card — matches the tauri source per plan tier. */
const PLAN_GRADIENT: Record<string, string> = {
free: 'radial-gradient(circle, var(--violet-bg) 0%, var(--primary-soft) 70%, transparent 100%)',
basic: 'radial-gradient(circle, var(--success-bg) 0%, var(--violet-bg) 60%, transparent 100%)',
professional: 'linear-gradient(180deg, var(--warning-bg) 0%, var(--primary-soft) 0%, transparent 100%)',
};
export const planMetaOf = (name: string) => PLAN_META[name] ?? PLAN_META.free;
// ── Main Page ─────────────────────────────────────────────────────────────
export default function SubscriptionPage() {
const qc = useQueryClient();
const [purchaseTarget, setPurchaseTarget] = useState<{ period: SubscriptionPeriod; planName: string } | null>(null);
const [selectedGateway, setSelectedGateway] = useState<string>('');
const { data: plansData, isLoading: plansLoading } = useQuery<ApiResponse<SubscriptionPlan[]>>({
queryKey: ['subscription-plans'],
queryFn: () => api.get('/api/v1/subscription/plans'),
});
const { data: myData, isLoading: myLoading } = useQuery<ApiResponse<MySubscriptionData>>({
queryKey: ['subscription-my'],
queryFn: () => api.get('/api/v1/subscription/my'),
});
const { isTestMode, gateways } = usePaymentConfig();
useEffect(() => {
if (gateways.length > 0 && !gateways.some((g) => g.name === selectedGateway)) {
setSelectedGateway(gateways[0].name);
}
}, [gateways, selectedGateway]);
// Highest plan first so, under RTL, the top-tier plan sits on the right (source order).
const plans = [...(plansData?.data ?? [])].sort((a, b) => b.level - a.level);
const myRaw = myData?.data;
const my = myRaw?.subscription ?? null;
const usedTrial = myRaw?.used_trial ?? false;
const trialMutation = useMutation({
mutationFn: () => api.post('/api/v1/subscription/trial', {}),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['subscription-my'] });
toast.success('تریال رایگان با موفقیت فعال شد');
},
onError: (err: any) => toast.error(err.message),
});
const purchaseMutation = useMutation({
mutationFn: ({ period_uuid, gateway, amount_rials }: { period_uuid: string; gateway: string; amount_rials: number }) =>
api.post<{ data: { pay_url?: string; redirect_url?: string; payment_url?: string } }>('/api/v1/subscription-payment', {
period_uuid, gateway, amount_rials,
// Gateway returns here (with ?payment_uuid&status); the success page reads them.
frontend_address: `${window.location.origin}/admin/subscription/success`,
}),
onSuccess: (res: any) => {
const url = res?.data?.pay_url ?? res?.data?.redirect_url ?? res?.data?.payment_url;
if (url) window.location.href = url;
else toast.error('خطا در دریافت لینک پرداخت');
},
onError: (err: any) => toast.error(err.message),
});
// Open the payment modal for the current plan's best-value paid period (renew).
const renewCurrentPlan = () => {
const current = plans.find((p) => p.name === my?.plan.name);
const paid = (current?.periods ?? []).filter((p) => !p.is_trial)
.sort((a, b) => a.duration_months - b.duration_months);
const period = paid[paid.length - 1];
if (period) setPurchaseTarget({ period, planName: current!.name });
};
return (
<>
<SettingsLayout active="subscription">
<div
style={{ background: 'var(--surface)', minWidth: 0 }}
className="px-4 py-4 md:px-6 md:py-6 rounded-[var(--r-lg)]"
>
<div dir="ltr" style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 32, paddingTop: 8 }}>
{/* ── Header: current plan (left) + title (right) ── */}
<div style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center', gap: 16, flexWrap: 'wrap' }}>
{!myLoading && <CurrentPlanCard my={my} onRenew={renewCurrentPlan} />}
<div>
<div style={{ fontSize: 22, fontWeight: 800, color: 'var(--text)' }}>انتخاب پلن اشتراک</div>
<div style={{ fontSize: 14, fontWeight: 500, color: 'var(--text-3)', marginTop: 4, textAlign: 'left' }}>
پلن اشتراک مناسب خود را انتخاب نمایید:
</div>
</div>
</div>
{/* ── Plan cards ── */}
{plansLoading ? (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 16, width: '100%' }}>
{[1, 2, 3].map((i) => (
<div key={i} className="skeleton" style={{ height: 522, borderRadius: 10 }} />
))}
</div>
) : plans.length === 0 ? (
<div style={{ padding: '48px 0', textAlign: 'center', color: 'var(--text-3)', fontSize: 14 }}>
در حال حاضر پلنی برای نمایش وجود ندارد.
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 16, width: '100%' }}>
{plans.map((plan) => (
<PlanCard
key={plan.uuid}
plan={plan}
currentPlanName={my?.plan.name ?? 'free'}
currentPlanLevel={my?.plan.level ?? 0}
currentPlanActive={!!my && (my.days_remaining ?? 0) > 0}
usedTrial={usedTrial}
onPurchase={(period) => setPurchaseTarget({ period, planName: plan.name })}
onTrial={() => trialMutation.mutate()}
trialPending={trialMutation.isPending}
/>
))}
</div>
)}
</div>
</div>
</SettingsLayout>
{/* ── Payment modal (gateway selection) ── */}
<Modal
open={!!purchaseTarget}
onClose={() => { setPurchaseTarget(null); setSelectedGateway(gateways[0]?.name ?? ''); }}
title="پرداخت اشتراک"
size="sm"
>
{purchaseTarget && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<div style={{
background: 'var(--surface-2)', borderRadius: 'var(--r-sm)',
padding: '14px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
}}>
<div>
<div style={{ fontSize: 13, color: 'var(--text-3)', marginBottom: 2 }}>دوره انتخابی</div>
<div style={{ fontWeight: 700, fontSize: 15 }}>
{planMetaOf(purchaseTarget.planName).label} {purchaseTarget.period.label}
</div>
<div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 1 }}>
{formatNumber(purchaseTarget.period.duration_months)} ماه
</div>
</div>
<div style={{ textAlign: 'left' }}>
<div style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 2 }}>مبلغ قابل پرداخت</div>
<div style={{ fontWeight: 800, fontSize: 20, color: 'var(--primary)' }}>
{formatRial(purchaseTarget.period.price_rials)}
</div>
</div>
</div>
{isTestMode ? (
<div style={{
background: 'var(--warning-bg)', border: '1px solid var(--warning)',
borderRadius: 10, padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 10,
}}>
<span style={{ fontSize: 20 }}>⚠️</span>
<div>
<div style={{ fontWeight: 700, fontSize: 13.5, color: 'var(--warning)' }}>درگاه آزمایشی فعال است</div>
<div style={{ fontSize: 12, color: 'var(--warning)', marginTop: 2, opacity: 0.8 }}>پول واقعی کسر نخواهد شد این تراکنش آزمایشی است</div>
</div>
</div>
) : gateways.length === 0 ? (
<div style={{
background: 'var(--danger-bg)', border: '1px solid var(--danger)',
borderRadius: 10, padding: '12px 16px', fontSize: 13, color: 'var(--danger)',
}}>
در حال حاضر هیچ درگاه پرداخت فعالی وجود ندارد. لطفاً با پشتیبانی تماس بگیرید.
</div>
) : (
<div>
<div style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--text-2)', marginBottom: 10 }}>
انتخاب درگاه پرداخت
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
{gateways.map((gw) => (
<button
key={gw.name}
onClick={() => setSelectedGateway(gw.name)}
style={{
padding: '12px 16px', borderRadius: 'var(--r-sm)', cursor: 'pointer',
border: `2px solid ${selectedGateway === gw.name ? 'var(--primary)' : 'var(--border)'}`,
background: selectedGateway === gw.name ? 'var(--primary-soft)' : 'var(--surface)',
color: selectedGateway === gw.name ? 'var(--primary-700)' : 'var(--text-2)',
fontWeight: selectedGateway === gw.name ? 700 : 500,
fontSize: 13, transition: '.14s', fontFamily: 'inherit',
display: 'flex', alignItems: 'center', gap: 8,
}}
>
<CreditCardIcon style={{ width: 17, flexShrink: 0 }} />
{gw.label}
</button>
))}
</div>
</div>
)}
<div style={{ display: 'flex', gap: 10, marginTop: 4 }}>
<button
className="btn primary"
style={{ flex: 1, height: 44 }}
disabled={purchaseMutation.isPending || (!isTestMode && (gateways.length === 0 || selectedGateway === ''))}
onClick={() =>
purchaseMutation.mutate({ period_uuid: purchaseTarget.period.uuid, gateway: selectedGateway, amount_rials: purchaseTarget.period.price_rials })
}
>
{purchaseMutation.isPending
? 'در حال انتقال...'
: `پرداخت ${formatRial(purchaseTarget.period.price_rials)}`}
</button>
<button className="btn ghost" style={{ height: 44 }} onClick={() => setPurchaseTarget(null)}>
انصراف
</button>
</div>
</div>
)}
</Modal>
</>
);
}
// ── Current plan card (365×104, ported from tauri CurrentPlanCard.jsx) ───────
function CurrentPlanCard({ my, onRenew }: { my: MySubscriptionData['subscription']; onRenew: () => void }) {
const daysLeft = my?.days_remaining ?? 0;
const expiresAt = my?.expires_at ?? null;
return (
<div
dir="rtl"
style={{
width: 365, maxWidth: '100%', minHeight: 104,
background: 'var(--surface-2)', border: '1px solid var(--border)',
borderRadius: 16, display: 'flex', flexDirection: 'column', justifyContent: 'center',
padding: '0 16px',
}}
>
{my ? (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-2)' }}>پلن فعلی شما:</span>
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>{planMetaOf(my.plan.name).label}</span>
</div>
{expiresAt ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 4 }}>
<span style={{ fontSize: 12, fontWeight: 500, color: 'var(--text-3)' }}>زمان پایان اشتراک:</span>
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-2)' }}>{formatDate(expiresAt)}</span>
{daysLeft > 0 && (
<span style={{ fontSize: 12, fontWeight: 500, color: 'var(--text-3)' }}>
({formatNumber(daysLeft)} روز مانده)
</span>
)}
</div>
) : (
<div style={{ marginTop: 4 }}>
<span style={{ fontSize: 12, fontWeight: 500, color: 'var(--text-3)' }}>بدون تاریخ انقضا</span>
</div>
)}
{daysLeft < 3 && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 4 }}>
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<InfoCircleRedFilled color="var(--danger)" />
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
{daysLeft <= 0 ? 'اشتراک شما به پایان رسیده است' : `${formatNumber(daysLeft)} روز تا پایان اشتراک`}
</span>
</div>
<button
onClick={onRenew}
className="btn ghost sm"
style={{ fontWeight: 600, color: 'var(--primary)', minWidth: 0, padding: '4px 12px' }}
>
تمدید اشتراک
</button>
</div>
)}
</>
) : (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-2)' }}>پلن فعلی شما:</span>
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>اشتراک ندارید</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', marginTop: 4 }}>
<span style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-3)' }}>
برای استفاده از امکانات یک پلن تهیه کنید
</span>
</div>
</>
)}
</div>
);
}
// ── PlanCard (522px, ported from tauri PlanCard.jsx) ─────────────────────────
function PlanCard({
plan, currentPlanName, currentPlanLevel, currentPlanActive,
usedTrial, onPurchase, onTrial, trialPending,
}: {
plan: SubscriptionPlan;
currentPlanName: string;
currentPlanLevel: number;
currentPlanActive: boolean;
usedTrial: boolean;
onPurchase: (period: SubscriptionPeriod) => void;
onTrial: () => void;
trialPending: boolean;
}) {
const meta = planMetaOf(plan.name);
const isCurrent = plan.name === currentPlanName;
const isPopular = plan.name === POPULAR_PLAN_NAME;
const isFree = plan.level <= 0;
const isDowngrade = currentPlanActive && (plan.level ?? 0) < currentPlanLevel;
const paidPeriods = useMemo(
() => [...plan.periods].filter((p) => !p.is_trial).sort((a, b) => a.duration_months - b.duration_months),
[plan.periods],
);
const trialPeriod = plan.periods.find((p) => p.is_trial);
// Default to the longest (best-value) period, matching the source default.
const [selectedUuid, setSelectedUuid] = useState<string>('');
useEffect(() => {
if (paidPeriods.length > 0 && !paidPeriods.some((p) => p.uuid === selectedUuid)) {
setSelectedUuid(paidPeriods[paidPeriods.length - 1].uuid);
}
}, [paidPeriods, selectedUuid]);
const selectedPeriod = paidPeriods.find((p) => p.uuid === selectedUuid) ?? paidPeriods[0];
const isSelected = isPopular; // the source highlights the popular card with the indigo border
return (
<div
data-testid={`plan-card-${plan.name}`}
style={{
width: '100%', minWidth: 0, height: 522, borderRadius: 10,
border: isSelected ? '2px solid var(--primary-600)' : '2px solid var(--border)',
background: 'var(--surface)', display: 'flex', flexDirection: 'column',
padding: 16, position: 'relative', overflow: 'hidden',
}}
>
{/* Gradient blur backdrop */}
<div style={{
position: 'absolute', top: -80, left: 0, width: 550, height: 160,
background: PLAN_GRADIENT[plan.name] ?? PLAN_GRADIENT.free,
opacity: 0.8, filter: 'blur(80px)', zIndex: 0, pointerEvents: 'none',
}} />
<div style={{ position: 'relative', zIndex: 1 }}>
{/* Popular badge (top-right corner) */}
{isPopular && (
<div style={{
position: 'absolute', width: 'fit-content', height: 32, top: -18, right: -20,
background: 'var(--primary-700)', color: 'var(--on-primary)', padding: '0 16px', borderRadius: '0 10px 0 10px',
fontSize: 13, fontWeight: 500, display: 'flex', alignItems: 'center', gap: 8, zIndex: 1,
}}>
محبوب‌ترین
<SparkleIcon size={15} color="var(--warning)" />
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ fontSize: 20, fontWeight: 800, color: 'var(--text)', textAlign: 'left' }}>
پلن {meta.label}
</div>
<div style={{ fontSize: 14, color: 'var(--text-2)', fontWeight: 500, textAlign: 'left', lineHeight: 1.6 }}>
{meta.desc}
</div>
{/* Secretaries pill */}
<div style={{ width: '100%', display: 'flex', justifyContent: 'flex-end' }}>
<div dir="rtl" style={{
minWidth: 86, height: 32, borderRadius: 29, background: 'var(--surface-3)',
color: 'var(--text)', display: 'flex', gap: 6, alignItems: 'center', justifyContent: 'center',
padding: '0 12px',
}}>
<span style={{ fontSize: 14, fontWeight: 500, whiteSpace: 'nowrap' }}>
{formatNumber(plan.max_secretaries)} منشی
</span>
<PlanCardPerson size={18} />
</div>
</div>
{/* Period toggle */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', minHeight: 48 }}>
{paidPeriods.length > 0 && (
<div role="tablist" aria-label="دوره اشتراک" style={{
width: 263, maxWidth: '100%', height: 48, border: '1px solid var(--border)',
borderRadius: 8, background: 'var(--surface)', display: 'flex', alignItems: 'center', overflow: 'hidden',
}}>
{paidPeriods.map((p, idx) => {
const on = p.uuid === selectedPeriod?.uuid;
return (
<React.Fragment key={p.uuid}>
<button
role="tab" aria-selected={on}
onClick={() => setSelectedUuid(p.uuid)}
style={{
flex: 1, height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center',
background: on ? 'var(--primary-soft)' : 'transparent', cursor: 'pointer',
border: 'none', fontFamily: 'inherit',
fontSize: 14, fontWeight: 500, color: on ? 'var(--primary)' : 'var(--text)',
}}
>
{p.label}
</button>
{idx < paidPeriods.length - 1 && (
<div style={{ width: 1, alignSelf: 'stretch', background: 'var(--border)' }} />
)}
</React.Fragment>
);
})}
</div>
)}
</div>
{/* Price */}
<div style={{ minHeight: 22 }}>
{!isFree && selectedPeriod && (
<div style={{
marginBottom: 8, display: 'flex', alignItems: 'center', gap: 4,
justifyContent: 'flex-end', width: '100%',
}}>
<span style={{ fontSize: 14, color: 'var(--text)', fontWeight: 600, direction: 'ltr' }}>
{formatRial(selectedPeriod.price_rials)}
</span>
<span style={{ fontSize: 14, color: 'var(--text)', fontWeight: 500 }}>: قیمت</span>
</div>
)}
</div>
</div>
</div>
<div style={{ borderTop: '1px solid var(--border)' }} />
{/* Features */}
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
{Object.entries(plan.features).map(([key, enabled]) => {
const label = CARD_FEATURE_LABELS[key] ?? PLAN_FEATURE_LABELS[key] ?? key;
return (
<div key={key} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', padding: '9.6px 0', gap: 8 }}>
<span style={{ fontSize: 14, color: enabled ? 'var(--text)' : 'var(--text-3)', fontWeight: 500, textAlign: 'left' }}>
{label}
</span>
<span style={{ position: 'relative', display: 'inline-flex', width: 22, height: 22 }}>
{enabled
? <CheckCircleIcon style={{ width: 22, height: 22, color: 'var(--success)' }} />
: <CloseCircleFilled size={22} color="var(--border-2)" />}
</span>
</div>
);
})}
</div>
{/* Trial box (dashed) */}
{!isFree && trialPeriod && !usedTrial && !isDowngrade && (
<div style={{ position: 'relative', width: 268, maxWidth: '100%', height: 59, margin: '0 auto 16px' }}>
<div style={{
position: 'relative', zIndex: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between',
height: '100%', padding: '0 16px',
}}>
<button
onClick={onTrial} disabled={trialPending}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 16, fontWeight: 500, color: 'var(--primary)' }}
>
{trialPending ? 'در حال فعال‌سازی...' : 'فعال سازی'}
</button>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text-2)' }}>
تریال {formatNumber(trialPeriod.duration_months)} ماهه رایگان
</span>
<GiftIcon />
</div>
</div>
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none' }} viewBox="0 0 268 59" preserveAspectRatio="none">
<rect x="0.5" y="0.5" width="267" height="58" rx="6" fill="transparent" strokeWidth="1" strokeDasharray="6 4" style={{ stroke: 'var(--primary)' }}/>
</svg>
</div>
)}
<div style={{ borderTop: '1px solid var(--border)', margin: '0 8px 16px' }} />
{/* Buy button */}
{isFree ? (
<button className="btn" style={{ marginTop: 'auto', width: '100%', height: 48, cursor: 'default' }} disabled>
{isCurrent ? 'پلن فعلی شما' : 'رایگان'}
</button>
) : isDowngrade ? (
<button className="btn" style={{ marginTop: 'auto', width: '100%', height: 48, opacity: 0.6, cursor: 'not-allowed' }} disabled>
تا پایان اشتراک فعلی قابل انتخاب نیست
</button>
) : (
<button
onClick={() => selectedPeriod && onPurchase(selectedPeriod)}
disabled={!selectedPeriod}
style={{
marginTop: 'auto', width: '100%', height: 48, borderRadius: 4, border: 'none',
background: 'var(--primary)', color: 'var(--on-primary)', fontWeight: 700, fontSize: 16,
fontFamily: 'inherit', cursor: selectedPeriod ? 'pointer' : 'not-allowed',
}}
>
{isCurrent ? 'تمدید اشتراک' : 'خرید اشتراک'}
</button>
)}
</div>
);
}