{% extends '@System/layouts/main_layout.html.twig' %}
{% block title %}LeadGen — Outbound Prospects{% endblock %}
{% block body %}
<style>
/* This is a central/super-admin surface: the tenant ERP module rail (Finance/POS/…) does not
apply here — hide it page-scoped and let the LeadGen sidebar carry the admin links instead. */
.cp-shell > .cp-rail { display: none !important; }
/* The shell's default 1.12 content zoom reads too small on this dense two-column page. */
.cp-content.cp-content-zoom { zoom: 1.3; }
.lg-wrap { padding: 4px 22px 60px; }
.lg-head { display:flex; align-items:flex-end; justify-content:space-between; gap:16px; margin-bottom:16px; flex-wrap:wrap; }
.lg-head h1 { font-family:var(--cp-font-head); font-size:var(--cp-fs-2xl); font-weight:700; color:var(--cp-text); margin:0; }
.lg-head p { margin:5px 0 0; color:var(--cp-text-faint); font-size:var(--cp-fs-base); }
.lg-banner { border-radius:var(--cp-radius); padding:10px 14px; font-size:var(--cp-fs-base); margin-bottom:14px; }
.lg-banner--warn { background:rgba(186,26,26,.08); color:var(--cp-error); border:1px solid rgba(186,26,26,.25); }
.lg-banner--info { background:var(--cp-amber-dim); color:var(--cp-primary); }
.lg-grid { display:grid; grid-template-columns: 2.2fr 1fr; gap:16px; align-items:start; }
@media (max-width: 1100px) { .lg-grid { grid-template-columns: 1fr; } }
.lg-card { background:var(--cp-surface-white); border-radius:var(--cp-radius-lg); box-shadow:var(--cp-shadow-card); overflow:hidden; margin-bottom:16px; }
.lg-card h2 { font-size:var(--cp-fs-md); font-weight:700; color:var(--cp-text); margin:0; padding:14px 16px 10px; }
.lg-card .lg-sub { padding:0 16px 10px; color:var(--cp-text-faint); font-size:var(--cp-fs-sm); }
.lg-table { width:100%; border-collapse:collapse; font-size:var(--cp-fs-base); }
.lg-table th { padding:9px 12px; text-align:left; background:var(--cp-surface-low); color:var(--cp-text-faint); font-size:var(--cp-fs-xs); font-weight:700; text-transform:uppercase; letter-spacing:.05em; border-bottom:1px solid var(--cp-outline-variant); }
.lg-table td { padding:9px 12px; border-bottom:1px solid var(--cp-outline-variant); color:var(--cp-text); vertical-align:top; }
.lg-table tr:last-child td { border-bottom:none; }
.lg-table tr:hover td { background:var(--cp-surface); }
.lg-pill { display:inline-block; border-radius:20px; padding:2px 9px; font-size:var(--cp-fs-xs); font-weight:700; }
.lg-pill--new { background:var(--cp-amber-dim); color:var(--cp-primary); }
.lg-pill--enriched { background:rgba(46,125,50,.12); color:var(--cp-success); }
.lg-pill--no_site { background:rgba(95,99,99,.12); color:var(--cp-text-faint); }
.lg-pill--suppressed { background:rgba(186,26,26,.10); color:var(--cp-error); }
.lg-pill--matched { background:rgba(46,125,50,.12); color:var(--cp-success); }
.lg-pill--drafted, .lg-pill--sent { background:var(--cp-amber-dim); color:var(--cp-primary); }
.lg-pill--converted, .lg-pill--replied { background:rgba(29,91,158,.12); color:#1d5b9e; }
.lg-pill--low_fit { background:rgba(95,99,99,.12); color:var(--cp-text-faint); }
.lg-pill--unresolved { background:rgba(186,26,26,.10); color:var(--cp-error); }
.lg-pill--crm { background:rgba(29,91,158,.12); color:#1d5b9e; }
.lg-btn { border:none; border-radius:var(--cp-radius); padding:6px 12px; font-size:var(--cp-fs-sm); font-weight:600; cursor:pointer; font-family:var(--cp-font-body); }
.lg-btn--primary { background:var(--cp-primary); color:var(--cp-on-primary); }
.lg-btn--primary:hover { background:var(--cp-primary-hover); }
.lg-btn--ghost { background:var(--cp-surface-white); border:1px solid var(--cp-outline-variant); color:var(--cp-primary); }
.lg-btn[disabled] { opacity:.45; cursor:not-allowed; }
.lg-filters { display:flex; gap:8px; padding:0 16px 12px; flex-wrap:wrap; }
.lg-filters a { text-decoration:none; font-size:var(--cp-fs-sm); font-weight:600; padding:4px 10px; border-radius:20px; border:1px solid var(--cp-outline-variant); color:var(--cp-text-faint); }
.lg-filters a.active { background:var(--cp-primary); color:var(--cp-on-primary); border-color:var(--cp-primary); }
.lg-form { padding:0 16px 16px; }
.lg-form textarea { width:100%; min-height:90px; border:1px solid var(--cp-outline-variant); border-radius:var(--cp-radius); padding:8px 10px; font-size:var(--cp-fs-base); font-family:var(--cp-font-mono, monospace); background:var(--cp-surface-white); box-sizing:border-box; }
.lg-form input[type=text] { width:100%; border:1px solid var(--cp-outline-variant); border-radius:var(--cp-radius); padding:7px 10px; font-size:var(--cp-fs-base); background:var(--cp-surface-white); box-sizing:border-box; margin-bottom:8px; }
.lg-form .lg-row { display:flex; gap:8px; align-items:center; margin-top:8px; flex-wrap:wrap; }
.lg-dns { padding:0 16px 14px; font-size:var(--cp-fs-base); }
.lg-dns .ok { color:var(--cp-success); font-weight:700; }
.lg-dns .miss { color:var(--cp-error); font-weight:700; }
.lg-dns div { margin-bottom:4px; }
.lg-muted { color:var(--cp-text-faint); font-size:var(--cp-fs-sm); }
.lg-enrich-detail { font-size:var(--cp-fs-sm); color:var(--cp-text-faint); max-width:340px; }
#lg-toast { position:fixed; bottom:24px; right:24px; background:var(--cp-text); color:#fff; border-radius:var(--cp-radius); padding:10px 16px; font-size:var(--cp-fs-base); display:none; z-index:1300; max-width:420px; }
</style>
<div class="lg-wrap">
<div class="lg-head">
<div>
<h1>LeadGen — Outbound Prospects</h1>
<p>Product A: HoneyBee's own outbound. AI suggests, a human approves — nothing cold-sends without approval.</p>
</div>
<div style="display:flex;gap:8px;">
<button class="lg-btn lg-btn--ghost" id="lg-resolve-batch" {{ is_central ? '' : 'disabled' }} title="Find websites for name-only prospects">Find sites (names)</button>
<button class="lg-btn lg-btn--primary" id="lg-enrich-batch" {{ is_central ? '' : 'disabled' }}>Enrich next 10</button>
<button class="lg-btn lg-btn--ghost" id="lg-match-batch" {{ is_central ? '' : 'disabled' }}>Fit-match all enriched</button>
</div>
</div>
{% if not is_central %}
<div class="lg-banner lg-banner--info">Read-only view — ingestion, enrichment and suppression management run on the central server.</div>
{% endif %}
{% if placeholder_secret %}
<div class="lg-banner lg-banner--warn">The unsubscribe-token secret is still the shipped placeholder — rotate <code>LEADGEN_UNSUBSCRIBE_SECRET</code> before any send goes out.</div>
{% endif %}
{% if not outreach_enabled %}
<div class="lg-banner lg-banner--info">Outreach is <strong>OFF</strong> (default). Ingest + enrich work now; sending stays impossible until the owner sets <code>leadgen_outreach_enabled</code> — and even then every mail needs a human approve.</div>
{% endif %}
<div class="lg-grid">
<div>
<div class="lg-card">
<h2>Prospects{{ status_filter != '' ? ' — ' ~ status_filter : '' }}</h2>
<table class="lg-table">
<thead>
<tr><th>Company</th><th>Contact</th><th>Country</th><th>Status</th><th>Enrichment</th><th>Fit</th><th></th></tr>
</thead>
<tbody>
{% for p in prospects %}
<tr id="lg-prospect-{{ p.id }}">
<td>
<strong>{{ p.companyName }}</strong>
{% if p.existingLeadId %}<span class="lg-pill lg-pill--crm" title="Already a CRM lead — will not be cold-mailed">CRM #{{ p.existingLeadId }}</span>{% endif %}
<div class="lg-muted">{{ p.domain ?: p.websiteUrl ?: '—' }}</div>
</td>
<td>
{% set penr = p.enrichmentJson ? p.enrichmentJson|jsonDecode(true) : {} %}
{{ p.contactName ?: '—' }}
{% if p.contactEmail %}
<div class="lg-muted">{{ p.contactEmail }}
{% if penr.contact_email_source is defined %}<span class="lg-pill" title="How this email was obtained" style="font-size:var(--cp-fs-micro);">{{ penr.contact_email_source == 'manual' ? 'manual' : (penr.contact_email_source|replace({'site:': 'site '})) }}</span>{% endif %}
</div>
{% elseif p.status not in ['sent','converted','suppressed'] %}
{# ③ manual email edit — for prospects the enricher couldn't resolve a contact for. #}
<form class="lg-contact-form" data-id="{{ p.id }}" onsubmit="return false;" style="margin-top:3px;">
<input type="email" name="email" placeholder="add email…" style="width:150px;border:1px solid var(--cp-outline-variant);border-radius:var(--cp-radius);padding:4px 6px;font-size:var(--cp-fs-xs);">
<input type="text" name="contact_name" placeholder="name (optional)" style="width:110px;border:1px solid var(--cp-outline-variant);border-radius:var(--cp-radius);padding:4px 6px;font-size:var(--cp-fs-xs);margin-top:2px;">
<button type="button" class="lg-btn lg-btn--ghost lg-contact-save" data-id="{{ p.id }}" style="font-size:var(--cp-fs-xs);padding:3px 8px;" {{ is_central ? '' : 'disabled' }}>Save</button>
</form>
{% else %}
<div class="lg-muted">no email</div>
{% endif %}
</td>
<td>{{ p.country ?: '—' }} <div class="lg-muted">{{ p.jurisdiction ?: '' }}</div></td>
<td><span class="lg-pill lg-pill--{{ p.status }}">{{ p.status }}</span></td>
<td class="lg-enrich-detail">
{% if p.sector %}<strong>{{ p.sector }}</strong>{% endif %}
{% if p.energySignal %} ⚡{% endif %}
{% if p.enrichmentJson %}
{% set enr = p.enrichmentJson|jsonDecode(true) %}
<div>{{ enr.summary is defined and enr.summary ? enr.summary[:200] : '' }}</div>
{% if enr.imprint is defined and enr.imprint %}
<div class="lg-muted" style="margin-top:3px;font-size:var(--cp-fs-xs);">
🇩🇪 Impressum{% if enr.imprint.legal_name %} · {{ enr.imprint.legal_name }}{% endif %}
{% if enr.imprint.managing_directors is defined and enr.imprint.managing_directors|length %} · GF {{ enr.imprint.managing_directors|join(', ') }}{% endif %}
{% if enr.imprint.ust_id %} · {{ enr.imprint.ust_id }}{% endif %}
{% if enr.imprint.register and enr.imprint.register.number %} · {{ enr.imprint.register.type }} {{ enr.imprint.register.number }} {{ enr.imprint.register.court }}{% endif %}
</div>
{% endif %}
{% endif %}
</td>
<td>
{% if p.fitScore is not null %}
<strong>{{ p.fitScore }}</strong>
<div class="lg-muted" title="{{ p.fitAngle }}">{{ p.fitAngle ? p.fitAngle|replace({'_': ' '}) : '' }}</div>
{% if not p.contactEmail %}<div class="lg-muted" style="color:var(--cp-error);" title="Great fit, but no contact email — add one in the Contact column before it can send">⚠ needs email</div>{% endif %}
{% else %}
<span class="lg-muted">—</span>
{% endif %}
</td>
<td style="white-space:nowrap;">
{% if p.status == 'new' and not p.domain and not p.websiteUrl %}
<button class="lg-btn lg-btn--ghost lg-resolve-one" data-id="{{ p.id }}" title="Find this company's website" {{ is_central ? '' : 'disabled' }}>Find site</button>
{% elseif p.status == 'new' %}
<button class="lg-btn lg-btn--ghost lg-enrich-one" data-id="{{ p.id }}" {{ is_central ? '' : 'disabled' }}>Enrich</button>
{% elseif p.status == 'unresolved' %}
<input type="text" class="lg-url-input" data-id="{{ p.id }}" placeholder="paste their URL" style="width:130px;border:1px solid var(--cp-outline-variant);border-radius:var(--cp-radius);padding:5px 7px;font-size:var(--cp-fs-xs);">
<button class="lg-btn lg-btn--ghost lg-seturl-one" data-id="{{ p.id }}" {{ is_central ? '' : 'disabled' }}>Set URL</button>
{% elseif p.status in ['enriched', 'no_site', 'low_fit'] %}
<button class="lg-btn lg-btn--ghost lg-match-one" data-id="{{ p.id }}" {{ is_central ? '' : 'disabled' }}>Fit-match</button>
{% elseif p.status == 'matched' %}
<button class="lg-btn lg-btn--primary lg-draft-one" data-id="{{ p.id }}" {{ is_central ? '' : 'disabled' }}>Draft</button>
{% elseif p.status in ['sent', 'converted'] %}
<button class="lg-btn lg-btn--ghost lg-replied-one" data-id="{{ p.id }}" title="Pauses the follow-up cadence + logs a reply activity" {{ is_central ? '' : 'disabled' }}>Mark replied</button>
{% endif %}
{# LG-GATE — DE/EU (+ unknown) is consent-only: offer a consent-record control when
the prospect lacks a consent-grade basis (else its cold send is fail-closed blocked). #}
{% set _consentOnly = (p.jurisdiction in ['EU', 'OTHER']) or (p.jurisdiction is empty) %}
{% set _hasConsent = (p.lawfulBasis|default('')|lower) in ['consent', 'opt_in', 'opt-in', 'opted_in', 'existing_customer', 'owner_override'] %}
{% if _consentOnly and not _hasConsent and p.status not in ['sent', 'converted', 'suppressed'] %}
<div class="lg-consent" data-id="{{ p.id }}" style="margin-top:4px;" title="{{ p.jurisdiction ?: 'unknown' }} is consent-only (UWG §7 / GDPR) — cold send is blocked until a consent basis is recorded">
<select class="lg-consent-basis" style="font-size:var(--cp-fs-xs);border:1px solid var(--cp-outline-variant);border-radius:var(--cp-radius);padding:3px 5px;">
<option value="">consent-only ⚠</option>
<option value="consent">opt-in consent</option>
<option value="existing_customer">existing customer</option>
<option value="owner_override">owner override</option>
</select>
<button class="lg-btn lg-btn--ghost lg-consent-save" data-id="{{ p.id }}" style="font-size:var(--cp-fs-xs);padding:3px 7px;" {{ is_central ? '' : 'disabled' }}>Record consent</button>
</div>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="7" class="lg-muted" style="padding:22px 16px;">No prospects yet — upload a CSV to start.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="lg-card">
<h2>Review queue (LA5)</h2>
<div class="lg-sub">Approve records who clicked; Send re-checks compliance + deliverability at send time. Blocked sends report their reasons and are never attempted.</div>
<table class="lg-table">
<thead><tr><th>To</th><th>Subject</th><th>Status</th><th>Created</th><th></th></tr></thead>
<tbody>
{% for d in drafts %}
{% set meta = d.eventJson ? d.eventJson|jsonDecode(true) : {} %}
<tr>
<td>{{ d.recipientEmail }}</td>
<td>{{ d.subject }} <span class="lg-muted">({{ meta.method|default('template') }})</span></td>
<td><span class="lg-pill lg-pill--{{ d.status == 'sent' ? 'enriched' : (d.status == 'failed' ? 'suppressed' : 'new') }}">{{ d.status }}</span></td>
<td class="lg-muted">{{ d.createdAt ? d.createdAt|date('m-d H:i') : '' }}</td>
<td style="white-space:nowrap;">
<button class="lg-btn lg-btn--ghost lg-or-view" data-id="{{ d.id }}">View</button>
{% if d.status == 'draft' %}
<button class="lg-btn lg-btn--primary lg-or-approve" data-id="{{ d.id }}" {{ is_central ? '' : 'disabled' }}>Approve</button>
<button class="lg-btn lg-btn--ghost lg-or-reject" data-id="{{ d.id }}" {{ is_central ? '' : 'disabled' }}>Reject</button>
{% elseif d.status == 'approved' %}
<button class="lg-btn lg-btn--primary lg-or-send" data-id="{{ d.id }}" {{ is_central ? '' : 'disabled' }}>Send</button>
<button class="lg-btn lg-btn--ghost lg-or-reject" data-id="{{ d.id }}" {{ is_central ? '' : 'disabled' }}>Reject</button>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="5" class="lg-muted" style="padding:16px;">Queue is empty — fit-match a prospect, then hit Draft.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="lg-overlay" id="lg-or-modal" style="position:fixed;inset:0;background:rgba(26,28,28,.45);display:none;align-items:center;justify-content:center;z-index:1250;">
<div style="background:var(--cp-surface-white);border-radius:var(--cp-radius-lg);width:640px;max-width:94vw;max-height:88vh;overflow:auto;box-shadow:0 18px 50px rgba(26,28,28,.3);padding:20px;">
<h2 style="margin:0 0 4px;font-size:var(--cp-fs-lg);">Outreach draft <span id="lg-or-modal-to" class="lg-muted"></span></h2>
<input type="text" id="lg-or-modal-subject" style="width:100%;box-sizing:border-box;border:1px solid var(--cp-outline-variant);border-radius:var(--cp-radius);padding:8px 10px;font-size:var(--cp-fs-base);margin:10px 0 8px;">
<textarea id="lg-or-modal-body" style="width:100%;box-sizing:border-box;min-height:260px;border:1px solid var(--cp-outline-variant);border-radius:var(--cp-radius);padding:10px;font-size:var(--cp-fs-base);font-family:var(--cp-font-mono, monospace);"></textarea>
<div class="lg-muted" style="margin:6px 0 12px;">Edits are re-checked by the quality gate on approve. The unsubscribe placeholder must stay.</div>
<div style="display:flex;gap:8px;justify-content:flex-end;">
<button class="lg-btn lg-btn--ghost" id="lg-or-modal-close">Close</button>
<button class="lg-btn lg-btn--primary" id="lg-or-modal-approve" {{ is_central ? '' : 'disabled' }}>Approve with edits</button>
</div>
</div>
</div>
</div>
<div>
<div class="lg-card">
<h2>Import prospects (LA1)</h2>
<div class="lg-sub">CSV with headers (company, email, website, contact, country) or one company/domain per line. Legitimate sources only — no gated-platform scraping.</div>
<form class="lg-form" id="lg-ingest-form">
<input type="text" name="source_ref" placeholder="Source label (e.g. SG solar directory 2026-07)">
<textarea name="csv_text" placeholder="company,email,website,contact,country Acme Solar Pte Ltd,info@acmesolar.sg,acmesolar.sg,Jane Tan,Singapore"></textarea>
<div class="lg-row" style="align-items:flex-start;">
<label style="font-size:var(--cp-fs-sm);color:var(--cp-text-faint);flex:1;">
Lawful basis for this list (EU/SG require it)
<select name="lawful_basis" style="width:100%;border:1px solid var(--cp-outline-variant);border-radius:var(--cp-radius);padding:7px 8px;font-size:var(--cp-fs-sm);margin-top:3px;">
<option value="legitimate_interest_b2b">Legitimate interest (B2B) — you attest these are legitimately-sourced business contacts</option>
<option value="consent">Consent / opted in</option>
<option value="">None yet (EU/SG prospects will be blocked at draft)</option>
</select>
</label>
</div>
<div class="lg-row">
<input type="file" name="csv_file" accept=".csv,.txt" style="font-size:var(--cp-fs-sm);">
<button type="submit" class="lg-btn lg-btn--primary" {{ is_central ? '' : 'disabled' }}>Ingest</button>
</div>
</form>
</div>
<div class="lg-card">
<h2>Import from ACRA (SG) — public register</h2>
<div class="lg-sub">Pull LIVE Singapore companies from a <a href="https://data.gov.sg/datasets?agencies=Accounting+and+Corporate+Regulatory+Authority+(ACRA)" target="_blank" rel="noopener">data.gov.sg ACRA dataset</a> (UEN, name, industry, address). No emails come from ACRA — each lands <em>needing an email</em>, which Enrich resolves from the company website. De-registered entities are dropped at the source.</div>
<form class="lg-form" id="lg-acra-form">
<input type="text" name="dataset_id" placeholder="ACRA dataset id (e.g. d_3f960c10fed6145404ca7b821f263b87)">
<div class="lg-row">
<input type="text" name="q" placeholder="Optional keyword (name/street)" style="flex:2;">
<input type="number" name="max_records" value="200" min="1" max="2000" title="Max live companies to pull (≤2000)" style="flex:1;">
</div>
<div class="lg-row" style="align-items:flex-start;">
<label style="font-size:var(--cp-fs-sm);color:var(--cp-text-faint);flex:1;">
Lawful basis
<select name="lawful_basis" style="width:100%;border:1px solid var(--cp-outline-variant);border-radius:var(--cp-radius);padding:7px 8px;font-size:var(--cp-fs-sm);margin-top:3px;">
<option value="legitimate_interest_b2b">Legitimate interest (B2B) — public register sourcing</option>
<option value="consent">Consent / opted in</option>
</select>
</label>
</div>
<div class="lg-row">
<label style="font-size:var(--cp-fs-xs);color:var(--cp-text-faint);"><input type="checkbox" name="registered_only" value="1" checked> Registered (live) only</label>
<button type="submit" class="lg-btn lg-btn--primary" {{ is_central ? '' : 'disabled' }}>Import from ACRA</button>
</div>
<div class="lg-muted" style="font-size:var(--cp-fs-xs);margin-top:4px;">Sourcing note stamped on every prospect. Confirm the Singapore Open Data Licence covers your intended use.</div>
<div class="lg-muted" style="font-size:var(--cp-fs-xs);margin-top:6px;padding-top:6px;border-top:1px solid var(--cp-outline-variant);">
<strong>Attribution:</strong> Contains information from ACRA on data.gov.sg, made available under the terms of the
<a href="https://data.gov.sg/open-data-licence" target="_blank" rel="noopener">Singapore Open Data Licence v{{ '1.0' }}</a>.
The dataset id + access date are recorded on every imported prospect.
</div>
</form>
</div>
<div class="lg-card">
<h2>Deliverability (LX1)</h2>
<div class="lg-dns">
{% if deliverability is null or not deliverability.configured %}
<div class="lg-muted">Sender domain not configured (<code>leadgen_sender_domain</code>) — sending gate reports "not configured", which is the intended default-OFF posture.</div>
{% else %}
<div>Domain: <strong>{{ deliverability.domain }}</strong></div>
<div>SPF: <span class="{{ deliverability.spf.found ? 'ok' : 'miss' }}">{{ deliverability.spf.found ? 'found' : 'missing' }}</span></div>
<div>DMARC: <span class="{{ deliverability.dmarc.found ? 'ok' : 'miss' }}">{{ deliverability.dmarc.found ? 'found' : 'missing' }}</span></div>
<div>DKIM ({{ deliverability.dkim.selector }}): <span class="{{ deliverability.dkim.found ? 'ok' : 'miss' }}">{{ deliverability.dkim.found ? 'found' : 'missing' }}</span></div>
<div>Warmup cap today: <strong>{{ deliverability.effective_cap_today }}</strong> mails</div>
{% endif %}
<div class="lg-muted" style="margin-top:6px;">Sender: {{ sender_address ?: 'not set' }}</div>
</div>
</div>
<div class="lg-card">
<h2>Viral touches (GR2)</h2>
<div class="lg-sub">Landings on ourhoneybee.eu via branded footers (last 90 days), and which converted to signups — the inbound twin of this funnel.</div>
<table class="lg-table">
<thead><tr><th>Surface</th><th>Touches</th><th>Signups</th></tr></thead>
<tbody>
{% for v in viral_touches %}
<tr>
<td>viral_{{ v.surface }}</td>
<td>{{ v.touches }}</td>
<td>{{ v.conversions > 0 ? v.conversions : '—' }}</td>
</tr>
{% else %}
<tr><td colspan="3" class="lg-muted" style="padding:16px;">No touches yet — they appear once branded links go out into the world.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="lg-card">
<h2>Suppression list (LX2)</h2>
<div class="lg-sub">Never contacted again — unsubscribes, bounces, complaints, manual blocks.</div>
<form class="lg-form" id="lg-suppress-form">
<div class="lg-row">
<select name="value_type" style="border:1px solid var(--cp-outline-variant); border-radius:var(--cp-radius); padding:7px 8px; font-size:var(--cp-fs-sm);">
<option value="email">email</option>
<option value="domain">domain</option>
</select>
<input type="text" name="value" placeholder="person@example.com" style="flex:1; margin-bottom:0;">
<button type="submit" class="lg-btn lg-btn--ghost" {{ is_central ? '' : 'disabled' }}>Block</button>
</div>
</form>
<table class="lg-table">
<thead><tr><th>Value</th><th>Reason</th><th>When</th></tr></thead>
<tbody>
{% for s in suppressions %}
<tr>
<td>{{ s.value }} <span class="lg-muted">({{ s.valueType }})</span></td>
<td>{{ s.reason }}</td>
<td class="lg-muted">{{ s.createdAt ? s.createdAt|date('Y-m-d') : '' }}</td>
</tr>
{% else %}
<tr><td colspan="3" class="lg-muted" style="padding:16px;">Empty.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div id="lg-toast"></div>
{% endblock %}
{% block page_scripts %}
<script>
(function () {
function toast(msg, ms) {
var t = document.getElementById('lg-toast');
t.textContent = msg;
t.style.display = 'block';
clearTimeout(t._h);
t._h = setTimeout(function () { t.style.display = 'none'; }, ms || 4000);
}
function post(url, formData, cb) {
fetch(url, { method: 'POST', body: formData, credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(cb)
.catch(function (e) { toast('Request failed: ' + e, 6000); });
}
var ingest = document.getElementById('lg-ingest-form');
if (ingest) ingest.addEventListener('submit', function (ev) {
ev.preventDefault();
post('{{ path('leadgen_ingest_csv') }}', new FormData(ingest), function (res) {
if (!res.success) { toast(res.message || 'Ingest failed', 6000); return; }
var s = res.summary;
toast('Ingested: ' + s.created + ' new, ' + (s.updated || 0) + ' updated, ' + s.duplicates + ' duplicates, ' + s.crm_matches + ' already in CRM, ' + s.suppressed + ' suppressed.', 8000);
setTimeout(function () { location.reload(); }, 2600);
});
});
var acra = document.getElementById('lg-acra-form');
if (acra) acra.addEventListener('submit', function (ev) {
ev.preventDefault();
var btn = acra.querySelector('button[type=submit]');
if (btn) { btn.disabled = true; btn.textContent = 'Importing…'; }
post('{{ path('leadgen_acra_import') }}', new FormData(acra), function (res) {
if (btn) { btn.disabled = false; btn.textContent = 'Import from ACRA'; }
if (!res.success) { toast(res.message || 'ACRA import failed', 7000); return; }
var s = res.summary || {}, m = res.meta || {};
toast('ACRA: ' + (s.created || 0) + ' new, ' + (s.updated || 0) + ' updated, ' + (s.duplicates || 0) + ' dup — ' + (m.kept || 0) + ' live kept, ' + (m.dropped_non_registered || 0) + ' de-registered dropped' + (m.capped ? ' (capped)' : '') + '. Run Enrich to resolve their websites/emails.', 9000);
setTimeout(function () { location.reload(); }, 3200);
});
});
// ③ manual contact email save
Array.prototype.forEach.call(document.querySelectorAll('.lg-contact-save'), function (btn) {
btn.addEventListener('click', function () {
var form = document.querySelector('.lg-contact-form[data-id="' + btn.getAttribute('data-id') + '"]');
if (!form) return;
var email = (form.querySelector('input[name=email]').value || '').trim();
if (!email) { toast('Enter an email first', 3000); return; }
var fd = new FormData(form);
btn.disabled = true; btn.textContent = '…';
post('{{ path('leadgen_set_contact', {id: 0}) }}'.replace('/0/contact', '/' + btn.getAttribute('data-id') + '/contact'), fd, function (res) {
btn.disabled = false; btn.textContent = 'Save';
if (!res.success) { toast(res.message || 'Save failed', 6000); return; }
toast('Contact saved (' + res.status + ')', 4000);
setTimeout(function () { location.reload(); }, 900);
});
});
});
// LG-GATE record consent (opt-in / existing customer / owner override)
Array.prototype.forEach.call(document.querySelectorAll('.lg-consent-save'), function (btn) {
btn.addEventListener('click', function () {
var box = document.querySelector('.lg-consent[data-id="' + btn.getAttribute('data-id') + '"]');
var basis = box ? (box.querySelector('.lg-consent-basis').value || '') : '';
if (!basis) { toast('Pick a consent basis first', 3000); return; }
var note = window.prompt('Note for the audit trail (how/when consent was obtained):', '') || '';
var fd = new FormData(); fd.append('basis', basis); fd.append('note', note);
btn.disabled = true; btn.textContent = '…';
post('{{ path('leadgen_set_consent', {id: 0}) }}'.replace('/0/consent', '/' + btn.getAttribute('data-id') + '/consent'), fd, function (res) {
btn.disabled = false; btn.textContent = 'Record consent';
if (!res.success) { toast(res.message || 'Failed', 6000); return; }
toast('Consent recorded (' + res.basis + ') — this prospect can now be drafted/sent', 5000);
setTimeout(function () { location.reload(); }, 1100);
});
});
});
var suppress = document.getElementById('lg-suppress-form');
if (suppress) suppress.addEventListener('submit', function (ev) {
ev.preventDefault();
post('{{ path('leadgen_suppress') }}', new FormData(suppress), function (res) {
if (!res.success) { toast(res.message || 'Failed', 6000); return; }
toast('Suppressed ' + res.value);
setTimeout(function () { location.reload(); }, 1000);
});
});
Array.prototype.forEach.call(document.querySelectorAll('.lg-enrich-one'), function (btn) {
btn.addEventListener('click', function () {
btn.disabled = true; btn.textContent = '…';
post('/leadgen/enrich/' + btn.getAttribute('data-id'), new FormData(), function (res) {
toast(res.message || (res.success ? 'Enriched' : 'Failed'), 5000);
if (res.success) setTimeout(function () { location.reload(); }, 1200);
else { btn.disabled = false; btn.textContent = 'Enrich'; }
});
});
});
function rowAction(selector, urlPrefix, label) {
Array.prototype.forEach.call(document.querySelectorAll(selector), function (btn) {
btn.addEventListener('click', function () {
btn.disabled = true; btn.textContent = '…';
post(urlPrefix + btn.getAttribute('data-id'), new FormData(), function (res) {
toast(res.message || (res.success ? label + ' done' : label + ' failed'), 5000);
if (res.success) setTimeout(function () { location.reload(); }, 1200);
else { btn.disabled = false; btn.textContent = label; }
});
});
});
}
rowAction('.lg-match-one', '/leadgen/match/', 'Fit-match');
rowAction('.lg-draft-one', '/leadgen/draft/', 'Draft');
rowAction('.lg-replied-one', '/leadgen/replied/', 'Mark replied');
rowAction('.lg-resolve-one', '/leadgen/resolve/', 'Find site');
// LA7 — manual URL entry for an unresolved prospect.
Array.prototype.forEach.call(document.querySelectorAll('.lg-seturl-one'), function (btn) {
btn.addEventListener('click', function () {
var input = document.querySelector('.lg-url-input[data-id="' + btn.getAttribute('data-id') + '"]');
var url = input ? input.value.trim() : '';
if (!url) { toast('Enter a URL first'); return; }
btn.disabled = true;
var fd = new FormData(); fd.append('url', url);
post('/leadgen/set-url/' + btn.getAttribute('data-id'), fd, function (res) {
toast(res.message || (res.success ? 'URL set' : 'Failed'), 5000);
if (res.success) setTimeout(function () { location.reload(); }, 1000);
else btn.disabled = false;
});
});
});
var resolveBatch = document.getElementById('lg-resolve-batch');
if (resolveBatch) resolveBatch.addEventListener('click', function () {
resolveBatch.disabled = true; resolveBatch.textContent = 'Finding…';
post('{{ path('leadgen_resolve_batch') }}', new FormData(), function (res) {
if (!res.success) { toast(res.message || 'Resolve failed', 6000); resolveBatch.disabled = false; resolveBatch.textContent = 'Find sites (names)'; return; }
toast('Resolved ' + res.counts.resolved + ', unresolved ' + res.counts.unresolved + ', errors ' + res.counts.errors + '.', 6000);
setTimeout(function () { location.reload(); }, 1400);
});
});
// ── LA5 review queue ──
function orPost(id, action, fd, label, btn) {
btn.disabled = true;
post('/leadgen/outreach/' + id + '/' + action, fd || new FormData(), function (res) {
toast(res.message || (res.success ? label + ' done' : label + ' failed'), 7000);
if (res.success) setTimeout(function () { location.reload(); }, 1200);
else btn.disabled = false;
});
}
Array.prototype.forEach.call(document.querySelectorAll('.lg-or-approve'), function (btn) {
btn.addEventListener('click', function () { orPost(btn.getAttribute('data-id'), 'approve', null, 'Approve', btn); });
});
Array.prototype.forEach.call(document.querySelectorAll('.lg-or-send'), function (btn) {
btn.addEventListener('click', function () { orPost(btn.getAttribute('data-id'), 'send', null, 'Send', btn); });
});
Array.prototype.forEach.call(document.querySelectorAll('.lg-or-reject'), function (btn) {
btn.addEventListener('click', function () {
var reason = prompt('Reject reason (optional):') || '';
var fd = new FormData(); fd.append('reason', reason);
orPost(btn.getAttribute('data-id'), 'reject', fd, 'Reject', btn);
});
});
var orModal = document.getElementById('lg-or-modal');
var orId = null;
Array.prototype.forEach.call(document.querySelectorAll('.lg-or-view'), function (btn) {
btn.addEventListener('click', function () {
fetch('/leadgen/outreach/' + btn.getAttribute('data-id'), { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) { toast(res.message || 'Load failed'); return; }
orId = res.outreach.id;
document.getElementById('lg-or-modal-to').textContent = '→ ' + res.outreach.to + ' (' + res.outreach.status + ')';
document.getElementById('lg-or-modal-subject').value = res.outreach.subject || '';
document.getElementById('lg-or-modal-body').value = res.outreach.body_text || '';
document.getElementById('lg-or-modal-approve').style.display = res.outreach.status === 'draft' ? '' : 'none';
orModal.style.display = 'flex';
});
});
});
document.getElementById('lg-or-modal-close').addEventListener('click', function () { orModal.style.display = 'none'; });
document.getElementById('lg-or-modal-approve').addEventListener('click', function () {
var fd = new FormData();
fd.append('subject', document.getElementById('lg-or-modal-subject').value);
fd.append('body_text', document.getElementById('lg-or-modal-body').value);
orPost(orId, 'approve', fd, 'Approve', this);
});
var matchBatch = document.getElementById('lg-match-batch');
if (matchBatch) matchBatch.addEventListener('click', function () {
matchBatch.disabled = true; matchBatch.textContent = 'Matching…';
post('{{ path('leadgen_match_batch') }}', new FormData(), function (res) {
if (!res.success) { toast(res.message || 'Match failed', 6000); matchBatch.disabled = false; matchBatch.textContent = 'Fit-match all enriched'; return; }
toast('Fit-match: ' + res.counts.matched + ' matched, ' + res.counts.low_fit + ' low fit.', 6000);
setTimeout(function () { location.reload(); }, 1400);
});
});
var batch = document.getElementById('lg-enrich-batch');
if (batch) batch.addEventListener('click', function () {
batch.disabled = true; batch.textContent = 'Enriching…';
var fd = new FormData(); fd.append('limit', '10');
post('{{ path('leadgen_enrich_batch') }}', fd, function (res) {
if (!res.success) { toast(res.message || 'Batch failed', 6000); batch.disabled = false; batch.textContent = 'Enrich next 10'; return; }
var c = res.counts;
toast('Batch: ' + c.enriched + ' enriched, ' + c.no_site + ' no site, ' + c.from_cache + ' from cache, ' + c.errors + ' errors.', 6000);
setTimeout(function () { location.reload(); }, 1400);
});
});
})();
</script>
{% endblock %}