'use client'; import { useState } from 'react'; import { Settings2, CreditCard, TrendingUp, Users, ChevronRight, Loader2, Eye, EyeOff, Save, RefreshCw, Check, AlertCircle, BarChart3, ArrowLeft } from 'lucide-react'; import Link from 'next/link'; import AdminBilling from './admin/AdminBilling'; // ────────────────────────────────────────────────────────────── // Sidebar navigation // ────────────────────────────────────────────────────────────── const SECTIONS = [ { id: 'settings', label: 'Настройки API', icon: Settings2, desc: 'AI-провайдеры, поиск фото' }, { id: 'payments', label: 'ЮKassa', icon: CreditCard, desc: 'Ключи для приёма оплат' }, { id: 'spending', label: 'Расходы AI', icon: TrendingUp, desc: 'aiprimetech + routerai' }, { 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 === 'settings' && } {section === 'payments' && } {section === 'spending' && } {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)}
)}
); }