feat: billing complete — plans page, admin billing, credit cost hints

/plans: страница тарифов с карточками, стоимостью операций, FAQ
/system → Биллинг: таблица пользователей с кредитами, ручное начисление
ChannelView: badge стоимости (2кр текст + 5кр картинка) под кнопкой генерации
  Ошибка INSUFFICIENT_CREDITS → понятное сообщение
  После генерации — event credits-updated → обновление badge в header
Header: подписка на credits-updated event
API роуты: /api/billing/plans, /api/billing/admin/users, /api/billing/admin/credit
This commit is contained in:
Ник (Claude)
2026-06-11 18:42:54 +03:00
parent 1cce478f27
commit 9bd38bc645
8 changed files with 380 additions and 10 deletions
+24 -3
View File
@@ -1,9 +1,13 @@
'use client';
import { useState, useEffect } from 'react';
import { Loader2, Save, Eye, EyeOff, RefreshCw, Check, AlertCircle, BarChart3 } from 'lucide-react';
import { Loader2, Save, Eye, EyeOff, RefreshCw, Check, AlertCircle, BarChart3, Coins } from 'lucide-react';
import AdminBilling from './admin/AdminBilling';
const TABS_SYS = [
{ id: 'settings', label: 'Настройки API' },
{ id: 'billing', label: 'Биллинг' },
];
// Категории, которые управляются здесь (в админке tool, а не в админке блога).
// Категория `engine` (TELEGRAM_API_BASE и т.п.) намеренно живёт в zeropost.ru/admin.
const CATEGORIES = [
{ slug: 'ai_providers', title: 'AI провайдеры',
hint: 'Ключи, URL и модели для текстовой и картиночной генерации. Меняются на лету — рестарт engine не нужен. Курс USD↔РУБ и наценка реселлера тут же — влияют на расчёт стоимости в блоке «Расход AI» выше.' },
@@ -12,6 +16,7 @@ const CATEGORIES = [
];
export default function SystemSettings() {
const [sysTab, setSysTab] = useState('settings');
const [byCategory, setByCategory] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
@@ -60,6 +65,21 @@ export default function SystemSettings() {
return (
<div className="space-y-6">
{/* Вкладки системной страницы */}
<div className="flex gap-1 border-b border-border pb-0">
{TABS_SYS.map(t => (
<button key={t.id} onClick={() => setSysTab(t.id)}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors -mb-px ${
sysTab === t.id ? 'border-accent text-accent' : 'border-transparent text-gray-400 hover:text-gray-200'
}`}>
{t.label}
</button>
))}
</div>
{sysTab === 'billing' && <AdminBilling />}
{sysTab === 'settings' && (<>
<UsageSummary />
{CATEGORIES.map(cat => (
<CategoryBlock
@@ -69,6 +89,7 @@ export default function SystemSettings() {
onSaved={() => load()}
/>
))}
</>)}
</div>
);
}