'use client';
import { useState } from 'react';
import { Settings2, CreditCard, TrendingUp, Users, ChevronRight, Loader2, Eye, EyeOff, Save, RefreshCw, Check, AlertCircle, BarChart3, ArrowLeft, Zap, Tag } from 'lucide-react';
import Link from 'next/link';
import AdminBilling from './admin/AdminBilling';
import AdminUsers from './admin/AdminUsers';
import AdminPromos from './admin/AdminPromos';
// ──────────────────────────────────────────────────────────────
// 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: '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 === '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 => (
))}
{/* По провайдерам */}
{[
{ 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 (
);
})}
)}
);
}
// ──────────────────────────────────────────────────────────────
// 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' })}
}
));
})()}
)}
>)}
);
}