diff --git a/web/src/padelnomics/admin/pipeline_routes.py b/web/src/padelnomics/admin/pipeline_routes.py index 3bc926f..9e1f354 100644 --- a/web/src/padelnomics/admin/pipeline_routes.py +++ b/web/src/padelnomics/admin/pipeline_routes.py @@ -35,7 +35,7 @@ from pathlib import Path from quart import Blueprint, flash, redirect, render_template, request, url_for from ..auth.routes import role_required -from ..core import csrf_protect +from ..core import count_where, csrf_protect logger = logging.getLogger(__name__) @@ -298,11 +298,8 @@ async def _inject_sidebar_data(): """Load unread inbox count for the admin sidebar badge.""" from quart import g - from ..core import fetch_one - try: - row = await fetch_one("SELECT COUNT(*) as cnt FROM inbound_emails WHERE is_read = 0") - g.admin_unread_count = row["cnt"] if row else 0 + g.admin_unread_count = await count_where("inbound_emails WHERE is_read = 0") except Exception: g.admin_unread_count = 0 diff --git a/web/src/padelnomics/admin/pseo_routes.py b/web/src/padelnomics/admin/pseo_routes.py index 183684d..f35ec26 100644 --- a/web/src/padelnomics/admin/pseo_routes.py +++ b/web/src/padelnomics/admin/pseo_routes.py @@ -25,7 +25,7 @@ from ..content.health import ( get_template_freshness, get_template_stats, ) -from ..core import csrf_protect, fetch_all, fetch_one +from ..core import count_where, csrf_protect, fetch_all, fetch_one bp = Blueprint( "pseo", @@ -41,8 +41,7 @@ async def _inject_sidebar_data(): from quart import g try: - row = await fetch_one("SELECT COUNT(*) as cnt FROM inbound_emails WHERE is_read = 0") - g.admin_unread_count = row["cnt"] if row else 0 + g.admin_unread_count = await count_where("inbound_emails WHERE is_read = 0") except Exception: g.admin_unread_count = 0 @@ -80,8 +79,7 @@ async def pseo_dashboard(): total_published = sum(r["stats"]["published"] for r in template_rows) stale_count = sum(1 for f in freshness if f["status"] == "stale") - noindex_row = await fetch_one("SELECT COUNT(*) as cnt FROM articles WHERE noindex = 1") - noindex_count = noindex_row["cnt"] if noindex_row else 0 + noindex_count = await count_where("articles WHERE noindex = 1") # Recent generation jobs — enough for the dashboard summary. jobs = await fetch_all( diff --git a/web/src/padelnomics/dashboard/routes.py b/web/src/padelnomics/dashboard/routes.py index 6afb375..e446a8f 100644 --- a/web/src/padelnomics/dashboard/routes.py +++ b/web/src/padelnomics/dashboard/routes.py @@ -6,7 +6,7 @@ from pathlib import Path from quart import Blueprint, flash, g, redirect, render_template, request, url_for from ..auth.routes import login_required, update_user -from ..core import csrf_protect, fetch_one, soft_delete, utcnow_iso +from ..core import count_where, csrf_protect, fetch_one, soft_delete, utcnow_iso from ..i18n import get_translations bp = Blueprint( @@ -18,17 +18,13 @@ bp = Blueprint( async def get_user_stats(user_id: int) -> dict: - scenarios = await fetch_one( - "SELECT COUNT(*) as count FROM scenarios WHERE user_id = ? AND deleted_at IS NULL", - (user_id,), - ) - leads = await fetch_one( - "SELECT COUNT(*) as count FROM lead_requests WHERE user_id = ?", - (user_id,), - ) return { - "scenarios": scenarios["count"] if scenarios else 0, - "leads": leads["count"] if leads else 0, + "scenarios": await count_where( + "scenarios WHERE user_id = ? AND deleted_at IS NULL", (user_id,) + ), + "leads": await count_where( + "lead_requests WHERE user_id = ?", (user_id,) + ), } diff --git a/web/src/padelnomics/directory/routes.py b/web/src/padelnomics/directory/routes.py index a7f46cd..c1b450f 100644 --- a/web/src/padelnomics/directory/routes.py +++ b/web/src/padelnomics/directory/routes.py @@ -6,7 +6,7 @@ from pathlib import Path from quart import Blueprint, g, make_response, redirect, render_template, request, url_for -from ..core import csrf_protect, execute, fetch_all, fetch_one, utcnow_iso +from ..core import count_where, csrf_protect, execute, fetch_all, fetch_one, utcnow_iso from ..i18n import COUNTRY_LABELS, get_translations bp = Blueprint( @@ -79,11 +79,7 @@ async def _build_directory_query(q, country, category, region, page, per_page=24 where = " AND ".join(wheres) if wheres else "1=1" - count_row = await fetch_one( - f"SELECT COUNT(*) as cnt FROM suppliers s WHERE {where}", - tuple(params), - ) - total = count_row["cnt"] if count_row else 0 + total = await count_where(f"suppliers s WHERE {where}", tuple(params)) offset = (page - 1) * per_page # Tier-based ordering: sticky first, then pro > growth > free, then name @@ -159,16 +155,16 @@ async def index(): "SELECT category, COUNT(*) as cnt FROM suppliers GROUP BY category ORDER BY cnt DESC" ) - total_suppliers = await fetch_one("SELECT COUNT(*) as cnt FROM suppliers") - total_countries = await fetch_one("SELECT COUNT(DISTINCT country_code) as cnt FROM suppliers") + total_suppliers = await count_where("suppliers") + total_countries = await count_where("(SELECT DISTINCT country_code FROM suppliers)") return await render_template( "directory.html", **ctx, country_counts=country_counts, category_counts=category_counts, - total_suppliers=total_suppliers["cnt"] if total_suppliers else 0, - total_countries=total_countries["cnt"] if total_countries else 0, + total_suppliers=total_suppliers, + total_countries=total_countries, ) @@ -204,11 +200,9 @@ async def supplier_detail(slug: str): # Enquiry count (Basic+) enquiry_count = 0 if supplier.get("tier") in ("basic", "growth", "pro"): - row = await fetch_one( - "SELECT COUNT(*) as cnt FROM supplier_enquiries WHERE supplier_id = ?", - (supplier["id"],), + enquiry_count = await count_where( + "supplier_enquiries WHERE supplier_id = ?", (supplier["id"],) ) - enquiry_count = row["cnt"] if row else 0 lang = g.get("lang", "en") cat_labels, country_labels, region_labels = get_directory_labels(lang) diff --git a/web/src/padelnomics/planner/routes.py b/web/src/padelnomics/planner/routes.py index 50ed5ef..a236205 100644 --- a/web/src/padelnomics/planner/routes.py +++ b/web/src/padelnomics/planner/routes.py @@ -12,6 +12,7 @@ from quart import Blueprint, Response, g, jsonify, render_template, request from ..auth.routes import login_required from ..core import ( config, + count_where, csrf_protect, execute, feature_gate, @@ -50,11 +51,9 @@ COUNTRY_PRESETS = { async def count_scenarios(user_id: int) -> int: - row = await fetch_one( - "SELECT COUNT(*) as cnt FROM scenarios WHERE user_id = ? AND deleted_at IS NULL", - (user_id,), + return await count_where( + "scenarios WHERE user_id = ? AND deleted_at IS NULL", (user_id,) ) - return row["cnt"] if row else 0 async def get_default_scenario(user_id: int) -> dict | None: diff --git a/web/src/padelnomics/public/routes.py b/web/src/padelnomics/public/routes.py index 94b9eb4..43d945f 100644 --- a/web/src/padelnomics/public/routes.py +++ b/web/src/padelnomics/public/routes.py @@ -5,7 +5,7 @@ from pathlib import Path from quart import Blueprint, g, render_template, request, session -from ..core import check_rate_limit, csrf_protect, execute, fetch_all, fetch_one +from ..core import check_rate_limit, count_where, csrf_protect, execute, fetch_all, fetch_one from ..i18n import get_translations bp = Blueprint( @@ -17,13 +17,9 @@ bp = Blueprint( async def _supplier_counts(): """Fetch aggregate supplier stats for landing/marketing pages.""" - total = await fetch_one("SELECT COUNT(*) as cnt FROM suppliers") - countries = await fetch_one( - "SELECT COUNT(DISTINCT country_code) as cnt FROM suppliers" - ) return ( - total["cnt"] if total else 0, - countries["cnt"] if countries else 0, + await count_where("suppliers"), + await count_where("(SELECT DISTINCT country_code FROM suppliers)"), ) @@ -75,15 +71,15 @@ async def suppliers(): total_suppliers, total_countries = await _supplier_counts() # Live stats - calc_requests = await fetch_one("SELECT COUNT(*) as cnt FROM scenarios WHERE deleted_at IS NULL") + calc_requests = await count_where("scenarios WHERE deleted_at IS NULL") avg_budget = await fetch_one( "SELECT AVG(budget_estimate) as avg FROM lead_requests WHERE budget_estimate > 0 AND lead_type = 'quote'" ) - active_suppliers = await fetch_one( - "SELECT COUNT(*) as cnt FROM suppliers WHERE tier IN ('growth', 'pro') AND claimed_by IS NOT NULL" + active_suppliers = await count_where( + "suppliers WHERE tier IN ('growth', 'pro') AND claimed_by IS NOT NULL" ) - monthly_leads = await fetch_one( - "SELECT COUNT(*) as cnt FROM lead_requests WHERE lead_type = 'quote' AND created_at >= date('now', '-30 days')" + monthly_leads = await count_where( + "lead_requests WHERE lead_type = 'quote' AND created_at >= date('now', '-30 days')" ) # Lead feed preview — 3 recent verified hot/warm leads, anonymized @@ -100,10 +96,10 @@ async def suppliers(): "suppliers.html", total_suppliers=total_suppliers, total_countries=total_countries, - calc_requests=calc_requests["cnt"] if calc_requests else 0, + calc_requests=calc_requests, avg_budget=int(avg_budget["avg"]) if avg_budget and avg_budget["avg"] else 0, - active_suppliers=active_suppliers["cnt"] if active_suppliers else 0, - monthly_leads=monthly_leads["cnt"] if monthly_leads else 0, + active_suppliers=active_suppliers, + monthly_leads=monthly_leads, preview_leads=preview_leads, ) diff --git a/web/src/padelnomics/suppliers/routes.py b/web/src/padelnomics/suppliers/routes.py index 2d51faf..afbd394 100644 --- a/web/src/padelnomics/suppliers/routes.py +++ b/web/src/padelnomics/suppliers/routes.py @@ -11,6 +11,7 @@ from werkzeug.utils import secure_filename from ..core import ( capture_waitlist_email, config, + count_where, csrf_protect, execute, feature_gate, @@ -776,9 +777,8 @@ async def dashboard_overview(): supplier = g.supplier # Leads unlocked count - unlocked = await fetch_one( - "SELECT COUNT(*) as cnt FROM lead_forwards WHERE supplier_id = ?", - (supplier["id"],), + leads_unlocked = await count_where( + "lead_forwards WHERE supplier_id = ?", (supplier["id"],) ) # New leads matching supplier's area since last login @@ -787,22 +787,20 @@ async def dashboard_overview(): new_leads_count = 0 if service_area: placeholders = ",".join("?" * len(service_area)) - row = await fetch_one( - f"""SELECT COUNT(*) as cnt FROM lead_requests + new_leads_count = await count_where( + f"""lead_requests WHERE lead_type = 'quote' AND status = 'new' AND verified_at IS NOT NULL AND country IN ({placeholders}) AND NOT EXISTS (SELECT 1 FROM lead_forwards WHERE lead_id = lead_requests.id AND supplier_id = ?)""", (*service_area, supplier["id"]), ) - new_leads_count = row["cnt"] if row else 0 else: - row = await fetch_one( - """SELECT COUNT(*) as cnt FROM lead_requests + new_leads_count = await count_where( + """lead_requests WHERE lead_type = 'quote' AND status = 'new' AND verified_at IS NOT NULL AND NOT EXISTS (SELECT 1 FROM lead_forwards WHERE lead_id = lead_requests.id AND supplier_id = ?)""", (supplier["id"],), ) - new_leads_count = row["cnt"] if row else 0 # Recent activity (last 10 events from credit_ledger + lead_forwards) recent_activity = await fetch_all( @@ -825,16 +823,14 @@ async def dashboard_overview(): # Enquiry count for Basic tier enquiry_count = 0 if supplier.get("tier") == "basic": - eq_row = await fetch_one( - "SELECT COUNT(*) as cnt FROM supplier_enquiries WHERE supplier_id = ?", - (supplier["id"],), + enquiry_count = await count_where( + "supplier_enquiries WHERE supplier_id = ?", (supplier["id"],) ) - enquiry_count = eq_row["cnt"] if eq_row else 0 return await render_template( "suppliers/partials/dashboard_overview.html", supplier=supplier, - leads_unlocked=unlocked["cnt"] if unlocked else 0, + leads_unlocked=leads_unlocked, new_leads_count=new_leads_count, recent_activity=recent_activity, active_boosts=active_boosts,