feat(admin): cross-section links across leads, suppliers, marketplace, emails

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 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-25 10:15:25 +01:00
parent 55f179ba54
commit e5960c08ff
6 changed files with 58 additions and 20 deletions

View File

@@ -829,21 +829,22 @@ async def marketplace_dashboard():
async def marketplace_activity(): async def marketplace_activity():
"""HTMX: Recent marketplace activity stream.""" """HTMX: Recent marketplace activity stream."""
rows = await fetch_all( 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, contact_name as actor, status as detail,
country as extra, created_at country as extra, created_at
FROM lead_requests WHERE lead_type = 'quote' FROM lead_requests WHERE lead_type = 'quote'
UNION ALL 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, s.name as actor, lf.status as detail,
CAST(lf.credit_cost AS TEXT) as extra, lf.created_at CAST(lf.credit_cost AS TEXT) as extra, lf.created_at
FROM lead_forwards lf FROM lead_forwards lf
JOIN suppliers s ON s.id = lf.supplier_id JOIN suppliers s ON s.id = lf.supplier_id
UNION ALL UNION ALL
SELECT 'credit' as event_type, id as ref_id, SELECT 'credit' as event_type, id as ref_id, supplier_id as ref2_id,
CAST(supplier_id AS TEXT) as actor, event_type as detail, s.name as actor, cl.event_type as detail,
CAST(delta AS TEXT) as extra, created_at CAST(cl.delta AS TEXT) as extra, cl.created_at
FROM credit_ledger FROM credit_ledger cl
JOIN suppliers s ON s.id = cl.supplier_id
ORDER BY created_at DESC LIMIT 50""" ORDER BY created_at DESC LIMIT 50"""
) )
return await render_template("admin/partials/marketplace_activity.html", events=rows) return await render_template("admin/partials/marketplace_activity.html", events=rows)
@@ -1285,10 +1286,19 @@ async def email_detail(email_id: int):
except Exception: except Exception:
logger.warning("Failed to fetch email body from Resend for %s", email["resend_id"], exc_info=True) 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( return await render_template(
"admin/email_detail.html", "admin/email_detail.html",
email=email, email=email,
enriched_html=enriched_html, enriched_html=enriched_html,
related_lead=related_lead,
related_supplier=related_supplier,
) )
@@ -1408,8 +1418,9 @@ async def email_compose():
email_addresses=EMAIL_ADDRESSES, email_addresses=EMAIL_ADDRESSES,
) )
prefill_to = request.args.get("to", "")
return await render_template( 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,
) )

View File

@@ -14,7 +14,15 @@
<h2 class="text-lg mb-4">Details</h2> <h2 class="text-lg mb-4">Details</h2>
<dl style="display:grid;grid-template-columns:100px 1fr;gap:6px 12px;font-size:0.8125rem"> <dl style="display:grid;grid-template-columns:100px 1fr;gap:6px 12px;font-size:0.8125rem">
<dt class="text-slate">To</dt> <dt class="text-slate">To</dt>
<dd>{{ email.to_addr }}</dd> <dd class="flex items-center gap-2 flex-wrap">
{{ email.to_addr }}
{% if related_lead %}
<a href="{{ url_for('admin.lead_detail', lead_id=related_lead.id) }}" class="badge" style="background:#DBEAFE;color:#1E40AF">Lead #{{ related_lead.id }}</a>
{% endif %}
{% if related_supplier %}
<a href="{{ url_for('admin.supplier_detail', supplier_id=related_supplier.id) }}" class="badge" style="background:#D1FAE5;color:#065F46">{{ related_supplier.name }}</a>
{% endif %}
</dd>
<dt class="text-slate">From</dt> <dt class="text-slate">From</dt>
<dd>{{ email.from_addr }}</dd> <dd>{{ email.from_addr }}</dd>
<dt class="text-slate">Subject</dt> <dt class="text-slate">Subject</dt>

View File

