Subversion Repositories SmartDukaan

Rev

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

Rev 36877 Rev 36882
Line 83... Line 83...
83
  .att .ic{width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:14px;flex:none}
83
  .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}
84
  .att .ab{flex:1}
85
  .att .atitle{font-size:13px;font-weight:650}
85
  .att .atitle{font-size:13px;font-weight:650}
86
  .att .adet{font-size:12px;color:var(--muted);margin-top:2px}
86
  .att .adet{font-size:12px;color:var(--muted);margin-top:2px}
87
  .att .acount{font-size:18px;font-weight:780}
87
  .att .acount{font-size:18px;font-weight:780}
-
 
88
  .att.clickable{cursor:pointer;transition:border-color .12s,box-shadow .12s}
-
 
89
  .att.clickable:hover{border-color:#c7d0e8;box-shadow:0 2px 8px rgba(20,30,60,.08)}
-
 
90
  .att .chev{color:#c2cadb;font-size:13px;margin-left:8px}
88
  .barrow{display:grid;grid-template-columns:60px 1fr 90px;align-items:center;gap:11px;margin:8px 0}
91
  .barrow{display:grid;grid-template-columns:60px 1fr 90px;align-items:center;gap:11px;margin:8px 0}
89
  .barrow .name{font-size:13px;font-weight:600}
92
  .barrow .name{font-size:13px;font-weight:600}
90
  .track{background:#f1f3f9;border-radius:7px;height:20px;position:relative;overflow:hidden}
93
  .track{background:#f1f3f9;border-radius:7px;height:20px;position:relative;overflow:hidden}
91
  .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}
94
  .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}
92
  .barrow .meta{font-size:11.5px;color:var(--muted);text-align:right}
95
  .barrow .meta{font-size:11.5px;color:var(--muted);text-align:right}
Line 121... Line 124...
121
  .loading{padding:70px;text-align:center;color:var(--muted)}
124
  .loading{padding:70px;text-align:center;color:var(--muted)}
122
  .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}
125
  .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}
123
  @keyframes spin{to{transform:rotate(360deg)}}
126
  @keyframes spin{to{transform:rotate(360deg)}}
124
  .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}
127
  .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}
125
  .modal-back.show{display:flex}
128
  .modal-back.show{display:flex}
126
  .modal{background:#fff;border-radius:14px;max-width:680px;width:100%;padding:20px 22px;box-shadow:0 20px 60px rgba(10,20,50,.3)}
129
  .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}
127
  .modal h3{margin:0 0 2px;font-size:17px}
130
  .modal h3{margin:0 0 2px;font-size:17px;text-align:center}
-
 
131
  .modal > .desc{text-align:center}
128
  .modal .x{float:right;cursor:pointer;color:var(--soft);font-size:20px;line-height:1;border:0;background:none}
132
  .modal .x{position:absolute;top:14px;right:16px;cursor:pointer;color:var(--soft);font-size:22px;line-height:1;border:0;background:none}
-
 
133
  /* journey totals bar */
-
 
134
  .jstats{display:flex;gap:9px;margin:12px 0 2px;flex-wrap:wrap}
-
 
135
  .js{flex:1;min-width:104px;background:#f8f9fc;border:1px solid var(--line2);border-radius:10px;padding:9px 10px;text-align:center}
-
 
136
  .js .jv{font-size:15.5px;font-weight:760;color:var(--ink);font-variant-numeric:tabular-nums}
-
 
137
  .js .jl{font-size:10px;color:var(--soft);text-transform:uppercase;letter-spacing:.04em;margin-top:3px;font-weight:600}
129
  .btn-ok{background:var(--good);border:0;color:#fff;font-weight:650;padding:6px 12px;border-radius:7px;cursor:pointer;font-size:12px}
138
  .btn-ok{background:var(--good);border:0;color:#fff;font-weight:650;padding:6px 12px;border-radius:7px;cursor:pointer;font-size:12px}
-
 
139
  /* journey timeline (exec drilldown) */
-
 
140
  .tl{position:relative;margin:14px 2px 4px;padding:4px 0}
-
 
141
  .tl::before{content:"";position:absolute;left:50%;top:6px;bottom:6px;width:2px;background:#e9edf5;transform:translateX(-50%)}
-
 
142
  .tlgap{position:relative;display:flex;justify-content:center;margin:-3px 0}
-
 
143
  .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}
-
 
144
  .tlrow{position:relative;display:flex;margin:14px 0}
-
 
145
  .tlrow.left{justify-content:flex-start}
-
 
146
  .tlrow.right{justify-content:flex-end}
-
 
147
  .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}
-
 
148
  .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}
-
 
149
  .tlrow.left .tlcard{flex-direction:row-reverse;text-align:right}
-
 
150
  .tlrow.right .tlcard{flex-direction:row;text-align:left}
-
 
151
  .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}
