// ════════════════════════════════════════════════════════════════ // /admin/billing — выручка, подписки, заявки на оформление // super_admin only (admin видит totals) // ════════════════════════════════════════════════════════════════ function AdminBillingPage({ currentUser }) { const A = window.SDH_ADMIN; const B = window.SDH_BILLING; const isSuperAdmin = currentUser.role === 'super_admin'; const summary = B.adminSummary(); const newReqs = B.paymentRequests.filter(r => r.status === 'new'); const inProgress = B.paymentRequests.filter(r => r.status === 'in_progress' || r.status === 'invoiced'); const completed = B.paymentRequests.filter(r => r.status === 'completed'); // Per-tier breakdown const tierBreakdown = B.TIERS.filter(t => t.isPublic).map(t => { const subs = Object.values(B.SUBSCRIPTIONS_BY_USER).filter(s => s.tierCode === t.code && !s.isInternal); return { tier: t, count: subs.length, revenue: subs.length * t.priceRub, }; }); if (!isSuperAdmin && currentUser.role !== 'admin') { return ; } return (
{/* Top tiles */}
0 ? 'amber' : 'info'} footer={newReqs.length > 0 ? 'ждут оформления' : 'все обработаны'} /> {isSuperAdmin && ( )}
{/* Revenue chart */}

выручка vs расходы · 12 месяцев

в рублях
{/* Manual payment requests */} {isSuperAdmin && (

ручные заявки

{newReqs.length} новых · {inProgress.length} в работе · {completed.length} завершено
{[...newReqs, ...inProgress, ...completed].map(req => )}
)} {/* Subscriptions breakdown */}

активные подписки по tier'ам

{tierBreakdown.filter(b => b.count > 0).map(b => (
{b.tier.name}
{b.count} {b.count === 1 ? 'подписка' : 'подписок'} {b.revenue.toLocaleString('ru-RU')} ₽
))}
{/* Internal tier grant */} {isSuperAdmin && (

выдать internal tier

Бесплатный безлимитный tier для членов команды Symbiosis Lab. Только super-admin может выдавать. Действие логируется.

)}
); } function MetricTile({ label, value, hue, footer, help }) { return (
{help ? {label} : label}
{value} {footer && (
{footer}
)}
); } function RevenueChart({ data }) { const maxRev = Math.max(...data.map(d => d.revenue)); return (
{data.map((m, i) => { const revH = (m.revenue / maxRev) * 140; const costH = (m.cost / maxRev) * 140; return (
{Math.round(m.revenue / 1000)}K
{m.month.slice(5)}
); })}
выручка расходы
); } function PaymentRequestRow({ req }) { const B = window.SDH_BILLING; const tier = B.getTier(req.tierCode); const statusMap = { new: { hue: 'amber', label: 'новая' }, in_progress: { hue: 'purple', label: 'в работе' }, invoiced: { hue: 'info', label: 'счёт выставлен' }, completed: { hue: 'emerald', label: 'завершена' }, }; const s = statusMap[req.status]; const minsAgo = Math.round((new Date('2026-05-23T10:00:00Z') - new Date(req.createdAt)) / 60000); const timeAgo = minsAgo < 60 ? `${minsAgo} мин назад` : minsAgo < 60 * 24 ? `${Math.round(minsAgo / 60)} ч назад` : `${Math.round(minsAgo / 60 / 24)} д назад`; return (
{req.name}
{req.email}
{req.comment && (
«{req.comment.length > 80 ? req.comment.slice(0, 78) + '…' : req.comment}»
)}
{tier.name} {tier.priceRub ? `${tier.priceRub.toLocaleString('ru-RU')} ₽` : 'по запросу'} {timeAgo}
{s.label} {req.status === 'new' && ( )}
); } window.AdminBillingPage = AdminBillingPage;