Subversion Repositories SmartDukaan

Rev

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

Rev 36903 Rev 36905
Line 33... Line 33...
33
  .lens{padding:11px 4px;margin-right:22px;font-size:14px;font-weight:650;color:var(--muted);cursor:pointer;border:0;background:transparent;border-bottom:2.5px solid transparent;margin-bottom:-1px}
33
  .lens{padding:11px 4px;margin-right:22px;font-size:14px;font-weight:650;color:var(--muted);cursor:pointer;border:0;background:transparent;border-bottom:2.5px solid transparent;margin-bottom:-1px}
34
  .lens.active{color:var(--brand);border-bottom-color:var(--brand)}
34
  .lens.active{color:var(--brand);border-bottom-color:var(--brand)}
35
  .lens .b{display:inline-block;background:#eef1fb;color:var(--brand);font-size:11px;font-weight:700;border-radius:999px;padding:1px 7px;margin-left:6px}
35
  .lens .b{display:inline-block;background:#eef1fb;color:var(--brand);font-size:11px;font-weight:700;border-radius:999px;padding:1px 7px;margin-left:6px}
36
  .kpis{display:grid;grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:20px}
36
  .kpis{display:grid;grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:20px}
37
  .kpi{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:14px 15px;box-shadow:0 1px 2px rgba(20,30,60,.04)}
37
  .kpi{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:14px 15px;box-shadow:0 1px 2px rgba(20,30,60,.04)}
-
 
38
  .kpi.clickable{cursor:pointer;transition:border-color .12s,box-shadow .12s,transform .12s}
-
 
39
  .kpi.clickable:hover{border-color:#c7d0e8;box-shadow:0 3px 10px rgba(20,30,60,.09);transform:translateY(-1px)}
38
  .kpi .top{display:flex;align-items:center;gap:8px}
40
  .kpi .top{display:flex;align-items:center;gap:8px}
39
  .kpi .ic{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;flex:none}
41
  .kpi .ic{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;flex:none}
40
  .kpi .label{font-size:11.5px;color:var(--muted);font-weight:600;line-height:1.2}
42
  .kpi .label{font-size:11.5px;color:var(--muted);font-weight:600;line-height:1.2}
41
  .kpi .val{font-size:26px;font-weight:760;margin-top:9px;letter-spacing:-.02em;line-height:1}
43
  .kpi .val{font-size:26px;font-weight:760;margin-top:9px;letter-spacing:-.02em;line-height:1}
42
  .kpi .val small{font-size:13px;color:var(--soft);font-weight:600}
44
  .kpi .val small{font-size:13px;color:var(--soft);font-weight:600}
Line 48... Line 50...
48
  .card h2{margin:0 0 3px;font-size:15px;font-weight:720;letter-spacing:-.01em}
50
  .card h2{margin:0 0 3px;font-size:15px;font-weight:720;letter-spacing:-.01em}
49
  .card .desc{color:var(--muted);font-size:12.5px;margin:0 0 14px}
51
  .card .desc{color:var(--muted);font-size:12.5px;margin:0 0 14px}
50
  .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}
51
  .funnel{display:flex;gap:9px;align-items:stretch}
53
  .funnel{display:flex;gap:9px;align-items:stretch}
52
  .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}
-
 
56
  .fstep.clickable:hover{border-color:#c7d0e8;box-shadow:0 3px 10px rgba(20,30,60,.09);transform:translateY(-1px)}
53
  .fstep .fn{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);font-weight:600}
57
  .fstep .fn{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);font-weight:600}
54
  .fstep .fv{font-size:26px;font-weight:770;margin-top:6px}
58
  .fstep .fv{font-size:26px;font-weight:770;margin-top:6px}
55
  .fstep .fbar{height:5px;border-radius:3px;margin-top:10px;background:linear-gradient(90deg,var(--brand),var(--brand2))}
59
  .fstep .fbar{height:5px;border-radius:3px;margin-top:10px;background:linear-gradient(90deg,var(--brand),var(--brand2))}
