| Line 52... |
Line 52... |
| 52 |
.sectionlabel{font-size:11.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--soft);font-weight:700;margin:24px 2px 12px}
|
52 |
.sectionlabel{font-size:11.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--soft);font-weight:700;margin:24px 2px 12px}
|
| 53 |
.funnel{display:flex;gap:9px;align-items:stretch}
|
53 |
.funnel{display:flex;gap:9px;align-items:stretch}
|
| 54 |
.fstep{flex:1;border:1px solid var(--line);border-radius:11px;padding:13px 12px;position:relative;text-align:center;background:#fbfcfe}
|
54 |
.fstep{flex:1;border:1px solid var(--line);border-radius:11px;padding:13px 12px;position:relative;text-align:center;background:#fbfcfe}
|
| 55 |
.fstep.clickable{cursor:pointer;transition:border-color .12s,box-shadow .12s,transform .12s}
|
55 |
.fstep.clickable{cursor:pointer;transition:border-color .12s,box-shadow .12s,transform .12s}
|
| 56 |
.fstep.clickable:hover{border-color:#c7d0e8;box-shadow:0 3px 10px rgba(20,30,60,.09);transform:translateY(-1px)}
|
56 |
.fstep.clickable:hover{border-color:#c7d0e8;box-shadow:0 3px 10px rgba(20,30,60,.09);transform:translateY(-1px)}
|
| - |
|
57 |
.fstep.active{border-color:var(--brand);background:#eef1fb;box-shadow:0 0 0 1px var(--brand) inset}
|
| - |
|
58 |
.boardfilter{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted);margin:0 0 10px;padding:7px 11px;background:#eef1fb;border:1px solid #d7deef;border-radius:8px}
|
| - |
|
59 |
.boardfilter b{color:#28304a}
|
| - |
|
60 |
.boardfilter .clearf{margin-left:auto;border:1px solid #c7d0e8;background:#fff;color:#3b5bdb;border-radius:7px;padding:3px 10px;font-size:12px;font-weight:600;cursor:pointer}
|
| - |
|
61 |
.boardfilter .clearf:hover{background:#f1f4ff}
|
| 57 |
.fstep .fn{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);font-weight:600}
|
62 |
.fstep .fn{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);font-weight:600}
|
| 58 |
.fstep .fv{font-size:26px;font-weight:770;margin-top:6px}
|
63 |
.fstep .fv{font-size:26px;font-weight:770;margin-top:6px}
|
| 59 |
.fstep .fbar{height:5px;border-radius:3px;margin-top:10px;background:linear-gradient(90deg,var(--brand),var(--brand2))}
|
64 |
.fstep .fbar{height:5px;border-radius:3px;margin-top:10px;background:linear-gradient(90deg,var(--brand),var(--brand2))}
|
| 60 |
.fstep .drop{font-size:11px;color:var(--red);margin-top:7px;font-weight:600}
|
65 |
.fstep .drop{font-size:11px;color:var(--red);margin-top:7px;font-weight:600}
|
| 61 |
.fstep .keep{font-size:11px;color:var(--good);margin-top:7px;font-weight:600}
|
66 |
.fstep .keep{font-size:11px;color:var(--good);margin-top:7px;font-weight:600}
|
| Line 90... |
Line 95... |
| 90 |
.bdot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px;vertical-align:middle}
|
95 |
.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}
|
96 |
.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}
|
97 |
.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}
|
| 93 |
td.cnt{font-weight:700;font-variant-numeric:tabular-nums}
|
98 |
td.cnt{font-weight:700;font-variant-numeric:tabular-nums}
|
| 94 |
.cgreen{color:#2f9e44}.camber{color:#f08c00}.cred{color:#e03131}.cmuted{color:#69748c}
|
99 |
.cgreen{color:#2f9e44}.camber{color:#f08c00}.cred{color:#e03131}.cmuted{color:#69748c}
|
| - |
|
100 |
tr.boardtot td{background:#f4f6fb;border-top:2px solid #cdd3df;font-weight:700}
|
| 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}
|
101 |
.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}
|
| 96 |
.actbtn.map{background:#12b886}.actbtn.view{background:#3b5bdb}
|
102 |
.actbtn.map{background:#12b886}.actbtn.view{background:#3b5bdb}
|
| 97 |
.actbtn.edit{background:#f08c00}.actbtn.add{background:#2f9e44;font-size:15px}
|
103 |
.actbtn.edit{background:#f08c00}.actbtn.add{background:#2f9e44;font-size:15px}
|
| 98 |
.actbtn:active{transform:translateY(1px)}
|
104 |
.actbtn:active{transform:translateY(1px)}
|
| 99 |
.att{display:flex;align-items:center;gap:12px;padding:11px 12px;border:1px solid var(--line);border-radius:11px;margin-bottom:9px}
|
105 |
.att{display:flex;align-items:center;gap:12px;padding:11px 12px;border:1px solid var(--line);border-radius:11px;margin-bottom:9px}
|
| Line 337... |
Line 343... |
| 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>'+
|
343 |
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>'+
|
| 338 |
'<div class="label">'+label+'</div></div><div class="val"'+(valColor?' style="color:'+valColor+'"':'')+'>'+val+'</div>'+
|
344 |
'<div class="label">'+label+'</div></div><div class="val"'+(valColor?' style="color:'+valColor+'"':'')+'>'+val+'</div>'+
|
| 339 |
'<div class="foot">'+foot+'</div></div>';
|
345 |
'<div class="foot">'+foot+'</div></div>';
|
| 340 |
}
|
346 |
}
|
| 341 |
function fstep(name, val, w, sub, drop, last, key){
|
347 |
function fstep(name, val, w, sub, drop, last, key){
|
| 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>'+
|
348 |
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>'+
|
| 343 |
'<div class="fbar" style="width:'+Math.min(w,100)+'%"></div><div class="'+(drop?'drop':'keep')+'">'+sub+'</div>'+
|
349 |
'<div class="fbar" style="width:'+Math.min(w,100)+'%"></div><div class="'+(drop?'drop':'keep')+'">'+sub+'</div>'+
|
| 344 |
(last?'':'<span class="arrow">➤</span>')+'</div>';
|
350 |
(last?'':'<span class="arrow">➤</span>')+'</div>';
|
| 345 |
}
|
351 |
}
|
| 346 |
// Funnel drop sub-text: positive = fell out (red); negative = above planned
|
352 |
// Funnel drop sub-text: positive = fell out (red); negative = above planned
|
| 347 |
// (leads push visits past planned stops) shown green; zero = on plan.
|
353 |
// (leads push visits past planned stops) shown green; zero = on plan.
|
| Line 376... |
Line 382... |
| 376 |
h+=fstep('Made a check-in',f.madeCheckin,pct(f.madeCheckin,sc),'−'+(f.punchedIn-f.madeCheckin)+' punched, idle',true,false,'madeCheckin');
|
382 |
h+=fstep('Made a check-in',f.madeCheckin,pct(f.madeCheckin,sc),'−'+(f.punchedIn-f.madeCheckin)+' punched, idle',true,false,'madeCheckin');
|
| 377 |
h+=fstep('Completed all visits',f.completedAll,pct(f.completedAll,sc),(f.madeCheckin-f.completedAll)+' still mid-journey',true,false,'completedAll');
|
383 |
h+=fstep('Completed all visits',f.completedAll,pct(f.completedAll,sc),(f.madeCheckin-f.completedAll)+' still mid-journey',true,false,'completedAll');
|
| 378 |
h+=fstep('Punched out',f.punchedOut,pct(f.punchedOut,sc),'journey ended',false,true,'punchedOut');
|
384 |
h+=fstep('Punched out',f.punchedOut,pct(f.punchedOut,sc),'journey ended',false,true,'punchedOut');
|
| 379 |
h+='</div></div>';
|
385 |
h+='</div></div>';
|
| 380 |
|
386 |
|
| - |
|
387 |
window._boardFilter = null; // funnel-card filter (null = all rows)
|
| 381 |
h+='<div class="grid g3">';
|
388 |
h+='<div class="grid g3">';
|
| 382 |
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>'+boardTable(d.board, d.date)+'</div>';
|
389 |
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>';
|
| 383 |
h+='<div class="card"><h2>Needs attention now</h2><p class="desc">Things a manager should clear today.</p>'+attentionList(a)+'</div>';
|
390 |
h+='<div class="card"><h2>Needs attention now</h2><p class="desc">Things a manager should clear today.</p>'+attentionList(a)+'</div>';
|
| 384 |
h+='</div>';
|
391 |
h+='</div>';
|
| 385 |
el('bj-content').innerHTML=h;
|
392 |
el('bj-content').innerHTML=h;
|
| 386 |
}
|
393 |
}
|
| 387 |
function stateClass(st){ return st==='IN_STORE'?'ps-store':st==='TRAVELLING'?'ps-travel':st==='DONE'?'ps-done':st==='NOT_STARTED'?'ps-no':'ps-idle'; }
|
394 |
function stateClass(st){ return st==='IN_STORE'?'ps-store':st==='TRAVELLING'?'ps-travel':st==='DONE'?'ps-done':st==='NOT_STARTED'?'ps-no':'ps-idle'; }
|
| Line 392... |
Line 399... |
| 392 |
var u = CTX + '/beatPlanWindow?autoUserId=' + encodeURIComponent(authUserId) + '&editDate=' + encodeURIComponent(date);
|
399 |
var u = CTX + '/beatPlanWindow?autoUserId=' + encodeURIComponent(authUserId) + '&editDate=' + encodeURIComponent(date);
|
| 393 |
if(beatId && beatId>0) u += '&editBeatId=' + encodeURIComponent(beatId);
|
400 |
if(beatId && beatId>0) u += '&editBeatId=' + encodeURIComponent(beatId);
|
| 394 |
if(mode==='add') u += '&assign=1';
|
401 |
if(mode==='add') u += '&assign=1';
|
| 395 |
window.open(u, '_blank');
|
402 |
window.open(u, '_blank');
|
| 396 |
}
|
403 |
}
|
| - |
|
404 |
// Compact "completed − deferred − pending / planned" cell: completed green,
|
| - |
|
405 |
// deferred amber, pending grey (planned stops not yet acted on). Denominator is
|
| - |
|
406 |
// the plan (or activity total if it ran past the plan), so the parts reconcile.
|
| - |
|
407 |
// Pending is shown only when there's a remainder.
|
| - |
|
408 |
function cdp(completed, deferred, planned){
|
| - |
|
409 |
completed=completed||0; deferred=deferred||0; planned=planned||0;
|
| - |
|
410 |
var denom=Math.max(planned, completed+deferred);
|
| - |
|
411 |
var pending=denom-completed-deferred;
|
| - |
|
412 |
var s='<span class="cgreen">'+completed+'</span> <span style="color:#cdd3df">-</span> '+
|
| - |
|
413 |
'<span class="camber">'+deferred+'</span>';
|
| - |
|
414 |
if(pending>0) s+=' <span style="color:#cdd3df">-</span> <span class="cmuted">'+pending+'</span>';
|
| - |
|
415 |
s+=' <span style="color:#97a1b5;font-weight:400">/ '+denom+'</span>';
|
| - |
|
416 |
return s;
|
| - |
|
417 |
}
|
| - |
|
418 |
// Funnel-card click: period cards (p_*) open their drill-down popup; today's
|
| - |
|
419 |
// funnel cards filter the Live executive board in place. Clicking the active
|
| - |
|
420 |
// card again clears the filter.
|
| - |
|
421 |
function funnelClick(key){
|
| - |
|
422 |
if(key && key.indexOf('p_')===0) return openFunnel(key);
|
| - |
|
423 |
setBoardFilter(window._boardFilter===key ? null : key);
|
| - |
|
424 |
}
|
| - |
|
425 |
// Apply / clear the funnel filter and re-render just the board card. Also marks
|
| - |
|
426 |
// the active funnel card so it's clear which cohort the board is showing.
|
| - |
|
427 |
function setBoardFilter(key){
|
| - |
|
428 |
window._boardFilter = key || null;
|
| - |
|
429 |
var c=el('bj-board'); if(c) c.innerHTML=boardCard();
|
| - |
|
430 |
document.querySelectorAll('.fstep[data-fkey]').forEach(function(s){
|
| - |
|
431 |
s.classList.toggle('active', !!key && s.getAttribute('data-fkey')===key);
|
| - |
|
432 |
});
|
| - |
|
433 |
}
|
| - |
|
434 |
// Board card content: an optional "filtered by …" bar plus the table, showing
|
| - |
|
435 |
// only the rows that reached the selected funnel stage (Scheduled = all rows).
|
| - |
|
436 |
function boardCard(){
|
| - |
|
437 |
var rows=window._board||[], date=window._boardDate||'', key=window._boardFilter;
|
| - |
|
438 |
var view=rows, m=key?FUNNEL_META[key]:null;
|
| - |
|
439 |
if(m) view=rows.filter(m.f);
|
| - |
|
440 |
var bar='';
|
| - |
|
441 |
if(m){
|
| - |
|
442 |
bar='<div class="boardfilter">Showing <b>'+esc(m.title)+'</b> · '+view.length+
|
| - |
|
443 |
' of '+rows.length+'<button class="clearf" onclick="setBoardFilter(null)">× Clear filter</button></div>';
|
| - |
|
444 |
}
|
| - |
|
445 |
return bar+boardTable(view, date);
|
| - |
|
446 |
}
|
| 397 |
function boardTable(rows, date){
|
447 |
function boardTable(rows, date){
|
| 398 |
if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
|
448 |
if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
|
| 399 |
var h='<div style="overflow-x:auto"><table><thead><tr>'+
|
449 |
var h='<div style="overflow-x:auto"><table><thead><tr>'+
|
| 400 |
'<th>Agenda</th><th>Executive</th><th>Punch In/Out</th><th class="num">Planned</th>'+
|
450 |
'<th>Agenda</th><th>Executive</th><th>Punch In/Out</th>'+
|
| 401 |
'<th class="num">Completed</th><th class="num">Pending</th><th class="num">Deferred</th><th class="num">Total</th><th>State</th><th>Action</th>'+
|
451 |
'<th class="num" title="Completed − Deferred − Pending / Planned">Planned - Visits</th>'+
|
| - |
|
452 |
'<th class="num" title="Completed − Deferred − Pending / Planned">Self Assigned Lead</th>'+
|
| - |
|
453 |
'<th class="num">Total</th><th>State</th><th>Action</th>'+
|
| 402 |
'</tr></thead><tbody>';
|
454 |
'</tr></thead><tbody>';
|
| - |
|
455 |
var t={cp:0,dp:0,pp:0,cl:0,dl:0,pl:0,tot:0}; // column totals
|
| 403 |
rows.forEach(function(r){
|
456 |
rows.forEach(function(r){
|
| - |
|
457 |
t.cp+=r.completedPartner||0; t.dp+=r.deferredPartner||0; t.pp+=r.plannedPartner||0;
|
| - |
|
458 |
t.cl+=r.completedLead||0; t.dl+=r.deferredLead||0; t.pl+=r.plannedLead||0;
|
| - |
|
459 |
t.tot+=r.totalVisits||0;
|
| 404 |
var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (r.idleAway?'rowwarn':'');
|
460 |
var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (r.idleAway?'rowwarn':'');
|
| 405 |
var A = r.userId+",'"+js(r.name)+"','"+date+"'"; // openExec args
|
461 |
var A = r.userId+",'"+js(r.name)+"','"+date+"'"; // openExec args
|
| 406 |
var go = 'onclick="openExec('+A+')"';
|
462 |
var go = 'onclick="openExec('+A+')"';
|
| 407 |
h+='<tr class="clickable '+rowcls+'" '+go+'>'+
|
463 |
h+='<tr class="clickable '+rowcls+'" '+go+'>'+
|
| 408 |
'<td style="text-align:center">'+(r.agendaFilled?'<span style="color:#2f9e44;font-size:15px">✓</span>':'<span class="badflag" style="font-size:15px">✗</span>')+'</td>'+
|
464 |
'<td style="text-align:center">'+(r.agendaFilled?'<span style="color:#2f9e44;font-size:15px">✓</span>':'<span class="badflag" style="font-size:15px">✗</span>')+'</td>'+
|
| 409 |
'<td><b>'+esc(r.name)+'</b><br>'+esc(r.beatName||'-')+'</td>'+
|
465 |
'<td><b>'+esc(r.name)+'</b><br>'+esc(r.beatName||'-')+'</td>'+
|
| 410 |
'<td style="white-space:nowrap">'+(r.punchInTime?'<span class="ptime">'+esc(r.punchInTime)+'</span>':'<span class="badflag">-</span>')+
|
466 |
'<td style="white-space:nowrap">'+(r.punchInTime?'<span class="ptime">'+esc(r.punchInTime)+'</span>':'<span class="badflag">-</span>')+
|
| 411 |
' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
|
467 |
' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
|
| 412 |
'<td class="num">'+r.planned+'</td>'+
|
- |
|
| 413 |
'<td class="num cnt cgreen">'+r.completedVisits+'</td>'+
|
468 |
'<td class="num cnt">'+cdp(r.completedPartner,r.deferredPartner,r.plannedPartner)+'</td>'+
|
| 414 |
'<td class="num cnt camber">'+r.pendingVisits+'</td>'+
|
- |
|
| 415 |
'<td class="num cnt cred">'+r.deferred+'</td>'+
|
469 |
'<td class="num cnt">'+cdp(r.completedLead,r.deferredLead,r.plannedLead)+'</td>'+
|
| 416 |
'<td class="num cnt cmuted">'+r.totalVisits+'</td>'+
|
470 |
'<td class="num cnt cmuted">'+r.totalVisits+'</td>'+
|
| 417 |
'<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
|
471 |
'<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
|
| 418 |
'<td style="white-space:nowrap" onclick="event.stopPropagation()">'+
|
472 |
'<td style="white-space:nowrap" onclick="event.stopPropagation()">'+
|
| 419 |
'<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">📍</button>'+
|
473 |
'<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">📍</button>'+
|
| 420 |
'<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">👁</button>'+
|
474 |
'<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">👁</button>'+
|
| 421 |
'<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">✎</button>'+
|
475 |
'<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">✎</button>'+
|
| 422 |
'<button class="actbtn add" title="Assign / add visit in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'add\')">+</button>'+
|
476 |
'<button class="actbtn add" title="Assign / add visit in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'add\')">+</button>'+
|
| 423 |
'</td></tr>';
|
477 |
'</td></tr>';
|
| 424 |
});
|
478 |
});
|
| - |
|
479 |
h+='</tbody><tfoot><tr class="boardtot">'+
|
| - |
|
480 |
'<td></td><td><b>Total</b><br><span style="color:#97a1b5;font-size:11px">'+rows.length+' executives</span></td><td></td>'+
|
| - |
|
481 |
'<td class="num cnt">'+cdp(t.cp,t.dp,t.pp)+'</td>'+
|
| - |
|
482 |
'<td class="num cnt">'+cdp(t.cl,t.dl,t.pl)+'</td>'+
|
| - |
|
483 |
'<td class="num cnt cmuted">'+t.tot+'</td><td></td><td></td>'+
|
| - |
|
484 |
'</tr></tfoot>';
|
| 425 |
return h+'</tbody></table></div>';
|
485 |
return h+'</table></div>';
|
| 426 |
}
|
486 |
}
|
| 427 |
// Funnel drill-down: each stage shows the executives who reached it AND the
|
487 |
// Funnel drill-down: each stage shows the executives who reached it AND the
|
| 428 |
// drop-off cohort that fell out at this step (the "−N …" sub-line). Filters
|
488 |
// drop-off cohort that fell out at this step (the "−N …" sub-line). Filters
|
| 429 |
// mirror the backend funnel conditions exactly (rows carry the per-stage flags).
|
489 |
// mirror the backend funnel conditions exactly (rows carry the per-stage flags).
|
| 430 |
var FUNNEL_META = {
|
490 |
var FUNNEL_META = {
|