-
 
152
  .tlcard .tm small{font-weight:600;font-size:8.5px;opacity:.9;text-transform:uppercase;letter-spacing:.04em;margin-top:3px}
-
 
153
  .tlcard .bd{padding:8px 11px;flex:1;min-width:0}
-
 
154
  .tlcard .bd .ti{font-size:12.5px;font-weight:700;color:var(--ink);line-height:1.25;word-break:break-word}
-
 
155
  .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}
-
 
156
  .tlcard .bd .mt{font-size:11px;color:var(--muted);margin-top:6px}
-
 
157
  .tlextra{display:flex;align-items:center;gap:7px;margin-top:8px;flex-wrap:wrap}
-
 
158
  .tlrow.left .tlextra{justify-content:flex-end}
-
 
159
  .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}
-
 
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}
-
 
162
  .tlchip{font-size:10.5px;font-weight:600;color:var(--muted);background:#f1f3f9;border-radius:6px;padding:2px 7px;white-space:nowrap}
130
  @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}}
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}}
-
 
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}}
131
</style>
165
</style>
132
</head>
166
</head>
133
<body>
167
<body>
134
<div class="wrap">
168
<div class="wrap">
135
  <header class="top">
169
  <header class="top">
Line 257... Line 291...
257
  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>';
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>';
258
  rows.forEach(function(r){
292
  rows.forEach(function(r){
259
    var stale = r.lastPingMin!=null && r.lastPingMin>=40;
293
    var stale = r.lastPingMin!=null && r.lastPingMin>=40;
260
    var idle = r.state==='TRAVELLING' && stale;
294
    var idle = r.state==='TRAVELLING' && stale;
261
    var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (idle?'rowwarn':'');
295
    var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (idle?'rowwarn':'');
262
    var ping = r.lastPingMin==null ? '&mdash;' : (r.lastPingMin+' min');
296
    var ping = r.lastPingMin==null ? '-' : (r.lastPingMin+' min');
263
    h+='<tr class="clickable '+rowcls+'" onclick="openExec('+r.userId+',\''+esc(r.name)+'\',\''+date+'\')">'+
297
    h+='<tr class="clickable '+rowcls+'" onclick="openExec('+r.userId+',\''+esc(r.name)+'\',\''+date+'\')">'+
264
       '<td><b>'+esc(r.name)+'</b><br><span style="color:#97a1b5;font-size:11px">'+esc(r.beatName||'&mdash;')+'</span></td>'+
298
       '<td><b>'+esc(r.name)+'</b><br><span style="color:#97a1b5;font-size:11px">'+esc(r.beatName||'-')+'</span></td>'+
265
       '<td>'+(r.agendaFilled?'<span style="color:#2f9e44">&#10003;</span>':'<span class="badflag">&#10007;</span>')+'</td>'+
299
       '<td>'+(r.agendaFilled?'<span style="color:#2f9e44">&#10003;</span>':'<span class="badflag">&#10007;</span>')+'</td>'+
266
       '<td>'+(r.punchInTime?esc(r.punchInTime):'<span class="badflag">&mdash;</span>')+'</td>'+
300
       '<td>'+(r.punchInTime?esc(r.punchInTime):'<span class="badflag">-</span>')+'</td>'+
267
       '<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
301
       '<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
268
       '<td class="num">'+r.visitsDone+'/'+r.visitsTotal+'</td>'+
302
       '<td class="num">'+r.visitsDone+'/'+r.visitsTotal+'</td>'+
269
       '<td class="num'+(stale?' warnflag':'')+'">'+ping+'</td>'+
303
       '<td class="num'+(stale?' warnflag':'')+'">'+ping+'</td>'+
270
       '<td class="num'+(r.battery!=null&&r.battery<25?' warnflag':'')+'">'+(r.battery==null?'&mdash;':r.battery+'%')+'</td>'+
304
       '<td class="num'+(r.battery!=null&&r.battery<25?' warnflag':'')+'">'+(r.battery==null?'-':r.battery+'%')+'</td>'+
271
       '<td class="num'+(r.locOff>0?' warnflag':'')+'">'+r.locOff+'</td></tr>';
305
       '<td class="num'+(r.locOff>0?' warnflag':'')+'">'+r.locOff+'</td></tr>';
272
  });
306
  });
