diff --git a/web/src/padelnomics/admin/routes.py b/web/src/padelnomics/admin/routes.py index 6613cae..da5cdea 100644 --- a/web/src/padelnomics/admin/routes.py +++ b/web/src/padelnomics/admin/routes.py @@ -1581,19 +1581,112 @@ async def scenario_pdf(scenario_id: int): # Article Management # ============================================================================= +async def _get_article_list( + status: str = None, + template_slug: str = None, + language: str = None, + search: str = None, + page: int = 1, + per_page: int = 50, +) -> list[dict]: + """Get articles with optional filters and pagination.""" + wheres = ["1=1"] + params: list = [] + + if status == "live": + wheres.append("status = 'published' AND published_at <= datetime('now')") + elif status == "scheduled": + wheres.append("status = 'published' AND published_at > datetime('now')") + elif status == "draft": + wheres.append("status = 'draft'") + if template_slug: + wheres.append("template_slug = ?") + params.append(template_slug) + if language: + wheres.append("language = ?") + params.append(language) + if search: + wheres.append("title LIKE ?") + params.append(f"%{search}%") + + where = " AND ".join(wheres) + offset = (page - 1) * per_page + params.extend([per_page, offset]) + + return await fetch_all( + f"""SELECT *, + CASE WHEN status = 'published' AND published_at > datetime('now') + THEN 'scheduled' + WHEN status = 'published' THEN 'live' + ELSE status END AS display_status + FROM articles WHERE {where} + ORDER BY created_at DESC LIMIT ? OFFSET ?""", + tuple(params), + ) + + +async def _get_article_stats() -> dict: + """Get aggregate article stats for the admin list header.""" + row = await fetch_one( + """SELECT + COUNT(*) AS total, + SUM(CASE WHEN status='published' AND published_at <= datetime('now') THEN 1 ELSE 0 END) AS live, + SUM(CASE WHEN status='published' AND published_at > datetime('now') THEN 1 ELSE 0 END) AS scheduled, + SUM(CASE WHEN status='draft' THEN 1 ELSE 0 END) AS draft + FROM articles""" + ) + return dict(row) if row else {"total": 0, "live": 0, "scheduled": 0, "draft": 0} + + @bp.route("/articles") @role_required("admin") async def articles(): - """List all articles.""" - article_list = await fetch_all( - """SELECT *, - CASE WHEN status = 'published' AND published_at > datetime('now') - THEN 'scheduled' - WHEN status = 'published' THEN 'live' - ELSE status END AS display_status - FROM articles ORDER BY created_at DESC""" + """List all articles with filters.""" + search = request.args.get("search", "").strip() + status_filter = request.args.get("status", "") + template_filter = request.args.get("template", "") + language_filter = request.args.get("language", "") + page = max(1, int(request.args.get("page", "1") or "1")) + + article_list = await _get_article_list( + status=status_filter or None, template_slug=template_filter or None, + language=language_filter or None, search=search or None, page=page, + ) + stats = await _get_article_stats() + templates = await fetch_all( + "SELECT DISTINCT template_slug FROM articles WHERE template_slug IS NOT NULL ORDER BY template_slug" + ) + + return await render_template( + "admin/articles.html", + articles=article_list, + stats=stats, + template_slugs=[t["template_slug"] for t in templates], + current_search=search, + current_status=status_filter, + current_template=template_filter, + current_language=language_filter, + page=page, + ) + + +@bp.route("/articles/results") +@role_required("admin") +async def article_results(): + """HTMX partial for filtered article results.""" + search = request.args.get("search", "").strip() + status_filter = request.args.get("status", "") + template_filter = request.args.get("template", "") + language_filter = request.args.get("language", "") + page = max(1, int(request.args.get("page", "1") or "1")) + + article_list = await _get_article_list( + status=status_filter or None, template_slug=template_filter or None, + language=language_filter or None, search=search or None, page=page, + ) + return await render_template( + "admin/partials/article_results.html", articles=article_list, page=page, ) - return await render_template("admin/articles.html", articles=article_list) @bp.route("/articles/new", methods=["GET", "POST"]) diff --git a/web/src/padelnomics/admin/templates/admin/articles.html b/web/src/padelnomics/admin/templates/admin/articles.html index 81392c6..f5a4af9 100644 --- a/web/src/padelnomics/admin/templates/admin/articles.html +++ b/web/src/padelnomics/admin/templates/admin/articles.html @@ -4,71 +4,72 @@ {% block title %}Articles - Admin - {{ config.APP_NAME }}{% endblock %} {% block admin_content %} -
+

Articles

-

{{ articles | length }} article{{ 's' if articles | length != 1 }}

+

+ {{ stats.total }} total + · {{ stats.live }} live + · {{ stats.scheduled }} scheduled + · {{ stats.draft }} draft +

- New Article -
+ New Article + - +
- Back
-
- {% if articles %} - - - - - - - - - - - - - {% for a in articles %} - - - - - - - - - {% endfor %} - -
TitleURLStatusPublishedSource
{{ a.title }}{{ a.url_path }} - {% if a.display_status == 'live' %} - Published - {% elif a.display_status == 'scheduled' %} - Scheduled - {% else %} - Draft - {% endif %} - {{ a.published_at[:10] if a.published_at else '-' }}{% if a.template_slug %}{{ a.template_slug }}{% else %}Manual{% endif %} -
- - -
- Edit -
- - -
-
- - -
-
- {% else %} -

No articles yet. Create one or generate from a template.

- {% endif %} + {# Filters #} +
+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + {# Results #} +
+ {% include "admin/partials/article_results.html" %}
{% endblock %} diff --git a/web/src/padelnomics/admin/templates/admin/partials/article_results.html b/web/src/padelnomics/admin/templates/admin/partials/article_results.html new file mode 100644 index 0000000..5756cb1 --- /dev/null +++ b/web/src/padelnomics/admin/templates/admin/partials/article_results.html @@ -0,0 +1,73 @@ +{% if articles %} +
+ + + + + + + + + + + + + {% for a in articles %} + + + + + + + + + {% endfor %} + +
TitleStatusPublishedLangTemplate
{{ a.title }} + {% if a.display_status == 'live' %} + Live + {% elif a.display_status == 'scheduled' %} + Scheduled + {% else %} + Draft + {% endif %} + {{ a.published_at[:10] if a.published_at else '-' }}{{ a.language | upper if a.language else '-' }}{{ a.template_slug or 'Manual' }} + {% if a.display_status == 'live' %} + View + {% endif %} + Edit +
+ + +
+
+ + +
+
+ {% if articles | length >= 50 %} +
+ {% if page > 1 %} + + {% else %} + + {% endif %} + Page {{ page }} + +
+ {% endif %} +
+{% else %} +
+

No articles match the current filters.

+
+{% endif %}