Improve campaign personalization and contact duplicate handling.
Add merge tags for email/SMS, paginated removable recipients, PCM event panels for postcard campaigns, and a portal modal when phone/address matches an existing contact.
This commit is contained in:
@@ -39,7 +39,11 @@
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-primary" type="submit">Send now to recipients</button>
|
||||
<p class="hint-block" style="margin-top:8px">
|
||||
{% if campaign.channel == "postcard" %}
|
||||
Enqueues draft / scheduled / failed messages via PCM Integrations.
|
||||
{% else %}
|
||||
Enqueues draft / scheduled / failed messages via SMTP2GO (dev ImmediateBackend runs inline).
|
||||
{% endif %}
|
||||
</p>
|
||||
</form>
|
||||
{% endif %}
|
||||
@@ -60,6 +64,7 @@
|
||||
<div class="label">Delivered</div>
|
||||
<div class="value" data-stat="delivered">{{ stats.delivered }}</div>
|
||||
</div>
|
||||
{% if campaign.channel == "email" %}
|
||||
<div class="stat-card">
|
||||
<div class="label">Opens</div>
|
||||
<div class="value" data-stat="opens">{{ stats.opens }}</div>
|
||||
@@ -68,6 +73,7 @@
|
||||
<div class="label">Clicks</div>
|
||||
<div class="value" data-stat="clicks">{{ stats.clicks }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="stat-card">
|
||||
<div class="label">Bounced / failed</div>
|
||||
<div class="value" data-stat="failed">{{ stats.failed }}</div>
|
||||
@@ -82,21 +88,32 @@
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Engagement</h2></div>
|
||||
<div class="panel-b">
|
||||
{% if campaign.channel == "email" %}
|
||||
<p class="hint-block" style="margin-top:0">
|
||||
Unique recipients: <strong data-stat="opens">{{ stats.opens }}</strong> opened ·
|
||||
<strong data-stat="clicks">{{ stats.clicks }}</strong> clicked
|
||||
({{ stats.open_events }} open events / {{ stats.click_events }} click events from SMTP2GO).
|
||||
</p>
|
||||
{% elif campaign.channel == "sms" %}
|
||||
<p class="hint-block" style="margin-top:0">
|
||||
Delivery status updates from SMTP2GO SMS webhooks.
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="hint-block" style="margin-top:0">
|
||||
Postcard status updates from PCM Integrations webhooks.
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="chart-placeholder" aria-hidden="true">
|
||||
<div class="bar" style="height:55%"></div>
|
||||
<div class="bar" style="height:70%"></div>
|
||||
<div class="bar" style="height:40%"></div>
|
||||
<div class="bar" style="height:30%"></div>
|
||||
<div class="bar" style="height:20%"></div>
|
||||
</div> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Recent SMTP2GO events</h2></div>
|
||||
<div class="panel-h"><h2>{{ events_title }}</h2></div>
|
||||
<div class="panel-b" style="padding:0">
|
||||
<table class="table">
|
||||
<thead><tr><th>When</th><th>Event</th><th>Contact</th></tr></thead>
|
||||
@@ -108,7 +125,7 @@
|
||||
<td>{% if event.message %}{{ event.message.contact }}{% else %}—{% endif %}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="3" class="empty-state">No webhook events yet. SMTP2GO must POST opens/clicks to <code>/portal/messaging/webhooks/email/</code> (see messaging README). SMTP2GO’s own “Clicked” feed does not fill this table by itself.</td></tr>
|
||||
<tr><td colspan="3" class="empty-state">{{ events_empty|safe }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -116,15 +133,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Recipients</h2></div>
|
||||
<div class="panel" id="recipients-panel" data-page="{{ page_obj.number }}">
|
||||
<div class="panel-h" style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">
|
||||
<h2 style="margin:0">Recipients</h2>
|
||||
<span class="muted" style="font-size:13px">
|
||||
{{ page_obj.paginator.count }} total
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
· page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-b" style="padding:0">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Contact</th><th>Status</th><th>Provider id</th><th>Error</th></tr>
|
||||
<tr><th>Contact</th><th>Status</th><th>Provider id</th><th>Error</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody id="recipients-body">
|
||||
{% for message in messages %}
|
||||
{% for message in recipient_messages %}
|
||||
<tr data-message-id="{{ message.pk }}">
|
||||
<td>
|
||||
<div>{{ message.contact }}</div>
|
||||
@@ -135,24 +160,56 @@
|
||||
<td><span class="badge badge-{{ message.status }}">{{ message.get_status_display }}</span></td>
|
||||
<td class="muted">{{ message.provider_message_id|default:"—" }}</td>
|
||||
<td class="muted">{{ message.error|truncatechars:60|default:"—" }}</td>
|
||||
<td style="white-space:nowrap;text-align:right">
|
||||
{% if message.can_remove %}
|
||||
<form method="post"
|
||||
action="{% url 'messaging:campaign_message_remove' campaign.pk message.pk %}"
|
||||
style="display:inline"
|
||||
onsubmit="return confirm('Remove this recipient from the campaign?');">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="page" value="{{ page_obj.number }}">
|
||||
<button class="btn btn-ghost btn-sm" type="submit">Remove</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4" class="empty-state">No messages on this campaign.</td></tr>
|
||||
<tr><td colspan="5" class="empty-state">No messages on this campaign.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<div class="panel-b" style="display:flex;gap:8px;align-items:center;justify-content:flex-end;border-top:1px solid var(--monica-border)">
|
||||
{% if page_obj.has_previous %}
|
||||
<a class="btn btn-ghost btn-sm" href="?page={{ page_obj.previous_page_number }}">← Prev</a>
|
||||
{% endif %}
|
||||
<span class="muted" style="font-size:13px">Page {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span>
|
||||
{% if page_obj.has_next %}
|
||||
<a class="btn btn-ghost btn-sm" href="?page={{ page_obj.next_page_number }}">Next →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
(function () {
|
||||
var url = "{% url 'messaging:campaign_status_json' campaign.pk %}";
|
||||
var panel = document.getElementById("recipients-panel");
|
||||
var page = (panel && panel.getAttribute("data-page")) || "1";
|
||||
var removeBase = "{% url 'messaging:campaign_message_remove' campaign.pk '00000000-0000-0000-0000-000000000000' %}";
|
||||
var csrfToken = "{{ csrf_token }}";
|
||||
var url = "{% url 'messaging:campaign_status_json' campaign.pk %}?page=" + encodeURIComponent(page);
|
||||
function esc(s) {
|
||||
return String(s || "").replace(/[&<>"']/g, function (c) {
|
||||
return ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c];
|
||||
});
|
||||
}
|
||||
function removeUrl(id) {
|
||||
return removeBase.replace("00000000-0000-0000-0000-000000000000", id);
|
||||
}
|
||||
function apply(data) {
|
||||
var badge = document.getElementById("campaign-status-badge");
|
||||
if (badge) {
|
||||
@@ -167,17 +224,25 @@
|
||||
var body = document.getElementById("recipients-body");
|
||||
if (body && data.messages) {
|
||||
if (!data.messages.length) {
|
||||
body.innerHTML = '<tr><td colspan="4" class="empty-state">No messages on this campaign.</td></tr>';
|
||||
body.innerHTML = '<tr><td colspan="5" class="empty-state">No messages on this campaign.</td></tr>';
|
||||
} else {
|
||||
body.innerHTML = data.messages.map(function (m) {
|
||||
var dest = m.destination
|
||||
? '<div class="muted" style="font-size:12px;margin-top:2px">' + esc(m.destination) + '</div>'
|
||||
: '';
|
||||
var action = m.can_remove
|
||||
? '<form method="post" action="' + esc(removeUrl(m.id)) + '" style="display:inline" ' +
|
||||
'onsubmit="return confirm(\'Remove this recipient from the campaign?\');">' +
|
||||
'<input type="hidden" name="csrfmiddlewaretoken" value="' + esc(csrfToken) + '">' +
|
||||
'<input type="hidden" name="page" value="' + esc(page) + '">' +
|
||||
'<button class="btn btn-ghost btn-sm" type="submit">Remove</button></form>'
|
||||
: '<span class="muted">—</span>';
|
||||
return "<tr data-message-id=\"" + esc(m.id) + "\">" +
|
||||
"<td><div>" + esc(m.contact) + "</div>" + dest + "</td>" +
|
||||
"<td><span class=\"badge badge-" + esc(m.status) + "\">" + esc(m.status_display) + "</span></td>" +
|
||||
"<td class=\"muted\">" + esc(m.provider_message_id || "—") + "</td>" +
|
||||
"<td class=\"muted\">" + esc(m.error || "—") + "</td></tr>";
|
||||
"<td class=\"muted\">" + esc(m.error || "—") + "</td>" +
|
||||
"<td style=\"white-space:nowrap;text-align:right\">" + action + "</td></tr>";
|
||||
}).join("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,14 +77,14 @@
|
||||
<label>Body</label>
|
||||
<div id="email-editor"></div>
|
||||
<textarea id="id_body" name="body" hidden>{{ form_data.body }}</textarea>
|
||||
<div class="hint">Bold, fonts, sizes, links, images · merge tags: {first_name}, {last_name}</div>
|
||||
<div class="hint">Bold, fonts, sizes, links, images · merge tags: <code>{% templatetag openvariable %}first_name{% templatetag closevariable %}</code>, <code>{% templatetag openvariable %}last_name{% templatetag closevariable %}</code></div>
|
||||
</div>
|
||||
<div class="field" id="sms-body-wrap" hidden>
|
||||
<label for="id_body_sms">Body</label>
|
||||
<textarea id="id_body_sms" style="min-height:140px"
|
||||
placeholder="Hi {first_name}, …"
|
||||
placeholder="Hi {% templatetag openvariable %}first_name{% templatetag closevariable %}, …"
|
||||
oninput="syncSmsBody()">{{ form_data.body }}</textarea>
|
||||
<div class="hint">Plain text for SMS · keep it short</div>
|
||||
<div class="hint">Plain text for SMS · keep it short · merge tags: <code>{% templatetag openvariable %}first_name{% templatetag closevariable %}</code>, <code>{% templatetag openvariable %}last_name{% templatetag closevariable %}</code></div>
|
||||
</div>
|
||||
<div class="field" id="postcard-body-hint" hidden>
|
||||
<p class="hint-block" style="margin:0">
|
||||
|
||||
Reference in New Issue
Block a user