'use client'; import { useState } from 'react'; import { Settings2, CreditCard, TrendingUp, Users, ChevronRight, Loader2, Eye, EyeOff, Save, RefreshCw, Check, AlertCircle, BarChart3, ArrowLeft, Zap, Tag, AlertTriangle } from 'lucide-react'; import Link from 'next/link'; import AdminBilling from './admin/AdminBilling'; import AdminUsers from './admin/AdminUsers'; import AdminPromos from './admin/AdminPromos'; import AdminQueue from './admin/AdminQueue'; import AdminLogs from './admin/AdminLogs'; // ────────────────────────────────────────────────────────────── // Sidebar navigation // ────────────────────────────────────────────────────────────── const SECTIONS = [ { id: 'dashboard', label: 'Сводка', icon: BarChart3, desc: 'Пользователи, посты, финансы' }, { id: 'settings', label: 'AI-провайдеры', icon: Settings2, desc: 'Ключи aiprimetech и routerai' }, { id: 'engine', label: 'Движок', icon: Zap, desc: 'URL, Telegram, авто-черновики' }, { id: 'payments', label: 'ЮKassa', icon: CreditCard, desc: 'Ключи для приёма оплат' }, { id: 'spending', label: 'Расходы AI', icon: TrendingUp, desc: 'aiprimetech + routerai' }, { id: 'queue', label: 'Очередь', icon: Zap, desc: 'Задачи генерации, ошибки' }, { id: 'logs', label: 'Логи ошибок', icon: AlertTriangle, desc: 'Последние сбои и проблемы' }, { id: 'plans', label: 'Тарифы', icon: BarChart3, desc: 'Планы, кредиты, операции' }, { id: 'promos', label: 'Промокоды', icon: Tag, desc: 'Коды для кредитов и скидок' }, { id: 'billing', label: 'Пользователи', icon: Users, desc: 'Балансы и кредиты' }, ]; export default function AdminPanel({ initialSection = 'settings' }) { const [section, setSection] = useState(initialSection); return (
{/* Breadcrumb */}
Главная Администрирование {SECTIONS.find(s => s.id === section)?.label}
{/* Sidebar */} {/* Content */}
{section === 'dashboard' && } {section === 'settings' && } {section === 'engine' && } {section === 'payments' && } {section === 'spending' && } {section === 'queue' && } {section === 'logs' && } {section === 'plans' && } {section === 'promos' && } {section === 'billing' && }
); } // ────────────────────────────────────────────────────────────── // Settings section (API keys) // ────────────────────────────────────────────────────────────── const CATEGORY_META = { ai_providers: { title: 'AI-провайдеры', hint: 'Ключи и URL для текстовой и картиночной генерации. Меняются на лету.', }, photo_search: { title: 'Поиск фото', hint: 'Yandex Search API: ключ и folder.', }, payments: { title: 'ЮKassa', hint: 'Shop ID и Secret Key из личного кабинета. Webhook: https://engine.zeropost.ru/api/billing/webhook', }, }; function SettingsSection({ categories }) { const [data, setData] = useState({}); const [loaded, setLoaded] = useState(false); const [loading, setLoading] = useState(false); async function load() { setLoading(true); const all = {}; for (const cat of categories) { try { const rows = await fetch(`/api/admin/settings?category=${cat}`).then(r => r.json()); all[cat] = Array.isArray(rows) ? rows : []; } catch { all[cat] = []; } } setData(all); setLoaded(true); setLoading(false); } if (!loaded && !loading) { load(); } return (
{categories.map(cat => { const meta = CATEGORY_META[cat] || { title: cat, hint: '' }; const rows = data[cat] || []; return (

{meta.title}

{meta.hint &&

{meta.hint}

}
{loading && !rows.length ?
: rows.map(row => ( )) }
); })}
); } function SettingRow({ row, onSaved }) { const [val, setVal] = useState(row.value || ''); const [show, setShow] = useState(false); const [saving, setSaving] = useState(false); const [status, setStatus] = useState(null); // 'ok' | 'error' const [errMsg, setErrMsg] = useState(''); const dirty = val !== (row.value || ''); async function save() { setSaving(true); setStatus(null); try { const res = await fetch(`/api/admin/settings/${encodeURIComponent(row.key)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: val }), }).then(r => r.json()); if (res.error) { setStatus('error'); setErrMsg(res.error); } else { setStatus('ok'); onSaved(); setTimeout(() => setStatus(null), 2000); } } catch (e) { setStatus('error'); setErrMsg(e.message); } setSaving(false); } const isSecret = row.is_secret; const inputType = isSecret && !show ? 'password' : 'text'; return (
{row.category}
{row.description &&

{row.description}

}
setVal(e.target.value)} className="input w-full pr-8 font-mono text-sm" placeholder={isSecret ? '••••••••' : 'Введите значение...'} /> {isSecret && ( )}
{dirty && ( )} {status === 'ok' && } {status === 'error' && }
обновлено: {row.updated_at ? new Date(row.updated_at).toLocaleString('ru-RU') : '—'}
); } // ────────────────────────────────────────────────────────────── // Spending section // ────────────────────────────────────────────────────────────── const PERIODS = [ { v: 'today', label: 'Сегодня' }, { v: 'week', label: '7 дней' }, { v: 'month', label: '30 дней' }, { v: 'alltime', label: 'Всё время' }, ]; const TYPE_LABELS = { 'chat': '💬 Текст', 'image': '🖼 Изображение', 'image_via_responses': '🖼 Изображение', 'article': '📝 Статья', 'topic': '🔍 Топики', }; function fmt(n) { return Number(n || 0).toFixed(2); } function fmtI(n) { return Number(n || 0).toLocaleString('ru-RU'); } function SpendingSection() { const [period, setPeriod] = useState('month'); const [data, setData] = useState(null); const [byProv, setByProv] = useState(null); const [loading, setLoading] = useState(false); async function load(p) { setLoading(true); try { const [r1, r2] = await Promise.all([ fetch(`/api/usage/summary?range=${p}&group_by=request_type`).then(r => r.json()), fetch(`/api/usage/summary?range=${p}&group_by=provider`).then(r => r.json()), ]); setData(r1); setByProv(r2); } catch {} setLoading(false); } if (!data && !loading) load(period); const totals = data?.totals || {}; const aiprimetech = byProv?.breakdown?.find(b => b.key === 'aiprimetech'); const routerai = byProv?.breakdown?.find(b => b.key === 'routerai'); return (

