← back to Marketing Command Center
clients panel: quick filters (email/IG/LinkedIn/uncontacted), Copy-all-emails, IG+LinkedIn count pills, density toggle (completes sort+density rule)
63885bc29b9998939e92bcc7e8ec2481514d0039 · 2026-07-16 11:57:39 -0700 · steve
Files touched
M public/panels/clients.htmlM public/panels/clients.js
Diff
commit 63885bc29b9998939e92bcc7e8ec2481514d0039
Author: steve <steve@designerwallcoverings.com>
Date: Thu Jul 16 11:57:39 2026 -0700
clients panel: quick filters (email/IG/LinkedIn/uncontacted), Copy-all-emails, IG+LinkedIn count pills, density toggle (completes sort+density rule)
---
public/panels/clients.html | 19 +++++++++++++++++++
public/panels/clients.js | 43 +++++++++++++++++++++++++++++++++++++++++--
2 files changed, 60 insertions(+), 2 deletions(-)
diff --git a/public/panels/clients.html b/public/panels/clients.html
index 73c3539..1bc6cb0 100644
--- a/public/panels/clients.html
+++ b/public/panels/clients.html
@@ -1,3 +1,9 @@
+<style>
+ #cl-list.cl-compact .cl-row { padding: 2px 0 !important; }
+ #cl-list.cl-compact .cl-row b { font-size: 12px !important; }
+ #cl-list.cl-compact .cl-row span { line-height: 1.12 !important; }
+ #cl-filters button.gold { color: #1a1a1a; }
+</style>
<div id="cl-root" style="max-width:960px;">
<div class="muted-banner" id="cl-banner">Loading client & prospect groups…</div>
@@ -16,6 +22,8 @@
<b id="cl-grouptitle" style="font-size:14px;"></b>
<span class="pill" id="cl-progress">0 / 0 contacted</span>
<span class="pill" id="cl-emailcount"></span>
+ <span class="pill" id="cl-igcount"></span>
+ <span class="pill" id="cl-licount"></span>
<span style="flex:1;"></span>
<button class="btn gold" id="cl-export" style="font-size:11px;padding:5px 9px;">⬇ Export CSV</button>
<button class="btn ghost" id="cl-reset" style="font-size:11px;padding:5px 9px;">Reset checks</button>
@@ -33,6 +41,17 @@
<option value="linkedin">Has LinkedIn first</option>
</select>
</div>
+ <div class="row" id="cl-filters" style="gap:6px;margin-top:10px;align-items:center;flex-wrap:wrap;">
+ <span class="muted" style="font-size:11px;">Only:</span>
+ <button class="btn ghost" data-filter="email" style="font-size:10.5px;padding:4px 9px;">✉ email</button>
+ <button class="btn ghost" data-filter="instagram" style="font-size:10.5px;padding:4px 9px;">IG</button>
+ <button class="btn ghost" data-filter="linkedin" style="font-size:10.5px;padding:4px 9px;">LinkedIn</button>
+ <button class="btn ghost" data-filter="uncontacted" style="font-size:10.5px;padding:4px 9px;">○ uncontacted</button>
+ <span style="flex:1;"></span>
+ <span class="muted" id="cl-copymsg" style="font-size:10.5px;"></span>
+ <button class="btn ghost" id="cl-copyemails" style="font-size:10.5px;padding:4px 9px;">⧉ Copy emails</button>
+ <button class="btn ghost" id="cl-density" style="font-size:10.5px;padding:4px 9px;">Compact</button>
+ </div>
<div class="muted" id="cl-groupnote" style="font-size:11.5px;margin-top:8px;"></div>
</div>
diff --git a/public/panels/clients.js b/public/panels/clients.js
index ede787f..92da8f5 100644
--- a/public/panels/clients.js
+++ b/public/panels/clients.js
@@ -58,18 +58,32 @@ window.MCC_PANELS['clients'] = {
try { data = await jget('/data/' + d.file); } catch { $('#cl-list').innerHTML = '<div class="card muted">Could not load ' + esc(d.file) + '.</div>'; return; }
const arr = data.clients || data.businesses || [];
RECS = arr.map(r => norm(r, d.kind));
- $('#cl-emailcount').textContent = (data.with_email != null ? data.with_email.toLocaleString() : RECS.filter(r => r.email).length.toLocaleString()) + ' with email';
+ const nEmail = RECS.filter(r => r.email).length, nIg = RECS.filter(r => r._raw.instagram).length, nLi = RECS.filter(r => r._raw.linkedin).length;
+ $('#cl-emailcount').textContent = nEmail.toLocaleString() + ' email';
+ $('#cl-igcount').textContent = nIg ? nIg.toLocaleString() + ' IG' : '';
+ $('#cl-licount').textContent = nLi ? nLi.toLocaleString() + ' LinkedIn' : '';
$('#cl-groupnote').textContent = `Source: ${data.source || d.file}${data.updated ? ' · updated ' + data.updated : ''}. LinkedIn opens a people/company search; IG opens a Google “<name> instagram” lookup — open the right match and follow.`;
$('#cl-search').value = '';
$('#cl-search').oninput = render;
$('#cl-sort').value = sortMode();
$('#cl-sort').onchange = () => { try { localStorage.setItem(SK, $('#cl-sort').value); } catch {} render(); };
+ FILTERS.clear();
+ $('#cl-filters').querySelectorAll('[data-filter]').forEach(b => {
+ b.className = 'btn ghost';
+ b.onclick = () => { const f = b.dataset.filter; if (FILTERS.has(f)) { FILTERS.delete(f); b.className = 'btn ghost'; } else { FILTERS.add(f); b.className = 'btn gold'; } render(); };
+ });
+ $('#cl-copymsg').textContent = '';
+ $('#cl-copyemails').onclick = copyEmails;
+ $('#cl-density').onclick = toggleDensity;
+ applyDensity();
$('#cl-reset').onclick = () => { if (confirm('Clear contacted checks for ' + d.label + '?')) { save(d.id, new Set()); render(); } };
$('#cl-export').onclick = () => exportCSV(d);
render();
}
const SK = 'mcc_cl_sort';
+ const DK = 'mcc_cl_density';
+ const FILTERS = new Set(); // active quick-filters: email | instagram | linkedin | uncontacted
const sortMode = () => { try { return localStorage.getItem(SK) || 'default'; } catch { return 'default'; } };
function sortRecs(list) {
const az = (a, b) => a.label.localeCompare(b.label);
@@ -86,10 +100,35 @@ window.MCC_PANELS['clients'] = {
}
function currentMatches() {
const q = ($('#cl-search').value || '').trim().toLowerCase();
- const list = q ? RECS.filter(r => r._all.includes(q)) : RECS;
+ let list = q ? RECS.filter(r => r._all.includes(q)) : RECS;
+ if (FILTERS.size) {
+ const set = FILTERS.has('uncontacted') ? done(current.id) : null;
+ list = list.filter(r =>
+ (!FILTERS.has('email') || r.email) &&
+ (!FILTERS.has('instagram') || r._raw.instagram) &&
+ (!FILTERS.has('linkedin') || r._raw.linkedin) &&
+ (!FILTERS.has('uncontacted') || !set.has(r.id)));
+ }
return sortRecs(list);
}
+ async function copyEmails() {
+ const emails = Array.from(new Set(currentMatches().map(r => r.email).filter(Boolean)));
+ if (!emails.length) { $('#cl-copymsg').textContent = 'no emails in view'; return; }
+ try { await navigator.clipboard.writeText(emails.join(', ')); $('#cl-copymsg').textContent = `copied ${emails.length.toLocaleString()} emails`; }
+ catch { $('#cl-copymsg').textContent = 'copy blocked — export CSV instead'; }
+ }
+ function applyDensity() {
+ const compact = (() => { try { return localStorage.getItem(DK) === 'compact'; } catch { return false; } })();
+ $('#cl-list').classList.toggle('cl-compact', compact);
+ $('#cl-density').textContent = compact ? 'Comfortable' : 'Compact';
+ }
+ function toggleDensity() {
+ const compact = !$('#cl-list').classList.contains('cl-compact');
+ try { localStorage.setItem(DK, compact ? 'compact' : 'comfortable'); } catch {}
+ applyDensity();
+ }
+
// Review-oriented CSV export of the current group (respects the filter).
// Includes emails + the LinkedIn/Instagram discovery URLs. Local download —
// this does NOT send anything to Constant Contact.
← d885131 auto-save: 2026-07-16T11:43:05 (1 files) — public/data/clien
·
back to Marketing Command Center
·
clients panel: merged ★ All Prospects group (search/sort/fil d544ba1 →