Subversion Repositories SmartDukaan

Rev

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

Rev 37749 Rev 37760
Line 403... Line 403...
403
</div>
403
</div>
404
 
404
 
405
<!-- ============ TABS ============ -->
405
<!-- ============ TABS ============ -->
406
<div class="tabbar">
406
<div class="tabbar">
407
  <div class="inner">
407
  <div class="inner">
408
    <button class="tab active" onclick="go('cmd',this)">Command Center</button>
408
    <button class="tab active" data-screen="cmd" onclick="go('cmd',this)">Command Center</button>
409
    <button class="tab" onclick="go('record',this)">Lead Record</button>
409
    <button class="tab" data-screen="record" onclick="go('record',this)">Lead Record</button>
-
 
410
    #if($canCreateLead)
410
    <button class="tab" onclick="go('create',this)">Create Lead</button>
411
    <button class="tab" data-screen="create" onclick="go('create',this)">Create Lead</button>
-
 
412
    #end
411
    <button class="tab" onclick="go('assign',this)">Auto-Assignment</button>
413
    <button class="tab" data-screen="assign" onclick="go('assign',this)">Auto-Assignment</button>
412
    <button class="tab" onclick="go('live',this)">Live Calls</button>
414
    <button class="tab" data-screen="live" onclick="go('live',this)">Live Calls</button>
413
    <button class="tab" onclick="go('dash',this)">Dashboards</button>
415
    <button class="tab" data-screen="dash" onclick="go('dash',this)">Dashboards</button>
414
    <button class="tab" onclick="go('chk',this)">Checklist</button>
416
    <button class="tab" data-screen="chk" onclick="go('chk',this)">Checklist</button>
415
    <button class="tab" onclick="go('audit',this)">Recordings &amp; Audit</button>
417
    <button class="tab" data-screen="audit" onclick="go('audit',this)">Recordings &amp; Audit</button>
416
  </div>
418
  </div>
417
</div>
419
</div>
418
 
420
 
419
<div class="wrap">
421
<div class="wrap">
420
 
422
 
Line 450... Line 452...
450
    </div>
452
    </div>
451
 
453
 
452
    <div class="card">
454
    <div class="card">
453
        <h2 style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">Lead pipeline
455
        <h2 style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">Lead pipeline
454
          <span style="display:flex;gap:6px;align-items:center">
456
          <span style="display:flex;gap:6px;align-items:center">
-
 
457
            <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">
-
 
458
              <option value="">Call Center user…</option>
-
 
459
            </select>
455
            <input id="pipeQ" class="inp" style="width:230px;font-size:12.5px;padding:7px 10px"
460
            <input id="pipeQ" class="inp" style="width:210px;font-size:12.5px;padding:7px 10px"
456
                   placeholder="Name, outlet, mobile, city, created by">
461
                   placeholder="Name, outlet, mobile, city, created by">
457
            <button class="btn ghost sm" onclick="pipeSearch()">Search</button>
462
            <button class="btn ghost sm" onclick="pipeSearch()">Search</button>
458
            <button class="btn ghost sm hide" id="pipeClear" onclick="pipeClearSearch()">Clear</button>
463
            <button class="btn ghost sm hide" id="pipeClear" onclick="pipeClearSearch()">Clear</button>
459
          </span>
464
          </span>
460
        </h2>
465
        </h2>
Line 567... Line 572...
567
                <option value="$sn">$sn</option>
572
                <option value="$sn">$sn</option>
568
              #end
573
              #end
569
            </select></div>
574
            </select></div>
570
          <div class="fld"><label>Conversion probability</label>
575
          <div class="fld"><label>Conversion probability</label>
571
            <select class="inp" id="cConversion"><option value="false" selected>Low</option><option value="true">High</option></select></div>
576
            <select class="inp" id="cConversion"><option value="false" selected>Low</option><option value="true">High</option></select></div>
572
          <div class="fld"><label>Counter size</label><input class="inp mono" id="cCounter" placeholder="Number of counters" inputmode="numeric"></div>
577
          <div class="fld"><label>Counter size <span class="auto" id="cCounterHint">= brand total, editable</span></label>
-
 
578
            <input class="inp mono" id="cCounter" placeholder="₹ per month" inputmode="numeric" oninput="counterEdited()"></div>
573
          <div class="fld full"><label>Remark</label><input class="inp" id="cRemark" placeholder="Anything the BM should know before the first call" maxlength="500"></div>
579
          <div class="fld full"><label>Remark</label><input class="inp" id="cRemark" placeholder="Anything the BM should know before the first call" maxlength="500"></div>
574
          <div class="fld full">
580
          <div class="fld full">
575
            <label>Store photos <span class="auto">optional — same five shots as the Leads screen</span></label>
581
            <label>Store photos <span class="auto">optional — same five shots as the Leads screen</span></label>