Расходы на AI

{PERIODS.map(p => ( ))}
{loading &&
} {!loading && data && (<> {/* Итого */}
{[ { label: 'Итого, ₽', value: `₽ ${fmt(totals.cost_rub)}`, accent: true }, { label: 'Запросов', value: fmtI(totals.calls) }, { label: 'Токенов', value: fmtI((totals.prompt_tokens||0)+(totals.completion_tokens||0)) }, { label: 'Картинок', value: fmtI(totals.image_count) }, ].map(s => (
{s.value}
{s.label}
))}
{/* По провайдерам */}
{[ { key: 'aiprimetech', label: 'aiprimetech.io', icon: '💬', desc: 'Текст', data: aiprimetech }, { key: 'routerai', label: 'routerai.ru', icon: '🖼', desc: 'Картинки', data: routerai }, ].map(p => (
{p.icon}
{p.label}
{p.desc}
₽ {fmt(p.data?.cost_rub)}
{fmtI(p.data?.calls)}
запросов
{p.data?.failed||0}
ошибок
{fmtI(p.data?.image_count || (p.data?.prompt_tokens||0)+(p.data?.completion_tokens||0))}
{p.key==='routerai'?'картинок':'токенов'}
))}
{/* Таблица по операциям */}
{(data.breakdown || []).map((row, i) => ( ))}
Операция Запросов Ошибок Объём Стоимость
{TYPE_LABELS[row.key] || row.key} {fmtI(row.calls)} {row.failed || 0} {row.image_count > 0 ? `${row.image_count} шт.` : fmtI((row.prompt_tokens||0)+(row.completion_tokens||0))} ₽ {fmt(row.cost_rub)}
Итого {fmtI(totals.calls)} {totals.failed||0} ₽ {fmt(totals.cost_rub)}
)}
); } // ────────────────────────────────────────────────────────────── // Plans & Credits section // ────────────────────────────────────────────────────────────── function PlansSection() { const [plans, setPlans] = useState([]); const [costs, setCosts] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState({}); const [msg, setMsg] = useState(''); async function load() { setLoading(true); try { const res = await fetch('/api/billing/plans').then(r => r.json()); setPlans(res.plans || []); setCosts(res.costs || []); } catch {} setLoading(false); } async function savePlan(plan) { setSaving(s => ({ ...s, [plan.id]: true })); try { await fetch(`/api/admin/plans/${plan.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ price_rub: plan.price_rub, credits_month: plan.credits_month, channels_max: plan.channels_max }), }); setMsg('Сохранено ✓'); setTimeout(() => setMsg(''), 2000); } catch {} setSaving(s => ({ ...s, [plan.id]: false })); } async function saveCost(cost) { setSaving(s => ({ ...s, [`cost_${cost.operation}`]: true })); try { await fetch(`/api/admin/credit-costs/${cost.operation}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ credits: cost.credits }), }); setMsg('Сохранено ✓'); setTimeout(() => setMsg(''), 2000); } catch {} setSaving(s => ({ ...s, [`cost_${cost.operation}`]: false })); } if (loading && !plans.length) { load(); } const PLAN_LABELS = { free: 'Free', starter: 'Starter', pro: 'Pro', business: 'Business' }; return (
{msg &&
{msg}
} {/* Тарифные планы */}

