| Line 439... |
Line 439... |
| 439 |
h+='<div class="sectionlabel">Today\'s Journey Funnel</div><div class="card"><div class="funnel">';
|
439 |
h+='<div class="sectionlabel">Today\'s Journey Funnel</div><div class="card"><div class="funnel">';
|
| 440 |
var sp=f.scheduledPjp||1, pin=f.punchedIn||1;
|
440 |
var sp=f.scheduledPjp||1, pin=f.punchedIn||1;
|
| 441 |
h+=fstep('Punched In',f.punchedIn,pct(f.punchedIn,sp),f.punchedIn+' of '+f.scheduledPjp+' scheduled',false,false,'punchedIn');
|
441 |
h+=fstep('Punched In',f.punchedIn,pct(f.punchedIn,sp),f.punchedIn+' of '+f.scheduledPjp+' scheduled',false,false,'punchedIn');
|
| 442 |
h+=fstep('Traveling',f.travelling,pct(f.travelling,pin),'on the road',false,false,'travelling');
|
442 |
h+=fstep('Traveling',f.travelling,pct(f.travelling,pin),'on the road',false,false,'travelling');
|
| 443 |
h+=fstep('In Store',f.inStore,pct(f.inStore,pin),'at a stop',false,false,'inStore');
|
443 |
h+=fstep('In Store',f.inStore,pct(f.inStore,pin),'at a stop',false,false,'inStore');
|
| 444 |
h+=fstep('Idle / Break',f.idle,pct(f.idle,pin),'off-store > 40 min',true,false,'idle');
|
444 |
h+=fstep('Idle / Break',f.idle,pct(f.idle,pin),'< 1 km in 15 min',true,false,'idle');
|
| 445 |
h+=fstep('Punched Out',f.punchedOut,pct(f.punchedOut,pin),'journey ended',false,true,'punchedOut');
|
445 |
h+=fstep('Punched Out',f.punchedOut,pct(f.punchedOut,pin),'journey ended',false,true,'punchedOut');
|
| 446 |
h+='</div></div>';
|
446 |
h+='</div></div>';
|
| 447 |
|
447 |
|
| 448 |
window._boardFilter = null; // funnel-card filter (null = all rows)
|
448 |
window._boardFilter = null; // funnel-card filter (null = all rows)
|
| 449 |
h+='<div class="card"><h2>Live executive board</h2><p class="desc">Each executive\'s journey right now. Click a row for the day\'s visit detail.</p><div id="bj-board">'+boardCard()+'</div></div>';
|
449 |
h+='<div class="card"><h2>Live executive board</h2><p class="desc">Each executive\'s journey right now. Click a row for the day\'s visit detail.</p><div id="bj-board">'+boardCard()+'</div></div>';
|
| Line 494... |
Line 494... |
| 494 |
function boardCounts(r){
|
494 |
function boardCounts(r){
|
| 495 |
var completed=r.completedVisits||0, deferred=r.deferred||0;
|
495 |
var completed=r.completedVisits||0, deferred=r.deferred||0;
|
| 496 |
var total=Math.max(r.planned||0, completed+deferred);
|
496 |
var total=Math.max(r.planned||0, completed+deferred);
|
| 497 |
return {total:total, completed:completed, deferred:deferred, pending:total-completed-deferred};
|
497 |
return {total:total, completed:completed, deferred:deferred, pending:total-completed-deferred};
|
| 498 |
}
|
498 |
}
|
| - |
|
499 |
// seconds -> compact "1h 20m" / "45m" for the board's time columns.
|
| - |
|
500 |
function bhm(sec){ sec=Math.max(0,Math.round(sec||0)); var h=Math.floor(sec/3600), m=Math.round((sec%3600)/60); return h?(h+'h '+m+'m'):(m+'m'); }
|
| 499 |
function boardTable(rows, date){
|
501 |
function boardTable(rows, date){
|
| 500 |
if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
|
502 |
if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
|
| 501 |
var h='<div style="overflow-x:auto"><table><thead><tr>'+
|
503 |
var h='<div style="overflow-x:auto"><table><thead><tr>'+
|
| 502 |
'<th>Agenda</th><th>Executive</th><th>Punch In/Out</th>'+
|
504 |
'<th>Agenda</th><th>Executive</th><th>Punch In/Out</th>'+
|
| 503 |
'<th class="num" title="Planned partner visits + self-assigned leads">Total</th>'+
|
505 |
'<th class="num" title="Planned partner visits + self-assigned leads">Total</th>'+
|
| 504 |
'<th class="num">Completed</th><th class="num">Pending</th><th class="num">Deferred</th>'+
|
506 |
'<th class="num">Completed</th><th class="num">Pending</th><th class="num">Deferred</th>'+
|
| - |
|
507 |
'<th class="num" title="Transit time between stops so far today">Transit</th>'+
|
| - |
|
508 |
'<th class="num" title="Time spent inside stores so far today">In-store</th>'+
|
| 505 |
'<th>State</th><th>Action</th>'+
|
509 |
'<th>State</th><th>Action</th>'+
|
| 506 |
'</tr></thead><tbody>';
|
510 |
'</tr></thead><tbody>';
|
| 507 |
// column totals, computed up front so the Total row can lead the table
|
511 |
// column totals, computed up front so the Total row can lead the table
|
| 508 |
var t={total:0,completed:0,pending:0,deferred:0};
|
512 |
var t={total:0,completed:0,pending:0,deferred:0,transit:0,inStore:0};
|
| 509 |
rows.forEach(function(r){
|
513 |
rows.forEach(function(r){
|
| 510 |
var c=boardCounts(r);
|
514 |
var c=boardCounts(r);
|
| 511 |
t.total+=c.total; t.completed+=c.completed; t.pending+=c.pending; t.deferred+=c.deferred;
|
515 |
t.total+=c.total; t.completed+=c.completed; t.pending+=c.pending; t.deferred+=c.deferred;
|
| - |
|
516 |
t.transit+=(r.transitSecs||0); t.inStore+=(r.inStoreSecs||0);
|
| 512 |
});
|
517 |
});
|
| 513 |
h+='<tr class="boardtot">'+
|
518 |
h+='<tr class="boardtot">'+
|
| 514 |
'<td></td><td><b>Total</b><br><span style="color:#97a1b5;font-size:11px">'+rows.length+' executives</span></td><td></td>'+
|
519 |
'<td></td><td><b>Total</b><br><span style="color:#97a1b5;font-size:11px">'+rows.length+' executives</span></td><td></td>'+
|
| 515 |
'<td class="num cnt cmuted">'+t.total+'</td>'+
|
520 |
'<td class="num cnt cmuted">'+t.total+'</td>'+
|
| 516 |
'<td class="num cnt cgreen">'+t.completed+'</td>'+
|
521 |
'<td class="num cnt cgreen">'+t.completed+'</td>'+
|
| 517 |
'<td class="num cnt">'+t.pending+'</td>'+
|
522 |
'<td class="num cnt">'+t.pending+'</td>'+
|
| - |
|
523 |
'<td class="num cnt camber">'+t.deferred+'</td>'+
|
| - |
|
524 |
'<td class="num cnt cmuted">'+bhm(t.transit)+'</td>'+
|
| 518 |
'<td class="num cnt camber">'+t.deferred+'</td><td></td><td></td>'+
|
525 |
'<td class="num cnt cmuted">'+bhm(t.inStore)+'</td><td></td><td></td>'+
|
| 519 |
'</tr>';
|
526 |
'</tr>';
|
| 520 |
rows.forEach(function(r){
|
527 |
rows.forEach(function(r){
|
| 521 |
var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (r.idleAway?'rowwarn':'');
|
528 |
var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (r.idleAway?'rowwarn':'');
|
| 522 |
var A = r.userId+",'"+js(r.name)+"','"+date+"'"; // openExec args
|
529 |
var A = r.userId+",'"+js(r.name)+"','"+date+"'"; // openExec args
|
| 523 |
var go = 'onclick="openExec('+A+')"';
|
530 |
var go = 'onclick="openExec('+A+')"';
|
| Line 529... |
Line 536... |
| 529 |
' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
|
536 |
' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
|
| 530 |
'<td class="num cnt cmuted">'+c.total+'</td>'+
|
537 |
'<td class="num cnt cmuted">'+c.total+'</td>'+
|
| 531 |
'<td class="num cnt cgreen">'+c.completed+'</td>'+
|
538 |
'<td class="num cnt cgreen">'+c.completed+'</td>'+
|
| 532 |
'<td class="num cnt">'+c.pending+'</td>'+
|
539 |
'<td class="num cnt">'+c.pending+'</td>'+
|
| 533 |
'<td class="num cnt camber">'+c.deferred+'</td>'+
|
540 |
'<td class="num cnt camber">'+c.deferred+'</td>'+
|
| - |
|
541 |
'<td class="num cnt cmuted">'+(r.transitSecs?bhm(r.transitSecs):'<span style="color:#c5cce0">—</span>')+'</td>'+
|
| - |
|
542 |
'<td class="num cnt cmuted">'+(r.inStoreSecs?bhm(r.inStoreSecs):'<span style="color:#c5cce0">—</span>')+'</td>'+
|
| 534 |
'<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
|
543 |
'<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
|
| 535 |
'<td style="white-space:nowrap" onclick="event.stopPropagation()">'+
|
544 |
'<td style="white-space:nowrap" onclick="event.stopPropagation()">'+
|
| 536 |
'<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">📍</button>'+
|
545 |
'<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">📍</button>'+
|
| 537 |
'<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">👁</button>'+
|
546 |
'<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">👁</button>'+
|
| 538 |
(CAN_EDIT_BEAT
|
547 |
(CAN_EDIT_BEAT
|
| Line 546... |
Line 555... |
| 546 |
// Funnel drill-down: each stage shows the executives who reached it AND the
|
555 |
// Funnel drill-down: each stage shows the executives who reached it AND the
|
| 547 |
// drop-off cohort that fell out at this step (the "−N …" sub-line). Filters
|
556 |
// drop-off cohort that fell out at this step (the "−N …" sub-line). Filters
|
| 548 |
// mirror the backend funnel conditions exactly (rows carry the per-stage flags).
|
557 |
// mirror the backend funnel conditions exactly (rows carry the per-stage flags).
|
| 549 |
var FUNNEL_META = {
|
558 |
var FUNNEL_META = {
|
| 550 |
punchedIn: {title:'Punched In', desc:'Started the day (punch-in recorded)', f:function(r){ return r.punchedIn; }},
|
559 |
punchedIn: {title:'Punched In', desc:'Started the day (punch-in recorded)', f:function(r){ return r.punchedIn; }},
|
| 551 |
travelling: {title:'Traveling', desc:'On the road between stops', f:function(r){ return r.state==='TRAVELLING' && !r.idleAway; }},
|
560 |
travelling: {title:'Traveling', desc:'On the road between stops', f:function(r){ return r.state==='TRAVELLING'; }},
|
| 552 |
inStore: {title:'In Store', desc:'Currently checked in at a stop', f:function(r){ return r.state==='IN_STORE'; }},
|
561 |
inStore: {title:'In Store', desc:'Currently checked in at a stop', f:function(r){ return r.state==='IN_STORE'; }},
|
| 553 |
idle: {title:'Idle / Break', desc:'Between stops, idle off-store beyond the threshold', f:function(r){ return r.state==='TRAVELLING' && r.idleAway; }},
|
562 |
idle: {title:'Idle / Break', desc:'Punched in but < 1 km covered in the last 15 min', f:function(r){ return r.state==='IDLE'; }},
|
| 554 |
punchedOut: {title:'Punched Out', desc:'Ended the day (punch-out recorded)', f:function(r){ return r.punchedOut; }}
|
563 |
punchedOut: {title:'Punched Out', desc:'Ended the day (punch-out recorded)', f:function(r){ return r.punchedOut; }}
|
| 555 |
};
|
564 |
};
|
| 556 |
function funnelRows(list, date){
|
565 |
function funnelRows(list, date){
|
| 557 |
if(!list.length) return '<p class="desc">None.</p>';
|
566 |
if(!list.length) return '<p class="desc">None.</p>';
|
| 558 |
var h='<table><thead><tr><th>Executive / Beat</th><th>Punch In/Out</th><th class="num">Completed</th><th class="num">Planned</th><th>State</th></tr></thead><tbody>';
|
567 |
var h='<table><thead><tr><th>Executive / Beat</th><th>Punch In/Out</th><th class="num">Completed</th><th class="num">Planned</th><th>State</th></tr></thead><tbody>';
|
| Line 662... |
Line 671... |
| 662 |
pModal('<button class="x" onclick="closeModal()">×</button><h3>'+esc(title)+'</h3>'+
|
671 |
pModal('<button class="x" onclick="closeModal()">×</button><h3>'+esc(title)+'</h3>'+
|
| 663 |
'<p class="desc">'+esc(desc)+' · '+esc(pRange())+' · click a row for the day-by-day</p>'+pScoreTable(rows));
|
672 |
'<p class="desc">'+esc(desc)+' · '+esc(pRange())+' · click a row for the day-by-day</p>'+pScoreTable(rows));
|
| 664 |
}
|
673 |
}
|
| 665 |
function pScoreTable(rows){
|
674 |
function pScoreTable(rows){
|
| 666 |
if(!rows.length) return '<p class="desc">No executives in scope.</p>';
|
675 |
if(!rows.length) return '<p class="desc">No executives in scope.</p>';
|
| - |
|
676 |
var h='<table><thead><tr><th>Executive</th><th class="num">Plan</th><th class="num">Done</th><th class="num">Adher.</th>'+
|
| - |
|
677 |
'<th class="num" title="Avg lead visits per active (punched-in) day">Lead/day</th>'+
|
| - |
|
678 |
'<th class="num" title="Avg franchisee visits per active (punched-in) day">Fr/day</th>'+
|
| 667 |
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>';
|
679 |
'<th class="num">Disc.</th><th class="num">Hrs</th><th class="num">Leads</th><th class="num">Geo⚠</th></tr></thead><tbody>';
|
| 668 |
rows.forEach(function(r){
|
680 |
rows.forEach(function(r){
|
| 669 |
var c=adhColor(r.adherencePct);
|
681 |
var c=adhColor(r.adherencePct);
|
| 670 |
h+='<tr class="clickable" onclick="openExecPeriod('+r.authUserId+',\''+js(r.name)+'\')">'+
|
682 |
h+='<tr class="clickable" onclick="openExecPeriod('+r.authUserId+',\''+js(r.name)+'\')">'+
|
| 671 |
'<td><b>'+esc(r.name)+'</b> <span class="lvl">'+esc(r.level)+'</span></td>'+
|
683 |
'<td><span class="lvl">'+esc(r.level)+'</span> <b>'+esc(r.name)+'</b></td>'+
|
| 672 |
'<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
|
684 |
'<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
|
| 673 |
'<td class="num"><b style="color:'+c+'">'+r.adherencePct+'%</b></td>'+
|
685 |
'<td class="num"><b style="color:'+c+'">'+r.adherencePct+'%</b></td>'+
|
| - |
|
686 |
'<td class="num">'+(r.avgLeadVisitsPerDay||'-')+'</td>'+
|
| - |
|
687 |
'<td class="num">'+(r.avgFranchiseeVisitsPerDay||'-')+'</td>'+
|
| 674 |
'<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'-')+(r.discussionMin?'m':'')+'</td>'+
|
688 |
'<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'-')+(r.discussionMin?'m':'')+'</td>'+
|
| 675 |
'<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td>'+
|
689 |
'<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td>'+
|
| 676 |
'<td class="num'+(r.geoFlags>0?' badflag':'')+'">'+r.geoFlags+'</td></tr>';
|
690 |
'<td class="num'+(r.geoFlags>0?' badflag':'')+'">'+r.geoFlags+'</td></tr>';
|
| 677 |
});
|
691 |
});
|
| 678 |
return h+'</tbody></table>';
|
692 |
return h+'</tbody></table>';
|