Subversion Repositories SmartDukaan

Rev

Rev 36882 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Beat Journey</title>
<style>
  :root{
    --bg:#eef1f7; --card:#ffffff; --ink:#1d2433; --muted:#69748c; --soft:#97a1b5;
    --line:#e6e9f1; --line2:#eef1f6;
    --brand:#3b5bdb; --brand2:#5c7cfa; --teal:#0ca678; --cyan:#1098ad;
    --amber:#f59f00; --orange:#e8590c; --red:#e03131; --violet:#7048e8; --pink:#d6336c;
    --good:#2f9e44; --warn:#f08c00; --bad:#e03131;
  }
  *{box-sizing:border-box}
  html,body{margin:0;background:var(--bg);color:var(--ink);
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;font-size:14px}
  .wrap{max-width:1320px;margin:0 auto;padding:20px 24px 90px}
  a{color:var(--brand)}
  header.top{display:flex;align-items:center;justify-content:space-between;gap:18px;flex-wrap:wrap;margin-bottom:16px}
  .brand .crumb{font-size:12px;color:var(--soft);font-weight:600}
  .brand h1{margin:3px 0 0;font-size:23px;font-weight:740;letter-spacing:-.01em}
  .brand h1 .live{font-size:11px;font-weight:700;color:var(--teal);background:#e6fcf5;border:1px solid #c3fae8;border-radius:999px;padding:3px 9px;vertical-align:middle;margin-left:10px}
  .brand h1 .live i{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--teal);margin-right:5px;animation:pulse 1.6s infinite}
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
  .hd-right{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
  .ctl{background:var(--card);border:1px solid var(--line);border-radius:9px;padding:8px 12px;font-size:13px;color:var(--ink);font-weight:600}
  .ctl .lbl{color:var(--soft);font-weight:500;margin-right:5px}
  select.ctl,input.ctl{appearance:none;cursor:pointer;height:36px}
  input.ctl{cursor:auto}
  .btn-r{background:var(--brand);border:0;color:#fff;font-weight:650;padding:9px 15px;border-radius:9px;cursor:pointer;font-size:13px;height:36px}
  .lenses{display:flex;gap:4px;border-bottom:1px solid var(--line);margin-bottom:20px}
  .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}
  .lens.active{color:var(--brand);border-bottom-color:var(--brand)}
  .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}
  .kpis{display:grid;grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:20px}
  .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)}
  .kpi .top{display:flex;align-items:center;gap:8px}
  .kpi .ic{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;flex:none}
  .kpi .label{font-size:11.5px;color:var(--muted);font-weight:600;line-height:1.2}
  .kpi .val{font-size:26px;font-weight:760;margin-top:9px;letter-spacing:-.02em;line-height:1}
  .kpi .val small{font-size:13px;color:var(--soft);font-weight:600}
  .kpi .foot{font-size:11.5px;color:var(--soft);margin-top:7px}
  .grid{display:grid;gap:16px}
  .g2{grid-template-columns:1fr 1fr}
  .g3{grid-template-columns:1.55fr 1fr}
  .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:17px 19px;margin-bottom:16px;box-shadow:0 1px 2px rgba(20,30,60,.04)}
  .card h2{margin:0 0 3px;font-size:15px;font-weight:720;letter-spacing:-.01em}
  .card .desc{color:var(--muted);font-size:12.5px;margin:0 0 14px}
  .sectionlabel{font-size:11.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--soft);font-weight:700;margin:24px 2px 12px}
  .funnel{display:flex;gap:9px;align-items:stretch}
  .fstep{flex:1;border:1px solid var(--line);border-radius:11px;padding:13px 12px;position:relative;text-align:center;background:#fbfcfe}
  .fstep .fn{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);font-weight:600}
  .fstep .fv{font-size:26px;font-weight:770;margin-top:6px}
  .fstep .fbar{height:5px;border-radius:3px;margin-top:10px;background:linear-gradient(90deg,var(--brand),var(--brand2))}
  .fstep .drop{font-size:11px;color:var(--red);margin-top:7px;font-weight:600}
  .fstep .keep{font-size:11px;color:var(--good);margin-top:7px;font-weight:600}
  .fstep .arrow{position:absolute;right:-12px;top:50%;transform:translateY(-50%);z-index:2;color:#c5cce0;font-size:15px}
  .leak{display:flex;gap:10px;margin-top:12px}
  .leakbox{flex:1;border:1px solid var(--line2);border-radius:10px;padding:10px 12px;background:#f8f9fc}
  .leakbox .lt{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:600}
  .leakbox .lv{font-size:20px;font-weight:760;margin-top:4px}
  table{width:100%;border-collapse:collapse;font-size:13px}
  th,td{text-align:left;padding:9px 9px;border-bottom:1px solid var(--line2)}
  th{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:700;white-space:nowrap;cursor:pointer}
  th.num,td.num{text-align:right}
  tbody tr:hover{background:#f7f9fd}
  tr.clickable{cursor:pointer}
  .lvl{display:inline-block;font-size:10.5px;font-weight:700;padding:2px 6px;border-radius:5px;background:#eef1fb;color:var(--brand)}
  .pill-state{display:inline-flex;align-items:center;gap:6px;font-weight:600;font-size:12.5px;padding:3px 9px;border-radius:999px}
  .ps-store{background:#e7f5ff;color:#1971c2}
  .ps-travel{background:#e6fcf5;color:#0ca678}
  .ps-done{background:#ebfbee;color:#2f9e44}
  .ps-idle{background:#fff4e6;color:#e8590c}
  .ps-no{background:#fff0f0;color:#e03131}
  .meter{display:inline-block;width:54px;height:7px;border-radius:4px;background:#eef1f6;vertical-align:middle;margin-left:7px;overflow:hidden}
  .meter > i{display:block;height:100%;border-radius:4px}
  .warnflag{color:var(--orange);font-weight:700}
  .badflag{color:var(--red);font-weight:700}
  .rowwarn{background:#fff9f0}
  .rowbad{background:#fff5f5}
  .att{display:flex;align-items:center;gap:12px;padding:11px 12px;border:1px solid var(--line);border-radius:11px;margin-bottom:9px}
  .att .ic{width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:14px;flex:none}
  .att .ab{flex:1}
  .att .atitle{font-size:13px;font-weight:650}
  .att .adet{font-size:12px;color:var(--muted);margin-top:2px}
  .att .acount{font-size:18px;font-weight:780}
  .barrow{display:grid;grid-template-columns:60px 1fr 90px;align-items:center;gap:11px;margin:8px 0}
  .barrow .name{font-size:13px;font-weight:600}
  .track{background:#f1f3f9;border-radius:7px;height:20px;position:relative;overflow:hidden}
  .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}
  .barrow .meta{font-size:11.5px;color:var(--muted);text-align:right}
  .donutwrap{display:flex;align-items:center;gap:20px}
  .donut{width:138px;height:138px;border-radius:50%;flex:none;position:relative;
    -webkit-mask:radial-gradient(circle 42px at center,transparent 98%,#000 100%);mask:radial-gradient(circle 42px at center,transparent 98%,#000 100%)}
  .donut .center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;-webkit-mask:none;mask:none}
  .legend{display:flex;flex-direction:column;gap:9px;font-size:12.5px}
  .legend .it{display:flex;align-items:flex-start;gap:9px}
  .dot{width:11px;height:11px;border-radius:3px;flex:none;margin-top:3px}
  .tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:11px}
  .tile{background:#f8f9fc;border:1px solid var(--line2);border-radius:11px;padding:12px}
  .tile .t{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:600}
  .tile .v{font-size:21px;font-weight:760;margin-top:5px}
  .tile .v small{font-size:12px;color:var(--soft);font-weight:600}
  .util{display:flex;height:30px;border-radius:8px;overflow:hidden}
  .util div{display:flex;align-items:center;justify-content:center;font-size:11.5px;font-weight:650;color:#fff}
  .utlegend{display:flex;gap:18px;margin-top:12px;font-size:12px;color:var(--muted);flex-wrap:wrap}
  .utlegend span{display:inline-flex;align-items:center;gap:7px}
  .callout{background:linear-gradient(180deg,#fbfcff,#f4f7ff);border:1px solid #dfe6fb;border-radius:14px;padding:18px 20px;margin-top:6px}
  .callout h2{margin:0 0 3px;font-size:16px}
  .callout .desc{color:var(--muted);font-size:12.5px;margin-bottom:14px}
  .fix{display:flex;gap:13px;padding:13px 0;border-bottom:1px solid #e6ebf8}
  .fix:last-child{border-bottom:0}
  .fix .sev{width:6px;border-radius:4px;flex:none}
  .fix .fb{flex:1}
  .fix .ft{font-size:14px;font-weight:700}
  .fix .fd{font-size:13px;color:#3a4459;margin-top:4px;line-height:1.5}
  .fix .fd b{color:var(--ink)}
  .fix .act{font-size:12.5px;color:var(--good);margin-top:6px;font-weight:650}
  .hide{display:none}
  .loading{padding:70px;text-align:center;color:var(--muted)}
  .spinner{width:30px;height:30px;border:3px solid #e3e8f5;border-top-color:var(--brand);border-radius:50%;animation:spin 1s linear infinite;margin:0 auto 12px}
  @keyframes spin{to{transform:rotate(360deg)}}
  .modal-back{position:fixed;inset:0;background:rgba(15,22,40,.45);display:none;align-items:flex-start;justify-content:center;z-index:50;padding:40px 16px;overflow:auto}
  .modal-back.show{display:flex}
  .modal{background:#fff;border-radius:14px;max-width:680px;width:100%;padding:20px 22px;box-shadow:0 20px 60px rgba(10,20,50,.3)}
  .modal h3{margin:0 0 2px;font-size:17px}
  .modal .x{float:right;cursor:pointer;color:var(--soft);font-size:20px;line-height:1;border:0;background:none}
  .btn-ok{background:var(--good);border:0;color:#fff;font-weight:650;padding:6px 12px;border-radius:7px;cursor:pointer;font-size:12px}
  @media (max-width:1000px){.kpis{grid-template-columns:repeat(3,1fr)}.g2,.g3{grid-template-columns:1fr}.funnel{flex-wrap:wrap}.fstep{min-width:46%}.fstep .arrow{display:none}}
</style>
</head>
<body>
<div class="wrap">
  <header class="top">
    <div class="brand">
      <div class="crumb"><a href="${rc.contextPath}/dashboard">&larr; Dashboard</a> &middot; Field Sales &middot; Beat / Monthly PJP</div>
      <h1>Beat Journey <span class="live"><i></i>Live</span></h1>
    </div>
    <div class="hd-right">
      <select class="ctl" id="bj-level">
        <option value="">All levels below me</option>
        #foreach($lvl in $escalationTypes)
          <option value="$lvl">$lvl</option>
        #end
      </select>
      <input class="ctl" type="date" id="bj-date" value="$today">
      <span id="bj-period-range" class="hide">
        <input class="ctl" type="date" id="bj-start" value="$monthStart">
        <input class="ctl" type="date" id="bj-end" value="$today">
      </span>
      <button class="btn-r" id="bj-apply">Apply</button>
    </div>
  </header>

  <div class="lenses">
    <button class="lens active" data-lens="today" id="lens-today">Today (Live)</button>
    <button class="lens" data-lens="period" id="lens-period">Period (Analytics)</button>
    <button class="lens" data-lens="approval" id="lens-approval">Location Approval</button>
  </div>

  <div id="bj-content"><div class="loading"><div class="spinner"></div>Loading&hellip;</div></div>
</div>

<div class="modal-back" id="bj-modal-back">
  <div class="modal" id="bj-modal"></div>
</div>

<script>
var CTX = '${rc.contextPath}';
var BASE = CTX + '/beat-journey';
var LENS = 'today';

function el(id){ return document.getElementById(id); }
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
function num(n){ return (n==null?0:n).toLocaleString(); }
function adhColor(p){ if(p>=80)return '#2f9e44'; if(p>=60)return '#f08c00'; if(p>=40)return '#e8590c'; return '#e03131'; }
function pct(a,b){ return b? Math.round(100*a/b):0; }
function qs(){
  var lvl = el('bj-level').value;
  var s = '';
  if(lvl) s += '&escalationType=' + encodeURIComponent(lvl);
  return s;
}
function spinner(){ el('bj-content').innerHTML = '<div class="loading"><div class="spinner"></div>Loading&hellip;</div>'; }
function fail(e){ el('bj-content').innerHTML = '<div class="loading">Could not load data ('+esc(e&&e.message||e)+'). Are you signed in?</div>'; }

function setLens(name){
  LENS = name;
  var tabs = document.querySelectorAll('.lens');
  for(var i=0;i<tabs.length;i++){ tabs[i].classList.toggle('active', tabs[i].getAttribute('data-lens')===name); }
  el('bj-date').classList.toggle('hide', name!=='today');
  el('bj-period-range').classList.toggle('hide', name==='today');
  load();
}
function load(){
  if(LENS==='today') return loadToday();
  if(LENS==='period') return loadPeriod();
  return loadApproval();
}

/* ============================ TODAY ============================ */
function loadToday(){
  spinner();
  fetch(BASE + '/today?date=' + encodeURIComponent(el('bj-date').value) + qs(), {headers:{'Accept':'application/json'}})
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
    .then(renderToday).catch(fail);
}
function kpi(ic, bg, fg, label, val, foot, valColor){
  return '<div class="kpi"><div class="top"><div class="ic" style="background:'+bg+';color:'+fg+'">'+ic+'</div>'+
    '<div class="label">'+label+'</div></div><div class="val"'+(valColor?' style="color:'+valColor+'"':'')+'>'+val+'</div>'+
    '<div class="foot">'+foot+'</div></div>';
}
function fstep(name, val, w, sub, drop, last){
  return '<div class="fstep"><div class="fn">'+name+'</div><div class="fv">'+num(val)+'</div>'+
    '<div class="fbar" style="width:'+w+'%"></div><div class="'+(drop?'drop':'keep')+'">'+sub+'</div>'+
    (last?'':'<span class="arrow">&#10148;</span>')+'</div>';
}
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>'; }

function renderToday(d){
  var k=d.kpis, f=d.funnel, s=d.stops, a=d.attention;
  var h='';
  h+='<div class="kpis">';
  h+=kpi('&#128101;','#eef1fb','#3b5bdb','Executives scheduled',num(k.scheduled),k.notStarted+' not started yet');
  h+=kpi('&#9201;','#ebfbee','#2f9e44','Punched in',num(k.punchedIn)+' <small>/'+k.scheduled+'</small>',k.notStarted+' pending');
  h+=kpi('&#128205;','#e7f5ff','#1098ad','On route now',num(k.onRouteNow),'live journeys');
  h+=kpi('&#10003;','#eef1fb','#3b5bdb','Visits done',num(k.visitsDone)+' <small>/'+k.visitsPlanned+'</small>',k.inProgress+' in progress');
  h+=kpi('&#8635;','#fff4e6','#e8590c','Deferred today',num(k.deferred),'shop closed / unavailable');
  h+=kpi('&#9733;','#fff0f6','#d6336c','Leads on route',num(k.leads),'new shops logged');
  h+='</div>';

  h+='<div class="sectionlabel">Today\'s Journey Funnel</div><div class="card"><div class="funnel">';
  var sc=f.scheduled||1;
  h+=fstep('Scheduled executives',f.scheduled,100,'start of day',false,false);
  h+=fstep('Punched in',f.punchedIn,pct(f.punchedIn,sc),'&minus;'+(f.scheduled-f.punchedIn)+' never started',true,false);
  h+=fstep('Made a check-in',f.madeCheckin,pct(f.madeCheckin,sc),'&minus;'+(f.punchedIn-f.madeCheckin)+' punched, idle',true,false);
  h+=fstep('Completed all visits',f.completedAll,pct(f.completedAll,sc),(f.madeCheckin-f.completedAll)+' still mid-journey',true,false);
  h+=fstep('Punched out',f.punchedOut,pct(f.punchedOut,sc),'journey ended',false,true);
  h+='</div><div class="leak">';
  h+=leakbox('Stops planned',s.planned,null);
  h+=leakbox('Checked out (done)',s.done,'#2f9e44');
  h+=leakbox('In progress now',s.inProgress,'#1098ad');
  h+=leakbox('Deferred',s.deferred,'#e8590c');
  h+=leakbox('Untouched',s.untouched,'#e03131');
  h+='</div></div>';

  h+='<div class="grid g3">';
  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>';
  h+='<div class="card"><h2>Needs attention now</h2><p class="desc">Things a manager should clear today.</p>'+attentionList(a)+'</div>';
  h+='</div>';
  el('bj-content').innerHTML=h;
}
function stateClass(st){ return st==='IN_STORE'?'ps-store':st==='TRAVELLING'?'ps-travel':st==='DONE'?'ps-done':st==='NOT_STARTED'?'ps-no':'ps-idle'; }
function boardTable(rows, date){
  if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
  var h='<table><thead><tr><th>Executive / Beat</th><th>Agenda</th><th>Punch-in</th><th>State now</th><th class="num">Visits</th><th class="num">Last ping</th><th class="num">Batt</th><th class="num">Loc-off</th></tr></thead><tbody>';
  rows.forEach(function(r){
    var stale = r.lastPingMin!=null && r.lastPingMin>=40;
    var idle = r.state==='TRAVELLING' && stale;
    var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (idle?'rowwarn':'');
    var ping = r.lastPingMin==null ? '&mdash;' : (r.lastPingMin+' min');
    h+='<tr class="clickable '+rowcls+'" onclick="openExec('+r.userId+',\''+esc(r.name)+'\',\''+date+'\')">'+
       '<td><b>'+esc(r.name)+'</b><br><span style="color:#97a1b5;font-size:11px">'+esc(r.beatName||'&mdash;')+'</span></td>'+
       '<td>'+(r.agendaFilled?'<span style="color:#2f9e44">&#10003;</span>':'<span class="badflag">&#10007;</span>')+'</td>'+
       '<td>'+(r.punchInTime?esc(r.punchInTime):'<span class="badflag">&mdash;</span>')+'</td>'+
       '<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
       '<td class="num">'+r.visitsDone+'/'+r.visitsTotal+'</td>'+
       '<td class="num'+(stale?' warnflag':'')+'">'+ping+'</td>'+
       '<td class="num'+(r.battery!=null&&r.battery<25?' warnflag':'')+'">'+(r.battery==null?'&mdash;':r.battery+'%')+'</td>'+
       '<td class="num'+(r.locOff>0?' warnflag':'')+'">'+r.locOff+'</td></tr>';
  });
  return h+'</tbody></table>';
}
function attRow(bg,fg,ic,title,detail,count,cls){
  return '<div class="att"><div class="ic" style="background:'+bg+';color:'+fg+'">'+ic+'</div><div class="ab"><div class="atitle">'+title+'</div>'+
    '<div class="adet">'+detail+'</div></div><div class="acount '+(cls||'')+'">'+count+'</div></div>';
}
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; }
function attentionList(a){
  var h='';
  h+=attRow('#fff0f0','#e03131','&#9888;','Not punched in', names(a.notPunchedIn,3), a.notPunchedIn.length, a.notPunchedIn.length?'badflag':'');
  h+=attRow('#fff4e6','#e8590c','&#9201;','Idle / stale GPS &gt; 40 min', names(a.idleStale,3), a.idleStale.length, a.idleStale.length?'warnflag':'');
  h+=attRow('#fff0f0','#e03131','&#128205;','Check-in outside store geofence','&gt;50 m from partner\'s saved location', a.geofence, a.geofence?'badflag':'');
  h+=attRow('#fff4e6','#e8590c','&#128244;','Location turned off mid-journey', names(a.locationOff,3), a.locationOff.length, a.locationOff.length?'warnflag':'');
  h+=attRow('#eef1f6','#69748c','&#8635;','Deferred, awaiting re-action','shops deferred today', a.deferredAwaiting, '');
  return h;
}

/* ============================ PERIOD ============================ */
function loadPeriod(){
  spinner();
  var u = BASE + '/data?startDate=' + encodeURIComponent(el('bj-start').value) + '&endDate=' + encodeURIComponent(el('bj-end').value) + qs();
  fetch(u, {headers:{'Accept':'application/json'}})
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
    .then(renderPeriod).catch(fail);
}
function tile(t,v){ return '<div class="tile"><div class="t">'+t+'</div><div class="v">'+v+'</div></div>'; }
function renderPeriod(d){
  var k=d.kpis, f=d.funnel, df=d.deferral, u=d.utilization, t=d.travel, o=d.outcomes;
  var neverTouched = Math.max((f.plannedStops||0)-(f.checkedIn||0)-(df.total||0),0);
  var h='';
  h+='<div class="kpis">';
  h+=kpi('&#9878;','#fff4e6','#f08c00','Adherence',k.adherencePct+'<small>%</small>',num(k.doneStops)+' of '+num(k.plannedStops)+' stops done',adhColor(k.adherencePct));
  h+=kpi('&#128506;','#e7f5ff','#1098ad','PJP coverage L1',k.coverageL1Pct+'<small>%</small>',k.coverageL1With+'/'+k.coverageL1Total+' have a plan','#1098ad');
  h+=kpi('&#128694;','#ebfbee','#2f9e44','Visits / active day',k.avgVisitsPerActiveDay,'across '+num(k.journeys)+' journeys');
  h+=kpi('&#128172;','#eef1fb','#3b5bdb','Discussion / store',k.avgDiscussionMin+' <small>min</small>','target 30&ndash;45 min', k.avgDiscussionMin>0&&k.avgDiscussionMin<30?'#e8590c':null);
  h+=kpi('&#8635;','#fff0f0','#e03131','Auto "missed"',num(k.autoMissed),k.autoMissedSystemPct+'% system-flagged','#e03131');
  h+=kpi('&#9733;','#fff0f6','#d6336c','Leads on route',num(k.leadsCreated),'this period');
  h+='</div>';

  h+='<div class="sectionlabel">Aggregate Journey Funnel</div><div class="card"><div class="funnel">';
  var pl=f.plannedStops||1;
  h+=fstep('Planned stops',f.plannedStops,100,'scheduled',false,false);
  h+=fstep('Days punched in',f.punchedIn,pct(f.punchedIn,pl),'&minus;'+f.dropPunch+'% no punch-in',true,false);
  h+=fstep('Checked in',f.checkedIn,pct(f.checkedIn,pl),'&minus;'+f.dropCheckin+'% reached, skipped',true,false);
  h+=fstep('Checked out (done)',f.checkedOut,pct(f.checkedOut,pl),'&minus;'+f.dropCheckout+'% no checkout',false,true);
  h+='</div><div class="leak">';
  h+=leakbox('Deferred &mdash; genuine',df.human,'#2f9e44');
  h+=leakbox('Deferred &mdash; system flag',df.systemGenerated,'#f08c00');
  h+=leakbox('Heads acted on',df.actioned,null);
  h+=leakbox('Never touched',neverTouched,'#e03131');
  h+='</div></div>';

  h+='<div class="sectionlabel">Adherence &amp; Coverage</div><div class="grid g3">';
  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>';
  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>';
  h+='</div>';

  h+='<div class="sectionlabel">Deferrals, Travel &amp; Outcomes</div><div class="grid g2">';
  h+='<div class="card"><h2>Deferral nature</h2><p class="desc">Deliberate, or just auto-flagged missed beats?</p>'+deferDonut(df)+'</div>';
  h+='<div class="card"><h2>Travel &amp; route quality</h2><p class="desc">Straight-line distance per leg between consecutive stops.</p>'+travelBlock(t)+'</div>';
  h+='</div>';

  h+='<div class="sectionlabel">Outcomes &amp; Visit Quality</div><div class="card"><p class="desc">Captured on the check-out form &mdash; the substance of each store conversation.</p>'+
     '<div class="tiles" style="grid-template-columns:repeat(4,1fr)">'+
     tile('Partner remark filled', o.remarkFilledPct+'<small>%</small>')+
     tile('Avg partner rating', (o.avgRating||0)+'<small>/5</small>')+
     tile('Audit checklist done', o.auditDonePct+'<small>%</small>')+
     tile('Next-visit follow-up set', o.nextVisitPct+'<small>%</small>')+
     '</div></div>';

  h+='<div class="sectionlabel">What to fix</div>'+findingsBlock(d.findings);
  h+=sourcesCard();
  el('bj-content').innerHTML=h;
  window._scorecard = d.scorecard;
}
var sortKey=null, sortAsc=false;
function sortCard(key){
  if(sortKey===key) sortAsc=!sortAsc; else { sortKey=key; sortAsc=false; }
  window._scorecard.sort(function(a,b){ var x=a[key],y=b[key];
    if(typeof x==='string') return sortAsc? (''+x).localeCompare(y):(''+y).localeCompare(x);
    return sortAsc? x-y : y-x; });
  document.getElementById('scTbl').outerHTML = scorecardTable(window._scorecard);
}
function scorecardTable(rows){
  if(!rows||!rows.length) return '<p class="desc">No beat owners in scope for this period.</p>';
  var h='<table id="scTbl"><thead><tr>'+
    '<th onclick="sortCard(\'name\')">Executive</th>'+
    '<th class="num" onclick="sortCard(\'planned\')">Plan</th>'+
    '<th class="num" onclick="sortCard(\'done\')">Done</th>'+
    '<th class="num" onclick="sortCard(\'adherencePct\')">Adher.</th>'+
    '<th class="num" onclick="sortCard(\'discussionMin\')">Disc.</th>'+
    '<th class="num" onclick="sortCard(\'workingHrs\')">Hrs</th>'+
    '<th class="num" onclick="sortCard(\'leads\')">Leads</th>'+
    '<th class="num" onclick="sortCard(\'geoFlags\')">Geo&#9888;</th></tr></thead><tbody>';
  rows.forEach(function(r){
    var c=adhColor(r.adherencePct);
    h+='<tr><td>'+esc(r.name)+' <span class="lvl">'+esc(r.level)+'</span></td>'+
       '<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
       '<td class="num"><b style="color:'+c+'">'+r.adherencePct+'%</b><span class="meter"><i style="width:'+Math.min(100,r.adherencePct)+'%;background:'+c+'"></i></span></td>'+
       '<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'&mdash;')+(r.discussionMin?'m':'')+'</td>'+
       '<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td>'+
       '<td class="num'+(r.geoFlags>0?' badflag':'')+'">'+r.geoFlags+'</td></tr>';
  });
  return h+'</tbody></table>';
}
function coverageBars(rows){
  if(!rows||!rows.length) return '<p class="desc">No coverage data.</p>';
  var max=0; rows.forEach(function(r){ if(r.totalUsers>max)max=r.totalUsers; }); if(!max)max=1;
  var h=''; rows.forEach(function(r){
    var w=pct(r.withPjp,max), wt=pct(r.totalUsers,max);
    h+='<div class="barrow"><div class="name">'+esc(r.level)+'</div><div class="track">'+
       '<div style="position:absolute;inset:0;width:'+wt+'%;background:#e3e8f5;border-radius:7px"></div>'+
       '<div class="fill" style="width:'+w+'%;position:relative;background:linear-gradient(90deg,#1098ad,#22b8cf)">'+(r.withPjp||'')+'</div></div>'+
       '<div class="meta">'+r.withPjp+'/'+r.totalUsers+' &middot; '+r.coveragePct+'%</div></div>';
  });
  return h;
}
function utilBlock(u){
  return '<h2 style="margin-top:18px">Working-hour utilization</h2><p class="desc">Where the average working day goes.</p>'+
    '<div class="util">'+
    '<div style="width:'+u.inStorePct+'%;background:#2f9e44">In-store '+u.inStorePct+'%</div>'+
    '<div style="width:'+u.travelPct+'%;background:#3b5bdb">Travel '+u.travelPct+'%</div>'+
    '<div style="width:'+u.idlePct+'%;background:#e03131">Idle '+u.idlePct+'%</div></div>'+
    '<div class="utlegend"><span><span class="dot" style="background:#2f9e44;margin:0"></span>Selling '+u.inStoreHrs+' h</span>'+
    '<span><span class="dot" style="background:#3b5bdb;margin:0"></span>Travel '+u.travelHrs+' h</span>'+
    '<span><span class="dot" style="background:#e03131;margin:0"></span>Idle '+u.idleHrs+' h</span></div>';
}
function deferDonut(df){
  var sysPct=df.systemPct||0;
  var g='conic-gradient(#f59f00 0 '+sysPct+'%, #2f9e44 '+sysPct+'% 100%)';
  return '<div class="donutwrap"><div class="donut" style="background:'+g+'"><div class="center">'+
    '<div style="font-size:24px;font-weight:780">'+num(df.total)+'</div><div style="font-size:11px;color:#97a1b5">deferrals</div></div></div>'+
    '<div class="legend">'+
    '<div class="it"><span class="dot" style="background:#f59f00"></span><div><b>'+df.systemGenerated+'</b> system auto-flagged ('+sysPct+'%)<br><span style="color:#97a1b5;font-size:11.5px">"beat missed", "no punch-in"</span></div></div>'+
    '<div class="it"><span class="dot" style="background:#2f9e44"></span><div><b>'+df.human+'</b> genuine reason ('+df.humanPct+'%)</div></div>'+
    '<div class="it"><span class="dot" style="background:#3b5bdb"></span><div><b>'+df.actioned+'</b> acted on by a head'+(df.avgDaysToAction?'<br><span style="color:#97a1b5;font-size:11.5px">avg '+df.avgDaysToAction+' days to action</span>':'')+'</div></div>'+
    '</div></div><p class="desc" style="margin-top:13px">"System" vs "genuine" is read from the deferral reason; there is no separate flag.</p>';
}
function travelBlock(t){
  var rows=t.legBuckets||[], max=0; rows.forEach(function(r){ if(r.count>max)max=r.count; }); if(!max)max=1;
  var colors={'0-2 km':'#2f9e44','2-5 km':'#2f9e44','5-15 km':'#82c91e','15-50 km':'#f59f00','50 km+':'#e03131'};
  var h=''; rows.forEach(function(r){
    h+='<div class="barrow"><div class="name">'+esc(r.bucket)+'</div><div class="track"><div class="fill" style="width:'+pct(r.count,max)+'%;background:'+(colors[r.bucket]||'#3b5bdb')+'">'+r.count+'</div></div><div class="meta"></div></div>';
  });
  h+='<div class="tiles" style="margin-top:14px">'+
     tile('Avg leg', t.avgLegKm+'<small> km</small>')+
     tile('Legs &gt; 15 km', t.pctOver15+'<small>%</small>')+
     tile('Actual / estimate', (t.actualVsEstimateRatio||0)+'<small>&times;</small>')+'</div>';
  return h;
}
function findingsBlock(items){
  if(!items||!items.length) return '<div class="callout"><h2>Nothing flagged</h2><p class="desc">No issues crossed the alert thresholds for this period.</p></div>';
  var sev={high:'#e03131',med:'#f59f00',low:'#2f9e44'};
  var h='<div class="callout"><h2>Recommended management actions</h2><p class="desc">Generated from this period\'s data.</p>';
  items.forEach(function(x){
    h+='<div class="fix"><div class="sev" style="background:'+(sev[x.severity]||'#3b5bdb')+'"></div><div class="fb">'+
       '<div class="ft">'+esc(x.title)+'</div><div class="fd">'+esc(x.detail)+'</div>'+
       '<div class="act">&rarr; '+esc(x.action)+'</div></div></div>';
  });
  return h+'</div>';
}
function sourcesCard(){
  return '<div class="sectionlabel">Where the numbers come from</div><div class="card">'+
    '<p class="desc">Nothing new needs to be captured &mdash; everything here is built from what the field app already records on each journey.</p>'+
    '<ul style="margin:0;padding-left:18px;line-height:1.95;font-size:13px;color:#3a4459">'+
    '<li>The <b>journey timeline</b> (punch-in, check-ins/outs, deferrals, punch-out) comes from what the executive taps in the app.</li>'+
    '<li><b>Adherence</b> compares the partners planned on each beat against the stores actually visited.</li>'+
    '<li><b>Time, travel and distance</b> use the in-store and travel durations logged per visit.</li>'+
    '<li><b>Geofence / fake-visit checks</b> compare the check-in GPS against the partner\'s saved shop location (flagged beyond ~50 m).</li>'+
    '<li><b>Visit outcomes</b> (remarks, rating, audit, next visit) come from the check-out form.</li>'+
    '<li><b>Coverage and the manager view</b> follow the existing L1&ndash;L5 sales hierarchy, so each manager sees only their team.</li>'+
    '</ul></div>';
}

/* ============================ EXEC DRILLDOWN ============================ */
function openExec(userId, name, date){
  if(!userId){ return; }
  var mb=el('bj-modal-back'), m=el('bj-modal');
  m.innerHTML='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(name)+'</h3><p class="desc">Journey detail &middot; '+esc(date)+'</p><div class="loading"><div class="spinner"></div>Loading&hellip;</div>';
  mb.classList.add('show');
  fetch(CTX+'/beat-report/live-tracking?userId='+userId+'&date='+encodeURIComponent(date),{headers:{'Accept':'application/json'}})
    .then(function(r){ return r.json(); }).then(function(d){
      var v=d.visits||[];
      var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(name)+'</h3><p class="desc">Journey detail &middot; '+esc(date)+' &middot; battery '+(d.batteryLevel==null?'&mdash;':d.batteryLevel+'%')+' &middot; loc-off '+(d.locationToggleCount||0)+'</p>';
      if(!v.length){ h+='<p class="desc">No tracking events for this day.</p>'; }
      else{
        h+='<table><thead><tr><th>Stop</th><th>Type</th><th>Status</th><th>In</th><th>Out</th><th class="num">Time</th></tr></thead><tbody>';
        v.forEach(function(x){
          h+='<tr><td>'+esc(x.taskName||'&mdash;')+'</td><td>'+esc(x.taskType||'')+'</td><td>'+esc(x.markType||'')+'</td>'+
             '<td>'+(x.checkInTime?esc(x.checkInTime):'&mdash;')+'</td><td>'+(x.checkOutTime?esc(x.checkOutTime):'&mdash;')+'</td>'+
             '<td class="num">'+(x.timeSpent?esc(x.timeSpent):'&mdash;')+'</td></tr>';
        });
        h+='</tbody></table>';
      }
      m.innerHTML=h;
    }).catch(function(){ m.innerHTML='<button class="x" onclick="closeModal()">&times;</button><p class="desc">Could not load detail.</p>'; });
}
function closeModal(){ el('bj-modal-back').classList.remove('show'); }

/* ============================ APPROVAL ============================ */
function loadApproval(){
  spinner();
  var from=el('bj-start').value, to=el('bj-end').value;
  var u=CTX+'/beat-report/approval/data?fromDate='+encodeURIComponent(from)+'&toDate='+encodeURIComponent(to)+qs();
  fetch(u,{headers:{'Accept':'application/json'}}).then(function(r){ return r.json(); }).then(function(d){
    var rows=d.rows||[];
    var h='<div class="card"><h2>Location Approval <span style="color:#97a1b5;font-weight:500;font-size:12px">&middot; '+rows.length+' off-site check-ins</span></h2>'+
      '<p class="desc">Check-ins recorded &gt;50 m from the partner\'s saved location, across your downline. Approving updates the partner\'s coordinates.</p>';
    if(!rows.length){ h+='<p class="desc">Nothing pending in this window.</p></div>'; el('bj-content').innerHTML=h; return; }
    h+='<table><thead><tr><th>Executive</th><th>Store</th><th>Date</th><th>Check-in</th><th class="num">Distance</th><th class="num">Action</th></tr></thead><tbody>';
    rows.forEach(function(r){
      h+='<tr id="appr-'+r.trackingId+'"><td>'+esc(r.username)+'</td><td>'+esc(r.taskName)+'</td><td>'+esc(r.taskDate)+'</td><td>'+esc(r.checkInTime)+'</td>'+
         '<td class="num '+(r.distanceM>200?'badflag':'warnflag')+'">'+r.distanceM+' m</td>'+
         '<td class="num"><button class="btn-ok" onclick="approve('+r.trackingId+')">Approve</button></td></tr>';
    });
    h+='</tbody></table></div>';
    el('bj-content').innerHTML=h;
  }).catch(fail);
}
function approve(trackingId){
  fetch(CTX+'/beat-report/approve-checkin',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'trackingId='+trackingId})
    .then(function(r){ return r.json(); }).then(function(){ var row=el('appr-'+trackingId); if(row) row.parentNode.removeChild(row); }).catch(function(){ alert('Approve failed'); });
}

/* ============================ INIT ============================ */
document.querySelectorAll('.lens').forEach(function(b){ b.addEventListener('click', function(){ setLens(b.getAttribute('data-lens')); }); });
el('bj-apply').addEventListener('click', load);
el('bj-modal-back').addEventListener('click', function(e){ if(e.target===el('bj-modal-back')) closeModal(); });
load();
</script>
</body>
</html>