56
  .fstep .drop{font-size:11px;color:var(--red);margin-top:7px;font-weight:600}
60
  .fstep .drop{font-size:11px;color:var(--red);margin-top:7px;font-weight:600}
57
  .fstep .keep{font-size:11px;color:var(--good);margin-top:7px;font-weight:600}
61
  .fstep .keep{font-size:11px;color:var(--good);margin-top:7px;font-weight:600}
Line 321... Line 325...
321
  spinner();
325
  spinner();
322
  fetch(BASE + '/today?date=' + encodeURIComponent(el('bj-date').value) + qs(), {headers:{'Accept':'application/json'}})
326
  fetch(BASE + '/today?date=' + encodeURIComponent(el('bj-date').value) + qs(), {headers:{'Accept':'application/json'}})
323
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
327
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
324
    .then(renderToday).catch(fail);
328
    .then(renderToday).catch(fail);
325
}
329
}
326
function kpi(ic, bg, fg, label, val, foot, valColor){
330
function kpi(ic, bg, fg, label, val, foot, valColor, click){
327
  return '<div class="kpi"><div class="top"><div class="ic" style="background:'+bg+';color:'+fg+'">'+ic+'</div>'+
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>'+
328
    '<div class="label">'+label+'</div></div><div class="val"'+(valColor?' style="color:'+valColor+'"':'')+'>'+val+'</div>'+
332
    '<div class="label">'+label+'</div></div><div class="val"'+(valColor?' style="color:'+valColor+'"':'')+'>'+val+'</div>'+
329
    '<div class="foot">'+foot+'</div></div>';
333
    '<div class="foot">'+foot+'</div></div>';
330
}
334
}
331
function fstep(name, val, w, sub, drop, last){
335
function fstep(name, val, w, sub, drop, last, key){
332
  return '<div class="fstep"><div class="fn">'+name+'</div><div class="fv">'+num(val)+'</div>'+
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>'+
333
    '<div class="fbar" style="width:'+w+'%"></div><div class="'+(drop?'drop':'keep')+'">'+sub+'</div>'+
337
    '<div class="fbar" style="width:'+w+'%"></div><div class="'+(drop?'drop':'keep')+'">'+sub+'</div>'+
334
    (last?'':'<span class="arrow">&#10148;</span>')+'</div>';
338
    (last?'':'<span class="arrow">&#10148;</span>')+'</div>';
335
}
339
}
336
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>'; }
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>'; }
337
 
341
 
