'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 не нужен. Курс USD↔РУБ и наценка реселлера тут же — влияют на расчёт стоимости в блоке «Расход AI» выше.' }, { 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 (
{error}
); } 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 && (
{data.breakdown.map(row => ( ))}
{groupBy === 'service' ? 'Сервис' : groupBy === 'provider' ? 'Провайдер' : 'Модель'} Вызовов Токены Картинки Стоимость
{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 (
{row.key} {isSecret && ( secret )}
{row.description && (

{row.description}

)}
{ // При маскированном просмотре редактирование запрещаем — пусть сначала откроют 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') : '—'}
); }