Blame | Last modification | View Log | RSS feed
/* ============================================================================LMS dashboards front-end — shared behaviour for Command Center + Dashboards.- menu click handlers load the page fragments into #main-content- window.LmsDash exposes render/refresh/drill helpers the .vm fragments call- charts use the app's existing Chart.js global (Chart1); heat-map / tiles / SLAbars are CSS (see lms-dashboard.css). Drill-downs open a right-side drawer.Backed by /lms/dashboard/* JSON endpoints (LmsDashboardController).============================================================================ */(function () {'use strict';function ctx() { return (typeof context !== 'undefined' && context) ? context : ''; }function api(path) { return ctx() + path; }// responseSender.ok(...) wraps the payload under `.response`.function unwrap(r) {var o = (typeof r === 'string') ? JSON.parse(r) : r;return (o && o.response !== undefined) ? o.response : o;}function esc(s) {if (s === null || s === undefined) return '';return ('' + s).replace(/[&<>"]/g, function (c) {return { '&': '&', '<': '<', '>': '>', '"': '"' }[c];});}function fmtInt(n) {if (n === null || n === undefined) return '0';return ('' + n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');}function fmtInr(v) {if (v === null || v === undefined || v === '' || isNaN(v)) return '—';v = Number(v);if (v >= 10000000) return '₹' + (v / 10000000).toFixed(1) + ' Cr';if (v >= 100000) return '₹' + (v / 100000).toFixed(1) + ' L';if (v >= 1000) return '₹' + Math.round(v / 1000) + 'K';return '₹' + Math.round(v);}function pctClass(p) { return p >= 70 ? 'pct-good' : (p >= 45 ? 'pct-warn' : 'pct-bad'); }function slaFill(state) {switch (state) {case 'MET': return { w: 100, c: '#2f9e44' };case 'RUNNING': return { w: 55, c: '#1f2a5e' };case 'AT_RISK': return { w: 85, c: '#f08c00' };case 'BREACHED': return { w: 100, c: '#e03131' };default: return { w: 0, c: '#c9d0dd' };}}function toast(msg) {if (typeof showToast === 'function') { showToast(msg); return; }alert(msg);}// ---- drill-down drawer (one shared instance) -------------------------------function ensureDrawer() {if (document.getElementById('lmsDashDrawer')) return;var ov = document.createElement('div');ov.className = 'lms-dash-ov';ov.id = 'lmsDashOverlay';var dr = document.createElement('div');dr.className = 'lms-dash-drawer';dr.id = 'lmsDashDrawer';dr.innerHTML ='<div class="d-head" style="position:relative;">' +' <button class="d-close" id="lmsDashDrawerClose">×</button>' +' <div class="d-crumb" id="lmsDashDrawerCrumb"></div>' +' <div class="d-title" id="lmsDashDrawerTitle"></div>' +' <div class="d-sub" id="lmsDashDrawerSub"></div>' +'</div>' +'<div class="d-body" id="lmsDashDrawerBody"></div>';document.body.appendChild(ov);document.body.appendChild(dr);ov.addEventListener('click', closeDrawer);document.getElementById('lmsDashDrawerClose').addEventListener('click', closeDrawer);document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeDrawer(); });}function openDrawer(crumb, title, sub, bodyHtml) {ensureDrawer();document.getElementById('lmsDashDrawerCrumb').textContent = crumb || '';document.getElementById('lmsDashDrawerTitle').textContent = title || '';document.getElementById('lmsDashDrawerSub').textContent = sub || '';document.getElementById('lmsDashDrawerBody').innerHTML = bodyHtml || '';document.getElementById('lmsDashOverlay').classList.add('open');document.getElementById('lmsDashDrawer').classList.add('open');}function closeDrawer() {var ov = document.getElementById('lmsDashOverlay');var dr = document.getElementById('lmsDashDrawer');if (ov) ov.classList.remove('open');if (dr) dr.classList.remove('open');}function leadRows(leads) {if (!leads || !leads.length) return '<div class="d-empty">No leads in this bucket.</div>';var h = '<table class="lms-tbl"><thead><tr>' +'<th>LMS ID</th><th>Business / Retailer</th><th>Stage</th><th>Owner</th><th>SLA</th><th>Value</th>' +'</tr></thead><tbody>';leads.forEach(function (l) {var f = slaFill(l.slaState);h += '<tr class="row-click" data-leadid="' + l.id + '">' +'<td><span class="idpill">' + esc(l.lmsCode) + '</span></td>' +'<td>' + esc(l.name || '—') + (l.outlet ? ' <span style="color:#97a1b5">· ' + esc(l.outlet) + '</span>' : '') + '</td>' +'<td>' + stagePill(l.stage, l.stageLabel) + '</td>' +'<td>' + esc(l.owner) + '</td>' +'<td><span class="sla-wrap"><span class="slabar"><i style="width:' + f.w + '%;background:' + f.c + '"></i></span>' +'<span class="sla-clock sla-' + esc(l.slaState) + '">' + esc(l.slaState || 'NA') + '</span></span></td>' +'<td>' + fmtInr(l.value) + '</td>' +'</tr>';});h += '</tbody></table>';return h;}function stagePill(stage, label) {if (!stage) return '<span class="stage st-NEW">—</span>';return '<span class="stage st-' + esc(stage) + '">' + esc(label || stage) + '</span>';}// Fetch a bucket's leads and open the drawer.function drill(crumb, title, sub, bucketType, bucketKey) {var q = '?bucketType=' + encodeURIComponent(bucketType) +(bucketKey ? '&bucketKey=' + encodeURIComponent(bucketKey) : '') +filterQS('&');openDrawer(crumb, title, 'Loading…', '<div class="d-empty">Loading…</div>');doGetAjaxRequestHandler(api('/lms/dashboard/leads' + q), function (resp) {var data = unwrap(resp);var leads = (data && data.leads) ? data.leads : [];openDrawer(crumb, title, (sub || '') + ' · ' + leads.length + ' lead(s)', leadRows(leads));});}// ---- shared filter state ----------------------------------------------------var STATE = { regionId: '', from: '', to: '' };function readFilters() {var reg = document.getElementById('lmsFilterRegion');var fr = document.getElementById('lmsFilterFrom');var to = document.getElementById('lmsFilterTo');if (reg) STATE.regionId = reg.value || '';if (fr) STATE.from = fr.value || '';if (to) STATE.to = to.value || '';}function filterQS(sep) {var q = '';if (STATE.regionId) q += (sep || '?') + 'regionId=' + encodeURIComponent(STATE.regionId);if (STATE.from) q += (q ? '&' : (sep || '?')) + 'from=' + encodeURIComponent(STATE.from);if (STATE.to) q += (q ? '&' : (sep || '?')) + 'to=' + encodeURIComponent(STATE.to);return q;}// ---- charts (Chart1 = legacy Chart.js global) ------------------------------var CHARTS = {};function destroyChart(id) {if (CHARTS[id]) { try { CHARTS[id].destroy(); } catch (e) { } delete CHARTS[id]; }}function barChart(canvasId, labels, values, colors, horizontal, onIndexClick) {var cv = document.getElementById(canvasId);if (!cv || typeof Chart1 === 'undefined') return;destroyChart(canvasId);var chart = new Chart1(cv.getContext('2d'), {type: horizontal ? 'horizontalBar' : 'bar',data: { labels: labels, datasets: [{ data: values, backgroundColor: colors, borderWidth: 0, barPercentage: 0.72 }] },options: {responsive: true, maintainAspectRatio: false,legend: { display: false },tooltips: { enabled: true },scales: {xAxes: [{ ticks: { beginAtZero: true, precision: 0, fontColor: '#97a1b5' }, gridLines: { color: '#eef1f6' } }],yAxes: [{ ticks: { fontColor: '#69748c' }, gridLines: { color: '#eef1f6' } }]}}});CHARTS[canvasId] = chart;if (onIndexClick) {cv.onclick = function (evt) {var pts = chart.getElementsAtEvent(evt);if (pts && pts.length) onIndexClick(pts[0]._index);};}return chart;}// ======================= COMMAND CENTER ==================================function renderSummary(data) {// KPI tilesvar kh = '';(data.kpis || []).forEach(function (k) {kh += '<div class="lms-kpi tone-' + esc(k.tone) + '" data-kpikey="' + esc(k.key) + '">' +'<div class="k-label">' + esc(k.label) + '</div>' +'<div class="k-value">' + fmtInt(k.value) + '</div>' +'<div class="k-foot">' + esc(k.foot) + '</div></div>';});setHtml('lmsKpis', kh);// Funnel — Chart1 horizontal bar + clickablevar f = data.funnel || [];var labels = f.map(function (s) { return s.label; });var vals = f.map(function (s) { return s.count; });var cols = f.map(function () { return '#3b5bdb'; });barChart('lmsFunnelChart', labels, vals, cols, true, function (i) {var step = f[i];drill('Lifecycle funnel', step.label, 'Reached ' + step.label + ' (' + step.pct + '%)',step.stage === 'ALL' ? 'ALL' : 'STAGE', step.stage === 'ALL' ? '' : step.stage);});// Attention queuevar ah = '';(data.attention || []).forEach(function (a) {ah += '<div class="att tone-' + esc(a.tone) + '" data-attkey="' + esc(a.key) + '" data-atttitle="' + esc(a.title) + '">' +'<span class="a-dot"></span>' +'<span class="a-body"><span class="a-title">' + esc(a.title) + '</span><br>' +'<span class="a-detail">' + esc(a.detail) + '</span></span>' +'<span class="a-count">' + fmtInt(a.count) + '</span></div>';});setHtml('lmsAttention', ah);}function loadPipeline() {var box = document.getElementById('lmsPipeline');if (!box) return;box.innerHTML = '<div class="lms-ph">Loading pipeline…</div>';doGetAjaxRequestHandler(api('/lms/dashboard/leads?bucketType=ALL' + filterQS('&')), function (resp) {var data = unwrap(resp);var leads = (data && data.leads) ? data.leads : [];box.innerHTML = leadRows(leads);});}function refreshCommandCenter() {readFilters();doGetAjaxRequestHandler(api('/lms/dashboard/summary' + filterQS('?')), function (resp) {renderSummary(unwrap(resp));});loadPipeline();}function kpiBucket(key) {switch (key) {case 'new': return { t: 'STAGE', k: 'NEW' };case 'awaiting': return { t: 'STAGE', k: 'ASSIGNED' };case 'sla': return { t: 'ATTENTION', k: 'BREACHED_NO_CONTACT' };case 'contacted': return { t: 'STAGE', k: 'CONTACTED' };case 'qualified': return { t: 'STAGE', k: 'QUALIFIED' };case 'onboarded': return { t: 'STAGE', k: 'ONBOARDED' };default: return { t: 'ALL', k: '' };}}// ======================= DASHBOARDS ======================================function renderStateFunnel(data) {var rows = (data && data.rows) ? data.rows : [];var h = '<table class="lms-tbl"><thead><tr>' +'<th>Region</th><th>Leads</th><th>Contacted</th><th>Qualified</th><th>Visited</th><th>Onboarded</th><th>Conv%</th><th>SLA%</th>' +'</tr></thead><tbody>';rows.forEach(function (r) {var rc = r.convPct < 20 ? 'row-bad' : (r.slaPct < 60 ? 'row-warn' : '');h += '<tr class="row-click ' + rc + '" data-region="' + esc(r.regionCode) + '" data-regionname="' + esc(r.regionName) + '">' +'<td><b>' + esc(r.regionName) + '</b> <span class="idpill">' + esc(r.regionCode) + '</span></td>' +'<td>' + fmtInt(r.newCount) + '</td>' +'<td>' + fmtInt(r.contacted) + '</td>' +'<td>' + fmtInt(r.qualified) + '</td>' +'<td>' + fmtInt(r.visited) + '</td>' +'<td>' + fmtInt(r.onboarded) + '</td>' +'<td><span class="pctchip ' + pctClass(r.convPct) + '">' + r.convPct + '%</span></td>' +'<td><span class="pctchip ' + pctClass(r.slaPct) + '">' + r.slaPct + '%</span></td>' +'</tr>';});h += '</tbody></table>';setHtml('lmsStateFunnel', h);// Conversion-by-region Chart1 bar.barChart('lmsConvChart',rows.map(function (r) { return r.regionCode; }),rows.map(function (r) { return r.convPct; }),rows.map(function (r) { return r.convPct < 20 ? '#e03131' : (r.convPct < 40 ? '#f59f00' : '#2f9e44'); }),false,function (i) {var r = rows[i];drill('State funnel', r.regionName, 'Region ' + r.regionCode, 'REGION', r.regionCode);});// Dashboards KPIs derived from summary + state + ageing (set by initDashboards).buildDashKpis(rows);}var DASH = { summary: null, ageing: null };function buildDashKpis(stateRows) {if (!DASH.summary) return;var s = DASH.summary;var contacted = 0;(s.funnel || []).forEach(function (f) { if (f.stage === 'CONTACTED') contacted = f.count; });// Weighted SLA% across regions.var totLeads = 0, slaAcc = 0;(stateRows || []).forEach(function (r) { totLeads += r.newCount; slaAcc += r.slaPct * r.newCount; });var slaPct = totLeads > 0 ? Math.round(slaAcc / totLeads) : 0;var days = 90;var kpis = [{ label: 'LMS created', value: fmtInt(s.total), foot: 'in period', tone: 'navy' },{ label: 'Conversion', value: s.conversionPct + '%', foot: 'onboarded / created', tone: 'good' },{ label: 'SLA compliance', value: slaPct + '%', foot: 'first contact in SLA', tone: slaPct < 60 ? 'red' : 'navy' },{ label: 'Calls/day (BGC)', value: fmtInt(Math.round(contacted / days)), foot: 'proxy · contacted ÷ days', tone: 'amber' },{ label: 'Ageing > 7 days', value: DASH.ageing ? fmtInt(DASH.ageing.total) : '—', foot: 'stuck open leads', tone: 'red' },{ label: 'Onboarded value', value: fmtInr(s.onboardedValue), foot: 'sum of potential', tone: 'good' }];var kh = '';kpis.forEach(function (k) {kh += '<div class="lms-kpi tone-' + k.tone + '">' +'<div class="k-label">' + esc(k.label) + '</div>' +'<div class="k-value">' + k.value + '</div>' +'<div class="k-foot">' + esc(k.foot) + '</div></div>';});setHtml('lmsDashKpis', kh);}function renderHeatmap(data) {var weeks = (data && data.weeks) ? data.weeks : [];var bms = (data && data.bms) ? data.bms : [];if (!bms.length) { setHtml('lmsHeatmap', '<div class="lms-ph">No SLA data for the selected period.</div>'); return; }var h = '<table class="heat"><thead><tr><th></th>';weeks.forEach(function (w) { h += '<th>' + esc(w) + '</th>'; });h += '</tr></thead><tbody>';bms.forEach(function (bm) {h += '<tr><td class="bm">' + esc(bm.bmName) + '</td>';(bm.cells || []).forEach(function (c) {if (c.pct === null || c.pct === undefined) {h += '<td class="cell empty">–</td>';} else {h += '<td class="cell" style="background:' + heatColor(c.pct) + '">' + c.pct + '</td>';}});h += '</tr>';});h += '</tbody></table>';setHtml('lmsHeatmap', h);}function heatColor(p) {if (p >= 90) return '#2f9e44';if (p >= 75) return '#82c91e';if (p >= 60) return '#f59f00';if (p >= 40) return '#e8590c';return '#e03131';}function renderAgeing(data) {DASH.ageing = data;var rows = (data && data.rows) ? data.rows : [];if (!rows.length) { setHtml('lmsAgeing', '<div class="lms-ph">No ageing leads. 🎉</div>'); return; }var h = '<table class="lms-tbl"><thead><tr><th>Stuck at stage</th><th>Leads > ' + (data.days || 7) + 'd</th></tr></thead><tbody>';rows.forEach(function (r) {h += '<tr class="row-click" data-agestage="' + esc(r.stage) + '">' +'<td>' + stagePill(r.stage, r.label) + '</td>' +'<td><b>' + fmtInt(r.count) + '</b></td></tr>';});h += '</tbody></table>';setHtml('lmsAgeing', h);}function refreshDashboards() {readFilters();doGetAjaxRequestHandler(api('/lms/dashboard/summary' + filterQS('?')), function (resp) {DASH.summary = unwrap(resp);// re-run KPI build after state loads; also refresh command-style summary if presentif (document.getElementById('lmsStateFunnel')) fetchState();});doGetAjaxRequestHandler(api('/lms/dashboard/sla-heatmap' + filterQS('?')), function (resp) { renderHeatmap(unwrap(resp)); });doGetAjaxRequestHandler(api('/lms/dashboard/ageing'), function (resp) { renderAgeing(unwrap(resp)); });}function fetchState() {doGetAjaxRequestHandler(api('/lms/dashboard/state-funnel' + filterQS('?')), function (resp) { renderStateFunnel(unwrap(resp)); });}// ---- DOM helpers ------------------------------------------------------------function setHtml(id, html) { var el = document.getElementById(id); if (el) el.innerHTML = html; }// ---- delegated clicks (drill) ----------------------------------------------$(document).off('click.lmsdash');$(document).on('click.lmsdash', '.lms-dash .lms-kpi[data-kpikey]', function () {var key = $(this).data('kpikey');var b = kpiBucket(key);drill('Command Center', $(this).find('.k-label').text(), 'KPI drill', b.t, b.k);});$(document).on('click.lmsdash', '.lms-dash .att[data-attkey]', function () {drill('Needs attention', $(this).data('atttitle'), 'Attention queue', 'ATTENTION', $(this).data('attkey'));});$(document).on('click.lmsdash', '.lms-dash table.lms-tbl tr.row-click[data-leadid]', function () {var id = $(this).data('leadid');closeDrawer();doGetAjaxRequestHandler(api('/leadRecord?leadId=' + id), function (r) { $('#main-content').html(r); });});$(document).on('click.lmsdash', '.lms-dash tr.row-click[data-region]', function () {drill('State funnel', $(this).data('regionname'), 'Region ' + $(this).data('region'), 'REGION', $(this).data('region'));});$(document).on('click.lmsdash', '.lms-dash tr.row-click[data-agestage]', function () {drill('Ageing', $(this).find('.stage').text(), 'Stuck leads', 'STAGE', $(this).data('agestage'));});// Filter changes.$(document).on('change.lmsdash', '#lmsFilterRegion, #lmsFilterFrom, #lmsFilterTo', function () {if (document.getElementById('lmsKpis') && document.getElementById('lmsPipeline')) {refreshCommandCenter();} else {refreshDashboards();}});// ---- public API the .vm fragments call --------------------------------------window.LmsDash = {initCommandCenter: function (cfg) {cfg = cfg || {};STATE.regionId = cfg.regionId || '';STATE.from = cfg.from || '';STATE.to = cfg.to || '';ensureDrawer();if (cfg.summary) renderSummary(cfg.summary);loadPipeline();},initDashboards: function (cfg) {cfg = cfg || {};STATE.regionId = cfg.regionId || '';STATE.from = cfg.from || '';STATE.to = cfg.to || '';ensureDrawer();DASH.summary = cfg.summary || null;DASH.ageing = cfg.ageing || null;if (cfg.stateFunnel) renderStateFunnel(cfg.stateFunnel);if (cfg.slaHeatmap) renderHeatmap(cfg.slaHeatmap);if (cfg.ageing) renderAgeing(cfg.ageing);},refreshCommandCenter: refreshCommandCenter,refreshDashboards: refreshDashboards};// ---- sidebar menu entry points ----------------------------------------------// The LMS console is a full standalone page (own top-bar + 6 tabs, sts-mockup UI). To keep// the app's left sidebar + header visible, embed it in an iframe inside #main-content// (style/script isolated), deep-linked to the relevant tab.function openLmsConsole(hash) {var url = api('/lms/dashboard') + (hash || '');$('#main-content').html('<iframe title="LMS Console" src="' + url + '" ' +'style="width:100%;height:calc(100vh - 92px);min-height:620px;border:0;display:block"></iframe>');}$(document).on('click', '.lms-command-center', function (e) {if (e && e.preventDefault) e.preventDefault();openLmsConsole('#cmd');});$(document).on('click', '.lms-dashboards', function (e) {if (e && e.preventDefault) e.preventDefault();openLmsConsole('#dash');});})();