feat: admin dashboard API + separate admin routes file
routes/admin.js: GET /dashboard, /users, POST /credit, PATCH /plans/:id, /credit-costs/:op
index.js: app.use('/api/admin', adminRoutes) — чистый монтаж без хаков
dashboard: users (total/7d/30d), channels by platform, posts (total/today/week),
revenue (YuKassa), AI costs (30d), registrations chart (14d), pending drafts alert
This commit is contained in:
@@ -153,3 +153,54 @@ router.patch('/admin/credit-costs/:operation', async (req, res) => {
|
||||
res.json({ ok: true, operation: req.params.operation, credits });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// GET /api/admin/dashboard — сводная статистика для admin
|
||||
router.get('/admin/dashboard', async (req, res) => {
|
||||
const adminId = uid(req);
|
||||
if (!adminId) return res.status(401).json({ error: 'x-user-id required' });
|
||||
const { rows: [admin] } = await query('SELECT is_admin FROM users WHERE id=$1', [adminId]);
|
||||
if (!admin?.is_admin) return res.status(403).json({ error: 'Forbidden' });
|
||||
|
||||
try {
|
||||
const [users, channels, posts, drafts, revenue, ai, regs] = await Promise.all([
|
||||
// Пользователи
|
||||
query(`SELECT count(*)::int as total,
|
||||
count(*) FILTER (WHERE created_at > NOW()-INTERVAL '7 days')::int as new_7d,
|
||||
count(*) FILTER (WHERE created_at > NOW()-INTERVAL '30 days')::int as new_30d
|
||||
FROM users`),
|
||||
// Каналы по платформам
|
||||
query(`SELECT platform, count(*)::int as cnt FROM channels WHERE is_active=true GROUP BY platform`),
|
||||
// Посты
|
||||
query(`SELECT count(*)::int as total,
|
||||
count(*) FILTER (WHERE published_at > NOW()-INTERVAL '24 hours')::int as today,
|
||||
count(*) FILTER (WHERE published_at > NOW()-INTERVAL '7 days')::int as week
|
||||
FROM scheduled_posts WHERE status='sent'`),
|
||||
// Черновики ожидающие
|
||||
query(`SELECT count(*)::int as pending FROM post_drafts WHERE status='pending'`),
|
||||
// Выручка ЮKassa
|
||||
query(`SELECT coalesce(sum(amount_rub),0)::int as total_rub,
|
||||
count(*) FILTER (WHERE status='succeeded')::int as paid_count,
|
||||
coalesce(sum(amount_rub) FILTER (WHERE created_at > NOW()-INTERVAL '30 days'),0)::int as month_rub
|
||||
FROM payment_orders WHERE status='succeeded'`),
|
||||
// Расходы AI за 30 дней
|
||||
query(`SELECT coalesce(sum(cost_rub),0)::numeric(10,2) as month_rub,
|
||||
count(*)::int as calls,
|
||||
count(*) FILTER (WHERE NOT succeeded)::int as errors
|
||||
FROM ai_usage WHERE created_at > NOW()-INTERVAL '30 days'`),
|
||||
// Регистрации по дням (последние 14 дней)
|
||||
query(`SELECT date_trunc('day', created_at)::date as day, count(*)::int as cnt
|
||||
FROM users WHERE created_at > NOW()-INTERVAL '14 days'
|
||||
GROUP BY 1 ORDER BY 1`),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
users: users.rows[0],
|
||||
channels: channels.rows,
|
||||
posts: posts.rows[0],
|
||||
drafts: drafts.rows[0],
|
||||
revenue: revenue.rows[0],
|
||||
ai: { ...ai.rows[0], cost_rub: parseFloat(ai.rows[0].month_rub) },
|
||||
registrations_14d: regs.rows,
|
||||
});
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user