From 4d45cdcba304475d39c1efd26ec6cbdca5b31ea5 Mon Sep 17 00:00:00 2001 From: karim Date: Sun, 31 May 2026 12:00:03 +0200 Subject: [PATCH] feat(admin): erweiterte Kennzahlen + Kunden-Detail-API - /admin/stats: newAccounts30d, suspendedInstances, ARR, byPlan (count+revenue) - /admin/accounts/:id: Voll-Detail (Profil + Abo-Historie + Instanzen, Plan-Preis) Co-Authored-By: Claude Opus 4.8 --- server/routes/admin.js | 44 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/server/routes/admin.js b/server/routes/admin.js index f9999e4..501c405 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -24,20 +24,34 @@ adminRouter.use(requireAdmin); // — Kennzahlen fürs Dashboard — adminRouter.get("/stats", async (_req, res) => { const accounts = await one("select count(*)::int n from accounts"); + const newAccounts = await one("select count(*)::int n from accounts where created_at > now() - interval '30 days'"); const activeSubs = await one("select count(*)::int n from subscriptions where status = 'active'"); const instances = await one("select count(*)::int n from instances"); const activeInst = await one("select count(*)::int n from instances where status = 'active'"); + const suspendedInst = await one("select count(*)::int n from instances where status = 'suspended'"); - // MRR (geschätzt): Summe der Plan-Preise aller aktiven Abos. + // MRR + Verteilung nach Plan (nur aktive Abos). const { rows: subs } = await query("select plan from subscriptions where status = 'active'"); - const mrr = subs.reduce((sum, s) => sum + (getPlan(s.plan)?.priceChf || 0), 0); + let mrr = 0; + const byPlan = {}; + for (const s of subs) { + const p = getPlan(s.plan); + mrr += p?.priceChf || 0; + byPlan[s.plan] = byPlan[s.plan] || { count: 0, revenue: 0, name: p?.name || s.plan }; + byPlan[s.plan].count += 1; + byPlan[s.plan].revenue += p?.priceChf || 0; + } res.json({ accounts: accounts.n, + newAccounts30d: newAccounts.n, activeSubscriptions: activeSubs.n, instances: instances.n, activeInstances: activeInst.n, + suspendedInstances: suspendedInst.n, mrrChf: mrr, + arrChf: mrr * 12, + byPlan, // { solo: {count,revenue,name}, ... } }); }); @@ -61,7 +75,31 @@ adminRouter.get("/accounts", async (_req, res) => { res.json({ accounts: rows }); }); -// — Instanzen eines Kontos — +// — Voll-Detail eines Kontos: Profil + Abo-Historie + Instanzen — +adminRouter.get("/accounts/:id", async (req, res) => { + const account = await one( + `select id, email, company, contact_name, street, zip, city, country, phone, created_at + from accounts where id = $1`, + [req.params.id] + ); + if (!account) return res.status(404).json({ error: "Konto nicht gefunden." }); + + const { rows: subscriptions } = await query( + `select plan, status, current_period_end, stripe_subscription_id, created_at + from subscriptions where account_id = $1 order by created_at desc`, + [req.params.id] + ); + const { rows: instances } = await query( + "select id, studio_slug, label, instance_url, status, created_at from instances where account_id = $1 order by created_at", + [req.params.id] + ); + // Plan-Preis an die Abos hängen (fürs UI). + for (const s of subscriptions) s.priceChf = getPlan(s.plan)?.priceChf ?? null; + + res.json({ account, subscriptions, instances }); +}); + +// — Instanzen eines Kontos (Kurzform) — adminRouter.get("/accounts/:id/instances", async (req, res) => { const { rows } = await query( "select id, studio_slug, label, instance_url, status, created_at from instances where account_id = $1 order by created_at",