| Line 188... |
Line 188... |
| 188 |
.jstats{display:flex;gap:9px;margin:12px 0 2px;flex-wrap:wrap}
|
188 |
.jstats{display:flex;gap:9px;margin:12px 0 2px;flex-wrap:wrap}
|
| 189 |
.js{flex:1;min-width:104px;background:#f8f9fc;border:1px solid var(--line2);border-radius:10px;padding:9px 10px;text-align:center}
|
189 |
.js{flex:1;min-width:104px;background:#f8f9fc;border:1px solid var(--line2);border-radius:10px;padding:9px 10px;text-align:center}
|
| 190 |
.js .jv{font-size:15.5px;font-weight:760;color:var(--ink);font-variant-numeric:tabular-nums}
|
190 |
.js .jv{font-size:15.5px;font-weight:760;color:var(--ink);font-variant-numeric:tabular-nums}
|
| 191 |
.js .jl{font-size:10px;color:var(--soft);text-transform:uppercase;letter-spacing:.04em;margin-top:3px;font-weight:600}
|
191 |
.js .jl{font-size:10px;color:var(--soft);text-transform:uppercase;letter-spacing:.04em;margin-top:3px;font-weight:600}
|
| 192 |
.btn-ok{background:var(--good);border:0;color:#fff;font-weight:650;padding:6px 12px;border-radius:7px;cursor:pointer;font-size:12px}
|
192 |
.btn-ok{background:var(--good);border:0;color:#fff;font-weight:650;padding:6px 12px;border-radius:7px;cursor:pointer;font-size:12px}
|
| - |
|
193 |
.btn-no{background:var(--bad);border:0;color:#fff;font-weight:650;padding:6px 12px;border-radius:7px;cursor:pointer;font-size:12px}
|
| 193 |
/* journey timeline (exec drilldown) */
|
194 |
/* journey timeline (exec drilldown) */
|
| 194 |
.tl{position:relative;margin:14px 2px 4px;padding:4px 0}
|
195 |
.tl{position:relative;margin:14px 2px 4px;padding:4px 0}
|
| 195 |
.tl::before{content:"";position:absolute;left:50%;top:6px;bottom:6px;width:2px;background:#e9edf5;transform:translateX(-50%)}
|
196 |
.tl::before{content:"";position:absolute;left:50%;top:6px;bottom:6px;width:2px;background:#e9edf5;transform:translateX(-50%)}
|
| 196 |
.tlgap{position:relative;display:flex;justify-content:center;margin:-3px 0}
|
197 |
.tlgap{position:relative;display:flex;justify-content:center;margin:-3px 0}
|
| 197 |
.tlgap .gappill{background:#fff;border:1px solid var(--line);border-radius:999px;padding:3px 11px;font-size:11px;font-weight:600;color:var(--muted);z-index:2;box-shadow:0 1px 2px rgba(20,30,60,.05);font-variant-numeric:tabular-nums}
|
198 |
.tlgap .gappill{background:#fff;border:1px solid var(--line);border-radius:999px;padding:3px 11px;font-size:11px;font-weight:600;color:var(--muted);z-index:2;box-shadow:0 1px 2px rgba(20,30,60,.05);font-variant-numeric:tabular-nums}
|
| Line 275... |
Line 276... |
| 275 |
.bjlegend{display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin-top:12px;padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:#f8f9fc;font-size:12.5px;min-height:20px}
|
276 |
.bjlegend{display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin-top:12px;padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:#f8f9fc;font-size:12.5px;min-height:20px}
|
| 276 |
.bjlegend .lg{font-weight:700}
|
277 |
.bjlegend .lg{font-weight:700}
|
| 277 |
.bjlegend .sw{display:inline-block;width:24px;vertical-align:middle;margin-right:7px}
|
278 |
.bjlegend .sw{display:inline-block;width:24px;vertical-align:middle;margin-right:7px}
|
| 278 |
.bjlegend .ln{border-top:4px solid #e03131}
|
279 |
.bjlegend .ln{border-top:4px solid #e03131}
|
| 279 |
.bjlegend .dt{border-top:4px dotted #2f9e44}
|
280 |
.bjlegend .dt{border-top:4px dotted #2f9e44}
|
| 280 |
.valwrap{display:flex;justify-content:center;margin:14px 0 6px}
|
281 |
.valwrap{display:flex;justify-content:center;align-items:flex-start;gap:16px;flex-wrap:wrap;margin:14px 0 6px}
|
| - |
|
282 |
.valfig{display:flex;flex-direction:column;align-items:center;gap:6px;margin:0;max-width:100%}
|
| - |
|
283 |
.valcap{font-size:11px;font-weight:650;color:var(--soft);text-transform:uppercase;letter-spacing:.03em}
|
| 281 |
.valimg{max-width:100%;max-height:440px;border-radius:12px;border:1px solid var(--line);cursor:zoom-in}
|
284 |
.valimg{max-width:100%;max-height:440px;border-radius:12px;border:1px solid var(--line);cursor:zoom-in}
|
| 282 |
.valph{width:100%;padding:48px;text-align:center;color:var(--soft);border:1px dashed #cfd6e6;border-radius:12px;background:#f4f6fb;font-size:14px}
|
285 |
.valph{width:100%;padding:48px;text-align:center;color:var(--soft);border:1px dashed #cfd6e6;border-radius:12px;background:#f4f6fb;font-size:14px}
|
| 283 |
.valactions{display:flex;justify-content:center;gap:10px;margin-top:8px}
|
286 |
.valactions{display:flex;justify-content:center;gap:10px;margin-top:8px}
|
| 284 |
@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}}
|
287 |
@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}}
|
| 285 |
@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}.tlremark{display:none}}
|
288 |
@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}.tlremark{display:none}}
|
| Line 931... |
Line 934... |
| 931 |
// Step 2: review the check-in photo/document before approving.
|
934 |
// Step 2: review the check-in photo/document before approving.
|
| 932 |
function openValidate(trackingId){
|
935 |
function openValidate(trackingId){
|
| 933 |
var list=(window._att&&window._att.geofence)||[], item=null;
|
936 |
var list=(window._att&&window._att.geofence)||[], item=null;
|
| 934 |
for(var i=0;i<list.length;i++){ if(list[i].trackingId===trackingId){ item=list[i]; break; } }
|
937 |
for(var i=0;i<list.length;i++){ if(list[i].trackingId===trackingId){ item=list[i]; break; } }
|
| 935 |
if(!item){ return; }
|
938 |
if(!item){ return; }
|
| 936 |
var img = (item.attachment && window._mediaUrl)
|
- |
|
| 937 |
? '<img class="valimg" src="'+esc(window._mediaUrl+item.attachment)+'" onclick="bjOpenImage(this.src)" onerror="this.outerHTML=\'<div class="valph">📷 Photo unavailable</div>\'">'
|
- |
|
| 938 |
: '<div class="valph">📷 No photo on this check-in</div>';
|
939 |
var img = bjValImg(item.attachment) || '<div class="valph">📷 No photo on this check-in</div>';
|
| 939 |
el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">×</button><h3>Validate check-in</h3>'+
|
940 |
el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">×</button><h3>Validate check-in</h3>'+
|
| 940 |
'<p class="desc">'+esc(item.name)+' · '+esc(item.detail||'')+'</p>'+
|
941 |
'<p class="desc">'+esc(item.name)+' · '+esc(item.detail||'')+'</p>'+
|
| 941 |
'<div class="valwrap">'+img+'</div>'+
|
942 |
'<div class="valwrap">'+
|
| - |
|
943 |
'<figure class="valfig"><figcaption class="valcap">This check-in</figcaption>'+img+'</figure>'+
|
| - |
|
944 |
// Previous visit photo (same store) — hidden until the lazy fetch fills it in.
|
| - |
|
945 |
'<figure class="valfig" id="bj-prevfig" style="display:none"><figcaption class="valcap" id="bj-prevcap">Previous visit</figcaption><div id="bj-previmg"></div></figure>'+
|
| - |
|
946 |
'</div>'+
|
| 942 |
'<div class="valactions">'+
|
947 |
'<div class="valactions">'+
|
| 943 |
'<button class="exectab" onclick="openAttention(\'geofence\')">← Back</button>'+
|
948 |
'<button class="exectab" onclick="openAttention(\'geofence\')">← Back</button>'+
|
| - |
|
949 |
'<button class="btn-no" onclick="rejectValidated('+trackingId+',this)">Reject</button>'+
|
| 944 |
'<button class="btn-ok" onclick="approveValidated('+trackingId+',this)">Approve</button>'+
|
950 |
'<button class="btn-ok" onclick="approveValidated('+trackingId+',this)">Approve</button>'+
|
| 945 |
'</div>';
|
951 |
'</div>';
|
| - |
|
952 |
bjLoadPrevCheckin(trackingId);
|
| - |
|
953 |
}
|
| - |
|
954 |
// Build the <img> markup for a check-in attachment (empty string when there's none).
|
| - |
|
955 |
function bjValImg(att){
|
| - |
|
956 |
return (att && window._mediaUrl)
|
| - |
|
957 |
? '<img class="valimg" src="'+esc(window._mediaUrl+att)+'" onclick="bjOpenImage(this.src)" onerror="this.outerHTML=\'<div class="valph">📷 Photo unavailable</div>\'">'
|
| - |
|
958 |
: '';
|
| - |
|
959 |
}
|
| - |
|
960 |
// Lazily fetch the most recent OTHER check-in photo at the same store and, if one
|
| - |
|
961 |
// exists, reveal it beside the current photo so the reviewer can compare shopfronts.
|
| - |
|
962 |
function bjLoadPrevCheckin(trackingId){
|
| - |
|
963 |
fetch(CTX+'/beat-journey/prev-checkin?trackingId='+trackingId,{headers:{'Accept':'application/json'}})
|
| - |
|
964 |
.then(function(r){ return r.ok ? r.json() : null; })
|
| - |
|
965 |
.then(function(d){
|
| - |
|
966 |
if(!d || !d.attachment) return;
|
| - |
|
967 |
var fig=el('bj-prevfig'); if(!fig) return;
|
| - |
|
968 |
var im=bjValImg(d.attachment); if(!im) return;
|
| - |
|
969 |
el('bj-previmg').innerHTML=im;
|
| - |
|
970 |
var when=[d.date,d.time].filter(Boolean).join(' ');
|
| - |
|
971 |
el('bj-prevcap').innerHTML='Previous visit'+(when?' · '+esc(when):'');
|
| - |
|
972 |
fig.style.display='';
|
| - |
|
973 |
})
|
| - |
|
974 |
.catch(function(){});
|
| 946 |
}
|
975 |
}
|
| 947 |
function approveValidated(trackingId, btn){
|
976 |
function approveValidated(trackingId, btn){
|
| 948 |
btn.disabled=true; btn.textContent='Approving…';
|
977 |
btn.disabled=true; btn.textContent='Approving…';
|
| 949 |
fetch(CTX+'/beat-journey/approve-checkin',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'trackingId='+trackingId})
|
978 |
fetch(CTX+'/beat-journey/approve-checkin',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'trackingId='+trackingId})
|
| 950 |
.then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
|
979 |
.then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
|
| Line 953... |
Line 982... |
| 953 |
if(window._att && window._att.geofence && window._att.geofence.length) openAttention('geofence');
|
982 |
if(window._att && window._att.geofence && window._att.geofence.length) openAttention('geofence');
|
| 954 |
else closeModal();
|
983 |
else closeModal();
|
| 955 |
})
|
984 |
})
|
| 956 |
.catch(function(){ btn.disabled=false; btn.textContent='Approve'; alert('Approve failed (not authorised, or server error).'); });
|
985 |
.catch(function(){ btn.disabled=false; btn.textContent='Approve'; alert('Approve failed (not authorised, or server error).'); });
|
| 957 |
}
|
986 |
}
|
| - |
|
987 |
// Reject the off-site check-in: coords are left untouched (unlike approve), and the
|
| - |
|
988 |
// executive, their reporting manager and ops are emailed. Optional reason is prompted.
|
| - |
|
989 |
function rejectValidated(trackingId, btn){
|
| - |
|
990 |
var reason = prompt('Reject this off-site check-in?\nThe executive and their manager will be emailed.\nOptional reason:', '');
|
| - |
|
991 |
if(reason===null) return; // cancelled
|
| - |
|
992 |
btn.disabled=true; btn.textContent='Rejecting…';
|
| - |
|
993 |
var body='trackingId='+trackingId+'&reason='+encodeURIComponent(reason||'');
|
| - |
|
994 |
fetch(CTX+'/beat-journey/reject-checkin',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body})
|
| - |
|
995 |
.then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
|
| - |
|
996 |
.then(function(){
|
| - |
|
997 |
if(window._att && window._att.geofence){ window._att.geofence = window._att.geofence.filter(function(x){ return x.trackingId!==trackingId; }); }
|
| - |
|
998 |
if(window._att && window._att.geofence && window._att.geofence.length) openAttention('geofence');
|
| - |
|
999 |
else closeModal();
|
| - |
|
1000 |
})
|
| - |
|
1001 |
.catch(function(){ btn.disabled=false; btn.textContent='Reject'; alert('Reject failed (not authorised, or server error).'); });
|
| - |
|
1002 |
}
|
| 958 |
|
1003 |
|
| 959 |
// Mark an executive on leave for the board date (unscheduled, or scheduled but not
|
1004 |
// Mark an executive on leave for the board date (unscheduled, or scheduled but not
|
| 960 |
// started / agenda unfilled). Manager / super-user only (server-enforced).
|
1005 |
// started / agenda unfilled). Manager / super-user only (server-enforced).
|
| 961 |
// Optimistically flips the row to "On leave" and re-renders the open drill-down.
|
1006 |
// Optimistically flips the row to "On leave" and re-renders the open drill-down.
|
| 962 |
function markLeave(authUserId, name, btn){
|
1007 |
function markLeave(authUserId, name, btn){
|