Subversion Repositories SmartDukaan

Rev

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

Rev 36882 Rev 36897
Line 77... Line 77...
77
  .meter > i{display:block;height:100%;border-radius:4px}
77
  .meter > i{display:block;height:100%;border-radius:4px}
78
  .warnflag{color:var(--orange);font-weight:700}
78
  .warnflag{color:var(--orange);font-weight:700}
79
  .badflag{color:var(--red);font-weight:700}
79
  .badflag{color:var(--red);font-weight:700}
80
  .rowwarn{background:#fff9f0}
80
  .rowwarn{background:#fff9f0}
81
  .rowbad{background:#fff5f5}
81
  .rowbad{background:#fff5f5}
-
 
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)}
82
  .att{display:flex;align-items:center;gap:12px;padding:11px 12px;border:1px solid var(--line);border-radius:11px;margin-bottom:9px}
94
  .att{display:flex;align-items:center;gap:12px;padding:11px 12px;border:1px solid var(--line);border-radius:11px;margin-bottom:9px}
83
  .att .ic{width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:14px;flex:none}
95
  .att .ic{width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:14px;flex:none}
84
  .att .ab{flex:1}
96
  .att .ab{flex:1}
85
  .att .atitle{font-size:13px;font-weight:650}
97
  .att .atitle{font-size:13px;font-weight:650}
86
  .att .adet{font-size:12px;color:var(--muted);margin-top:2px}
98
  .att .adet{font-size:12px;color:var(--muted);margin-top:2px}
Line 158... Line 170...
158
  .tlrow.left .tlextra{justify-content:flex-end}
170
  .tlrow.left .tlextra{justify-content:flex-end}
159
  .tlrow.right .tlextra{justify-content:flex-start}
171
  .tlrow.right .tlextra{justify-content:flex-start}
160
  .tlthumb{width:46px;height:46px;border-radius:8px;object-fit:cover;border:1px solid var(--line);cursor:pointer;background:#f1f3f9}
172
  .tlthumb{width:46px;height:46px;border-radius:8px;object-fit:cover;border:1px solid var(--line);cursor:pointer;background:#f1f3f9}
161
  .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}
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}
162
  .tlchip{font-size:10.5px;font-weight:600;color:var(--muted);background:#f1f3f9;border-radius:6px;padding:2px 7px;white-space:nowrap}
174
  .tlchip{font-size:10.5px;font-weight:600;color:var(--muted);background:#f1f3f9;border-radius:6px;padding:2px 7px;white-space:nowrap}
-
 
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}
163
  @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}}
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}}
164
  @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}}
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}}
165
</style>
185
</style>
166
</head>
186
</head>
167
<body>
187
<body>
Line 176... Line 196...
176
        <option value="">All levels below me</option>
196
        <option value="">All levels below me</option>
177
        #foreach($lvl in $escalationTypes)
197
        #foreach($lvl in $escalationTypes)
178
          <option value="$lvl">$lvl</option>
198
          <option value="$lvl">$lvl</option>
179
        #end
199
        #end
180
      </select>
200
      </select>
-
 
201
      #if($topLevel)
-
 
202
      <select class="ctl hide" id="bj-user">
-
 
203
        <option value="">All users at level</option>
-
 
204
      </select>
-
 
205
      #end
181
      <input class="ctl" type="date" id="bj-date" value="$today">
206
      <input class="ctl" type="date" id="bj-date" value="$today">
182
      <span id="bj-period-range" class="hide">
207
      <span id="bj-period-range" class="hide">
183
        <input class="ctl" type="date" id="bj-start" value="$monthStart">
208
        <input class="ctl" type="date" id="bj-start" value="$monthStart">
184
        <input class="ctl" type="date" id="bj-end" value="$today">
209
        <input class="ctl" type="date" id="bj-end" value="$today">
185
      </span>
210
      </span>
Line 188... Line 213...
188
  </header>
213
  </header>
189
 
214
 
190
  <div class="lenses">
215
  <div class="lenses">
191
    <button class="lens active" data-lens="today" id="lens-today">Today (Live)</button>
216
    <button class="lens active" data-lens="today" id="lens-today">Today (Live)</button>
192
    <button class="lens" data-lens="period" id="lens-period">Period (Analytics)</button>
217
    <button class="lens" data-lens="period" id="lens-period">Period (Analytics)</button>