576
            <div class="brandgrid" id="cPhotoGrid">
582
            <div class="brandgrid" id="cPhotoGrid">
577
              <div class="brandcell"><span class="bname">Front</span><input type="file" accept="image/*" class="inp cphoto" data-photo="frontp"></div>
583
              <div class="brandcell"><span class="bname">Front</span><input type="file" accept="image/*" class="inp cphoto" data-photo="frontp"></div>
Line 639... Line 645...
639
    <div class="grid g3">
645
    <div class="grid g3">
640
      <div class="card">
646
      <div class="card">
641
        <h2>Region &rarr; BM/RSM mapping</h2>
647
        <h2>Region &rarr; BM/RSM mapping</h2>
642
        <p class="desc">The live routing table — resolved for each region right now.</p>
648
        <p class="desc">The live routing table — resolved for each region right now.</p>
643
        <table>
649
        <table>
644
          <thead><tr><th>Region / State</th><th>Assigned BM / RSM</th><th>Status</th></tr></thead>
650
          <thead><tr><th>Region / State</th><th>Assigned BM / RSM</th><th>Status</th><th></th></tr></thead>
645
          <tbody id="assignMap"><tr><td colspan="3"><div class="ph">Resolving…</div></td></tr></tbody>
651
          <tbody id="assignMap"><tr><td colspan="4"><div class="ph">Resolving…</div></td></tr></tbody>
646
        </table>
652
        </table>
647
      </div>
653
      </div>
648
      <div>
654
      <div>
649
        <div class="card">
655
        <div class="card">
650
          <h2>Engine rules</h2>
656
          <h2>Engine rules</h2>
Line 1046... Line 1052...
1046
    </div>
1052
    </div>
1047
    <div class="mf"><button class="btn ghost" onclick="closeAssign()">Cancel</button></div>
1053
    <div class="mf"><button class="btn ghost" onclick="closeAssign()">Cancel</button></div>
1048
  </div>
1054
  </div>
1049
</div>
1055
</div>
1050
 
1056
 
-
 
1057
<div class="modal hide" id="rgnModal">
-
 
1058
  <div class="box">
-
 
1059
    <div class="mh"><h2>Region owner</h2>
-
 
1060
      <p class="desc" id="rgnWho">&mdash;</p></div>
-
 
1061
    <div class="mb">
-
 
1062
      <p class="desc" style="margin-bottom:8px">Pick the BM/RSM who should own this region. This writes the region's Sales L4 position, which the engine reads first; leads already assigned keep their owner.</p>
-
 
1063
      <input id="rgnSearch" class="inp" style="width:100%" placeholder="Filter by name, tier or region…" oninput="renderRegionOwnerList()">
-
 
1064
      <div id="rgnList" style="max-height:340px;overflow-y:auto;margin-top:10px"></div>
-
 
1065
    </div>
-
 
1066
    <div class="mf"><button class="btn ghost" onclick="closeRegionOwner()">Cancel</button></div>
-
 
1067
  </div>
-
 
1068
</div>
-
 
1069
 
1051
<div class="toastwrap" id="toasts"></div>
1070
<div class="toastwrap" id="toasts"></div>
1052
 
1071
 
1053
<script>
1072
<script>
1054
  var CTX = "${rc.contextPath}";
1073
  var CTX = "${rc.contextPath}";
1055
  var SEED = { summary:$summaryJson, state:$stateFunnelJson, heat:$slaHeatmapJson, ageing:$ageingJson };
1074
  var SEED = { summary:$summaryJson, state:$stateFunnelJson, heat:$slaHeatmapJson, ageing:$ageingJson };
Line 1172... Line 1191...
1172
 
1191
 
1173
  // ---- Lead pipeline: filtered, sorted and paged on the server ------------------------------
1192
  // ---- Lead pipeline: filtered, sorted and paged on the server ------------------------------
1174
  // PIPE.q is the search term. While it is set the date filter does not apply — a search hunts for
1193
  // PIPE.q is the search term. While it is set the date filter does not apply — a search hunts for
1175
  // one known retailer, and quietly scoping it to the selected month would report "not found" for a
1194
  // one known retailer, and quietly scoping it to the selected month would report "not found" for a
1176
  // lead that exists. The caption says which mode is in force so that is never a surprise.
1195
  // lead that exists. The caption says which mode is in force so that is never a surprise.
1177
  var PIPE = { q:'', sort:'created', dir:'desc', page:1, pageSize:50, pages:1, total:0 };
1196
  var PIPE = { q:'', agentId:'', sort:'created', dir:'desc', page:1, pageSize:50, pages:1, total:0 };
-
 
1197
  // Call Center users for the pipeline filter. "Their" leads = created by them OR owned by them.
-
 
