| 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 ? '—' : (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||'—')+'</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">✓</span>':'<span class="badflag">✗</span>')+'</td>'+
|
299 |
'<td>'+(r.agendaFilled?'<span style="color:#2f9e44">✓</span>':'<span class="badflag">✗</span>')+'</td>'+
|
| 266 |
'<td>'+(r.punchInTime?esc(r.punchInTime):'<span class="badflag">—</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?'—':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:'⚠', title:'Not punched in', sub:'executives with a beat, not started', cls:'badflag'},
|
| - |
|
312 |
idleStale: {bg:'#fff4e6', fg:'#e8590c', ic:'⏱', title:'Idle / stale GPS > 40 min', sub:'no recent GPS ping', cls:'warnflag'},
|
| - |
|
313 |
geofence: {bg:'#fff0f0', fg:'#e03131', ic:'📍', title:'Check-in outside store geofence', sub:'>50 m from partner\'s saved location', cls:'badflag'},
|
| - |
|
314 |
locationOff: {bg:'#fff4e6', fg:'#e8590c', ic:'📴', title:'Location turned off mid-journey', sub:'GPS toggled off', cls:'warnflag'},
|
| - |
|
315 |
deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'↻', 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','⚠','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','⏱','Idle / stale GPS > 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','📍','Check-in outside store geofence','>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','📴','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','↻','Deferred, awaiting re-action','shops deferred today', a.deferredAwaiting, '');
|
328 |
'<div class="acount '+(n?m.cls:'')+'">'+n+'</div>'+(n?'<span class="chev">▸</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()">×</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||'—')+(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()">×</button><h3>'+esc(name)+'</h3><p class="desc">Journey detail · '+esc(date)+'</p><div class="loading"><div class="spinner"></div>Loading…</div>';
|
510 |
m.innerHTML='<button class="x" onclick="closeModal()">×</button><h3>'+esc(name)+'</h3><p class="desc">Journey detail · '+esc(date)+'</p><div class="loading"><div class="spinner"></div>Loading…</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()">×</button><h3>'+esc(name)+'</h3>'+
|
| 456 |
var h='<button class="x" onclick="closeModal()">×</button><h3>'+esc(name)+'</h3><p class="desc">Journey detail · '+esc(date)+' · battery '+(d.batteryLevel==null?'—':d.batteryLevel+'%')+' · loc-off '+(d.locationToggleCount||0)+'</p>';
|
548 |
'<p class="desc">Journey detail · '+esc(date)+' · battery '+(d.batteryLevel==null?'-':d.batteryLevel+'%')+' · 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?' · '+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||'—')+'</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):'—')+'</td><td>'+(x.checkOutTime?esc(x.checkOutTime):'—')+'</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('🚗 '+esc(tr)); if(km) parts.push(esc(km));
|
| 463 |
'<td class="num">'+(x.timeSpent?esc(x.timeSpent):'—')+'</td></tr>';
|
567 |
h+='<div class="tlgap"><span class="gappill">'+parts.join(' · ')+'</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()">×</button><p class="desc">Could not load detail.</p>'; });
|
575 |
}).catch(function(){ m.innerHTML='<button class="x" onclick="closeModal()">×</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">📷</div>';
|
| - |
|
585 |
}
|
| - |
|
586 |
function tlImgErr(im){ var d=document.createElement('div'); d.className='tlph'; d.title='Photo unavailable'; d.innerHTML='📷'; 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:'⚑'};
|
| - |
|
591 |
if(mt.indexOf('CHECKOUT')>=0) return {c:'#2f9e44', l:'Visited', ic:'✓'};
|
| - |
|
592 |
if(mt==='CHECKIN') return {c:'#1098ad', l:'In store', ic:'📍'};
|
| - |
|
593 |
if(mt==='DEFERRED') return {c:'#e8590c', l:'Deferred', ic:'↻'};
|
| - |
|
594 |
if(mt==='PENDING' || !x.checkInTime) return {c:'#97a1b5', l:'Pending', ic:'○'};
|
| - |
|
595 |
return {c:'#3b5bdb', l:(mt||'-'), ic:'●'};
|
| - |
|
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') + ' · ' + hhmm(x.checkInTime);
|
| - |
|
612 |
} else {
|
| - |
|
613 |
var io = (x.checkInTime?hhmm(x.checkInTime):'-') + ' → ' + (x.checkOutTime?hhmm(x.checkOutTime):'-');
|
| - |
|
614 |
var dur = (x.timeSpent && x.timeSpent!=='00:00:00') ? ' · '+esc(x.timeSpent) : '';
|
| - |
|
615 |
var typeLabel = tt==='lead' ? 'Lead' : (tt==='office-visit'?'Office':'Partner');
|
| - |
|
616 |
meta = x.checkInTime ? (typeLabel+' · '+io+dur) : (typeLabel+' · 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">🔋 '+x.battery+'%</span>';
|
| - |
|
623 |
if(x.locOff!=null) chips += '<span class="tlchip">📴 '+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();
|