// ════════════════════════════════════════════════════════════════
// /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;