Subversion Repositories SmartDukaan

Rev

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

Rev Author Line No. Line
36877 vikas 1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
<title>Beat Journey</title>
7
<style>
8
  :root{
9
    --bg:#eef1f7; --card:#ffffff; --ink:#1d2433; --muted:#69748c; --soft:#97a1b5;
10
    --line:#e6e9f1; --line2:#eef1f6;
11
    --brand:#3b5bdb; --brand2:#5c7cfa; --teal:#0ca678; --cyan:#1098ad;
12
    --amber:#f59f00; --orange:#e8590c; --red:#e03131; --violet:#7048e8; --pink:#d6336c;
13
    --good:#2f9e44; --warn:#f08c00; --bad:#e03131;
14
  }
15
  *{box-sizing:border-box}
16
  html,body{margin:0;background:var(--bg);color:var(--ink);
17
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;font-size:14px}
18
  .wrap{max-width:1320px;margin:0 auto;padding:20px 24px 90px}
19
  a{color:var(--brand)}
20
  header.top{display:flex;align-items:center;justify-content:space-between;gap:18px;flex-wrap:wrap;margin-bottom:16px}
21
  .brand .crumb{font-size:12px;color:var(--soft);font-weight:600}
22
  .brand h1{margin:3px 0 0;font-size:23px;font-weight:740;letter-spacing:-.01em}
23
  .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}
24
  .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}
25
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
26
  .hd-right{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
27
  .ctl{background:var(--card);border:1px solid var(--line);border-radius:9px;padding:8px 12px;font-size:13px;color:var(--ink);font-weight:600}
28
  .ctl .lbl{color:var(--soft);font-weight:500;margin-right:5px}
29
  select.ctl,input.ctl{appearance:none;cursor:pointer;height:36px}
30
  input.ctl{cursor:auto}
31
  .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}
32
  .lenses{display:flex;gap:4px;border-bottom:1px solid var(--line);margin-bottom:20px}
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)}
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}
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 .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}
40
  .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}
42
  .kpi .val small{font-size:13px;color:var(--soft);font-weight:600}
43
  .kpi .foot{font-size:11.5px;color:var(--soft);margin-top:7px}
44
  .grid{display:grid;gap:16px}
45
  .g2{grid-template-columns:1fr 1fr}
46
  .g3{grid-template-columns:1.55fr 1fr}
47
  .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)}
48
  .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}
50
  .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}
52
  .fstep{flex:1;border:1px solid var(--line);border-radius:11px;padding:13px 12px;position:relative;text-align:center;background:#fbfcfe}
53
  .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}
55
  .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}
57
  .fstep .keep{font-size:11px;color:var(--good);margin-top:7px;font-weight:600}
