Subversion Repositories SmartDukaan

Rev

Rev 37783 | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 37783 Rev 37784
Line 51... Line 51...
51
  .tab.active{color:var(--navy);border-bottom-color:var(--coral)}
51
  .tab.active{color:var(--navy);border-bottom-color:var(--coral)}
52
 
52
 
53
  /* ===== generic ===== */
53
  /* ===== generic ===== */
54
  .crumb{font-size:12px;color:var(--soft);font-weight:600}
54
  .crumb{font-size:12px;color:var(--soft);font-weight:600}
55
  h1.page{margin:2px 0 2px;font-size:23px;font-weight:760;letter-spacing:-.01em}
55
  h1.page{margin:2px 0 2px;font-size:23px;font-weight:760;letter-spacing:-.01em}
-
 
56
  /* Title block and filters share one line: the title block shrinks (its subtitle wraps) rather than
-
 
57
     pushing the filters onto a second row. Only a narrow viewport is allowed to wrap. */
56
  .pagehead{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:18px}
58
  .pagehead{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:nowrap;margin-bottom:18px}
-
 
59
  .pagehead > div:first-child{min-width:0;flex:1 1 auto}
-
 
60
  .pagehead .hd-right{flex:0 0 auto}
-
 
61
  .pagehead .hd-right select.ctl{max-width:200px;text-overflow:ellipsis}
-
 
62
  @media (max-width:1100px){ .pagehead{flex-wrap:wrap} }
57
  .pagehead .sub{color:var(--muted);font-size:13px;margin-top:2px}
63
  .pagehead .sub{color:var(--muted);font-size:13px;margin-top:2px}
-
 
64
  /* When the filter row wraps under the title it must still hug the right edge, not restart at the left. */
