Subversion Repositories SmartDukaan

Rev

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

Rev 37125 Rev 37651
Line 211... Line 211...
211
  .inp{border:1px solid var(--line);border-radius:9px;padding:9px 11px;font-size:13px;font-weight:500;background:#fff;color:var(--ink);width:100%}
211
  .inp{border:1px solid var(--line);border-radius:9px;padding:9px 11px;font-size:13px;font-weight:500;background:#fff;color:var(--ink);width:100%}
212
  .inp:focus{outline:none;border-color:var(--brand2);box-shadow:0 0 0 3px #eaefff}
212
  .inp:focus{outline:none;border-color:var(--brand2);box-shadow:0 0 0 3px #eaefff}
213
  .inp[readonly],.inp.ro{background:#f5f6fa;color:var(--muted);font-weight:600}
213
  .inp[readonly],.inp.ro{background:#f5f6fa;color:var(--muted);font-weight:600}
214
  select.inp{appearance:none;cursor:pointer;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 11px center}
214
  select.inp{appearance:none;cursor:pointer;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 11px center}
215
  .hinttext{font-size:11px;color:var(--soft)}
215
  .hinttext{font-size:11px;color:var(--soft)}
-
 
216
  /* Brand-wise value capture. auto-fill keeps it compact on a laptop and collapses to one column
-
 
217
     on a phone, which is where Path B (field encounter) is actually used. */
-
 
218
  .brandgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(148px,1fr));gap:8px;margin-top:2px}
-
 
219
  .brandcell{display:flex;align-items:center;gap:6px;background:#f7f8fc;border:1px solid var(--line);
-
 
220
    border-radius:8px;padding:4px 8px}
-
 
221
  .brandcell .bname{font-size:11.5px;font-weight:700;color:var(--ink);white-space:nowrap;min-width:58px}
-
 
222
  .brandcell .bval{border:0;background:transparent;padding:3px 0;height:auto;font-size:12.5px;text-align:right}
-
 
223
  .brandcell .bval:focus{outline:0;box-shadow:none}
-
 
224
  .brandcell:focus-within{border-color:var(--brand);background:#fff}
216
  .resolvebox{background:#f5f7ff;border:1px solid #dfe4f7;border-radius:11px;padding:13px 15px;display:flex;align-items:center;gap:13px}
225
  .resolvebox{background:#f5f7ff;border:1px solid #dfe4f7;border-radius:11px;padding:13px 15px;display:flex;align-items:center;gap:13px}
217
  .resolvebox .ra{width:38px;height:38px;border-radius:10px;background:var(--navy);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;flex:none}
226
  .resolvebox .ra{width:38px;height:38px;border-radius:10px;background:var(--navy);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;flex:none}
218
  .resolvebox .rt{font-size:11px;color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:.04em}
227
  .resolvebox .rt{font-size:11px;color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:.04em}
219
  .resolvebox .rn{font-size:15px;font-weight:750;margin-top:1px}
228
  .resolvebox .rn{font-size:15px;font-weight:750;margin-top:1px}
220
  .resolvebox .rc{font-size:11.5px;color:var(--muted);margin-top:2px}
229
  .resolvebox .rc{font-size:11.5px;color:var(--muted);margin-top:2px}
Line 356... Line 365...
356
  <div id="create" class="hide">
365
  <div id="create" class="hide">
357
    <div class="pagehead">
366
    <div class="pagehead">
358
      <div>
367
      <div>
359
        <div class="crumb">Create · New Lead</div>
368
        <div class="crumb">Create · New Lead</div>
360
        <h1 class="page">Create Lead</h1>
369
        <h1 class="page">Create Lead</h1>
361
        <div class="sub">Mandatory fields must be filled before submit. LMS ID is generated only on successful submit — auto-assigned from region.</div>
370
        <div class="sub">Only what an agent can know on a first call. LMS ID is generated on submit and the lead is auto-assigned from region. Address, store photo and geo-pin come from the capture link, which is created automatically and copied to your clipboard.</div>
362
      </div>
371
      </div>
363
      <div class="hd-right">
372
      <div class="hd-right">
364
        <div class="ctl" style="padding:4px;display:flex;gap:4px">
373
        <div class="ctl" style="padding:4px;display:flex;gap:4px">
365
          <button class="btn coral sm" id="pathA" style="border-radius:7px" onclick="setPath('A',this)">Path A · BGC outbound</button>
374
          <button class="btn coral sm" id="pathA" style="border-radius:7px" onclick="setPath('A',this)">Path A · BGC outbound</button>
366
          <button class="btn ghost sm" id="pathB" style="border-radius:7px" onclick="setPath('B',this)">Path B · Field encounter</button>
375
          <button class="btn ghost sm" id="pathB" style="border-radius:7px" onclick="setPath('B',this)">Path B · Field encounter</button>
Line 373... Line 382...
373
        <h2>Lead details</h2>
382
        <h2>Lead details</h2>
374
        <p class="desc" id="createDesc">Path A — captured by BGC agent during an outbound call, after consent.</p>
383
        <p class="desc" id="createDesc">Path A — captured by BGC agent during an outbound call, after consent.</p>
375
        <div class="form-grid">
384
        <div class="form-grid">
376
          <div class="fld"><label>Retailer name <span class="m">*</span></label><input class="inp" id="cRetailer" placeholder="Contact person"></div>
385
          <div class="fld"><label>Retailer name <span class="m">*</span></label><input class="inp" id="cRetailer" placeholder="Contact person"></div>
377
          <div class="fld"><label>Business name <span class="m">*</span></label><input class="inp" id="cBusiness" placeholder="Shop / trade name"></div>
386
          <div class="fld"><label>Business name <span class="m">*</span></label><input class="inp" id="cBusiness" placeholder="Shop / trade name"></div>
378
          <div class="fld full"><label>Location <span class="m">*</span> <span class="hinttext" style="float:right" id="cLocHint">geo-pin optional on Path A</span></label><input class="inp" id="cLocation" placeholder="Full address"></div>
-
 
379
          <div class="fld"><label>Contact number <span class="m">*</span></label><input class="inp mono" id="cContact" placeholder="10-digit mobile" inputmode="numeric"></div>
387
          <div class="fld"><label>Contact number <span class="m">*</span></label><input class="inp mono" id="cContact" placeholder="10-digit mobile" inputmode="numeric"></div>
-
 
388
          <div class="fld full">
380
          <div class="fld"><label>Business value</label><input class="inp" id="cValue" placeholder="e.g. 85000 (₹/month)"></div>
389
            <label>Monthly business value — brand wise <span class="auto" id="cValueTotal">total ₹0</span></label>
-
 
390
            <div class="brandgrid" id="cBrandGrid">
-
 
391
              #foreach($b in $leadBrands)
-
 
392
                <div class="brandcell">
-
 
393
                  <span class="bname">$b</span>
-
 
394
                  <input class="inp bval mono" data-brand="$b" placeholder="0" inputmode="numeric"
-
 
395
                         oninput="brandTotal()">
-
 
396
                </div>
-
 
397
              #end
-
 
398
            </div>
-
 
399
            <div class="hinttext">Type in rupees per month — 100000 or 1L, 50k both work. Leave a brand blank if it is not stocked.</div>
-
 
400
          </div>
381
          <div class="fld"><label>Interest status</label><select class="inp" id="cInterest"><option>Interested</option><option>Callback</option><option>Not Interested</option></select></div>
401
          <div class="fld"><label>Interest status</label><select class="inp" id="cInterest"><option>Interested</option><option>Callback</option><option>Not Interested</option></select></div>
382
          <div class="fld"><label>Category / brands</label><input class="inp" id="cBrands" placeholder="Samsung, Vivo…"></div>
-
 
383
          <div class="fld full">
402
          <div class="fld full">
384
            <label>Region / State <span class="m">*</span> <span class="auto">drives auto-assignment</span></label>
403
            <label>Region / State <span class="m">*</span> <span class="auto">drives auto-assignment</span></label>
385
            <select class="inp" id="cRegion" onchange="resolveBM(this.value)">
404
            <select class="inp" id="cRegion" onchange="resolveBM(this.value)">
386
              <option value="">Select a region…</option>
405
              <option value="">Select a region…</option>
387
              #foreach($r in $regions)
406
              #foreach($r in $regions)
Line 608... Line 627...
608
    setTimeout(function(){ t.style.opacity='0'; t.style.transition='opacity .3s'; setTimeout(function(){ t.remove(); },300); },2600); }
627
    setTimeout(function(){ t.style.opacity='0'; t.style.transition='opacity .3s'; setTimeout(function(){ t.remove(); },300); },2600); }
609
 
628
 
610
  // ---- filters + fetch
629
  // ---- filters + fetch
611
  function qs(){ var q=''; if(STATE.regionId)q+='&regionId='+encodeURIComponent(STATE.regionId);
630
  function qs(){ var q=''; if(STATE.regionId)q+='&regionId='+encodeURIComponent(STATE.regionId);
612
    if(STATE.from)q+='&from='+encodeURIComponent(STATE.from); if(STATE.to)q+='&to='+encodeURIComponent(STATE.to); return q; }
631
    if(STATE.from)q+='&from='+encodeURIComponent(STATE.from); if(STATE.to)q+='&to='+encodeURIComponent(STATE.to); return q; }
613
  function getJSON(path,cb){ fetch(CTX+path,{credentials:'same-origin',headers:{'X-Requested-With':'XMLHttpRequest'}})
632
  function getJSON(path,cb,onErr){ fetch(CTX+path,{credentials:'same-origin',headers:{'X-Requested-With':'XMLHttpRequest'}})
-
 
633
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
614
    .then(function(r){ return r.json(); }).then(function(j){ cb((j&&j.response!==undefined)?j.response:j); })
634
    .then(function(j){ cb((j&&j.response!==undefined)?j.response:j); })
615
    .catch(function(e){ console.error('LMS fetch failed',path,e); toast('Could not load data'); }); }
635
    .catch(function(e){ console.error('LMS fetch failed',path,e);
-
 
636
      if(typeof onErr==='function'){ onErr(e); } else { toast('Could not load data'); } }); }
616
 
637
 
617
  // ---- stage helpers
638
  // ---- stage helpers
618
  var STCLASS={NEW:'st-new',ASSIGNED:'st-assigned',CONTACTED:'st-contacted',QUALIFIED:'st-qualified',
639
  var STCLASS={NEW:'st-new',ASSIGNED:'st-assigned',CONTACTED:'st-contacted',QUALIFIED:'st-qualified',
619
    BEAT_PLANNED:'st-beat',VISITED:'st-visited',ONBOARDED:'st-onboarded',ACTIVE:'st-active',NOT_INTERESTED:'st-ni',DROPPED:'st-dead'};
640
    BEAT_PLANNED:'st-beat',VISITED:'st-visited',ONBOARDED:'st-onboarded',ACTIVE:'st-active',NOT_INTERESTED:'st-ni',DROPPED:'st-dead'};
620
  function stagePill(st,lb){ if(!st) return '<span class="stage st-new">—</span>'; return '<span class="stage '+(STCLASS[st]||'st-new')+'">'+esc(lb||st)+'</span>'; }
641
  function stagePill(st,lb){ if(!st) return '<span class="stage st-new">—</span>'; return '<span class="stage '+(STCLASS[st]||'st-new')+'">'+esc(lb||st)+'</span>'; }
Line 762... Line 783...
762
  function drill(crumb,title,sub,bt,bk){ openDrawer(crumb,title,'Loading…','<div class="ph">Loading…</div>');
783
  function drill(crumb,title,sub,bt,bk){ openDrawer(crumb,title,'Loading…','<div class="ph">Loading…</div>');
763
    var q='?bucketType='+encodeURIComponent(bt)+(bk?'&bucketKey='+encodeURIComponent(bk):'')+qs();
784
    var q='?bucketType='+encodeURIComponent(bt)+(bk?'&bucketKey='+encodeURIComponent(bk):'')+qs();
764
    getJSON('/lms/dashboard/leads'+q,function(d){ var leads=(d&&d.leads)?d.leads:[];
785
    getJSON('/lms/dashboard/leads'+q,function(d){ var leads=(d&&d.leads)?d.leads:[];
765
      openDrawer(crumb,title,(sub||'')+' · '+leads.length+' lead(s)',drawerLeadTable(leads)); }); }
786
      openDrawer(crumb,title,(sub||'')+' · '+leads.length+' lead(s)',drawerLeadTable(leads)); }); }
766
 
787
 
-
 
788
  // ---- app-bar search: LMS id / retailer / business / phone / city -> results in the drawer
-
 
789
  var searchTimer=null, searchSeq=0;
-
 
790
  function runSearch(){ var q=(byId('lmsSearch').value||'').trim();
-
 
791
    if(q.length<2){ toast('Type at least 2 characters'); return; }
-
 
792
    var seq=++searchSeq;
-
 
793
    openDrawer('Search','\u201c'+q+'\u201d','Searching…','<div class="ph">Searching…</div>');
-
 
794
    getJSON('/lms/dashboard/search?q='+encodeURIComponent(q),function(d){
-
 
795
      if(seq!==searchSeq) return;                       // a newer search already won
-
 
796
      var leads=(d&&d.leads)?d.leads:[];
-
 
797
      openDrawer('Search','\u201c'+q+'\u201d',leads.length+' match(es)',drawerLeadTable(leads)); },
-
 
798
      function(){ if(seq!==searchSeq) return;
-
 
799
        openDrawer('Search','\u201c'+q+'\u201d','Search failed','<div class="ph">Could not run the search.</div>'); }); }
-
 
800
  byId('lmsSearch').addEventListener('keydown',function(e){
-
 
801
    if(e.key==='Enter'){ e.preventDefault(); clearTimeout(searchTimer); runSearch(); }
-
 
802
    else if(e.key==='Escape'){ this.blur(); closeDrawer(); } });
-
 
803
  byId('lmsSearch').addEventListener('input',function(){ clearTimeout(searchTimer);
-
 
804
    var v=(this.value||'').trim(); if(v.length<2) return;
-
 
805
    searchTimer=setTimeout(runSearch,400); });
-
 
806
 
767
  var REPORT_NOTE={ 'State Funnel':'Live from the lead record — grouped by region, BM and ASM.',
807
  var REPORT_NOTE={ 'State Funnel':'Live from the lead record — grouped by region, BM and ASM.',
768
    'SLA Compliance':'Live — first_contacted_at vs first_contact_due, owner heat-map.',
808
    'SLA Compliance':'Live — first_contacted_at vs first_contact_due, owner heat-map.',
769
    'Ageing':'Live — open leads untouched > 7 days, by stage.',
809
    'Ageing':'Live — open leads untouched > 7 days, by stage.',
770
    'Management Roll-up':'Live — cross-region leads, onboardings and estimated value.',
810
    'Management Roll-up':'Live — cross-region leads, onboardings and estimated value.',
771
    'BGC Daily':'Partial — created + dispositions are real; call volume is a PROXY (contacted ÷ days). No telephony bridge yet.',
811
    'BGC Daily':'Partial — created + dispositions are real; call volume is a PROXY (contacted ÷ days). No telephony bridge yet.',
Line 863... Line 903...
863
      '<div class="tt">'+(t.type?esc(t.type):'Update')+'</div>'+
903
      '<div class="tt">'+(t.type?esc(t.type):'Update')+'</div>'+
864
      '<div class="tm">'+esc(t.actor||'System')+' · '+fmtDT(t.when)+(t.scheduled?(' · scheduled '+fmtDT(t.scheduled)):'')+'</div>'+
904
      '<div class="tm">'+esc(t.actor||'System')+' · '+fmtDT(t.when)+(t.scheduled?(' · scheduled '+fmtDT(t.scheduled)):'')+'</div>'+
865
      (t.body?('<div class="td">'+esc(t.body)+'</div>'):'')+'</div>'; }); trail+='</div>'; }
905
      (t.body?('<div class="td">'+esc(t.body)+'</div>'):'')+'</div>'; }); trail+='</div>'; }
866
    trail+='</div>';
906
    trail+='</div>';
867
 
907
 
868
    var openBtn='<div style="text-align:right;margin-bottom:12px"><button class="btn coral sm" onclick="window.open(CTX+\'/leadRecord?leadId='+d.id+'\',\'_blank\')">Open in app to act &rarr;</button></div>';
908
    var openBtn='<div style="text-align:right;margin-bottom:12px"><button class="btn coral sm" onclick="window.open(CTX+\'/dashboard?leadRecord='+d.id+'\',\'_blank\')">Open in app to act &rarr;</button></div>';
869
    byId('recordHost').innerHTML=head+step+openBtn+'<div class="grid g3"><div>'+details+trail+'</div><div>'+sla+'</div></div>';
909
    byId('recordHost').innerHTML=head+step+openBtn+'<div class="grid g3"><div>'+details+trail+'</div><div>'+sla+'</div></div>';
870
  }