@@ -62,7 +62,10 @@
<dt class="text-slate">Courts</dt> <dd>{{ lead.court_count or '-' }}</dd> <dt class="text-slate">Courts</dt> <dd>{{ lead.court_count or '-' }}</dd>
<dt class="text-slate">Glass</dt> <dd>{{ lead.glass_type or '-' }}</dd> <dt class="text-slate">Glass</dt> <dd>{{ lead.glass_type or '-' }}</dd>
<dt class="text-slate">Lighting</dt> <dd>{{ lead.lighting_type or '-' }}</dd> <dt class="text-slate">Lighting</dt> <dd>{{ lead.lighting_type or '-' }}</dd>
<dt class="text-slate">Location</dt> <dd>{{ lead.location or '-' }}, {{ lead.country or '-' }}</dd> <dt class="text-slate">Location</dt>
<dd>{{ lead.location or '-' }}{% if lead.country %},
<a href="{{ url_for('admin.leads', country=lead.country) }}" class="text-sm">{{ lead.country }}</a>
{% else %}-{% endif %}</dd>
<dt class="text-slate">Phase</dt> <dd>{{ lead.location_status or '-' }}</dd> <dt class="text-slate">Phase</dt> <dd>{{ lead.location_status or '-' }}</dd>
<dt class="text-slate">Timeline</dt> <dd>{{ lead.timeline or '-' }}</dd> <dt class="text-slate">Timeline</dt> <dd>{{ lead.timeline or '-' }}</dd>
<dt class="text-slate">Budget</dt> <dd>{% if lead.budget_estimate %}€{{ "{:,}".format(lead.budget_estimate | int) }}{% else %}-{% endif %}</dd> <dt class="text-slate">Budget</dt> <dd>{% if lead.budget_estimate %}€{{ "{:,}".format(lead.budget_estimate | int) }}{% else %}-{% endif %}</dd>
@@ -79,7 +82,15 @@
<h2 class="text-lg mb-4">Contact</h2> <h2 class="text-lg mb-4">Contact</h2>
<dl style="display:grid;grid-template-columns:100px 1fr;gap:6px 12px;font-size:0.8125rem"> <dl style="display:grid;grid-template-columns:100px 1fr;gap:6px 12px;font-size:0.8125rem">
<dt class="text-slate">Name</dt> <dd>{{ lead.contact_name or '-' }}</dd> <dt class="text-slate">Name</dt> <dd>{{ lead.contact_name or '-' }}</dd>
<dt class="text-slate">Email</dt> <dd>{{ lead.contact_email or '-' }}</dd> <dt class="text-slate">Email</dt>
<dd class="flex items-center gap-2 flex-wrap">
{{ lead.contact_email or '-' }}
{% if lead.contact_email %}
<a href="{{ url_for('admin.emails', search=lead.contact_email) }}" class="text-xs text-slate" title="Email log">📧</a>
<a href="mailto:{{ lead.contact_email }}" class="text-xs text-slate" title="mailto"></a>
<a href="{{ url_for('admin.email_compose') }}?to={{ lead.contact_email }}" class="btn-outline btn-sm" style="padding:1px 8px;font-size:0.7rem">Send email</a>
{% endif %}
</dd>
<dt class="text-slate">Phone</dt> <dd>{{ lead.contact_phone or '-' }}</dd> <dt class="text-slate">Phone</dt> <dd>{{ lead.contact_phone or '-' }}</dd>
<dt class="text-slate">Company</dt> <dd>{{ lead.contact_company or '-' }}</dd> <dt class="text-slate">Company</dt> <dd>{{ lead.contact_company or '-' }}</dd>
<dt class="text-slate">Role</dt> <dd>{{ lead.stakeholder_type or '-' }}</dd> <dt class="text-slate">Role</dt> <dd>{{ lead.stakeholder_type or '-' }}</dd>

View File