338
function renderToday(d){
342
function renderToday(d){
339
  var k=d.kpis, f=d.funnel, s=d.stops, a=d.attention;
343
  var k=d.kpis, f=d.funnel, s=d.stops, a=d.attention;
340
  window._canApprove = !!d.canApprove;
344
  window._canApprove = !!d.canApprove;
341
  window._mediaUrl = d.mediaUrl || window._mediaUrl || '';
345
  window._mediaUrl = d.mediaUrl || window._mediaUrl || '';
-
 
346
  window._board = d.board || [];          // funnel drill-down filters this
-
 
347
  window._boardDate = d.date || '';
-
 
348
  window._leadsList = d.leadsList || [];  // leads drill-down
342
  var h='';
349
  var h='';
343
  h+='<div class="kpis">';
350
  h+='<div class="kpis">';
344
  h+=kpi('&#128101;','#eef1fb','#3b5bdb','Executives scheduled',num(k.scheduled),k.notStarted+' not started yet');
351
  h+=kpi('&#128101;','#eef1fb','#3b5bdb','Executives scheduled',num(k.scheduled),k.notStarted+' not started yet');
345
  h+=kpi('&#9201;','#ebfbee','#2f9e44','Punched in',num(k.punchedIn)+' <small>/'+k.scheduled+'</small>',k.notStarted+' pending');
352
  h+=kpi('&#9201;','#ebfbee','#2f9e44','Punched in',num(k.punchedIn)+' <small>/'+k.scheduled+'</small>',k.notStarted+' pending');
346
  h+=kpi('&#128205;','#e7f5ff','#1098ad','On route now',num(k.onRouteNow),'live journeys');
353
  h+=kpi('&#128205;','#e7f5ff','#1098ad','On route now',num(k.onRouteNow),'live journeys');
347
  h+=kpi('&#10003;','#eef1fb','#3b5bdb','Visits done',num(k.visitsDone)+' <small>/'+k.visitsPlanned+'</small>',k.inProgress+' in progress');
354
  h+=kpi('&#10003;','#eef1fb','#3b5bdb','Visit vs Planned',num(k.visitsDone)+' <small>/'+k.visitsPlanned+'</small>',k.inProgress+' in progress');
348
  h+=kpi('&#8635;','#fff4e6','#e8590c','Deferred today',num(k.deferred),'shop closed / unavailable');
355
  h+=kpi('&#8635;','#fff4e6','#e8590c','Deferred today',num(k.deferred),'shop closed / unavailable');
349
  h+=kpi('&#9733;','#fff0f6','#d6336c','Leads on route',num(k.leads),'new shops logged');
356
  h+=kpi('&#9733;','#fff0f6','#d6336c','Leads on route',num(k.leads),'new shops logged',null,'openLeads()');
350
  h+='</div>';
357
  h+='</div>';
351
 
358
 
352
  h+='<div class="sectionlabel">Today\'s Journey Funnel</div><div class="card"><div class="funnel">';
359
  h+='<div class="sectionlabel">Today\'s Journey Funnel</div><div class="card"><div class="funnel">';
353
  var sc=f.scheduled||1;
360
  var sc=f.scheduled||1;
354
  h+=fstep('Scheduled executives',f.scheduled,100,'start of day',false,false);
361
  h+=fstep('Scheduled executives',f.scheduled,100,'start of day',false,false,'scheduled');
355
  h+=fstep('Punched in',f.punchedIn,pct(f.punchedIn,sc),'&minus;'+(f.scheduled-f.punchedIn)+' never started',true,false);
362
  h+=fstep('Punched in',f.punchedIn,pct(f.punchedIn,sc),'&minus;'+(f.scheduled-f.punchedIn)+' never started',true,false,'punchedIn');
356
  h+=fstep('Made a check-in',f.madeCheckin,pct(f.madeCheckin,sc),'&minus;'+(f.punchedIn-f.madeCheckin)+' punched, idle',true,false);
363
  h+=fstep('Made a check-in',f.madeCheckin,pct(f.madeCheckin,sc),'&minus;'+(f.punchedIn-f.madeCheckin)+' punched, idle',true,false,'madeCheckin');
357
  h+=fstep('Completed all visits',f.completedAll,pct(f.completedAll,sc),(f.madeCheckin-f.completedAll)+' still mid-journey',true,false);
364
  h+=fstep('Completed all visits',f.completedAll,pct(f.completedAll,sc),(f.madeCheckin-f.completedAll)+' still mid-journey',true,false,'completedAll');
358
  h+=fstep('Punched out',f.punchedOut,pct(f.punchedOut,sc),'journey ended',false,true);
365
  h+=fstep('Punched out',f.punchedOut,pct(f.punchedOut,sc),'journey ended',false,true,'punchedOut');
359
  h+='</div></div>';
366
  h+='</div></div>';
360
 
367
 
361
  h+='<div class="grid g3">';
368
  h+='<div class="grid g3">';
362
  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>';
369
  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>';
363
  h+='<div class="card"><h2>Needs attention now</h2><p class="desc">Things a manager should clear today.</p>'+attentionList(a)+'</div>';
370
  h+='<div class="card"><h2>Needs attention now</h2><p class="desc">Things a manager should clear today.</p>'+attentionList(a)+'</div>';
Line 375... Line 382...
375
  window.open(u, '_blank');
382
  window.open(u, '_blank');
376
}
383
}
377
function boardTable(rows, date){
384
function boardTable(rows, date){
378
  if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
385
  if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
379
  var h='<div style="overflow-x:auto"><table><thead><tr>'+
386
  var h='<div style="overflow-x:auto"><table><thead><tr>'+
380
    '<th>Agenda</th><th>Executive</th><th class="num">Planned</th>'+
387
    '<th>Agenda</th><th>Executive</th><th>Punch In/Out</th><th class="num">Planned</th>'+
381
    '<th>Punch In/Out</th><th>State</th><th class="num">Total</th><th class="num">Completed</th><th class="num">Pending</th><th class="num">Deferred</th><th>Action</th>'+
388
    '<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>'+
382
    '</tr></thead><tbody>';
389
    '</tr></thead><tbody>';
383
  rows.forEach(function(r){
390
  rows.forEach(function(r){
384
    var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (r.idleAway?'rowwarn':'');
391
    var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (r.idleAway?'rowwarn':'');
385
    var A = r.userId+",'"+js(r.name)+"','"+date+"'";   // openExec args
392
    var A = r.userId+",'"+js(r.name)+"','"+date+"'";   // openExec args
386
    var go = 'onclick="openExec('+A+')"';
393
    var go = 'onclick="openExec('+A+')"';
387
    h+='<tr class="clickable '+rowcls+'" '+go+'>'+
394
    h+='<tr class="clickable '+rowcls+'" '+go+'>'+
388
       '<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>'+
395
       '<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>'+
389
       '<td><b>'+esc(r.name)+'</b><br>'+esc(r.beatName||'-')+'</td>'+
396
       '<td><b>'+esc(r.name)+'</b><br>'+esc(r.beatName||'-')+'</td>'+
390
       '<td class="num">'+r.planned+'</td>'+
-
 
391
       '<td style="white-space:nowrap">'+(r.punchInTime?'<span class="ptime">'+esc(r.punchInTime)+'</span>':'<span class="badflag">-</span>')+
397
       '<td style="white-space:nowrap">'+(r.punchInTime?'<span class="ptime">'+esc(r.punchInTime)+'</span>':'<span class="badflag">-</span>')+
392
         ' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
398
         ' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
393
       '<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
-
 
394
       '<td class="num cnt cmuted">'+r.totalVisits+'</td>'+
399
       '<td class="num">'+r.planned+'</td>'+
395
       '<td class="num cnt cgreen">'+r.completedVisits+'</td>'+
400
       '<td class="num cnt cgreen">'+r.completedVisits+'</td>'+
396
       '<td class="num cnt camber">'+r.pendingVisits+'</td>'+
401
       '<td class="num cnt camber">'+r.pendingVisits+'</td>'+
397
       '<td class="num cnt cred">'+r.deferred+'</td>'+
402
       '<td class="num cnt cred">'+r.deferred+'</td>'+
-
 
403
       '<td class="num cnt cmuted">'+r.totalVisits+'</td>'+
-
 
404
       '<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
398
       '<td style="white-space:nowrap" onclick="event.stopPropagation()">'+
405
       '<td style="white-space:nowrap" onclick="event.stopPropagation()">'+
399
         '<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">&#128205;</button>'+
406
         '<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">&#128205;</button>'+
400
         '<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">&#128065;</button>'+
407
         '<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">&#128065;</button>'+
401
         '<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">&#9998;</button>'+
408
         '<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">&#9998;</button>'+
402
         '<button class="actbtn add" title="Assign / add visit in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'add\')">&#43;</button>'+
409
         '<button class="actbtn add" title="Assign / add visit in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'add\')">&#43;</button>'+
403
       '</td></tr>';
410
       '</td></tr>';
404
  });
411
  });
405
  return h+'</tbody></table></div>';
412
  return h+'</tbody></table></div>';
406
}
413
}
-
 
