Subversion Repositories SmartDukaan

Rev

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

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