[object Object]

← back to Rentv 2026

feat(services): 'Solicit all visible' bulk action (admin)

0250fee38c8eb0ed95c9015d4c95460bc8d6c77d · 2026-08-06 13:21:18 -0700 · Steve

Adds a one-click 'Solicit all visible' button (admin-only, in the control bar) that
queues every firm currently loaded on screen into the outreach basket at once —
deduped, respecting the current category/metro/search/city filter and any Load-More
pages already expanded. Refactored the single-Solicit path into a shared
addToOutreach() helper; BYID is cleared on each reset so 'visible' means exactly the
current view; button shows an 'Added N (M visible)' confirmation.

Files touched

Diff

commit 0250fee38c8eb0ed95c9015d4c95460bc8d6c77d
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 6 13:21:18 2026 -0700

    feat(services): 'Solicit all visible' bulk action (admin)
    
    Adds a one-click 'Solicit all visible' button (admin-only, in the control bar) that
    queues every firm currently loaded on screen into the outreach basket at once —
    deduped, respecting the current category/metro/search/city filter and any Load-More
    pages already expanded. Refactored the single-Solicit path into a shared
    addToOutreach() helper; BYID is cleared on each reset so 'visible' means exactly the
    current view; button shows an 'Added N (M visible)' confirmation.
---
 public/services.html | 36 +++++++++++++++++++++++++-----------
 1 file changed, 25 insertions(+), 11 deletions(-)