414
// Funnel drill-down: each stage shows the executives who reached it AND the
-
 
415
// drop-off cohort that fell out at this step (the "−N …" sub-line). Filters
-
 
416
// mirror the backend funnel conditions exactly (rows carry the per-stage flags).
-
 
417
var FUNNEL_META = {
-
 
418
  scheduled:    {title:'Scheduled executives', desc:'Have a beat today, or any activity', f:function(r){ return true; }},
-
 
419
  punchedIn:    {title:'Punched in',           desc:'Started the day (punch-in recorded)', f:function(r){ return r.punchedIn; },
-
 
420
                 dropLabel:'Never started (scheduled, no punch-in)', drop:function(r){ return !r.punchedIn; }},
-
 
421
  madeCheckin:  {title:'Made a check-in',      desc:'Checked in at one or more stops', f:function(r){ return r.madeCheckin; },
-
 
422
                 dropLabel:'Punched in but idle (no check-in)', drop:function(r){ return r.punchedIn && !r.madeCheckin; }},
-
 
423
  completedAll: {title:'Completed all visits', desc:'Finished every planned stop', f:function(r){ return r.completedAll; },
-
 
424
                 dropLabel:'Still mid-journey (not all stops done)', drop:function(r){ return r.madeCheckin && !r.completedAll; }},
-
 
425
  punchedOut:   {title:'Punched out',          desc:'Ended the day (punch-out recorded)', f:function(r){ return r.punchedOut; },
-
 
426
                 dropLabel:'Still out (punched in, not punched out)', drop:function(r){ return r.punchedIn && !r.punchedOut; }}
-
 
427
};
-
 
