// ════════════════════════════════════════════════════════════════ // /account/billing — детальная панель подписки пользователя // usage progress + packs + history // ════════════════════════════════════════════════════════════════ function BillingPage({ currentUser, onChangeRoute, onContactUpgrade }) { const B = window.SDH_BILLING; const sub = B.getSub(currentUser.id); const tier = sub ? B.getTier(sub.tierCode) : null; const usage = sub ? B.buildUsage(currentUser.id, sub.tierCode) : null; const packs = B.userPacks[currentUser.id] || []; const userPayments = B.payments.filter(p => p.userId === currentUser.id).sort((a, b) => new Date(b.date) - new Date(a.date)); if (!sub || !tier) { return (
); } const daysLeft = sub.expiresAt ? Math.round((new Date(sub.expiresAt) - new Date('2026-05-23T10:00:00Z')) / 86400000) : null; return (
onChangeRoute('account')}> В профиль } />
{/* Active subscription hero */}
{tier.name} {sub.isInternal && internal} активна

{sub.isInternal ? Внутренний tier команды : `${tier.priceRub.toLocaleString('ru-RU')} ₽/мес`}

{sub.isInternal ? 'Бессрочный доступ ко всем функциям без оплаты. Назначается super-admin вручную членам команды Symbiosis Lab.' : `Активна до ${new Date(sub.expiresAt).toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' })} · ${sub.autoRenew ? 'автопродление включено' : 'автопродление отключено'}`}

{!sub.isInternal && (
осталось
{daysLeft}
дней до продления
)}
{/* Usage section */} {tier.tokens > 0 && (

использование в этом периоде

{new Date(usage.periodStartedAt).toLocaleDateString('ru-RU')} → {new Date(usage.periodEndsAt).toLocaleDateString('ru-RU')}
usage.tokensIncluded ? 'rose' : 'amber'} suffix={usage.forecast > usage.tokensIncluded ? 'превышение' : 'в рамках'} />

на что ушло

)} {/* Packs */}

доп. пакеты токенов

{packs.length === 0 ? ( ) : (
{packs.map(p => (
{p.code} куплен {new Date(p.purchasedAt).toLocaleDateString('ru-RU')} {p.priceRub.toLocaleString('ru-RU')} ₽
))}
)}
{/* Payment history */}

история платежей

{userPayments.length === 0 ? ( ) : (
дата
что
метод
сумма
статус
{userPayments.map(p => (
{new Date(p.date).toLocaleDateString('ru-RU')} {p.type === 'subscription_renewal' ? `Подписка ${tier.name}` : p.type === 'token_pack' ? 'Пакет токенов' : p.type} {p.method === 'manual' ? 'банк (счёт)' : p.method} {p.amountRub.toLocaleString('ru-RU')} ₽
))}
)}
{/* Actions */} {!sub.isInternal && (

действия

)}
); } function UsageMini({ label, value, hue, suffix }) { return (
{label}
{value}
{suffix &&
{suffix}
}
); } function UsageByOp({ byOp, total }) { const opLabels = { deep_dive: 'Deep-dive отчёты', cron_pipeline: 'Cron pipeline', phase45: 'Phase 4.5 валидация', mcp_call: 'MCP API вызовы', classify: 'Классификация стартапов', embed: 'Эмбеддинги', }; const opHues = { deep_dive: 'purple', cron_pipeline: 'amber', phase45: 'rose', mcp_call: 'info', classify: 'emerald', embed: 'info', }; const sorted = Object.entries(byOp).sort((a, b) => b[1] - a[1]); return (
{sorted.map(([op, tokens]) => { const pct = tokens / total; if (tokens === 0) return null; return (
{opLabels[op]}
{tokens.toLocaleString('ru-RU')} · {Math.round(pct * 100)}%
); })}
); } window.BillingPage = BillingPage;