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
|
*.env
|
||||||
address_book/conference_mail_merge/conference_contacts.xlsx
|
address_book/conference_mail_merge/conference_contacts.xlsx
|
||||||
address_book/conference_mail_merge/output_drafts/
|
address_book/conference_mail_merge/output_drafts/
|
||||||
|
~$*.xlsx
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.Rhistory
|
.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}},
|
Hi {{First Name}},
|
||||||
|
|
||||||
It was great meeting you at {{Conference}}. Following up on our conversation
|
It was great meeting you at {{Conference}} on {{Date Met}}.
|
||||||
about {{Topic}} 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,
|
Best regards,
|
||||||
|
|
||||||
|
|||||||
@@ -365,38 +365,44 @@ a { color: var(--accent-strong); }
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>What the mail actually looks like</h2>
|
<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">
|
||||||
<div class="email-preview-header">
|
<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">From:</span>mckinley@mckinleyblack.com</div>
|
||||||
<div><span class="k">To:</span>pgaal@txproducts.de</div>
|
<div><span class="k">To:</span>pgaal@txproducts.de</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="email-preview-body">Hi Peter,
|
<div class="email-preview-body">Hi Peter,
|
||||||
|
|
||||||
It was great meeting you at SPIE 2026. Following up on our conversation
|
It was a pleasure meeting you at SPIE 2026 on 25.08.26.
|
||||||
about Radiation-Hardness Testing & Wavegate 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,
|
Best regards,
|
||||||
|
|
||||||
Mckinley</div>
|
Mckinley</div>
|
||||||
<div class="email-preview-attachments"><code>txservices.pdf</code><code>wavegate.pdf</code></div>
|
<div class="email-preview-attachments"><code>txservices.pdf</code><code>wavegate.pdf</code></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="template-source-label">The template behind it — <code>message_templates/wavegate_focus.txt</code></div>
|
<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 <span class="ph">{{Topic}}</span>
|
<pre class="template-source">SUBJECT: Following up on TXServices & Wavegate
|
||||||
|
|
||||||
Hi <span class="ph">{{First Name}}</span>,
|
Hi <span class="ph">{{First Name}}</span>,
|
||||||
|
|
||||||
It was great meeting you at <span class="ph">{{Conference}}</span>. Following up on our conversation
|
It was a pleasure meeting you at <span class="ph">{{Conference}}</span> on <span class="ph">{{Date Met}}</span>.
|
||||||
about <span class="ph">{{Topic}}</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,
|
Best regards,
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@
|
|||||||
.contact-row:last-child { border-bottom: none; }
|
.contact-row:last-child { border-bottom: none; }
|
||||||
.contact-row .meta { color: #5b6a78; font-size: 0.85rem; }
|
.contact-row .meta { color: #5b6a78; font-size: 0.85rem; }
|
||||||
.contact-row.disabled { color: #b6bfc8; }
|
.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] {
|
select, input[type=text] {
|
||||||
padding: 7px 9px;
|
padding: 7px 9px;
|
||||||
border: 1px solid #d3dbe4;
|
border: 1px solid #d3dbe4;
|
||||||
|
|||||||
@@ -7,15 +7,20 @@
|
|||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<div class="card">
|
<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;">
|
<div style="margin-bottom:10px;">
|
||||||
<button type="button" onclick="document.querySelectorAll('.contact-check').forEach(c=>c.checked=true)">Select All</button>
|
<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>
|
<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>
|
<a href="/" style="margin-left:8px; font-size:0.85rem;">Refresh from Excel</a>
|
||||||
</div>
|
</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 %}
|
{% for c in contacts %}
|
||||||
<div class="contact-row {% if c.sent %}disabled{% endif %}">
|
{% set is_preselected = preselect_email and c.email|lower == preselect_email %}
|
||||||
<input type="checkbox" class="contact-check" name="contacts" value="{{ c.email }}" {% if c.sent %}disabled{% endif %}>
|
{% 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>
|
<span>{{ c.first }} {{ c.last }} <{{ c.email }}></span>
|
||||||
{% if c.conference %}<span class="meta">— {{ c.conference }}</span>{% endif %}
|
{% 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.draft_opened %}<span class="meta">(draft opened {{ c.draft_opened }})</span>{% endif %}
|
||||||
@@ -24,6 +29,10 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<p class="sub">No contacts in the sheet yet.</p>
|
<p class="sub">No contacts in the sheet yet.</p>
|
||||||
{% endfor %}
|
{% 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>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -66,5 +75,15 @@
|
|||||||
btn.disabled = true;
|
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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -95,6 +95,7 @@ def index():
|
|||||||
attachments=get_attachment_options(),
|
attachments=get_attachment_options(),
|
||||||
default_from=mm.DEFAULT_FROM,
|
default_from=mm.DEFAULT_FROM,
|
||||||
error=request.args.get("error"),
|
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)
|
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"])
|
@app.route("/preview", methods=["POST"])
|
||||||
def preview():
|
def preview():
|
||||||
selected = request.form.getlist("contacts")
|
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="Engaged Non-Follower">Engaged Non-Follower</button>
|
||||||
<button class="ab-filter-chip" data-source="Conference">SPIE 2026 Conference</button>
|
<button class="ab-filter-chip" data-source="Conference">SPIE 2026 Conference</button>
|
||||||
<button class="ab-filter-chip" id="ab-followers-link">Followers ↗</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>
|
<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>
|
||||||
<div class="ab-toolbar">
|
<div class="ab-toolbar">
|
||||||
@@ -3828,6 +3828,14 @@
|
|||||||
{
|
{
|
||||||
"date": "15.08.26",
|
"date": "15.08.26",
|
||||||
"note": "Draft opened in Mail.app (wavegate_focus.txt), 15.08.26 21:24"
|
"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": "",
|
"notes": "",
|
||||||
@@ -4686,12 +4694,12 @@
|
|||||||
}).join("") + "</div>";
|
}).join("") + "</div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hands off to the real Mail Merge tool (localhost:8505), which does the actual template
|
// Hands off to the real Mail Merge tool (localhost:8505) with this contact pre-checked,
|
||||||
// merge, attachments, and Mail.app draft creation server-side — the tool must be running.
|
// so the template/attachments/from-address stay a deliberate choice, not an auto-pick.
|
||||||
function buildComposeEmailButton(p) {
|
function buildComposeEmailButton(p) {
|
||||||
if (!p.email) { return ""; }
|
if (!p.email) { return ""; }
|
||||||
var href = "http://localhost:8505/quick_open?email=" + encodeURIComponent(p.email);
|
var href = "http://localhost:8505/?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>";
|
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() {
|
function renderConferences() {
|
||||||
|
|||||||
Reference in new issue
Block a user