273
  return h+'</tbody></table>';
307
  return h+'</tbody></table>';
274
}
308
}
275
function attRow(bg,fg,ic,title,detail,count,cls){
-
 
276
  return '<div class="att"><div class="ic" style="background:'+bg+';color:'+fg+'">'+ic+'</div><div class="ab"><div class="atitle">'+title+'</div>'+
-
 
277
    '<div class="adet">'+detail+'</div></div><div class="acount '+(cls||'')+'">'+count+'</div></div>';
-
 
278
}
-
 
279
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; }
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; }
-
 
310
var ATT_META = {
-
 
311
  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'},
-
 
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'},
-
 
314
  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:''}
-
 
316
};
280
function attentionList(a){
317
function attentionList(a){
-
 
318
  window._att = a;
-
 
319
  var order=['notPunchedIn','idleStale','geofence','locationOff','deferredAwaiting'];
281
  var h='';
320
  var h='';
-
 
321
  order.forEach(function(k){
-
 
322
    var m=ATT_META[k], list=a[k]||[], n=list.length;
282
  h+=attRow('#fff0f0','#e03131','&#9888;','Not punched in', names(a.notPunchedIn,3), a.notPunchedIn.length, a.notPunchedIn.length?'badflag':'');
323
    var detail = n ? names(list.map(function(x){return x.name;}),3) : m.sub;
-
 
324
    var cls = n ? ' att clickable' : ' att';
283
  h+=attRow('#fff4e6','#e8590c','&#9201;','Idle / stale GPS &gt; 40 min', names(a.idleStale,3), a.idleStale.length, a.idleStale.length?'warnflag':'');
325
    var click = n ? ' onclick="openAttention(\''+k+'\')"' : '';
284
  h+=attRow('#fff0f0','#e03131','&#128205;','Check-in outside store geofence','&gt;50 m from partner\'s saved location', a.geofence, a.geofence?'badflag':'');
326
    h+='<div class="'+cls.trim()+'"'+click+'><div class="ic" style="background:'+m.bg+';color:'+m.fg+'">'+m.ic+'</div>'+
285
  h+=attRow('#fff4e6','#e8590c','&#128244;','Location turned off mid-journey', names(a.locationOff,3), a.locationOff.length, a.locationOff.length?'warnflag':'');
327
       '<div class="ab"><div class="atitle">'+m.title+'</div><div class="adet">'+detail+'</div></div>'+
286
  h+=attRow('#eef1f6','#69748c','&#8635;','Deferred, awaiting re-action','shops deferred today', a.deferredAwaiting, '');
328
       '<div class="acount '+(n?m.cls:'')+'">'+n+'</div>'+(n?'<span class="chev">&#9656;</span>':'')+'</div>';
-
 
329
  });
287
  return h;
330
  return h;
288
}
331
}
-
 
332
function openAttention(key){
-
 
333
  var a=window._att||{}, list=a[key]||[], m=ATT_META[key]||{title:'Detail'};
-
 
334
  var mb=el('bj-modal-back'), md=el('bj-modal');
-
 
335
  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>';
-
 
337
  if(!list.length){ h+='<p class="desc">Nothing here.</p>'; }
-
 
338
  else{
-
 
339
    h+='<table><tbody>';
-
 
340
    list.forEach(function(x){
-
 
341
      h+='<tr><td style="font-weight:600;white-space:nowrap">'+esc(x.name)+'</td>'+
-
 
342
         '<td style="color:#69748c">'+esc(x.detail||'')+'</td></tr>';
-
 
343
    });
-
 
344
    h+='</tbody></table>';
-
 
345
  }
-
 
346
  md.innerHTML=h; mb.classList.add('show');
-
 
347
}
289
 
348
 
