Subversion Repositories SmartDukaan

Rev

Rev 36905 | Rev 36916 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 36905 Rev 36913
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">&#10148;</span>')+'</div>';
344
    (last?'':'<span class="arrow">&#10148;</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 '&minus;'+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()">&times;</button><h3>'+esc(title)+'</h3>'+
-
 
505
    '<p class="desc">'+esc(desc)+' &middot; '+esc(pRange())+' &middot; 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&#9888;</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()">&times;</button><h3>'+esc(name)+'</h3>'+
-
 
534
    '<p class="desc">Day-by-day &middot; '+esc(pRange())+'</p><div class="loading"><div class="spinner"></div>Loading&hellip;</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()">&times;</button><h3>'+esc(nm)+'</h3>'+
-
 
540
        '<p class="desc">'+rows.length+' day'+(rows.length===1?'':'s')+' &middot; '+esc(pRange())+(uid?' &middot; 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()">&times;</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()">&times;</button><h3>PJP coverage'+(level?' &middot; '+esc(level):'')+'</h3>'+
-
 
570
    '<p class="desc">'+withP.length+' of '+list.length+' have a monthly plan &middot; '+esc(pRange())+'</p>'+
-
 
571
    '<div class="sectionlabel" style="color:#e03131">No plan &middot; '+without.length+'</div>'+tbl(without)+
-
 
572
    '<div class="sectionlabel" style="color:#2f9e44;margin-top:16px">Has a plan &middot; '+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()">&times;</button><h3>'+esc(title)+'</h3>'+
-
 
581
        '<p class="desc">'+list.length+' deferral'+(list.length===1?'':'s')+' &middot; '+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()">&times;</button><h3>Leads on route</h3>'+
-
 
600
        '<p class="desc">'+list.length+' lead'+(list.length===1?'':'s')+' created &middot; '+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:'&#9888;',   title:'Not punched in',                  sub:'executives with a beat, not started', cls:'badflag'},
615
  notPunchedIn:    {bg:'#fff0f0', fg:'#e03131', ic:'&#9888;',   title:'Not punched in',                  sub:'executives with a beat, not started', cls:'badflag'},
484
  idleStale:       {bg:'#fff4e6', fg:'#e8590c', ic:'&#9201;',   title:'Idle &mdash; off-store &gt; 40 min', sub:'stationary &gt;20 m from any store',  cls:'warnflag'},
616
  idleStale:       {bg:'#fff4e6', fg:'#e8590c', ic:'&#9201;',   title:'Idle &mdash; off-store &gt; 40 min', sub:'stationary &gt;20 m from any store',  cls:'warnflag'},
485
  geofence:        {bg:'#fff0f0', fg:'#e03131', ic:'&#128205;', title:'Check-in outside store geofence',  sub:'&gt;50 m from partner\'s saved location', cls:'badflag'},
617
  geofence:        {bg:'#fff0f0', fg:'#e03131', ic:'&#128205;', title:'Check-in outside store geofence',  sub:'&gt;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('&#9878;','#fff4e6','#f08c00','Adherence',k.adherencePct+'<small>%</small>',num(k.doneStops)+' of '+num(k.plannedStops)+' stops done',adhColor(k.adherencePct));
700
  h+=kpi('&#9878;','#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('&#128506;','#e7f5ff','#1098ad','PJP coverage L1',k.coverageL1Pct+'<small>%</small>',k.coverageL1With+'/'+k.coverageL1Total+' have a plan','#1098ad');
701
  h+=kpi('&#128506;','#e7f5ff','#1098ad','PJP coverage L1',k.coverageL1Pct+'<small>%</small>',k.coverageL1With+'/'+k.coverageL1Total+' have a plan','#1098ad',"openCoverage('L1')");
569
  h+=kpi('&#128694;','#ebfbee','#2f9e44','Visits / active day',k.avgVisitsPerActiveDay,'across '+num(k.journeys)+' journeys');
702
  h+=kpi('&#128694;','#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('&#128172;','#eef1fb','#3b5bdb','Discussion / store',k.avgDiscussionMin+' <small>min</small>','target 30&ndash;45 min', k.avgDiscussionMin>0&&k.avgDiscussionMin<30?'#e8590c':null);
703
  h+=kpi('&#128172;','#eef1fb','#3b5bdb','Discussion / store',k.avgDiscussionMin+' <small>min</small>','target 30&ndash;45 min', k.avgDiscussionMin>0&&k.avgDiscussionMin<30?'#e8590c':null,"openPScore('discussionMin','Discussion / store','Avg in-store discussion per executive')");
571
  h+=kpi('&#8635;','#fff0f0','#e03131','Auto "missed"',num(k.autoMissed),k.autoMissedSystemPct+'% system-flagged','#e03131');
704
  h+=kpi('&#8635;','#fff0f0','#e03131','Auto "missed"',num(k.autoMissed),k.autoMissedSystemPct+'% system-flagged','#e03131',"openDeferrals()");
572
  h+=kpi('&#9733;','#fff0f6','#d6336c','Leads on route',num(k.leadsCreated),'this period');
705
  h+=kpi('&#9733;','#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),'&minus;'+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),'&minus;'+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),'&minus;'+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 &mdash; genuine',df.human,'#2f9e44');
-
 
583
  h+=leakbox('Deferred &mdash; 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 &amp; Coverage</div><div class="grid g3">';
716
  h+='<div class="sectionlabel">Adherence &amp; 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">&middot; '+d.scorecard.length+' executives</span></h2><p class="desc">The coaching list &mdash; 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">&middot; '+d.scorecard.length+' executives</span></h2><p class="desc">The coaching list &mdash; 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&#9888;</th></tr></thead><tbody>';
757
    '<th class="num" onclick="sortCard(\'geoFlags\')">Geo&#9888;</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+' &middot; '+r.coveragePct+'%</div></div>';
777
       '<div class="meta">'+r.withPjp+'/'+r.totalUsers+' &middot; '+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')); }); });