193
    <button class="lens" data-lens="approval" id="lens-approval">Location Approval</button>
-
 
194
  </div>
218
  </div>
195
 
219
 
196
  <div id="bj-content"><div class="loading"><div class="spinner"></div>Loading&hellip;</div></div>
220
  <div id="bj-content"><div class="loading"><div class="spinner"></div>Loading&hellip;</div></div>
197
</div>
221
</div>
198
 
222
 
Line 202... Line 226...
202
 
226
 
203
<script>
227
<script>
204
var CTX = '${rc.contextPath}';
228
var CTX = '${rc.contextPath}';
205
var BASE = CTX + '/beat-journey';
229
var BASE = CTX + '/beat-journey';
206
var LENS = 'today';
230
var LENS = 'today';
-
 
231
var TODAY = '$today';
207
 
232
 
208
function el(id){ return document.getElementById(id); }
233
function el(id){ return document.getElementById(id); }
-
 
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
}
209
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
246
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
210
function num(n){ return (n==null?0:n).toLocaleString(); }
247
function num(n){ return (n==null?0:n).toLocaleString(); }
211
function adhColor(p){ if(p>=80)return '#2f9e44'; if(p>=60)return '#f08c00'; if(p>=40)return '#e8590c'; return '#e03131'; }
248
function adhColor(p){ if(p>=80)return '#2f9e44'; if(p>=60)return '#f08c00'; if(p>=40)return '#e8590c'; return '#e03131'; }
212
function pct(a,b){ return b? Math.round(100*a/b):0; }
249
function pct(a,b){ return b? Math.round(100*a/b):0; }
213
function qs(){
250
function qs(){
214
  var lvl = el('bj-level').value;
251
  var lvl = el('bj-level').value;
215
  var s = '';
252
  var s = '';
216
  if(lvl) s += '&escalationType=' + encodeURIComponent(lvl);
253
  if(lvl) s += '&escalationType=' + encodeURIComponent(lvl);
-
 
254
  var usr = el('bj-user');           // only present for L5/Final
-
 
255
  if(usr && usr.value) s += '&teamOfAuthUserId=' + encodeURIComponent(usr.value);
217
  return s;
256
  return s;
218
}
257
}
-
 
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
}
219
function spinner(){ el('bj-content').innerHTML = '<div class="loading"><div class="spinner"></div>Loading&hellip;</div>'; }
278
function spinner(){ el('bj-content').innerHTML = '<div class="loading"><div class="spinner"></div>Loading&hellip;</div>'; }
220
function fail(e){ el('bj-content').innerHTML = '<div class="loading">Could not load data ('+esc(e&&e.message||e)+'). Are you signed in?</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>'; }
221
 
280
 
222
function setLens(name){
281
function setLens(name){
223
  LENS = name;
282
  LENS = name;
Line 233... Line 292...
233
  return loadApproval();
292
  return loadApproval();
234
}
293
}
235
 
294
 
236
/* ============================ TODAY ============================ */
295
/* ============================ TODAY ============================ */
237
function loadToday(){
296
function loadToday(){
-
 
297
  updateTodayLabel();
238
  spinner();
298
  spinner();
239
  fetch(BASE + '/today?date=' + encodeURIComponent(el('bj-date').value) + qs(), {headers:{'Accept':'application/json'}})
299
  fetch(BASE + '/today?date=' + encodeURIComponent(el('bj-date').value) + qs(), {headers:{'Accept':'application/json'}})
240
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
300
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
241
    .then(renderToday).catch(fail);
301
    .then(renderToday).catch(fail);
242
}
302
}
Line 252... Line 312...
252
}
312
}
253
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>'; }
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>'; }
254
 
314
 
