| Line 519... |
Line 519... |
| 519 |
h+=fstep('Punched Out',f.punchedOut,pct(f.punchedOut,pin),'journey ended',false,true,'punchedOut');
|
519 |
h+=fstep('Punched Out',f.punchedOut,pct(f.punchedOut,pin),'journey ended',false,true,'punchedOut');
|
| 520 |
h+='</div></div>';
|
520 |
h+='</div></div>';
|
| 521 |
|
521 |
|
| 522 |
window._boardFilter = null; // funnel-card filter (null = all rows)
|
522 |
window._boardFilter = null; // funnel-card filter (null = all rows)
|
| 523 |
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><div id="bj-board">'+boardCard()+'</div></div>';
|
523 |
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><div id="bj-board">'+boardCard()+'</div></div>';
|
| - |
|
524 |
|
| - |
|
525 |
window._visitFlags = d.visitFlags || [];
|
| - |
|
526 |
h+='<div class="sectionlabel">Visit-quality flags</div>';
|
| - |
|
527 |
h+='<div class="card"><h2>Visit-quality flags</h2><p class="desc">Every rule scored on today\'s visits. Click a row with hits to see the flagged visits.</p>'+flagsTable(window._visitFlags)+'</div>';
|
| - |
|
528 |
|
| 524 |
el('bj-content').innerHTML=h;
|
529 |
el('bj-content').innerHTML=h;
|
| 525 |
}
|
530 |
}
|
| 526 |
function stateClass(st){ return st==='IN_STORE'?'ps-store':st==='TRAVELLING'?'ps-travel':st==='DONE'?'ps-done':st==='NOT_STARTED'?'ps-no':'ps-idle'; }
|
531 |
function stateClass(st){ return st==='IN_STORE'?'ps-store':st==='TRAVELLING'?'ps-travel':st==='DONE'?'ps-done':st==='NOT_STARTED'?'ps-no':'ps-idle'; }
|
| 527 |
function js(s){ return String(s==null?'':s).replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
|
532 |
function js(s){ return String(s==null?'':s).replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
|
| 528 |
// Open the Beat Planner (map-first planner window) for this executive/beat/day,
|
533 |
// Open the Beat Planner (map-first planner window) for this executive/beat/day,
|
| Line 551... |
Line 556... |
| 551 |
}
|
556 |
}
|
| 552 |
// Board card content: an optional "filtered by …" bar plus the table, showing
|
557 |
// Board card content: an optional "filtered by …" bar plus the table, showing
|
| 553 |
// only the rows that reached the selected funnel stage (Scheduled = all rows).
|
558 |
// only the rows that reached the selected funnel stage (Scheduled = all rows).
|
| 554 |
function boardCard(){
|
559 |
function boardCard(){
|
| 555 |
var rows=window._board||[], date=window._boardDate||'', key=window._boardFilter;
|
560 |
var rows=window._board||[], date=window._boardDate||'', key=window._boardFilter;
|
| 556 |
var view=rows, m=key?FUNNEL_META[key]:null;
|
561 |
var view=(rows||[]).slice(), m=key?FUNNEL_META[key]:null; // copy — never reorder the source
|
| 557 |
if(m) view=rows.filter(m.f);
|
562 |
if(m) view=view.filter(m.f);
|
| - |
|
563 |
if(boardSortKey) view.sort(function(a,b){
|
| - |
|
564 |
var x=boardSortVal(a,boardSortKey), y=boardSortVal(b,boardSortKey);
|
| - |
|
565 |
if(typeof x==='string') return boardSortAsc? (''+x).localeCompare(''+y):(''+y).localeCompare(''+x);
|
| - |
|
566 |
return boardSortAsc? x-y : y-x;
|
| - |
|
567 |
});
|
| 558 |
var bar='';
|
568 |
var bar='';
|
| 559 |
if(m){
|
569 |
if(m){
|
| 560 |
bar='<div class="boardfilter">Showing <b>'+esc(m.title)+'</b> · '+view.length+
|
570 |
bar='<div class="boardfilter">Showing <b>'+esc(m.title)+'</b> · '+view.length+
|
| 561 |
' of '+rows.length+'<button class="clearf" onclick="setBoardFilter(null)">× Clear filter</button></div>';
|
571 |
' of '+rows.length+'<button class="clearf" onclick="setBoardFilter(null)">× Clear filter</button></div>';
|
| 562 |
}
|
572 |
}
|
| Line 570... |
Line 580... |
| 570 |
var total=Math.max(r.planned||0, completed+deferred);
|
580 |
var total=Math.max(r.planned||0, completed+deferred);
|
| 571 |
return {total:total, completed:completed, deferred:deferred, pending:total-completed-deferred};
|
581 |
return {total:total, completed:completed, deferred:deferred, pending:total-completed-deferred};
|
| 572 |
}
|
582 |
}
|
| 573 |
// seconds -> compact "1h 20m" / "45m" for the board's time columns.
|
583 |
// seconds -> compact "1h 20m" / "45m" for the board's time columns.
|
| 574 |
function bhm(sec){ sec=Math.max(0,Math.round(sec||0)); var h=Math.floor(sec/3600), m=Math.round((sec%3600)/60); return h?(h+'h '+m+'m'):(m+'m'); }
|
584 |
function bhm(sec){ sec=Math.max(0,Math.round(sec||0)); var h=Math.floor(sec/3600), m=Math.round((sec%3600)/60); return h?(h+'h '+m+'m'):(m+'m'); }
|
| - |
|
585 |
// Click-to-sort for the live board (mirrors the Period scorecard's sortCard). The
|
| - |
|
586 |
// numeric columns are derived (boardCounts) and times are raw seconds, so we sort on
|
| - |
|
587 |
// the underlying values here — not the rendered "1h 20m"/count strings.
|
| - |
|
588 |
var boardSortKey=null, boardSortAsc=false;
|
| - |
|
589 |
function boardSortVal(r, key){
|
| - |
|
590 |
switch(key){
|
| - |
|
591 |
case 'agenda': return r.agendaFilled?1:0;
|
| - |
|
592 |
case 'name': return r.name||'';
|
| - |
|
593 |
case 'punch': return r.punchInTime||''; // HH:MM:SS string; blanks group together
|
| - |
|
594 |
case 'total': case 'completed': case 'pending': case 'deferred':
|
| - |
|
595 |
return boardCounts(r)[key];
|
| - |
|
596 |
case 'transit': return r.transitSecs||0;
|
| - |
|
597 |
case 'inStore': return r.inStoreSecs||0;
|
| - |
|
598 |
case 'state': return r.stateLabel||'';
|
| - |
|
599 |
default: return 0;
|
| - |
|
600 |
}
|
| - |
|
601 |
}
|
| - |
|
602 |
function sortBoard(key){
|
| - |
|
603 |
if(boardSortKey===key) boardSortAsc=!boardSortAsc; else { boardSortKey=key; boardSortAsc=false; }
|
| - |
|
604 |
var c=el('bj-board'); if(c) c.innerHTML=boardCard(); // re-render; sort applied inside boardCard()
|
| - |
|
605 |
}
|
| - |
|
606 |
// Sort-direction caret, shown only on the active column.
|
| - |
|
607 |
function bsArrow(key){ return boardSortKey===key ? (boardSortAsc?' ▲':' ▼') : ''; }
|
| 575 |
function boardTable(rows, date){
|
608 |
function boardTable(rows, date){
|
| 576 |
if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
|
609 |
if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
|
| 577 |
var h='<div style="overflow-x:auto"><table><thead><tr>'+
|
610 |
var h='<div style="overflow-x:auto"><table><thead><tr>'+
|
| - |
|
611 |
'<th onclick="sortBoard(\'agenda\')">Agenda'+bsArrow('agenda')+'</th>'+
|
| 578 |
'<th>Agenda</th><th>Executive</th><th>Punch In/Out</th>'+
|
612 |
'<th onclick="sortBoard(\'name\')">Executive'+bsArrow('name')+'</th>'+
|
| - |
|
613 |
'<th onclick="sortBoard(\'punch\')">Punch In/Out'+bsArrow('punch')+'</th>'+
|
| 579 |
'<th class="num" title="Planned partner visits + self-assigned leads">Total</th>'+
|
614 |
'<th class="num" title="Planned partner visits + self-assigned leads" onclick="sortBoard(\'total\')">Total'+bsArrow('total')+'</th>'+
|
| - |
|
615 |
'<th class="num" onclick="sortBoard(\'completed\')">Completed'+bsArrow('completed')+'</th>'+
|
| 580 |
'<th class="num">Completed</th><th class="num">Pending</th><th class="num">Deferred</th>'+
|
616 |
'<th class="num" onclick="sortBoard(\'pending\')">Pending'+bsArrow('pending')+'</th>'+
|
| - |
|
617 |
'<th class="num" onclick="sortBoard(\'deferred\')">Deferred'+bsArrow('deferred')+'</th>'+
|
| 581 |
'<th class="num" title="Transit time between stops so far today">Transit</th>'+
|
618 |
'<th class="num" title="Transit time between stops so far today" onclick="sortBoard(\'transit\')">Transit'+bsArrow('transit')+'</th>'+
|
| 582 |
'<th class="num" title="Time spent inside stores so far today">In-store</th>'+
|
619 |
'<th class="num" title="Time spent inside stores so far today" onclick="sortBoard(\'inStore\')">In-store'+bsArrow('inStore')+'</th>'+
|
| 583 |
'<th>State</th><th>Action</th>'+
|
620 |
'<th onclick="sortBoard(\'state\')">State'+bsArrow('state')+'</th><th>Action</th>'+
|
| 584 |
'</tr></thead><tbody>';
|
621 |
'</tr></thead><tbody>';
|
| 585 |
// column totals, computed up front so the Total row can lead the table
|
622 |
// column totals, computed up front so the Total row can lead the table
|
| 586 |
var t={total:0,completed:0,pending:0,deferred:0,transit:0,inStore:0};
|
623 |
var t={total:0,completed:0,pending:0,deferred:0,transit:0,inStore:0};
|
| 587 |
rows.forEach(function(r){
|
624 |
rows.forEach(function(r){
|
| 588 |
var c=boardCounts(r);
|
625 |
var c=boardCounts(r);
|
| Line 603... |
Line 640... |
| 603 |
var A = r.userId+",'"+js(r.name)+"','"+date+"'"; // openExec args
|
640 |
var A = r.userId+",'"+js(r.name)+"','"+date+"'"; // openExec args
|
| 604 |
var go = 'onclick="openExec('+A+')"';
|
641 |
var go = 'onclick="openExec('+A+')"';
|
| 605 |
var c=boardCounts(r);
|
642 |
var c=boardCounts(r);
|
| 606 |
h+='<tr class="clickable '+rowcls+'" '+go+'>'+
|
643 |
h+='<tr class="clickable '+rowcls+'" '+go+'>'+
|
| 607 |
'<td style="text-align:center">'+(r.agendaFilled?'<span style="color:#2f9e44;font-size:15px">✓</span>':'<span class="badflag" style="font-size:15px">✗</span>')+'</td>'+
|
644 |
'<td style="text-align:center">'+(r.agendaFilled?'<span style="color:#2f9e44;font-size:15px">✓</span>':'<span class="badflag" style="font-size:15px">✗</span>')+'</td>'+
|
| 608 |
'<td><b>'+esc(r.name)+'</b><br>'+esc(r.beatName||'-')+'</td>'+
|
645 |
'<td><b>'+esc(r.name)+'</b>'+flagBadge(r)+'<br>'+esc(r.beatName||'-')+'</td>'+
|
| 609 |
'<td style="white-space:nowrap">'+(r.punchInTime?'<span class="ptime">'+esc(r.punchInTime)+'</span>':'<span class="badflag">-</span>')+
|
646 |
'<td style="white-space:nowrap">'+(r.punchInTime?'<span class="ptime">'+esc(r.punchInTime)+'</span>':'<span class="badflag">-</span>')+
|
| 610 |
' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
|
647 |
' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
|
| 611 |
'<td class="num cnt cmuted">'+c.total+'</td>'+
|
648 |
'<td class="num cnt cmuted">'+c.total+'</td>'+
|
| 612 |
'<td class="num cnt cgreen">'+c.completed+'</td>'+
|
649 |
'<td class="num cnt cgreen">'+c.completed+'</td>'+
|
| 613 |
'<td class="num cnt">'+c.pending+'</td>'+
|
650 |
'<td class="num cnt">'+c.pending+'</td>'+
|
| Line 889... |
Line 926... |
| 889 |
idleStale: {bg:'#fff4e6', fg:'#e8590c', ic:'⏱', title:'Idle — off-store > 59 min', sub:'stationary >20 m from any store', cls:'warnflag'},
|
926 |
idleStale: {bg:'#fff4e6', fg:'#e8590c', ic:'⏱', title:'Idle — off-store > 59 min', sub:'stationary >20 m from any store', cls:'warnflag'},
|
| 890 |
geofence: {bg:'#fff0f0', fg:'#e03131', ic:'📍', title:'Check-in outside store geofence', sub:'>50 m from partner\'s saved location', cls:'badflag'},
|
927 |
geofence: {bg:'#fff0f0', fg:'#e03131', ic:'📍', title:'Check-in outside store geofence', sub:'>50 m from partner\'s saved location', cls:'badflag'},
|
| 891 |
locationOff: {bg:'#fff4e6', fg:'#e8590c', ic:'📴', title:'Location turned off mid-journey', sub:'GPS toggled off', cls:'warnflag'},
|
928 |
locationOff: {bg:'#fff4e6', fg:'#e8590c', ic:'📴', title:'Location turned off mid-journey', sub:'GPS toggled off', cls:'warnflag'},
|
| 892 |
deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'↻', title:'Deferred, awaiting re-action', sub:'shops deferred today', cls:''}
|
929 |
deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'↻', title:'Deferred, awaiting re-action', sub:'shops deferred today', cls:''}
|
| 893 |
};
|
930 |
};
|
| - |
|
931 |
// ---- Visit-quality flags: a today-scoped replica of the 14-row monitor table ----
|
| - |
|
932 |
// higher rate = worse, so redder as the flagged share climbs.
|
| - |
|
933 |
function flagRateColor(p){ if(p>=50)return '#e03131'; if(p>=25)return '#e8590c'; if(p>=10)return '#f08c00'; return '#2f9e44'; }
|
| - |
|
934 |
function flagStatusColor(s){
|
| - |
|
935 |
switch(s){
|
| - |
|
936 |
case 'action': return '#e03131';
|
| - |
|
937 |
case 'systemic': return '#e8590c';
|
| - |
|
938 |
case 'partial': return '#f08c00';
|
| - |
|
939 |
case 'needs floor': return '#1098ad';
|
| - |
|
940 |
case 'too broad': return '#7048e8';
|
| - |
|
941 |
case 'contained': return '#2f9e44';
|
| - |
|
942 |
default: return '#97a1b5'; // "never fires" / "no data"
|
| - |
|
943 |
}
|
| - |
|
944 |
}
|
| - |
|
945 |
function flagStatusChip(s){
|
| - |
|
946 |
var c=flagStatusColor(s);
|
| - |
|
947 |
return '<span style="display:inline-block;font-size:10.5px;font-weight:700;padding:2px 8px;border-radius:999px;background:'+c+'1a;color:'+c+'">'+esc(s)+'</span>';
|
| - |
|
948 |
}
|
| - |
|
949 |
function flagsTable(list){
|
| - |
|
950 |
if(!list||!list.length) return '<p class="desc">No flag data for this date.</p>';
|
| - |
|
951 |
var h='<div style="overflow-x:auto"><table><thead><tr>'+
|
| - |
|
952 |
'<th style="width:34px">#</th><th>Rule</th><th class="num">Flagged</th><th class="num">Of</th><th class="num">Rate</th><th>Status</th>'+
|
| - |
|
953 |
'</tr></thead><tbody>';
|
| - |
|
954 |
list.forEach(function(r){
|
| - |
|
955 |
var hits=r.count||0, denom=r.denom||0, rate=pct(hits,denom);
|
| - |
|
956 |
var open = hits>0 ? ' class="clickable" onclick="openFlag(\''+r.code+'\')"' : '';
|
| - |
|
957 |
var cntColor = hits>0 ? flagRateColor(rate) : '#97a1b5';
|
| - |
|
958 |
h+='<tr'+open+'>'+
|
| - |
|
959 |
'<td style="font-weight:700;color:#97a1b5">'+esc(r.num)+'</td>'+
|
| - |
|
960 |
'<td>'+esc(r.title)+'</td>'+
|
| - |
|
961 |
'<td class="num cnt" style="font-weight:700;color:'+cntColor+'">'+num(hits)+'</td>'+
|
| - |
|
962 |
'<td class="num cnt cmuted">'+num(denom)+'</td>'+
|
| - |
|
963 |
'<td class="num cnt cmuted">'+(denom?rate+'%':'—')+'</td>'+
|
| - |
|
964 |
'<td>'+flagStatusChip(r.status)+(hits>0?' <span class="chev">▸</span>':'')+'</td>'+
|
| - |
|
965 |
'</tr>';
|
| - |
|
966 |
});
|
| - |
|
967 |
return h+'</tbody></table></div>';
|
| - |
|
968 |
}
|
| - |
|
969 |
function openFlag(code){
|
| - |
|
970 |
var list=window._visitFlags||[], r=null;
|
| - |
|
971 |
for(var i=0;i<list.length;i++){ if(list[i].code===code){ r=list[i]; break; } }
|
| - |
|
972 |
if(!r) return;
|
| - |
|
973 |
var items=r.items||[], date=window._boardDate||'';
|
| - |
|
974 |
var h='<button class="x" onclick="closeModal()">×</button><h3>'+esc(r.num)+' · '+esc(r.title)+'</h3>'+
|
| - |
|
975 |
'<p class="desc">'+items.length+' flagged '+(items.length===1?'visit':'visits')+
|
| - |
|
976 |
(r.denom?' of '+num(r.denom)+' · '+pct(r.count,r.denom)+'%':'')+' · '+flagStatusChip(r.status)+'</p>';
|
| - |
|
977 |
if(!items.length){ h+='<p class="desc">Nothing flagged today.</p>'; }
|
| - |
|
978 |
else{
|
| - |
|
979 |
h+='<p class="desc">Click a row to open that executive\'s journey detail.</p><table><tbody>';
|
| - |
|
980 |
items.forEach(function(x){
|
| - |
|
981 |
var canOpen=x.userId>0;
|
| - |
|
982 |
var go=canOpen?' class="clickable" onclick="openExec('+x.userId+",'"+js(x.name)+"','"+date+"')\"":'';
|
| - |
|
983 |
h+='<tr'+go+'><td style="font-weight:600;white-space:nowrap">'+esc(x.name)+'</td>'+
|
| - |
|
984 |
'<td style="color:#69748c">'+esc(x.detail||'')+'</td></tr>';
|
| - |
|
985 |
});
|
| - |
|
986 |
h+='</tbody></table>';
|
| - |
|
987 |
}
|
| - |
|
988 |
el('bj-modal').innerHTML=h; el('bj-modal-back').classList.add('show');
|
| - |
|
989 |
}
|
| - |
|
990 |
// Small red flag badge for the board's executive cell (count of rules tripped today).
|
| - |
|
991 |
function flagBadge(r){
|
| - |
|
992 |
var f=r.flags||[]; if(!f.length) return '';
|
| - |
|
993 |
return ' <span title="'+f.length+' visit-quality flag'+(f.length>1?'s':'')+'" style="display:inline-block;font-size:10px;font-weight:700;color:#e03131;background:#fff0f0;border:1px solid #ffc9c9;border-radius:999px;padding:0 6px;vertical-align:middle">⚑ '+f.length+'</span>';
|
| - |
|
994 |
}
|
| 894 |
function attentionList(a){
|
995 |
function attentionList(a){
|
| 895 |
window._att = a;
|
996 |
window._att = a;
|
| 896 |
var order=['notPunchedIn','idleStale','geofence','locationOff','deferredAwaiting'];
|
997 |
var order=['notPunchedIn','idleStale','geofence','locationOff','deferredAwaiting'];
|
| 897 |
var h='<div class="attrow">';
|
998 |
var h='<div class="attrow">';
|
| 898 |
order.forEach(function(k){
|
999 |
order.forEach(function(k){
|
| Line 1318... |
Line 1419... |
| 1318 |
'<polygon points="'+(r-7)+','+(d-7)+' '+(r+7)+','+(d-7)+' '+r+','+H+'" fill="#e8590c"/>'+
|
1419 |
'<polygon points="'+(r-7)+','+(d-7)+' '+(r+7)+','+(d-7)+' '+r+','+H+'" fill="#e8590c"/>'+
|
| 1319 |
'<circle cx="'+r+'" cy="'+r+'" r="'+(r-2)+'" fill="#e8590c" stroke="#ffffff" stroke-width="2"/>'+
|
1420 |
'<circle cx="'+r+'" cy="'+r+'" r="'+(r-2)+'" fill="#e8590c" stroke="#ffffff" stroke-width="2"/>'+
|
| 1320 |
'<text x="'+r+'" y="'+(r+4)+'" text-anchor="middle" font-size="12" font-weight="bold" fill="#ffffff">'+s+'</text></svg>';
|
1421 |
'<text x="'+r+'" y="'+(r+4)+'" text-anchor="middle" font-size="12" font-weight="bold" fill="#ffffff">'+s+'</text></svg>';
|
| 1321 |
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)};
|
1422 |
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)};
|
| 1322 |
}
|
1423 |
}
|
| - |
|
1424 |
// Numbered visit pin — a rounded badge (with pointer) that widens to fit a multi-
|
| - |
|
1425 |
// char label like "4→1" (planned→actual). Mirrors bjStopPin's scaling approach.
|
| - |
|
1426 |
function bjSeqPin(color, label){
|
| - |
|
1427 |
var s=String(label), w=Math.max(28, 12 + s.length*8), H=40, bh=24, rx=8;
|
| - |
|
1428 |
var svg='<svg xmlns="http://www.w3.org/2000/svg" width="'+w+'" height="'+H+'" viewBox="0 0 '+w+' '+H+'">'+
|
| - |
|
1429 |
'<rect x="0.5" y="0.5" width="'+(w-1)+'" height="'+bh+'" rx="'+rx+'" fill="'+color+'" stroke="#ffffff"/>'+
|
| - |
|
1430 |
'<polygon points="'+(w/2-6)+','+bh+' '+(w/2+6)+','+bh+' '+(w/2)+','+H+'" fill="'+color+'"/>'+
|
| - |
|
1431 |
'<text x="'+(w/2)+'" y="'+(bh/2+5)+'" text-anchor="middle" font-size="13" font-weight="bold" fill="#ffffff">'+s+'</text></svg>';
|
| - |
|
1432 |
return {url:'data:image/svg+xml;charset=UTF-8,'+encodeURIComponent(svg),
|
| - |
|
1433 |
scaledSize:new google.maps.Size(w,H), anchor:new google.maps.Point(w/2,H)};
|
| - |
|
1434 |
}
|
| 1323 |
function fmtDur2(sec){ sec=sec||0; var m=Math.floor(sec/60), s=sec%60; return m+'m'+(s>0?' '+s+'s':''); }
|
1435 |
function fmtDur2(sec){ sec=sec||0; var m=Math.floor(sec/60), s=sec%60; return m+'m'+(s>0?' '+s+'s':''); }
|
| 1324 |
function bjDrawMap(data){
|
1436 |
function bjDrawMap(data){
|
| 1325 |
var box=document.getElementById('bjMap'); if(!box) return;
|
1437 |
var box=document.getElementById('bjMap'); if(!box) return;
|
| 1326 |
box.innerHTML='';
|
1438 |
box.innerHTML='';
|
| 1327 |
var map=new google.maps.Map(box,{zoom:12,center:{lat:20.5937,lng:78.9629},mapTypeControl:true,streetViewControl:false});
|
1439 |
var map=new google.maps.Map(box,{zoom:12,center:{lat:20.5937,lng:78.9629},mapTypeControl:true,streetViewControl:false});
|
| Line 1350... |
Line 1462... |
| 1350 |
var lbl=fmtDur2(s.seconds);
|
1462 |
var lbl=fmtDur2(s.seconds);
|
| 1351 |
pin({lat:s.lat,lng:s.lng}, bjStopPin(lbl), 80, 'Idle '+lbl,
|
1463 |
pin({lat:s.lat,lng:s.lng}, bjStopPin(lbl), 80, 'Idle '+lbl,
|
| 1352 |
'<div style="font-size:12px"><b>⏱ Idle stop</b><br>Stayed ~'+esc(lbl)+' within 50 m</div>');
|
1464 |
'<div style="font-size:12px"><b>⏱ Idle stop</b><br>Stayed ~'+esc(lbl)+' within 50 m</div>');
|
| 1353 |
});
|
1465 |
});
|
| 1354 |
|
1466 |
|
| 1355 |
// 3) every visit — visited stops are numbered in array order; pending / deferred
|
1467 |
// 3) every visit — each pin shows planned→actual. planned # = position in the
|
| - |
|
1468 |
// assigned order the backend returns (id/insert order). actual # = position among
|
| 1356 |
// stops (plotted at their planned store location) get a neutral dot and sit
|
1469 |
// VISITED stops ordered by check-in time (same key the route/timeline use).
|
| - |
|
1470 |
// Pending / deferred stops (no check-in) show just the planned # and sit below.
|
| 1357 |
// below the visited pins.
|
1471 |
var visitsArr = data.visits||[];
|
| 1358 |
var idx=0;
|
1472 |
visitsArr.slice()
|
| - |
|
1473 |
.filter(function(v){ return !!v.checkInTime; })
|
| - |
|
1474 |
.sort(function(a,b){ return tlSecs(a.checkInTime)-tlSecs(b.checkInTime); })
|
| - |
|
1475 |
.forEach(function(v,i){ v._actualNo = i+1; }); // tags the shared object refs
|
| 1359 |
(data.visits||[]).forEach(function(v){
|
1476 |
visitsArr.forEach(function(v,i){
|
| 1360 |
var color=bjMarkColor(v.markType);
|
1477 |
var color=bjMarkColor(v.markType), visited=!!v.checkInTime, planned=i+1;
|
| 1361 |
var visited = !!v.checkInTime;
|
1478 |
var label = visited ? (planned+'→'+v._actualNo) : String(planned);
|
| 1362 |
pin({lat:v.lat,lng:v.lng}, bjPin(color, visited?String(++idx):'•'), visited?100:60,
|
1479 |
pin({lat:v.lat,lng:v.lng}, bjSeqPin(color, label), visited?100:60,
|
| 1363 |
(v.name||'')+' ('+v.markType+')', bjMarkInfo(v,color));
|
1480 |
(v.name||'')+' ('+v.markType+')', bjMarkInfo(v,color));
|
| 1364 |
});
|
1481 |
});
|
| 1365 |
|
1482 |
|
| 1366 |
// 4) punch in / punch out
|
1483 |
// 4) punch in / punch out
|
| 1367 |
if(data.punchIn){
|
1484 |
if(data.punchIn){
|