Improve campaign personalization and contact duplicate handling.
Deploy Beta / unit-tests (push) Successful in 13s
Deploy Beta / docker (push) Successful in 17s
Deploy Beta / deploy-beta (push) Successful in 1m44s

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:
2026-08-10 09:47:04 -05:00
parent 58258f2875
commit 8680c082fe
13 changed files with 747 additions and 101 deletions
+78 -3
View File
@@ -4,10 +4,32 @@
{% block topbar_title %}New contact{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{% static 'css/address-autocomplete.css' %}">
<style>
.portal-modal-backdrop {
position: fixed; inset: 0; background: rgba(17, 24, 39, 0.45);
display: flex; align-items: center; justify-content: center;
z-index: 1000; padding: 16px;
}
.portal-modal {
background: #fff; border: 1px solid var(--monica-border);
max-width: 480px; width: 100%; padding: 20px 22px;
box-shadow: 0 12px 40px rgba(0,0,0,0.18);
}
.portal-modal h3 { margin: 0 0 8px; font-size: 18px; }
.portal-modal p { margin: 0 0 12px; font-size: 14px; color: var(--monica-muted); line-height: 1.45; }
.portal-modal .match-card {
background: #f8fafc; border: 1px solid var(--monica-border);
padding: 12px; margin: 0 0 16px; font-size: 14px;
}
.portal-modal .match-card strong { display: block; margin-bottom: 4px; }
.portal-modal-actions { display: flex; gap: 8px; flex-wrap: wrap; }
</style>
{% endblock %}
{% block portal_content %}
<form method="post">
<form method="post" id="contact-create-form">
{% csrf_token %}
<input type="hidden" name="resolve_match" id="id_resolve_match" value="">
<input type="hidden" name="match_id" id="id_match_id" value="{% if match_prompt %}{{ match_prompt.contact.pk }}{% endif %}">
<div class="split">
<div class="panel">
<div class="panel-h"><h2>Profile</h2></div>
@@ -87,14 +109,67 @@
<label class="check-row"><input type="checkbox" name="consent_postcard" value="1" {% if form.consent_postcard %}checked{% endif %}> Postcard mailings</label>
</div>
<p class="hint-block" style="margin-top:16px">
Matches existing contacts by email, phone, or address when possible.
Postcard defaults on when an address is saved.
Same email always updates that contact. Same phone or address asks whether to update or create new.
</p>
</div>
</div>
</div>
</form>
{% if match_prompt %}
<div class="portal-modal-backdrop" id="match-modal" role="dialog" aria-modal="true" aria-labelledby="match-modal-title">
<div class="portal-modal">
<h3 id="match-modal-title">Possible duplicate</h3>
<p>
An existing contact shares this {{ match_prompt.reason_label }}.
Update that record, or create a separate contact anyway?
</p>
<div class="match-card">
<strong>{{ match_prompt.contact }}</strong>
{% if match_prompt.contact.email %}<div>{{ match_prompt.contact.email }}</div>{% endif %}
{% if match_prompt.contact.phone %}<div>{{ match_prompt.contact.phone }}</div>{% endif %}
{% if match_prompt.contact.postal_address.line1 %}
<div class="muted">
{{ match_prompt.contact.postal_address.line1 }}{% if match_prompt.contact.postal_address.city %}, {{ match_prompt.contact.postal_address.city }}{% endif %}
</div>
{% endif %}
<div style="margin-top:8px">
<a href="{% url 'contacts:detail' match_prompt.contact.pk %}" target="_blank" rel="noopener">Open existing contact</a>
</div>
</div>
<div class="portal-modal-actions">
<button class="btn btn-primary" type="button" id="match-update">Update existing</button>
<button class="btn btn-ghost" type="button" id="match-create">Create new contact</button>
<button class="btn btn-ghost" type="button" id="match-cancel">Cancel</button>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/address-autocomplete.js' %}"></script>
{% if match_prompt %}
<script>
(function () {
var form = document.getElementById('contact-create-form');
var resolve = document.getElementById('id_resolve_match');
var modal = document.getElementById('match-modal');
function submitWith(choice) {
if (resolve) resolve.value = choice;
if (modal) modal.hidden = true;
form.submit();
}
document.getElementById('match-update')?.addEventListener('click', function () {
submitWith('update');
});
document.getElementById('match-create')?.addEventListener('click', function () {
submitWith('create');
});
document.getElementById('match-cancel')?.addEventListener('click', function () {
if (resolve) resolve.value = '';
if (modal) modal.remove();
});
})();
</script>
{% endif %}
{% endblock %}
+16 -4
View File
@@ -13,12 +13,24 @@
<div class="panel-h"><h2>Profile</h2></div>
<div class="panel-b form-grid">
<div class="form-grid cols-2">
<div class="field"><label>First name</label><input value="{{ contact.first_name }}" readonly></div>
<div class="field"><label>Last name</label><input value="{{ contact.last_name }}" readonly></div>
<div class="field">
<label for="id_first_name">First name</label>
<input id="id_first_name" name="first_name" value="{{ contact.first_name }}" required>
</div>
<div class="field">
<label for="id_last_name">Last name</label>
<input id="id_last_name" name="last_name" value="{{ contact.last_name }}">
</div>
</div>
<div class="form-grid cols-2">
<div class="field"><label>Email</label><input value="{{ contact.email }}" readonly></div>
<div class="field"><label>Phone</label><input value="{{ contact.phone }}" readonly></div>
<div class="field">
<label for="id_email">Email</label>
<input id="id_email" name="email" type="email" value="{{ contact.email }}" required>
</div>
<div class="field">
<label for="id_phone">Phone</label>
<input id="id_phone" name="phone" value="{{ contact.phone }}">
</div>
</div>
<div class="field"><label>Source</label><input value="{{ contact.get_source_display }}" readonly></div>