1198
  function loadCallCenterUsers(){
-
 
1199
    getJSON('/lms/dashboard/callcenter-users', function(d){
-
 
1200
      var sel=byId('pipeAgent'); if(!sel) return; var keep=sel.value;
-
 
1201
      var users=(d&&d.users)||[];
-
 
1202
      var h='<option value="">'+(users.length?'Call Center user…':'No Call Center users found')+'</option>';
-
 
1203
      users.forEach(function(u){ h+='<option value="'+u.authId+'">'+esc(u.name)+'</option>'; });
-
 
1204
      sel.innerHTML=h; sel.value=keep||'';
-
 
1205
    }, function(){ var sel=byId('pipeAgent'); if(sel) sel.innerHTML='<option value="">Call Center list unavailable</option>'; });
-
 
1206
  }
-
 
1207
  function pipeAgentChanged(){ PIPE.agentId=byId('pipeAgent').value||''; PIPE.page=1; loadPipeline(); }
-
 
1208
  function agentQs(){ return PIPE.agentId ? ('&agentId='+encodeURIComponent(PIPE.agentId)) : ''; }
1178
 
1209
 
1179
  function loadPipeline(){
1210
  function loadPipeline(){
1180
    byId('ccPipeline').innerHTML='<tr><td colspan="10"><div class="ph">Loading…</div></td></tr>';
1211
    byId('ccPipeline').innerHTML='<tr><td colspan="10"><div class="ph">Loading…</div></td></tr>';
1181
    var p='/lms/dashboard/pipeline?page='+PIPE.page+'&pageSize='+PIPE.pageSize+
1212
    var p='/lms/dashboard/pipeline?page='+PIPE.page+'&pageSize='+PIPE.pageSize+
1182
          '&sort='+encodeURIComponent(PIPE.sort)+'&dir='+encodeURIComponent(PIPE.dir);
1213
          '&sort='+encodeURIComponent(PIPE.sort)+'&dir='+encodeURIComponent(PIPE.dir)+agentQs();
1183
    if(PIPE.q){ p+='&q='+encodeURIComponent(PIPE.q);
1214
    if(PIPE.q){ p+='&q='+encodeURIComponent(PIPE.q);
1184
                if(STATE.regionId) p+='&regionId='+encodeURIComponent(STATE.regionId); }
1215
                if(STATE.regionId) p+='&regionId='+encodeURIComponent(STATE.regionId); }
1185
    else { p+=qs(); }
1216
    else { p+=qs(); }
1186
    getJSON(p,function(d){
1217
    getJSON(p,function(d){
1187
      d=d||{}; var rows=d.rows||[];
1218
      d=d||{}; var rows=d.rows||[];
Line 1466... Line 1497...
1466
    if(asg){ e.preventDefault(); e.stopPropagation();
1497
    if(asg){ e.preventDefault(); e.stopPropagation();
1467
      openAssign(parseInt(asg.getAttribute('data-assign'),10), asg.getAttribute('data-assignname'),
1498
      openAssign(parseInt(asg.getAttribute('data-assign'),10), asg.getAttribute('data-assignname'),
1468
                 asg.getAttribute('data-assignowner'), asg.getAttribute('data-assignregion')); return; }
1499
                 asg.getAttribute('data-assignowner'), asg.getAttribute('data-assignregion')); return; }
1469
    var pick=e.target.closest('[data-assignto]');
1500
    var pick=e.target.closest('[data-assignto]');
1470
    if(pick){ assignTo(parseInt(pick.getAttribute('data-assignto'),10)); return; }
1501
    if(pick){ assignTo(parseInt(pick.getAttribute('data-assignto'),10)); return; }
-
 
1502
    var rb=e.target.closest('[data-rgn]');
-
 
1503
    if(rb){ openRegionOwner(parseInt(rb.getAttribute('data-rgn'),10), rb.getAttribute('data-rgnlabel'), rb.getAttribute('data-rgnowner')); return; }
-
 
1504
    var rp=e.target.closest('[data-rgnpick]');
-
 
1505
    if(rp){ setRegionOwner(parseInt(rp.getAttribute('data-rgnpick'),10), rp.getAttribute('data-rgnname')); return; }
1471
    var ed=e.target.closest('[data-editlead]');
1506
    var ed=e.target.closest('[data-editlead]');
1472
    if(ed){ e.preventDefault(); e.stopPropagation();
1507
    if(ed){ e.preventDefault(); e.stopPropagation();
1473
      editLeadById(parseInt(ed.getAttribute('data-editlead'),10)); return; }
1508
      editLeadById(parseInt(ed.getAttribute('data-editlead'),10)); return; }
1474
    var cb=e.target.closest('[data-call]');
1509
    var cb=e.target.closest('[data-call]');
1475
    if(cb){ e.preventDefault(); e.stopPropagation();
1510
    if(cb){ e.preventDefault(); e.stopPropagation();
Line 1484... Line 1519...
1484
    var att=e.target.closest('.att[data-att]'); if(att){ drill('Needs attention',att.getAttribute('data-attt'),'Attention queue','ATTENTION',att.getAttribute('data-att')); return; }
1519
    var att=e.target.closest('.att[data-att]'); if(att){ drill('Needs attention',att.getAttribute('data-attt'),'Attention queue','ATTENTION',att.getAttribute('data-att')); return; }
1485
    var reg=e.target.closest('tr[data-region]'); if(reg){ drill('State funnel',reg.getAttribute('data-rn'),'Region '+reg.getAttribute('data-region'),'REGION',reg.getAttribute('data-region')); return; }
1520
    var reg=e.target.closest('tr[data-region]'); if(reg){ drill('State funnel',reg.getAttribute('data-rn'),'Region '+reg.getAttribute('data-region'),'REGION',reg.getAttribute('data-region')); return; }
1486
    var rep=e.target.closest('tr[data-rep]'); if(rep){ openReport(rep.getAttribute('data-rep')); return; }
1521
    var rep=e.target.closest('tr[data-rep]'); if(rep){ openReport(rep.getAttribute('data-rep')); return; }
1487
    var lead=e.target.closest('[data-leadid]'); if(lead){ var lid=lead.getAttribute('data-leadid');
1522
    var lead=e.target.closest('[data-leadid]'); if(lead){ var lid=lead.getAttribute('data-leadid');
1488
      closeDrawer(); LOADED.record=true; byId('recLookup').value=lid; injectRecord(lid);
1523
      closeDrawer(); LOADED.record=true; byId('recLookup').value=lid; injectRecord(lid);
1489
      go('record',document.querySelectorAll('.tab')[1]); return; }
1524
      go('record',document.querySelector('.tab[data-screen="record"]')); return; }
1490
  });
1525
  });
1491
 
1526
 
1492
  // ---- filters
1527
  // ---- filters
1493
  function refresh(){ STATE.regionId=byId('fRegion').value||''; STATE.from=byId('fFrom').value||''; STATE.to=byId('fTo').value||'';
1528
  function refresh(){ STATE.regionId=byId('fRegion').value||''; STATE.from=byId('fFrom').value||''; STATE.to=byId('fTo').value||'';
1494
    getJSON('/lms/dashboard/summary?_=1'+qs(),renderSummary); loadPipeline();
1529
    getJSON('/lms/dashboard/summary?_=1'+qs(),renderSummary); loadPipeline();
Line 2068... Line 2103...
2068
  // ---- Export (SOP §18.1) -------------------------------------------------------------------
2103
  // ---- Export (SOP §18.1) -------------------------------------------------------------------
2069
  // Exports what is on screen — the current filters, search and sort — rather than everything,
2104
  // Exports what is on screen — the current filters, search and sort — rather than everything,
2070
  // because an export that ignores the filters is a different report than the one being looked at.
2105
  // because an export that ignores the filters is a different report than the one being looked at.
2071
  function exportPipeline(){
2106
  function exportPipeline(){
2072
    var p='/lms/dashboard/pipeline?page=1&pageSize=500&sort='+encodeURIComponent(PIPE.sort)+
2107
    var p='/lms/dashboard/pipeline?page=1&pageSize=500&sort='+encodeURIComponent(PIPE.sort)+
2073
          '&dir='+encodeURIComponent(PIPE.dir);
2108
          '&dir='+encodeURIComponent(PIPE.dir)+agentQs();
2074
    if(PIPE.q){ p+='&q='+encodeURIComponent(PIPE.q); if(STATE.regionId) p+='&regionId='+encodeURIComponent(STATE.regionId); }
2109
    if(PIPE.q){ p+='&q='+encodeURIComponent(PIPE.q); if(STATE.regionId) p+='&regionId='+encodeURIComponent(STATE.regionId); }
2075
    else { p+=qs(); }
2110
    else { p+=qs(); }
2076
    toast('Preparing export…');
2111
    toast('Preparing export…');
2077
    getJSON(p,function(d){
2112
    getJSON(p,function(d){
2078
      var rows=(d&&d.rows)?d.rows:[];
2113
      var rows=(d&&d.rows)?d.rows:[];
Line 2333... Line 2368...
2333
      if(REC && REC.id===ASG.leadId) reloadRecord();
2368
      if(REC && REC.id===ASG.leadId) reloadRecord();
2334
      refresh();
2369
      refresh();
2335
    });
2370
    });
2336
  }
2371
  }
2337
 
2372
 
-
 
2373
  // ---- region owner (Auto-Assignment tab): add or change the BM/RSM behind a region -----------
-
 
2374
  var RGN={ regionId:null, label:'', current:'', people:[] };
-
 
2375
  function openRegionOwner(regionId,label,current){
-
 
2376
    RGN.regionId=regionId; RGN.label=label; RGN.current=current||'';
-
 
2377
    byId('rgnWho').innerHTML='<b>'+esc(label)+'</b> &middot; '+(current?('currently '+esc(current)):'vacant (HOLD)');
-
 
2378
    byId('rgnSearch').value=''; byId('rgnList').innerHTML='<div class="ph">Loading…</div>';
-
 
2379
    byId('rgnModal').classList.remove('hide');
-
 
2380
    getJSON('/lms/salespeople?regionId='+encodeURIComponent(regionId), function(d){
-
 
2381
      RGN.people=(d&&d.people)||[]; renderRegionOwnerList();
-
 
2382
    }, function(){ byId('rgnList').innerHTML='<div class="ph">Could not load the sales list.</div>'; });
-
 
2383
  }
-
 
2384
  function closeRegionOwner(){ byId('rgnModal').classList.add('hide'); }
-
 
2385
  function renderRegionOwnerList(){
-
 
2386
    var q=(byId('rgnSearch').value||'').trim().toLowerCase();
-
 
2387
    // One row per person: the tier they hold elsewhere is context, not a choice — the region
-
 
2388
    // always gets an L4 row so the engine's first lookup finds them.
-
 
2389
    var seen={}, list=[];
-
 
2390
    RGN.people.forEach(function(p){ if(seen[p.authId]) { if(p.coversRegion) seen[p.authId].coversRegion=true; return; }
-
 
2391
      var c={authId:p.authId,name:p.name,tier:p.tier,regions:(p.regions||[]).slice(),coversRegion:!!p.coversRegion}; seen[p.authId]=c; list.push(c); });
-
 
2392
    list=list.filter(function(p){ if(!q) return true;
-
 
2393
      return (p.name||'').toLowerCase().indexOf(q)>=0 || (p.tier||'').toLowerCase().indexOf(q)>=0 || (p.regions||[]).join(' ').toLowerCase().indexOf(q)>=0; });
-
 
2394
    list.sort(function(a,b){ if(a.coversRegion!==b.coversRegion) return a.coversRegion?-1:1; return (a.name||'').localeCompare(b.name||''); });
-
 
2395
    if(!list.length){ byId('rgnList').innerHTML='<div class="ph">Nobody matches. A person must already hold a sales position (L1–L5) to be offered here.</div>'; return; }
-
 
2396
    byId('rgnList').innerHTML=list.map(function(p){
-
 
2397
      return '<div class="asgrow'+(p.coversRegion?' covers':'')+'" data-rgnpick="'+p.authId+'" data-rgnname="'+esc(p.name)+'">'+
-
 
2398
        '<span class="tierpill">'+esc(p.tier)+'</span><span class="nm">'+esc(p.name)+'</span>'+
-
 
2399
        '<span class="rg">'+(p.coversRegion?'already covers this region · ':'')+esc((p.regions||[]).join(', ')||'no region')+'</span></div>'; }).join('');
-
 
2400
  }
-
 
2401
  function setRegionOwner(authId,name){
-
 
2402
    if(!RGN.regionId) return;
-
 
2403
    if(!confirm((RGN.current?('Change '+RGN.label+' from '+RGN.current+' to '+name+'?'):('Assign '+RGN.label+' to '+name+'?'))+'\n\nNew leads in this region will be routed to them.')) return;
-
 
2404
    postForm('/lms/region-owner',{regionId:RGN.regionId, authId:authId}, function(res){
-
 
2405
      var why=refusal(res); if(why){ toast(why); return; }
-
 
2406
      closeRegionOwner();
-
 
2407
      toast((res&&res.bmName?res.bmName:name)+' now owns '+RGN.label+(res&&res.previous?(' (was '+res.previous+')'):''),1);
-
 
2408
      buildAssign();
-
 
2409
    });
-
 
2410
  }
-
 
2411
 
2338
  // ---- checklist question management (CRUD) --------------------------------------------------
2412
  // ---- checklist question management (CRUD) --------------------------------------------------
2339
  var QADM = { questions:[], stages:[], editing:null };
2413
  var QADM = { questions:[], stages:[], editing:null };
2340
 
2414
 
2341
  function loadQuestions(){
2415
  function loadQuestions(){
2342
    byId('qBody').innerHTML='<tr><td colspan="8"><div class="ph">Loading…</div></td></tr>';
2416
    byId('qBody').innerHTML='<tr><td colspan="8"><div class="ph">Loading…</div></td></tr>';
Line 2694... Line 2768...
2694
  }
2768
  }
2695
  function brandTotal(){
2769
  function brandTotal(){
2696
    var t=0; collectBrands().forEach(function(b){ t+=b.value; });
2770
    var t=0; collectBrands().forEach(function(b){ t+=b.value; });
2697
    var el=byId('cValueTotal');
2771
    var el=byId('cValueTotal');
2698
    if(el) el.textContent='total '+(t>0?fmtMoney(t):'₹0');
2772
    if(el) el.textContent='total '+(t>0?fmtMoney(t):'₹0');
-
 
2773
    syncCounter(t);
2699
    return t;
2774
    return t;
2700
  }
2775
  }
-
 
2776
  // Counter size is the legacy name for the shop's monthly business in rupees (the Leads screen
-
 
2777
  // stored 10,00,000-style values there), so it follows the brand total until the agent overrides
-
 
2778
  // it by typing. Clearing the field hands it back to the sum.
-
 
2779
  var COUNTER_MANUAL=false;
-
 
2780
  function syncCounter(total){
-
 
2781
    var c=byId('cCounter'); if(!c) return;
-
 
2782
    if(COUNTER_MANUAL) return;
-
 
2783
    c.value = total>0 ? String(total) : '';
-
 
2784
    var h=byId('cCounterHint'); if(h) h.textContent='= brand total, editable';
-
 
2785
  }
-
 
2786
  function counterEdited(){
-
 
2787
    var c=byId('cCounter'); if(!c) return;
-
 
2788
    var v=(c.value||'').trim();
-
 
2789
    COUNTER_MANUAL = v.length>0;
-
 
2790
    var h=byId('cCounterHint');
-
 
2791
    if(h) h.textContent = COUNTER_MANUAL ? 'manual — clear to use brand total' : '= brand total, editable';
-
 
2792
    if(!COUNTER_MANUAL) syncCounter(brandTotal());
-
 
2793
  }
2701
  function fmtMoney(n){
2794
  function fmtMoney(n){
2702
    if(n>=10000000) return '₹'+(n/10000000).toFixed(n%10000000?2:0)+'Cr';
2795
    if(n>=10000000) return '₹'+(n/10000000).toFixed(n%10000000?2:0)+'Cr';
2703
    if(n>=100000)   return '₹'+(n/100000).toFixed(n%100000?2:0)+'L';
2796
    if(n>=100000)   return '₹'+(n/100000).toFixed(n%100000?2:0)+'L';
2704
    if(n>=1000)     return '₹'+(n/1000).toFixed(n%1000?1:0)+'K';
2797
    if(n>=1000)     return '₹'+(n/1000).toFixed(n%1000?1:0)+'K';
2705
    return '₹'+n;
2798
    return '₹'+n;
Line 2732... Line 2825...
2732
    if(phone.length!==10){ toast('A valid 10-digit contact is required'); return; }
2825
    if(phone.length!==10){ toast('A valid 10-digit contact is required'); return; }
2733
    if(!region){ toast('Pick a region'); return; }
2826
    if(!region){ toast('Pick a region'); return; }
2734
    var address=val('cAddress').trim(), city=val('cCity').trim(), state=val('cState');
2827
    var address=val('cAddress').trim(), city=val('cCity').trim(), state=val('cState');
2735
    if(address.length>128){ toast('Address is too long (128 max)'); return; }
2828
    if(address.length>128){ toast('Address is too long (128 max)'); return; }
2736
    if(city.length>30){ toast('City is too long (30 max)'); return; }
2829
    if(city.length>30){ toast('City is too long (30 max)'); return; }
2737
    var counter=parseInt((val('cCounter')||'').replace(/\D/g,''),10);
2830
    var counter=parseMoney(val('cCounter'));
-
 
2831
    if(counter>2147483647){ toast('Counter size is too large — enter rupees per month'); return; }
2738
    // No address check on either path: the capture link is what establishes location, and it is
2832
    // No address check on either path: the capture link is what establishes location, and it is
2739
    // generated for every lead regardless of path. Address / city / state are kept when typed.
2833
    // generated for every lead regardless of path. Address / city / state are kept when typed.
2740
    var payload={ firstName:fn, mobile:phone, outletName:biz||'', source:val('cSource')||'LMS Dashboard',
2834
    var payload={ firstName:fn, mobile:phone, outletName:biz||'', source:val('cSource')||'LMS Dashboard',
2741
      regionId:parseInt(region,10), creationPath:path,
2835
      regionId:parseInt(region,10), creationPath:path,
2742
      address:address, city:city, state:state||'',
2836
      address:address, city:city, state:state||'',
Line 2771... Line 2865...
2771
        ['cRetailer','cBusiness','cContact','cAddress','cCity','cCounter','cRemark'].forEach(function(x){ var e=byId(x); if(e) e.value=''; });
2865
        ['cRetailer','cBusiness','cContact','cAddress','cCity','cCounter','cRemark'].forEach(function(x){ var e=byId(x); if(e) e.value=''; });
2772
        ['cState'].forEach(function(x){ var e=byId(x); if(e) e.value=''; });
2866
        ['cState'].forEach(function(x){ var e=byId(x); if(e) e.value=''; });
2773
        var cs=byId('cSource'); if(cs) cs.value='LMS Dashboard'; var cc=byId('cConversion'); if(cc) cc.value='false';
2867
        var cs=byId('cSource'); if(cs) cs.value='LMS Dashboard'; var cc=byId('cConversion'); if(cc) cc.value='false';
2774
        document.querySelectorAll('#cBrandGrid .bval').forEach(function(el){ el.value=''; });
2868
        document.querySelectorAll('#cBrandGrid .bval').forEach(function(el){ el.value=''; });
2775
        document.querySelectorAll('#cPhotoGrid .cphoto').forEach(function(el){ el.value=''; });
2869
        document.querySelectorAll('#cPhotoGrid .cphoto').forEach(function(el){ el.value=''; });
2776
        brandTotal();
2870
        COUNTER_MANUAL=false; brandTotal();
2777
 
2871
 
2778
        // Hand the capture link over immediately — the whole point of generating it on create is
2872
        // Hand the capture link over immediately — the whole point of generating it on create is
2779
        // that the agent is still on the call with the retailer.
2873
        // that the agent is still on the call with the retailer.
2780
        if(res.geoLink){
2874
        if(res.geoLink){
2781
          if(navigator.clipboard && navigator.clipboard.writeText){
2875
          if(navigator.clipboard && navigator.clipboard.writeText){
Line 2786... Line 2880...
2786
            prompt('Send this capture link to the retailer:', res.geoLink);
2880
            prompt('Send this capture link to the retailer:', res.geoLink);
2787
          }
2881
          }
2788
        }
2882
        }
2789
 
2883
 
2790
        byId('recLookup').value=res.leadId; injectRecord(res.leadId);
2884
        byId('recLookup').value=res.leadId; injectRecord(res.leadId);
2791
        go('record',document.querySelectorAll('.tab')[1]); refresh(); })
2885
        go('record',document.querySelector('.tab[data-screen="record"]')); refresh(); })
2792
      .catch(function(e){ CREATE_BUSY=false; toast(e&&e.message==='upload'?'A photo failed to upload — the lead was not created':'Create failed'); }); }
2886
      .catch(function(e){ CREATE_BUSY=false; toast(e&&e.message==='upload'?'A photo failed to upload — the lead was not created':'Create failed'); }); }
2793
 
2887
 
2794
  // ====================== AUTO-ASSIGNMENT (real resolve per region) ======================
2888
  // ====================== AUTO-ASSIGNMENT (real resolve per region) ======================
2795
  function buildAssign(){ var opts=byId('cRegion').querySelectorAll('option'); var holdCount=0, assignedCount=0, pending=0;
2889
  function buildAssign(){ var opts=byId('cRegion').querySelectorAll('option'); var holdCount=0, assignedCount=0, pending=0;
2796
    var body=byId('assignMap'); body.innerHTML='';
2890
    var body=byId('assignMap'); body.innerHTML='';
2797
    // Rows are created up-front in region order, then filled in as each resolve lands — otherwise
2891
    // Rows are created up-front in region order, then filled in as each resolve lands — otherwise
2798
    // the table would be ordered by whichever request happened to answer first.
2892
    // the table would be ordered by whichever request happened to answer first.
2799
    opts.forEach(function(o){ if(!o.value) return; pending++;
2893
    opts.forEach(function(o){ if(!o.value) return; pending++;
2800
      var label=o.textContent;
2894
      var label=o.textContent;
2801
      var tr=document.createElement('tr');
2895
      var tr=document.createElement('tr');
2802
      tr.innerHTML='<td><b>'+esc(label)+'</b></td><td colspan="2"><span class="ph">Resolving…</span></td>';
2896
      tr.innerHTML='<td><b>'+esc(label)+'</b></td><td colspan="3"><span class="ph">Resolving…</span></td>';
2803
      body.appendChild(tr);
2897
      body.appendChild(tr);
2804
      function done(){ if(--pending===0){ renderAssignKpis(assignedCount,holdCount); } }
2898
      function done(){ if(--pending===0){ renderAssignKpis(assignedCount,holdCount); } }
2805
      getJSON('/lms/resolve-region?regionId='+encodeURIComponent(o.value),function(d){ d=d||{};
2899
      getJSON('/lms/resolve-region?regionId='+encodeURIComponent(o.value),function(d){ d=d||{};
2806
        var hold=(d.assignmentStatus==='HOLD' || !d.bmName);
2900
        var hold=(d.assignmentStatus==='HOLD' || !d.bmName);
2807
        if(hold) holdCount++; else assignedCount++;
2901
        if(hold) holdCount++; else assignedCount++;
2808
        tr.innerHTML='<td><b>'+esc(label)+'</b></td>'+
2902
        tr.innerHTML='<td><b>'+esc(label)+'</b></td>'+
2809
          '<td>'+(hold?'<span class="warnflag">— vacant —</span>':esc(d.bmName))+'</td>'+
2903
          '<td>'+(hold?'<span class="warnflag">— vacant —</span>':esc(d.bmName))+'</td>'+
2810
          '<td>'+(hold?'<span class="stage st-ni">HOLD</span>':'<span class="stage st-active">Active</span>')+'</td>';
2904
          '<td>'+(hold?'<span class="stage st-ni">HOLD</span>':'<span class="stage st-active">Active</span>')+'</td>'+
-
 
2905
          '<td class="num"><button type="button" class="btn ghost sm" data-rgn="'+esc(o.value)+'" data-rgnlabel="'+esc(label)+'" data-rgnowner="'+esc(hold?'':(d.bmName||''))+'">'+(hold?'Assign':'Change')+'</button></td>';
2811
        done();
2906
        done();
2812
      },function(){ tr.innerHTML='<td><b>'+esc(label)+'</b></td><td colspan="2"><span class="warnflag">could not resolve</span></td>';
2907
      },function(){ tr.innerHTML='<td><b>'+esc(label)+'</b></td><td colspan="3"><span class="warnflag">could not resolve</span></td>';
2813
        done(); }); });
2908
        done(); }); });
2814
    // HOLD queue leads
2909
    // HOLD queue leads
2815
    getJSON('/lms/dashboard/leads?bucketType=ATTENTION&bucketKey=HOLD',function(d){ var leads=(d&&d.leads)?d.leads:[];
2910
    getJSON('/lms/dashboard/leads?bucketType=ATTENTION&bucketKey=HOLD',function(d){ var leads=(d&&d.leads)?d.leads:[];
2816
      if(!leads.length){ byId('holdQueue').innerHTML='<div class="ph">No leads in HOLD. 🎉</div>'; return; }
2911
      if(!leads.length){ byId('holdQueue').innerHTML='<div class="ph">No leads in HOLD. 🎉</div>'; return; }
2817
      var h=''; leads.slice(0,8).forEach(function(l){ h+='<div style="display:flex;align-items:center;gap:10px;font-size:12.5px;padding:5px 0" class="clickrow" data-leadid="'+l.id+'">'+
2912
      var h=''; leads.slice(0,8).forEach(function(l){ h+='<div style="display:flex;align-items:center;gap:10px;font-size:12.5px;padding:5px 0" class="clickrow" data-leadid="'+l.id+'">'+
Line 3052... Line 3147...
3052
    obs.observe(document.body, {childList:true, subtree:true});
3147
    obs.observe(document.body, {childList:true, subtree:true});
3053
  })();
3148
  })();
3054
 
3149
 
3055
  // ---- initial paint from seeds, pipeline loads live
3150
  // ---- initial paint from seeds, pipeline loads live
3056
  renderSummary(SEED.summary); renderState(SEED.state); renderHeat(SEED.heat); renderAgeing(SEED.ageing);
3151
  renderSummary(SEED.summary); renderState(SEED.state); renderHeat(SEED.heat); renderAgeing(SEED.ageing);
-
 
3152
  loadCallCenterUsers();
3057
  setDashPeriod();
3153
  setDashPeriod();
3058
  loadPipeline();
3154
  loadPipeline();
3059
 
3155
 
3060
  // ---- deep-link: a URL hash such as dash or record opens that tab
3156
  // ---- deep-link: a URL hash such as dash or record opens that tab
3061
  (function(){ var TABIDX={cmd:0,record:1,create:2,assign:3,dash:4,audit:5};
3157
  // Tabs are looked up by data-screen, not position: a tab the viewer is not allowed to see (Create
3062
    var h=(location.hash||'').replace('#','').trim();
3158
  // Lead below L2) is simply absent, and a hash for it is ignored rather than opening the screen.
3063
    if(h && TABIDX[h]!=null){ go(h, document.querySelectorAll('.tab')[TABIDX[h]]); }
3159
  (function(){ function tabFor(k){ return document.querySelector('.tab[data-screen="'+k+'"]'); }
-
 
3160
    function open(k){ var b=tabFor(k); if(k && b){ go(k, b); } }
3064
    window.addEventListener('hashchange',function(){ var k=(location.hash||'').replace('#','').trim();
3161
    open((location.hash||'').replace('#','').trim());
3065
      if(k && TABIDX[k]!=null){ go(k, document.querySelectorAll('.tab')[TABIDX[k]]); } }); })();
3162
    window.addEventListener('hashchange',function(){ open((location.hash||'').replace('#','').trim()); }); })();
3066
</script>
3163
</script>
3067
</body>
3164
</body>
3068
</html>
3165
</html>