feat(admin): erweiterte Kennzahlen + Kunden-Detail-API
- /admin/stats: newAccounts30d, suspendedInstances, ARR, byPlan (count+revenue) - /admin/accounts/🆔 Voll-Detail (Profil + Abo-Historie + Instanzen, Plan-Preis) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+41
-3
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user