290
/* ============================ PERIOD ============================ */
349
/* ============================ PERIOD ============================ */
291
function loadPeriod(){
350
function loadPeriod(){
292
  spinner();
351
  spinner();
293
  var u = BASE + '/data?startDate=' + encodeURIComponent(el('bj-start').value) + '&endDate=' + encodeURIComponent(el('bj-end').value) + qs();
352
  var u = BASE + '/data?startDate=' + encodeURIComponent(el('bj-start').value) + '&endDate=' + encodeURIComponent(el('bj-end').value) + qs();
Line 367... Line 426...
367
  rows.forEach(function(r){
426
  rows.forEach(function(r){
368
    var c=adhColor(r.adherencePct);
427
    var c=adhColor(r.adherencePct);
369
    h+='<tr><td>'+esc(r.name)+' <span class="lvl">'+esc(r.level)+'</span></td>'+
428
    h+='<tr><td>'+esc(r.name)+' <span class="lvl">'+esc(r.level)+'</span></td>'+
370
       '<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
429
       '<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
371
       '<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>'+
430
       '<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>'+
372
       '<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'&mdash;')+(r.discussionMin?'m':'')+'</td>'+
431
       '<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'-')+(r.discussionMin?'m':'')+'</td>'+
373
       '<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td>'+
432
       '<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td>'+
374
       '<td class="num'+(r.geoFlags>0?' badflag':'')+'">'+r.geoFlags+'</td></tr>';
433
       '<td class="num'+(r.geoFlags>0?' badflag':'')+'">'+r.geoFlags+'</td></tr>';
375
  });
434
  });
376
  return h+'</tbody></table>';
435
  return h+'</tbody></table>';
377
}
436
}
Line 448... Line 507...
448
function openExec(userId, name, date){
507
function openExec(userId, name, date){
449
  if(!userId){ return; }
508
  if(!userId){ return; }
450
  var mb=el('bj-modal-back'), m=el('bj-modal');
509
  var mb=el('bj-modal-back'), m=el('bj-modal');
451
  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>';
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>';
452
  mb.classList.add('show');
511
  mb.classList.add('show');
453
  fetch(CTX+'/beat-report/live-tracking?userId='+userId+'&date='+encodeURIComponent(date),{headers:{'Accept':'application/json'}})
512
  fetch(CTX+'/beat-journey/journey-detail?userId='+userId+'&date='+encodeURIComponent(date),{headers:{'Accept':'application/json'}})
454
    .then(function(r){ return r.json(); }).then(function(d){
513
    .then(function(r){ return r.json(); }).then(function(d){
-
 
514
      window._mediaUrl = d.mediaUrl || '';
455
      var v=d.visits||[];
515
      var all=(d.visits||[]).slice();
-
 
516
      // Sequence like the field report: Punch in -> visited stops (by check-in
-
 
517
      // time) -> PENDING/not-visited -> Punch out. The attendance row carries
-
 
518
      // both punch-in & punch-out times; split it into the two bookends.
-
 
519
      var att=null, visited=[], pending=[];
-
 
520
      all.forEach(function(x){
-
 
521
        var tt=(x.taskType||'').toLowerCase(), mt=(x.markType||'').toUpperCase();
-
 
522
        if(tt==='attendance' || mt.indexOf('PUNCH')>=0){ if(!att) att=x; }
-
 
523
        else if(x.checkInTime){ visited.push(x); }
-
 
524
        else { pending.push(x); }
-
 
525
      });
-
 
526
      visited.sort(function(a,b){ return tlSecs(a.checkInTime)-tlSecs(b.checkInTime); });
-
 
527
 
-
 
528
      // totals across the day's stops (attendance row excluded from leg sums)
-
 
529
      var distKm=0, inStoreSec=0, transitSec=0, leadsN=0;
-
 
530
      all.forEach(function(x){
-
 
531
        var tt=(x.taskType||'').toLowerCase();
-
 
532
        if(tt==='attendance') return;
-
 
533
        if(x.totalDistance) distKm += (parseFloat(x.totalDistance)||0);
-
 
534
        inStoreSec += toSec(x.timeSpent);
-
 
535
        transitSec += toSec(x.transitTime);
-
 
536
        if(tt==='lead') leadsN++;
-
 
537
      });
-
 
538
      var workSec = (att && att.checkInTime && att.checkOutTime) ? (toSec(att.checkOutTime)-toSec(att.checkInTime)) : (att?toSec(att.timeSpent):0);
-
 
539
      if(workSec<0) workSec=0;
-
 
540
 
-
 
541
      var seq=[];
-
 
542
      if(att && att.checkInTime) seq.push(punchEntry('in', att.checkInTime));
-
 
543
      visited.forEach(function(x){ seq.push(x); });
-
 
544
      pending.forEach(function(x){ seq.push(x); });
-
 
545
      if(att && att.checkOutTime) seq.push(punchEntry('out', att.checkOutTime));
-
 
546
 
-
 
547
      var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(name)+'</h3>'+
456
      var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(name)+'</h3><p class="desc">Journey detail &middot; '+esc(date)+' &middot; battery '+(d.batteryLevel==null?'&mdash;':d.batteryLevel+'%')+' &middot; loc-off '+(d.locationToggleCount||0)+'</p>';
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">'+
-
 
550
        jstat(fmtSec(workSec),'Working hours')+
-
 
551
        jstat(fmtSec(inStoreSec),'In-store time')+
-
 
552
        jstat(fmtSec(transitSec),'Travel time')+
-
 
553
        jstat(distKm.toFixed(2)+' km','Distance Covered')+
-
 
554
        jstat(visited.length+(leadsN?' &middot; '+leadsN+'L':''),'Stores visited')+
-
 
555
        '</div>';
457
      if(!v.length){ h+='<p class="desc">No tracking events for this day.</p>'; }
556
      if(!seq.length){ h+='<p class="desc">No tracking events for this day.</p>'; }
458
      else{
557
      else{
459
        h+='<table><thead><tr><th>Stop</th><th>Type</th><th>Status</th><th>In</th><th>Out</th><th class="num">Time</th></tr></thead><tbody>';
558
        h+='<div class="tl">';
-
 
559
        var ri=0;
460
        v.forEach(function(x){
560
        seq.forEach(function(x){
461
          h+='<tr><td>'+esc(x.taskName||'&mdash;')+'</td><td>'+esc(x.taskType||'')+'</td><td>'+esc(x.markType||'')+'</td>'+
561
          // travel connector before each visited stop: time taken + distance to reach it
-
 
562
          if(!x._punch && x.checkInTime){
-
 
563
            var tr=(x.transitTime && x.transitTime!=='00:00:00') ? x.transitTime : null;
462
             '<td>'+(x.checkInTime?esc(x.checkInTime):'&mdash;')+'</td><td>'+(x.checkOutTime?esc(x.checkOutTime):'&mdash;')+'</td>'+
564
            var km=(x.totalDistance && parseFloat(x.totalDistance)>0) ? (parseFloat(x.totalDistance).toFixed(2)+' km') : null;
-
 
565
            if(tr || km){
-
 
566
              var parts=[]; if(tr) parts.push('&#128663; '+esc(tr)); if(km) parts.push(esc(km));
463
             '<td class="num">'+(x.timeSpent?esc(x.timeSpent):'&mdash;')+'</td></tr>';
567
              h+='<div class="tlgap"><span class="gappill">'+parts.join(' &middot; ')+'</span></div>';
-
 
568
            }
-
 
569
          }
-
 
570
          h+=tlRow(x, ri%2===0?'left':'right'); ri++;
464
        });
571
        });
465
        h+='</tbody></table>';
572
        h+='</div>';
466
      }
573
      }
467
      m.innerHTML=h;
574
      m.innerHTML=h;
468
    }).catch(function(){ m.innerHTML='<button class="x" onclick="closeModal()">&times;</button><p class="desc">Could not load detail.</p>'; });
575
    }).catch(function(){ m.innerHTML='<button class="x" onclick="closeModal()">&times;</button><p class="desc">Could not load detail.</p>'; });
469
}
576
}
-
 
