Subversion Repositories SmartDukaan

Rev

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

Rev 36929 Rev 36941
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">&#10148;</span>')+'</div>';
390
    (last?'':'<span class="arrow">&#10148;</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">&#10148;</span>')+'</div>';
408
    (last?'':'<span class="arrow">&#10148;</span>')+'</div>';
Line 423... Line 430...
423
  h+=kpi('&#128197;','#ebfbee','#2f9e44','Scheduled PJP',num(k.scheduledPjp)+' <small>/'+k.totalExecs+'</small>',k.unscheduled+' unscheduled',null,"openCards('scheduled')");
430
  h+=kpi('&#128197;','#ebfbee','#2f9e44','Scheduled PJP',num(k.scheduledPjp)+' <small>/'+k.totalExecs+'</small>',k.unscheduled+' unscheduled',null,"openCards('scheduled')");
424
  h+=kpi('&#9998;','#fff4e6','#e8590c','Agenda Filled',num(k.agendaFilled)+' <small>/'+k.scheduledPjp+'</small>',(k.scheduledPjp-k.agendaFilled)+' pending',null,"openCards('agenda')");
431
  h+=kpi('&#9998;','#fff4e6','#e8590c','Agenda Filled',num(k.agendaFilled)+' <small>/'+k.scheduledPjp+'</small>',(k.scheduledPjp-k.agendaFilled)+' pending',null,"openCards('agenda')");
425
  h+=kpi('&#10060;','#fff0f6','#d6336c','Unscheduled',num(k.unscheduled),'no PJP today',null,"openCards('unscheduled')");
432
  h+=kpi('&#10060;','#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 &gt; 40 min',true,false,'idle');
442
  h+=fstep('Idle / Break',f.idle,pct(f.idle,pin),'off-store &gt; 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> &middot; '+view.length+
484
    bar='<div class="boardfilter">Showing <b>'+esc(m.title)+'</b> &middot; '+view.length+
493
        ' of '+rows.length+'<button class="clearf" onclick="setBoardFilter(null)">&times; Clear filter</button></div>';
485
        ' of '+rows.length+'<button class="clearf" onclick="setBoardFilter(null)">&times; 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">&#10003;</span>':'<span class="badflag" style="font-size:15px">&#10007;</span>')+'</td>'+
524
       '<td style="text-align:center">'+(r.agendaFilled?'<span style="color:#2f9e44;font-size:15px">&#10003;</span>':'<span class="badflag" style="font-size:15px">&#10007;</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\')">&#128205;</button>'+
534
         '<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">&#128205;</button>'+
533
         '<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">&#128065;</button>'+
535
         '<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">&#128065;</button>'+
534
         '<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">&#9998;</button>'+
536
         '<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">&#9998;</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:'&#9888;',   title:'Not punched in',                  sub:'executives with a beat, not started', cls:'badflag'},
791
  notPunchedIn:    {bg:'#fff0f0', fg:'#e03131', ic:'&#9888;',   title:'Not punched in',                  sub:'executives with a beat, not started', cls:'badflag'},
790
  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'},
792
  idleStale:       {bg:'#fff4e6', fg:'#e8590c', ic:'&#9201;',   title:'Idle &mdash; off-store &gt; 59 min', sub:'stationary &gt;20 m from any store',  cls:'warnflag'},
791
  geofence:        {bg:'#fff0f0', fg:'#e03131', ic:'&#128205;', title:'Check-in outside store geofence',  sub:'&gt;50 m from partner\'s saved location', cls:'badflag'},
793
  geofence:        {bg:'#fff0f0', fg:'#e03131', ic:'&#128205;', title:'Check-in outside store geofence',  sub:'&gt;50 m from partner\'s saved location', cls:'badflag'},
792
  locationOff:     {bg:'#fff4e6', fg:'#e8590c', ic:'&#128244;', title:'Location turned off mid-journey',  sub:'GPS toggled off',                     cls:'warnflag'},
794
  locationOff:     {bg:'#fff4e6', fg:'#e8590c', ic:'&#128244;', title:'Location turned off mid-journey',  sub:'GPS toggled off',                     cls:'warnflag'},
793
  deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'&#8635;',   title:'Deferred, awaiting re-action',     sub:'shops deferred today',                cls:''}
795
  deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'&#8635;',   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">&#9656;</span>':'')+'</div>';
808
       '<div class="acount '+(n?m.cls:'')+'">'+n+'</div>'+(n?'<span class="chev">&#9656;</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()">&times;</button><h3>'+m.title+'</h3>'+
817
  var h='<button class="x" onclick="closeModal()">&times;</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?' &middot; approving updates the partner\'s saved coordinates':'')+'</p>';
819
        (showApprove?' &middot; 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 &amp; 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 &amp; 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('&#9878;','#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('&#9878;','#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('&#128506;','#e7f5ff','#1098ad','PJP coverage L1',k.coverageL1Pct+'<small>%</small>',k.coverageL1With+'/'+k.coverageL1Total+' have a plan','#1098ad',"openCoverage('L1')");
881
  h+=kpi('&#128506;','#e7f5ff','#1098ad','PJP coverage L1',k.coverageL1Pct+'<small>%</small>',k.coverageL1With+'/'+k.coverageL1Total+' have a plan','#1098ad',"openCoverage('L1')");
876
  h+=kpiCols('&#128694;','#ebfbee','#2f9e44','Average Visit (per day)',
882
  h+=kpiCols('&#128694;','#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('&#128172;','#eef1fb','#3b5bdb','Average discussion (per executive)',
885
  h+=kpiCols('&#128172;','#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('&#8635;','#fff0f0','#e03131','Auto "missed"',
888
  h+=kpiCols('&#8635;','#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('&#9733;','#fff0f6','#d6336c','Leads on route',num(k.leadsCreated),'this period',null,"openPLeads()");
891
  h+=kpi('&#9733;','#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 &middot; 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 &amp; Coverage</div><div class="grid g3">';
896
  h+='<div class="sectionlabel">Adherence &amp; 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">&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>';
897
  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>';
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>';