58
  .fstep .arrow{position:absolute;right:-12px;top:50%;transform:translateY(-50%);z-index:2;color:#c5cce0;font-size:15px}
59
  .leak{display:flex;gap:10px;margin-top:12px}
60
  .leakbox{flex:1;border:1px solid var(--line2);border-radius:10px;padding:10px 12px;background:#f8f9fc}
61
  .leakbox .lt{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:600}
62
  .leakbox .lv{font-size:20px;font-weight:760;margin-top:4px}
63
  table{width:100%;border-collapse:collapse;font-size:13px}
64
  th,td{text-align:left;padding:9px 9px;border-bottom:1px solid var(--line2)}
65
  th{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:700;white-space:nowrap;cursor:pointer}
66
  th.num,td.num{text-align:right}
67
  tbody tr:hover{background:#f7f9fd}
68
  tr.clickable{cursor:pointer}
69
  .lvl{display:inline-block;font-size:10.5px;font-weight:700;padding:2px 6px;border-radius:5px;background:#eef1fb;color:var(--brand)}
70
  .pill-state{display:inline-flex;align-items:center;gap:6px;font-weight:600;font-size:12.5px;padding:3px 9px;border-radius:999px}
71
  .ps-store{background:#e7f5ff;color:#1971c2}
72
  .ps-travel{background:#e6fcf5;color:#0ca678}
73
  .ps-done{background:#ebfbee;color:#2f9e44}
74
  .ps-idle{background:#fff4e6;color:#e8590c}
75
  .ps-no{background:#fff0f0;color:#e03131}
76
  .meter{display:inline-block;width:54px;height:7px;border-radius:4px;background:#eef1f6;vertical-align:middle;margin-left:7px;overflow:hidden}
77
  .meter > i{display:block;height:100%;border-radius:4px}
78
  .warnflag{color:var(--orange);font-weight:700}
79
  .badflag{color:var(--red);font-weight:700}
80
  .rowwarn{background:#fff9f0}
81
  .rowbad{background:#fff5f5}
36897 vikas 82
  .bstat{display:inline-block;font-size:10.5px;font-weight:700;padding:3px 9px;border-radius:999px;white-space:nowrap}
83
  .bstat.ok{background:#ebfbee;color:#2f9e44}
84
  .bstat.warn{background:#fff4e6;color:#e8590c}
85
  .bstat.no{background:#fff0f0;color:#e03131}
86
  .bdot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px;vertical-align:middle}
87
  .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}
88
  td.cnt{font-weight:700;font-variant-numeric:tabular-nums}
89
  .cgreen{color:#2f9e44}.camber{color:#f08c00}.cred{color:#e03131}.cmuted{color:#69748c}
90
  .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}
91
  .actbtn.map{background:#12b886}.actbtn.view{background:#3b5bdb}
92
  .actbtn.edit{background:#f08c00}.actbtn.add{background:#2f9e44;font-size:15px}
93
  .actbtn:active{transform:translateY(1px)}
36877 vikas 94
  .att{display:flex;align-items:center;gap:12px;padding:11px 12px;border:1px solid var(--line);border-radius:11px;margin-bottom:9px}
95
  .att .ic{width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:14px;flex:none}
96
  .att .ab{flex:1}
97
  .att .atitle{font-size:13px;font-weight:650}
98
  .att .adet{font-size:12px;color:var(--muted);margin-top:2px}
99
  .att .acount{font-size:18px;font-weight:780}
36882 vikas 100
  .att.clickable{cursor:pointer;transition:border-color .12s,box-shadow .12s}
101
  .att.clickable:hover{border-color:#c7d0e8;box-shadow:0 2px 8px rgba(20,30,60,.08)}
102
  .att .chev{color:#c2cadb;font-size:13px;margin-left:8px}
36877 vikas 103
  .barrow{display:grid;grid-template-columns:60px 1fr 90px;align-items:center;gap:11px;margin:8px 0}
104
  .barrow .name{font-size:13px;font-weight:600}
105
  .track{background:#f1f3f9;border-radius:7px;height:20px;position:relative;overflow:hidden}
106
  .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}
107
  .barrow .meta{font-size:11.5px;color:var(--muted);text-align:right}
108
  .donutwrap{display:flex;align-items:center;gap:20px}
109
  .donut{width:138px;height:138px;border-radius:50%;flex:none;position:relative;
110
    -webkit-mask:radial-gradient(circle 42px at center,transparent 98%,#000 100%);mask:radial-gradient(circle 42px at center,transparent 98%,#000 100%)}
111
  .donut .center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;-webkit-mask:none;mask:none}
112
  .legend{display:flex;flex-direction:column;gap:9px;font-size:12.5px}
113
  .legend .it{display:flex;align-items:flex-start;gap:9px}
114
  .dot{width:11px;height:11px;border-radius:3px;flex:none;margin-top:3px}
115
  .tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:11px}
116
  .tile{background:#f8f9fc;border:1px solid var(--line2);border-radius:11px;padding:12px}
117
  .tile .t{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:600}
118
  .tile .v{font-size:21px;font-weight:760;margin-top:5px}
119
  .tile .v small{font-size:12px;color:var(--soft);font-weight:600}
120
  .util{display:flex;height:30px;border-radius:8px;overflow:hidden}
121
  .util div{display:flex;align-items:center;justify-content:center;font-size:11.5px;font-weight:650;color:#fff}
122
  .utlegend{display:flex;gap:18px;margin-top:12px;font-size:12px;color:var(--muted);flex-wrap:wrap}
123
  .utlegend span{display:inline-flex;align-items:center;gap:7px}
124
  .callout{background:linear-gradient(180deg,#fbfcff,#f4f7ff);border:1px solid #dfe6fb;border-radius:14px;padding:18px 20px;margin-top:6px}
125
  .callout h2{margin:0 0 3px;font-size:16px}
126
  .callout .desc{color:var(--muted);font-size:12.5px;margin-bottom:14px}
127
  .fix{display:flex;gap:13px;padding:13px 0;border-bottom:1px solid #e6ebf8}
128
  .fix:last-child{border-bottom:0}
129
  .fix .sev{width:6px;border-radius:4px;flex:none}
130
  .fix .fb{flex:1}
131
  .fix .ft{font-size:14px;font-weight:700}
132
  .fix .fd{font-size:13px;color:#3a4459;margin-top:4px;line-height:1.5}
133
  .fix .fd b{color:var(--ink)}
134
  .fix .act{font-size:12.5px;color:var(--good);margin-top:6px;font-weight:650}
135
  .hide{display:none}
136
  .loading{padding:70px;text-align:center;color:var(--muted)}
137
  .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}
138
  @keyframes spin{to{transform:rotate(360deg)}}
139
  .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}
140
  .modal-back.show{display:flex}
36882 vikas 141
  .modal{background:#fff;border-radius:14px;max-width:780px;width:100%;padding:20px 22px;box-shadow:0 20px 60px rgba(10,20,50,.3);position:relative}
142
  .modal h3{margin:0 0 2px;font-size:17px;text-align:center}
143
  .modal > .desc{text-align:center}
144
  .modal .x{position:absolute;top:14px;right:16px;cursor:pointer;color:var(--soft);font-size:22px;line-height:1;border:0;background:none}
145
  /* journey totals bar */
146
  .jstats{display:flex;gap:9px;margin:12px 0 2px;flex-wrap:wrap}
147
  .js{flex:1;min-width:104px;background:#f8f9fc;border:1px solid var(--line2);border-radius:10px;padding:9px 10px;text-align:center}
148
  .js .jv{font-size:15.5px;font-weight:760;color:var(--ink);font-variant-numeric:tabular-nums}
149
  .js .jl{font-size:10px;color:var(--soft);text-transform:uppercase;letter-spacing:.04em;margin-top:3px;font-weight:600}
36877 vikas 150
  .btn-ok{background:var(--good);border:0;color:#fff;font-weight:650;padding:6px 12px;border-radius:7px;cursor:pointer;font-size:12px}
36882 vikas 151
  /* journey timeline (exec drilldown) */
152
  .tl{position:relative;margin:14px 2px 4px;padding:4px 0}
153
  .tl::before{content:"";position:absolute;left:50%;top:6px;bottom:6px;width:2px;background:#e9edf5;transform:translateX(-50%)}
154
  .tlgap{position:relative;display:flex;justify-content:center;margin:-3px 0}
155
  .tlgap .gappill{background:#fff;border:1px solid var(--line);border-radius:999px;padding:3px 11px;font-size:11px;font-weight:600;color:var(--muted);z-index:2;box-shadow:0 1px 2px rgba(20,30,60,.05);font-variant-numeric:tabular-nums}
156
  .tlrow{position:relative;display:flex;margin:14px 0}
157
  .tlrow.left{justify-content:flex-start}
158
  .tlrow.right{justify-content:flex-end}
159
  .tlrow .node{position:absolute;left:50%;top:8px;transform:translateX(-50%);width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:13px;z-index:2;box-shadow:0 0 0 4px #fff}
160
  .tlcard{width:calc(50% - 26px);background:#fff;border:1px solid var(--line);border-radius:11px;box-shadow:0 2px 10px rgba(20,30,60,.07);display:flex;overflow:hidden}
161
  .tlrow.left .tlcard{flex-direction:row-reverse;text-align:right}
162
  .tlrow.right .tlcard{flex-direction:row;text-align:left}
163
  .tlcard .tm{flex:none;width:62px;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;font-weight:750;font-size:13px;padding:6px 4px;line-height:1.05}
164
  .tlcard .tm small{font-weight:600;font-size:8.5px;opacity:.9;text-transform:uppercase;letter-spacing:.04em;margin-top:3px}
165
  .tlcard .bd{padding:8px 11px;flex:1;min-width:0}
166
  .tlcard .bd .ti{font-size:12.5px;font-weight:700;color:var(--ink);line-height:1.25;word-break:break-word}
167
  .tlcard .bd .stt{display:inline-block;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;padding:1px 6px;border-radius:5px;margin-top:5px;color:#fff}
168
  .tlcard .bd .mt{font-size:11px;color:var(--muted);margin-top:6px}
169
  .tlextra{display:flex;align-items:center;gap:7px;margin-top:8px;flex-wrap:wrap}
170
  .tlrow.left .tlextra{justify-content:flex-end}
171
  .tlrow.right .tlextra{justify-content:flex-start}
172
  .tlthumb{width:46px;height:46px;border-radius:8px;object-fit:cover;border:1px solid var(--line);cursor:pointer;background:#f1f3f9}
173
  .tlph{width:46px;height:46px;border-radius:8px;border:1px dashed #cfd6e6;background:#f4f6fb;display:flex;align-items:center;justify-content:center;color:#b3bcce;font-size:18px}
174
  .tlchip{font-size:10.5px;font-weight:600;color:var(--muted);background:#f1f3f9;border-radius:6px;padding:2px 7px;white-space:nowrap}
36897 vikas 175
  .exectabs{display:flex;gap:6px;justify-content:center;margin:12px 0 4px}
176
  .exectab{padding:6px 14px;border-radius:8px;font-size:12.5px;font-weight:650;color:var(--muted);cursor:pointer;border:1px solid var(--line);background:#fff}
177
  .exectab.active{background:var(--brand);color:#fff;border-color:var(--brand)}
178
  #bjMap{width:100%;height:460px;border-radius:12px;border:1px solid var(--line);margin-top:10px;background:#eef1f7;overflow:hidden}
179
  .valwrap{display:flex;justify-content:center;margin:14px 0 6px}
180
  .valimg{max-width:100%;max-height:440px;border-radius:12px;border:1px solid var(--line);cursor:zoom-in}
181
  .valph{width:100%;padding:48px;text-align:center;color:var(--soft);border:1px dashed #cfd6e6;border-radius:12px;background:#f4f6fb;font-size:14px}
182
  .valactions{display:flex;justify-content:center;gap:10px;margin-top:8px}
36877 vikas 183
  @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}}
36882 vikas 184
  @media (max-width:560px){.tl::before{left:20px}.tlrow.left,.tlrow.right{justify-content:flex-end}.tlrow .node{left:20px}.tlcard{width:calc(100% - 44px)}.tlrow.left .tlcard{flex-direction:row;text-align:left}}
36877 vikas 185
</style>
186
</head>
187
<body>
188
<div class="wrap">
189
  <header class="top">
190
    <div class="brand">
191
      <div class="crumb"><a href="${rc.contextPath}/dashboard">&larr; Dashboard</a> &middot; Field Sales &middot; Beat / Monthly PJP</div>
192
      <h1>Beat Journey <span class="live"><i></i>Live</span></h1>
193
    </div>
194
    <div class="hd-right">
195
      <select class="ctl" id="bj-level">
196
        <option value="">All levels below me</option>
197
        #foreach($lvl in $escalationTypes)
198
          <option value="$lvl">$lvl</option>
199
        #end
200
      </select>
36897 vikas 201
      #if($topLevel)
202
      <select class="ctl hide" id="bj-user">
203
        <option value="">All users at level</option>
204
      </select>
205
      #end
36877 vikas 206
      <input class="ctl" type="date" id="bj-date" value="$today">
207
      <span id="bj-period-range" class="hide">
208
        <input class="ctl" type="date" id="bj-start" value="$monthStart">
209
        <input class="ctl" type="date" id="bj-end" value="$today">
210
      </span>
211
      <button class="btn-r" id="bj-apply">Apply</button>
212
    </div>
213
  </header>
214
 
215
  <div class="lenses">
216
    <button class="lens active" data-lens="today" id="lens-today">Today (Live)</button>
217
    <button class="lens" data-lens="period" id="lens-period">Period (Analytics)</button>
218
  </div>
219
 
220
  <div id="bj-content"><div class="loading"><div class="spinner"></div>Loading&hellip;</div></div>
221
</div>
222
 
223
<div class="modal-back" id="bj-modal-back">
224
  <div class="modal" id="bj-modal"></div>
225
</div>
226
 
227
<script>
228
var CTX = '${rc.contextPath}';
229
var BASE = CTX + '/beat-journey';
230
var LENS = 'today';
36897 vikas 231
var TODAY = '$today';
36877 vikas 232
 
233
function el(id){ return document.getElementById(id); }
36897 vikas 234
// The first lens reads "Today (Live)" for today's date, otherwise it shows the
235
// selected date (the tab is still the live/single-day view, just for that day).
236
function fmtDateLabel(d){
237
  var p=(d||'').split('-'); if(p.length!==3) return d;
238
  var mo=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
239
  return parseInt(p[2],10)+' '+(mo[parseInt(p[1],10)-1]||p[1])+' '+p[0];
240
}
241
function updateTodayLabel(){
242
  var b=el('bj-date'), lbl=el('lens-today'); if(!lbl) return;
243
  var d=b?b.value:'';
244
  lbl.innerHTML = (!d || d===TODAY) ? 'Today (Live)' : esc(fmtDateLabel(d));
245
}
36877 vikas 246
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
247
function num(n){ return (n==null?0:n).toLocaleString(); }
248
function adhColor(p){ if(p>=80)return '#2f9e44'; if(p>=60)return '#f08c00'; if(p>=40)return '#e8590c'; return '#e03131'; }
249
function pct(a,b){ return b? Math.round(100*a/b):0; }
250
function qs(){
251
  var lvl = el('bj-level').value;
252
  var s = '';
253
  if(lvl) s += '&escalationType=' + encodeURIComponent(lvl);
36897 vikas 254
  var usr = el('bj-user');           // only present for L5/Final
255
  if(usr && usr.value) s += '&teamOfAuthUserId=' + encodeURIComponent(usr.value);
36877 vikas 256
  return s;
257
}
36897 vikas 258
// L5/Final: when a level is picked, list its users so a specific manager's team
259
// can be selected. Selecting a user scopes the whole view to that user's team.
260
function onLevelChange(){
261
  var usr = el('bj-user');
262
  if(!usr){ load(); return; }       // not top-level: just reload on level change
263
  var lvl = el('bj-level').value;
264
  usr.innerHTML = '<option value="">All users at level</option>';
265
  if(!lvl){ usr.classList.add('hide'); load(); return; }
266
  fetch(BASE + '/users?escalationType=' + encodeURIComponent(lvl), {headers:{'Accept':'application/json'}})
267
    .then(function(r){ return r.json(); })
268
    .then(function(d){
269
      var us = d.users || [];
270
      var h = '<option value="">All ' + esc(lvl) + ' users</option>';
271
      us.forEach(function(u){ h += '<option value="' + u.id + '">' + esc(u.name) + '</option>'; });
272
      usr.innerHTML = h;
273
      usr.classList.toggle('hide', us.length === 0);
274
      load();
275
    })
276
    .catch(function(){ usr.classList.add('hide'); load(); });
277
}
36877 vikas 278
function spinner(){ el('bj-content').innerHTML = '<div class="loading"><div class="spinner"></div>Loading&hellip;</div>'; }
279
function fail(e){ el('bj-content').innerHTML = '<div class="loading">Could not load data ('+esc(e&&e.message||e)+'). Are you signed in?</div>'; }
280
 
281
function setLens(name){
282
  LENS = name;
283
  var tabs = document.querySelectorAll('.lens');
284
  for(var i=0;i<tabs.length;i++){ tabs[i].classList.toggle('active', tabs[i].getAttribute('data-lens')===name); }
285
  el('bj-date').classList.toggle('hide', name!=='today');
286
  el('bj-period-range').classList.toggle('hide', name==='today');
287
  load();
288
}
289
function load(){
290
  if(LENS==='today') return loadToday();
291
  if(LENS==='period') return loadPeriod();
292
  return loadApproval();
293
}
294
 
295
/* ============================ TODAY ============================ */
296
function loadToday(){
36897 vikas 297
  updateTodayLabel();
36877 vikas 298
  spinner();
299
  fetch(BASE + '/today?date=' + encodeURIComponent(el('bj-date').value) + qs(), {headers:{'Accept':'application/json'}})
300
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
301
    .then(renderToday).catch(fail);
302
}
303
function kpi(ic, bg, fg, label, val, foot, valColor){
304
  return '<div class="kpi"><div class="top"><div class="ic" style="background:'+bg+';color:'+fg+'">'+ic+'</div>'+
305
    '<div class="label">'+label+'</div></div><div class="val"'+(valColor?' style="color:'+valColor+'"':'')+'>'+val+'</div>'+
306
    '<div class="foot">'+foot+'</div></div>';
307
}
308
function fstep(name, val, w, sub, drop, last){
309
  return '<div class="fstep"><div class="fn">'+name+'</div><div class="fv">'+num(val)+'</div>'+
310
    '<div class="fbar" style="width:'+w+'%"></div><div class="'+(drop?'drop':'keep')+'">'+sub+'</div>'+
311
    (last?'':'<span class="arrow">&#10148;</span>')+'</div>';
312
}
313
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>'; }
314
 
315
function renderToday(d){
316
  var k=d.kpis, f=d.funnel, s=d.stops, a=d.attention;
36897 vikas 317
  window._canApprove = !!d.canApprove;
318
  window._mediaUrl = d.mediaUrl || window._mediaUrl || '';
36877 vikas 319
  var h='';
320
  h+='<div class="kpis">';
321
  h+=kpi('&#128101;','#eef1fb','#3b5bdb','Executives scheduled',num(k.scheduled),k.notStarted+' not started yet');
322
  h+=kpi('&#9201;','#ebfbee','#2f9e44','Punched in',num(k.punchedIn)+' <small>/'+k.scheduled+'</small>',k.notStarted+' pending');
323
  h+=kpi('&#128205;','#e7f5ff','#1098ad','On route now',num(k.onRouteNow),'live journeys');
324
  h+=kpi('&#10003;','#eef1fb','#3b5bdb','Visits done',num(k.visitsDone)+' <small>/'+k.visitsPlanned+'</small>',k.inProgress+' in progress');
325
  h+=kpi('&#8635;','#fff4e6','#e8590c','Deferred today',num(k.deferred),'shop closed / unavailable');
326
  h+=kpi('&#9733;','#fff0f6','#d6336c','Leads on route',num(k.leads),'new shops logged');
327
  h+='</div>';
328
 
329
  h+='<div class="sectionlabel">Today\'s Journey Funnel</div><div class="card"><div class="funnel">';
330
  var sc=f.scheduled||1;
331
  h+=fstep('Scheduled executives',f.scheduled,100,'start of day',false,false);
332
  h+=fstep('Punched in',f.punchedIn,pct(f.punchedIn,sc),'&minus;'+(f.scheduled-f.punchedIn)+' never started',true,false);
333
  h+=fstep('Made a check-in',f.madeCheckin,pct(f.madeCheckin,sc),'&minus;'+(f.punchedIn-f.madeCheckin)+' punched, idle',true,false);
334
  h+=fstep('Completed all visits',f.completedAll,pct(f.completedAll,sc),(f.madeCheckin-f.completedAll)+' still mid-journey',true,false);
335
  h+=fstep('Punched out',f.punchedOut,pct(f.punchedOut,sc),'journey ended',false,true);
336
  h+='</div></div>';
337
 
338
  h+='<div class="grid g3">';
339
  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>';
340
  h+='<div class="card"><h2>Needs attention now</h2><p class="desc">Things a manager should clear today.</p>'+attentionList(a)+'</div>';
341
  h+='</div>';
342
  el('bj-content').innerHTML=h;
343
}
344
function stateClass(st){ return st==='IN_STORE'?'ps-store':st==='TRAVELLING'?'ps-travel':st==='DONE'?'ps-done':st==='NOT_STARTED'?'ps-no':'ps-idle'; }
36897 vikas 345
function js(s){ return String(s==null?'':s).replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
346
// Open the Beat Planner (map-first planner window) for this executive/beat/day,
347
// in a new tab. Edit pre-selects the beat; add opens it to assign/add a visit.
348
function openPlanner(authUserId, beatId, date, mode){
349
  var u = CTX + '/beatPlanWindow?autoUserId=' + encodeURIComponent(authUserId) + '&editDate=' + encodeURIComponent(date);
350
  if(beatId && beatId>0) u += '&editBeatId=' + encodeURIComponent(beatId);
351
  if(mode==='add') u += '&assign=1';
352
  window.open(u, '_blank');
353
}
36877 vikas 354
function boardTable(rows, date){
355
  if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
36897 vikas 356
  var h='<div style="overflow-x:auto"><table><thead><tr>'+
357
    '<th>Agenda</th><th>Executive</th><th class="num">Planned</th>'+
358
    '<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>'+
359
    '</tr></thead><tbody>';
36877 vikas 360
  rows.forEach(function(r){
36897 vikas 361
    var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (r.idleAway?'rowwarn':'');
362
    var A = r.userId+",'"+js(r.name)+"','"+date+"'";   // openExec args
363
    var go = 'onclick="openExec('+A+')"';
364
    h+='<tr class="clickable '+rowcls+'" '+go+'>'+
365
       '<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>'+
366
       '<td><b>'+esc(r.name)+'</b><br>'+esc(r.beatName||'-')+'</td>'+
367
       '<td class="num">'+r.planned+'</td>'+
368
       '<td style="white-space:nowrap">'+(r.punchInTime?'<span class="ptime">'+esc(r.punchInTime)+'</span>':'<span class="badflag">-</span>')+
369
         ' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
36877 vikas 370
       '<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
36897 vikas 371
       '<td class="num cnt cmuted">'+r.totalVisits+'</td>'+
372
       '<td class="num cnt cgreen">'+r.completedVisits+'</td>'+
373
       '<td class="num cnt camber">'+r.pendingVisits+'</td>'+
374
       '<td class="num cnt cred">'+r.deferred+'</td>'+
375
       '<td style="white-space:nowrap" onclick="event.stopPropagation()">'+
376
         '<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">&#128205;</button>'+
377
         '<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">&#128065;</button>'+
378
         '<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">&#9998;</button>'+
379
         '<button class="actbtn add" title="Assign / add visit in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'add\')">&#43;</button>'+
380
       '</td></tr>';
36877 vikas 381
  });
36897 vikas 382
  return h+'</tbody></table></div>';
36877 vikas 383
}
384
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; }
36882 vikas 385
var ATT_META = {
386
  notPunchedIn:    {bg:'#fff0f0', fg:'#e03131', ic:'&#9888;',   title:'Not punched in',                  sub:'executives with a beat, not started', cls:'badflag'},
36897 vikas 387
  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'},
36882 vikas 388
  geofence:        {bg:'#fff0f0', fg:'#e03131', ic:'&#128205;', title:'Check-in outside store geofence',  sub:'&gt;50 m from partner\'s saved location', cls:'badflag'},
389
  locationOff:     {bg:'#fff4e6', fg:'#e8590c', ic:'&#128244;', title:'Location turned off mid-journey',  sub:'GPS toggled off',                     cls:'warnflag'},
390
  deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'&#8635;',   title:'Deferred, awaiting re-action',     sub:'shops deferred today',                cls:''}
391
};
36877 vikas 392
function attentionList(a){
36882 vikas 393
  window._att = a;
394
  var order=['notPunchedIn','idleStale','geofence','locationOff','deferredAwaiting'];
36877 vikas 395
  var h='';
36882 vikas 396
  order.forEach(function(k){
397
    var m=ATT_META[k], list=a[k]||[], n=list.length;
398
    var detail = n ? names(list.map(function(x){return x.name;}),3) : m.sub;
399
    var cls = n ? ' att clickable' : ' att';
400
    var click = n ? ' onclick="openAttention(\''+k+'\')"' : '';
401
    h+='<div class="'+cls.trim()+'"'+click+'><div class="ic" style="background:'+m.bg+';color:'+m.fg+'">'+m.ic+'</div>'+
402
       '<div class="ab"><div class="atitle">'+m.title+'</div><div class="adet">'+detail+'</div></div>'+
403
       '<div class="acount '+(n?m.cls:'')+'">'+n+'</div>'+(n?'<span class="chev">&#9656;</span>':'')+'</div>';
404
  });
36877 vikas 405
  return h;
406
}
36882 vikas 407
function openAttention(key){
408
  var a=window._att||{}, list=a[key]||[], m=ATT_META[key]||{title:'Detail'};
409
  var mb=el('bj-modal-back'), md=el('bj-modal');
36897 vikas 410
  // Off-site check-ins can be approved here, but only by L5/Final (canApprove).
411
  var showApprove = (key==='geofence' && window._canApprove);
36882 vikas 412
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+m.title+'</h3>'+
36897 vikas 413
        '<p class="desc">'+list.length+' '+(list.length===1?'item':'items')+
414
        (showApprove?' &middot; approving updates the partner\'s saved coordinates':'')+'</p>';
36882 vikas 415
  if(!list.length){ h+='<p class="desc">Nothing here.</p>'; }
416
  else{
417
    h+='<table><tbody>';
418
    list.forEach(function(x){
36897 vikas 419
      var act='';
420
      if(showApprove) act='<td class="num" style="white-space:nowrap">'+(x.trackingId?'<button class="btn-ok" onclick="openValidate('+x.trackingId+')">Validate &amp; Approve</button>':'')+'</td>';
36882 vikas 421
      h+='<tr><td style="font-weight:600;white-space:nowrap">'+esc(x.name)+'</td>'+
36897 vikas 422
         '<td style="color:#69748c">'+esc(x.detail||'')+'</td>'+act+'</tr>';
36882 vikas 423
    });
424
    h+='</tbody></table>';
425
  }
426
  md.innerHTML=h; mb.classList.add('show');
427
}
36897 vikas 428
// Step 2: review the check-in photo/document before approving.
429
function openValidate(trackingId){
430
  var list=(window._att&&window._att.geofence)||[], item=null;
431
  for(var i=0;i<list.length;i++){ if(list[i].trackingId===trackingId){ item=list[i]; break; } }
432
  if(!item){ return; }
433
  var img = (item.attachment && window._mediaUrl)
434
    ? '<img class="valimg" src="'+esc(window._mediaUrl+item.attachment)+'" onclick="window.open(this.src,\'_blank\')" onerror="this.outerHTML=\'<div class=&quot;valph&quot;>&#128247; Photo unavailable</div>\'">'
435
    : '<div class="valph">&#128247; No photo on this check-in</div>';
436
  el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">&times;</button><h3>Validate check-in</h3>'+
437
    '<p class="desc">'+esc(item.name)+' &middot; '+esc(item.detail||'')+'</p>'+
438
    '<div class="valwrap">'+img+'</div>'+
439
    '<div class="valactions">'+
440
      '<button class="exectab" onclick="openAttention(\'geofence\')">&larr; Back</button>'+
441
      '<button class="btn-ok" onclick="approveValidated('+trackingId+',this)">Approve</button>'+
442
    '</div>';
443
}
444
function approveValidated(trackingId, btn){
445
  btn.disabled=true; btn.textContent='Approving…';
446
  fetch(CTX+'/beat-journey/approve-checkin',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'trackingId='+trackingId})
447
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
448
    .then(function(){
449
      if(window._att && window._att.geofence){ window._att.geofence = window._att.geofence.filter(function(x){ return x.trackingId!==trackingId; }); }
450
      if(window._att && window._att.geofence && window._att.geofence.length) openAttention('geofence');
451
      else closeModal();
452
    })
453
    .catch(function(){ btn.disabled=false; btn.textContent='Approve'; alert('Approve failed (not authorised, or server error).'); });
454
}
36877 vikas 455
 
456
/* ============================ PERIOD ============================ */
457
function loadPeriod(){
458
  spinner();
459
  var u = BASE + '/data?startDate=' + encodeURIComponent(el('bj-start').value) + '&endDate=' + encodeURIComponent(el('bj-end').value) + qs();
460
  fetch(u, {headers:{'Accept':'application/json'}})
461
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
462
    .then(renderPeriod).catch(fail);
463
}
464
function tile(t,v){ return '<div class="tile"><div class="t">'+t+'</div><div class="v">'+v+'</div></div>'; }
465
function renderPeriod(d){
466
  var k=d.kpis, f=d.funnel, df=d.deferral, u=d.utilization, t=d.travel, o=d.outcomes;
467
  var neverTouched = Math.max((f.plannedStops||0)-(f.checkedIn||0)-(df.total||0),0);
468
  var h='';
469
  h+='<div class="kpis">';
470
  h+=kpi('&#9878;','#fff4e6','#f08c00','Adherence',k.adherencePct+'<small>%</small>',num(k.doneStops)+' of '+num(k.plannedStops)+' stops done',adhColor(k.adherencePct));
471
  h+=kpi('&#128506;','#e7f5ff','#1098ad','PJP coverage L1',k.coverageL1Pct+'<small>%</small>',k.coverageL1With+'/'+k.coverageL1Total+' have a plan','#1098ad');
472
  h+=kpi('&#128694;','#ebfbee','#2f9e44','Visits / active day',k.avgVisitsPerActiveDay,'across '+num(k.journeys)+' journeys');
473
  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);
474
  h+=kpi('&#8635;','#fff0f0','#e03131','Auto "missed"',num(k.autoMissed),k.autoMissedSystemPct+'% system-flagged','#e03131');
475
  h+=kpi('&#9733;','#fff0f6','#d6336c','Leads on route',num(k.leadsCreated),'this period');
476
  h+='</div>';
477
 
478
  h+='<div class="sectionlabel">Aggregate Journey Funnel</div><div class="card"><div class="funnel">';
479
  var pl=f.plannedStops||1;
480
  h+=fstep('Planned stops',f.plannedStops,100,'scheduled',false,false);
481
  h+=fstep('Days punched in',f.punchedIn,pct(f.punchedIn,pl),'&minus;'+f.dropPunch+'% no punch-in',true,false);
482
  h+=fstep('Checked in',f.checkedIn,pct(f.checkedIn,pl),'&minus;'+f.dropCheckin+'% reached, skipped',true,false);
483
  h+=fstep('Checked out (done)',f.checkedOut,pct(f.checkedOut,pl),'&minus;'+f.dropCheckout+'% no checkout',false,true);
484
  h+='</div><div class="leak">';
485
  h+=leakbox('Deferred &mdash; genuine',df.human,'#2f9e44');
486
  h+=leakbox('Deferred &mdash; system flag',df.systemGenerated,'#f08c00');
487
  h+=leakbox('Heads acted on',df.actioned,null);
488
  h+=leakbox('Never touched',neverTouched,'#e03131');
489
  h+='</div></div>';
490
 
491
  h+='<div class="sectionlabel">Adherence &amp; Coverage</div><div class="grid g3">';
492
  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>';
493
  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>';
494
  h+='</div>';
495
 
496
  h+='<div class="sectionlabel">Deferrals, Travel &amp; Outcomes</div><div class="grid g2">';
497
  h+='<div class="card"><h2>Deferral nature</h2><p class="desc">Deliberate, or just auto-flagged missed beats?</p>'+deferDonut(df)+'</div>';
498
  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>';
499
  h+='</div>';
500
 
501
  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>'+
502
     '<div class="tiles" style="grid-template-columns:repeat(4,1fr)">'+
503
     tile('Partner remark filled', o.remarkFilledPct+'<small>%</small>')+
504
     tile('Avg partner rating', (o.avgRating||0)+'<small>/5</small>')+
505
     tile('Audit checklist done', o.auditDonePct+'<small>%</small>')+
506
     tile('Next-visit follow-up set', o.nextVisitPct+'<small>%</small>')+
507
     '</div></div>';
508
 
509
  h+='<div class="sectionlabel">What to fix</div>'+findingsBlock(d.findings);
510
  h+=sourcesCard();
511
  el('bj-content').innerHTML=h;
512
  window._scorecard = d.scorecard;
513
}
514
var sortKey=null, sortAsc=false;
515
function sortCard(key){
516
  if(sortKey===key) sortAsc=!sortAsc; else { sortKey=key; sortAsc=false; }
517
  window._scorecard.sort(function(a,b){ var x=a[key],y=b[key];
518
    if(typeof x==='string') return sortAsc? (''+x).localeCompare(y):(''+y).localeCompare(x);
519
    return sortAsc? x-y : y-x; });
520
  document.getElementById('scTbl').outerHTML = scorecardTable(window._scorecard);
521
}
522
function scorecardTable(rows){
523
  if(!rows||!rows.length) return '<p class="desc">No beat owners in scope for this period.</p>';
524
  var h='<table id="scTbl"><thead><tr>'+
525
    '<th onclick="sortCard(\'name\')">Executive</th>'+
526
    '<th class="num" onclick="sortCard(\'planned\')">Plan</th>'+
527
    '<th class="num" onclick="sortCard(\'done\')">Done</th>'+
528
    '<th class="num" onclick="sortCard(\'adherencePct\')">Adher.</th>'+
529
    '<th class="num" onclick="sortCard(\'discussionMin\')">Disc.</th>'+
530
    '<th class="num" onclick="sortCard(\'workingHrs\')">Hrs</th>'+
531
    '<th class="num" onclick="sortCard(\'leads\')">Leads</th>'+
532
    '<th class="num" onclick="sortCard(\'geoFlags\')">Geo&#9888;</th></tr></thead><tbody>';
533
  rows.forEach(function(r){
534
    var c=adhColor(r.adherencePct);
535
    h+='<tr><td>'+esc(r.name)+' <span class="lvl">'+esc(r.level)+'</span></td>'+
536
       '<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
537
       '<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>'+
36882 vikas 538
       '<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'-')+(r.discussionMin?'m':'')+'</td>'+
36877 vikas 539
       '<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td>'+
540
       '<td class="num'+(r.geoFlags>0?' badflag':'')+'">'+r.geoFlags+'</td></tr>';
541
  });
542
  return h+'</tbody></table>';
543
}
544
function coverageBars(rows){
545
  if(!rows||!rows.length) return '<p class="desc">No coverage data.</p>';
546
  var max=0; rows.forEach(function(r){ if(r.totalUsers>max)max=r.totalUsers; }); if(!max)max=1;
547
  var h=''; rows.forEach(function(r){
548
    var w=pct(r.withPjp,max), wt=pct(r.totalUsers,max);
549
    h+='<div class="barrow"><div class="name">'+esc(r.level)+'</div><div class="track">'+
550
       '<div style="position:absolute;inset:0;width:'+wt+'%;background:#e3e8f5;border-radius:7px"></div>'+
551
       '<div class="fill" style="width:'+w+'%;position:relative;background:linear-gradient(90deg,#1098ad,#22b8cf)">'+(r.withPjp||'')+'</div></div>'+
552
       '<div class="meta">'+r.withPjp+'/'+r.totalUsers+' &middot; '+r.coveragePct+'%</div></div>';
553
  });
554
  return h;
555
}
556
function utilBlock(u){
557
  return '<h2 style="margin-top:18px">Working-hour utilization</h2><p class="desc">Where the average working day goes.</p>'+
558
    '<div class="util">'+
559
    '<div style="width:'+u.inStorePct+'%;background:#2f9e44">In-store '+u.inStorePct+'%</div>'+
560
    '<div style="width:'+u.travelPct+'%;background:#3b5bdb">Travel '+u.travelPct+'%</div>'+
561
    '<div style="width:'+u.idlePct+'%;background:#e03131">Idle '+u.idlePct+'%</div></div>'+
562
    '<div class="utlegend"><span><span class="dot" style="background:#2f9e44;margin:0"></span>Selling '+u.inStoreHrs+' h</span>'+
563
    '<span><span class="dot" style="background:#3b5bdb;margin:0"></span>Travel '+u.travelHrs+' h</span>'+
564
    '<span><span class="dot" style="background:#e03131;margin:0"></span>Idle '+u.idleHrs+' h</span></div>';
565
}
566
function deferDonut(df){
567
  var sysPct=df.systemPct||0;
568
  var g='conic-gradient(#f59f00 0 '+sysPct+'%, #2f9e44 '+sysPct+'% 100%)';
569
  return '<div class="donutwrap"><div class="donut" style="background:'+g+'"><div class="center">'+
570
    '<div style="font-size:24px;font-weight:780">'+num(df.total)+'</div><div style="font-size:11px;color:#97a1b5">deferrals</div></div></div>'+
571
    '<div class="legend">'+
572
    '<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>'+
573
    '<div class="it"><span class="dot" style="background:#2f9e44"></span><div><b>'+df.human+'</b> genuine reason ('+df.humanPct+'%)</div></div>'+
574
    '<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>'+
575
    '</div></div><p class="desc" style="margin-top:13px">"System" vs "genuine" is read from the deferral reason; there is no separate flag.</p>';
576
}
577
function travelBlock(t){
578
  var rows=t.legBuckets||[], max=0; rows.forEach(function(r){ if(r.count>max)max=r.count; }); if(!max)max=1;
579
  var colors={'0-2 km':'#2f9e44','2-5 km':'#2f9e44','5-15 km':'#82c91e','15-50 km':'#f59f00','50 km+':'#e03131'};
580
  var h=''; rows.forEach(function(r){
581
    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>';
582
  });
583
  h+='<div class="tiles" style="margin-top:14px">'+
584
     tile('Avg leg', t.avgLegKm+'<small> km</small>')+
585
     tile('Legs &gt; 15 km', t.pctOver15+'<small>%</small>')+
586
     tile('Actual / estimate', (t.actualVsEstimateRatio||0)+'<small>&times;</small>')+'</div>';
587
  return h;
588
}
589
function findingsBlock(items){
590
  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>';
591
  var sev={high:'#e03131',med:'#f59f00',low:'#2f9e44'};
592
  var h='<div class="callout"><h2>Recommended management actions</h2><p class="desc">Generated from this period\'s data.</p>';
593
  items.forEach(function(x){
594
    h+='<div class="fix"><div class="sev" style="background:'+(sev[x.severity]||'#3b5bdb')+'"></div><div class="fb">'+
595
       '<div class="ft">'+esc(x.title)+'</div><div class="fd">'+esc(x.detail)+'</div>'+
596
       '<div class="act">&rarr; '+esc(x.action)+'</div></div></div>';
597
  });
598
  return h+'</div>';
599
}
600
function sourcesCard(){
601
  return '<div class="sectionlabel">Where the numbers come from</div><div class="card">'+
602
    '<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>'+
603
    '<ul style="margin:0;padding-left:18px;line-height:1.95;font-size:13px;color:#3a4459">'+
604
    '<li>The <b>journey timeline</b> (punch-in, check-ins/outs, deferrals, punch-out) comes from what the executive taps in the app.</li>'+
605
    '<li><b>Adherence</b> compares the partners planned on each beat against the stores actually visited.</li>'+
606
    '<li><b>Time, travel and distance</b> use the in-store and travel durations logged per visit.</li>'+
607
    '<li><b>Geofence / fake-visit checks</b> compare the check-in GPS against the partner\'s saved shop location (flagged beyond ~50 m).</li>'+
608
    '<li><b>Visit outcomes</b> (remarks, rating, audit, next visit) come from the check-out form.</li>'+
609
    '<li><b>Coverage and the manager view</b> follow the existing L1&ndash;L5 sales hierarchy, so each manager sees only their team.</li>'+
610
    '</ul></div>';
611
}
612
 
613
/* ============================ EXEC DRILLDOWN ============================ */
36897 vikas 614
function openExec(userId, name, date, tab){
36877 vikas 615
  if(!userId){ return; }
36897 vikas 616
  window._exec = {userId:userId, name:name, date:date, detail:null, tab:(tab||'timeline')};
36877 vikas 617
  var mb=el('bj-modal-back'), m=el('bj-modal');
618
  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>';
619
  mb.classList.add('show');
36882 vikas 620
  fetch(CTX+'/beat-journey/journey-detail?userId='+userId+'&date='+encodeURIComponent(date),{headers:{'Accept':'application/json'}})
36877 vikas 621
    .then(function(r){ return r.json(); }).then(function(d){
36882 vikas 622
      window._mediaUrl = d.mediaUrl || '';
36897 vikas 623
      window._exec.detail = d;
624
      renderExec(window._exec.tab);
625
    }).catch(function(){ el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">&times;</button><p class="desc">Could not load detail.</p>'; });
626
}
627
function renderExec(tab){
628
  var ex=window._exec; if(!ex) return;
629
  var d=ex.detail||{};
630
  var head='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(ex.name)+'</h3>'+
631
    '<p class="desc">Journey detail &middot; '+esc(ex.date)+' &middot; battery '+(d.batteryLevel==null?'-':d.batteryLevel+'%')+' &middot; loc-off '+(d.locationToggleCount||0)+'</p>'+
632
    '<div class="exectabs">'+
633
      '<button class="exectab'+(tab==='timeline'?' active':'')+'" onclick="renderExec(\'timeline\')">&#128221; Timeline</button>'+
634
      '<button class="exectab'+(tab==='map'?' active':'')+'" onclick="renderExec(\'map\')">&#128205; Live Location</button>'+
635
    '</div>';
636
  if(tab==='map'){
637
    el('bj-modal').innerHTML=head+'<div id="bjMap"><div class="loading"><div class="spinner"></div>Loading map&hellip;</div></div>';
638
    bjShowMap(ex.userId, ex.date);
639
  } else {
640
    el('bj-modal').innerHTML=head+buildTimelineHtml(d, ex.date);
641
  }
642
}
643
function buildTimelineHtml(d, date){
644
  var all=(d.visits||[]).slice();
645
  // Sequence like the field report: Punch in -> visited stops (by check-in time)
646
  // -> PENDING/not-visited -> Punch out. The attendance row carries both punch-in
647
  // & punch-out times; split it into the two bookends.
648
  var att=null, visited=[], pending=[];
649
  all.forEach(function(x){
650
    var tt=(x.taskType||'').toLowerCase(), mt=(x.markType||'').toUpperCase();
651
    if(tt==='attendance' || mt.indexOf('PUNCH')>=0){ if(!att) att=x; }
652
    else if(x.checkInTime){ visited.push(x); }
653
    else { pending.push(x); }
654
  });
655
  visited.sort(function(a,b){ return tlSecs(a.checkInTime)-tlSecs(b.checkInTime); });
36882 vikas 656
 
36897 vikas 657
  var distKm=0, inStoreSec=0, transitSec=0, leadsN=0;
658
  all.forEach(function(x){
659
    var tt=(x.taskType||'').toLowerCase();
660
    if(tt==='attendance') return;
661
    if(x.totalDistance) distKm += (parseFloat(x.totalDistance)||0);
662
    inStoreSec += toSec(x.timeSpent);
663
    transitSec += toSec(x.transitTime);
664
    if(tt==='lead') leadsN++;
665
  });
666
  var workSec = (att && att.checkInTime && att.checkOutTime) ? (toSec(att.checkOutTime)-toSec(att.checkInTime)) : (att?toSec(att.timeSpent):0);
667
  if(workSec<0) workSec=0;
36882 vikas 668
 
36897 vikas 669
  var seq=[];
670
  if(att && att.checkInTime) seq.push(punchEntry('in', att.checkInTime));
671
  visited.forEach(function(x){ seq.push(x); });
672
  pending.forEach(function(x){ seq.push(x); });
673
  if(att && att.checkOutTime) seq.push(punchEntry('out', att.checkOutTime));
36882 vikas 674
 
36897 vikas 675
  var h='<div class="jstats">'+
676
    jstat(fmtSec(workSec),'Working hours')+
677
    jstat(fmtSec(inStoreSec),'In-store time')+
678
    jstat(fmtSec(transitSec),'Travel time')+
679
    jstat(distKm.toFixed(2)+' km','Distance Covered')+
680
    jstat(visited.length+(leadsN?' &middot; '+leadsN+'L':''),'Stores / Leads')+
681
    '</div>';
682
  if(!seq.length){ return h+'<p class="desc">No tracking events for this day.</p>'; }
683
  h+='<div class="tl">';
684
  var ri=0;
685
  seq.forEach(function(x){
686
    if(!x._punch && x.checkInTime){
687
      var tr=(x.transitTime && x.transitTime!=='00:00:00') ? x.transitTime : null;
688
      var km=(x.totalDistance && parseFloat(x.totalDistance)>0) ? (parseFloat(x.totalDistance).toFixed(2)+' km') : null;
689
      if(tr || km){
690
        var parts=[]; if(tr) parts.push('&#128663; '+esc(tr)); if(km) parts.push(esc(km));
691
        h+='<div class="tlgap"><span class="gappill">'+parts.join(' &middot; ')+'</span></div>';
36877 vikas 692
      }
36897 vikas 693
    }
694
    h+=tlRow(x, ri%2===0?'left':'right'); ri++;
695
  });
696
  h+='</div>';
697
  return h;
36877 vikas 698
}
36897 vikas 699
 
700
/* ---- live location map (reuses /beat-report/live-tracking pathPoints) ---- */
701
function bjLoadMaps(cb){
702
  if(window.google && window.google.maps){ cb(); return; }
703
  window._mapsCbs = window._mapsCbs || [];
704
  window._mapsCbs.push(cb);
705
  if(window._mapsLoading) return;
706
  window._mapsLoading=true;
707
  window.__bjMapsReady=function(){ (window._mapsCbs||[]).forEach(function(f){ try{f();}catch(e){} }); window._mapsCbs=[]; };
708
  var s=document.createElement('script');
709
  s.src='https://maps.googleapis.com/maps/api/js?key=AIzaSyAckO0y4Z6WhBOuMjNjioWLSYZDhGEvGBc&libraries=geometry&v=weekly&callback=__bjMapsReady';
710
  s.async=true; document.head.appendChild(s);
711
}
712
function bjShowMap(userId, date){
713
  bjLoadMaps(function(){
714
    fetch(CTX+'/beat-journey/track-map?userId='+userId+'&date='+encodeURIComponent(date),{headers:{'Accept':'application/json'}})
715
      .then(function(r){ return r.json(); }).then(function(data){ bjDrawMap(data); })
716
      .catch(function(){ var e=document.getElementById('bjMap'); if(e) e.innerHTML='<div class="loading">Could not load location.</div>'; });
717
  });
718
}
719
function bjMarkColor(mt){
720
  mt=(mt||'').toUpperCase();
721
  if(mt.indexOf('CHECKOUT')>=0) return '#2f9e44';
722
  if(mt==='CHECKIN') return '#f59f00';
723
  if(mt==='DEFERRED') return '#e8590c';
724
  if(mt==='PENDING') return '#97a1b5';
725
  return '#3b5bdb';
726
}
727
function bjMarkInfo(v,color){
728
  return '<div style="font-size:12px;max-width:240px"><b>'+esc(v.name||'')+'</b><br>'+
729
    '<span style="background:'+color+';color:#fff;border-radius:4px;padding:1px 6px;font-size:10px">'+esc(v.markType||'')+'</span>'+
730
    (v.checkInTime?'<br>In: <b>'+esc(v.checkInTime)+'</b>':'')+
731
    (v.checkOutTime?'<br>Out: <b>'+esc(v.checkOutTime)+'</b>':'')+
732
    (v.timeSpent?'<br>Time spent: <b>'+esc(v.timeSpent)+'</b>':'')+
733
    (v.address?'<br>'+esc(v.address):'')+'</div>';
734
}
735
// Teardrop pin (same shape as Beat Report) with a white disc + label.
736
function bjPin(color, label){
737
  var svg='<svg xmlns="http://www.w3.org/2000/svg" width="28" height="40" viewBox="0 0 28 40">'+
738
    '<path d="M14 0C6.268 0 0 6.268 0 14c0 10.5 14 26 14 26s14-15.5 14-26C28 6.268 21.732 0 14 0z" fill="'+color+'"/>'+
739
    '<circle cx="14" cy="14" r="7" fill="#ffffff"/>'+
740
    '<text x="14" y="18" text-anchor="middle" font-size="10" font-weight="bold" fill="'+color+'">'+label+'</text></svg>';
741
  return {url:'data:image/svg+xml;charset=UTF-8,'+encodeURIComponent(svg), scaledSize:new google.maps.Size(28,40), anchor:new google.maps.Point(14,40)};
742
}
743
// Larger pin sized to fit a duration label (idle stops).
744
function bjStopPin(label){
745
  var s=String(label), d=Math.max(40, 14+s.length*7), H=d+10, r=d/2;
746
  var svg='<svg xmlns="http://www.w3.org/2000/svg" width="'+d+'" height="'+H+'" viewBox="0 0 '+d+' '+H+'">'+
747
    '<polygon points="'+(r-7)+','+(d-7)+' '+(r+7)+','+(d-7)+' '+r+','+H+'" fill="#e8590c"/>'+
748
    '<circle cx="'+r+'" cy="'+r+'" r="'+(r-2)+'" fill="#e8590c" stroke="#ffffff" stroke-width="2"/>'+
749
    '<text x="'+r+'" y="'+(r+4)+'" text-anchor="middle" font-size="12" font-weight="bold" fill="#ffffff">'+s+'</text></svg>';
750
  return {url:'data:image/svg+xml;charset=UTF-8,'+encodeURIComponent(svg), scaledSize:new google.maps.Size(d,H), anchor:new google.maps.Point(r,H)};
751
}
752
function fmtDur2(sec){ sec=sec||0; var m=Math.floor(sec/60), s=sec%60; return m+'m'+(s>0?' '+s+'s':''); }
753
function bjDrawMap(data){
754
  var box=document.getElementById('bjMap'); if(!box) return;
755
  box.innerHTML='';
756
  var map=new google.maps.Map(box,{zoom:12,center:{lat:20.5937,lng:78.9629},mapTypeControl:true,streetViewControl:false});
757
  var bounds=new google.maps.LatLngBounds(), has=false;
758
  function pin(pos,icon,zi,title,info){
759
    has=true; bounds.extend(pos);
760
    var m=new google.maps.Marker({position:pos,map:map,zIndex:zi||100,icon:icon,title:title||''});
761
    if(info){ var iw=new google.maps.InfoWindow({content:info}); m.addListener('click',function(){ iw.open(map,m); }); }
762
    return m;
763
  }
764
 
765
  // 1) GPS trail + last-known location (same bike/rider icon as Beat Report)
766
  var pts=(data.path||[]).map(function(p){ return {lat:p.lat,lng:p.lng}; });
767
  if(pts.length){
768
    new google.maps.Polyline({path:pts,geodesic:true,strokeColor:'#e03131',strokeOpacity:.7,strokeWeight:3,zIndex:40,map:map});
769
    pts.forEach(function(p){ bounds.extend(p); has=true; });
770
    var lastPt=pts[pts.length-1];
771
    var lk=new google.maps.Marker({position:lastPt,map:map,zIndex:999,title:'Last Known Location',
772
      icon:{url:CTX+'/resources/images/salesman.png',scaledSize:new google.maps.Size(40,40),anchor:new google.maps.Point(20,20)}});
773
    var lkIw=new google.maps.InfoWindow({content:'<div style="font-size:12px"><b>&#128205; Last known location</b></div>'});
774
    lk.addListener('click',function(){ lkIw.open(map,lk); });
775
  }
776
 
777
  // 2) idle stops (>=10 min within 50 m)
778
  (data.idleStops||[]).forEach(function(s){
779
    var lbl=fmtDur2(s.seconds);
780
    pin({lat:s.lat,lng:s.lng}, bjStopPin(lbl), 80, 'Idle '+lbl,
781
      '<div style="font-size:12px"><b>&#9201; Idle stop</b><br>Stayed ~'+esc(lbl)+' within 50 m</div>');
782
  });
783
 
784
  // 3) every visit
785
  var idx=0;
786
  (data.visits||[]).forEach(function(v){
787
    var color=bjMarkColor(v.markType);
788
    pin({lat:v.lat,lng:v.lng}, bjPin(color, String(++idx)), 100,
789
      (v.name||'')+' ('+v.markType+')', bjMarkInfo(v,color));
790
  });
791
 
792
  // 4) punch in / punch out
793
  if(data.punchIn){
794
    pin({lat:data.punchIn.lat,lng:data.punchIn.lng}, bjPin('#2f9e44','P'), 300,
795
      'Punch in'+(data.punchIn.time?' · '+data.punchIn.time:''),
796
      '<div style="font-size:12px"><b>&#9873; Punch in</b>'+(data.punchIn.time?'<br>'+esc(data.punchIn.time):'')+'</div>');
797
  }
798
  if(data.punchOut){
799
    pin({lat:data.punchOut.lat,lng:data.punchOut.lng}, bjPin('#7048e8','X'), 300,
800
      'Punch out'+(data.punchOut.time?' · '+data.punchOut.time:''),
801
      '<div style="font-size:12px"><b>&#9873; Punch out</b>'+(data.punchOut.time?'<br>'+esc(data.punchOut.time):'')+'</div>');
802
  }
803
 
804
  if(has){
805
    map.fitBounds(bounds);
806
    google.maps.event.addListenerOnce(map,'idle',function(){ if(map.getZoom()>16) map.setZoom(16); });
807
  } else {
808
    box.innerHTML='<div class="loading">No GPS / location recorded for this day.</div>';
809
  }
810
}
36882 vikas 811
function tlSecs(t){ if(!t) return 1e9; var p=t.split(':'); return (+p[0])*3600+(+(p[1]||0))*60+(+(p[2]||0)); }
812
function toSec(t){ if(!t||t==='00:00:00') return 0; var p=t.split(':'); return (+p[0])*3600+(+(p[1]||0))*60+(+(p[2]||0)); }
813
function fmtSec(s){ s=Math.max(0,Math.round(s)); var h=Math.floor(s/3600), m=Math.floor((s%3600)/60), ss=s%60; function z(n){return (n<10?'0':'')+n;} return z(h)+':'+z(m)+':'+z(ss); }
814
function jstat(v,l){ return '<div class="js"><div class="jv">'+v+'</div><div class="jl">'+l+'</div></div>'; }
815
function hhmm(t){ return t? t.slice(0,5):''; }
816
function tlThumb(att){
817
  if(att && window._mediaUrl) return '<img class="tlthumb" src="'+esc(window._mediaUrl+att)+'" onclick="window.open(this.src,\'_blank\')" onerror="tlImgErr(this)">';
818
  return '<div class="tlph" title="No photo">&#128247;</div>';
819
}
820
function tlImgErr(im){ var d=document.createElement('div'); d.className='tlph'; d.title='Photo unavailable'; d.innerHTML='&#128247;'; if(im.parentNode) im.parentNode.replaceChild(d, im); }
821
function punchEntry(kind, time){ return {_punch:kind, checkInTime:time, markType:(kind==='in'?'PUNCHIN':'PUNCHOUT'), taskType:'attendance'}; }
822
function tlStyle(x){
823
  var mt=(x.markType||'').toUpperCase(), tt=(x.taskType||'').toLowerCase();
824
  if(x._punch || tt==='attendance' || mt.indexOf('PUNCH')>=0) return {c:'#7048e8', l:(x._punch==='out'?'Punch out':'Punch in'), ic:'&#9873;'};
825
  if(mt.indexOf('CHECKOUT')>=0) return {c:'#2f9e44', l:'Visited', ic:'&#10003;'};
826
  if(mt==='CHECKIN') return {c:'#1098ad', l:'In store', ic:'&#128205;'};
827
  if(mt==='DEFERRED') return {c:'#e8590c', l:'Deferred', ic:'&#8635;'};
828
  if(mt==='PENDING' || !x.checkInTime) return {c:'#97a1b5', l:'Pending', ic:'&#9675;'};
829
  return {c:'#3b5bdb', l:(mt||'-'), ic:'&#9679;'};
830
}
831
function tlTitle(x){
832
  if(x._punch) return x._punch==='in' ? 'Journey start (Punch in)' : 'Journey end (Punch out)';
833
  var n=x.taskName||'';
834
  var pipe=n.lastIndexOf('|'); if(pipe>=0) n=n.slice(pipe+1);      // drop leading agenda tags
835
  n=n.replace(/\s*VISIT\b.*$/i,'').replace(/\s*visit\s*$/i,'').replace(/\bnull\b/gi,'').replace(/\s{2,}/g,' ').trim();
836
  return n || '(stop)';
837
}
838
function tlRow(x, side){
839
  var s=tlStyle(x);
840
  var tt=(x.taskType||'').toLowerCase();
841
  var timeTop = x.checkInTime ? hhmm(x.checkInTime) : '-';
842
  var tlabel = x._punch ? (x._punch==='in'?'punch in':'punch out') : (x.checkInTime?'check-in':'pending');
843
  var meta;
844
  if(x._punch){
845
    meta = (x._punch==='in'?'Punch in':'Punch out') + ' &middot; ' + hhmm(x.checkInTime);
846
  } else {
847
    var io = (x.checkInTime?hhmm(x.checkInTime):'-') + ' &rarr; ' + (x.checkOutTime?hhmm(x.checkOutTime):'-');
848
    var dur = (x.timeSpent && x.timeSpent!=='00:00:00') ? ' &middot; '+esc(x.timeSpent) : '';
849
    var typeLabel = tt==='lead' ? 'Lead' : (tt==='office-visit'?'Office':'Partner');
850
    meta = x.checkInTime ? (typeLabel+' &middot; '+io+dur) : (typeLabel+' &middot; not visited');
851
  }
852
  // image (or placeholder) + battery + location-toggle on real stops
853
  var extra='';
854
  if(!x._punch){
855
    var chips='';
856
    if(x.battery!=null) chips += '<span class="tlchip">&#128267; '+x.battery+'%</span>';
857
    if(x.locOff!=null) chips += '<span class="tlchip">&#128244; '+x.locOff+'</span>';
858
    extra = '<div class="tlextra">'+tlThumb(x.attachment)+chips+'</div>';
859
  }
860
  return '<div class="tlrow '+side+'">'+
861
      '<div class="node" style="background:'+s.c+'">'+s.ic+'</div>'+
862
      '<div class="tlcard">'+
863
        '<div class="tm" style="background:'+s.c+'">'+timeTop+'<small>'+tlabel+'</small></div>'+
864
        '<div class="bd"><div class="ti">'+esc(tlTitle(x))+'</div>'+
865
          '<span class="stt" style="background:'+s.c+'">'+s.l+'</span>'+
866
          '<div class="mt">'+meta+'</div>'+extra+'</div>'+
867
      '</div></div>';
868
}
36877 vikas 869
function closeModal(){ el('bj-modal-back').classList.remove('show'); }
870
 
871
/* ============================ APPROVAL ============================ */
872
function loadApproval(){
873
  spinner();
874
  var from=el('bj-start').value, to=el('bj-end').value;
875
  var u=CTX+'/beat-report/approval/data?fromDate='+encodeURIComponent(from)+'&toDate='+encodeURIComponent(to)+qs();
876
  fetch(u,{headers:{'Accept':'application/json'}}).then(function(r){ return r.json(); }).then(function(d){
877
    var rows=d.rows||[];
878
    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>'+
879
      '<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>';
880
    if(!rows.length){ h+='<p class="desc">Nothing pending in this window.</p></div>'; el('bj-content').innerHTML=h; return; }
881
    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>';
882
    rows.forEach(function(r){
883
      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>'+
884
         '<td class="num '+(r.distanceM>200?'badflag':'warnflag')+'">'+r.distanceM+' m</td>'+
885
         '<td class="num"><button class="btn-ok" onclick="approve('+r.trackingId+')">Approve</button></td></tr>';
886
    });
887
    h+='</tbody></table></div>';
888
    el('bj-content').innerHTML=h;
889
  }).catch(fail);
890
}
891
function approve(trackingId){
892
  fetch(CTX+'/beat-report/approve-checkin',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'trackingId='+trackingId})
893
    .then(function(r){ return r.json(); }).then(function(){ var row=el('appr-'+trackingId); if(row) row.parentNode.removeChild(row); }).catch(function(){ alert('Approve failed'); });
894
}
895
 
896
/* ============================ INIT ============================ */
897
document.querySelectorAll('.lens').forEach(function(b){ b.addEventListener('click', function(){ setLens(b.getAttribute('data-lens')); }); });
898
el('bj-apply').addEventListener('click', load);
36897 vikas 899
el('bj-level').addEventListener('change', onLevelChange);
900
if(el('bj-user')) el('bj-user').addEventListener('change', load);
901
el('bj-date').addEventListener('change', updateTodayLabel);
36877 vikas 902
el('bj-modal-back').addEventListener('click', function(e){ if(e.target===el('bj-modal-back')) closeModal(); });
36897 vikas 903
updateTodayLabel();
36877 vikas 904
load();
905
</script>
906
</body>
907
</html>