910
  }
871
 
911
 
872
  // ====================== CREATE LEAD (real /lms/resolve-region + /createLead) ======================
912
  // ====================== CREATE LEAD (real /lms/resolve-region + /createLead) ======================
873
  var CREATE_PATH='A';
913
  var CREATE_PATH='A';
874
  function setPath(p,btn){ CREATE_PATH=p;
914
  function setPath(p,btn){ CREATE_PATH=p;
875
    byId('pathA').className='btn '+(p==='A'?'coral':'ghost')+' sm'; byId('pathB').className='btn '+(p==='B'?'coral':'ghost')+' sm';
915
    byId('pathA').className='btn '+(p==='A'?'coral':'ghost')+' sm'; byId('pathB').className='btn '+(p==='B'?'coral':'ghost')+' sm';
876
    byId('createDesc').textContent=(p==='A')?'Path A — captured by BGC agent during an outbound call, after consent.':'Path B — field encounter; geo-pin/location is mandatory, owner = creator.';
916
    byId('createDesc').textContent=(p==='A')?'Path A — captured by a BGC agent during an outbound call, after consent. Owner is the region BM.':'Path B — field encounter. Owner is you, the creator.';
877
    byId('cLocHint').textContent=(p==='A')?'geo-pin optional on Path A':'location mandatory on Path B'; }
-
 
-
 
917
  }
