Subversion Repositories SmartDukaan

Rev

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

Rev 37174 Rev 37248
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> &middot; '+view.length+
570
    bar='<div class="boardfilter">Showing <b>'+esc(m.title)+'</b> &middot; '+view.length+
561
        ' of '+rows.length+'<button class="clearf" onclick="setBoardFilter(null)">&times; Clear filter</button></div>';
571
        ' of '+rows.length+'<button class="clearf" onclick="setBoardFilter(null)">&times; 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?' &#9650;':' &#9660;') : ''; }
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">&#10003;</span>':'<span class="badflag" style="font-size:15px">&#10007;</span>')+'</td>'+
644
       '<td style="text-align:center">'+(r.agendaFilled?'<span style="color:#2f9e44;font-size:15px">&#10003;</span>':'<span class="badflag" style="font-size:15px">&#10007;</span>')+'</td>'+
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:'&#9201;',   title:'Idle &mdash; off-store &gt; 59 min', sub:'stationary &gt;20 m from any store',  cls:'warnflag'},
926
  idleStale:       {bg:'#fff4e6', fg:'#e8590c', ic:'&#9201;',   title:'Idle &mdash; off-store &gt; 59 min', sub:'stationary &gt;20 m from any store',  cls:'warnflag'},
890
  geofence:        {bg:'#fff0f0', fg:'#e03131', ic:'&#128205;', title:'Check-in outside store geofence',  sub:'&gt;50 m from partner\'s saved location', cls:'badflag'},
927
  geofence:        {bg:'#fff0f0', fg:'#e03131', ic:'&#128205;', title:'Check-in outside store geofence',  sub:'&gt;50 m from partner\'s saved location', cls:'badflag'},
891
  locationOff:     {bg:'#fff4e6', fg:'#e8590c', ic:'&#128244;', title:'Location turned off mid-journey',  sub:'GPS toggled off',                     cls:'warnflag'},
928
  locationOff:     {bg:'#fff4e6', fg:'#e8590c', ic:'&#128244;', title:'Location turned off mid-journey',  sub:'GPS toggled off',                     cls:'warnflag'},
892
  deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'&#8635;',   title:'Deferred, awaiting re-action',     sub:'shops deferred today',                cls:''}
929
  deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'&#8635;',   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+'%':'&mdash;')+'</td>'+
-
 
964
       '<td>'+flagStatusChip(r.status)+(hits>0?' <span class="chev">&#9656;</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()">&times;</button><h3>'+esc(r.num)+' &middot; '+esc(r.title)+'</h3>'+
-
 
975
        '<p class="desc">'+items.length+' flagged '+(items.length===1?'visit':'visits')+
-
 
976
        (r.denom?' of '+num(r.denom)+' &middot; '+pct(r.count,r.denom)+'%':'')+' &middot; '+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">&#9873; '+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>&#9201; Idle stop</b><br>Stayed ~'+esc(lbl)+' within 50 m</div>');
1464
      '<div style="font-size:12px"><b>&#9201; 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){