diff --git a/web/src/padelnomics/admin/routes.py b/web/src/padelnomics/admin/routes.py index fc35e7d..5a927b8 100644 --- a/web/src/padelnomics/admin/routes.py +++ b/web/src/padelnomics/admin/routes.py @@ -3534,6 +3534,39 @@ async def affiliate_delete(product_id: int): return redirect(url_for("admin.affiliate_products")) +@bp.route("/affiliate/dashboard") +@role_required("admin") +async def affiliate_dashboard(): + """Affiliate click statistics dashboard.""" + from ..affiliate import get_click_stats + + days_count = int(request.args.get("days", "30") or "30") + days_count = max(7, min(days_count, 365)) + stats = await get_click_stats(days_count) + + # Build estimated revenue: clicks × assumed 3% CR × avg basket €80 + est_revenue = round(stats["total_clicks"] * 0.03 * 80) + + # Article count (live articles that have been clicked) + article_count = len(stats["top_articles"]) + + # Retailer bars: compute pct of max for width + max_ret_clicks = max((r["click_count"] for r in stats["by_retailer"]), default=1) + for r in stats["by_retailer"]: + r["pct"] = round(r["click_count"] / max_ret_clicks * 100) if max_ret_clicks else 0 + total = stats["total_clicks"] or 1 + r["share_pct"] = round(r["click_count"] / total * 100) + + return await render_template( + "admin/affiliate_dashboard.html", + admin_page="affiliate_dashboard", + stats=stats, + est_revenue=est_revenue, + article_count=article_count, + days_count=days_count, + ) + + @bp.route("/affiliate//toggle", methods=["POST"]) @role_required("admin") async def affiliate_toggle(product_id: int): diff --git a/web/src/padelnomics/admin/templates/admin/affiliate_dashboard.html b/web/src/padelnomics/admin/templates/admin/affiliate_dashboard.html new file mode 100644 index 0000000..b9091df --- /dev/null +++ b/web/src/padelnomics/admin/templates/admin/affiliate_dashboard.html @@ -0,0 +1,121 @@ +{% extends "admin/base_admin.html" %} +{% set admin_page = "affiliate_dashboard" %} + +{% block title %}Affiliate Dashboard - Admin - {{ config.APP_NAME }}{% endblock %} + +{% block admin_content %} +
+

Affiliate Dashboard

+
+ {% for d in [7, 30, 90] %} + {{ d }}d + {% endfor %} +
+
+ + {# ── Stats strip ── #} +
+ +
+
Clicks ({{ days_count }}d)
+
{{ stats.total_clicks | int }}
+
+ +
+
Products
+
{{ stats.active_products or 0 }}
+
{{ stats.draft_products or 0 }} draft
+
+ +
+
Articles (clicked)
+
{{ article_count }}
+
+ +
+
Est. Revenue
+
~€{{ est_revenue }}
+
3% CR × €80 basket
+
+ +
+ + {# ── Daily bar chart ── #} + {% if stats.daily_bars %} +
+
Clicks · Last {{ days_count }} Days
+
+ {% for bar in stats.daily_bars %} +
+
+ {% endfor %} +
+
+ {{ stats.daily_bars[0].day if stats.daily_bars else '' }} + {{ stats.daily_bars[-1].day if stats.daily_bars else '' }} +
+
+ {% endif %} + +
+ + {# ── Top products ── #} +
+
Top Products
+ {% if stats.top_products %} + {% for p in stats.top_products %} +
+ {{ loop.index }} + + {{ p.name }} + + {{ p.click_count }} +
+ {% endfor %} + {% else %} +

No clicks yet.

+ {% endif %} +
+ + {# ── Top articles ── #} +
+
Top Articles
+ {% if stats.top_articles %} + {% for a in stats.top_articles %} +
+ {{ loop.index }} + {{ a.article_slug }} + {{ a.click_count }} +
+ {% endfor %} + {% else %} +

No clicks with article source yet.

+ {% endif %} +
+ +
+ + {# ── Clicks by retailer ── #} + {% if stats.by_retailer %} +
+
Clicks by Retailer
+ {% for r in stats.by_retailer %} +
+ + {{ r.retailer or 'Unknown' }} + +
+
+
+ + {{ r.click_count }} ({{ r.share_pct }}%) + +
+ {% endfor %} +
+ {% endif %} + +{% endblock %}