diff --git a/public/services.html b/public/services.html
index 87c9e5ed..0c086b0d 100644
--- a/public/services.html
+++ b/public/services.html
@@ -99,6 +99,8 @@
   .obasket.show{display:inline-flex}
   .obasket b{background:var(--red);border-radius:20px;padding:1px 9px;font-variant-numeric:tabular-nums}
   .obasket:hover{background:#141a21}
+  .solall{border:1.5px solid #1857b8;color:#1857b8;background:#f2f7ff;border-radius:8px;padding:7px 12px;font:800 12px 'Inter';cursor:pointer;white-space:nowrap}
+  .solall:hover{background:#1857b8;color:#fff}
 
   .more{display:block;width:100%;margin:14px 0 60px;padding:13px;border:1.5px solid var(--line);border-radius:10px;background:#fff;font:800 13px 'Inter';color:var(--ink);cursor:pointer}
   .more:hover{border-color:var(--red);color:var(--red)}
@@ -166,6 +168,7 @@
   <div class="ctlbar">
     <div class="meta" id="meta"></div>
     <div class="ctls">
+      <button class="solall" id="solall" style="display:none" title="Add every firm currently loaded on screen to the outreach list">✉ Solicit all visible</button>
       <label class="ctl">Sort
         <select id="sort" aria-label="Sort providers">
           <option value="locations">Most locations</option>
@@ -219,26 +222,37 @@ const GKEY='rentv.'+'growthLI.v1';   // shared localStorage bucket social.html r
 const SOCIAL_CAT={construction:'Construction / GCs',lenders:'Lenders & Capital Sources',escrow:'Escrow',developers:'Developers',legal:'Legal',title:'Title',property:'Vendors / Service Providers'};
 function gload(){ try{ return JSON.parse(localStorage.getItem(GKEY))||[]; }catch(e){ return []; } }
 function gsave(l){ localStorage.setItem(GKEY,JSON.stringify(l)); }
-function outreachRefresh(){ const n=gload().filter(x=>x&&x.kind==='contact').length; $('ocount').textContent=n; $('obasket').classList.toggle('show',ADMIN); }
+function outreachRefresh(){ const n=gload().filter(x=>x&&x.kind==='contact').length; $('ocount').textContent=n; $('obasket').classList.toggle('show',ADMIN); $('solall').style.display=ADMIN?'inline-flex':'none'; }
 function inOutreach(name,scat){ return gload().some(x=>x&&x.kind==='contact'&&x.cat===scat&&(x.name||'').toLowerCase()===String(name).toLowerCase()); }
+function addToOutreach(r){   // returns true if newly added (deduped by name+mapped role)
+  const scat=SOCIAL_CAT[cat]||'Vendors / Service Providers';
+  if(inOutreach(r.name,scat)) return false;
+  const l=gload();
+  l.push({ id:'sv'+Date.now().toString(36)+Math.floor(Math.random()*1e6).toString(36), kind:'contact', cat:scat,
+    name:cap(r.name), sub:[r.city?cap(r.city):'',r.loc_count>1?(r.loc_count+' locations'):'',r.featured?'Featured':''].filter(Boolean).join(' · '),
+    terms:r.name+' '+(r.city||'')+' commercial real estate', market:r.market||market,
+    phone:r.phone||'', website:r.website||'', license_no:r.license_no||'', status:0, added:Date.now() });
+  gsave(l); return true;
+}
 function solicit(id,btn){
   const r=BYID[id]; if(!r)return;
-  const scat=SOCIAL_CAT[cat]||'Vendors / Service Providers';
-  if(!inOutreach(r.name,scat)){
-    const l=gload();
-    l.push({ id:'sv'+Date.now().toString(36)+Math.floor(Math.random()*1e4).toString(36), kind:'contact', cat:scat,
-      name:cap(r.name), sub:[r.city?cap(r.city):'',r.loc_count>1?(r.loc_count+' locations'):'',r.featured?'Featured':''].filter(Boolean).join(' · '),
-      terms:r.name+' '+(r.city||'')+' commercial real estate', market:r.market||market,
-      phone:r.phone||'', website:r.website||'', license_no:r.license_no||'', status:0, added:Date.now() });
-    gsave(l);
-  }
+  addToOutreach(r);
   if(btn){ btn.classList.add('added'); btn.textContent='✓ In outreach'; }
   outreachRefresh();
 }
+function solicitAll(){
+  const rows=Object.values(BYID); let added=0;
+  rows.forEach(r=>{ if(addToOutreach(r)) added++; });
+  document.querySelectorAll('.act.sol').forEach(b=>{ b.classList.add('added'); b.textContent='✓ In outreach'; });
+  outreachRefresh();
+  const btn=$('solall'), prev=btn.dataset.label||btn.textContent; btn.dataset.label=prev;
+  btn.textContent='✓ Added '+added+' ('+rows.length+' visible)'; setTimeout(()=>{ btn.textContent=btn.dataset.label; },2800);
+}
 async function checkAdmin(){
   try{ const w=await (await fetch('/api/whoami')).json(); ADMIN=(w&&w.role==='admin'); }catch(e){ ADMIN=false; }
   outreachRefresh();
 }
+$('solall').onclick=solicitAll;
 
 // URL state (deep-linkable): /services?cat=lenders&city=GLENDALE&q=capital
 const p=new URLSearchParams(location.search);
@@ -318,7 +332,7 @@ function card(r){
 
 async function load(reset){
   const my=++seq;
-  if(reset){$('rows').innerHTML='';$('meta').innerHTML='<span class="spin"></span>Loading…';$('more').style.display='none';}
+  if(reset){$('rows').innerHTML='';$('meta').innerHTML='<span class="spin"></span>Loading…';$('more').style.display='none';for(const k in BYID)delete BYID[k];}
   const u=new URLSearchParams(); u.set('cat',cat); u.set('market',market); u.set('sort',sort); u.set('limit',PAGE); u.set('offset',offset);
   if(q)u.set('q',q); if(city)u.set('city',city);
   let d; try{ d=await (await fetch('/api/services?'+u.toString())).json(); }catch(e){ if(my===seq)$('meta').textContent='Services directory unavailable — try again.'; return; }

← 17716ea2 harden(location): Cody gate — normalize whitespace before th  ·  back to Rentv 2026  ·  auto-data-snapshot: 2026-08-06T13:27:53 (7 data files) — dat a3ae8ad5 →