| 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">➤</span>')+'</div>';
|
338 |
(last?'':'<span class="arrow">➤</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('👥','#eef1fb','#3b5bdb','Executives scheduled',num(k.scheduled),k.notStarted+' not started yet');
|
351 |
h+=kpi('👥','#eef1fb','#3b5bdb','Executives scheduled',num(k.scheduled),k.notStarted+' not started yet');
|
| 345 |
h+=kpi('⏱','#ebfbee','#2f9e44','Punched in',num(k.punchedIn)+' <small>/'+k.scheduled+'</small>',k.notStarted+' pending');
|
352 |
h+=kpi('⏱','#ebfbee','#2f9e44','Punched in',num(k.punchedIn)+' <small>/'+k.scheduled+'</small>',k.notStarted+' pending');
|
| 346 |
h+=kpi('📍','#e7f5ff','#1098ad','On route now',num(k.onRouteNow),'live journeys');
|
353 |
h+=kpi('📍','#e7f5ff','#1098ad','On route now',num(k.onRouteNow),'live journeys');
|
| 347 |
h+=kpi('✓','#eef1fb','#3b5bdb','Visits done',num(k.visitsDone)+' <small>/'+k.visitsPlanned+'</small>',k.inProgress+' in progress');
|
354 |
h+=kpi('✓','#eef1fb','#3b5bdb','Visit vs Planned',num(k.visitsDone)+' <small>/'+k.visitsPlanned+'</small>',k.inProgress+' in progress');
|
| 348 |
h+=kpi('↻','#fff4e6','#e8590c','Deferred today',num(k.deferred),'shop closed / unavailable');
|
355 |
h+=kpi('↻','#fff4e6','#e8590c','Deferred today',num(k.deferred),'shop closed / unavailable');
|
| 349 |
h+=kpi('★','#fff0f6','#d6336c','Leads on route',num(k.leads),'new shops logged');
|
356 |
h+=kpi('★','#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),'−'+(f.scheduled-f.punchedIn)+' never started',true,false);
|
362 |
h+=fstep('Punched in',f.punchedIn,pct(f.punchedIn,sc),'−'+(f.scheduled-f.punchedIn)+' never started',true,false,'punchedIn');
|
| 356 |
h+=fstep('Made a check-in',f.madeCheckin,pct(f.madeCheckin,sc),'−'+(f.punchedIn-f.madeCheckin)+' punched, idle',true,false);
|
363 |
h+=fstep('Made a check-in',f.madeCheckin,pct(f.madeCheckin,sc),'−'+(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">✓</span>':'<span class="badflag" style="font-size:15px">✗</span>')+'</td>'+
|
395 |
'<td style="text-align:center">'+(r.agendaFilled?'<span style="color:#2f9e44;font-size:15px">✓</span>':'<span class="badflag" style="font-size:15px">✗</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\')">📍</button>'+
|
406 |
'<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">📍</button>'+
|
| 400 |
'<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">👁</button>'+
|
407 |
'<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">👁</button>'+
|
| 401 |
'<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">✎</button>'+
|
408 |
'<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">✎</button>'+
|
| 402 |
'<button class="actbtn add" title="Assign / add visit in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'add\')">+</button>'+
|
409 |
'<button class="actbtn add" title="Assign / add visit in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'add\')">+</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()">×</button><h3>'+esc(m.title)+'</h3>'+
|
| - |
|
450 |
'<p class="desc">'+reached.length+' '+(reached.length===1?'executive':'executives')+' · '+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)+' · '+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()">×</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:'⚠', title:'Not punched in', sub:'executives with a beat, not started', cls:'badflag'},
|
483 |
notPunchedIn: {bg:'#fff0f0', fg:'#e03131', ic:'⚠', title:'Not punched in', sub:'executives with a beat, not started', cls:'badflag'},
|
| 410 |
idleStale: {bg:'#fff4e6', fg:'#e8590c', ic:'⏱', title:'Idle — off-store > 40 min', sub:'stationary >20 m from any store', cls:'warnflag'},
|
484 |
idleStale: {bg:'#fff4e6', fg:'#e8590c', ic:'⏱', title:'Idle — off-store > 40 min', sub:'stationary >20 m from any store', cls:'warnflag'},
|
| 411 |
geofence: {bg:'#fff0f0', fg:'#e03131', ic:'📍', title:'Check-in outside store geofence', sub:'>50 m from partner\'s saved location', cls:'badflag'},
|
485 |
geofence: {bg:'#fff0f0', fg:'#e03131', ic:'📍', title:'Check-in outside store geofence', sub:'>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?' · '+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){
|