255
function renderToday(d){
315
function renderToday(d){
256
  var k=d.kpis, f=d.funnel, s=d.stops, a=d.attention;
316
  var k=d.kpis, f=d.funnel, s=d.stops, a=d.attention;
-
 
317
  window._canApprove = !!d.canApprove;
-
 
318
  window._mediaUrl = d.mediaUrl || window._mediaUrl || '';
257
  var h='';
319
  var h='';
258
  h+='<div class="kpis">';
320
  h+='<div class="kpis">';
259
  h+=kpi('&#128101;','#eef1fb','#3b5bdb','Executives scheduled',num(k.scheduled),k.notStarted+' not started yet');
321
  h+=kpi('&#128101;','#eef1fb','#3b5bdb','Executives scheduled',num(k.scheduled),k.notStarted+' not started yet');
260
  h+=kpi('&#9201;','#ebfbee','#2f9e44','Punched in',num(k.punchedIn)+' <small>/'+k.scheduled+'</small>',k.notStarted+' pending');
322
  h+=kpi('&#9201;','#ebfbee','#2f9e44','Punched in',num(k.punchedIn)+' <small>/'+k.scheduled+'</small>',k.notStarted+' pending');
261
  h+=kpi('&#128205;','#e7f5ff','#1098ad','On route now',num(k.onRouteNow),'live journeys');
323
  h+=kpi('&#128205;','#e7f5ff','#1098ad','On route now',num(k.onRouteNow),'live journeys');
Line 269... Line 331...
269
  h+=fstep('Scheduled executives',f.scheduled,100,'start of day',false,false);
331
  h+=fstep('Scheduled executives',f.scheduled,100,'start of day',false,false);
270
  h+=fstep('Punched in',f.punchedIn,pct(f.punchedIn,sc),'&minus;'+(f.scheduled-f.punchedIn)+' never started',true,false);
332
  h+=fstep('Punched in',f.punchedIn,pct(f.punchedIn,sc),'&minus;'+(f.scheduled-f.punchedIn)+' never started',true,false);
271
  h+=fstep('Made a check-in',f.madeCheckin,pct(f.madeCheckin,sc),'&minus;'+(f.punchedIn-f.madeCheckin)+' punched, idle',true,false);
333
  h+=fstep('Made a check-in',f.madeCheckin,pct(f.madeCheckin,sc),'&minus;'+(f.punchedIn-f.madeCheckin)+' punched, idle',true,false);
272
  h+=fstep('Completed all visits',f.completedAll,pct(f.completedAll,sc),(f.madeCheckin-f.completedAll)+' still mid-journey',true,false);
334
  h+=fstep('Completed all visits',f.completedAll,pct(f.completedAll,sc),(f.madeCheckin-f.completedAll)+' still mid-journey',true,false);
273
  h+=fstep('Punched out',f.punchedOut,pct(f.punchedOut,sc),'journey ended',false,true);
335
  h+=fstep('Punched out',f.punchedOut,pct(f.punchedOut,sc),'journey ended',false,true);
274
  h+='</div><div class="leak">';
-
 
275
  h+=leakbox('Stops planned',s.planned,null);
-
 
276
  h+=leakbox('Checked out (done)',s.done,'#2f9e44');
-
 
277
  h+=leakbox('In progress now',s.inProgress,'#1098ad');
-
 
278
  h+=leakbox('Deferred',s.deferred,'#e8590c');
-
 
279
  h+=leakbox('Untouched',s.untouched,'#e03131');
-
 
280
  h+='</div></div>';
336
  h+='</div></div>';
281
 
337
 
282
  h+='<div class="grid g3">';
338
  h+='<div class="grid g3">';
283
  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>';
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>';
284
  h+='<div class="card"><h2>Needs attention now</h2><p class="desc">Things a manager should clear today.</p>'+attentionList(a)+'</div>';
340
  h+='<div class="card"><h2>Needs attention now</h2><p class="desc">Things a manager should clear today.</p>'+attentionList(a)+'</div>';
285
  h+='</div>';
341
  h+='</div>';
286
  el('bj-content').innerHTML=h;
342
  el('bj-content').innerHTML=h;
287
}
343
}
288
function stateClass(st){ return st==='IN_STORE'?'ps-store':st==='TRAVELLING'?'ps-travel':st==='DONE'?'ps-done':st==='NOT_STARTED'?'ps-no':'ps-idle'; }
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'; }
-
 
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
}
289
function boardTable(rows, date){
354
function boardTable(rows, date){
290
  if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
355
  if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
-
 
356
  var h='<div style="overflow-x:auto"><table><thead><tr>'+
-
 
357
    '<th>Agenda</th><th>Executive</th><th class="num">Planned</th>'+
291
  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>';
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>';
292
  rows.forEach(function(r){
360
  rows.forEach(function(r){
293
    var stale = r.lastPingMin!=null && r.lastPingMin>=40;
361
    var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (r.idleAway?'rowwarn':'');
294
    var idle = r.state==='TRAVELLING' && stale;
362
    var A = r.userId+",'"+js(r.name)+"','"+date+"'";   // openExec args
295
    var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (idle?'rowwarn':'');
363
    var go = 'onclick="openExec('+A+')"';
296
    var ping = r.lastPingMin==null ? '-' : (r.lastPingMin+' min');
364
    h+='<tr class="clickable '+rowcls+'" '+go+'>'+
297
    h+='<tr class="clickable '+rowcls+'" onclick="openExec('+r.userId+',\''+esc(r.name)+'\',\''+date+'\')">'+
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>'+
298
       '<td><b>'+esc(r.name)+'</b><br><span style="color:#97a1b5;font-size:11px">'+esc(r.beatName||'-')+'</span></td>'+
366
       '<td><b>'+esc(r.name)+'</b><br>'+esc(r.beatName||'-')+'</td>'+
-
 
367
       '<td class="num">'+r.planned+'</td>'+
299
       '<td>'+(r.agendaFilled?'<span style="color:#2f9e44">&#10003;</span>':'<span class="badflag">&#10007;</span>')+'</td>'+
368
       '<td style="white-space:nowrap">'+(r.punchInTime?'<span class="ptime">'+esc(r.punchInTime)+'</span>':'<span class="badflag">-</span>')+
300
       '<td>'+(r.punchInTime?esc(r.punchInTime):'<span class="badflag">-</span>')+'</td>'+
369
         ' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
301
       '<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
370
       '<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
302
       '<td class="num">'+r.visitsDone+'/'+r.visitsTotal+'</td>'+
371
       '<td class="num cnt cmuted">'+r.totalVisits+'</td>'+
303
       '<td class="num'+(stale?' warnflag':'')+'">'+ping+'</td>'+
372
       '<td class="num cnt cgreen">'+r.completedVisits+'</td>'+
304
       '<td class="num'+(r.battery!=null&&r.battery<25?' warnflag':'')+'">'+(r.battery==null?'-':r.battery+'%')+'</td>'+
373
       '<td class="num cnt camber">'+r.pendingVisits+'</td>'+
305
       '<td class="num'+(r.locOff>0?' warnflag':'')+'">'+r.locOff+'</td></tr>';
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>';
306
  });
381
  });
307
  return h+'</tbody></table>';
382
  return h+'</tbody></table></div>';
308
}
383
}
309
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; }
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; }
310
var ATT_META = {
385
var ATT_META = {
311
  notPunchedIn:    {bg:'#fff0f0', fg:'#e03131', ic:'&#9888;',   title:'Not punched in',                  sub:'executives with a beat, not started', cls:'badflag'},
386
  notPunchedIn:    {bg:'#fff0f0', fg:'#e03131', ic:'&#9888;',   title:'Not punched in',                  sub:'executives with a beat, not started', cls:'badflag'},
312
  idleStale:       {bg:'#fff4e6', fg:'#e8590c', ic:'&#9201;',   title:'Idle / stale GPS &gt; 40 min',     sub:'no recent GPS ping',                  cls:'warnflag'},
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'},
313
  geofence:        {bg:'#fff0f0', fg:'#e03131', ic:'&#128205;', title:'Check-in outside store geofence',  sub:'&gt;50 m from partner\'s saved location', cls:'badflag'},
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'},
314
  locationOff:     {bg:'#fff4e6', fg:'#e8590c', ic:'&#128244;', title:'Location turned off mid-journey',  sub:'GPS toggled off',                     cls:'warnflag'},
389
  locationOff:     {bg:'#fff4e6', fg:'#e8590c', ic:'&#128244;', title:'Location turned off mid-journey',  sub:'GPS toggled off',                     cls:'warnflag'},
315
  deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'&#8635;',   title:'Deferred, awaiting re-action',     sub:'shops deferred today',                cls:''}
390
  deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'&#8635;',   title:'Deferred, awaiting re-action',     sub:'shops deferred today',                cls:''}
