[object Object]

← back to Prestige Car Wash

Admin Overview: lead-funnel summary (status counts, new-this-week, conversion %, unworked-leads nudge) — dtd:D

53de6fdd74698de8b731620b6d5f38fb42d4b1ad · 2026-07-26 15:39:49 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit 53de6fdd74698de8b731620b6d5f38fb42d4b1ad
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Sun Jul 26 15:39:49 2026 -0700

    Admin Overview: lead-funnel summary (status counts, new-this-week, conversion %, unworked-leads nudge) — dtd:D
---
 public/admin/index.html | 24 ++++++++++++++++++++++--
 1 file changed, 22 insertions(+), 2 deletions(-)

diff --git a/public/admin/index.html b/public/admin/index.html
index afd3fa8..79efa27 100644
--- a/public/admin/index.html
+++ b/public/admin/index.html
@@ -370,11 +370,30 @@ function render(){
   });
 }
 
+// Lead funnel roll-up (dtd:D 2026-07-26) — computed from /api/admin/leads (which carries
+// each lead's pipeline status), so the owner sees funnel health where they already look.
+function leadFunnel(leads){
+  const st={new:0,contacted:0,booked:0,won:0,lost:0}; let week=0; const wk=Date.now()-7*864e5;
+  (leads||[]).forEach(l=>{ st[l.status]=(st[l.status]||0)+1; const t=l.created_at?new Date(l.created_at).getTime():0; if(t>=wk) week++; });
+  const total=(leads||[]).length;
+  return {st,total,week,conv:total?Math.round((st.won/total)*100):0};
+}
+function funnelCard(f){
+  const cells=[['new','new · unworked'],['contacted','contacted'],['booked','booked'],['won','won'],['lost','lost']];
+  return `<div class="card"><div class="body">
+    <div class="ttl">📨 Lead funnel</div>
+    <div class="stat">${cells.map(([k,lab])=>`<div class="s"><b>${f.st[k]}</b><span>${lab}</span></div>`).join('')}</div>
+    <div class="kv"><span>New this week</span><b>${f.week}</b></div>
+    <div class="kv"><span>Conversion (won / total)</span><b>${f.conv}%</b></div>
+    ${f.st.new>0?`<div class="blurb" style="color:var(--warn)">⚠️ ${f.st.new} new lead${f.st.new!==1?'s':''} waiting to be worked → <a href="#" onclick="go('leads');return false" style="color:var(--brand)">open Leads</a></div>`:'<div class="blurb b-mut" style="color:var(--mut)">No unworked leads — all caught up.</div>'}
+  </div></div>`;
+}
 function renderOverview(){
   Promise.all([
     fetch('/api/health').then(r=>r.json()),
-    fetch('/api/admin/credentials').then(r=>r.json()).catch(()=>[])
-  ]).then(([h,creds])=>{
+    fetch('/api/admin/credentials').then(r=>r.json()).catch(()=>[]),
+    fetch('/api/admin/leads').then(r=>r.json()).catch(()=>[])
+  ]).then(([h,creds,leads])=>{
     const c=h.counts;
     const places=(creds||[]).find(k=>k.key==='GOOGLE_PLACES_API_KEY');
     const placesTag=places&&places.present
@@ -384,6 +403,7 @@ function renderOverview(){
       <div class="stat">
         ${Object.entries(c).map(([k,v])=>`<div class="s"><b>${v}</b><span>${k}</span></div>`).join('')}
       </div>
+      ${funnelCard(leadFunnel(leads))}
       <div class="card"><div class="body">
         <div class="ttl">Growth command center</div>
         <div class="blurb">Every bucket below is seeded from live SFV market research. Buckets with a search box are data-driven; the rest (best-times, Google, credentials) are tools. Start in <b>✨ Compose & Post</b> to build a social post from your real photos/videos and open each network's composer in one click. Cards show their created date + time. All write/spend/post actions are gated.</div>

← 7c77409 Admin: lead-status pipeline (new→contacted→booked/won/lost)  ·  back to Prestige Car Wash  ·  Admin: flag unworked leads going cold (>48h) on cards + funn f74fd9f →