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