From a5f6c080bd407c90ebb8bf061a6223d1a7351207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=20=28Claude=29?= Date: Sat, 13 Jun 2026 00:02:52 +0300 Subject: [PATCH] feat: admin panel improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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] --- .../admin/credit-costs/[operation]/route.js | 17 +++ app/api/admin/plans/[id]/route.js | 17 +++ components/AdminPanel.js | 135 +++++++++++++++++- components/Header.js | 8 +- 4 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 app/api/admin/credit-costs/[operation]/route.js create mode 100644 app/api/admin/plans/[id]/route.js 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}