diff --git a/app/api/admin/credit-costs/[operation]/route.js b/app/api/admin/credit-costs/[operation]/route.js
new file mode 100644
index 0000000..fff0c3b
--- /dev/null
+++ b/app/api/admin/credit-costs/[operation]/route.js
@@ -0,0 +1,17 @@
+import { NextResponse } from 'next/server';
+import { requireUser } from '@/lib/session';
+
+const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030';
+const ENGINE_SECRET = process.env.ENGINE_SECRET || '';
+
+export async function PATCH(req, { params }) {
+ const user = await requireUser();
+ if (!user?.isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ const body = await req.json();
+ const res = await fetch(`${ENGINE_URL}/api/admin/credit-costs/${params.operation}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json', 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(user.id) },
+ body: JSON.stringify(body),
+ });
+ return NextResponse.json(await res.json());
+}
diff --git a/app/api/admin/plans/[id]/route.js b/app/api/admin/plans/[id]/route.js
new file mode 100644
index 0000000..439ec01
--- /dev/null
+++ b/app/api/admin/plans/[id]/route.js
@@ -0,0 +1,17 @@
+import { NextResponse } from 'next/server';
+import { requireUser } from '@/lib/session';
+
+const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030';
+const ENGINE_SECRET = process.env.ENGINE_SECRET || '';
+
+export async function PATCH(req, { params }) {
+ const user = await requireUser();
+ if (!user?.isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ const body = await req.json();
+ const res = await fetch(`${ENGINE_URL}/api/admin/plans/${params.id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json', 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(user.id) },
+ body: JSON.stringify(body),
+ });
+ return NextResponse.json(await res.json());
+}
diff --git a/components/AdminPanel.js b/components/AdminPanel.js
index fdeece6..e43c2a2 100644
--- a/components/AdminPanel.js
+++ b/components/AdminPanel.js
@@ -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 */}
{section === 'settings' &&
}
+ {section === 'engine' &&
}
{section === 'payments' &&
}
{section === 'spending' &&
}
+ {section === 'plans' &&
}
{section === 'billing' &&
}
@@ -336,3 +340,130 @@ function SpendingSection() {
);
}
+
+// ──────────────────────────────────────────────────────────────
+// 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 (
+
+
{PLAN_LABELS[p.code] || p.code}
+
+ ₽/мес:
+ setP({ price_rub: +e.target.value })}
+ className="input w-20 text-sm py-1" />
+
+
+ кредитов:
+ setP({ credits_month: +e.target.value })}
+ className="input w-20 text-sm py-1" />
+
+
+ каналов:
+ setP({ channels_max: +e.target.value })}
+ className="input w-16 text-sm py-1" />
+
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* Стоимость операций */}
+
+ Стоимость операций (кредиты)
+ Сколько кредитов списывается за каждую операцию.
+ {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 (
+
+
{icons[c.operation] || '⚙️'}
+
{c.description || c.operation}
+
+ setC({ credits: +e.target.value })}
+ className="input w-16 text-sm py-1 text-center" min={0} />
+ кр
+
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
diff --git a/components/Header.js b/components/Header.js
index 24fba4c..c43411f 100644
--- a/components/Header.js
+++ b/components/Header.js
@@ -2,7 +2,7 @@
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
-import { Sparkles, LogOut, Settings2, CalendarDays, Coins, FileText } from 'lucide-react';
+import { Sparkles, LogOut, CalendarDays, Coins, FileText } from 'lucide-react';
import ThemeToggle from './ThemeToggle';
export default function Header({ user }) {
@@ -53,12 +53,6 @@ export default function Header({ user }) {
{credits} кр
)}
- {user?.isAdmin && (
-
-
- Система
-
- )}
{user?.email}