316
};
391
};
317
function attentionList(a){
392
function attentionList(a){
Line 330... Line 405...
330
  return h;
405
  return h;
331
}
406
}
332
function openAttention(key){
407
function openAttention(key){
333
  var a=window._att||{}, list=a[key]||[], m=ATT_META[key]||{title:'Detail'};
408
  var a=window._att||{}, list=a[key]||[], m=ATT_META[key]||{title:'Detail'};
334
  var mb=el('bj-modal-back'), md=el('bj-modal');
409
  var mb=el('bj-modal-back'), md=el('bj-modal');
-
 
410
  // Off-site check-ins can be approved here, but only by L5/Final (canApprove).
-
 
411
  var showApprove = (key==='geofence' && window._canApprove);
335
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+m.title+'</h3>'+
412
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+m.title+'</h3>'+
336
        '<p class="desc">'+list.length+' '+(list.length===1?'item':'items')+'</p>';
413
        '<p class="desc">'+list.length+' '+(list.length===1?'item':'items')+
-
 
414
        (showApprove?' &middot; approving updates the partner\'s saved coordinates':'')+'</p>';
337
  if(!list.length){ h+='<p class="desc">Nothing here.</p>'; }
415
  if(!list.length){ h+='<p class="desc">Nothing here.</p>'; }
338
  else{
416
  else{
339
    h+='<table><tbody>';
417
    h+='<table><tbody>';
340
    list.forEach(function(x){
418
    list.forEach(function(x){
-
 
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>';
341
      h+='<tr><td style="font-weight:600;white-space:nowrap">'+esc(x.name)+'</td>'+
421
      h+='<tr><td style="font-weight:600;white-space:nowrap">'+esc(x.name)+'</td>'+
342
         '<td style="color:#69748c">'+esc(x.detail||'')+'</td></tr>';
422
         '<td style="color:#69748c">'+esc(x.detail||'')+'</td>'+act+'</tr>';
343
    });
423
    });
344
    h+='</tbody></table>';
424
    h+='</tbody></table>';
345
  }
425
  }
346
  md.innerHTML=h; mb.classList.add('show');
426
  md.innerHTML=h; mb.classList.add('show');
347
}
427
}
-
 
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
}
348
 
