| Line 674... |
Line 674... |
| 674 |
function pScoreTable(rows){
|
674 |
function pScoreTable(rows){
|
| 675 |
if(!rows.length) return '<p class="desc">No executives in scope.</p>';
|
675 |
if(!rows.length) return '<p class="desc">No executives in scope.</p>';
|
| 676 |
var h='<table><thead><tr><th>Executive</th><th class="num">Plan</th><th class="num">Done</th><th class="num">Adher.</th>'+
|
676 |
var h='<table><thead><tr><th>Executive</th><th class="num">Plan</th><th class="num">Done</th><th class="num">Adher.</th>'+
|
| 677 |
'<th class="num" title="Avg lead visits per active (punched-in) day">Lead/day</th>'+
|
677 |
'<th class="num" title="Avg lead visits per active (punched-in) day">Lead/day</th>'+
|
| 678 |
'<th class="num" title="Avg franchisee visits per active (punched-in) day">Fr/day</th>'+
|
678 |
'<th class="num" title="Avg franchisee visits per active (punched-in) day">Fr/day</th>'+
|
| 679 |
'<th class="num">Disc.</th><th class="num">Hrs</th><th class="num">Leads</th><th class="num">Geo⚠</th></tr></thead><tbody>';
|
679 |
'<th class="num">Disc.</th><th class="num">Hrs</th><th class="num">Leads</th></tr></thead><tbody>';
|
| 680 |
rows.forEach(function(r){
|
680 |
rows.forEach(function(r){
|
| 681 |
var c=adhColor(r.adherencePct);
|
681 |
var c=adhColor(r.adherencePct);
|
| 682 |
h+='<tr class="clickable" onclick="openExecPeriod('+r.authUserId+',\''+js(r.name)+'\')">'+
|
682 |
h+='<tr class="clickable" onclick="openExecPeriod('+r.authUserId+',\''+js(r.name)+'\')">'+
|
| 683 |
'<td><span class="lvl">'+esc(r.level)+'</span> <b>'+esc(r.name)+'</b></td>'+
|
683 |
'<td><span class="lvl">'+esc(r.level)+'</span> <b>'+esc(r.name)+'</b></td>'+
|
| 684 |
'<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
|
684 |
'<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
|
| 685 |
'<td class="num"><b style="color:'+c+'">'+r.adherencePct+'%</b></td>'+
|
685 |
'<td class="num"><b style="color:'+c+'">'+r.adherencePct+'%</b></td>'+
|
| 686 |
'<td class="num">'+(r.avgLeadVisitsPerDay||'-')+'</td>'+
|
686 |
'<td class="num">'+(r.avgLeadVisitsPerDay||'-')+'</td>'+
|
| 687 |
'<td class="num">'+(r.avgFranchiseeVisitsPerDay||'-')+'</td>'+
|
687 |
'<td class="num">'+(r.avgFranchiseeVisitsPerDay||'-')+'</td>'+
|
| 688 |
'<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'-')+(r.discussionMin?'m':'')+'</td>'+
|
688 |
'<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'-')+(r.discussionMin?'m':'')+'</td>'+
|
| 689 |
'<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td>'+
|
689 |
'<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td></tr>';
|
| 690 |
'<td class="num'+(r.geoFlags>0?' badflag':'')+'">'+r.geoFlags+'</td></tr>';
|
- |
|
| 691 |
});
|
690 |
});
|
| 692 |
return h+'</tbody></table>';
|
691 |
return h+'</tbody></table>';
|
| 693 |
}
|
692 |
}
|
| 694 |
// Funnel stage → the executives behind it (the scorecard, focused).
|
693 |
// Funnel stage → the executives behind it (the scorecard, focused).
|
| 695 |
function openPFunnel(key){
|
694 |
function openPFunnel(key){
|
| Line 947... |
Line 946... |
| 947 |
var h='<table id="scTbl"><thead><tr>'+
|
946 |
var h='<table id="scTbl"><thead><tr>'+
|
| 948 |
'<th onclick="sortCard(\'name\')">Executive</th>'+
|
947 |
'<th onclick="sortCard(\'name\')">Executive</th>'+
|
| 949 |
'<th class="num" onclick="sortCard(\'planned\')">Plan</th>'+
|
948 |
'<th class="num" onclick="sortCard(\'planned\')">Plan</th>'+
|
| 950 |
'<th class="num" onclick="sortCard(\'done\')">Done</th>'+
|
949 |
'<th class="num" onclick="sortCard(\'done\')">Done</th>'+
|
| 951 |
'<th class="num" onclick="sortCard(\'adherencePct\')">Adher.</th>'+
|
950 |
'<th class="num" onclick="sortCard(\'adherencePct\')">Adher.</th>'+
|
| - |
|
951 |
'<th class="num" onclick="sortCard(\'avgLeadVisitsPerDay\')" title="Avg lead visits per active (punched-in) day">Lead/day</th>'+
|
| - |
|
952 |
'<th class="num" onclick="sortCard(\'avgFranchiseeVisitsPerDay\')" title="Avg franchisee visits per active (punched-in) day">Fr/day</th>'+
|
| 952 |
'<th class="num" onclick="sortCard(\'discussionMin\')">Disc.</th>'+
|
953 |
'<th class="num" onclick="sortCard(\'discussionMin\')">Disc.</th>'+
|
| 953 |
'<th class="num" onclick="sortCard(\'workingHrs\')">Hrs</th>'+
|
954 |
'<th class="num" onclick="sortCard(\'workingHrs\')">Hrs</th>'+
|
| 954 |
'<th class="num" onclick="sortCard(\'leads\')">Leads</th>'+
|
- |
|
| 955 |
'<th class="num" onclick="sortCard(\'geoFlags\')">Geo⚠</th></tr></thead><tbody>';
|
955 |
'<th class="num" onclick="sortCard(\'leads\')">Leads</th></tr></thead><tbody>';
|
| 956 |
rows.forEach(function(r){
|
956 |
rows.forEach(function(r){
|
| 957 |
var c=adhColor(r.adherencePct);
|
957 |
var c=adhColor(r.adherencePct);
|
| 958 |
h+='<tr class="clickable" onclick="openExecPeriod('+r.authUserId+',\''+js(r.name)+'\')"><td>'+esc(r.name)+' <span class="lvl">'+esc(r.level)+'</span></td>'+
|
958 |
h+='<tr class="clickable" onclick="openExecPeriod('+r.authUserId+',\''+js(r.name)+'\')"><td><span class="lvl">'+esc(r.level)+'</span>'+esc(r.name)+'</td>'+
|
| 959 |
'<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
|
959 |
'<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
|
| 960 |
'<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>'+
|
960 |
'<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>'+
|
| - |
|
961 |
'<td class="num">'+(r.avgLeadVisitsPerDay||'-')+'</td>'+
|
| - |
|
962 |
'<td class="num">'+(r.avgFranchiseeVisitsPerDay||'-')+'</td>'+
|
| 961 |
'<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'-')+(r.discussionMin?'m':'')+'</td>'+
|
963 |
'<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'-')+(r.discussionMin?'m':'')+'</td>'+
|
| 962 |
'<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td>'+
|
964 |
'<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td></tr>';
|
| 963 |
'<td class="num'+(r.geoFlags>0?' badflag':'')+'">'+r.geoFlags+'</td></tr>';
|
- |
|
| 964 |
});
|
965 |
});
|
| 965 |
return h+'</tbody></table>';
|
966 |
return h+'</tbody></table>';
|
| 966 |
}
|
967 |
}
|
| 967 |
function coverageBars(rows){
|
968 |
function coverageBars(rows){
|
| 968 |
if(!rows||!rows.length) return '<p class="desc">No coverage data.</p>';
|
969 |
if(!rows||!rows.length) return '<p class="desc">No coverage data.</p>';
|
| Line 1232... |
Line 1233... |
| 1232 |
'Punch out'+(data.punchOut.time?' · '+data.punchOut.time:''),
|
1233 |
'Punch out'+(data.punchOut.time?' · '+data.punchOut.time:''),
|
| 1233 |
'<div style="font-size:12px"><b>⚑ Punch out</b>'+(data.punchOut.time?'<br>'+esc(data.punchOut.time):'')+'</div>');
|
1234 |
'<div style="font-size:12px"><b>⚑ Punch out</b>'+(data.punchOut.time?'<br>'+esc(data.punchOut.time):'')+'</div>');
|
| 1234 |
}
|
1235 |
}
|
| 1235 |
|
1236 |
|
| 1236 |
// 5) routes — actual road taken vs Google-suggested optimal, for comparison.
|
1237 |
// 5) routes — actual road taken vs Google-suggested optimal, for comparison.
|
| - |
|
1238 |
// Suggested-route checkpoints = the whole day's stops (planned + visited), not
|
| - |
|
1239 |
// just the ones already visited, so the suggestion shows from punch-in onward
|
| - |
|
1240 |
// (and even before the first check-in). optimizeWaypoints reorders them, so the
|
| 1237 |
// Checkpoints in journey order: punch in → visits (by check-in time) → punch out.
|
1241 |
// input order only needs to keep punch-in first / punch-out last; visited stops
|
| - |
|
1242 |
// are listed by check-in time and still-pending stops after them.
|
| 1238 |
var cps=[];
|
1243 |
var cps=[];
|
| 1239 |
if(data.punchIn) cps.push({lat:data.punchIn.lat,lng:data.punchIn.lng});
|
1244 |
if(data.punchIn) cps.push({lat:data.punchIn.lat,lng:data.punchIn.lng});
|
| 1240 |
(data.visits||[]).slice()
|
1245 |
(data.visits||[]).slice()
|
| 1241 |
.filter(function(v){ return v.lat && v.lng && v.checkInTime; }) // visited stops only
|
1246 |
.filter(function(v){ return v.lat && v.lng; }) // every stop with coords
|
| 1242 |
.sort(function(a,b){ return tlSecs(a.checkInTime)-tlSecs(b.checkInTime); })
|
1247 |
.sort(function(a,b){ return tlSecs(a.checkInTime)-tlSecs(b.checkInTime); })
|
| 1243 |
.forEach(function(v){ cps.push({lat:v.lat,lng:v.lng}); });
|
1248 |
.forEach(function(v){ cps.push({lat:v.lat,lng:v.lng}); });
|
| 1244 |
if(data.punchOut) cps.push({lat:data.punchOut.lat,lng:data.punchOut.lng});
|
1249 |
if(data.punchOut) cps.push({lat:data.punchOut.lat,lng:data.punchOut.lng});
|
| 1245 |
// Actual time window of the GPS trail (first → last breadcrumb timestamp),
|
1250 |
// Actual time window of the GPS trail (first → last breadcrumb timestamp),
|
| 1246 |
// shown on the taken-route click detail alongside the driving estimate.
|
1251 |
// shown on the taken-route click detail alongside the driving estimate.
|