'use client';
import { useState, useEffect } from '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: 'Биллинг' },
];
const CATEGORIES = [
{ slug: 'ai_providers', title: 'AI провайдеры',
hint: 'Ключи, URL и модели для текстовой и картиночной генерации. Меняются на лету — рестарт engine не нужен.' },
{ slug: 'payments', title: 'ЮKassa',
hint: 'Shop ID и Secret Key из личного кабинета ЮKassa. Webhook URL для настройки: https://engine.postcast.ru/api/billing/webhook' },
{ slug: 'photo_search', title: 'Поиск фото',
hint: 'Yandex Search API: provider, ключ, folder, лимиты.' },
];
export default function SystemSettings() {
const [sysTab, setSysTab] = useState('settings');
const [byCategory, setByCategory] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
async function load() {
setLoading(true);
setError('');
try {
const result = {};
for (const cat of CATEGORIES) {
const res = await fetch(`/api/admin/settings?category=${cat.slug}`);
if (!res.ok) throw new Error((await res.json()).error || `HTTP ${res.status}`);
result[cat.slug] = await res.json();
}
setByCategory(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
useEffect(() => { load(); }, []);
if (loading) {
return (
);
}
if (error) {
return (
);
}
return (
{/* Вкладки системной страницы */}
{TABS_SYS.map(t => (
))}
{sysTab === 'billing' &&
}
{sysTab === 'settings' && (<>
{CATEGORIES.map(cat => (
load()}
/>
))}
>)}
);
}
const RANGE_LABELS = [
{ key: 'today', label: 'Сегодня' },
{ key: 'week', label: 'Неделя' },
{ key: 'month', label: 'Месяц' },
{ key: 'all', label: 'Всё' },
];
function UsageSummary() {
const [range, setRange] = useState('today');
const [groupBy, setGroupBy] = useState('service');
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState('');
async function load() {
setLoading(true);
setErr('');
try {
const res = await fetch(`/api/admin/usage/summary?range=${range}&group_by=${groupBy}`);
if (!res.ok) throw new Error((await res.json()).error || `HTTP ${res.status}`);
setData(await res.json());
} catch (e) {
setErr(e.message);
} finally {
setLoading(false);
}
}
useEffect(() => { load(); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [range, groupBy]);
const fmtRub = v => (Number(v) || 0).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' ₽';
const fmtInt = v => (Number(v) || 0).toLocaleString('ru-RU');
return (
Расход AI
{RANGE_LABELS.map(r => (
))}
{err && (
{err}
)}
{data && (
<>
{data.breakdown.length > 0 && (
|
{groupBy === 'service' ? 'Сервис' : groupBy === 'provider' ? 'Провайдер' : 'Модель'}
|
Вызовов |
Токены |
Картинки |
Стоимость |
{data.breakdown.map(row => (
| {row.key} |
{fmtInt(row.calls)}{row.failed ? ({row.failed} err) : null} |
{fmtInt(row.prompt_tokens + row.completion_tokens)} |
{fmtInt(row.image_count)} |
{fmtRub(row.cost_rub)} |
))}
)}
{data.breakdown.length === 0 && (
За выбранный период вызовов не было.
)}
>
)}
);
}
function Stat({ label, value, sub, accent }) {
return (
{label}
{value}
{sub &&
{sub}
}
);
}
function CategoryBlock({ category, rows, onSaved }) {
return (
{category.title}
{category.hint &&
{category.hint}
}
{rows.length === 0 ? (
Нет настроек в этой категории.
) : (
{rows.map(r => (
))}
)}
);
}
function SettingRow({ row, onSaved }) {
const [value, setValue] = useState(row.value ?? '');
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [reveal, setReveal] = useState(false);
const [err, setErr] = useState('');
// если row.value меняется снаружи — синхронизируем
useEffect(() => { setValue(row.value ?? ''); }, [row.value]);
const isSecret = row.is_secret;
const dirty = value !== (row.value ?? '');
async function save() {
setSaving(true);
setErr('');
try {
const res = await fetch(`/api/admin/settings/${encodeURIComponent(row.key)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ value: value === '' ? null : value }),
});
if (!res.ok) throw new Error((await res.json()).error || `HTTP ${res.status}`);
setSaved(true);
setTimeout(() => setSaved(false), 1500);
onSaved?.();
} catch (e) {
setErr(e.message);
} finally {
setSaving(false);
}
}
// Маскировка отображения секретов: показываем хвост ***last4 пока не reveal=true
const masked = isSecret && value && !reveal
? '•'.repeat(Math.max(value.length - 4, 4)) + value.slice(-4)
: value;
return (
{SETTING_LABELS[row.key] || row.description || row.key}
{isSecret && (
secret
)}
{row.key}
{
// При маскированном просмотре редактирование запрещаем — пусть сначала откроют
if (isSecret && !reveal) return;
setValue(e.target.value);
}}
placeholder={isSecret ? '(скрыто)' : '(пусто)'}
spellCheck={false}
/>
{isSecret && (
)}
{err && (
{err}
)}
Категория: {row.category} · обновлено{' '}
{row.updated_at ? new Date(row.updated_at).toLocaleString('ru-RU') : '—'}
);
}