878
  function initials(n){ n=(n||'').trim(); if(!n) return '—'; var p=n.split(/\s+/); return ((p[0]||'')[0]||'').toUpperCase()+((p[1]||'')[0]||'').toUpperCase(); }
918
  function initials(n){ n=(n||'').trim(); if(!n) return '—'; var p=n.split(/\s+/); return ((p[0]||'')[0]||'').toUpperCase()+((p[1]||'')[0]||'').toUpperCase(); }
879
  function resolveBM(regionId){ if(!regionId){ byId('rName').textContent='Pick a region…'; byId('rRegion').textContent='—'; byId('raAv').textContent='—';
919
  function resolveBM(regionId){ if(!regionId){ byId('rName').textContent='Pick a region…'; byId('rRegion').textContent='—'; byId('raAv').textContent='—';
880
      byId('rEsc').textContent='The owning BM/RSM is resolved from the region (cs.position).'; return; }
920
      byId('rEsc').textContent='The owning BM/RSM is resolved from the region (cs.position).'; return; }
881
    getJSON('/lms/resolve-region?regionId='+encodeURIComponent(regionId),function(d){ d=d||{};
921
    getJSON('/lms/resolve-region?regionId='+encodeURIComponent(regionId),function(d){ d=d||{};
882
      byId('rRegion').textContent=d.regionCode||''; var nm=d.bmName||'';
922
      byId('rRegion').textContent=d.regionCode||''; var nm=d.bmName||'';
883
      if(d.assignmentStatus==='HOLD' || !nm){ byId('rName').textContent='No active BM/RSM'; byId('raAv').textContent='!';
923
      if(d.assignmentStatus==='HOLD' || !nm){ byId('rName').textContent='No active BM/RSM'; byId('raAv').textContent='!';
884
        byId('raAv').style.background='#e03131'; byId('rEsc').innerHTML='Region has no active owner &rarr; <b style="color:#e03131">HOLD queue</b>, Sales Head notified.'; }
924
        byId('raAv').style.background='#e03131'; byId('rEsc').innerHTML='Region has no active owner &rarr; <b style="color:#e03131">HOLD queue</b>, Sales Head notified.'; }
885
      else { byId('rName').textContent=nm; byId('raAv').textContent=initials(nm); byId('raAv').style.background='#1f2a5e';
925
      else { byId('rName').textContent=nm; byId('raAv').textContent=initials(nm); byId('raAv').style.background='#1f2a5e';
886
        byId('rEsc').textContent='Auto-resolved from region · escalation contact copied on the assignment email.'; } }); }
926
        byId('rEsc').textContent='Auto-resolved from region · escalation contact copied on the assignment email.'; } }); }
