From e5960c08ff34352d1deb72e81f365dd695d4b942 Mon Sep 17 00:00:00 2001 From: Deeman Date: Wed, 25 Feb 2026 10:15:25 +0100 Subject: [PATCH] feat(admin): cross-section links across leads, suppliers, marketplace, emails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lead detail: - contact_email → 📧 email log (pre-filtered), mailto, Send Email compose - country → leads list filtered by that country Supplier detail: - contact_email → 📧 email log (pre-filtered), mailto, Send Email compose - claimed_by → user detail page (was plain "User #N") Marketplace dashboard: - Funnel card numbers are now links: Total → /leads, Verified New → /leads?status=new, Unlocked → /leads?status=forwarded, Won → /leads?status=closed_won - Active suppliers number links to /suppliers Marketplace activity stream: - lead events → link to lead_detail - unlock events → supplier name links to supplier_detail, "lead #N" links to lead_detail - credit events → supplier name links to supplier_detail (query now joins suppliers table for name; ref2_id exposes supplier_id and lead_id per event) Email detail: - Reverse-lookup to_addr against lead_requests + suppliers; renders linked "Lead #N" / "Supplier Name" chips next to the To field Email compose: - Accepts ?to= query param to pre-fill recipient (enables Send Email links) Co-Authored-By: Claude Sonnet 4.6 --- web/src/padelnomics/admin/routes.py | 25 +++++++++++++------ .../admin/templates/admin/email_detail.html | 10 +++++++- .../admin/templates/admin/lead_detail.html | 15 +++++++++-- .../admin/templates/admin/marketplace.html | 10 ++++---- .../admin/partials/marketplace_activity.html | 7 +++--- .../templates/admin/supplier_detail.html | 11 ++++++-- 6 files changed, 58 insertions(+), 20 deletions(-) diff --git a/web/src/padelnomics/admin/routes.py b/web/src/padelnomics/admin/routes.py index 64e103c..32bf36d 100644 --- a/web/src/padelnomics/admin/routes.py +++ b/web/src/padelnomics/admin/routes.py @@ -829,21 +829,22 @@ async def marketplace_dashboard(): async def marketplace_activity(): """HTMX: Recent marketplace activity stream.""" rows = await fetch_all( - """SELECT 'lead' as event_type, id as ref_id, + """SELECT 'lead' as event_type, id as ref_id, NULL as ref2_id, contact_name as actor, status as detail, country as extra, created_at FROM lead_requests WHERE lead_type = 'quote' UNION ALL - SELECT 'unlock' as event_type, lf.id as ref_id, + SELECT 'unlock' as event_type, lf.lead_id as ref_id, lf.supplier_id as ref2_id, s.name as actor, lf.status as detail, CAST(lf.credit_cost AS TEXT) as extra, lf.created_at FROM lead_forwards lf JOIN suppliers s ON s.id = lf.supplier_id UNION ALL - SELECT 'credit' as event_type, id as ref_id, - CAST(supplier_id AS TEXT) as actor, event_type as detail, - CAST(delta AS TEXT) as extra, created_at - FROM credit_ledger + SELECT 'credit' as event_type, id as ref_id, supplier_id as ref2_id, + s.name as actor, cl.event_type as detail, + CAST(cl.delta AS TEXT) as extra, cl.created_at + FROM credit_ledger cl + JOIN suppliers s ON s.id = cl.supplier_id ORDER BY created_at DESC LIMIT 50""" ) return await render_template("admin/partials/marketplace_activity.html", events=rows) @@ -1285,10 +1286,19 @@ async def email_detail(email_id: int): except Exception: logger.warning("Failed to fetch email body from Resend for %s", email["resend_id"], exc_info=True) + related_lead = await fetch_one( + "SELECT id FROM lead_requests WHERE contact_email = ? LIMIT 1", (email["to_addr"],) + ) + related_supplier = await fetch_one( + "SELECT id, name FROM suppliers WHERE contact_email = ? LIMIT 1", (email["to_addr"],) + ) + return await render_template( "admin/email_detail.html", email=email, enriched_html=enriched_html, + related_lead=related_lead, + related_supplier=related_supplier, ) @@ -1408,8 +1418,9 @@ async def email_compose(): email_addresses=EMAIL_ADDRESSES, ) + prefill_to = request.args.get("to", "") return await render_template( - "admin/email_compose.html", data={}, email_addresses=EMAIL_ADDRESSES, + "admin/email_compose.html", data={"to": prefill_to}, email_addresses=EMAIL_ADDRESSES, ) diff --git a/web/src/padelnomics/admin/templates/admin/email_detail.html b/web/src/padelnomics/admin/templates/admin/email_detail.html index 597851a..46ed4ce 100644 --- a/web/src/padelnomics/admin/templates/admin/email_detail.html +++ b/web/src/padelnomics/admin/templates/admin/email_detail.html @@ -14,7 +14,15 @@

Details

To
-
{{ email.to_addr }}
+
+ {{ email.to_addr }} + {% if related_lead %} + Lead #{{ related_lead.id }} + {% endif %} + {% if related_supplier %} + {{ related_supplier.name }} + {% endif %} +
From
{{ email.from_addr }}
Subject
diff --git a/web/src/padelnomics/admin/templates/admin/lead_detail.html b/web/src/padelnomics/admin/templates/admin/lead_detail.html index 423a1fe..f6ef05e 100644 --- a/web/src/padelnomics/admin/templates/admin/lead_detail.html +++ b/web/src/padelnomics/admin/templates/admin/lead_detail.html @@ -62,7 +62,10 @@
Courts
{{ lead.court_count or '-' }}
Glass
{{ lead.glass_type or '-' }}
Lighting
{{ lead.lighting_type or '-' }}
-
Location
{{ lead.location or '-' }}, {{ lead.country or '-' }}
+
Location
+
{{ lead.location or '-' }}{% if lead.country %}, + {{ lead.country }} + {% else %}-{% endif %}
Phase
{{ lead.location_status or '-' }}
Timeline
{{ lead.timeline or '-' }}
Budget
{% if lead.budget_estimate %}€{{ "{:,}".format(lead.budget_estimate | int) }}{% else %}-{% endif %}
@@ -79,7 +82,15 @@

Contact

Name
{{ lead.contact_name or '-' }}
-
Email
{{ lead.contact_email or '-' }}
+
Email
+
+ {{ lead.contact_email or '-' }} + {% if lead.contact_email %} + 📧 + + Send email + {% endif %} +
Phone
{{ lead.contact_phone or '-' }}
Company
{{ lead.contact_company or '-' }}
Role
{{ lead.stakeholder_type or '-' }}
diff --git a/web/src/padelnomics/admin/templates/admin/marketplace.html b/web/src/padelnomics/admin/templates/admin/marketplace.html index 39e56b2..37a35aa 100644 --- a/web/src/padelnomics/admin/templates/admin/marketplace.html +++ b/web/src/padelnomics/admin/templates/admin/marketplace.html @@ -38,22 +38,22 @@

Total Leads

-

{{ funnel.total }}

+ {{ funnel.total }}

Verified New

-

{{ funnel.verified_new }}

+ {{ funnel.verified_new }}

ready to unlock

Unlocked

-

{{ funnel.unlocked }}

+ {{ funnel.unlocked }}

by suppliers

Conversion

{{ funnel.conversion_rate }}%

-

{{ funnel.won }} won

+

{{ funnel.won }} won

@@ -86,7 +86,7 @@

Active suppliers

-

{{ suppliers.active }}

+ {{ suppliers.active }}

growth/pro w/ credits

diff --git a/web/src/padelnomics/admin/templates/admin/partials/marketplace_activity.html b/web/src/padelnomics/admin/templates/admin/partials/marketplace_activity.html index 09e7d06..569dd93 100644 --- a/web/src/padelnomics/admin/templates/admin/partials/marketplace_activity.html +++ b/web/src/padelnomics/admin/templates/admin/partials/marketplace_activity.html @@ -7,16 +7,17 @@
{% if ev.event_type == 'lead' %} - New lead + New lead {% if ev.actor %} from {{ ev.actor }}{% endif %} {% if ev.extra %} — {{ ev.extra }}{% endif %} {% if ev.detail %} ({{ ev.detail }}){% endif %} {% elif ev.event_type == 'unlock' %} - {{ ev.actor }} unlocked a lead + {{ ev.actor }} + unlocked lead #{{ ev.ref_id }} {% if ev.extra %} — {{ ev.extra }} credits{% endif %} {% if ev.detail and ev.detail != 'sent' %} ({{ ev.detail }}){% endif %} {% elif ev.event_type == 'credit' %} - Credit event + {{ ev.actor }} {% if ev.extra and ev.extra | int > 0 %}+{{ ev.extra }} {% elif ev.extra %}{{ ev.extra }}{% endif %} {% if ev.detail %} ({{ ev.detail }}){% endif %} diff --git a/web/src/padelnomics/admin/templates/admin/supplier_detail.html b/web/src/padelnomics/admin/templates/admin/supplier_detail.html index 67fcc3d..abdb49f 100644 --- a/web/src/padelnomics/admin/templates/admin/supplier_detail.html +++ b/web/src/padelnomics/admin/templates/admin/supplier_detail.html @@ -45,7 +45,14 @@
{% if supplier.website %}{{ supplier.website }}{% else %}-{% endif %}
Contact
{{ supplier.contact_name or '-' }}
- {{ supplier.contact_email or '-' }}
+ + {{ supplier.contact_email or '-' }} + {% if supplier.contact_email %} + 📧 + + Send email + {% endif %} +
Tagline
{{ supplier.tagline or '-' }}
Description
@@ -73,7 +80,7 @@
Enquiries
{{ enquiry_count }}
Claimed By
-
{% if supplier.claimed_by %}User #{{ supplier.claimed_by }}{% else %}Unclaimed{% endif %}
+
{% if supplier.claimed_by %}User #{{ supplier.claimed_by }}{% else %}Unclaimed{% endif %}
Created
{{ supplier.created_at or '-' }}