uchill/front_material/components/profile/ProfilePaymentTab.tsx

213 lines
8.7 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.

'use client';
import { useState, useEffect, useCallback } from 'react';
import apiClient from '@/lib/api-client';
import { LoadingSpinner } from '@/components/common/LoadingSpinner';
import Link from 'next/link';
import { activateFreeSubscription, getActiveSubscription } from '@/api/subscriptions';
/** Подписи преимуществ из plan.features (API) */
const FEATURE_LABELS: Record<string, string> = {
video_calls: 'Видеозвонки',
screen_sharing: 'Демонстрация экрана',
whiteboard: 'Интерактивная доска',
homework: 'Домашние задания',
materials: 'Материалы',
analytics: 'Аналитика',
telegram_bot: 'Telegram-бот',
api_access: 'API',
};
/** Стандартные преимущества по типу тарифа (если API не вернул features) */
const DEFAULT_FEATURES: string[] = [
'Видеозвонки',
'Демонстрация экрана',
'Интерактивная доска',
'Домашние задания',
'Материалы',
'Аналитика',
'Telegram-бот',
];
function getBenefitList(plan: any): string[] {
const fromApi = plan.features && typeof plan.features === 'object'
? Object.entries(plan.features)
.filter(([, enabled]) => enabled)
.map(([key]) => FEATURE_LABELS[key] || key)
: [];
if (fromApi.length > 0) return fromApi;
// Fallback: все стандартные функции для месячных, для "за ученика" добавляем "Гибкая оплата"
if (plan.subscription_type === 'per_student') {
return ['Гибкая оплата за каждого ученика', ...DEFAULT_FEATURES];
}
return ['Безлимит учеников', ...DEFAULT_FEATURES];
}
function getPlanDescription(plan: any): string {
if (plan.description) return plan.description;
if (plan.subscription_type === 'per_student') {
return 'Оплата за каждого ученика. Гибкая система в зависимости от количества.';
}
return 'Ежемесячная подписка без ограничений по количеству учеников. Все функции доступны.';
}
function isFreePlan(plan: any): boolean {
const price = Number(plan.price) || 0;
const pricePerStudent = Number(plan.price_per_student) ?? 0;
if (plan.subscription_type === 'per_student') {
return pricePerStudent === 0;
}
return price === 0;
}
const CheckIcon = () => (
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden>
<path d="M16.7071 5.29289C17.0976 5.68342 17.0976 6.31658 16.7071 6.70711L8.70711 14.7071C8.31658 15.0976 7.68342 15.0976 7.29289 14.7071L3.29289 10.7071C2.90237 10.3166 2.90237 9.68342 3.29289 9.29289C3.68342 8.90237 4.31658 8.90237 4.70711 9.29289L8 12.5858L15.2929 5.29289C15.6834 4.90237 16.3166 4.90237 16.7071 5.29289Z" fill="currentColor" />
</svg>
);
export function ProfilePaymentTab() {
const [plans, setPlans] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [subscription, setSubscription] = useState<any>(null);
const [activatingPlanId, setActivatingPlanId] = useState<number | null>(null);
const [activateError, setActivateError] = useState<string | null>(null);
const loadData = useCallback(async () => {
try {
const [plansRes, subRes] = await Promise.all([
apiClient.get<any>('/subscriptions/plans/').then((r) => r.data?.results || r.data || []),
getActiveSubscription(),
]);
setPlans(Array.isArray(plansRes) ? plansRes : []);
setSubscription(subRes);
} catch {
setPlans([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadData();
}, [loadData]);
const handleActivateFree = async (plan: any) => {
setActivateError(null);
setActivatingPlanId(plan.id);
const body = {
plan_id: plan.id,
duration_days: 30,
student_count: plan.subscription_type === 'per_student' ? 1 : undefined,
};
if (typeof window !== 'undefined') {
console.log('[Subscription] Отправка запроса activate_free:', body);
}
try {
await activateFreeSubscription(body);
if (typeof window !== 'undefined') {
console.log('[Subscription] activate_free успешно');
}
await loadData();
} catch (err: any) {
if (typeof window !== 'undefined') {
console.log('[Subscription] activate_free ошибка:', err.response?.status, err.response?.data);
}
const data = err.response?.data;
const message =
(typeof data?.error === 'string' && data.error) ||
(typeof data?.detail === 'string' && data.detail) ||
(Array.isArray(data?.detail) ? data.detail[0] : null) ||
'Не удалось активировать подписку';
setActivateError(message);
} finally {
setActivatingPlanId(null);
}
};
if (loading) {
return <LoadingSpinner size="medium" />;
}
if (plans.length === 0) {
return (
<p style={{ color: 'var(--md-sys-color-on-surface-variant)', fontSize: 14 }}>
Нет доступных тарифов
</p>
);
}
return (
<div className="ios26-payment-tab">
{subscription && (
<div className="ios26-plan-card ios26-plan-card--current">
<span className="ios26-plan-card__overline">Текущая подписка</span>
<h3 className="ios26-plan-card__headline">{subscription.plan?.name || 'Активна'}</h3>
{subscription.end_date && (
<p className="ios26-plan-card__supporting">
До {new Date(subscription.end_date).toLocaleDateString('ru-RU')}
</p>
)}
</div>
)}
{activateError && (
<p style={{ color: 'var(--md-sys-color-error)', fontSize: 14, marginBottom: 12 }}>
{activateError}
</p>
)}
<span className="ios26-payment-tab__label">ТАРИФНЫЕ ПЛАНЫ</span>
<div className="ios26-plan-card-grid">
{plans.slice(0, 5).map((plan: any) => {
const benefits = getBenefitList(plan);
const description = getPlanDescription(plan);
const free = isFreePlan(plan);
const priceText = plan.price_per_student
? `${Math.round(plan.price_per_student || 0).toLocaleString('ru-RU')} ₽/уч.`
: `${Math.round(plan.price || 0).toLocaleString('ru-RU')} ₽/мес`;
return (
<article key={plan.id} className="ios26-plan-card ios26-plan-card--elevated">
<span className="ios26-plan-card__badge">{plan.name}</span>
<div className="ios26-plan-card__price-block">
<span className="ios26-plan-card__price-value">
{plan.price_per_student
? Math.round(plan.price_per_student || 0).toLocaleString('ru-RU')
: Math.round(plan.price || 0).toLocaleString('ru-RU')}
</span>
<span className="ios26-plan-card__price-period">
{plan.price_per_student ? 'за ученика' : '/мес'}
</span>
</div>
<p className="ios26-plan-card__supporting">{description}</p>
<ul className="ios26-plan-card__benefits" aria-label="Преимущества">
{benefits.slice(0, 5).map((label) => (
<li key={label} className="ios26-plan-card__benefit">
<span className="ios26-plan-card__benefit-icon"><CheckIcon /></span>
{label}
</li>
))}
</ul>
<div className="ios26-plan-card__actions">
{free ? (
<button
type="button"
className="ios26-plan-card__action"
onClick={() => handleActivateFree(plan)}
disabled={!!activatingPlanId}
style={{ cursor: activatingPlanId ? 'wait' : 'pointer' }}
>
{activatingPlanId === plan.id ? 'Активация...' : 'Активировать'}
</button>
) : (
<Link href="/payment" className="ios26-plan-card__action">
Подробнее и оплатить
</Link>
)}
</div>
</article>
);
})}
</div>
</div>
);
}