-
 
927
  // ---- brand-wise business value ------------------------------------------------------------
-
 
928
  // Agents write "1L", "50k", "1.5 lakh" as readily as digits, and rejecting those would just push
-
 
929
  // them back to typing a single lump sum. Parsed here so the stored value is always plain rupees.
-
 
930
  function parseMoney(raw){
-
 
931
    if(raw===null||raw===undefined) return 0;
-
 
932
    var t=(''+raw).trim().toLowerCase().replace(/[₹,\s]/g,'');
-
 
933
    if(!t) return 0;
-
 
934
    var m=t.match(/^([0-9]*\.?[0-9]+)(k|l|lac|lacs|lakh|lakhs|cr|crore|crores)?$/);
-
 
935
    if(!m) return 0;
-
 
936
    var n=parseFloat(m[1]); if(isNaN(n)) return 0;
-
 
937
    switch(m[2]){
-
 
938
      case 'k': return Math.round(n*1000);
-
 
939
      case 'l': case 'lac': case 'lacs': case 'lakh': case 'lakhs': return Math.round(n*100000);
-
 
940
      case 'cr': case 'crore': case 'crores': return Math.round(n*10000000);
-
 
941
      default: return Math.round(n);
-
 
942
    }
-
 
943
  }
-
 
944
  function collectBrands(){
-
 
945
    var out=[];
-
 
946
    document.querySelectorAll('#cBrandGrid .bval').forEach(function(el){
-
 
947
      var v=parseMoney(el.value);
-
 
948
      if(v>0) out.push({brand:el.getAttribute('data-brand'), value:v});
-
 
949
    });
-
 
950
    return out;
-
 
951
  }
