feat: admin panel improvements

Header: убрана кнопка Система (дубль Админ), убраны устаревшие импорты
AdminPanel: 6 разделов (AI-провайдеры, Движок, ЮKassa, Расходы AI, Тарифы, Пользователи)
  Тарифы: редактор планов (цена/кредиты/каналы) + стоимость операций
  Движок: ENGINE_PUBLIC_URL, APP_PUBLIC_URL, TELEGRAM_API_BASE, AUTO_DRAFT_*
PlansSection: inline-редактирование тарифов и credit_costs
API routes: /api/admin/plans/[id], /api/admin/credit-costs/[operation]
This commit is contained in:
Ник (Claude)
2026-06-13 00:02:52 +03:00
parent 1fbdc9f9b9
commit a5f6c080bd
4 changed files with 168 additions and 9 deletions
+133 -2
View File
@@ -1,6 +1,6 @@
'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 { 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';
@@ -8,9 +8,11 @@ import AdminBilling from './admin/AdminBilling';
// Sidebar navigation
// ──────────────────────────────────────────────────────────────
const SECTIONS = [
{ id: 'settings', label: 'Настройки API', icon: Settings2, desc: 'AI-провайдеры, поиск фото' },
{ 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: 'Балансы и кредиты' },
];
@@ -55,8 +57,10 @@ export default function AdminPanel({ initialSection = 'settings' }) {
{/* Content */}
<div className="flex-1 min-w-0">
{section === 'settings' && <SettingsSection categories={['ai_providers', 'photo_search']} />}
{section === 'engine' && <SettingsSection categories={['engine']} />}
{section === 'payments' && <SettingsSection categories={['payments']} />}
{section === 'spending' && <SpendingSection />}
{section === 'plans' && <PlansSection />}
{section === 'billing' && <AdminBilling />}
</div>
</div>
@@ -336,3 +340,130 @@ function SpendingSection() {
</div>
);
}
// ──────────────────────────────────────────────────────────────
// 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 (
<div className="space-y-6">
{msg && <div className="text-sm text-green-400 flex items-center gap-1"><Check className="w-4 h-4" />{msg}</div>}
{/* Тарифные планы */}
<section className="card p-5">
<h2 className="font-semibold mb-1">Тарифные планы</h2>
<p className="text-xs text-gray-500 mb-4">Цены и лимиты. -1 = безлимит.</p>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : (
<div className="space-y-3">
{plans.map(plan => {
const [p, setP] = [plan, (updates) => setPlans(pp => pp.map(x => x.id === plan.id ? { ...x, ...updates } : x))];
return (
<div key={p.id} className="flex items-center gap-3 p-3 rounded-lg bg-surface2">
<div className="w-20 font-medium text-sm">{PLAN_LABELS[p.code] || p.code}</div>
<div className="flex items-center gap-1">
<span className="text-xs text-gray-500">/мес:</span>
<input type="number" value={p.price_rub} onChange={e => setP({ price_rub: +e.target.value })}
className="input w-20 text-sm py-1" />
</div>
<div className="flex items-center gap-1">
<span className="text-xs text-gray-500">кредитов:</span>
<input type="number" value={p.credits_month} onChange={e => setP({ credits_month: +e.target.value })}
className="input w-20 text-sm py-1" />
</div>
<div className="flex items-center gap-1">
<span className="text-xs text-gray-500">каналов:</span>
<input type="number" value={p.channels_max} onChange={e => setP({ channels_max: +e.target.value })}
className="input w-16 text-sm py-1" />
</div>
<button onClick={() => savePlan(p)} disabled={saving[p.id]}
className="btn-primary text-xs px-2.5 py-1.5 ml-auto flex items-center gap-1">
{saving[p.id] ? <Loader2 className="w-3 h-3 animate-spin" /> : <Save className="w-3 h-3" />}
Сохранить
</button>
</div>
);
})}
</div>
)}
</section>
{/* Стоимость операций */}
<section className="card p-5">
<h2 className="font-semibold mb-1">Стоимость операций (кредиты)</h2>
<p className="text-xs text-gray-500 mb-4">Сколько кредитов списывается за каждую операцию.</p>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : (
<div className="space-y-2">
{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 (
<div key={c.operation} className="flex items-center gap-3 p-3 rounded-lg bg-surface2">
<span className="text-lg w-7 text-center">{icons[c.operation] || '⚙️'}</span>
<div className="flex-1 text-sm">{c.description || c.operation}</div>
<div className="flex items-center gap-2">
<input type="number" value={c.credits} onChange={e => setC({ credits: +e.target.value })}
className="input w-16 text-sm py-1 text-center" min={0} />
<span className="text-xs text-gray-500">кр</span>
</div>
<button onClick={() => saveCost(c)} disabled={saving[`cost_${c.operation}`]}
className="btn-primary text-xs px-2.5 py-1.5 flex items-center gap-1">
{saving[`cost_${c.operation}`] ? <Loader2 className="w-3 h-3 animate-spin" /> : <Save className="w-3 h-3" />}
Сохранить
</button>
</div>
);
})}
</div>
)}
</section>
</div>
);
}