'use client';
import { useState } from 'react';
import { Settings2, CreditCard, TrendingUp, Users, ChevronRight, Loader2, Eye, EyeOff, Save, RefreshCw, Check, AlertCircle, BarChart3, ArrowLeft, Zap } from 'lucide-react';
import Link from 'next/link';
import AdminBilling from './admin/AdminBilling';
// ──────────────────────────────────────────────────────────────
// Sidebar navigation
// ──────────────────────────────────────────────────────────────
const SECTIONS = [
{ 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: 'plans', label: 'Тарифы', icon: BarChart3, 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 === 'settings' &&
}
{section === 'engine' &&
}
{section === 'payments' &&
}
{section === 'spending' &&
}
{section === 'plans' &&
}
{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 => (
))}
{/* По провайдерам */}
{[
{ key: 'aiprimetech', label: 'aiprimetech.io', icon: '💬', desc: 'Текст', data: aiprimetech },
{ key: 'routerai', label: 'routerai.ru', icon: '🖼', desc: 'Картинки', data: routerai },
].map(p => (
{p.icon}
₽ {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 (
);
})}
)}
{/* Стоимость операций */}
Стоимость операций (кредиты)
Сколько кредитов списывается за каждую операцию.
{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 (
);
})}
)}
);
}