← 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
M public/admin/index.html
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 →