| 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…</div></div>
|
220 |
<div id="bj-content"><div class="loading"><div class="spinner"></div>Loading…</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 {'&':'&','<':'<','>':'>','"':'"'}[c];}); }
|
246 |
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[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…</div>'; }
|
278 |
function spinner(){ el('bj-content').innerHTML = '<div class="loading"><div class="spinner"></div>Loading…</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('👥','#eef1fb','#3b5bdb','Executives scheduled',num(k.scheduled),k.notStarted+' not started yet');
|
321 |
h+=kpi('👥','#eef1fb','#3b5bdb','Executives scheduled',num(k.scheduled),k.notStarted+' not started yet');
|
| 260 |
h+=kpi('⏱','#ebfbee','#2f9e44','Punched in',num(k.punchedIn)+' <small>/'+k.scheduled+'</small>',k.notStarted+' pending');
|
322 |
h+=kpi('⏱','#ebfbee','#2f9e44','Punched in',num(k.punchedIn)+' <small>/'+k.scheduled+'</small>',k.notStarted+' pending');
|
| 261 |
h+=kpi('📍','#e7f5ff','#1098ad','On route now',num(k.onRouteNow),'live journeys');
|
323 |
h+=kpi('📍','#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),'−'+(f.scheduled-f.punchedIn)+' never started',true,false);
|
332 |
h+=fstep('Punched in',f.punchedIn,pct(f.punchedIn,sc),'−'+(f.scheduled-f.punchedIn)+' never started',true,false);
|
| 271 |
h+=fstep('Made a check-in',f.madeCheckin,pct(f.madeCheckin,sc),'−'+(f.punchedIn-f.madeCheckin)+' punched, idle',true,false);
|
333 |
h+=fstep('Made a check-in',f.madeCheckin,pct(f.madeCheckin,sc),'−'+(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">✓</span>':'<span class="badflag" style="font-size:15px">✗</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">✓</span>':'<span class="badflag">✗</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\')">📍</button>'+
|
| - |
|
377 |
'<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">👁</button>'+
|
| - |
|
378 |
'<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">✎</button>'+
|
| - |
|
379 |
'<button class="actbtn add" title="Assign / add visit in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'add\')">+</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:'⚠', title:'Not punched in', sub:'executives with a beat, not started', cls:'badflag'},
|
386 |
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'},
|
387 |
idleStale: {bg:'#fff4e6', fg:'#e8590c', ic:'⏱', title:'Idle — off-store > 40 min', sub:'stationary >20 m from any store', 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'},
|
388 |
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'},
|
389 |
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:''}
|
390 |
deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'↻', 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()">×</button><h3>'+m.title+'</h3>'+
|
412 |
var h='<button class="x" onclick="closeModal()">×</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?' · 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 & 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="valph">📷 Photo unavailable</div>\'">'
|
| - |
|
435 |
: '<div class="valph">📷 No photo on this check-in</div>';
|
| - |
|
436 |
el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">×</button><h3>Validate check-in</h3>'+
|
| - |
|
437 |
'<p class="desc">'+esc(item.name)+' · '+esc(item.detail||'')+'</p>'+
|
| - |
|
438 |
'<div class="valwrap">'+img+'</div>'+
|
| - |
|
439 |
'<div class="valactions">'+
|
| - |
|
440 |
'<button class="exectab" onclick="openAttention(\'geofence\')">← 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–L5 sales hierarchy, so each manager sees only their team.</li>'+
|
609 |
'<li><b>Coverage and the manager view</b> follow the existing L1–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()">×</button><h3>'+esc(name)+'</h3><p class="desc">Journey detail · '+esc(date)+'</p><div class="loading"><div class="spinner"></div>Loading…</div>';
|
618 |
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>';
|
| 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()">×</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()">×</button><h3>'+esc(ex.name)+'</h3>'+
|
| - |
|
631 |
'<p class="desc">Journey detail · '+esc(ex.date)+' · battery '+(d.batteryLevel==null?'-':d.batteryLevel+'%')+' · loc-off '+(d.locationToggleCount||0)+'</p>'+
|
| - |
|
632 |
'<div class="exectabs">'+
|
| - |
|
633 |
'<button class="exectab'+(tab==='timeline'?' active':'')+'" onclick="renderExec(\'timeline\')">📝 Timeline</button>'+
|
| - |
|
634 |
'<button class="exectab'+(tab==='map'?' active':'')+'" onclick="renderExec(\'map\')">📍 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…</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()">×</button><h3>'+esc(name)+'</h3>'+
|
- |
|
| 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">'+
|
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?' · '+leadsN+'L':''),'Stores visited')+
|
680 |
jstat(visited.length+(leadsN?' · '+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('🚗 '+esc(tr)); if(km) parts.push(esc(km));
|
690 |
var parts=[]; if(tr) parts.push('🚗 '+esc(tr)); if(km) parts.push(esc(km));
|
| 567 |
h+='<div class="tlgap"><span class="gappill">'+parts.join(' · ')+'</span></div>';
|
691 |
h+='<div class="tlgap"><span class="gappill">'+parts.join(' · ')+'</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>📍 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>⏱ 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>⚑ 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>⚑ 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()">×</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>
|