-
 
952
  function brandTotal(){
-
 
953
    var t=0; collectBrands().forEach(function(b){ t+=b.value; });
-
 
954
    var el=byId('cValueTotal');
-
 
955
    if(el) el.textContent='total '+(t>0?fmtMoney(t):'₹0');
-
 
956
    return t;
-
 
957
  }
-
 
958
  function fmtMoney(n){
-
 
959
    if(n>=10000000) return '₹'+(n/10000000).toFixed(n%10000000?2:0)+'Cr';
-
 
960
    if(n>=100000)   return '₹'+(n/100000).toFixed(n%100000?2:0)+'L';
-
 
961
    if(n>=1000)     return '₹'+(n/1000).toFixed(n%1000?1:0)+'K';
-
 
962
    return '₹'+n;
-
 
963
  }
-
 
964
 
887
  function val(id){ var e=byId(id); return e?e.value:''; }
965
  function val(id){ var e=byId(id); return e?e.value:''; }
888
  function submitLead(){ var fn=val('cRetailer').trim(), biz=val('cBusiness').trim(), loc=val('cLocation').trim();
966
  function submitLead(){ var fn=val('cRetailer').trim(), biz=val('cBusiness').trim();
889
    var phone=(val('cContact')||'').replace(/\D/g,'').slice(-10); var value=parseFloat((val('cValue')||'').replace(/[^0-9.]/g,''));
967
    var phone=(val('cContact')||'').replace(/\D/g,'').slice(-10);
-
 
968
    var brands=collectBrands(); var value=brandTotal();
890
    var region=val('cRegion'); var path=CREATE_PATH;
969
    var region=val('cRegion'); var path=CREATE_PATH;
-
 
970
    if(!fn){ toast('Retailer name is required'); return; }
891
    if(!fn){ toast('Retailer name is required'); return; } if(phone.length!==10){ toast('A valid 10-digit contact is required'); return; }
971
    if(phone.length!==10){ toast('A valid 10-digit contact is required'); return; }
892
    if(!region){ toast('Pick a region'); return; } if(path==='B' && !loc){ toast('Path B needs a location'); return; }
972
    if(!region){ toast('Pick a region'); return; }
-
 
973
    // No address check on either path any more: the capture link is what establishes location,
-
 
974
    // and it is generated for every lead regardless of path.
893
    var payload={ firstName:fn, lastName:'', mobile:phone, address:loc||'', city:'', state:'', source:'LMS Dashboard',
975
    var payload={ firstName:fn, mobile:phone, outletName:biz||'', source:'LMS Dashboard',
894
      remark:'Created from LMS dashboard', outletName:biz||'', regionId:parseInt(region,10), creationPath:path };
976
      regionId:parseInt(region,10), creationPath:path };
-
 
977
    // potential is the sum, so the funnel's value column keeps working, while the per-brand split
-
 
978
    // is what actually tells a BM what the counter is worth.
895
    if(value && value>0) payload.potential=value;
979
    if(value && value>0) payload.potential=value;
-
 
980
    if(brands.length) payload.leadBrands=brands;
-
 
981
    // /lms/createLead, NOT the shared /createLead: this one honours regionId and creationPath, so
-
 
982
    // the lead actually gets a stage, an LMS code, an owner and an SLA clock.
-
 
983
    fetch(CTX+'/lms/createLead',{method:'POST',credentials:'same-origin',
896
    fetch(CTX+'/createLead',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},body:JSON.stringify(payload)})
984
        headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},
-
 
985
        body:JSON.stringify(payload)})