@@ -38,22 +38,22 @@
<div class="grid-4 mb-8"> <div class="grid-4 mb-8">
<div class="card text-center"> <div class="card text-center">
<p class="card-header">Total Leads</p> <p class="card-header">Total Leads</p>
<p class="text-3xl font-bold text-navy">{{ funnel.total }}</p> <a href="{{ url_for('admin.leads') }}" class="text-3xl font-bold text-navy">{{ funnel.total }}</a>
</div> </div>
<div class="card text-center"> <div class="card text-center">
<p class="card-header">Verified New</p> <p class="card-header">Verified New</p>
<p class="text-3xl font-bold text-navy">{{ funnel.verified_new }}</p> <a href="{{ url_for('admin.leads', status='new') }}" class="text-3xl font-bold text-navy">{{ funnel.verified_new }}</a>
<p class="text-xs text-slate mt-1">ready to unlock</p> <p class="text-xs text-slate mt-1">ready to unlock</p>
</div> </div>
<div class="card text-center"> <div class="card text-center">
<p class="card-header">Unlocked</p> <p class="card-header">Unlocked</p>
<p class="text-3xl font-bold text-navy">{{ funnel.unlocked }}</p> <a href="{{ url_for('admin.leads', status='forwarded') }}" class="text-3xl font-bold text-navy">{{ funnel.unlocked }}</a>
<p class="text-xs text-slate mt-1">by suppliers</p> <p class="text-xs text-slate mt-1">by suppliers</p>
</div> </div>
<div class="card text-center"> <div class="card text-center">
<p class="card-header">Conversion</p> <p class="card-header">Conversion</p>
<p class="text-3xl font-bold {% if funnel.conversion_rate > 0 %}text-navy{% else %}text-slate{% endif %}">{{ funnel.conversion_rate }}%</p> <p class="text-3xl font-bold {% if funnel.conversion_rate > 0 %}text-navy{% else %}text-slate{% endif %}">{{ funnel.conversion_rate }}%</p>
<p class="text-xs text-slate mt-1">{{ funnel.won }} won</p> <p class="text-xs text-slate mt-1"><a href="{{ url_for('admin.leads', status='closed_won') }}">{{ funnel.won }} won</a></p>
</div> </div>
</div> </div>
@@ -86,7 +86,7 @@
<dl style="display:grid;grid-template-columns:1fr 1fr;gap:12px"> <dl style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div> <div>
<p class="text-xs text-slate">Active suppliers</p> <p class="text-xs text-slate">Active suppliers</p>
<p class="text-2xl font-bold text-navy">{{ suppliers.active }}</p> <a href="{{ url_for('admin.suppliers') }}" class="text-2xl font-bold text-navy">{{ suppliers.active }}</a>
<p class="text-xs text-slate mt-1">growth/pro w/ credits</p> <p class="text-xs text-slate mt-1">growth/pro w/ credits</p>
</div> </div>
<div> <div>

View File

