| Line 88... |
Line 88... |
| 88 |
.fstep .fn{font-size:10.5px;text-transform:uppercase;letter-spacing:.03em;color:var(--muted);font-weight:700}
|
88 |
.fstep .fn{font-size:10.5px;text-transform:uppercase;letter-spacing:.03em;color:var(--muted);font-weight:700}
|
| 89 |
.fstep .fv{font-size:24px;font-weight:770;margin-top:6px}
|
89 |
.fstep .fv{font-size:24px;font-weight:770;margin-top:6px}
|
| 90 |
.fstep .fbar{height:5px;border-radius:3px;margin-top:9px;background:linear-gradient(90deg,var(--navy),var(--brand))}
|
90 |
.fstep .fbar{height:5px;border-radius:3px;margin-top:9px;background:linear-gradient(90deg,var(--navy),var(--brand))}
|
| 91 |
.fstep .drop{font-size:10.5px;color:var(--red);margin-top:6px;font-weight:600}
|
91 |
.fstep .drop{font-size:10.5px;color:var(--red);margin-top:6px;font-weight:600}
|
| 92 |
.fstep .keep{font-size:10.5px;color:var(--good);margin-top:6px;font-weight:600}
|
92 |
.fstep .keep{font-size:10.5px;color:var(--good);margin-top:6px;font-weight:600}
|
| - |
|
93 |
.fstep .fcap[data-drop]{cursor:pointer;text-decoration:underline dotted;text-underline-offset:2px}
|
| - |
|
94 |
.fstep .fcap[data-drop]:hover{text-decoration-style:solid}
|
| 93 |
.fstep .arrow{position:absolute;right:-11px;top:50%;transform:translateY(-50%);z-index:2;color:#c5cce0;font-size:14px}
|
95 |
.fstep .arrow{position:absolute;right:-11px;top:50%;transform:translateY(-50%);z-index:2;color:#c5cce0;font-size:14px}
|
| 94 |
|
96 |
|
| 95 |
/* table */
|
97 |
/* table */
|
| 96 |
table{width:100%;border-collapse:collapse;font-size:13px}
|
98 |
table{width:100%;border-collapse:collapse;font-size:13px}
|
| 97 |
th,td{text-align:left;padding:9px 9px;border-bottom:1px solid var(--line2)}
|
99 |
th,td{text-align:left;padding:9px 9px;border-bottom:1px solid var(--line2)}
|
| Line 392... |
Line 394... |
| 392 |
<div class="search">
|
394 |
<div class="search">
|
| 393 |
<span class="si">🔍</span>
|
395 |
<span class="si">🔍</span>
|
| 394 |
<input id="lmsSearch" placeholder="Search LMS ID, business, phone, retailer… e.g. LMS-UPW-26-000123">
|
396 |
<input id="lmsSearch" placeholder="Search LMS ID, business, phone, retailer… e.g. LMS-UPW-26-000123">
|
| 395 |
<span class="hint">/ to focus</span>
|
397 |
<span class="hint">/ to focus</span>
|
| 396 |
</div>
|
398 |
</div>
|
| - |
|
399 |
#if($canCreateLead)
|
| 397 |
<button class="abtn coral" onclick="window.open(CTX+'/getOpenLead','_blank')">+ New Lead</button>
|
400 |
<button class="abtn coral" onclick="window.open(CTX+'/getOpenLead','_blank')">+ New Lead</button>
|
| - |
|
401 |
#end
|
| 398 |
<div class="role">
|
402 |
<div class="role">
|
| 399 |
<div class="av" title="Signed in as $!viewerName">$!viewerInitials</div>
|
403 |
<div class="av" title="Signed in as $!viewerName">$!viewerInitials</div>
|
| 400 |
<div class="who"><b>$!viewerName</b><br><span>$!viewerRole</span></div>
|
404 |
<div class="who"><b>$!viewerName</b><br><span>$!viewerRole</span></div>
|
| 401 |
</div>
|
405 |
</div>
|
| 402 |
</div>
|
406 |
</div>
|
| Line 1176... |
Line 1180... |
| 1176 |
function renderFunnel(funnel){
|
1180 |
function renderFunnel(funnel){
|
| 1177 |
var byStage={}; funnel.forEach(function(s){ byStage[s.stage]=s; });
|
1181 |
var byStage={}; funnel.forEach(function(s){ byStage[s.stage]=s; });
|
| 1178 |
// First box is every lead created in the period (server key ALL) -- not the NEW stage, which is
|
1182 |
// First box is every lead created in the period (server key ALL) -- not the NEW stage, which is
|
| 1179 |
// the "New" KPI card. BEAT_PLANNED / VISITED / ONBOARDED are evidence-derived on the server.
|
1183 |
// the "New" KPI card. BEAT_PLANNED / VISITED / ONBOARDED are evidence-derived on the server.
|
| 1180 |
var seq=[['ALL','All leads'],['ASSIGNED','Assigned'],['CONTACTED','Contacted'],['QUALIFIED','Qualified'],['BEAT_PLANNED','Beat planned'],['VISITED','Visited'],['ONBOARDED','Onboarded']];
|
1184 |
var seq=[['ALL','All leads'],['ASSIGNED','Assigned'],['CONTACTED','Contacted'],['QUALIFIED','Qualified'],['BEAT_PLANNED','Beat planned'],['VISITED','Visited'],['ONBOARDED','Onboarded']];
|
| 1181 |
var h=''; var prev=null;
|
1185 |
var h=''; var prev=null, prevKey=null;
|
| 1182 |
seq.forEach(function(p,idx){ var s=byStage[p[0]]; if(!s) return; var last=(idx===seq.length-1);
|
1186 |
seq.forEach(function(p,idx){ var s=byStage[p[0]]; if(!s) return; var last=(idx===seq.length-1);
|
| - |
|
1187 |
// Leads that fell out between the previous box and this one: at the previous stage exactly,
|
| - |
|
1188 |
// or -- for the first gap -- anything in scope that never reached ASSIGNED (still NEW, or
|
| - |
|
1189 |
// closed as NOT_INTERESTED / DROPPED). Same buckets the drill endpoint already understands.
|
| - |
|
1190 |
var dropKey = prevKey===null ? '' : (prevKey==='ALL' ? 'NEW,NOT_INTERESTED,DROPPED' : prevKey);
|
| - |
|
1191 |
var dropAttr = dropKey ? ' data-drop="'+esc(dropKey)+'" data-droplabel="'+esc('Dropped before '+p[1])+'" title="Open the leads that did not reach '+esc(p[1])+'"' : '';
|
| 1183 |
var cap; if(idx===0){ cap='<div class="keep">created</div>'; }
|
1192 |
var cap; if(idx===0){ cap='<div class="keep">created</div>'; }
|
| 1184 |
else if(last){ cap='<div class="keep">'+s.pct+'% conversion</div>'; }
|
1193 |
else if(last){ var dropL=prev?prev.count-s.count:0;
|
| - |
|
1194 |
cap='<div class="keep"><span class="fcap"'+dropAttr+'>'+s.pct+'% conversion'+(dropL>0?(' · −'+fmtInt(dropL)):'')+'</span></div>'; }
|
| - |
|
1195 |
else { var drop=prev?prev.count-s.count:0;
|
| 1185 |
else { var drop=prev?prev.count-s.count:0; cap=drop>0?('<div class="drop">−'+fmtInt(drop)+'</div>'):('<div class="keep">'+s.pct+'%</div>'); }
|
1196 |
cap=drop>0?('<div class="drop"><span class="fcap"'+dropAttr+'>−'+fmtInt(drop)+'</span></div>')
|
| - |
|
1197 |
:('<div class="keep"><span class="fcap"'+dropAttr+'>'+s.pct+'%</span></div>'); }
|
| 1186 |
h+='<div class="fstep" data-stage="'+p[0]+'" data-label="'+esc(p[1])+'"><div class="fn">'+esc(p[1])+'</div><div class="fv">'+fmtInt(s.count)+'</div>'+
|
1198 |
h+='<div class="fstep" data-stage="'+p[0]+'" data-label="'+esc(p[1])+'"><div class="fn">'+esc(p[1])+'</div><div class="fv">'+fmtInt(s.count)+'</div>'+
|
| 1187 |
'<div class="fbar" style="width:'+Math.max(s.pct,3)+'%"></div>'+cap+(last?'':'<span class="arrow">➤</span>')+'</div>';
|
1199 |
'<div class="fbar" style="width:'+Math.max(s.pct,3)+'%"></div>'+cap+(last?'':'<span class="arrow">➤</span>')+'</div>';
|
| 1188 |
prev=s; });
|
1200 |
prev=s; prevKey=p[0]; });
|
| 1189 |
byId('ccFunnel').innerHTML=h||'<div class="ph">No leads in scope.</div>';
|
1201 |
byId('ccFunnel').innerHTML=h||'<div class="ph">No leads in scope.</div>';
|
| 1190 |
}
|
1202 |
}
|
| 1191 |
|
1203 |
|
| 1192 |
// ---- Lead pipeline: filtered, sorted and paged on the server ------------------------------
|
1204 |
// ---- Lead pipeline: filtered, sorted and paged on the server ------------------------------
|
| 1193 |
// PIPE.q is the search term. While it is set the date filter does not apply — a search hunts for
|
1205 |
// PIPE.q is the search term. While it is set the date filter does not apply — a search hunts for
|
| Line 1512... |
Line 1524... |
| 1512 |
var la=e.target.closest('tr[data-liveagent]');
|
1524 |
var la=e.target.closest('tr[data-liveagent]');
|
| 1513 |
if(la){ openAgentCalls(parseInt(la.getAttribute('data-liveagent'),10)); return; }
|
1525 |
if(la){ openAgentCalls(parseInt(la.getAttribute('data-liveagent'),10)); return; }
|
| 1514 |
var th=e.target.closest('th.sortable'); if(th){ pipeSort(th.getAttribute('data-sort')); return; }
|
1526 |
var th=e.target.closest('th.sortable'); if(th){ pipeSort(th.getAttribute('data-sort')); return; }
|
| 1515 |
var kpi=e.target.closest('.kpi[data-kpi]'); if(kpi){ var b=kpiBucket(kpi.getAttribute('data-kpi'));
|
1527 |
var kpi=e.target.closest('.kpi[data-kpi]'); if(kpi){ var b=kpiBucket(kpi.getAttribute('data-kpi'));
|
| 1516 |
drill('Command Center',kpi.querySelector('.label').textContent,'KPI drill',b[0],b[1]); return; }
|
1528 |
drill('Command Center',kpi.querySelector('.label').textContent,'KPI drill',b[0],b[1]); return; }
|
| - |
|
1529 |
var fc=e.target.closest('.fcap[data-drop]'); if(fc){ var dk=fc.getAttribute('data-drop');
|
| - |
|
1530 |
drill('Lifecycle funnel',fc.getAttribute('data-droplabel'),'At the previous step, never reached this one',
|
| - |
|
1531 |
dk.indexOf(',')>=0?'STAGE_IN':'STAGE',dk); return; }
|
| 1517 |
var fs=e.target.closest('.fstep[data-stage]'); if(fs){ var st=fs.getAttribute('data-stage');
|
1532 |
var fs=e.target.closest('.fstep[data-stage]'); if(fs){ var st=fs.getAttribute('data-stage');
|
| 1518 |
drill('Lifecycle funnel',fs.getAttribute('data-label'),'Reached this stage',st==='ALL'?'ALL':'STAGE_REACHED',st==='ALL'?'':st); return; }
|
1533 |
drill('Lifecycle funnel',fs.getAttribute('data-label'),'Reached this stage',st==='ALL'?'ALL':'STAGE_REACHED',st==='ALL'?'':st); return; }
|
| 1519 |
var att=e.target.closest('.att[data-att]'); if(att){ drill('Needs attention',att.getAttribute('data-attt'),'Attention queue','ATTENTION',att.getAttribute('data-att')); return; }
|
1534 |
var att=e.target.closest('.att[data-att]'); if(att){ drill('Needs attention',att.getAttribute('data-attt'),'Attention queue','ATTENTION',att.getAttribute('data-att')); return; }
|
| 1520 |
var reg=e.target.closest('tr[data-region]'); if(reg){ drill('State funnel',reg.getAttribute('data-rn'),'Region '+reg.getAttribute('data-region'),'REGION',reg.getAttribute('data-region')); return; }
|
1535 |
var reg=e.target.closest('tr[data-region]'); if(reg){ drill('State funnel',reg.getAttribute('data-rn'),'Region '+reg.getAttribute('data-region'),'REGION',reg.getAttribute('data-region')); return; }
|
| 1521 |
var rep=e.target.closest('tr[data-rep]'); if(rep){ openReport(rep.getAttribute('data-rep')); return; }
|
1536 |
var rep=e.target.closest('tr[data-rep]'); if(rep){ openReport(rep.getAttribute('data-rep')); return; }
|