428
function funnelRows(list, date){
-
 
429
  if(!list.length) return '<p class="desc">None.</p>';
-
 
430
  var h='<table><thead><tr><th>Executive / Beat</th><th>Punch In/Out</th><th class="num">Completed</th><th class="num">Planned</th><th>State</th></tr></thead><tbody>';
-
 
431
  list.slice().sort(function(a,b){ return (''+a.name).localeCompare(b.name); }).forEach(function(r){
-
 
432
    var A=r.userId+",'"+js(r.name)+"','"+date+"'";
-
 
433
    h+='<tr class="clickable" onclick="openExec('+A+')">'+
-
 
434
       '<td><b>'+esc(r.name)+'</b><br><span style="color:#97a1b5;font-size:11px">'+esc(r.beatName||'-')+'</span></td>'+
-
 
435
       '<td style="white-space:nowrap">'+(r.punchInTime?'<span class="ptime">'+esc(r.punchInTime)+'</span>':'<span class="badflag">-</span>')+
-
 
436
         ' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
-
 
437
       '<td class="num cnt cgreen">'+r.completedVisits+'</td>'+
-
 
438
       '<td class="num">'+r.planned+'</td>'+
-
 
439
       '<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td></tr>';
-
 
440
  });
-
 
441
  return h+'</tbody></table>';
-
 
442
}
-
 
443
function openFunnel(key){
-
 
444
  var m=FUNNEL_META[key]; if(!m) return;
-
 
445
  var board=window._board||[], date=window._boardDate||'';
-
 
446
  var reached=board.filter(m.f);
-
 
447
  var dropped=m.drop ? board.filter(m.drop) : [];
-
 
448
  var mb=el('bj-modal-back'), md=el('bj-modal');
-
 
449
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(m.title)+'</h3>'+
-
 
450
        '<p class="desc">'+reached.length+' '+(reached.length===1?'executive':'executives')+' &middot; '+esc(m.desc)+'</p>'+
-
 
451
        funnelRows(reached, date);
-
 
452
  if(m.drop){
-
 
453
    h+='<div class="sectionlabel" style="margin-top:18px;color:#e8590c">'+esc(m.dropLabel)+' &middot; '+dropped.length+'</div>'+
-
 
454
       funnelRows(dropped, date);
-
 
455
  }
-
 
456
  h+='<p class="desc" style="margin-top:10px">Click a row to open that executive\'s journey detail.</p>';
-
 
457
  md.innerHTML=h; mb.classList.add('show');
-
 
458
}
-
 