@@ -7,16 +7,17 @@
<span class="activity-dot activity-dot-{{ ev.event_type }}"></span> <span class="activity-dot activity-dot-{{ ev.event_type }}"></span>
<div style="flex:1;font-size:0.8125rem"> <div style="flex:1;font-size:0.8125rem">
{% if ev.event_type == 'lead' %} {% if ev.event_type == 'lead' %}
<span class="font-semibold">New lead</span> <span class="font-semibold"><a href="{{ url_for('admin.lead_detail', lead_id=ev.ref_id) }}">New lead</a></span>
{% if ev.actor %} from {{ ev.actor }}{% endif %} {% if ev.actor %} from {{ ev.actor }}{% endif %}
{% if ev.extra %} — {{ ev.extra }}{% endif %} {% if ev.extra %} — {{ ev.extra }}{% endif %}
{% if ev.detail %} <span class="text-slate">({{ ev.detail }})</span>{% endif %} {% if ev.detail %} <span class="text-slate">({{ ev.detail }})</span>{% endif %}
{% elif ev.event_type == 'unlock' %} {% elif ev.event_type == 'unlock' %}
<span class="font-semibold">{{ ev.actor }}</span> unlocked a lead <a href="{{ url_for('admin.supplier_detail', supplier_id=ev.ref2_id) }}" class="font-semibold">{{ ev.actor }}</a>
unlocked <a href="{{ url_for('admin.lead_detail', lead_id=ev.ref_id) }}">lead #{{ ev.ref_id }}</a>
{% if ev.extra %} — {{ ev.extra }} credits{% endif %} {% if ev.extra %} — {{ ev.extra }} credits{% endif %}
{% if ev.detail and ev.detail != 'sent' %} <span class="text-slate">({{ ev.detail }})</span>{% endif %} {% if ev.detail and ev.detail != 'sent' %} <span class="text-slate">({{ ev.detail }})</span>{% endif %}
{% elif ev.event_type == 'credit' %} {% elif ev.event_type == 'credit' %}
Credit event <a href="{{ url_for('admin.supplier_detail', supplier_id=ev.ref2_id) }}">{{ ev.actor }}</a>
{% if ev.extra and ev.extra | int > 0 %}<span class="text-slate">+{{ ev.extra }}</span> {% if ev.extra and ev.extra | int > 0 %}<span class="text-slate">+{{ ev.extra }}</span>
{% elif ev.extra %}<span class="text-slate">{{ ev.extra }}</span>{% endif %} {% elif ev.extra %}<span class="text-slate">{{ ev.extra }}</span>{% endif %}
{% if ev.detail %} <span class="text-xs text-slate">({{ ev.detail }})</span>{% endif %} {% if ev.detail %} <span class="text-xs text-slate">({{ ev.detail }})</span>{% endif %}

View File

@@ -45,7 +45,14 @@
<dd>{% if supplier.website %}<a href="{{ supplier.website }}" target="_blank" class="text-sm">{{ supplier.website }}</a>{% else %}-{% endif %}</dd> <dd>{% if supplier.website %}<a href="{{ supplier.website }}" target="_blank" class="text-sm">{{ supplier.website }}</a>{% else %}-{% endif %}</dd>
<dt class="text-slate">Contact</dt> <dt class="text-slate">Contact</dt>
<dd>{{ supplier.contact_name or '-' }}<br> <dd>{{ supplier.contact_name or '-' }}<br>
<span class="text-xs text-slate">{{ supplier.contact_email or '-' }}</span></dd> <span class="text-xs text-slate flex items-center gap-2 flex-wrap mt-1">
{{ supplier.contact_email or '-' }}
{% if supplier.contact_email %}
<a href="{{ url_for('admin.emails', search=supplier.contact_email) }}" title="Email log">📧</a>
<a href="mailto:{{ supplier.contact_email }}" title="mailto"></a>
<a href="{{ url_for('admin.email_compose') }}?to={{ supplier.contact_email }}" class="btn-outline btn-sm" style="padding:1px 8px;font-size:0.7rem">Send email</a>
{% endif %}
</span></dd>
<dt class="text-slate">Tagline</dt> <dt class="text-slate">Tagline</dt>
<dd>{{ supplier.tagline or '-' }}</dd> <dd>{{ supplier.tagline or '-' }}</dd>
<dt class="text-slate">Description</dt> <dt class="text-slate">Description</dt>
@@ -73,7 +80,7 @@
<dt class="text-slate">Enquiries</dt> <dt class="text-slate">Enquiries</dt>
<dd>{{ enquiry_count }}</dd> <dd>{{ enquiry_count }}</dd>
<dt class="text-slate">Claimed By</dt> <dt class="text-slate">Claimed By</dt>
<dd>{% if supplier.claimed_by %}User #{{ supplier.claimed_by }}{% else %}Unclaimed{% endif %}</dd> <dd>{% if supplier.claimed_by %}<a href="{{ url_for('admin.user_detail', user_id=supplier.claimed_by) }}">User #{{ supplier.claimed_by }}</a>{% else %}Unclaimed{% endif %}</dd>
<dt class="text-slate">Created</dt> <dt class="text-slate">Created</dt>
<dd class="mono">{{ supplier.created_at or '-' }}</dd> <dd class="mono">{{ supplier.created_at or '-' }}</dd>
</dl> </dl>