455
 
349
/* ============================ PERIOD ============================ */
456
/* ============================ PERIOD ============================ */
350
function loadPeriod(){
457
function loadPeriod(){
351
  spinner();
458
  spinner();
352
  var u = BASE + '/data?startDate=' + encodeURIComponent(el('bj-start').value) + '&endDate=' + encodeURIComponent(el('bj-end').value) + qs();
459
  var u = BASE + '/data?startDate=' + encodeURIComponent(el('bj-start').value) + '&endDate=' + encodeURIComponent(el('bj-end').value) + qs();
Line 502... Line 609...
502
    '<li><b>Coverage and the manager view</b> follow the existing L1&ndash;L5 sales hierarchy, so each manager sees only their team.</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>'+
503
    '</ul></div>';
610
    '</ul></div>';
504
}
611
}
505
 
612
 
506
/* ============================ EXEC DRILLDOWN ============================ */
613
/* ============================ EXEC DRILLDOWN ============================ */
507
function openExec(userId, name, date){
614
function openExec(userId, name, date, tab){
508
  if(!userId){ return; }
615
  if(!userId){ return; }
-
 
616
  window._exec = {userId:userId, name:name, date:date, detail:null, tab:(tab||'timeline')};
509
  var mb=el('bj-modal-back'), m=el('bj-modal');
617
  var mb=el('bj-modal-back'), m=el('bj-modal');
510
  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>';
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>';
511
  mb.classList.add('show');
619
  mb.classList.add('show');
512
  fetch(CTX+'/beat-journey/journey-detail?userId='+userId+'&date='+encodeURIComponent(date),{headers:{'Accept':'application/json'}})
620
  fetch(CTX+'/beat-journey/journey-detail?userId='+userId+'&date='+encodeURIComponent(date),{headers:{'Accept':'application/json'}})
513
    .then(function(r){ return r.json(); }).then(function(d){
621
    .then(function(r){ return r.json(); }).then(function(d){
514
      window._mediaUrl = d.mediaUrl || '';
622
      window._mediaUrl = d.mediaUrl || '';
-
 
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){
515
      var all=(d.visits||[]).slice();
644
  var all=(d.visits||[]).slice();
516
      // Sequence like the field report: Punch in -> visited stops (by check-in
645
  // Sequence like the field report: Punch in -> visited stops (by check-in time)
517
      // time) -> PENDING/not-visited -> Punch out. The attendance row carries
646
  // -> PENDING/not-visited -> Punch out. The attendance row carries both punch-in
518
      // both punch-in & punch-out times; split it into the two bookends.
647
  // & punch-out times; split it into the two bookends.
519
      var att=null, visited=[], pending=[];
648
  var att=null, visited=[], pending=[];
520
      all.forEach(function(x){
649
  all.forEach(function(x){
521
        var tt=(x.taskType||'').toLowerCase(), mt=(x.markType||'').toUpperCase();
650
    var tt=(x.taskType||'').toLowerCase(), mt=(x.markType||'').toUpperCase();
522
        if(tt==='attendance' || mt.indexOf('PUNCH')>=0){ if(!att) att=x; }
651
    if(tt==='attendance' || mt.indexOf('PUNCH')>=0){ if(!att) att=x; }
523
        else if(x.checkInTime){ visited.push(x); }
652
    else if(x.checkInTime){ visited.push(x); }
524
        else { pending.push(x); }
653
    else { pending.push(x); }
525
      });
654
  });
526
      visited.sort(function(a,b){ return tlSecs(a.checkInTime)-tlSecs(b.checkInTime); });
655
  visited.sort(function(a,b){ return tlSecs(a.checkInTime)-tlSecs(b.checkInTime); });
527
 
656
 
528
      // totals across the day's stops (attendance row excluded from leg sums)
-
 
529
      var distKm=0, inStoreSec=0, transitSec=0, leadsN=0;
657
  var distKm=0, inStoreSec=0, transitSec=0, leadsN=0;
530
      all.forEach(function(x){
658
  all.forEach(function(x){
531
        var tt=(x.taskType||'').toLowerCase();
659
    var tt=(x.taskType||'').toLowerCase();
532
        if(tt==='attendance') return;
660
    if(tt==='attendance') return;
533
        if(x.totalDistance) distKm += (parseFloat(x.totalDistance)||0);
661
    if(x.totalDistance) distKm += (parseFloat(x.totalDistance)||0);
534
        inStoreSec += toSec(x.timeSpent);
662
    inStoreSec += toSec(x.timeSpent);
535
        transitSec += toSec(x.transitTime);
663
    transitSec += toSec(x.transitTime);
536
        if(tt==='lead') leadsN++;
664
    if(tt==='lead') leadsN++;
537
      });
665
  });
538
      var workSec = (att && att.checkInTime && att.checkOutTime) ? (toSec(att.checkOutTime)-toSec(att.checkInTime)) : (att?toSec(att.timeSpent):0);
666
  var workSec = (att && att.checkInTime && att.checkOutTime) ? (toSec(att.checkOutTime)-toSec(att.checkInTime)) : (att?toSec(att.timeSpent):0);
539
      if(workSec<0) workSec=0;
667
  if(workSec<0) workSec=0;
540
 
668
 
541
      var seq=[];
669
  var seq=[];
542
      if(att && att.checkInTime) seq.push(punchEntry('in', att.checkInTime));
670
  if(att && att.checkInTime) seq.push(punchEntry('in', att.checkInTime));
543
      visited.forEach(function(x){ seq.push(x); });
671
  visited.forEach(function(x){ seq.push(x); });
544
      pending.forEach(function(x){ seq.push(x); });
672
  pending.forEach(function(x){ seq.push(x); });
545
      if(att && att.checkOutTime) seq.push(punchEntry('out', att.checkOutTime));
673
  if(att && att.checkOutTime) seq.push(punchEntry('out', att.checkOutTime));
546
 
674
 
547
      var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(name)+'</h3>'+
-
 
548
        '<p class="desc">Journey detail &middot; '+esc(date)+' &middot; battery '+(d.batteryLevel==null?'-':d.batteryLevel+'%')+' &middot; loc-off '+(d.locationToggleCount||0)+'</p>';
-
 
549
      h+='<div class="jstats">'+
675
  var h='<div class="jstats">'+
550
        jstat(fmtSec(workSec),'Working hours')+
676
    jstat(fmtSec(workSec),'Working hours')+
551
        jstat(fmtSec(inStoreSec),'In-store time')+
677
    jstat(fmtSec(inStoreSec),'In-store time')+
552
        jstat(fmtSec(transitSec),'Travel time')+
678
    jstat(fmtSec(transitSec),'Travel time')+
553
        jstat(distKm.toFixed(2)+' km','Distance Covered')+
679
    jstat(distKm.toFixed(2)+' km','Distance Covered')+
554
        jstat(visited.length+(leadsN?' &middot; '+leadsN+'L':''),'Stores visited')+
680
    jstat(visited.length+(leadsN?' &middot; '+leadsN+'L':''),'Stores / Leads')+
555
        '</div>';
681
    '</div>';
556
      if(!seq.length){ h+='<p class="desc">No tracking events for this day.</p>'; }
682
  if(!seq.length){ return h+'<p class="desc">No tracking events for this day.</p>'; }
557
      else{
-
 
558
        h+='<div class="tl">';
683
  h+='<div class="tl">';
559
        var ri=0;
684
  var ri=0;
560
        seq.forEach(function(x){
685
  seq.forEach(function(x){
561
          // travel connector before each visited stop: time taken + distance to reach it
-
 
562
          if(!x._punch && x.checkInTime){
686
    if(!x._punch && x.checkInTime){
563
            var tr=(x.transitTime && x.transitTime!=='00:00:00') ? x.transitTime : null;
687
      var tr=(x.transitTime && x.transitTime!=='00:00:00') ? x.transitTime : null;
564
            var km=(x.totalDistance && parseFloat(x.totalDistance)>0) ? (parseFloat(x.totalDistance).toFixed(2)+' km') : null;
688
      var km=(x.totalDistance && parseFloat(x.totalDistance)>0) ? (parseFloat(x.totalDistance).toFixed(2)+' km') : null;
565
            if(tr || km){
689
      if(tr || km){
566
              var parts=[]; if(tr) parts.push('&#128663; '+esc(tr)); if(km) parts.push(esc(km));
690
        var parts=[]; if(tr) parts.push('&#128663; '+esc(tr)); if(km) parts.push(esc(km));
567
              h+='<div class="tlgap"><span class="gappill">'+parts.join(' &middot; ')+'</span></div>';
691
        h+='<div class="tlgap"><span class="gappill">'+parts.join(' &middot; ')+'</span></div>';
568
            }
-
 
569
          }
-
 
570
          h+=tlRow(x, ri%2===0?'left':'right'); ri++;
-
 
571
        });
-
 
572
        h+='</div>';
-
 
573
      }
692
      }
-
 
693
    }
-
 
694
    h+=tlRow(x, ri%2===0?'left':'right'); ri++;
-
 
695
  });
-
 
696
  h+='</div>';
-
 
697
  return h;
-
 
698
}
-
 
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;
574
      m.innerHTML=h;
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 {
575
    }).catch(function(){ m.innerHTML='<button class="x" onclick="closeModal()">&times;</button><p class="desc">Could not load detail.</p>'; });
808
    box.innerHTML='<div class="loading">No GPS / location recorded for this day.</div>';
-
 
809
  }
576
}
810
}
577
function tlSecs(t){ if(!t) return 1e9; var p=t.split(':'); return (+p[0])*3600+(+(p[1]||0))*60+(+(p[2]||0)); }
811
function tlSecs(t){ if(!t) return 1e9; var p=t.split(':'); return (+p[0])*3600+(+(p[1]||0))*60+(+(p[2]||0)); }
578
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)); }
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)); }
579
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); }
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); }
580
function jstat(v,l){ return '<div class="js"><div class="jv">'+v+'</div><div class="jl">'+l+'</div></div>'; }
814
function jstat(v,l){ return '<div class="js"><div class="jv">'+v+'</div><div class="jl">'+l+'</div></div>'; }
Line 660... Line 894...
660
}
894
}
661
 
895
 
662
/* ============================ INIT ============================ */
896
/* ============================ INIT ============================ */
663
document.querySelectorAll('.lens').forEach(function(b){ b.addEventListener('click', function(){ setLens(b.getAttribute('data-lens')); }); });
897
document.querySelectorAll('.lens').forEach(function(b){ b.addEventListener('click', function(){ setLens(b.getAttribute('data-lens')); }); });
664
el('bj-apply').addEventListener('click', load);
898
el('bj-apply').addEventListener('click', load);
-
 
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);
665
el('bj-modal-back').addEventListener('click', function(e){ if(e.target===el('bj-modal-back')) closeModal(); });
902
el('bj-modal-back').addEventListener('click', function(e){ if(e.target===el('bj-modal-back')) closeModal(); });
-
 
903
updateTodayLabel();
666
load();
904
load();
667
</script>
905
</script>
668
</body>
906
</body>
669
</html>
907
</html>