577
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)); }
-
 
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); }
-
 
580
function jstat(v,l){ return '<div class="js"><div class="jv">'+v+'</div><div class="jl">'+l+'</div></div>'; }
-
 
581
function hhmm(t){ return t? t.slice(0,5):''; }
-
 
582
function tlThumb(att){
-
 
583
  if(att && window._mediaUrl) return '<img class="tlthumb" src="'+esc(window._mediaUrl+att)+'" onclick="window.open(this.src,\'_blank\')" onerror="tlImgErr(this)">';
-
 
584
  return '<div class="tlph" title="No photo">&#128247;</div>';
-
 
585
}
-
 
586
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); }
-
 
587
function punchEntry(kind, time){ return {_punch:kind, checkInTime:time, markType:(kind==='in'?'PUNCHIN':'PUNCHOUT'), taskType:'attendance'}; }
-
 
588
function tlStyle(x){
-
 
589
  var mt=(x.markType||'').toUpperCase(), tt=(x.taskType||'').toLowerCase();
-
 
590
  if(x._punch || tt==='attendance' || mt.indexOf('PUNCH')>=0) return {c:'#7048e8', l:(x._punch==='out'?'Punch out':'Punch in'), ic:'&#9873;'};
-
 
591
  if(mt.indexOf('CHECKOUT')>=0) return {c:'#2f9e44', l:'Visited', ic:'&#10003;'};
-
 
592
  if(mt==='CHECKIN') return {c:'#1098ad', l:'In store', ic:'&#128205;'};
-
 
593
  if(mt==='DEFERRED') return {c:'#e8590c', l:'Deferred', ic:'&#8635;'};
-
 
594
  if(mt==='PENDING' || !x.checkInTime) return {c:'#97a1b5', l:'Pending', ic:'&#9675;'};
-
 
595
  return {c:'#3b5bdb', l:(mt||'-'), ic:'&#9679;'};
-
 
596
}
-
 
