| Line 123... |
Line 123... |
| 123 |
.att .adet{font-size:12px;color:var(--muted);margin-top:2px}
|
123 |
.att .adet{font-size:12px;color:var(--muted);margin-top:2px}
|
| 124 |
.att .acount{font-size:18px;font-weight:780}
|
124 |
.att .acount{font-size:18px;font-weight:780}
|
| 125 |
.att.clickable{cursor:pointer;transition:border-color .12s,box-shadow .12s}
|
125 |
.att.clickable{cursor:pointer;transition:border-color .12s,box-shadow .12s}
|
| 126 |
.att.clickable:hover{border-color:#c7d0e8;box-shadow:0 2px 8px rgba(20,30,60,.08)}
|
126 |
.att.clickable:hover{border-color:#c7d0e8;box-shadow:0 2px 8px rgba(20,30,60,.08)}
|
| 127 |
.att .chev{color:#c2cadb;font-size:13px;margin-left:8px}
|
127 |
.att .chev{color:#c2cadb;font-size:13px;margin-left:8px}
|
| - |
|
128 |
.attrow{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px}
|
| - |
|
129 |
.attrow .att{margin-bottom:0}
|
| - |
|
130 |
@media (max-width:1000px){.attrow{grid-template-columns:repeat(2,1fr)}}
|
| 128 |
.barrow{display:grid;grid-template-columns:60px 1fr 90px;align-items:center;gap:11px;margin:8px 0}
|
131 |
.barrow{display:grid;grid-template-columns:60px 1fr 90px;align-items:center;gap:11px;margin:8px 0}
|
| 129 |
.barrow .name{font-size:13px;font-weight:600}
|
132 |
.barrow .name{font-size:13px;font-weight:600}
|
| 130 |
.track{background:#f1f3f9;border-radius:7px;height:20px;position:relative;overflow:hidden}
|
133 |
.track{background:#f1f3f9;border-radius:7px;height:20px;position:relative;overflow:hidden}
|
| 131 |
.fill{height:100%;border-radius:7px;display:flex;align-items:center;justify-content:flex-end;padding-right:6px;font-size:10.5px;color:#fff;font-weight:650;min-width:2px}
|
134 |
.fill{height:100%;border-radius:7px;display:flex;align-items:center;justify-content:flex-end;padding-right:6px;font-size:10.5px;color:#fff;font-weight:650;min-width:2px}
|
| 132 |
.barrow .meta{font-size:11.5px;color:var(--muted);text-align:right}
|
135 |
.barrow .meta{font-size:11.5px;color:var(--muted);text-align:right}
|
| Line 205... |
Line 208... |
| 205 |
.tlcard .tm{flex:none;width:62px;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;font-weight:750;font-size:13px;padding:6px 4px;line-height:1.05}
|
208 |
.tlcard .tm{flex:none;width:62px;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;font-weight:750;font-size:13px;padding:6px 4px;line-height:1.05}
|
| 206 |
.tlcard .tm small{font-weight:600;font-size:8.5px;opacity:.9;text-transform:uppercase;letter-spacing:.04em;margin-top:3px}
|
209 |
.tlcard .tm small{font-weight:600;font-size:8.5px;opacity:.9;text-transform:uppercase;letter-spacing:.04em;margin-top:3px}
|
| 207 |
.tlcard .bd{padding:8px 11px;flex:1;min-width:0}
|
210 |
.tlcard .bd{padding:8px 11px;flex:1;min-width:0}
|
| 208 |
.tlcard .bd .ti{font-size:12.5px;font-weight:700;color:var(--ink);line-height:1.25;word-break:break-word}
|
211 |
.tlcard .bd .ti{font-size:12.5px;font-weight:700;color:var(--ink);line-height:1.25;word-break:break-word}
|
| 209 |
.tlcard .bd .stt{display:inline-block;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;padding:1px 6px;border-radius:5px;margin-top:5px;color:#fff}
|
212 |
.tlcard .bd .stt{display:inline-block;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;padding:1px 6px;border-radius:5px;margin-top:5px;color:#fff}
|
| - |
|
213 |
.tlcard .bd .stt.unplanned{background:#fff4e6;color:#e8590c;border:1px solid #ffd8a8;margin-left:5px}
|
| 210 |
.tlcard .bd .mt{font-size:11px;color:var(--muted);margin-top:6px}
|
214 |
.tlcard .bd .mt{font-size:11px;color:var(--muted);margin-top:6px}
|
| 211 |
.tlextra{display:flex;align-items:center;gap:7px;margin-top:8px;flex-wrap:wrap}
|
215 |
.tlextra{display:flex;align-items:center;gap:7px;margin-top:8px;flex-wrap:wrap}
|
| 212 |
.tlrow.left .tlextra{justify-content:flex-end}
|
216 |
.tlrow.left .tlextra{justify-content:flex-end}
|
| 213 |
.tlrow.right .tlextra{justify-content:flex-start}
|
217 |
.tlrow.right .tlextra{justify-content:flex-start}
|
| 214 |
.tlthumb{width:46px;height:46px;border-radius:8px;object-fit:cover;border:1px solid var(--line);cursor:pointer;background:#f1f3f9}
|
218 |
.tlthumb{width:46px;height:46px;border-radius:8px;object-fit:cover;border:1px solid var(--line);cursor:pointer;background:#f1f3f9}
|
| Line 372... |
Line 376... |
| 372 |
'<div class="top"><div class="ic" style="background:'+bg+';color:'+fg+'">'+ic+'</div><div class="label">'+title+'</div></div>'+
|
376 |
'<div class="top"><div class="ic" style="background:'+bg+';color:'+fg+'">'+ic+'</div><div class="label">'+title+'</div></div>'+
|
| 373 |
'<div class="brow blabs">'+lab+'</div>'+
|
377 |
'<div class="brow blabs">'+lab+'</div>'+
|
| 374 |
'<div class="brow bvals">'+val+'</div></div>';
|
378 |
'<div class="brow bvals">'+val+'</div></div>';
|
| 375 |
}
|
379 |
}
|
| 376 |
// Period journey summary as a row of cards (old funnel layout). Each card is a
|
380 |
// Period journey summary as a row of cards (old funnel layout). Each card is a
|
| 377 |
// group (Total Visits / Avg Time Spent / Idle-Break / Deferred / Completed) split
|
381 |
// group (Total Visits / Avg Time Spent / Idle-Break / Deferred / Completed),
|
| 378 |
// into Planned vs Self Assigned columns. col=[[label,value,colorClass],...].
|
382 |
// planned and self-assigned merged into one value. col=[[label,value,colorClass],...].
|
| 379 |
function summaryCard(title, cols, last){
|
383 |
function summaryCard(title, cols, last){
|
| 380 |
var body='';
|
384 |
var body='';
|
| 381 |
for(var i=0;i<cols.length;i++){
|
385 |
for(var i=0;i<cols.length;i++){
|
| 382 |
body+='<div class="scol"><div class="sl">'+cols[i][0]+'</div>'+
|
386 |
body+='<div class="scol"><div class="sl">'+cols[i][0]+'</div>'+
|
| 383 |
'<div class="sv'+(cols[i][2]?' '+cols[i][2]:'')+'">'+cols[i][1]+'</div></div>';
|
387 |
'<div class="sv'+(cols[i][2]?' '+cols[i][2]:'')+'">'+cols[i][1]+'</div></div>';
|
| Line 386... |
Line 390... |
| 386 |
(last?'':'<span class="arrow">➤</span>')+'</div>';
|
390 |
(last?'':'<span class="arrow">➤</span>')+'</div>';
|
| 387 |
}
|
391 |
}
|
| 388 |
function summaryCards(s){
|
392 |
function summaryCards(s){
|
| 389 |
if(!s) return '';
|
393 |
if(!s) return '';
|
| 390 |
var mn=function(v){return num(v)+'<span class="u">min</span>';};
|
394 |
var mn=function(v){return num(v)+'<span class="u">min</span>';};
|
| - |
|
395 |
// planned + self-assigned merged into single totals
|
| - |
|
396 |
var totalVisits=(s.plannedVisits||0)+(s.selfVisits||0);
|
| - |
|
397 |
var completed=(s.completedPlanned||0)+(s.completedSelf||0);
|
| 391 |
var dpC=s.deferredPlanned>0?'bad':'', dsC=s.deferredSelf>0?'bad':'';
|
398 |
var deferred=(s.deferredPlanned||0)+(s.deferredSelf||0);
|
| 392 |
return summaryCard('Total Visits',[['Planned',num(s.plannedVisits),''],['Self Assigned',num(s.selfVisits),'']])+
|
399 |
return summaryCard('Total Visits',[['Visits',num(totalVisits),'']])+
|
| 393 |
summaryCard('Average Time Spent',[['Planned',mn(s.avgTimePlannedMin),''],['Self Assigned',mn(s.avgTimeSelfMin),'']])+
|
400 |
summaryCard('Average Time Spent',[['Per visit',mn(s.avgTimeAllMin),'']])+
|
| 394 |
summaryCard('Idle / Break',[['Total Average',mn(s.idleAvgMin),'']])+
|
401 |
summaryCard('Idle / Break',[['Total Average',mn(s.idleAvgMin),'']])+
|
| 395 |
summaryCard('Completed Visits',[['Planned',num(s.completedPlanned),'good'],['Self Assigned',num(s.completedSelf),'good']])+
|
402 |
summaryCard('Completed Visits',[['Completed',num(completed),'good']])+
|
| 396 |
summaryCard('Deferred',[['Planned',num(s.deferredPlanned),dpC],['Self Assigned',num(s.deferredSelf),dsC]],true);
|
403 |
summaryCard('Deferred',[['Deferred',num(deferred),deferred>0?'bad':'']],true);
|
| 397 |
}
|
404 |
}
|
| 398 |
function fstep(name, val, w, sub, drop, last, key){
|
405 |
function fstep(name, val, w, sub, drop, last, key){
|
| 399 |
return '<div class="fstep'+(key?' clickable" data-fkey="'+key+'" onclick="funnelClick(\''+key+'\')" title="Click to filter the board to these executives':'')+'"><div class="fn">'+name+'</div><div class="fv">'+num(val)+'</div>'+
|
406 |
return '<div class="fstep'+(key?' clickable" data-fkey="'+key+'" onclick="funnelClick(\''+key+'\')" title="Click to filter the board to these executives':'')+'"><div class="fn">'+name+'</div><div class="fv">'+num(val)+'</div>'+
|
| 400 |
'<div class="fbar" style="width:'+Math.min(w,100)+'%"></div><div class="'+(drop?'drop':'keep')+'">'+sub+'</div>'+
|
407 |
'<div class="fbar" style="width:'+Math.min(w,100)+'%"></div><div class="'+(drop?'drop':'keep')+'">'+sub+'</div>'+
|
| 401 |
(last?'':'<span class="arrow">➤</span>')+'</div>';
|
408 |
(last?'':'<span class="arrow">➤</span>')+'</div>';
|
| Line 423... |
Line 430... |
| 423 |
h+=kpi('📅','#ebfbee','#2f9e44','Scheduled PJP',num(k.scheduledPjp)+' <small>/'+k.totalExecs+'</small>',k.unscheduled+' unscheduled',null,"openCards('scheduled')");
|
430 |
h+=kpi('📅','#ebfbee','#2f9e44','Scheduled PJP',num(k.scheduledPjp)+' <small>/'+k.totalExecs+'</small>',k.unscheduled+' unscheduled',null,"openCards('scheduled')");
|
| 424 |
h+=kpi('✎','#fff4e6','#e8590c','Agenda Filled',num(k.agendaFilled)+' <small>/'+k.scheduledPjp+'</small>',(k.scheduledPjp-k.agendaFilled)+' pending',null,"openCards('agenda')");
|
431 |
h+=kpi('✎','#fff4e6','#e8590c','Agenda Filled',num(k.agendaFilled)+' <small>/'+k.scheduledPjp+'</small>',(k.scheduledPjp-k.agendaFilled)+' pending',null,"openCards('agenda')");
|
| 425 |
h+=kpi('❌','#fff0f6','#d6336c','Unscheduled',num(k.unscheduled),'no PJP today',null,"openCards('unscheduled')");
|
432 |
h+=kpi('❌','#fff0f6','#d6336c','Unscheduled',num(k.unscheduled),'no PJP today',null,"openCards('unscheduled')");
|
| 426 |
h+='</div>';
|
433 |
h+='</div>';
|
| 427 |
|
434 |
|
| - |
|
435 |
h+='<div class="card"><h2>Needs attention now</h2><p class="desc">Things a manager should clear today.</p>'+attentionList(a)+'</div>';
|
| - |
|
436 |
|
| 428 |
h+='<div class="sectionlabel">Today\'s Journey Funnel</div><div class="card"><div class="funnel">';
|
437 |
h+='<div class="sectionlabel">Today\'s Journey Funnel</div><div class="card"><div class="funnel">';
|
| 429 |
var sp=f.scheduledPjp||1, pin=f.punchedIn||1;
|
438 |
var sp=f.scheduledPjp||1, pin=f.punchedIn||1;
|
| 430 |
h+=fstep('Punched In',f.punchedIn,pct(f.punchedIn,sp),f.punchedIn+' of '+f.scheduledPjp+' scheduled',false,false,'punchedIn');
|
439 |
h+=fstep('Punched In',f.punchedIn,pct(f.punchedIn,sp),f.punchedIn+' of '+f.scheduledPjp+' scheduled',false,false,'punchedIn');
|
| 431 |
h+=fstep('Traveling',f.travelling,pct(f.travelling,pin),'on the road',false,false,'travelling');
|
440 |
h+=fstep('Traveling',f.travelling,pct(f.travelling,pin),'on the road',false,false,'travelling');
|
| 432 |
h+=fstep('In Store',f.inStore,pct(f.inStore,pin),'at a stop',false,false,'inStore');
|
441 |
h+=fstep('In Store',f.inStore,pct(f.inStore,pin),'at a stop',false,false,'inStore');
|
| 433 |
h+=fstep('Idle / Break',f.idle,pct(f.idle,pin),'off-store > 40 min',true,false,'idle');
|
442 |
h+=fstep('Idle / Break',f.idle,pct(f.idle,pin),'off-store > 40 min',true,false,'idle');
|
| 434 |
h+=fstep('Punched Out',f.punchedOut,pct(f.punchedOut,pin),'journey ended',false,true,'punchedOut');
|
443 |
h+=fstep('Punched Out',f.punchedOut,pct(f.punchedOut,pin),'journey ended',false,true,'punchedOut');
|
| 435 |
h+='</div></div>';
|
444 |
h+='</div></div>';
|
| 436 |
|
445 |
|
| 437 |
window._boardFilter = null; // funnel-card filter (null = all rows)
|
446 |
window._boardFilter = null; // funnel-card filter (null = all rows)
|
| 438 |
h+='<div class="grid g3">';
|
- |
|
| 439 |
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>';
|
447 |
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>';
|
| 440 |
h+='<div class="card"><h2>Needs attention now</h2><p class="desc">Things a manager should clear today.</p>'+attentionList(a)+'</div>';
|
- |
|
| 441 |
h+='</div>';
|
- |
|
| 442 |
el('bj-content').innerHTML=h;
|
448 |
el('bj-content').innerHTML=h;
|
| 443 |
}
|
449 |
}
|
| 444 |
function stateClass(st){ return st==='IN_STORE'?'ps-store':st==='TRAVELLING'?'ps-travel':st==='DONE'?'ps-done':st==='NOT_STARTED'?'ps-no':'ps-idle'; }
|
450 |
function stateClass(st){ return st==='IN_STORE'?'ps-store':st==='TRAVELLING'?'ps-travel':st==='DONE'?'ps-done':st==='NOT_STARTED'?'ps-no':'ps-idle'; }
|
| 445 |
function js(s){ return String(s==null?'':s).replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
|
451 |
function js(s){ return String(s==null?'':s).replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
|
| 446 |
// Open the Beat Planner (map-first planner window) for this executive/beat/day,
|
452 |
// Open the Beat Planner (map-first planner window) for this executive/beat/day,
|
| Line 449... |
Line 455... |
| 449 |
var u = CTX + '/beatPlanWindow?autoUserId=' + encodeURIComponent(authUserId) + '&editDate=' + encodeURIComponent(date);
|
455 |
var u = CTX + '/beatPlanWindow?autoUserId=' + encodeURIComponent(authUserId) + '&editDate=' + encodeURIComponent(date);
|
| 450 |
if(beatId && beatId>0) u += '&editBeatId=' + encodeURIComponent(beatId);
|
456 |
if(beatId && beatId>0) u += '&editBeatId=' + encodeURIComponent(beatId);
|
| 451 |
if(mode==='add') u += '&assign=1';
|
457 |
if(mode==='add') u += '&assign=1';
|
| 452 |
window.open(u, '_blank');
|
458 |
window.open(u, '_blank');
|
| 453 |
}
|
459 |
}
|
| 454 |
// Compact "completed − deferred − pending / planned" cell: completed green,
|
- |
|
| 455 |
// deferred amber, pending grey (planned stops not yet acted on). Denominator is
|
- |
|
| 456 |
// the plan (or activity total if it ran past the plan), so the parts reconcile.
|
- |
|
| 457 |
// Pending is shown only when there's a remainder.
|
- |
|
| 458 |
function cdp(completed, deferred, planned){
|
- |
|
| 459 |
completed=completed||0; deferred=deferred||0; planned=planned||0;
|
- |
|
| 460 |
var denom=Math.max(planned, completed+deferred);
|
- |
|
| 461 |
var pending=denom-completed-deferred;
|
- |
|
| 462 |
var s='<span class="cgreen">'+completed+'</span> <span style="color:#cdd3df">-</span> '+
|
- |
|
| 463 |
'<span class="camber">'+deferred+'</span>';
|
- |
|
| 464 |
if(pending>0) s+=' <span style="color:#cdd3df">-</span> <span class="cmuted">'+pending+'</span>';
|
- |
|
| 465 |
s+=' <span style="color:#97a1b5;font-weight:400">/ '+denom+'</span>';
|
- |
|
| 466 |
return s;
|
- |
|
| 467 |
}
|
- |
|
| 468 |
// Funnel-card click: period cards (p_*) open their drill-down popup; today's
|
460 |
// Funnel-card click: period cards (p_*) open their drill-down popup; today's
|
| 469 |
// funnel cards filter the Live executive board in place. Clicking the active
|
461 |
// funnel cards filter the Live executive board in place. Clicking the active
|
| 470 |
// card again clears the filter.
|
462 |
// card again clears the filter.
|
| 471 |
function funnelClick(key){
|
463 |
function funnelClick(key){
|
| 472 |
if(key && key.indexOf('p_')===0) return openFunnel(key);
|
464 |
if(key && key.indexOf('p_')===0) return openFunnel(key);
|
| Line 492... |
Line 484... |
| 492 |
bar='<div class="boardfilter">Showing <b>'+esc(m.title)+'</b> · '+view.length+
|
484 |
bar='<div class="boardfilter">Showing <b>'+esc(m.title)+'</b> · '+view.length+
|
| 493 |
' of '+rows.length+'<button class="clearf" onclick="setBoardFilter(null)">× Clear filter</button></div>';
|
485 |
' of '+rows.length+'<button class="clearf" onclick="setBoardFilter(null)">× Clear filter</button></div>';
|
| 494 |
}
|
486 |
}
|
| 495 |
return bar+boardTable(view, date);
|
487 |
return bar+boardTable(view, date);
|
| 496 |
}
|
488 |
}
|
| - |
|
489 |
// Merge planned partner visits and self-assigned leads into one set of tallies.
|
| - |
|
490 |
// Total reconciles with the parts: Total = Completed + Pending + Deferred, where
|
| - |
|
491 |
// Total is the plan (or activity total if it ran past the plan).
|
| - |
|
492 |
function boardCounts(r){
|
| - |
|
493 |
var completed=r.completedVisits||0, deferred=r.deferred||0;
|
| - |
|
494 |
var total=Math.max(r.planned||0, completed+deferred);
|
| - |
|
495 |
return {total:total, completed:completed, deferred:deferred, pending:total-completed-deferred};
|
| - |
|
496 |
}
|
| 497 |
function boardTable(rows, date){
|
497 |
function boardTable(rows, date){
|
| 498 |
if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
|
498 |
if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
|
| 499 |
var h='<div style="overflow-x:auto"><table><thead><tr>'+
|
499 |
var h='<div style="overflow-x:auto"><table><thead><tr>'+
|
| 500 |
'<th>Agenda</th><th>Executive</th><th>Punch In/Out</th>'+
|
500 |
'<th>Agenda</th><th>Executive</th><th>Punch In/Out</th>'+
|
| 501 |
'<th class="num" title="Completed − Deferred − Pending / Planned">Planned - Visits</th>'+
|
501 |
'<th class="num" title="Planned partner visits + self-assigned leads">Total</th>'+
|
| 502 |
'<th class="num" title="Completed − Deferred − Pending / Planned">Self Assigned Lead</th>'+
|
502 |
'<th class="num">Completed</th><th class="num">Pending</th><th class="num">Deferred</th>'+
|
| 503 |
'<th class="num">Total</th><th>State</th><th>Action</th>'+
|
503 |
'<th>State</th><th>Action</th>'+
|
| 504 |
'</tr></thead><tbody>';
|
504 |
'</tr></thead><tbody>';
|
| 505 |
// column totals, computed up front so the Total row can lead the table
|
505 |
// column totals, computed up front so the Total row can lead the table
|
| 506 |
var t={cp:0,dp:0,pp:0,cl:0,dl:0,pl:0,tot:0};
|
506 |
var t={total:0,completed:0,pending:0,deferred:0};
|
| 507 |
rows.forEach(function(r){
|
507 |
rows.forEach(function(r){
|
| 508 |
t.cp+=r.completedPartner||0; t.dp+=r.deferredPartner||0; t.pp+=r.plannedPartner||0;
|
508 |
var c=boardCounts(r);
|
| 509 |
t.cl+=r.completedLead||0; t.dl+=r.deferredLead||0; t.pl+=r.plannedLead||0;
|
509 |
t.total+=c.total; t.completed+=c.completed; t.pending+=c.pending; t.deferred+=c.deferred;
|
| 510 |
t.tot+=r.totalVisits||0;
|
- |
|
| 511 |
});
|
510 |
});
|
| 512 |
h+='<tr class="boardtot">'+
|
511 |
h+='<tr class="boardtot">'+
|
| 513 |
'<td></td><td><b>Total</b><br><span style="color:#97a1b5;font-size:11px">'+rows.length+' executives</span></td><td></td>'+
|
512 |
'<td></td><td><b>Total</b><br><span style="color:#97a1b5;font-size:11px">'+rows.length+' executives</span></td><td></td>'+
|
| 514 |
'<td class="num cnt">'+cdp(t.cp,t.dp,t.pp)+'</td>'+
|
513 |
'<td class="num cnt cmuted">'+t.total+'</td>'+
|
| - |
|
514 |
'<td class="num cnt cgreen">'+t.completed+'</td>'+
|
| 515 |
'<td class="num cnt">'+cdp(t.cl,t.dl,t.pl)+'</td>'+
|
515 |
'<td class="num cnt">'+t.pending+'</td>'+
|
| 516 |
'<td class="num cnt cmuted">'+t.tot+'</td><td></td><td></td>'+
|
516 |
'<td class="num cnt camber">'+t.deferred+'</td><td></td><td></td>'+
|
| 517 |
'</tr>';
|
517 |
'</tr>';
|
| 518 |
rows.forEach(function(r){
|
518 |
rows.forEach(function(r){
|
| 519 |
var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (r.idleAway?'rowwarn':'');
|
519 |
var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (r.idleAway?'rowwarn':'');
|
| 520 |
var A = r.userId+",'"+js(r.name)+"','"+date+"'"; // openExec args
|
520 |
var A = r.userId+",'"+js(r.name)+"','"+date+"'"; // openExec args
|
| 521 |
var go = 'onclick="openExec('+A+')"';
|
521 |
var go = 'onclick="openExec('+A+')"';
|
| - |
|
522 |
var c=boardCounts(r);
|
| 522 |
h+='<tr class="clickable '+rowcls+'" '+go+'>'+
|
523 |
h+='<tr class="clickable '+rowcls+'" '+go+'>'+
|
| 523 |
'<td style="text-align:center">'+(r.agendaFilled?'<span style="color:#2f9e44;font-size:15px">✓</span>':'<span class="badflag" style="font-size:15px">✗</span>')+'</td>'+
|
524 |
'<td style="text-align:center">'+(r.agendaFilled?'<span style="color:#2f9e44;font-size:15px">✓</span>':'<span class="badflag" style="font-size:15px">✗</span>')+'</td>'+
|
| 524 |
'<td><b>'+esc(r.name)+'</b><br>'+esc(r.beatName||'-')+'</td>'+
|
525 |
'<td><b>'+esc(r.name)+'</b><br>'+esc(r.beatName||'-')+'</td>'+
|
| 525 |
'<td style="white-space:nowrap">'+(r.punchInTime?'<span class="ptime">'+esc(r.punchInTime)+'</span>':'<span class="badflag">-</span>')+
|
526 |
'<td style="white-space:nowrap">'+(r.punchInTime?'<span class="ptime">'+esc(r.punchInTime)+'</span>':'<span class="badflag">-</span>')+
|
| 526 |
' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
|
527 |
' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
|
| - |
|
528 |
'<td class="num cnt cmuted">'+c.total+'</td>'+
|
| 527 |
'<td class="num cnt">'+cdp(r.completedPartner,r.deferredPartner,r.plannedPartner)+'</td>'+
|
529 |
'<td class="num cnt cgreen">'+c.completed+'</td>'+
|
| 528 |
'<td class="num cnt">'+cdp(r.completedLead,r.deferredLead,r.plannedLead)+'</td>'+
|
530 |
'<td class="num cnt">'+c.pending+'</td>'+
|
| 529 |
'<td class="num cnt cmuted">'+r.totalVisits+'</td>'+
|
531 |
'<td class="num cnt camber">'+c.deferred+'</td>'+
|
| 530 |
'<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
|
532 |
'<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
|
| 531 |
'<td style="white-space:nowrap" onclick="event.stopPropagation()">'+
|
533 |
'<td style="white-space:nowrap" onclick="event.stopPropagation()">'+
|
| 532 |
'<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">📍</button>'+
|
534 |
'<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">📍</button>'+
|
| 533 |
'<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">👁</button>'+
|
535 |
'<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">👁</button>'+
|
| 534 |
'<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">✎</button>'+
|
536 |
'<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">✎</button>'+
|
| Line 785... |
Line 787... |
| 785 |
pModal(h);
|
787 |
pModal(h);
|
| 786 |
}
|
788 |
}
|
| 787 |
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; }
|
789 |
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; }
|
| 788 |
var ATT_META = {
|
790 |
var ATT_META = {
|
| 789 |
notPunchedIn: {bg:'#fff0f0', fg:'#e03131', ic:'⚠', title:'Not punched in', sub:'executives with a beat, not started', cls:'badflag'},
|
791 |
notPunchedIn: {bg:'#fff0f0', fg:'#e03131', ic:'⚠', title:'Not punched in', sub:'executives with a beat, not started', cls:'badflag'},
|
| 790 |
idleStale: {bg:'#fff4e6', fg:'#e8590c', ic:'⏱', title:'Idle — off-store > 40 min', sub:'stationary >20 m from any store', cls:'warnflag'},
|
792 |
idleStale: {bg:'#fff4e6', fg:'#e8590c', ic:'⏱', title:'Idle — off-store > 59 min', sub:'stationary >20 m from any store', cls:'warnflag'},
|
| 791 |
geofence: {bg:'#fff0f0', fg:'#e03131', ic:'📍', title:'Check-in outside store geofence', sub:'>50 m from partner\'s saved location', cls:'badflag'},
|
793 |
geofence: {bg:'#fff0f0', fg:'#e03131', ic:'📍', title:'Check-in outside store geofence', sub:'>50 m from partner\'s saved location', cls:'badflag'},
|
| 792 |
locationOff: {bg:'#fff4e6', fg:'#e8590c', ic:'📴', title:'Location turned off mid-journey', sub:'GPS toggled off', cls:'warnflag'},
|
794 |
locationOff: {bg:'#fff4e6', fg:'#e8590c', ic:'📴', title:'Location turned off mid-journey', sub:'GPS toggled off', cls:'warnflag'},
|
| 793 |
deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'↻', title:'Deferred, awaiting re-action', sub:'shops deferred today', cls:''}
|
795 |
deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'↻', title:'Deferred, awaiting re-action', sub:'shops deferred today', cls:''}
|
| 794 |
};
|
796 |
};
|
| 795 |
function attentionList(a){
|
797 |
function attentionList(a){
|
| 796 |
window._att = a;
|
798 |
window._att = a;
|
| 797 |
var order=['notPunchedIn','idleStale','geofence','locationOff','deferredAwaiting'];
|
799 |
var order=['notPunchedIn','idleStale','geofence','locationOff','deferredAwaiting'];
|
| 798 |
var h='';
|
800 |
var h='<div class="attrow">';
|
| 799 |
order.forEach(function(k){
|
801 |
order.forEach(function(k){
|
| 800 |
var m=ATT_META[k], list=a[k]||[], n=list.length;
|
802 |
var m=ATT_META[k], list=a[k]||[], n=list.length;
|
| 801 |
var detail = n ? names(list.map(function(x){return x.name;}),3) : m.sub;
|
803 |
var detail = n ? names(list.map(function(x){return x.name;}),3) : m.sub;
|
| 802 |
var cls = n ? ' att clickable' : ' att';
|
804 |
var cls = n ? ' att clickable' : ' att';
|
| 803 |
var click = n ? ' onclick="openAttention(\''+k+'\')"' : '';
|
805 |
var click = n ? ' onclick="openAttention(\''+k+'\')"' : '';
|
| 804 |
h+='<div class="'+cls.trim()+'"'+click+'><div class="ic" style="background:'+m.bg+';color:'+m.fg+'">'+m.ic+'</div>'+
|
806 |
h+='<div class="'+cls.trim()+'"'+click+'><div class="ic" style="background:'+m.bg+';color:'+m.fg+'">'+m.ic+'</div>'+
|
| 805 |
'<div class="ab"><div class="atitle">'+m.title+'</div><div class="adet">'+detail+'</div></div>'+
|
807 |
'<div class="ab"><div class="atitle">'+m.title+'</div><div class="adet">'+detail+'</div></div>'+
|
| 806 |
'<div class="acount '+(n?m.cls:'')+'">'+n+'</div>'+(n?'<span class="chev">▸</span>':'')+'</div>';
|
808 |
'<div class="acount '+(n?m.cls:'')+'">'+n+'</div>'+(n?'<span class="chev">▸</span>':'')+'</div>';
|
| 807 |
});
|
809 |
});
|
| 808 |
return h;
|
810 |
return h+'</div>';
|
| 809 |
}
|
811 |
}
|
| 810 |
function openAttention(key){
|
812 |
function openAttention(key){
|
| 811 |
var a=window._att||{}, list=a[key]||[], m=ATT_META[key]||{title:'Detail'};
|
813 |
var a=window._att||{}, list=a[key]||[], m=ATT_META[key]||{title:'Detail'};
|
| 812 |
var mb=el('bj-modal-back'), md=el('bj-modal');
|
814 |
var mb=el('bj-modal-back'), md=el('bj-modal');
|
| 813 |
// Off-site check-ins can be approved here, but only by L5/Final (canApprove).
|
815 |
// Off-site check-ins can be approved here, but only by L5/Final (canApprove).
|
| Line 815... |
Line 817... |
| 815 |
var h='<button class="x" onclick="closeModal()">×</button><h3>'+m.title+'</h3>'+
|
817 |
var h='<button class="x" onclick="closeModal()">×</button><h3>'+m.title+'</h3>'+
|
| 816 |
'<p class="desc">'+list.length+' '+(list.length===1?'item':'items')+
|
818 |
'<p class="desc">'+list.length+' '+(list.length===1?'item':'items')+
|
| 817 |
(showApprove?' · approving updates the partner\'s saved coordinates':'')+'</p>';
|
819 |
(showApprove?' · approving updates the partner\'s saved coordinates':'')+'</p>';
|
| 818 |
if(!list.length){ h+='<p class="desc">Nothing here.</p>'; }
|
820 |
if(!list.length){ h+='<p class="desc">Nothing here.</p>'; }
|
| 819 |
else{
|
821 |
else{
|
| 820 |
h+='<table><tbody>';
|
822 |
var date=window._boardDate||'';
|
| - |
|
823 |
h+='<p class="desc">Click a row to open that executive\'s journey detail.</p><table><tbody>';
|
| 821 |
list.forEach(function(x){
|
824 |
list.forEach(function(x){
|
| 822 |
var act='';
|
825 |
var act='';
|
| - |
|
826 |
// approve button gets its own cell that swallows the click so it doesn't drill down
|
| 823 |
if(showApprove) act='<td class="num" style="white-space:nowrap">'+(x.trackingId?'<button class="btn-ok" onclick="openValidate('+x.trackingId+')">Validate & Approve</button>':'')+'</td>';
|
827 |
if(showApprove) act='<td class="num" style="white-space:nowrap" onclick="event.stopPropagation()">'+(x.trackingId?'<button class="btn-ok" onclick="openValidate('+x.trackingId+')">Validate & Approve</button>':'')+'</td>';
|
| - |
|
828 |
var canOpen = x.userId>0;
|
| - |
|
829 |
var go = canOpen ? ' class="clickable" onclick="openExec('+x.userId+",'"+js(x.name)+"','"+date+"')\"" : '';
|
| 824 |
h+='<tr><td style="font-weight:600;white-space:nowrap">'+esc(x.name)+'</td>'+
|
830 |
h+='<tr'+go+'><td style="font-weight:600;white-space:nowrap">'+esc(x.name)+'</td>'+
|
| 825 |
'<td style="color:#69748c">'+esc(x.detail||'')+'</td>'+act+'</tr>';
|
831 |
'<td style="color:#69748c">'+esc(x.detail||'')+'</td>'+act+'</tr>';
|
| 826 |
});
|
832 |
});
|
| 827 |
h+='</tbody></table>';
|
833 |
h+='</tbody></table>';
|
| 828 |
}
|
834 |
}
|
| 829 |
md.innerHTML=h; mb.classList.add('show');
|
835 |
md.innerHTML=h; mb.classList.add('show');
|
| Line 872... |
Line 878... |
| 872 |
var h='';
|
878 |
var h='';
|
| 873 |
h+='<div class="kpis">';
|
879 |
h+='<div class="kpis">';
|
| 874 |
h+=kpi('⚖','#fff4e6','#f08c00','Adherence',k.adherencePct+'<small>%</small>',num(k.doneStops)+' of '+num(k.plannedStops)+' planned visits done',adhColor(k.adherencePct),"openPScore('adherencePct','Adherence','Planned visits done vs planned, per executive')");
|
880 |
h+=kpi('⚖','#fff4e6','#f08c00','Adherence',k.adherencePct+'<small>%</small>',num(k.doneStops)+' of '+num(k.plannedStops)+' planned visits done',adhColor(k.adherencePct),"openPScore('adherencePct','Adherence','Planned visits done vs planned, per executive')");
|
| 875 |
h+=kpi('🗺','#e7f5ff','#1098ad','PJP coverage L1',k.coverageL1Pct+'<small>%</small>',k.coverageL1With+'/'+k.coverageL1Total+' have a plan','#1098ad',"openCoverage('L1')");
|
881 |
h+=kpi('🗺','#e7f5ff','#1098ad','PJP coverage L1',k.coverageL1Pct+'<small>%</small>',k.coverageL1With+'/'+k.coverageL1Total+' have a plan','#1098ad',"openCoverage('L1')");
|
| 876 |
h+=kpiCols('🚶','#ebfbee','#2f9e44','Average Visit (per day)',
|
882 |
h+=kpiCols('🚶','#ebfbee','#2f9e44','Average Visit (per day)',
|
| 877 |
[['Franchisee',k.avgPartnersPerActiveDay],['Planned lead',k.avgPlannedLeadsPerActiveDay],['Unplanned lead',k.avgUnplannedLeadsPerActiveDay]],
|
883 |
[['Franchisee',k.avgPartnersPerActiveDay],['Lead',k.avgLeadsPerActiveDay]],
|
| 878 |
null,"openPScore('done','Visits / active day','Visits completed per executive')");
|
884 |
null,"openPScore('done','Visits / active day','Visits completed per executive')");
|
| 879 |
h+=kpiCols('💬','#eef1fb','#3b5bdb','Average discussion (per executive)',
|
885 |
h+=kpiCols('💬','#eef1fb','#3b5bdb','Average discussion (per executive)',
|
| 880 |
[['Partner',k.avgDiscussionPartnerMin],['Lead',k.avgDiscussionLeadMin]],
|
886 |
[['Partner',k.avgDiscussionPartnerMin],['Lead',k.avgDiscussionLeadMin]],
|
| 881 |
k.avgDiscussionMin>0&&k.avgDiscussionMin<30?'#e8590c':null,"openPScore('discussionMin','Average discussion minutes','Avg in-store discussion per executive')",'min');
|
887 |
k.avgDiscussionMin>0&&k.avgDiscussionMin<30?'#e8590c':null,"openPScore('discussionMin','Average discussion minutes','Avg in-store discussion per executive')",'min');
|
| 882 |
h+=kpiCols('↻','#fff0f0','#e03131','Auto "missed"',
|
888 |
h+=kpiCols('↻','#fff0f0','#e03131','Auto "missed"',
|
| 883 |
[['Unscheduled',k.autoMissedUnscheduled],['Unfilled agenda',k.autoMissedUnfilledAgenda],['Missed punch',k.autoMissedMissedPunchin]],
|
889 |
[['Unscheduled',k.autoMissedUnscheduled],['Unfilled agenda',k.autoMissedUnfilledAgenda],['Missed punch',k.autoMissedMissedPunchin]],
|
| 884 |
'#e03131',"openMissed()");
|
890 |
'#e03131',"openMissed()");
|
| 885 |
h+=kpi('★','#fff0f6','#d6336c','Leads on route',num(k.leadsCreated),'this period',null,"openPLeads()");
|
891 |
h+=kpi('★','#fff0f6','#d6336c','Leads on route',num(k.leadsCreated),'this period',null,"openPLeads()");
|
| 886 |
h+='</div>';
|
892 |
h+='</div>';
|
| 887 |
|
893 |
|
| 888 |
h+='<div class="sectionlabel">Journey Summary</div><div class="card"><p class="desc">Planned = visits on the PJP · Self Assigned = ad-hoc visits the executive added.</p><div class="funnel">'+summaryCards(d.summary)+'</div></div>';
|
894 |
h+='<div class="sectionlabel">Journey Summary</div><div class="card"><p class="desc">Planned (PJP) and self-assigned visits combined.</p><div class="funnel">'+summaryCards(d.summary)+'</div></div>';
|
| 889 |
|
895 |
|
| 890 |
h+='<div class="sectionlabel">Adherence & Coverage</div><div class="grid g3">';
|
896 |
h+='<div class="sectionlabel">Adherence & Coverage</div><div class="grid g3">';
|
| 891 |
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>';
|
897 |
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>';
|
| 892 |
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>';
|
898 |
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>';
|
| 893 |
h+='</div>';
|
899 |
h+='</div>';
|
| Line 1439... |
Line 1445... |
| 1439 |
}
|
1445 |
}
|
| 1440 |
var card='<div class="tlcard">'+
|
1446 |
var card='<div class="tlcard">'+
|
| 1441 |
'<div class="tm" style="background:'+s.c+'">'+timeTop+'<small>'+tlabel+'</small></div>'+
|
1447 |
'<div class="tm" style="background:'+s.c+'">'+timeTop+'<small>'+tlabel+'</small></div>'+
|
| 1442 |
'<div class="bd"><div class="ti">'+esc(tlTitle(x))+'</div>'+
|
1448 |
'<div class="bd"><div class="ti">'+esc(tlTitle(x))+'</div>'+
|
| 1443 |
'<span class="stt" style="background:'+s.c+'">'+s.l+'</span>'+
|
1449 |
'<span class="stt" style="background:'+s.c+'">'+s.l+'</span>'+
|
| - |
|
1450 |
(tt==='lead' ? '<span class="stt unplanned">Unplanned</span>' : '')+
|
| 1444 |
'<div class="mt">'+meta+'</div>'+extra+'</div>'+
|
1451 |
'<div class="mt">'+meta+'</div>'+extra+'</div>'+
|
| 1445 |
'</div>';
|
1452 |
'</div>';
|
| 1446 |
// remark sits on the opposite half of the timeline (empty spacer when none)
|
1453 |
// remark sits on the opposite half of the timeline (empty spacer when none)
|
| 1447 |
var rk=(!x._punch && x.remarks) ? '<div class="rk"><div class="rkl">Remark</div>'+esc(x.remarks)+'</div>' : '';
|
1454 |
var rk=(!x._punch && x.remarks) ? '<div class="rk"><div class="rkl">Remark</div>'+esc(x.remarks)+'</div>' : '';
|
| 1448 |
var remark='<div class="tlremark">'+rk+'</div>';
|
1455 |
var remark='<div class="tlremark">'+rk+'</div>';
|