| Line 98... |
Line 98... |
| 98 |
.ps-no{background:#fff0f0;color:#e03131}
|
98 |
.ps-no{background:#fff0f0;color:#e03131}
|
| 99 |
.meter{display:inline-block;width:54px;height:7px;border-radius:4px;background:#eef1f6;vertical-align:middle;margin-left:7px;overflow:hidden}
|
99 |
.meter{display:inline-block;width:54px;height:7px;border-radius:4px;background:#eef1f6;vertical-align:middle;margin-left:7px;overflow:hidden}
|
| 100 |
.meter > i{display:block;height:100%;border-radius:4px}
|
100 |
.meter > i{display:block;height:100%;border-radius:4px}
|
| 101 |
.warnflag{color:var(--orange);font-weight:700}
|
101 |
.warnflag{color:var(--orange);font-weight:700}
|
| 102 |
.badflag{color:var(--red);font-weight:700}
|
102 |
.badflag{color:var(--red);font-weight:700}
|
| - |
|
103 |
.leavebtn{border:1px solid #ffd8a8;background:#fff4e6;color:#e8590c;font-weight:650;font-size:11px;padding:2px 9px;border-radius:6px;cursor:pointer;white-space:nowrap}
|
| - |
|
104 |
.leavebtn:hover{background:#ffe8cc}
|
| - |
|
105 |
.leavebtn:disabled{opacity:.6;cursor:default}
|
| 103 |
.rowwarn{background:#fff9f0}
|
106 |
.rowwarn{background:#fff9f0}
|
| 104 |
.rowbad{background:#fff5f5}
|
107 |
.rowbad{background:#fff5f5}
|
| 105 |
.bstat{display:inline-block;font-size:10.5px;font-weight:700;padding:3px 9px;border-radius:999px;white-space:nowrap}
|
108 |
.bstat{display:inline-block;font-size:10.5px;font-weight:700;padding:3px 9px;border-radius:999px;white-space:nowrap}
|
| 106 |
.bstat.ok{background:#ebfbee;color:#2f9e44}
|
109 |
.bstat.ok{background:#ebfbee;color:#2f9e44}
|
| 107 |
.bstat.warn{background:#fff4e6;color:#e8590c}
|
110 |
.bstat.warn{background:#fff4e6;color:#e8590c}
|
| Line 486... |
Line 489... |
| 486 |
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>'; }
|
489 |
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>'; }
|
| 487 |
|
490 |
|
| 488 |
function renderToday(d){
|
491 |
function renderToday(d){
|
| 489 |
var k=d.kpis, f=d.funnel, s=d.stops, a=d.attention;
|
492 |
var k=d.kpis, f=d.funnel, s=d.stops, a=d.attention;
|
| 490 |
window._canApprove = !!d.canApprove;
|
493 |
window._canApprove = !!d.canApprove;
|
| - |
|
494 |
window._canMarkLeave = !!d.canMarkLeave;
|
| 491 |
window._mediaUrl = d.mediaUrl || window._mediaUrl || '';
|
495 |
window._mediaUrl = d.mediaUrl || window._mediaUrl || '';
|
| 492 |
window._board = d.board || []; // funnel drill-down filters this
|
496 |
window._board = d.board || []; // funnel drill-down filters this
|
| 493 |
window._boardDate = d.date || '';
|
497 |
window._boardDate = d.date || '';
|
| 494 |
window._leadsList = d.leadsList || []; // leads drill-down
|
498 |
window._leadsList = d.leadsList || []; // leads drill-down
|
| 495 |
window._execList = d.execList || []; // main-card drill-downs
|
499 |
window._execList = d.execList || []; // main-card drill-downs
|
| Line 667... |
Line 671... |
| 667 |
var CARD_META = {
|
671 |
var CARD_META = {
|
| 668 |
total: {title:'Total Executives', desc:'Everyone in your team', f:function(r){ return true; }},
|
672 |
total: {title:'Total Executives', desc:'Everyone in your team', f:function(r){ return true; }},
|
| 669 |
scheduled: {title:'Scheduled PJP', desc:'Executives with a beat today', f:function(r){ return r.hasBeat; }},
|
673 |
scheduled: {title:'Scheduled PJP', desc:'Executives with a beat today', f:function(r){ return r.hasBeat; }},
|
| 670 |
agenda: {title:'Agenda Filled', desc:'Beats with the agenda filled', f:function(r){ return r.agendaFilled; },
|
674 |
agenda: {title:'Agenda Filled', desc:'Beats with the agenda filled', f:function(r){ return r.agendaFilled; },
|
| 671 |
dropLabel:'Agenda not filled (scheduled)', drop:function(r){ return r.hasBeat && !r.agendaFilled; }},
|
675 |
dropLabel:'Agenda not filled (scheduled)', drop:function(r){ return r.hasBeat && !r.agendaFilled; }},
|
| 672 |
unscheduled: {title:'Unscheduled', desc:'No PJP scheduled today', f:function(r){ return !r.hasBeat; }}
|
676 |
unscheduled: {title:'Unscheduled', desc:'No PJP scheduled today', f:function(r){ return !r.hasBeat && !r.onLeave; }}
|
| 673 |
};
|
677 |
};
|
| 674 |
// Renders an executive roster table (used by the card drill-downs).
|
678 |
// Renders an executive roster table (used by the card drill-downs).
|
| 675 |
function execRows(list, date){
|
679 |
function execRows(list, date){
|
| 676 |
if(!list.length) return '<p class="desc">None.</p>';
|
680 |
if(!list.length) return '<p class="desc">None.</p>';
|
| 677 |
var h='<table><thead><tr><th>Executive</th><th>Beat</th><th>Agenda</th><th>Status</th></tr></thead><tbody>';
|
681 |
var h='<table><thead><tr><th>Executive</th><th>Beat</th><th>Agenda</th><th>Status</th></tr></thead><tbody>';
|
| 678 |
list.forEach(function(r){
|
682 |
list.forEach(function(r){
|
| 679 |
var A=r.userId+",'"+js(r.name)+"','"+date+"'";
|
683 |
var A=r.userId+",'"+js(r.name)+"','"+date+"'";
|
| 680 |
var go = r.userId ? ' class="clickable" onclick="openExec('+A+')"' : '';
|
684 |
var go = r.userId ? ' class="clickable" onclick="openExec('+A+')"' : '';
|
| 681 |
var agenda = !r.hasBeat ? '<span style="color:#97a1b5">—</span>'
|
685 |
var agenda = !r.hasBeat ? '<span style="color:#97a1b5">—</span>'
|
| 682 |
: (r.agendaFilled ? '<span style="color:#2f9e44">✓</span>' : '<span class="badflag">✗</span>');
|
686 |
: (r.agendaFilled ? '<span style="color:#2f9e44">✓</span>' : '<span class="badflag">✗</span>');
|
| - |
|
687 |
// Unscheduled execs can be marked on leave (manager / super-user only; server-enforced).
|
| - |
|
688 |
var leaveBtn = (window._canMarkLeave && r.authUserId && !r.hasBeat && !r.onLeave)
|
| - |
|
689 |
? ' <button class="leavebtn" onclick="event.stopPropagation();markLeave('+r.authUserId+",'"+js(r.name)+"',this)\">Mark leave</button>" : '';
|
| 683 |
h+='<tr'+go+'>'+
|
690 |
h+='<tr'+go+'>'+
|
| 684 |
'<td><b>'+esc(r.name)+'</b></td>'+
|
691 |
'<td><b>'+esc(r.name)+'</b></td>'+
|
| 685 |
'<td>'+esc(r.beatName||'-')+'</td>'+
|
692 |
'<td>'+esc(r.beatName||'-')+'</td>'+
|
| 686 |
'<td style="text-align:center">'+agenda+'</td>'+
|
693 |
'<td style="text-align:center">'+agenda+'</td>'+
|
| 687 |
'<td>'+esc(r.statusLabel||'')+'</td></tr>';
|
694 |
'<td>'+esc(r.statusLabel||'')+leaveBtn+'</td></tr>';
|
| 688 |
});
|
695 |
});
|
| 689 |
return h+'</tbody></table>';
|
696 |
return h+'</tbody></table>';
|
| 690 |
}
|
697 |
}
|
| 691 |
function openCards(key){
|
698 |
function openCards(key){
|
| 692 |
var m=CARD_META[key]; if(!m) return;
|
699 |
var m=CARD_META[key]; if(!m) return;
|
| Line 944... |
Line 951... |
| 944 |
else closeModal();
|
951 |
else closeModal();
|
| 945 |
})
|
952 |
})
|
| 946 |
.catch(function(){ btn.disabled=false; btn.textContent='Approve'; alert('Approve failed (not authorised, or server error).'); });
|
953 |
.catch(function(){ btn.disabled=false; btn.textContent='Approve'; alert('Approve failed (not authorised, or server error).'); });
|
| 947 |
}
|
954 |
}
|
| 948 |
|
955 |
|
| - |
|
956 |
// Mark an unscheduled executive on leave for the board date. Manager / super-user only
|
| - |
|
957 |
// (server-enforced). Optimistically drops them from the Unscheduled list on success.
|
| - |
|
958 |
function markLeave(authUserId, name, btn){
|
| - |
|
959 |
if(!authUserId) return;
|
| - |
|
960 |
var reason = prompt('Mark '+name+' on leave for '+(window._boardDate||'today')+'?\nOptional reason:', '');
|
| - |
|
961 |
if(reason===null) return; // cancelled
|
| - |
|
962 |
btn.disabled=true; btn.textContent='Marking…';
|
| - |
|
963 |
var body='authUserId='+authUserId+'&date='+encodeURIComponent(window._boardDate||'')+'&reason='+encodeURIComponent(reason||'');
|
| - |
|
964 |
fetch(CTX+'/beat-journey/mark-leave',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body})
|
| - |
|
965 |
.then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
|
| - |
|
966 |
.then(function(){
|
| - |
|
967 |
(window._execList||[]).forEach(function(x){ if(x.authUserId===authUserId){ x.onLeave=true; x.statusLabel='On leave'; } });
|
| - |
|
968 |
openCards('unscheduled'); // re-render (row now filtered out of Unscheduled)
|
| - |
|
969 |
loadToday(); // refresh KPI tiles / counts in the background
|
| - |
|
970 |
})
|
| - |
|
971 |
.catch(function(){ btn.disabled=false; btn.textContent='Mark leave'; alert('Could not mark leave (not authorised, or server error).'); });
|
| - |
|
972 |
}
|
| - |
|
973 |
|
| 949 |
/* ============================ PERIOD ============================ */
|
974 |
/* ============================ PERIOD ============================ */
|
| 950 |
function loadPeriod(){
|
975 |
function loadPeriod(){
|
| 951 |
spinner();
|
976 |
spinner();
|
| 952 |
var u = BASE + '/data?startDate=' + encodeURIComponent(el('bj-start').value) + '&endDate=' + encodeURIComponent(el('bj-end').value) + qs();
|
977 |
var u = BASE + '/data?startDate=' + encodeURIComponent(el('bj-start').value) + '&endDate=' + encodeURIComponent(el('bj-end').value) + qs();
|
| 953 |
fetch(u, {headers:{'Accept':'application/json'}})
|
978 |
fetch(u, {headers:{'Accept':'application/json'}})
|