597
function tlTitle(x){
-
 
598
  if(x._punch) return x._punch==='in' ? 'Journey start (Punch in)' : 'Journey end (Punch out)';
-
 
599
  var n=x.taskName||'';
-
 
600
  var pipe=n.lastIndexOf('|'); if(pipe>=0) n=n.slice(pipe+1);      // drop leading agenda tags
-
 
601
  n=n.replace(/\s*VISIT\b.*$/i,'').replace(/\s*visit\s*$/i,'').replace(/\bnull\b/gi,'').replace(/\s{2,}/g,' ').trim();
-
 
602
  return n || '(stop)';
-
 
603
}
-
 
604
function tlRow(x, side){
-
 
605
  var s=tlStyle(x);
-
 
606
  var tt=(x.taskType||'').toLowerCase();
-
 
607
  var timeTop = x.checkInTime ? hhmm(x.checkInTime) : '-';
-
 
608
  var tlabel = x._punch ? (x._punch==='in'?'punch in':'punch out') : (x.checkInTime?'check-in':'pending');
-
 
609
  var meta;
-
 
610
  if(x._punch){
-
 
611
    meta = (x._punch==='in'?'Punch in':'Punch out') + ' &middot; ' + hhmm(x.checkInTime);
-
 
612
  } else {
-
 
613
    var io = (x.checkInTime?hhmm(x.checkInTime):'-') + ' &rarr; ' + (x.checkOutTime?hhmm(x.checkOutTime):'-');
-
 
614
    var dur = (x.timeSpent && x.timeSpent!=='00:00:00') ? ' &middot; '+esc(x.timeSpent) : '';
-
 
615
    var typeLabel = tt==='lead' ? 'Lead' : (tt==='office-visit'?'Office':'Partner');
-
 
616
    meta = x.checkInTime ? (typeLabel+' &middot; '+io+dur) : (typeLabel+' &middot; not visited');
-
 
617
  }
-
 
618
  // image (or placeholder) + battery + location-toggle on real stops
-
 
619
  var extra='';
-
 
620
  if(!x._punch){
-
 
621
    var chips='';
-
 
622
    if(x.battery!=null) chips += '<span class="tlchip">&#128267; '+x.battery+'%</span>';
-
 
623
    if(x.locOff!=null) chips += '<span class="tlchip">&#128244; '+x.locOff+'</span>';
-
 
624
    extra = '<div class="tlextra">'+tlThumb(x.attachment)+chips+'</div>';
-
 
625
  }
-
 
626
  return '<div class="tlrow '+side+'">'+
-
 
627
      '<div class="node" style="background:'+s.c+'">'+s.ic+'</div>'+
-
 
628
      '<div class="tlcard">'+
-
 
629
        '<div class="tm" style="background:'+s.c+'">'+timeTop+'<small>'+tlabel+'</small></div>'+
-
 
630
        '<div class="bd"><div class="ti">'+esc(tlTitle(x))+'</div>'+
-
 
631
          '<span class="stt" style="background:'+s.c+'">'+s.l+'</span>'+
-
 
632
          '<div class="mt">'+meta+'</div>'+extra+'</div>'+
-
 
633
      '</div></div>';
-
 
634
}
470
function closeModal(){ el('bj-modal-back').classList.remove('show'); }
635
function closeModal(){ el('bj-modal-back').classList.remove('show'); }
471
 
636
 
472
/* ============================ APPROVAL ============================ */
637
/* ============================ APPROVAL ============================ */
473
function loadApproval(){
638
function loadApproval(){
474
  spinner();
639
  spinner();