conference_contacts.xlsx is now the single working file behind both the dash's Download button and the mail-merge tool -- populated with all 139 SPIE 2026 presenters (not just ones with emails already found), so Peter can fill in emails as he meets people at the conference. Also adds a second message template (reconnect_message) alongside the existing wavegate_focus one, and a search/scroll UI for the contacts list now that it's no longer a handful of rows.
90 lines
4.2 KiB
HTML
90 lines
4.2 KiB
HTML
{% extends "base.html" %}
|
|
{% block content %}
|
|
<h1>TXproducts Mail Merge</h1>
|
|
<p class="sub">Conference follow-ups — check who gets a message, pick a template, generate or send.</p>
|
|
|
|
{% if error %}<div class="warning">{{ error }}</div>{% endif %}
|
|
|
|
<form method="post">
|
|
<div class="card">
|
|
<h2>Contacts <span class="sub" style="font-weight:400;">({{ contacts|length }} total)</span></h2>
|
|
<div style="margin-bottom:10px;">
|
|
<button type="button" onclick="document.querySelectorAll('.contact-check').forEach(c=>c.checked=true)">Select All</button>
|
|
<button type="button" class="secondary" onclick="document.querySelectorAll('.contact-check').forEach(c=>c.checked=false)">Select None</button>
|
|
<a href="/" style="margin-left:8px; font-size:0.85rem;">Refresh from Excel</a>
|
|
</div>
|
|
<input type="text" id="contact-search" placeholder="Search by name..." style="margin-bottom:10px; max-width:300px;">
|
|
<div class="contact-list-scroll">
|
|
{% set ns = namespace(found=false) %}
|
|
{% for c in contacts %}
|
|
{% set is_preselected = preselect_email and c.email|lower == preselect_email %}
|
|
{% if is_preselected %}{% set ns.found = true %}{% endif %}
|
|
<div class="contact-row {% if c.sent %}disabled{% endif %}" data-name="{{ (c.first ~ ' ' ~ c.last)|lower }}" {% if is_preselected %}id="preselected-contact" style="background:#eef4fd;"{% endif %}>
|
|
<input type="checkbox" class="contact-check" name="contacts" value="{{ c.email }}" {% if c.sent %}disabled{% endif %} {% if is_preselected %}checked{% endif %}>
|
|
<span>{{ c.first }} {{ c.last }} <{{ c.email }}></span>
|
|
{% if c.conference %}<span class="meta">— {{ c.conference }}</span>{% endif %}
|
|
{% if c.draft_opened %}<span class="meta">(draft opened {{ c.draft_opened }})</span>{% endif %}
|
|
{% if c.sent %}<span class="meta">(already marked as sent)</span>{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<p class="sub">No contacts in the sheet yet.</p>
|
|
{% endfor %}
|
|
</div>
|
|
{% if preselect_email and not ns.found %}
|
|
<p class="warning" style="margin-top:10px;">No contact with email {{ preselect_email }} found in the sheet yet — add them first.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Message template</h2>
|
|
<label>Standard message:
|
|
<select name="template">
|
|
{% for t in templates %}
|
|
<option value="{{ t }}">{{ t }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</label>
|
|
|
|
<h2 style="margin-top:18px;">Attachments</h2>
|
|
{% for a in attachments %}
|
|
<label style="display:inline-flex; align-items:center; gap:6px; margin-right:16px;">
|
|
<input type="checkbox" name="attachments" value="{{ a.path }}" {% if a.exists %}checked{% else %}disabled{% endif %}>
|
|
{{ a.name }}{% if not a.exists %} (not found){% endif %}
|
|
</label>
|
|
{% endfor %}
|
|
|
|
<h2 style="margin-top:18px;">From address</h2>
|
|
<input type="text" name="from_address" value="{{ default_from }}">
|
|
</div>
|
|
|
|
<button type="submit" formaction="/open_in_mail">Open in Mail.app</button>
|
|
<button type="submit" formaction="/generate">Generate Drafts</button>
|
|
<button type="submit" formaction="/preview">Preview & Send Batch</button>
|
|
<button type="button" class="secondary" onclick="fetch('/open_folder', {method:'POST'})">Open Drafts Folder</button>
|
|
</form>
|
|
<script>
|
|
// Prevent double-clicks/double-submits from firing the same action twice (e.g. writing
|
|
// two draft files or opening two Mail.app windows for one click).
|
|
document.querySelector("form").addEventListener("submit", function (e) {
|
|
if (e.target.dataset.submitted) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
e.target.dataset.submitted = "true";
|
|
document.querySelectorAll("form button[type=submit]").forEach(function (btn) {
|
|
btn.disabled = true;
|
|
});
|
|
});
|
|
|
|
var preselected = document.getElementById("preselected-contact");
|
|
if (preselected) { preselected.scrollIntoView({ block: "center" }); }
|
|
|
|
document.getElementById("contact-search").addEventListener("input", function (e) {
|
|
var q = e.target.value.trim().toLowerCase();
|
|
document.querySelectorAll(".contact-row[data-name]").forEach(function (row) {
|
|
row.classList.toggle("no-match", q !== "" && row.dataset.name.indexOf(q) === -1);
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|