Тарифные планы

Цены и лимиты. -1 = безлимит.

{loading ? : (
{plans.map(plan => { const [p, setP] = [plan, (updates) => setPlans(pp => pp.map(x => x.id === plan.id ? { ...x, ...updates } : x))]; return (
{PLAN_LABELS[p.code] || p.code}
₽/мес: setP({ price_rub: +e.target.value })} className="input w-20 text-sm py-1" />
кредитов: setP({ credits_month: +e.target.value })} className="input w-20 text-sm py-1" />
каналов: setP({ channels_max: +e.target.value })} className="input w-16 text-sm py-1" />
); })}
)}
{/* Стоимость операций */}

Стоимость операций (кредиты)

Сколько кредитов списывается за каждую операцию.

{loading ? : (
{costs.map(cost => { const [c, setC] = [cost, (updates) => setCosts(cc => cc.map(x => x.operation === cost.operation ? { ...x, ...updates } : x))]; const icons = { image: '🖼', text_post: '✍️', article: '📝', autopublish: '📤' }; return (
{icons[c.operation] || '⚙️'}
{c.description || c.operation}
setC({ credits: +e.target.value })} className="input w-16 text-sm py-1 text-center" min={0} /> кр
); })}
)}
); } // ────────────────────────────────────────────────────────────── // Dashboard section // ────────────────────────────────────────────────────────────── function DashboardSection() { const [data, setData] = useState(null); const [loading, setLoading]= useState(true); async function load() { setLoading(true); try { const res = await fetch('/api/admin/dashboard').then(r => r.json()); setData(res); } catch {} setLoading(false); } if (!data && !loading) load(); if (!data && loading) { load(); } const PLATFORM_ICONS = { telegram: '✈️', vk: '🔵', max: '🟣' }; function Stat({ label, value, sub, accent }) { return (
{value}
{label}
{sub &&
{sub}
}
); } return (

Сводка

{loading &&
} {data && (<> {/* Пользователи */}

Пользователи

{/* Каналы */}

Каналы

s + c.cnt, 0)} /> {data.channels.map(c => ( ))}
{/* Посты */}

Публикации

{/* Финансы */}

Финансы

₽{data.revenue.month_rub.toLocaleString('ru-RU')}
Выручка за 30 дней
{data.revenue.paid_count} платежей • итого ₽{data.revenue.total_rub.toLocaleString('ru-RU')}
₽{data.ai.cost_rub.toFixed(2)}
Расходы на AI за 30 дней
{data.ai.calls} запросов • {data.ai.errors} ошибок
{/* Черновики */} {data.drafts.pending > 0 && (
⚡ {data.drafts.pending} черновиков ждут одобрения
Просмотрите и запланируйте публикацию
Смотреть →
)} {/* Регистрации 14 дней */} {data.registrations_14d.length > 0 && (

Регистрации — последние 14 дней

{(() => { const max = Math.max(...data.registrations_14d.map(r => r.cnt), 1); // Заполняем пустые дни const days = []; for (let i = 13; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); const key = d.toISOString().split('T')[0]; const found = data.registrations_14d.find(r => r.day === key); days.push({ day: key, cnt: found?.cnt || 0 }); } return days.map((r, i) => (
{i % 7 === 6 &&
{new Date(r.day).toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' })}
}
)); })()}
)} )}
); }