897
      .then(function(r){ return r.text(); }).then(function(t){ t=(''+t).trim(); var id=parseInt(t,10);
986
      .then(function(r){ return r.text(); })
-
 
987
      .then(function(t){
-
 
988
        var d=null; try{ d=JSON.parse(t); }catch(e){ d=null; }
-
 
989
        var res=(d&&d.response)?d.response:d;
-
 
990
        if(!res || res.ok!==true){
-
 
991
          // A refusal carries the reason as a bare string under `response` (badRequest("...")),
-
 
992
          // whereas success is an object. Surface the reason, not a generic failure.
-
 
993
          toast((typeof res==='string' && res) || (res&&res.message) || 'Create failed');
-
 
994
          return;
-
 
995
        }
-
 
996
        var held=(res.assignmentStatus==='HOLD');
-
 
997
        toast(held ? ('Lead '+res.lmsCode+' created · no BM for that region, parked in HOLD')
898
        if(!isNaN(id) && String(id)===t){ toast('Lead created · auto-assigned · SLA started',1);
998
                   : ('Lead '+res.lmsCode+' created · assigned to '+(res.ownerName||'the region BM')+' · SLA started'), 1);
899
          ['cRetailer','cBusiness','cLocation','cContact','cValue','cBrands'].forEach(function(x){ byId(x).value=''; });
999
        ['cRetailer','cBusiness','cContact'].forEach(function(x){ var e=byId(x); if(e) e.value=''; });
-
 
1000
        document.querySelectorAll('#cBrandGrid .bval').forEach(function(el){ el.value=''; });
-
 
1001
        brandTotal();
-
 
1002
 
-
 
1003
        // Hand the capture link over immediately — the whole point of generating it on create is
-
 
1004
        // that the agent is still on the call with the retailer.
-
 
1005
        if(res.geoLink){
-
 
1006
          if(navigator.clipboard && navigator.clipboard.writeText){
-
 
1007
            navigator.clipboard.writeText(res.geoLink).then(function(){
-
 
1008
              alert('Capture link copied to your clipboard — send it to the retailer now:\n\n'+res.geoLink);
-
 
1009
            }, function(){ prompt('Send this capture link to the retailer:', res.geoLink); });
-
 
1010
          } else {
-
 
1011
            prompt('Send this capture link to the retailer:', res.geoLink);
-
 
1012
          }
-
 
1013
        }
-
 
1014
 
900
          byId('recLookup').value=id; injectRecord(id); go('record',document.querySelectorAll('.tab')[1]); refresh(); }
1015
        byId('recLookup').value=res.leadId; injectRecord(res.leadId);
901
        else { toast(t||'Lead created'); } })
1016
        go('record',document.querySelectorAll('.tab')[1]); refresh(); })