459
// Leads drill-down: lists every lead visit logged today (matches the KPI count).
-
 
460
function openLeads(){
-
 
461
  var list=window._leadsList||[], date=window._boardDate||'';
-
 
462
  var mb=el('bj-modal-back'), md=el('bj-modal');
-
 
463
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>Leads on route</h3>'+
-
 
464
        '<p class="desc">'+list.length+' lead '+(list.length===1?'visit':'visits')+' logged'+(date?' on '+esc(date):'')+'</p>';
-
 
465
  if(!list.length){ h+='<p class="desc">No leads logged for this date.</p>'; }
-
 
466
  else {
-
 
467
    h+='<table><thead><tr><th>Executive</th><th>Lead</th><th>Check-in</th><th>Status</th></tr></thead><tbody>';
-
 
468
    list.slice().sort(function(a,b){ return (''+a.exec).localeCompare(b.exec); }).forEach(function(r){
-
 
469
      var A=r.userId+",'"+js(r.exec)+"','"+date+"'";
-
 
470
      var go = r.userId ? ' class="clickable" onclick="openExec('+A+')"' : '';
-
 
471
      h+='<tr'+go+'>'+
-
 
472
         '<td><b>'+esc(r.exec)+'</b></td>'+
-
 
473
         '<td>'+esc(r.lead||'-')+'</td>'+
-
 
474
         '<td style="white-space:nowrap">'+(r.checkInTime?'<span class="ptime">'+esc(hhmm(r.checkInTime))+'</span>':'<span class="badflag">-</span>')+'</td>'+
-
 
475
         '<td>'+esc(r.status||'')+'</td></tr>';
-
 
476
    });
-
 
477
    h+='</tbody></table><p class="desc" style="margin-top:10px">Click a row to open that executive\'s journey detail.</p>';
-
 
478
  }
-
 
479
  md.innerHTML=h; mb.classList.add('show');
-
 
480
}
407
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; }
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; }
408
var ATT_META = {
482
var ATT_META = {
409
  notPunchedIn:    {bg:'#fff0f0', fg:'#e03131', ic:'&#9888;',   title:'Not punched in',                  sub:'executives with a beat, not started', cls:'badflag'},
483
  notPunchedIn:    {bg:'#fff0f0', fg:'#e03131', ic:'&#9888;',   title:'Not punched in',                  sub:'executives with a beat, not started', cls:'badflag'},
410
  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'},
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'},
411
  geofence:        {bg:'#fff0f0', fg:'#e03131', ic:'&#128205;', title:'Check-in outside store geofence',  sub:'&gt;50 m from partner\'s saved location', cls:'badflag'},
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'},
Line 701... Line 775...
701
  var h='<div class="jstats">'+
775
  var h='<div class="jstats">'+
702
    jstat(fmtSec(workSec),'Working hours')+
776
    jstat(fmtSec(workSec),'Working hours')+
703
    jstat(fmtSec(inStoreSec),'In-store time')+
777
    jstat(fmtSec(inStoreSec),'In-store time')+
704
    jstat(fmtSec(transitSec),'Travel time')+
778
    jstat(fmtSec(transitSec),'Travel time')+
705
    jstat(distKm.toFixed(2)+' km','Distance Covered')+
779
    jstat(distKm.toFixed(2)+' km','Distance Covered')+
706
    jstat(visited.length+(leadsN?' &middot; '+leadsN+'L':''),'Stores / Leads')+
780
    jstat(visited.length+(leadsN?' / '+leadsN+'L':''),'Visits / Leads')+
707
    '</div>';
781
    '</div>';
708
  if(!seq.length){ return h+'<p class="desc">No tracking events for this day.</p>'; }
782
  if(!seq.length){ return h+'<p class="desc">No tracking events for this day.</p>'; }
709
  h+='<div class="tl">';
783
  h+='<div class="tl">';
710
  var ri=0;
784
  var ri=0;
711
  seq.forEach(function(x){
785
  seq.forEach(function(x){