| 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+='®ionId='+encodeURIComponent(STATE.regionId);
|
630 |
function qs(){ var q=''; if(STATE.regionId)q+='®ionId='+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 →</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 →</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 → <b style="color:#e03131">HOLD queue</b>, Sales Head notified.'; }
|
924 |
byId('raAv').style.background='#e03131'; byId('rEsc').innerHTML='Region has no active owner → <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>'; });
|