[object Object]

← 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

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 →