902
      .catch(function(e){ toast('Create failed'); }); }
1017
      .catch(function(e){ toast('Create failed'); }); }
903
 
1018
 
904
  // ====================== AUTO-ASSIGNMENT (real resolve per region) ======================
1019
  // ====================== AUTO-ASSIGNMENT (real resolve per region) ======================
905
  function buildAssign(){ var opts=byId('cRegion').querySelectorAll('option'); var rows=[]; var holdCount=0, assignedCount=0, pending=0;
1020
  function buildAssign(){ var opts=byId('cRegion').querySelectorAll('option'); var holdCount=0, assignedCount=0, pending=0;
906
    byId('assignMap').innerHTML='';
1021
    var body=byId('assignMap'); body.innerHTML='';
-
 
1022
    // Rows are created up-front in region order, then filled in as each resolve lands — otherwise
-
 
1023
    // the table would be ordered by whichever request happened to answer first.
907
    opts.forEach(function(o){ if(!o.value) return; pending++;
1024
    opts.forEach(function(o){ if(!o.value) return; pending++;
908
      var id=o.value, label=o.textContent;
1025
      var label=o.textContent;
-
 
1026
      var tr=document.createElement('tr');
-
 
1027
      tr.innerHTML='<td><b>'+esc(label)+'</b></td><td colspan="2"><span class="ph">Resolving…</span></td>';
-
 
1028
      body.appendChild(tr);
-
 
1029
      function done(){ if(--pending===0){ renderAssignKpis(assignedCount,holdCount); } }
909
      getJSON('/lms/resolve-region?regionId='+encodeURIComponent(id),function(d){ d=d||{}; pending--;
1030
      getJSON('/lms/resolve-region?regionId='+encodeURIComponent(o.value),function(d){ d=d||{};
910
        var hold=(d.assignmentStatus==='HOLD' || !d.bmName);
1031
        var hold=(d.assignmentStatus==='HOLD' || !d.bmName);
911
        if(hold) holdCount++; else assignedCount++;
1032
        if(hold) holdCount++; else assignedCount++;
912
        var tr=document.createElement('tr');
-
 
913
        tr.innerHTML='<td><b>'+esc(label)+'</b></td>'+
1033
        tr.innerHTML='<td><b>'+esc(label)+'</b></td>'+
914
          '<td>'+(hold?'<span class="warnflag">— vacant —</span>':esc(d.bmName))+'</td>'+
1034
          '<td>'+(hold?'<span class="warnflag">— vacant —</span>':esc(d.bmName))+'</td>'+
915
          '<td>'+(hold?'<span class="stage st-ni">HOLD</span>':'<span class="stage st-active">Active</span>')+'</td>';
1035
          '<td>'+(hold?'<span class="stage st-ni">HOLD</span>':'<span class="stage st-active">Active</span>')+'</td>';
916
        byId('assignMap').appendChild(tr);
1036
        done();
917
        if(pending===0){ renderAssignKpis(assignedCount,holdCount); } }); });
1037
      },function(){ tr.innerHTML='<td><b>'+esc(label)+'</b></td><td colspan="2"><span class="warnflag">could not resolve</span></td>';
-
 
1038
        done(); }); });
918
    // HOLD queue leads
1039
    // HOLD queue leads
919
    getJSON('/lms/dashboard/leads?bucketType=ATTENTION&bucketKey=HOLD',function(d){ var leads=(d&&d.leads)?d.leads:[];
1040
    getJSON('/lms/dashboard/leads?bucketType=ATTENTION&bucketKey=HOLD',function(d){ var leads=(d&&d.leads)?d.leads:[];
920
      if(!leads.length){ byId('holdQueue').innerHTML='<div class="ph">No leads in HOLD. 🎉</div>'; return; }
1041
      if(!leads.length){ byId('holdQueue').innerHTML='<div class="ph">No leads in HOLD. 🎉</div>'; return; }
921
      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+'">'+
1042
      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+'">'+
922
        '<span class="idpill">'+esc(l.lmsCode)+'</span> <span>'+esc(l.name||l.outlet||'—')+'</span></div>'; });
1043
        '<span class="idpill">'+esc(l.lmsCode)+'</span> <span>'+esc(l.name||l.outlet||'—')+'</span></div>'; });