Unify contact sheet, add reconnect message template, and scale the contacts UI
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.
This commit is contained in:
1 parent
b0360b6b68
commit
b745d6f15d
9 files changed
+80
-78
No files matched your search
@@ -5,6 +5,7 @@ nextcloud_export/
|
||||
*.env
|
||||
address_book/conference_mail_merge/conference_contacts.xlsx
|
||||
address_book/conference_mail_merge/output_drafts/
|
||||
~$*.xlsx
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.Rhistory
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
SUBJECT: Following up on TXServices & Wavegate
|
||||
|
||||
Hi {{First Name}},
|
||||
|
||||
It was a pleasure meeting you at {{Conference}} on {{Date Met}}.
|
||||
|
||||
I really enjoyed your session on {{Topic}}, I found it insightful and gave me several new perspectives.
|
||||
|
||||
I'd love to stay in touch, as I believe there are opportunities where our expertise could complement each other and create value for both of us.
|
||||
|
||||
I've attached a short overview of TXproducts, including our services and our flagship solution, WaveGate.
|
||||
|
||||
If you're open to it, I'd be happy to schedule a 20–30 minute call in the coming weeks to explore potential collaboration.
|
||||
|
||||
Best regards,
|
||||
|
||||
Mckinley
|
||||
@@ -1,13 +1,14 @@
|
||||
SUBJECT: Following up on {{Topic}}
|
||||
SUBJECT: Following up on TXServices & Wavegate
|
||||
|
||||
Hi {{First Name}},
|
||||
|
||||
It was great meeting you at {{Conference}}. Following up on our conversation
|
||||
about {{Topic}} on {{Date Met}}.
|
||||
It was great meeting you at {{Conference}} on {{Date Met}}.
|
||||
|
||||
Attached is more detail on how it speeds up beamline experiments.
|
||||
Your session about {{Topic}} was very interesting, and I am following up
|
||||
on the info material we talked about regarding TXproducts' services and
|
||||
flagship product WaveGate.
|
||||
|
||||
Happy to set up a short call if useful.
|
||||
Would love to dive deeper — let's find a time to talk.
|
||||
|
||||
Best regards,
|
||||
|
||||
|
||||
@@ -365,38 +365,44 @@ a { color: var(--accent-strong); }
|
||||
|
||||
<section>
|
||||
<h2>What the mail actually looks like</h2>
|
||||
<p class="section-lede">A real draft generated from <code>conference_contacts.xlsx</code>, using the <code>wavegate_focus</code> template — this is exactly what sits in <code>output_drafts/</code> today, unedited.</p>
|
||||
<p class="section-lede">A real draft generated from <code>conference_contacts.xlsx</code>, using the <code>reconnect_message</code> template — one of two templates now available in the tool's dropdown (the other, <code>wavegate_focus</code>, stays available too).</p>
|
||||
<div class="email-preview">
|
||||
<div class="email-preview-header">
|
||||
<div><span class="k">Subject:</span>Following up on Radiation-Hardness Testing & Wavegate</div>
|
||||
<div><span class="k">Subject:</span>Following up on TXServices & Wavegate</div>
|
||||
<div><span class="k">From:</span>mckinley@mckinleyblack.com</div>
|
||||
<div><span class="k">To:</span>pgaal@txproducts.de</div>
|
||||
</div>
|
||||
<div class="email-preview-body">Hi Peter,
|
||||
|
||||
It was great meeting you at SPIE 2026. Following up on our conversation
|
||||
about Radiation-Hardness Testing & Wavegate on 25.08.26.
|
||||
It was a pleasure meeting you at SPIE 2026 on 25.08.26.
|
||||
|
||||
Attached is more detail on how it speeds up beamline experiments.
|
||||
I really enjoyed your session on Radiation-Hardness Testing & Wavegate, I found it insightful and gave me several new perspectives.
|
||||
|
||||
Happy to set up a short call if useful.
|
||||
I'd love to stay in touch, as I believe there are opportunities where our expertise could complement each other and create value for both of us.
|
||||
|
||||
I've attached a short overview of TXproducts, including our services and our flagship solution, WaveGate.
|
||||
|
||||
If you're open to it, I'd be happy to schedule a 20–30 minute call in the coming weeks to explore potential collaboration.
|
||||
|
||||
Best regards,
|
||||
|
||||
Mckinley</div>
|
||||
<div class="email-preview-attachments"><code>txservices.pdf</code><code>wavegate.pdf</code></div>
|
||||
</div>
|
||||
<div class="template-source-label">The template behind it — <code>message_templates/wavegate_focus.txt</code></div>
|
||||
<pre class="template-source">SUBJECT: Following up on <span class="ph">{{Topic}}</span>
|
||||
<div class="template-source-label">The template behind it — <code>message_templates/reconnect_message.txt</code></div>
|
||||
<pre class="template-source">SUBJECT: Following up on TXServices & Wavegate
|
||||
|
||||
Hi <span class="ph">{{First Name}}</span>,
|
||||
|
||||
It was great meeting you at <span class="ph">{{Conference}}</span>. Following up on our conversation
|
||||
about <span class="ph">{{Topic}}</span> on <span class="ph">{{Date Met}}</span>.
|
||||
It was a pleasure meeting you at <span class="ph">{{Conference}}</span> on <span class="ph">{{Date Met}}</span>.
|
||||
|
||||
Attached is more detail on how it speeds up beamline experiments.
|
||||
I really enjoyed your session on <span class="ph">{{Topic}}</span>, I found it insightful and gave me several new perspectives.
|
||||
|
||||
Happy to set up a short call if useful.
|
||||
I'd love to stay in touch, as I believe there are opportunities where our expertise could complement each other and create value for both of us.
|
||||
|
||||
I've attached a short overview of TXproducts, including our services and our flagship solution, WaveGate.
|
||||
|
||||
If you're open to it, I'd be happy to schedule a 20–30 minute call in the coming weeks to explore potential collaboration.
|
||||
|
||||
Best regards,
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
.contact-row:last-child { border-bottom: none; }
|
||||
.contact-row .meta { color: #5b6a78; font-size: 0.85rem; }
|
||||
.contact-row.disabled { color: #b6bfc8; }
|
||||
.contact-row.no-match { display: none; }
|
||||
.contact-list-scroll { max-height: 420px; overflow-y: auto; padding-right: 4px; }
|
||||
select, input[type=text] {
|
||||
padding: 7px 9px;
|
||||
border: 1px solid #d3dbe4;
|
||||
|
||||
@@ -7,15 +7,20 @@
|
||||
|
||||
<form method="post">
|
||||
<div class="card">
|
||||
<h2>Contacts</h2>
|
||||
<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 %}
|
||||
<div class="contact-row {% if c.sent %}disabled{% endif %}">
|
||||
<input type="checkbox" class="contact-check" name="contacts" value="{{ c.email }}" {% if c.sent %}disabled{% endif %}>
|
||||
{% 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 %}
|
||||
@@ -24,6 +29,10 @@
|
||||
{% 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">
|
||||
@@ -66,5 +75,15 @@
|
||||
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 %}
|
||||
@@ -95,6 +95,7 @@ def index():
|
||||
attachments=get_attachment_options(),
|
||||
default_from=mm.DEFAULT_FROM,
|
||||
error=request.args.get("error"),
|
||||
preselect_email=(request.args.get("email") or "").strip().lower(),
|
||||
)
|
||||
|
||||
|
||||
@@ -195,59 +196,6 @@ def open_in_mail():
|
||||
return render_template("results.html", lines=lines, warnings=warnings)
|
||||
|
||||
|
||||
@app.route("/quick_open")
|
||||
def quick_open():
|
||||
"""One-click entry point from the Sales & Marketing dash's presenter cards -- opens a
|
||||
real Mail.app draft for a single contact by email, using the default template and
|
||||
attachments. Skips the checkbox/template-picker UI entirely."""
|
||||
email = request.args.get("email", "").strip()
|
||||
template_name = request.args.get("template") or "wavegate_focus.txt"
|
||||
|
||||
if not email:
|
||||
return render_template("results.html", lines=[], warnings=["No email provided."])
|
||||
|
||||
try:
|
||||
full_df = pd.read_excel(mm.CONTACTS_FILE, sheet_name="Contacts")
|
||||
except Exception as exc:
|
||||
return render_template("results.html", lines=[], warnings=[f"Could not read contact sheet: {exc}"])
|
||||
|
||||
if "Draft Opened" not in full_df.columns:
|
||||
full_df["Draft Opened"] = ""
|
||||
|
||||
match_mask = full_df["Email"].astype(str).str.strip().str.lower() == email.lower()
|
||||
if not match_mask.any():
|
||||
return render_template("results.html", lines=[],
|
||||
warnings=[f"No contact with email {email} found in conference_contacts.xlsx yet -- add them there first."])
|
||||
|
||||
try:
|
||||
rows = rows_for_selection([email], template_name)
|
||||
except ValueError as exc:
|
||||
return render_template("results.html", lines=[], warnings=[str(exc)])
|
||||
|
||||
default_attachments = [str(p) for p in DEFAULT_ATTACHMENTS if p.exists()]
|
||||
|
||||
lines = []
|
||||
warnings = []
|
||||
opened_any = False
|
||||
for row_dict, subject, body in rows:
|
||||
if "[[MISSING:" in subject or "[[MISSING:" in body:
|
||||
warnings.append(f"Missing field(s) for {row_dict['Email']} -- check before sending.")
|
||||
try:
|
||||
mm.open_in_mail_app(subject, body, row_dict["Email"], default_attachments, mm.DEFAULT_FROM)
|
||||
lines.append(f"Opened in Mail.app: {row_dict['First Name']} {row_dict['Last Name']} <{row_dict['Email']}>")
|
||||
timestamp = datetime.now().strftime("%d.%m.%y %H:%M")
|
||||
full_df.loc[match_mask, "Draft Opened"] = timestamp
|
||||
opened_any = True
|
||||
mm.log_to_dash_outreach(row_dict["Email"], f"Draft opened in Mail.app ({template_name}), {timestamp}")
|
||||
except RuntimeError as exc:
|
||||
warnings.append(f"Could not open Mail.app draft for {row_dict['Email']}: {exc}")
|
||||
|
||||
if opened_any:
|
||||
full_df.to_excel(mm.CONTACTS_FILE, index=False, sheet_name="Contacts")
|
||||
|
||||
return render_template("results.html", lines=lines, warnings=warnings)
|
||||
|
||||
|
||||
@app.route("/preview", methods=["POST"])
|
||||
def preview():
|
||||
selected = request.form.getlist("contacts")
|
||||
|
||||
+13
-5
@@ -807,7 +807,7 @@
|
||||
<button class="ab-filter-chip" data-source="Engaged Non-Follower">Engaged Non-Follower</button>
|
||||
<button class="ab-filter-chip" data-source="Conference">SPIE 2026 Conference</button>
|
||||
<button class="ab-filter-chip" id="ab-followers-link">Followers ↗</button>
|
||||
<a class="ab-filter-chip" style="text-decoration:none;" href="address_book/conference_mail_merge/SPIE_2026_Presenter_Contacts.xlsx" download>Download Contact Sheet ↓</a>
|
||||
<a class="ab-filter-chip" style="text-decoration:none;" href="address_book/conference_mail_merge/conference_contacts.xlsx" download>Download Contact Sheet ↓</a>
|
||||
<a class="ab-filter-chip" style="text-decoration:none;" href="http://localhost:8505" target="_blank" rel="noopener" title="Opens the Mail Merge tool in a new tab. Must be running first (webapp.py in address_book/conference_mail_merge/).">Open Mail Merge Tool ↗</a>
|
||||
</div>
|
||||
<div class="ab-toolbar">
|
||||
@@ -3828,6 +3828,14 @@
|
||||
{
|
||||
"date": "15.08.26",
|
||||
"note": "Draft opened in Mail.app (wavegate_focus.txt), 15.08.26 21:24"
|
||||
},
|
||||
{
|
||||
"date": "16.08.26",
|
||||
"note": "Draft opened in Mail.app (wavegate_focus.txt), 16.08.26 06:10"
|
||||
},
|
||||
{
|
||||
"date": "16.08.26",
|
||||
"note": "Draft opened in Mail.app (reconnect_message.txt), 16.08.26 08:44"
|
||||
}
|
||||
],
|
||||
"notes": "",
|
||||
@@ -4686,12 +4694,12 @@
|
||||
}).join("") + "</div>";
|
||||
}
|
||||
|
||||
// Hands off to the real Mail Merge tool (localhost:8505), which does the actual template
|
||||
// merge, attachments, and Mail.app draft creation server-side — the tool must be running.
|
||||
// Hands off to the real Mail Merge tool (localhost:8505) with this contact pre-checked,
|
||||
// so the template/attachments/from-address stay a deliberate choice, not an auto-pick.
|
||||
function buildComposeEmailButton(p) {
|
||||
if (!p.email) { return ""; }
|
||||
var href = "http://localhost:8505/quick_open?email=" + encodeURIComponent(p.email);
|
||||
return "<a class=\"conf-compose-email-btn\" href=\"" + href + "\" target=\"_blank\" rel=\"noopener\" title=\"Opens a ready-to-send Mail.app draft via the Mail Merge tool (must be running first).\">Compose Email</a>";
|
||||
var href = "http://localhost:8505/?email=" + encodeURIComponent(p.email);
|
||||
return "<a class=\"conf-compose-email-btn\" href=\"" + href + "\" target=\"_blank\" rel=\"noopener\" title=\"Opens the Mail Merge tool with this contact selected (must be running first).\">Compose Email</a>";
|
||||
}
|
||||
|
||||
function renderConferences() {
|
||||
|
||||
Reference in new issue
Block a user