| 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 & Audit</button>
|
417 |
<button class="tab" data-screen="audit" onclick="go('audit',this)">Recordings & 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 → BM/RSM mapping</h2>
|
647 |
<h2>Region → 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">—</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+='®ionId='+encodeURIComponent(STATE.regionId); }
|
1215 |
if(STATE.regionId) p+='®ionId='+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+='®ionId='+encodeURIComponent(STATE.regionId); }
|
2109 |
if(PIPE.q){ p+='&q='+encodeURIComponent(PIPE.q); if(STATE.regionId) p+='®ionId='+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> · '+(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>
|