| Line 87... |
Line 87... |
| 87 |
.bstat.ok{background:#ebfbee;color:#2f9e44}
|
87 |
.bstat.ok{background:#ebfbee;color:#2f9e44}
|
| 88 |
.bstat.warn{background:#fff4e6;color:#e8590c}
|
88 |
.bstat.warn{background:#fff4e6;color:#e8590c}
|
| 89 |
.bstat.no{background:#fff0f0;color:#e03131}
|
89 |
.bstat.no{background:#fff0f0;color:#e03131}
|
| 90 |
.bdot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px;vertical-align:middle}
|
90 |
.bdot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px;vertical-align:middle}
|
| 91 |
.ptime{display:inline-block;background:#ebfbee;color:#2f9e44;font-weight:700;font-size:11px;padding:2px 7px;border-radius:6px;font-variant-numeric:tabular-nums}
|
91 |
.ptime{display:inline-block;background:#ebfbee;color:#2f9e44;font-weight:700;font-size:11px;padding:2px 7px;border-radius:6px;font-variant-numeric:tabular-nums}
|
| - |
|
92 |
.potime{display:inline-block;background:#ebfbee;color: #D60303A8;font-weight:700;font-size:11px;padding:2px 7px;border-radius:6px;font-variant-numeric:tabular-nums}
|
| 92 |
td.cnt{font-weight:700;font-variant-numeric:tabular-nums}
|
93 |
td.cnt{font-weight:700;font-variant-numeric:tabular-nums}
|
| 93 |
.cgreen{color:#2f9e44}.camber{color:#f08c00}.cred{color:#e03131}.cmuted{color:#69748c}
|
94 |
.cgreen{color:#2f9e44}.camber{color:#f08c00}.cred{color:#e03131}.cmuted{color:#69748c}
|
| 94 |
.actbtn{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:7px;border:0;cursor:pointer;color:#fff;font-size:12px;margin-left:3px}
|
95 |
.actbtn{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:7px;border:0;cursor:pointer;color:#fff;font-size:12px;margin-left:3px}
|
| 95 |
.actbtn.map{background:#12b886}.actbtn.view{background:#3b5bdb}
|
96 |
.actbtn.map{background:#12b886}.actbtn.view{background:#3b5bdb}
|
| 96 |
.actbtn.edit{background:#f08c00}.actbtn.add{background:#2f9e44;font-size:15px}
|
97 |
.actbtn.edit{background:#f08c00}.actbtn.add{background:#2f9e44;font-size:15px}
|
| Line 113... |
Line 114... |
| 113 |
.donut{width:138px;height:138px;border-radius:50%;flex:none;position:relative;
|
114 |
.donut{width:138px;height:138px;border-radius:50%;flex:none;position:relative;
|
| 114 |
-webkit-mask:radial-gradient(circle 42px at center,transparent 98%,#000 100%);mask:radial-gradient(circle 42px at center,transparent 98%,#000 100%)}
|
115 |
-webkit-mask:radial-gradient(circle 42px at center,transparent 98%,#000 100%);mask:radial-gradient(circle 42px at center,transparent 98%,#000 100%)}
|
| 115 |
.donut .center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;-webkit-mask:none;mask:none}
|
116 |
.donut .center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;-webkit-mask:none;mask:none}
|
| 116 |
.legend{display:flex;flex-direction:column;gap:9px;font-size:12.5px}
|
117 |
.legend{display:flex;flex-direction:column;gap:9px;font-size:12.5px}
|
| 117 |
.legend .it{display:flex;align-items:flex-start;gap:9px}
|
118 |
.legend .it{display:flex;align-items:flex-start;gap:9px}
|
| - |
|
119 |
.legend .it.clickable{cursor:pointer;border-radius:8px;padding:4px 6px;margin:-4px -6px;transition:background .12s}
|
| - |
|
120 |
.legend .it.clickable:hover{background:#f1f3f9}
|
| - |
|
121 |
.barrow.clickable{cursor:pointer}
|
| - |
|
122 |
.barrow.clickable:hover .track{outline:2px solid #c7d0e8;outline-offset:1px;border-radius:7px}
|
| - |
|
123 |
.donut.clickable{cursor:pointer}
|
| 118 |
.dot{width:11px;height:11px;border-radius:3px;flex:none;margin-top:3px}
|
124 |
.dot{width:11px;height:11px;border-radius:3px;flex:none;margin-top:3px}
|
| 119 |
.tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:11px}
|
125 |
.tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:11px}
|
| 120 |
.tile{background:#f8f9fc;border:1px solid var(--line2);border-radius:11px;padding:12px}
|
126 |
.tile{background:#f8f9fc;border:1px solid var(--line2);border-radius:11px;padding:12px}
|
| 121 |
.tile .t{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:600}
|
127 |
.tile .t{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:600}
|
| 122 |
.tile .v{font-size:21px;font-weight:760;margin-top:5px}
|
128 |
.tile .v{font-size:21px;font-weight:760;margin-top:5px}
|
| Line 331... |
Line 337... |
| 331 |
return '<div class="kpi'+(click?' clickable':'')+'"'+(click?' onclick="'+click+'" title="Click for details"':'')+'><div class="top"><div class="ic" style="background:'+bg+';color:'+fg+'">'+ic+'</div>'+
|
337 |
return '<div class="kpi'+(click?' clickable':'')+'"'+(click?' onclick="'+click+'" title="Click for details"':'')+'><div class="top"><div class="ic" style="background:'+bg+';color:'+fg+'">'+ic+'</div>'+
|
| 332 |
'<div class="label">'+label+'</div></div><div class="val"'+(valColor?' style="color:'+valColor+'"':'')+'>'+val+'</div>'+
|
338 |
'<div class="label">'+label+'</div></div><div class="val"'+(valColor?' style="color:'+valColor+'"':'')+'>'+val+'</div>'+
|
| 333 |
'<div class="foot">'+foot+'</div></div>';
|
339 |
'<div class="foot">'+foot+'</div></div>';
|
| 334 |
}
|
340 |
}
|
| 335 |
function fstep(name, val, w, sub, drop, last, key){
|
341 |
function fstep(name, val, w, sub, drop, last, key){
|
| 336 |
return '<div class="fstep clickable" onclick="openFunnel(\''+key+'\')" title="Click to see these executives"><div class="fn">'+name+'</div><div class="fv">'+num(val)+'</div>'+
|
342 |
return '<div class="fstep'+(key?' clickable" onclick="openFunnel(\''+key+'\')" title="Click to see these executives':'')+'"><div class="fn">'+name+'</div><div class="fv">'+num(val)+'</div>'+
|
| 337 |
'<div class="fbar" style="width:'+w+'%"></div><div class="'+(drop?'drop':'keep')+'">'+sub+'</div>'+
|
343 |
'<div class="fbar" style="width:'+Math.min(w,100)+'%"></div><div class="'+(drop?'drop':'keep')+'">'+sub+'</div>'+
|
| 338 |
(last?'':'<span class="arrow">➤</span>')+'</div>';
|
344 |
(last?'':'<span class="arrow">➤</span>')+'</div>';
|
| 339 |
}
|
345 |
}
|
| - |
|
346 |
// Funnel drop sub-text: positive = fell out (red); negative = above planned
|
| - |
|
347 |
// (leads push visits past planned stops) shown green; zero = on plan.
|
| - |
|
348 |
function fdrop(p, badLabel){
|
| - |
|
349 |
if(p>0) return '−'+p+'% '+badLabel;
|
| - |
|
350 |
if(p<0) return '+'+(-p)+'% above planned';
|
| - |
|
351 |
return 'on plan';
|
| - |
|
352 |
}
|
| 340 |
function leakbox(t, v, color){ return '<div class="leakbox"><div class="lt">'+t+'</div><div class="lv"'+(color?' style="color:'+color+'"':'')+'>'+num(v)+'</div></div>'; }
|
353 |
function leakbox(t, v, color){ return '<div class="leakbox"><div class="lt">'+t+'</div><div class="lv"'+(color?' style="color:'+color+'"':'')+'>'+num(v)+'</div></div>'; }
|
| 341 |
|
354 |
|
| 342 |
function renderToday(d){
|
355 |
function renderToday(d){
|
| 343 |
var k=d.kpis, f=d.funnel, s=d.stops, a=d.attention;
|
356 |
var k=d.kpis, f=d.funnel, s=d.stops, a=d.attention;
|
| 344 |
window._canApprove = !!d.canApprove;
|
357 |
window._canApprove = !!d.canApprove;
|
| Line 439... |
Line 452... |
| 439 |
'<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td></tr>';
|
452 |
'<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td></tr>';
|
| 440 |
});
|
453 |
});
|
| 441 |
return h+'</tbody></table>';
|
454 |
return h+'</tbody></table>';
|
| 442 |
}
|
455 |
}
|
| 443 |
function openFunnel(key){
|
456 |
function openFunnel(key){
|
| - |
|
457 |
if(key && key.indexOf('p_')===0) return openPFunnel(key); // period funnel
|
| 444 |
var m=FUNNEL_META[key]; if(!m) return;
|
458 |
var m=FUNNEL_META[key]; if(!m) return;
|
| 445 |
var board=window._board||[], date=window._boardDate||'';
|
459 |
var board=window._board||[], date=window._boardDate||'';
|
| 446 |
var reached=board.filter(m.f);
|
460 |
var reached=board.filter(m.f);
|
| 447 |
var dropped=m.drop ? board.filter(m.drop) : [];
|
461 |
var dropped=m.drop ? board.filter(m.drop) : [];
|
| 448 |
var mb=el('bj-modal-back'), md=el('bj-modal');
|
462 |
var mb=el('bj-modal-back'), md=el('bj-modal');
|
| Line 476... |
Line 490... |
| 476 |
});
|
490 |
});
|
| 477 |
h+='</tbody></table><p class="desc" style="margin-top:10px">Click a row to open that executive\'s journey detail.</p>';
|
491 |
h+='</tbody></table><p class="desc" style="margin-top:10px">Click a row to open that executive\'s journey detail.</p>';
|
| 478 |
}
|
492 |
}
|
| 479 |
md.innerHTML=h; mb.classList.add('show');
|
493 |
md.innerHTML=h; mb.classList.add('show');
|
| 480 |
}
|
494 |
}
|
| - |
|
495 |
/* ===================== PERIOD (ANALYTICS) DRILL-DOWNS ===================== */
|
| - |
|
496 |
function pModal(html){ el('bj-modal').innerHTML=html; el('bj-modal-back').classList.add('show'); }
|
| - |
|
497 |
function pRange(){ return (el('bj-start')?el('bj-start').value:'')+' to '+(el('bj-end')?el('bj-end').value:''); }
|
| - |
|
498 |
|
| - |
|
499 |
// Scorecard list popup (used by KPI cards + funnel stages). Rows drill to the
|
| - |
|
500 |
// executive's day-by-day. sortKey sorts descending.
|
| - |
|
501 |
function openPScore(sortKey, title, desc){
|
| - |
|
502 |
var rows=(((window._period||{}).scorecard)||[]).slice();
|
| - |
|
503 |
if(sortKey) rows.sort(function(a,b){ return (b[sortKey]||0)-(a[sortKey]||0); });
|
| - |
|
504 |
pModal('<button class="x" onclick="closeModal()">×</button><h3>'+esc(title)+'</h3>'+
|
| - |
|
505 |
'<p class="desc">'+esc(desc)+' · '+esc(pRange())+' · click a row for the day-by-day</p>'+pScoreTable(rows));
|
| - |
|
506 |
}
|
| - |
|
507 |
function pScoreTable(rows){
|
| - |
|
508 |
if(!rows.length) return '<p class="desc">No executives in scope.</p>';
|
| - |
|
509 |
var h='<table><thead><tr><th>Executive</th><th class="num">Plan</th><th class="num">Done</th><th class="num">Adher.</th><th class="num">Disc.</th><th class="num">Hrs</th><th class="num">Leads</th><th class="num">Geo⚠</th></tr></thead><tbody>';
|
| - |
|
510 |
rows.forEach(function(r){
|
| - |
|
511 |
var c=adhColor(r.adherencePct);
|
| - |
|
512 |
h+='<tr class="clickable" onclick="openExecPeriod('+r.authUserId+',\''+js(r.name)+'\')">'+
|
| - |
|
513 |
'<td><b>'+esc(r.name)+'</b> <span class="lvl">'+esc(r.level)+'</span></td>'+
|
| - |
|
514 |
'<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
|
| - |
|
515 |
'<td class="num"><b style="color:'+c+'">'+r.adherencePct+'%</b></td>'+
|
| - |
|
516 |
'<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'-')+(r.discussionMin?'m':'')+'</td>'+
|
| - |
|
517 |
'<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td>'+
|
| - |
|
518 |
'<td class="num'+(r.geoFlags>0?' badflag':'')+'">'+r.geoFlags+'</td></tr>';
|
| - |
|
519 |
});
|
| - |
|
520 |
return h+'</tbody></table>';
|
| - |
|
521 |
}
|
| - |
|
522 |
// Funnel stage → the executives behind it (the scorecard, focused).
|
| - |
|
523 |
function openPFunnel(key){
|
| - |
|
524 |
var map={ p_planned:['planned','Planned stops','Planned partner + lead stops per executive'],
|
| - |
|
525 |
p_punchedin:['done','Planned on punch-in days','Executives, planned vs done'],
|
| - |
|
526 |
p_checkedin:['done','Checked in','Visits checked in per executive'],
|
| - |
|
527 |
p_checkedout:['done','Checked out (done)','Visits completed per executive'] };
|
| - |
|
528 |
var m=map[key]; if(m) openPScore(m[0],m[1],m[2]);
|
| - |
|
529 |
}
|
| - |
|
530 |
// Executive day-by-day (scorecard row drill). Fetches /exec-period; each day
|
| - |
|
531 |
// opens the journey detail / map modal.
|
| - |
|
532 |
function openExecPeriod(authUserId, name){
|
| - |
|
533 |
pModal('<button class="x" onclick="closeModal()">×</button><h3>'+esc(name)+'</h3>'+
|
| - |
|
534 |
'<p class="desc">Day-by-day · '+esc(pRange())+'</p><div class="loading"><div class="spinner"></div>Loading…</div>');
|
| - |
|
535 |
fetch(BASE+'/exec-period?authUserId='+authUserId+'&startDate='+encodeURIComponent(el('bj-start').value)+'&endDate='+encodeURIComponent(el('bj-end').value),{headers:{'Accept':'application/json'}})
|
| - |
|
536 |
.then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
|
| - |
|
537 |
.then(function(d){
|
| - |
|
538 |
var rows=d.rows||[], uid=d.userId, nm=d.name||name;
|
| - |
|
539 |
var h='<button class="x" onclick="closeModal()">×</button><h3>'+esc(nm)+'</h3>'+
|
| - |
|
540 |
'<p class="desc">'+rows.length+' day'+(rows.length===1?'':'s')+' · '+esc(pRange())+(uid?' · click a day for the journey detail':'')+'</p>';
|
| - |
|
541 |
if(!rows.length){ h+='<p class="desc">No scheduled or active days in this range.</p>'; }
|
| - |
|
542 |
else {
|
| - |
|
543 |
h+='<table><thead><tr><th>Date</th><th>Punch In/Out</th><th class="num">Planned</th><th class="num">Completed</th><th class="num">Deferred</th><th class="num">Total</th></tr></thead><tbody>';
|
| - |
|
544 |
rows.forEach(function(r){
|
| - |
|
545 |
var go = uid ? ' class="clickable" onclick="openExec('+uid+',\''+js(nm)+'\',\''+r.date+'\')"' : '';
|
| - |
|
546 |
h+='<tr'+go+'><td>'+esc(r.date)+'</td>'+
|
| - |
|
547 |
'<td style="white-space:nowrap">'+(r.punchIn?'<span class="ptime">'+esc(hhmm(r.punchIn))+'</span>':'<span class="badflag">-</span>')+
|
| - |
|
548 |
' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOut?esc(hhmm(r.punchOut)):'--')+'</span></td>'+
|
| - |
|
549 |
'<td class="num">'+r.planned+'</td><td class="num cnt cgreen">'+r.done+'</td>'+
|
| - |
|
550 |
'<td class="num cnt cred">'+r.deferred+'</td><td class="num cnt cmuted">'+r.total+'</td></tr>';
|
| - |
|
551 |
});
|
| - |
|
552 |
h+='</tbody></table>';
|
| - |
|
553 |
}
|
| - |
|
554 |
el('bj-modal').innerHTML=h;
|
| - |
|
555 |
})
|
| - |
|
556 |
.catch(function(){ el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">×</button><p class="desc">Could not load day-by-day.</p>'; });
|
| - |
|
557 |
}
|
| - |
|
558 |
// PJP coverage drill — execs with / without a plan (optionally one level).
|
| - |
|
559 |
function openCoverage(level){
|
| - |
|
560 |
var all=((window._period||{}).coverageDetail)||[];
|
| - |
|
561 |
var list=level ? all.filter(function(r){ return r.level===level; }) : all;
|
| - |
|
562 |
var without=list.filter(function(r){ return !r.hasPlan; }), withP=list.filter(function(r){ return r.hasPlan; });
|
| - |
|
563 |
function tbl(rs){
|
| - |
|
564 |
if(!rs.length) return '<p class="desc">None.</p>';
|
| - |
|
565 |
var h='<table><tbody>';
|
| - |
|
566 |
rs.forEach(function(r){ h+='<tr class="clickable" onclick="openExecPeriod('+r.authUserId+',\''+js(r.name)+'\')"><td style="font-weight:600">'+esc(r.name)+'</td><td><span class="lvl">'+esc(r.level)+'</span></td></tr>'; });
|
| - |
|
567 |
return h+'</tbody></table>';
|
| - |
|
568 |
}
|
| - |
|
569 |
pModal('<button class="x" onclick="closeModal()">×</button><h3>PJP coverage'+(level?' · '+esc(level):'')+'</h3>'+
|
| - |
|
570 |
'<p class="desc">'+withP.length+' of '+list.length+' have a monthly plan · '+esc(pRange())+'</p>'+
|
| - |
|
571 |
'<div class="sectionlabel" style="color:#e03131">No plan · '+without.length+'</div>'+tbl(without)+
|
| - |
|
572 |
'<div class="sectionlabel" style="color:#2f9e44;margin-top:16px">Has a plan · '+withP.length+'</div>'+tbl(withP));
|
| - |
|
573 |
}
|
| - |
|
574 |
// Deferrals drill — all, or just system / genuine (from the donut segments).
|
| - |
|
575 |
function openDeferrals(filter){
|
| - |
|
576 |
var all=((window._period||{}).deferralsList)||[];
|
| - |
|
577 |
var list = filter==='system' ? all.filter(function(r){ return r.system; })
|
| - |
|
578 |
: filter==='genuine' ? all.filter(function(r){ return !r.system; }) : all;
|
| - |
|
579 |
var title = filter==='system' ? 'Deferrals — system-flagged' : filter==='genuine' ? 'Deferrals — genuine reason' : 'Deferrals';
|
| - |
|
580 |
var h='<button class="x" onclick="closeModal()">×</button><h3>'+esc(title)+'</h3>'+
|
| - |
|
581 |
'<p class="desc">'+list.length+' deferral'+(list.length===1?'':'s')+' · '+esc(pRange())+'</p>';
|
| - |
|
582 |
if(!list.length){ h+='<p class="desc">None in this range.</p>'; }
|
| - |
|
583 |
else {
|
| - |
|
584 |
h+='<table><thead><tr><th>Executive</th><th>Partner</th><th>Reason</th><th>Date</th><th>Status</th></tr></thead><tbody>';
|
| - |
|
585 |
list.forEach(function(r){
|
| - |
|
586 |
h+='<tr><td style="font-weight:600;white-space:nowrap">'+esc(r.exec)+'</td>'+
|
| - |
|
587 |
'<td>'+esc(r.partner||'-')+'</td>'+
|
| - |
|
588 |
'<td style="color:'+(r.system?'#f08c00':'#2f9e44')+'">'+esc(r.reason||'-')+'</td>'+
|
| - |
|
589 |
'<td style="white-space:nowrap">'+esc(r.date||'')+'</td>'+
|
| - |
|
590 |
'<td>'+esc(r.status||'')+'</td></tr>';
|
| - |
|
591 |
});
|
| - |
|
592 |
h+='</tbody></table>';
|
| - |
|
593 |
}
|
| - |
|
594 |
pModal(h);
|
| - |
|
595 |
}
|
| - |
|
596 |
// Leads drill (period) — every lead created in the range.
|
| - |
|
597 |
function openPLeads(){
|
| - |
|
598 |
var list=((window._period||{}).leadsList)||[];
|
| - |
|
599 |
var h='<button class="x" onclick="closeModal()">×</button><h3>Leads on route</h3>'+
|
| - |
|
600 |
'<p class="desc">'+list.length+' lead'+(list.length===1?'':'s')+' created · '+esc(pRange())+'</p>';
|
| - |
|
601 |
if(!list.length){ h+='<p class="desc">No leads created in this range.</p>'; }
|
| - |
|
602 |
else {
|
| - |
|
603 |
h+='<table><thead><tr><th>Executive</th><th>Lead</th><th>City</th><th>Date</th><th>Status</th></tr></thead><tbody>';
|
| - |
|
604 |
list.forEach(function(r){
|
| - |
|
605 |
h+='<tr><td style="font-weight:600;white-space:nowrap">'+esc(r.exec)+'</td>'+
|
| - |
|
606 |
'<td>'+esc(r.lead||'-')+'</td><td>'+esc(r.city||'')+'</td>'+
|
| - |
|
607 |
'<td style="white-space:nowrap">'+esc(r.date||'')+'</td><td>'+esc(r.status||'')+'</td></tr>';
|
| - |
|
608 |
});
|
| - |
|
609 |
h+='</tbody></table>';
|
| - |
|
610 |
}
|
| - |
|
611 |
pModal(h);
|
| - |
|
612 |
}
|
| 481 |
function names(list,n){ if(!list||!list.length) return 'None'; var s=list.slice(0,n).join(', '); if(list.length>n) s+=' +'+(list.length-n); return s; }
|
613 |
function names(list,n){ if(!list||!list.length) return 'None'; var s=list.slice(0,n).join(', '); if(list.length>n) s+=' +'+(list.length-n); return s; }
|
| 482 |
var ATT_META = {
|
614 |
var ATT_META = {
|
| 483 |
notPunchedIn: {bg:'#fff0f0', fg:'#e03131', ic:'⚠', title:'Not punched in', sub:'executives with a beat, not started', cls:'badflag'},
|
615 |
notPunchedIn: {bg:'#fff0f0', fg:'#e03131', ic:'⚠', title:'Not punched in', sub:'executives with a beat, not started', cls:'badflag'},
|
| 484 |
idleStale: {bg:'#fff4e6', fg:'#e8590c', ic:'⏱', title:'Idle — off-store > 40 min', sub:'stationary >20 m from any store', cls:'warnflag'},
|
616 |
idleStale: {bg:'#fff4e6', fg:'#e8590c', ic:'⏱', title:'Idle — off-store > 40 min', sub:'stationary >20 m from any store', cls:'warnflag'},
|
| 485 |
geofence: {bg:'#fff0f0', fg:'#e03131', ic:'📍', title:'Check-in outside store geofence', sub:'>50 m from partner\'s saved location', cls:'badflag'},
|
617 |
geofence: {bg:'#fff0f0', fg:'#e03131', ic:'📍', title:'Check-in outside store geofence', sub:'>50 m from partner\'s saved location', cls:'badflag'},
|
| Line 558... |
Line 690... |
| 558 |
.then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
|
690 |
.then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
|
| 559 |
.then(renderPeriod).catch(fail);
|
691 |
.then(renderPeriod).catch(fail);
|
| 560 |
}
|
692 |
}
|
| 561 |
function tile(t,v){ return '<div class="tile"><div class="t">'+t+'</div><div class="v">'+v+'</div></div>'; }
|
693 |
function tile(t,v){ return '<div class="tile"><div class="t">'+t+'</div><div class="v">'+v+'</div></div>'; }
|
| 562 |
function renderPeriod(d){
|
694 |
function renderPeriod(d){
|
| - |
|
695 |
window._period = d; // drill-downs read scorecard / lists from here
|
| 563 |
var k=d.kpis, f=d.funnel, df=d.deferral, u=d.utilization, t=d.travel, o=d.outcomes;
|
696 |
var k=d.kpis, f=d.funnel, df=d.deferral, u=d.utilization, t=d.travel, o=d.outcomes;
|
| 564 |
var neverTouched = Math.max((f.plannedStops||0)-(f.checkedIn||0)-(df.total||0),0);
|
697 |
var neverTouched = Math.max((f.plannedStops||0)-(f.checkedIn||0)-(df.total||0),0);
|
| 565 |
var h='';
|
698 |
var h='';
|
| 566 |
h+='<div class="kpis">';
|
699 |
h+='<div class="kpis">';
|
| 567 |
h+=kpi('⚖','#fff4e6','#f08c00','Adherence',k.adherencePct+'<small>%</small>',num(k.doneStops)+' of '+num(k.plannedStops)+' stops done',adhColor(k.adherencePct));
|
700 |
h+=kpi('⚖','#fff4e6','#f08c00','Adherence',k.adherencePct+'<small>%</small>',num(k.doneStops)+' of '+num(k.plannedStops)+' stops done',adhColor(k.adherencePct),"openPScore('adherencePct','Adherence','Planned vs done per executive')");
|
| 568 |
h+=kpi('🗺','#e7f5ff','#1098ad','PJP coverage L1',k.coverageL1Pct+'<small>%</small>',k.coverageL1With+'/'+k.coverageL1Total+' have a plan','#1098ad');
|
701 |
h+=kpi('🗺','#e7f5ff','#1098ad','PJP coverage L1',k.coverageL1Pct+'<small>%</small>',k.coverageL1With+'/'+k.coverageL1Total+' have a plan','#1098ad',"openCoverage('L1')");
|
| 569 |
h+=kpi('🚶','#ebfbee','#2f9e44','Visits / active day',k.avgVisitsPerActiveDay,'across '+num(k.journeys)+' journeys');
|
702 |
h+=kpi('🚶','#ebfbee','#2f9e44','Visits / active day',k.avgVisitsPerActiveDay,'across '+num(k.journeys)+' journeys',null,"openPScore('done','Visits / active day','Visits completed per executive')");
|
| 570 |
h+=kpi('💬','#eef1fb','#3b5bdb','Discussion / store',k.avgDiscussionMin+' <small>min</small>','target 30–45 min', k.avgDiscussionMin>0&&k.avgDiscussionMin<30?'#e8590c':null);
|
703 |
h+=kpi('💬','#eef1fb','#3b5bdb','Discussion / store',k.avgDiscussionMin+' <small>min</small>','target 30–45 min', k.avgDiscussionMin>0&&k.avgDiscussionMin<30?'#e8590c':null,"openPScore('discussionMin','Discussion / store','Avg in-store discussion per executive')");
|
| 571 |
h+=kpi('↻','#fff0f0','#e03131','Auto "missed"',num(k.autoMissed),k.autoMissedSystemPct+'% system-flagged','#e03131');
|
704 |
h+=kpi('↻','#fff0f0','#e03131','Auto "missed"',num(k.autoMissed),k.autoMissedSystemPct+'% system-flagged','#e03131',"openDeferrals()");
|
| 572 |
h+=kpi('★','#fff0f6','#d6336c','Leads on route',num(k.leadsCreated),'this period');
|
705 |
h+=kpi('★','#fff0f6','#d6336c','Leads on route',num(k.leadsCreated),'this period',null,"openPLeads()");
|
| 573 |
h+='</div>';
|
706 |
h+='</div>';
|
| 574 |
|
707 |
|
| 575 |
h+='<div class="sectionlabel">Aggregate Journey Funnel</div><div class="card"><div class="funnel">';
|
708 |
h+='<div class="sectionlabel">Aggregate Journey Funnel</div><div class="card"><div class="funnel">';
|
| 576 |
var pl=f.plannedStops||1;
|
709 |
var pl=f.plannedStops||1;
|
| 577 |
h+=fstep('Planned stops',f.plannedStops,100,'scheduled',false,false);
|
710 |
h+=fstep('Planned stops',f.plannedStops,100,'scheduled',false,false,'p_planned');
|
| 578 |
h+=fstep('Days punched in',f.punchedIn,pct(f.punchedIn,pl),'−'+f.dropPunch+'% no punch-in',true,false);
|
711 |
h+=fstep('Planned on punch-in days',f.punchedIn,pct(f.punchedIn,pl),fdrop(f.dropPunch,'no punch-in'),f.dropPunch>0,false,'p_punchedin');
|
| 579 |
h+=fstep('Checked in',f.checkedIn,pct(f.checkedIn,pl),'−'+f.dropCheckin+'% reached, skipped',true,false);
|
712 |
h+=fstep('Checked in',f.checkedIn,pct(f.checkedIn,pl),fdrop(f.dropCheckin,'reached, skipped'),f.dropCheckin>0,false,'p_checkedin');
|
| 580 |
h+=fstep('Checked out (done)',f.checkedOut,pct(f.checkedOut,pl),'−'+f.dropCheckout+'% no checkout',false,true);
|
713 |
h+=fstep('Checked out (done)',f.checkedOut,pct(f.checkedOut,pl),fdrop(f.dropCheckout,'no checkout'),f.dropCheckout>0,true,'p_checkedout');
|
| 581 |
h+='</div><div class="leak">';
|
- |
|
| 582 |
h+=leakbox('Deferred — genuine',df.human,'#2f9e44');
|
- |
|
| 583 |
h+=leakbox('Deferred — system flag',df.systemGenerated,'#f08c00');
|
- |
|
| 584 |
h+=leakbox('Heads acted on',df.actioned,null);
|
- |
|
| 585 |
h+=leakbox('Never touched',neverTouched,'#e03131');
|
- |
|
| 586 |
h+='</div></div>';
|
714 |
h+='</div></div>';
|
| 587 |
|
715 |
|
| 588 |
h+='<div class="sectionlabel">Adherence & Coverage</div><div class="grid g3">';
|
716 |
h+='<div class="sectionlabel">Adherence & Coverage</div><div class="grid g3">';
|
| 589 |
h+='<div class="card"><h2>Per-executive scorecard <span style="color:#97a1b5;font-weight:500;font-size:12px">· '+d.scorecard.length+' executives</span></h2><p class="desc">The coaching list — click a header to sort. Reds are where to intervene.</p>'+scorecardTable(d.scorecard)+'</div>';
|
717 |
h+='<div class="card"><h2>Per-executive scorecard <span style="color:#97a1b5;font-weight:500;font-size:12px">· '+d.scorecard.length+' executives</span></h2><p class="desc">The coaching list — click a header to sort. Reds are where to intervene.</p>'+scorecardTable(d.scorecard)+'</div>';
|
| 590 |
h+='<div class="card"><h2>Coverage by sales level</h2><p class="desc">Filled = executives with a plan; light = total at that level.</p>'+coverageBars(d.coverageByLevel)+utilBlock(u)+'</div>';
|
718 |
h+='<div class="card"><h2>Coverage by sales level</h2><p class="desc">Filled = executives with a plan; light = total at that level.</p>'+coverageBars(d.coverageByLevel)+utilBlock(u)+'</div>';
|
| Line 627... |
Line 755... |
| 627 |
'<th class="num" onclick="sortCard(\'workingHrs\')">Hrs</th>'+
|
755 |
'<th class="num" onclick="sortCard(\'workingHrs\')">Hrs</th>'+
|
| 628 |
'<th class="num" onclick="sortCard(\'leads\')">Leads</th>'+
|
756 |
'<th class="num" onclick="sortCard(\'leads\')">Leads</th>'+
|
| 629 |
'<th class="num" onclick="sortCard(\'geoFlags\')">Geo⚠</th></tr></thead><tbody>';
|
757 |
'<th class="num" onclick="sortCard(\'geoFlags\')">Geo⚠</th></tr></thead><tbody>';
|
| 630 |
rows.forEach(function(r){
|
758 |
rows.forEach(function(r){
|
| 631 |
var c=adhColor(r.adherencePct);
|
759 |
var c=adhColor(r.adherencePct);
|
| 632 |
h+='<tr><td>'+esc(r.name)+' <span class="lvl">'+esc(r.level)+'</span></td>'+
|
760 |
h+='<tr class="clickable" onclick="openExecPeriod('+r.authUserId+',\''+js(r.name)+'\')"><td>'+esc(r.name)+' <span class="lvl">'+esc(r.level)+'</span></td>'+
|
| 633 |
'<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
|
761 |
'<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
|
| 634 |
'<td class="num"><b style="color:'+c+'">'+r.adherencePct+'%</b><span class="meter"><i style="width:'+Math.min(100,r.adherencePct)+'%;background:'+c+'"></i></span></td>'+
|
762 |
'<td class="num"><b style="color:'+c+'">'+r.adherencePct+'%</b><span class="meter"><i style="width:'+Math.min(100,r.adherencePct)+'%;background:'+c+'"></i></span></td>'+
|
| 635 |
'<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'-')+(r.discussionMin?'m':'')+'</td>'+
|
763 |
'<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'-')+(r.discussionMin?'m':'')+'</td>'+
|
| 636 |
'<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td>'+
|
764 |
'<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td>'+
|
| 637 |
'<td class="num'+(r.geoFlags>0?' badflag':'')+'">'+r.geoFlags+'</td></tr>';
|
765 |
'<td class="num'+(r.geoFlags>0?' badflag':'')+'">'+r.geoFlags+'</td></tr>';
|
| Line 641... |
Line 769... |
| 641 |
function coverageBars(rows){
|
769 |
function coverageBars(rows){
|
| 642 |
if(!rows||!rows.length) return '<p class="desc">No coverage data.</p>';
|
770 |
if(!rows||!rows.length) return '<p class="desc">No coverage data.</p>';
|
| 643 |
var max=0; rows.forEach(function(r){ if(r.totalUsers>max)max=r.totalUsers; }); if(!max)max=1;
|
771 |
var max=0; rows.forEach(function(r){ if(r.totalUsers>max)max=r.totalUsers; }); if(!max)max=1;
|
| 644 |
var h=''; rows.forEach(function(r){
|
772 |
var h=''; rows.forEach(function(r){
|
| 645 |
var w=pct(r.withPjp,max), wt=pct(r.totalUsers,max);
|
773 |
var w=pct(r.withPjp,max), wt=pct(r.totalUsers,max);
|
| 646 |
h+='<div class="barrow"><div class="name">'+esc(r.level)+'</div><div class="track">'+
|
774 |
h+='<div class="barrow clickable" onclick="openCoverage(\''+js(r.level)+'\')" title="Click to see these executives"><div class="name">'+esc(r.level)+'</div><div class="track">'+
|
| 647 |
'<div style="position:absolute;inset:0;width:'+wt+'%;background:#e3e8f5;border-radius:7px"></div>'+
|
775 |
'<div style="position:absolute;inset:0;width:'+wt+'%;background:#e3e8f5;border-radius:7px"></div>'+
|
| 648 |
'<div class="fill" style="width:'+w+'%;position:relative;background:linear-gradient(90deg,#1098ad,#22b8cf)">'+(r.withPjp||'')+'</div></div>'+
|
776 |
'<div class="fill" style="width:'+w+'%;position:relative;background:linear-gradient(90deg,#1098ad,#22b8cf)">'+(r.withPjp||'')+'</div></div>'+
|
| 649 |
'<div class="meta">'+r.withPjp+'/'+r.totalUsers+' · '+r.coveragePct+'%</div></div>';
|
777 |
'<div class="meta">'+r.withPjp+'/'+r.totalUsers+' · '+r.coveragePct+'%</div></div>';
|
| 650 |
});
|
778 |
});
|
| 651 |
return h;
|
779 |
return h;
|
| Line 661... |
Line 789... |
| 661 |
'<span><span class="dot" style="background:#e03131;margin:0"></span>Idle '+u.idleHrs+' h</span></div>';
|
789 |
'<span><span class="dot" style="background:#e03131;margin:0"></span>Idle '+u.idleHrs+' h</span></div>';
|
| 662 |
}
|
790 |
}
|
| 663 |
function deferDonut(df){
|
791 |
function deferDonut(df){
|
| 664 |
var sysPct=df.systemPct||0;
|
792 |
var sysPct=df.systemPct||0;
|
| 665 |
var g='conic-gradient(#f59f00 0 '+sysPct+'%, #2f9e44 '+sysPct+'% 100%)';
|
793 |
var g='conic-gradient(#f59f00 0 '+sysPct+'%, #2f9e44 '+sysPct+'% 100%)';
|
| 666 |
return '<div class="donutwrap"><div class="donut" style="background:'+g+'"><div class="center">'+
|
794 |
return '<div class="donutwrap"><div class="donut clickable" onclick="openDeferrals()" title="Click to see all deferrals" style="background:'+g+'"><div class="center">'+
|
| 667 |
'<div style="font-size:24px;font-weight:780">'+num(df.total)+'</div><div style="font-size:11px;color:#97a1b5">deferrals</div></div></div>'+
|
795 |
'<div style="font-size:24px;font-weight:780">'+num(df.total)+'</div><div style="font-size:11px;color:#97a1b5">deferrals</div></div></div>'+
|
| 668 |
'<div class="legend">'+
|
796 |
'<div class="legend">'+
|
| 669 |
'<div class="it"><span class="dot" style="background:#f59f00"></span><div><b>'+df.systemGenerated+'</b> system auto-flagged ('+sysPct+'%)<br><span style="color:#97a1b5;font-size:11.5px">"beat missed", "no punch-in"</span></div></div>'+
|
797 |
'<div class="it clickable" onclick="openDeferrals(\'system\')"><span class="dot" style="background:#f59f00"></span><div><b>'+df.systemGenerated+'</b> system auto-flagged ('+sysPct+'%)<br><span style="color:#97a1b5;font-size:11.5px">"beat missed", "no punch-in"</span></div></div>'+
|
| 670 |
'<div class="it"><span class="dot" style="background:#2f9e44"></span><div><b>'+df.human+'</b> genuine reason ('+df.humanPct+'%)</div></div>'+
|
798 |
'<div class="it clickable" onclick="openDeferrals(\'genuine\')"><span class="dot" style="background:#2f9e44"></span><div><b>'+df.human+'</b> genuine reason ('+df.humanPct+'%)</div></div>'+
|
| 671 |
'<div class="it"><span class="dot" style="background:#3b5bdb"></span><div><b>'+df.actioned+'</b> acted on by a head'+(df.avgDaysToAction?'<br><span style="color:#97a1b5;font-size:11.5px">avg '+df.avgDaysToAction+' days to action</span>':'')+'</div></div>'+
|
799 |
'<div class="it"><span class="dot" style="background:#3b5bdb"></span><div><b>'+df.actioned+'</b> acted on by a head'+(df.avgDaysToAction?'<br><span style="color:#97a1b5;font-size:11.5px">avg '+df.avgDaysToAction+' days to action</span>':'')+'</div></div>'+
|
| 672 |
'</div></div><p class="desc" style="margin-top:13px">"System" vs "genuine" is read from the deferral reason; there is no separate flag.</p>';
|
800 |
'</div></div><p class="desc" style="margin-top:13px">Click the chart for all deferrals, or a legend row for system / genuine. "System" vs "genuine" is read from the deferral reason.</p>';
|
| 673 |
}
|
801 |
}
|
| 674 |
function travelBlock(t){
|
802 |
function travelBlock(t){
|
| 675 |
var rows=t.legBuckets||[], max=0; rows.forEach(function(r){ if(r.count>max)max=r.count; }); if(!max)max=1;
|
803 |
var rows=t.legBuckets||[], max=0; rows.forEach(function(r){ if(r.count>max)max=r.count; }); if(!max)max=1;
|
| 676 |
var colors={'0-2 km':'#2f9e44','2-5 km':'#2f9e44','5-15 km':'#82c91e','15-50 km':'#f59f00','50 km+':'#e03131'};
|
804 |
var colors={'0-2 km':'#2f9e44','2-5 km':'#2f9e44','5-15 km':'#82c91e','15-50 km':'#f59f00','50 km+':'#e03131'};
|
| 677 |
var h=''; rows.forEach(function(r){
|
805 |
var h=''; rows.forEach(function(r){
|
| Line 1116... |
Line 1244... |
| 1116 |
// Download the Employee Task Report (.xlsx). Today lens exports the single
|
1244 |
// Download the Employee Task Report (.xlsx). Today lens exports the single
|
| 1117 |
// selected day; Period lens exports the whole start..end range. Both carry the
|
1245 |
// selected day; Period lens exports the whole start..end range. Both carry the
|
| 1118 |
// current level / team scope (qs()). The download is served as an attachment, so
|
1246 |
// current level / team scope (qs()). The download is served as an attachment, so
|
| 1119 |
// navigating to the URL triggers the file save without leaving the page.
|
1247 |
// navigating to the URL triggers the file save without leaving the page.
|
| 1120 |
function exportReport(){
|
1248 |
function exportReport(){
|
| 1121 |
var start, end;
|
1249 |
var start, end, path;
|
| - |
|
1250 |
if(LENS==='period'){
|
| - |
|
1251 |
// Period lens: per-employee totals + a Raw Data sheet (separate endpoint).
|
| 1122 |
if(LENS==='period'){ start=el('bj-start').value; end=el('bj-end').value; }
|
1252 |
start=el('bj-start').value; end=el('bj-end').value; path='/export-period';
|
| - |
|
1253 |
} else {
|
| - |
|
1254 |
// Today lens: the day-grouped Employee Task Report (unchanged).
|
| 1123 |
else { start=el('bj-date').value; end=start; }
|
1255 |
start=el('bj-date').value; end=start; path='/export';
|
| - |
|
1256 |
}
|
| 1124 |
var u = BASE + '/export?startDate=' + encodeURIComponent(start) + '&endDate=' + encodeURIComponent(end) + qs();
|
1257 |
var u = BASE + path + '?startDate=' + encodeURIComponent(start) + '&endDate=' + encodeURIComponent(end) + qs();
|
| 1125 |
window.location.href = u;
|
1258 |
window.location.href = u;
|
| 1126 |
}
|
1259 |
}
|
| 1127 |
|
1260 |
|
| 1128 |
/* ============================ INIT ============================ */
|
1261 |
/* ============================ INIT ============================ */
|
| 1129 |
document.querySelectorAll('.lens').forEach(function(b){ b.addEventListener('click', function(){ setLens(b.getAttribute('data-lens')); }); });
|
1262 |
document.querySelectorAll('.lens').forEach(function(b){ b.addEventListener('click', function(){ setLens(b.getAttribute('data-lens')); }); });
|