diff --git a/backend/database.py b/backend/database.py index 5c5516a..2c37d10 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2341,6 +2341,24 @@ def _migrate(conn_factory): except Exception: pass + # upgrade_requests: Abo-Upgrade-Anfragen von Nutzern + try: + conn.execute(""" + CREATE TABLE IF NOT EXISTS upgrade_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + tier TEXT NOT NULL, + message TEXT, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), + fulfilled_at TEXT, + fulfilled_by INTEGER REFERENCES users(id) + ) + """) + conn.execute("CREATE INDEX IF NOT EXISTS idx_upgrade_req_pending ON upgrade_requests(fulfilled_at, created_at DESC)") + logger.info("Migration: upgrade_requests bereit.") + except Exception as e: + logger.warning(f"Migration upgrade_requests: {e}") + # route_dogs: bestehende Routen allen Hunden des Users zuweisen try: existing = conn.execute("SELECT COUNT(*) FROM route_dogs").fetchone()[0] diff --git a/backend/main.py b/backend/main.py index 097edd7..9f5a2c4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -406,7 +406,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "918" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "921" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): @@ -465,10 +465,11 @@ async def sitemap(): today = date.today().isoformat() urls = [ ("https://banyaro.app/", "weekly", "1.0"), - ("https://banyaro.app/info", "monthly", "0.9"), - ("https://banyaro.app/presse", "monthly", "0.8"), + ("https://banyaro.app/zuechter", "weekly", "0.9"), + ("https://banyaro.app/info", "monthly", "0.8"), + ("https://banyaro.app/presse", "monthly", "0.7"), ("https://banyaro.app/wiki/rassen", "weekly", "0.8"), - ("https://banyaro.app/knigge", "monthly", "0.8"), + ("https://banyaro.app/knigge", "monthly", "0.7"), ("https://banyaro.app/wurfboerse", "daily", "0.8"), ] diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 92a199d..19e93eb 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -124,13 +124,17 @@ async def action_items(user=Depends(require_mod)): users_today = conn.execute( "SELECT COUNT(*) FROM users WHERE DATE(created_at)=DATE('now')" ).fetchone()[0] + upgrades_pending = conn.execute( + "SELECT COUNT(*) FROM upgrade_requests WHERE fulfilled_at IS NULL" + ).fetchone()[0] return { - "jobs_pending": jobs, - "breeder_pending": breeders, - "reports_open": reports, - "fotos_pending": fotos, - "poi_edits_pending": poi_edits, - "users_today": users_today, + "jobs_pending": jobs, + "breeder_pending": breeders, + "reports_open": reports, + "fotos_pending": fotos, + "poi_edits_pending": poi_edits, + "users_today": users_today, + "upgrades_pending": upgrades_pending, } @@ -1091,3 +1095,66 @@ async def generate_media_previews(user=Depends(require_admin)): errors += 1 return {"generated": generated, "skipped": skipped, "errors": errors} + + +# ------------------------------------------------------------------ +# GET /api/admin/upgrade-requests — offene Upgrade-Anfragen +# POST /api/admin/upgrade-requests/{id}/fulfill — Tier setzen + Mail +# ------------------------------------------------------------------ +@router.get("/upgrade-requests") +async def list_upgrade_requests(user=Depends(require_admin)): + with db() as conn: + rows = conn.execute(""" + SELECT r.id, r.user_id, r.tier, r.message, r.created_at, r.fulfilled_at, + u.name, u.email + FROM upgrade_requests r + JOIN users u ON u.id = r.user_id + ORDER BY r.fulfilled_at IS NOT NULL, r.created_at DESC + LIMIT 100 + """).fetchall() + return [dict(r) for r in rows] + + +@router.post("/upgrade-requests/{req_id}/fulfill") +async def fulfill_upgrade_request(req_id: int, user=Depends(require_admin)): + with db() as conn: + req = conn.execute( + "SELECT r.*, u.name, u.email FROM upgrade_requests r JOIN users u ON u.id=r.user_id WHERE r.id=?", + (req_id,) + ).fetchone() + if not req: + raise HTTPException(404, "Anfrage nicht gefunden.") + if req["fulfilled_at"]: + raise HTTPException(400, "Bereits erledigt.") + if req["tier"] not in _VALID_TIERS: + raise HTTPException(400, "Ungültiger Tier.") + conn.execute( + "UPDATE users SET subscription_tier=? WHERE id=?", + (req["tier"], req["user_id"]) + ) + conn.execute( + "UPDATE upgrade_requests SET fulfilled_at=strftime('%Y-%m-%dT%H:%M:%fZ','now'), fulfilled_by=? WHERE id=?", + (user["id"], req_id) + ) + _audit(conn, user, "fulfill_upgrade", f"user:{req['user_id']}", f"tier={req['tier']}") + + tier_labels = {"pro": "Ban Yaro Pro", "breeder": "Züchter"} + tier_label = tier_labels.get(req["tier"], req["tier"]) + try: + from mailer import send_email, email_html + body_html = f""" +
Hallo {req['name']},
+dein Account wurde soeben auf {tier_label} freigeschaltet.
+Du kannst alle {tier_label}-Features ab sofort in der App nutzen. + Öffne Ban Yaro und lade die App einmal neu — dann ist dein neuer Tarif aktiv.
+Vielen Dank für dein Vertrauen!
+Viele Grüße
René & das Ban Yaro Team
| ${h} | ` + ).join('')} +|||||||
|---|---|---|---|---|---|---|---|
| Noch keine Züchter | |||||||
| + Keine offenen Anfragen + |
+ Der Account wird auf ${tierLabel} gesetzt und + eine Bestätigungsmail gesendet. +
`, + confirmLabel: 'Freischalten', + danger: false, + }); + if (!ok) return; + btn.disabled = true; + btn.textContent = '…'; + try { + const res = await API.post(`/admin/upgrade-requests/${id}/fulfill`); + UI.toast.success(`${res.user} wurde auf ${tierLabel} freigeschaltet.`); + _renderTab(); + } catch (e) { + UI.toast.error(e.message); + btn.disabled = false; + btn.textContent = 'Freischalten'; + } + }); + }); + } + return { init, refresh, onDogChange }; })(); diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index 8767cc6..96ee9f7 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -77,6 +77,137 @@ window.Page_settings = (() => { } } + // ---------------------------------------------------------- + // ABO & TARIF + // ---------------------------------------------------------- + function _tierCard(u) { + const tier = u.subscription_tier || 'standard'; + const rolle = u.rolle || 'user'; + const isAdmin = rolle === 'admin' || rolle === 'moderator'; + const isPro = ['pro','pro_test'].includes(tier); + const isBreeder = ['breeder','breeder_test'].includes(tier) || rolle === 'breeder'; + const isStandard = !isAdmin && !isPro && !isBreeder; + + const _badge = (label, color) => + `${label}`; + + const _upgradeBtn = (id, label, price, color) => + ``; + + let statusHtml = ''; + let actionsHtml = ''; + + if (isAdmin) { + statusHtml = _badge('Admin', '#6366f1'); + } else if (isBreeder) { + statusHtml = _badge('Züchter aktiv', '#C4843A'); + } else if (isPro) { + statusHtml = _badge('Pro aktiv', '#16a34a'); + actionsHtml = ` +