58
  .hd-right{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
65
  .hd-right{display:flex;align-items:center;gap:9px;flex-wrap:wrap;justify-content:flex-end;margin-left:auto}
59
  .ctl{background:var(--card);border:1px solid var(--line);border-radius:9px;padding:8px 12px;font-size:13px;color:var(--ink);font-weight:600}
66
  .ctl{background:var(--card);border:1px solid var(--line);border-radius:9px;padding:8px 12px;font-size:13px;color:var(--ink);font-weight:600}
60
  .ctl .lbl{color:var(--soft);font-weight:500;margin-right:5px}
67
  .ctl .lbl{color:var(--soft);font-weight:500;margin-right:5px}
61
  select.ctl{appearance:none;cursor:pointer;padding-right:26px;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6'><path d='M1 1l4 4 4-4' stroke='%2369748c' fill='none' stroke-width='1.5'/></svg>");background-repeat:no-repeat;background-position:right 10px center}
68
  select.ctl{appearance:none;cursor:pointer;padding-right:26px;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6'><path d='M1 1l4 4 4-4' stroke='%2369748c' fill='none' stroke-width='1.5'/></svg>");background-repeat:no-repeat;background-position:right 10px center}
62
  .btn{background:var(--navy);border:0;color:#fff;font-weight:650;padding:9px 15px;border-radius:9px;cursor:pointer;font-size:13px}
69
  .btn{background:var(--navy);border:0;color:#fff;font-weight:650;padding:9px 15px;border-radius:9px;cursor:pointer;font-size:13px}
63
  .btn.coral{background:var(--coral)}
70
  .btn.coral{background:var(--coral)}
Line 431... Line 438...
431
        <div class="crumb">Sales · Lead Pipeline</div>
438
        <div class="crumb">Sales · Lead Pipeline</div>
432
        <h1 class="page">Command Center</h1>
439
        <h1 class="page">Command Center</h1>
433
        <div class="sub">Every lead you own or oversee — from creation to onboarding. SLA clock runs on all NEW records.</div>
440
        <div class="sub">Every lead you own or oversee — from creation to onboarding. SLA clock runs on all NEW records.</div>
434
      </div>
441
      </div>
435
      <div class="hd-right">
442
      <div class="hd-right">
-
 
443
        <select class="ctl" id="fAgent" title="Call Center user — every number below narrows to leads they created or currently own">
-
 
444
          <option value="">All Call Center users</option>
-
 
445
        </select>
436
        <select class="ctl" id="fRegion">
446
        <select class="ctl" id="fRegion">
437
          <option value="">All regions</option>
447
          <option value="">All regions</option>
438
          #foreach($r in $regions)
448
          #foreach($r in $regions)
439
            <option value="$r.getId()">$!r.getName()#if($r.getRegionCode()) ($r.getRegionCode())#end</option>
449
            <option value="$r.getId()">$!r.getName()#if($r.getRegionCode()) ($r.getRegionCode())#end</option>
440
          #end
450
          #end
Line 456... Line 466...
456
    </div>
466
    </div>
457
 
467
 
458
    <div class="card">
468
    <div class="card">
459
        <h2 style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">Lead pipeline
469
        <h2 style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">Lead pipeline
460
          <span style="display:flex;gap:6px;align-items:center">
470
          <span style="display:flex;gap:6px;align-items:center">
461
            <select id="pipeAgent" class="inp" style="font-size:12.5px;padding:7px 28px 7px 10px;width:190px" onchange="pipeAgentChanged()" title="Call Center user — leads they created or currently own">
-
 
462
              <option value="">Call Center user…</option>
-
 
463
            </select>
-
 
464
            <input id="pipeQ" class="inp" style="width:210px;font-size:12.5px;padding:7px 10px"
471
            <input id="pipeQ" class="inp" style="width:210px;font-size:12.5px;padding:7px 10px"
465
                   placeholder="Name, outlet, mobile, city, created by">
472
                   placeholder="Name, outlet, mobile, city, created by">
466
            <button class="btn ghost sm" onclick="pipeSearch()">Search</button>
473
            <button class="btn ghost sm" onclick="pipeSearch()">Search</button>
467
            <button class="btn ghost sm hide" id="pipeClear" onclick="pipeClearSearch()">Clear</button>
474
            <button class="btn ghost sm hide" id="pipeClear" onclick="pipeClearSearch()">Clear</button>
468
          </span>
475
          </span>
Line 1074... Line 1081...
1074
<div class="toastwrap" id="toasts"></div>
1081
<div class="toastwrap" id="toasts"></div>
1075
 
1082
 
1076
<script>
1083
<script>
1077
  var CTX = "${rc.contextPath}";
1084
  var CTX = "${rc.contextPath}";
1078
  var SEED = { summary:$summaryJson, state:$stateFunnelJson, heat:$slaHeatmapJson, ageing:$ageingJson };
1085
  var SEED = { summary:$summaryJson, state:$stateFunnelJson, heat:$slaHeatmapJson, ageing:$ageingJson };
1079
  var STATE = { regionId:'', from:'$!fromDate', to:'$!toDate' };
1086
  var STATE = { regionId:'', agentId:'', from:'$!fromDate', to:'$!toDate' };
1080
  var DATA = { summary:null, state:null, heat:null, ageing:null };
1087
  var DATA = { summary:null, state:null, heat:null, ageing:null };
1081
 
1088
 
1082
  function byId(x){ return document.getElementById(x); }
1089
  function byId(x){ return document.getElementById(x); }
1083
  function esc(s){ if(s==null) return ''; return (''+s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
1090
  function esc(s){ if(s==null) return ''; return (''+s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
1084
  function fmtInt(n){ if(n==null) return '0'; return (''+n).replace(/\B(?=(\d{3})+(?!\d))/g,','); }
1091
  function fmtInt(n){ if(n==null) return '0'; return (''+n).replace(/\B(?=(\d{3})+(?!\d))/g,','); }
Line 1134... Line 1141...
1134
    t.innerHTML=(ok?'<span class="ok">&#10003;</span>':'')+esc(msg); w.appendChild(t);
1141
    t.innerHTML=(ok?'<span class="ok">&#10003;</span>':'')+esc(msg); w.appendChild(t);
1135
    setTimeout(function(){ t.style.opacity='0'; t.style.transition='opacity .3s'; setTimeout(function(){ t.remove(); },300); },2600); }
1142
    setTimeout(function(){ t.style.opacity='0'; t.style.transition='opacity .3s'; setTimeout(function(){ t.remove(); },300); },2600); }
1136
 
1143
 
1137
  // ---- filters + fetch
1144
  // ---- filters + fetch
1138
  function qs(){ var q=''; if(STATE.regionId)q+='&regionId='+encodeURIComponent(STATE.regionId);
1145
  function qs(){ var q=''; if(STATE.regionId)q+='&regionId='+encodeURIComponent(STATE.regionId);
-
 
1146
    if(STATE.agentId)q+='&agentId='+encodeURIComponent(STATE.agentId);
1139
    if(STATE.from)q+='&from='+encodeURIComponent(STATE.from); if(STATE.to)q+='&to='+encodeURIComponent(STATE.to); return q; }
1147
    if(STATE.from)q+='&from='+encodeURIComponent(STATE.from); if(STATE.to)q+='&to='+encodeURIComponent(STATE.to); return q; }
1140
  function getJSON(path,cb,onErr){ fetch(CTX+path,{credentials:'same-origin',headers:{'X-Requested-With':'XMLHttpRequest'}})
1148
  function getJSON(path,cb,onErr){ fetch(CTX+path,{credentials:'same-origin',headers:{'X-Requested-With':'XMLHttpRequest'}})
1141
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
1149
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
1142
    .then(function(j){ cb((j&&j.response!==undefined)?j.response:j); })
1150
    .then(function(j){ cb((j&&j.response!==undefined)?j.response:j); })
1143
    .catch(function(e){ console.error('LMS fetch failed',path,e);
1151
    .catch(function(e){ console.error('LMS fetch failed',path,e);
Line 1203... Line 1211...
1203
 
1211
 
1204
  // ---- Lead pipeline: filtered, sorted and paged on the server ------------------------------
1212
  // ---- Lead pipeline: filtered, sorted and paged on the server ------------------------------
1205
  // PIPE.q is the search term. While it is set the date filter does not apply — a search hunts for
1213
  // PIPE.q is the search term. While it is set the date filter does not apply — a search hunts for
1206
  // one known retailer, and quietly scoping it to the selected month would report "not found" for a
1214
  // one known retailer, and quietly scoping it to the selected month would report "not found" for a
1207
  // lead that exists. The caption says which mode is in force so that is never a surprise.
1215
  // lead that exists. The caption says which mode is in force so that is never a surprise.
1208
  var PIPE = { q:'', agentId:'', sort:'created', dir:'desc', page:1, pageSize:50, pages:1, total:0 };
1216
  var PIPE = { q:'', sort:'created', dir:'desc', page:1, pageSize:50, pages:1, total:0 };
1209
  // Call Center users for the pipeline filter. "Their" leads = created by them OR owned by them.
1217
  // Call Center users for the page-level agent filter (header). "Their" leads = created by them OR
-
 
1218
  // owned by them; qs() carries it, so cards, funnel, attention queue, pipeline and every drawer
-
 
1219
  // narrow together.
1210
  function loadCallCenterUsers(){
1220
  function loadCallCenterUsers(){
1211
    getJSON('/lms/dashboard/callcenter-users', function(d){
1221
    getJSON('/lms/dashboard/callcenter-users', function(d){
1212
      var sel=byId('pipeAgent'); if(!sel) return; var keep=sel.value;
1222
      var sel=byId('fAgent'); if(!sel) return; var keep=sel.value;
1213
      var users=(d&&d.users)||[];
1223
      var users=(d&&d.users)||[];
1214
      var h='<option value="">'+(users.length?'Call Center user…':'No Call Center users found')+'</option>';
1224
      var h='<option value="">'+(users.length?'All Call Center users':'No Call Center users found')+'</option>';
1215
      users.forEach(function(u){ h+='<option value="'+u.authId+'">'+esc(u.name)+'</option>'; });
1225
      users.forEach(function(u){ h+='<option value="'+u.authId+'">'+esc(u.name)+'</option>'; });
1216
      sel.innerHTML=h; sel.value=keep||'';
1226
      sel.innerHTML=h; sel.value=keep||'';
1217
    }, function(){ var sel=byId('pipeAgent'); if(sel) sel.innerHTML='<option value="">Call Center list unavailable</option>'; });
1227
    }, function(){ var sel=byId('fAgent'); if(sel) sel.innerHTML='<option value="">Call Center list unavailable</option>'; });
1218
  }
1228
  }
-
 
1229
  // Search mode ignores the date window but keeps the scope filters (region + agent).
1219
  function pipeAgentChanged(){ PIPE.agentId=byId('pipeAgent').value||''; PIPE.page=1; loadPipeline(); }
1230
  function scopeQs(){ var q=''; if(STATE.regionId) q+='&regionId='+encodeURIComponent(STATE.regionId);
1220
  function agentQs(){ return PIPE.agentId ? ('&agentId='+encodeURIComponent(PIPE.agentId)) : ''; }
1231
    if(STATE.agentId) q+='&agentId='+encodeURIComponent(STATE.agentId); return q; }
1221
 
1232
 
1222
  function loadPipeline(){
1233
  function loadPipeline(){
1223
    byId('ccPipeline').innerHTML='<tr><td colspan="10"><div class="ph">Loading…</div></td></tr>';
1234
    byId('ccPipeline').innerHTML='<tr><td colspan="10"><div class="ph">Loading…</div></td></tr>';
1224
    var p='/lms/dashboard/pipeline?page='+PIPE.page+'&pageSize='+PIPE.pageSize+
1235
    var p='/lms/dashboard/pipeline?page='+PIPE.page+'&pageSize='+PIPE.pageSize+
1225
          '&sort='+encodeURIComponent(PIPE.sort)+'&dir='+encodeURIComponent(PIPE.dir)+agentQs();
1236
          '&sort='+encodeURIComponent(PIPE.sort)+'&dir='+encodeURIComponent(PIPE.dir);
1226
    if(PIPE.q){ p+='&q='+encodeURIComponent(PIPE.q);
1237
    if(PIPE.q){ p+='&q='+encodeURIComponent(PIPE.q)+scopeQs(); }
1227
                if(STATE.regionId) p+='&regionId='+encodeURIComponent(STATE.regionId); }
-
 
1228
    else { p+=qs(); }
1238
    else { p+=qs(); }
1229
    getJSON(p,function(d){
1239
    getJSON(p,function(d){
1230
      d=d||{}; var rows=d.rows||[];
1240
      d=d||{}; var rows=d.rows||[];
1231
      PIPE.total=d.total||0; PIPE.pages=d.pages||1; PIPE.page=d.page||1;
1241
      PIPE.total=d.total||0; PIPE.pages=d.pages||1; PIPE.page=d.page||1;
1232
      renderPipeHead();
1242
      renderPipeHead();
Line 1538... Line 1548...
1538
      closeDrawer(); LOADED.record=true; byId('recLookup').value=lid; injectRecord(lid);
1548
      closeDrawer(); LOADED.record=true; byId('recLookup').value=lid; injectRecord(lid);
1539
      go('record',document.querySelector('.tab[data-screen="record"]')); return; }
1549
      go('record',document.querySelector('.tab[data-screen="record"]')); return; }
1540
  });
1550
  });
1541
 
1551
 
1542
  // ---- filters
1552
  // ---- filters
1543
  function refresh(){ STATE.regionId=byId('fRegion').value||''; STATE.from=byId('fFrom').value||''; STATE.to=byId('fTo').value||'';
1553
  function refresh(){ STATE.regionId=byId('fRegion').value||''; STATE.agentId=byId('fAgent').value||''; STATE.from=byId('fFrom').value||''; STATE.to=byId('fTo').value||'';
1544
    getJSON('/lms/dashboard/summary?_=1'+qs(),renderSummary); loadPipeline();
1554
    getJSON('/lms/dashboard/summary?_=1'+qs(),renderSummary); loadPipeline();
1545
    getJSON('/lms/dashboard/state-funnel?_=1'+qs(),renderState);
1555
    getJSON('/lms/dashboard/state-funnel?_=1'+qs(),renderState);
1546
    getJSON('/lms/dashboard/sla-heatmap?_=1'+qs(),renderHeat);
1556
    getJSON('/lms/dashboard/sla-heatmap?_=1'+qs(),renderHeat);
1547
    getJSON('/lms/dashboard/ageing',renderAgeing);
1557
    getJSON('/lms/dashboard/ageing',renderAgeing);
1548
    setDashPeriod(); }
1558
    setDashPeriod(); }
1549
  ['fRegion','fFrom','fTo'].forEach(function(id){ byId(id).addEventListener('change',refresh); });
1559
  ['fAgent','fRegion','fFrom','fTo'].forEach(function(id){ byId(id).addEventListener('change',refresh); });
1550
 
1560
 
1551
  // ====================== LEAD RECORD (native sts-mockup layout, real data) ======================
1561
  // ====================== LEAD RECORD (native sts-mockup layout, real data) ======================
1552
  var STEPS=[['NEW','New'],['ASSIGNED','Assigned'],['CONTACTED','Contacted'],['QUALIFIED','Qualified'],
1562
  var STEPS=[['NEW','New'],['ASSIGNED','Assigned'],['CONTACTED','Contacted'],['QUALIFIED','Qualified'],
1553
    ['BEAT_PLANNED','Beat-plan'],['VISITED','Visited'],['ONBOARDED','Onboarded'],['ACTIVE','Active']];
1563
    ['BEAT_PLANNED','Beat-plan'],['VISITED','Visited'],['ONBOARDED','Onboarded'],['ACTIVE','Active']];
1554
  function fmtDT(s){ if(!s) return '—'; var d=new Date(s); if(isNaN(d)) return esc(s);
1564
  function fmtDT(s){ if(!s) return '—'; var d=new Date(s); if(isNaN(d)) return esc(s);
Line 2118... Line 2128...
2118
  // ---- Export (SOP §18.1) -------------------------------------------------------------------
2128
  // ---- Export (SOP §18.1) -------------------------------------------------------------------
2119
  // Exports what is on screen — the current filters, search and sort — rather than everything,
2129
  // Exports what is on screen — the current filters, search and sort — rather than everything,
2120
  // because an export that ignores the filters is a different report than the one being looked at.
2130
  // because an export that ignores the filters is a different report than the one being looked at.
2121
  function exportPipeline(){
2131
  function exportPipeline(){
2122
    var p='/lms/dashboard/pipeline?page=1&pageSize=500&sort='+encodeURIComponent(PIPE.sort)+
2132
    var p='/lms/dashboard/pipeline?page=1&pageSize=500&sort='+encodeURIComponent(PIPE.sort)+
2123
          '&dir='+encodeURIComponent(PIPE.dir)+agentQs();
2133
          '&dir='+encodeURIComponent(PIPE.dir);
2124
    if(PIPE.q){ p+='&q='+encodeURIComponent(PIPE.q); if(STATE.regionId) p+='&regionId='+encodeURIComponent(STATE.regionId); }
2134
    if(PIPE.q){ p+='&q='+encodeURIComponent(PIPE.q)+scopeQs(); }
2125
    else { p+=qs(); }
2135
    else { p+=qs(); }
2126
    toast('Preparing export…');
2136
    toast('Preparing export…');
2127
    getJSON(p,function(d){
2137
    getJSON(p,function(d){
2128
      var rows=(d&&d.rows)?d.rows:[];
2138
      var rows=(d&&d.rows)?d.rows:[];
2129
      if(!rows.length){ toast('Nothing to export'); return; }
2139
      if(!rows.length){ toast('Nothing to export'); return; }