Subversion Repositories SmartDukaan

Rev

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

Rev 37076 Rev 37174
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=&quot;valph&quot;>&#128247; Photo unavailable</div>\'">'
-
 
938
    : '<div class="valph">&#128247; No photo on this check-in</div>';
939
  var img = bjValImg(item.attachment) || '<div class="valph">&#128247; No photo on this check-in</div>';
939
  el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">&times;</button><h3>Validate check-in</h3>'+
940
  el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">&times;</button><h3>Validate check-in</h3>'+
940
    '<p class="desc">'+esc(item.name)+' &middot; '+esc(item.detail||'')+'</p>'+
941
    '<p class="desc">'+esc(item.name)+' &middot; '+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\')">&larr; Back</button>'+
948
      '<button class="exectab" onclick="openAttention(\'geofence\')">&larr; 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=&quot;valph&quot;>&#128247; 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?' &middot; '+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){