| 37125 |
vikas |
1 |
/* ============================================================================
|
|
|
2 |
LMS dashboards front-end — shared behaviour for Command Center + Dashboards.
|
|
|
3 |
- menu click handlers load the page fragments into #main-content
|
|
|
4 |
- window.LmsDash exposes render/refresh/drill helpers the .vm fragments call
|
|
|
5 |
- charts use the app's existing Chart.js global (Chart1); heat-map / tiles / SLA
|
|
|
6 |
bars are CSS (see lms-dashboard.css). Drill-downs open a right-side drawer.
|
|
|
7 |
Backed by /lms/dashboard/* JSON endpoints (LmsDashboardController).
|
|
|
8 |
============================================================================ */
|
|
|
9 |
(function () {
|
|
|
10 |
'use strict';
|
|
|
11 |
|
|
|
12 |
function ctx() { return (typeof context !== 'undefined' && context) ? context : ''; }
|
|
|
13 |
function api(path) { return ctx() + path; }
|
|
|
14 |
|
|
|
15 |
// responseSender.ok(...) wraps the payload under `.response`.
|
|
|
16 |
function unwrap(r) {
|
|
|
17 |
var o = (typeof r === 'string') ? JSON.parse(r) : r;
|
|
|
18 |
return (o && o.response !== undefined) ? o.response : o;
|
|
|
19 |
}
|
|
|
20 |
|
|
|
21 |
function esc(s) {
|
|
|
22 |
if (s === null || s === undefined) return '';
|
|
|
23 |
return ('' + s).replace(/[&<>"]/g, function (c) {
|
|
|
24 |
return { '&': '&', '<': '<', '>': '>', '"': '"' }[c];
|
|
|
25 |
});
|
|
|
26 |
}
|
|
|
27 |
|
|
|
28 |
function fmtInt(n) {
|
|
|
29 |
if (n === null || n === undefined) return '0';
|
|
|
30 |
return ('' + n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
|
31 |
}
|
|
|
32 |
|
|
|
33 |
function fmtInr(v) {
|
|
|
34 |
if (v === null || v === undefined || v === '' || isNaN(v)) return '—';
|
|
|
35 |
v = Number(v);
|
|
|
36 |
if (v >= 10000000) return '₹' + (v / 10000000).toFixed(1) + ' Cr';
|
|
|
37 |
if (v >= 100000) return '₹' + (v / 100000).toFixed(1) + ' L';
|
|
|
38 |
if (v >= 1000) return '₹' + Math.round(v / 1000) + 'K';
|
|
|
39 |
return '₹' + Math.round(v);
|
|
|
40 |
}
|
|
|
41 |
|
|
|
42 |
function pctClass(p) { return p >= 70 ? 'pct-good' : (p >= 45 ? 'pct-warn' : 'pct-bad'); }
|
|
|
43 |
|
|
|
44 |
function slaFill(state) {
|
|
|
45 |
switch (state) {
|
|
|
46 |
case 'MET': return { w: 100, c: '#2f9e44' };
|
|
|
47 |
case 'RUNNING': return { w: 55, c: '#1f2a5e' };
|
|
|
48 |
case 'AT_RISK': return { w: 85, c: '#f08c00' };
|
|
|
49 |
case 'BREACHED': return { w: 100, c: '#e03131' };
|
|
|
50 |
default: return { w: 0, c: '#c9d0dd' };
|
|
|
51 |
}
|
|
|
52 |
}
|
|
|
53 |
|
|
|
54 |
function toast(msg) {
|
|
|
55 |
if (typeof showToast === 'function') { showToast(msg); return; }
|
|
|
56 |
alert(msg);
|
|
|
57 |
}
|
|
|
58 |
|
|
|
59 |
// ---- drill-down drawer (one shared instance) -------------------------------
|
|
|
60 |
function ensureDrawer() {
|
|
|
61 |
if (document.getElementById('lmsDashDrawer')) return;
|
|
|
62 |
var ov = document.createElement('div');
|
|
|
63 |
ov.className = 'lms-dash-ov';
|
|
|
64 |
ov.id = 'lmsDashOverlay';
|
|
|
65 |
var dr = document.createElement('div');
|
|
|
66 |
dr.className = 'lms-dash-drawer';
|
|
|
67 |
dr.id = 'lmsDashDrawer';
|
|
|
68 |
dr.innerHTML =
|
|
|
69 |
'<div class="d-head" style="position:relative;">' +
|
|
|
70 |
' <button class="d-close" id="lmsDashDrawerClose">×</button>' +
|
|
|
71 |
' <div class="d-crumb" id="lmsDashDrawerCrumb"></div>' +
|
|
|
72 |
' <div class="d-title" id="lmsDashDrawerTitle"></div>' +
|
|
|
73 |
' <div class="d-sub" id="lmsDashDrawerSub"></div>' +
|
|
|
74 |
'</div>' +
|
|
|
75 |
'<div class="d-body" id="lmsDashDrawerBody"></div>';
|
|
|
76 |
document.body.appendChild(ov);
|
|
|
77 |
document.body.appendChild(dr);
|
|
|
78 |
ov.addEventListener('click', closeDrawer);
|
|
|
79 |
document.getElementById('lmsDashDrawerClose').addEventListener('click', closeDrawer);
|
|
|
80 |
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeDrawer(); });
|
|
|
81 |
}
|
|
|
82 |
|
|
|
83 |
function openDrawer(crumb, title, sub, bodyHtml) {
|
|
|
84 |
ensureDrawer();
|
|
|
85 |
document.getElementById('lmsDashDrawerCrumb').textContent = crumb || '';
|
|
|
86 |
document.getElementById('lmsDashDrawerTitle').textContent = title || '';
|
|
|
87 |
document.getElementById('lmsDashDrawerSub').textContent = sub || '';
|
|
|
88 |
document.getElementById('lmsDashDrawerBody').innerHTML = bodyHtml || '';
|
|
|
89 |
document.getElementById('lmsDashOverlay').classList.add('open');
|
|
|
90 |
document.getElementById('lmsDashDrawer').classList.add('open');
|
|
|
91 |
}
|
|
|
92 |
|
|
|
93 |
function closeDrawer() {
|
|
|
94 |
var ov = document.getElementById('lmsDashOverlay');
|
|
|
95 |
var dr = document.getElementById('lmsDashDrawer');
|
|
|
96 |
if (ov) ov.classList.remove('open');
|
|
|
97 |
if (dr) dr.classList.remove('open');
|
|
|
98 |
}
|
|
|
99 |
|
|
|
100 |
function leadRows(leads) {
|
|
|
101 |
if (!leads || !leads.length) return '<div class="d-empty">No leads in this bucket.</div>';
|
|
|
102 |
var h = '<table class="lms-tbl"><thead><tr>' +
|
|
|
103 |
'<th>LMS ID</th><th>Business / Retailer</th><th>Stage</th><th>Owner</th><th>SLA</th><th>Value</th>' +
|
|
|
104 |
'</tr></thead><tbody>';
|
|
|
105 |
leads.forEach(function (l) {
|
|
|
106 |
var f = slaFill(l.slaState);
|
|
|
107 |
h += '<tr class="row-click" data-leadid="' + l.id + '">' +
|
|
|
108 |
'<td><span class="idpill">' + esc(l.lmsCode) + '</span></td>' +
|
|
|
109 |
'<td>' + esc(l.name || '—') + (l.outlet ? ' <span style="color:#97a1b5">· ' + esc(l.outlet) + '</span>' : '') + '</td>' +
|
|
|
110 |
'<td>' + stagePill(l.stage, l.stageLabel) + '</td>' +
|
|
|
111 |
'<td>' + esc(l.owner) + '</td>' +
|
|
|
112 |
'<td><span class="sla-wrap"><span class="slabar"><i style="width:' + f.w + '%;background:' + f.c + '"></i></span>' +
|
|
|
113 |
'<span class="sla-clock sla-' + esc(l.slaState) + '">' + esc(l.slaState || 'NA') + '</span></span></td>' +
|
|
|
114 |
'<td>' + fmtInr(l.value) + '</td>' +
|
|
|
115 |
'</tr>';
|
|
|
116 |
});
|
|
|
117 |
h += '</tbody></table>';
|
|
|
118 |
return h;
|
|
|
119 |
}
|
|
|
120 |
|
|
|
121 |
function stagePill(stage, label) {
|
|
|
122 |
if (!stage) return '<span class="stage st-NEW">—</span>';
|
|
|
123 |
return '<span class="stage st-' + esc(stage) + '">' + esc(label || stage) + '</span>';
|
|
|
124 |
}
|
|
|
125 |
|
|
|
126 |
// Fetch a bucket's leads and open the drawer.
|
|
|
127 |
function drill(crumb, title, sub, bucketType, bucketKey) {
|
|
|
128 |
var q = '?bucketType=' + encodeURIComponent(bucketType) +
|
|
|
129 |
(bucketKey ? '&bucketKey=' + encodeURIComponent(bucketKey) : '') +
|
|
|
130 |
filterQS('&');
|
|
|
131 |
openDrawer(crumb, title, 'Loading…', '<div class="d-empty">Loading…</div>');
|
|
|
132 |
doGetAjaxRequestHandler(api('/lms/dashboard/leads' + q), function (resp) {
|
|
|
133 |
var data = unwrap(resp);
|
|
|
134 |
var leads = (data && data.leads) ? data.leads : [];
|
|
|
135 |
openDrawer(crumb, title, (sub || '') + ' · ' + leads.length + ' lead(s)', leadRows(leads));
|
|
|
136 |
});
|
|
|
137 |
}
|
|
|
138 |
|
|
|
139 |
// ---- shared filter state ----------------------------------------------------
|
|
|
140 |
var STATE = { regionId: '', from: '', to: '' };
|
|
|
141 |
|
|
|
142 |
function readFilters() {
|
|
|
143 |
var reg = document.getElementById('lmsFilterRegion');
|
|
|
144 |
var fr = document.getElementById('lmsFilterFrom');
|
|
|
145 |
var to = document.getElementById('lmsFilterTo');
|
|
|
146 |
if (reg) STATE.regionId = reg.value || '';
|
|
|
147 |
if (fr) STATE.from = fr.value || '';
|
|
|
148 |
if (to) STATE.to = to.value || '';
|
|
|
149 |
}
|
|
|
150 |
|
|
|
151 |
function filterQS(sep) {
|
|
|
152 |
var q = '';
|
|
|
153 |
if (STATE.regionId) q += (sep || '?') + 'regionId=' + encodeURIComponent(STATE.regionId);
|
|
|
154 |
if (STATE.from) q += (q ? '&' : (sep || '?')) + 'from=' + encodeURIComponent(STATE.from);
|
|
|
155 |
if (STATE.to) q += (q ? '&' : (sep || '?')) + 'to=' + encodeURIComponent(STATE.to);
|
|
|
156 |
return q;
|
|
|
157 |
}
|
|
|
158 |
|
|
|
159 |
// ---- charts (Chart1 = legacy Chart.js global) ------------------------------
|
|
|
160 |
var CHARTS = {};
|
|
|
161 |
|
|
|
162 |
function destroyChart(id) {
|
|
|
163 |
if (CHARTS[id]) { try { CHARTS[id].destroy(); } catch (e) { } delete CHARTS[id]; }
|
|
|
164 |
}
|
|
|
165 |
|
|
|
166 |
function barChart(canvasId, labels, values, colors, horizontal, onIndexClick) {
|
|
|
167 |
var cv = document.getElementById(canvasId);
|
|
|
168 |
if (!cv || typeof Chart1 === 'undefined') return;
|
|
|
169 |
destroyChart(canvasId);
|
|
|
170 |
var chart = new Chart1(cv.getContext('2d'), {
|
|
|
171 |
type: horizontal ? 'horizontalBar' : 'bar',
|
|
|
172 |
data: { labels: labels, datasets: [{ data: values, backgroundColor: colors, borderWidth: 0, barPercentage: 0.72 }] },
|
|
|
173 |
options: {
|
|
|
174 |
responsive: true, maintainAspectRatio: false,
|
|
|
175 |
legend: { display: false },
|
|
|
176 |
tooltips: { enabled: true },
|
|
|
177 |
scales: {
|
|
|
178 |
xAxes: [{ ticks: { beginAtZero: true, precision: 0, fontColor: '#97a1b5' }, gridLines: { color: '#eef1f6' } }],
|
|
|
179 |
yAxes: [{ ticks: { fontColor: '#69748c' }, gridLines: { color: '#eef1f6' } }]
|
|
|
180 |
}
|
|
|
181 |
}
|
|
|
182 |
});
|
|
|
183 |
CHARTS[canvasId] = chart;
|
|
|
184 |
if (onIndexClick) {
|
|
|
185 |
cv.onclick = function (evt) {
|
|
|
186 |
var pts = chart.getElementsAtEvent(evt);
|
|
|
187 |
if (pts && pts.length) onIndexClick(pts[0]._index);
|
|
|
188 |
};
|
|
|
189 |
}
|
|
|
190 |
return chart;
|
|
|
191 |
}
|
|
|
192 |
|
|
|
193 |
// ======================= COMMAND CENTER ==================================
|
|
|
194 |
function renderSummary(data) {
|
|
|
195 |
// KPI tiles
|
|
|
196 |
var kh = '';
|
|
|
197 |
(data.kpis || []).forEach(function (k) {
|
|
|
198 |
kh += '<div class="lms-kpi tone-' + esc(k.tone) + '" data-kpikey="' + esc(k.key) + '">' +
|
|
|
199 |
'<div class="k-label">' + esc(k.label) + '</div>' +
|
|
|
200 |
'<div class="k-value">' + fmtInt(k.value) + '</div>' +
|
|
|
201 |
'<div class="k-foot">' + esc(k.foot) + '</div></div>';
|
|
|
202 |
});
|
|
|
203 |
setHtml('lmsKpis', kh);
|
|
|
204 |
|
|
|
205 |
// Funnel — Chart1 horizontal bar + clickable
|
|
|
206 |
var f = data.funnel || [];
|
|
|
207 |
var labels = f.map(function (s) { return s.label; });
|
|
|
208 |
var vals = f.map(function (s) { return s.count; });
|
|
|
209 |
var cols = f.map(function () { return '#3b5bdb'; });
|
|
|
210 |
barChart('lmsFunnelChart', labels, vals, cols, true, function (i) {
|
|
|
211 |
var step = f[i];
|
|
|
212 |
drill('Lifecycle funnel', step.label, 'Reached ' + step.label + ' (' + step.pct + '%)',
|
|
|
213 |
step.stage === 'ALL' ? 'ALL' : 'STAGE', step.stage === 'ALL' ? '' : step.stage);
|
|
|
214 |
});
|
|
|
215 |
|
|
|
216 |
// Attention queue
|
|
|
217 |
var ah = '';
|
|
|
218 |
(data.attention || []).forEach(function (a) {
|
|
|
219 |
ah += '<div class="att tone-' + esc(a.tone) + '" data-attkey="' + esc(a.key) + '" data-atttitle="' + esc(a.title) + '">' +
|
|
|
220 |
'<span class="a-dot"></span>' +
|
|
|
221 |
'<span class="a-body"><span class="a-title">' + esc(a.title) + '</span><br>' +
|
|
|
222 |
'<span class="a-detail">' + esc(a.detail) + '</span></span>' +
|
|
|
223 |
'<span class="a-count">' + fmtInt(a.count) + '</span></div>';
|
|
|
224 |
});
|
|
|
225 |
setHtml('lmsAttention', ah);
|
|
|
226 |
}
|
|
|
227 |
|
|
|
228 |
function loadPipeline() {
|
|
|
229 |
var box = document.getElementById('lmsPipeline');
|
|
|
230 |
if (!box) return;
|
|
|
231 |
box.innerHTML = '<div class="lms-ph">Loading pipeline…</div>';
|
|
|
232 |
doGetAjaxRequestHandler(api('/lms/dashboard/leads?bucketType=ALL' + filterQS('&')), function (resp) {
|
|
|
233 |
var data = unwrap(resp);
|
|
|
234 |
var leads = (data && data.leads) ? data.leads : [];
|
|
|
235 |
box.innerHTML = leadRows(leads);
|
|
|
236 |
});
|
|
|
237 |
}
|
|
|
238 |
|
|
|
239 |
function refreshCommandCenter() {
|
|
|
240 |
readFilters();
|
|
|
241 |
doGetAjaxRequestHandler(api('/lms/dashboard/summary' + filterQS('?')), function (resp) {
|
|
|
242 |
renderSummary(unwrap(resp));
|
|
|
243 |
});
|
|
|
244 |
loadPipeline();
|
|
|
245 |
}
|
|
|
246 |
|
|
|
247 |
function kpiBucket(key) {
|
|
|
248 |
switch (key) {
|
|
|
249 |
case 'new': return { t: 'STAGE', k: 'NEW' };
|
|
|
250 |
case 'awaiting': return { t: 'STAGE', k: 'ASSIGNED' };
|
|
|
251 |
case 'sla': return { t: 'ATTENTION', k: 'BREACHED_NO_CONTACT' };
|
|
|
252 |
case 'contacted': return { t: 'STAGE', k: 'CONTACTED' };
|
|
|
253 |
case 'qualified': return { t: 'STAGE', k: 'QUALIFIED' };
|
|
|
254 |
case 'onboarded': return { t: 'STAGE', k: 'ONBOARDED' };
|
|
|
255 |
default: return { t: 'ALL', k: '' };
|
|
|
256 |
}
|
|
|
257 |
}
|
|
|
258 |
|
|
|
259 |
// ======================= DASHBOARDS ======================================
|
|
|
260 |
function renderStateFunnel(data) {
|
|
|
261 |
var rows = (data && data.rows) ? data.rows : [];
|
|
|
262 |
var h = '<table class="lms-tbl"><thead><tr>' +
|
|
|
263 |
'<th>Region</th><th>Leads</th><th>Contacted</th><th>Qualified</th><th>Visited</th><th>Onboarded</th><th>Conv%</th><th>SLA%</th>' +
|
|
|
264 |
'</tr></thead><tbody>';
|
|
|
265 |
rows.forEach(function (r) {
|
|
|
266 |
var rc = r.convPct < 20 ? 'row-bad' : (r.slaPct < 60 ? 'row-warn' : '');
|
|
|
267 |
h += '<tr class="row-click ' + rc + '" data-region="' + esc(r.regionCode) + '" data-regionname="' + esc(r.regionName) + '">' +
|
|
|
268 |
'<td><b>' + esc(r.regionName) + '</b> <span class="idpill">' + esc(r.regionCode) + '</span></td>' +
|
|
|
269 |
'<td>' + fmtInt(r.newCount) + '</td>' +
|
|
|
270 |
'<td>' + fmtInt(r.contacted) + '</td>' +
|
|
|
271 |
'<td>' + fmtInt(r.qualified) + '</td>' +
|
|
|
272 |
'<td>' + fmtInt(r.visited) + '</td>' +
|
|
|
273 |
'<td>' + fmtInt(r.onboarded) + '</td>' +
|
|
|
274 |
'<td><span class="pctchip ' + pctClass(r.convPct) + '">' + r.convPct + '%</span></td>' +
|
|
|
275 |
'<td><span class="pctchip ' + pctClass(r.slaPct) + '">' + r.slaPct + '%</span></td>' +
|
|
|
276 |
'</tr>';
|
|
|
277 |
});
|
|
|
278 |
h += '</tbody></table>';
|
|
|
279 |
setHtml('lmsStateFunnel', h);
|
|
|
280 |
|
|
|
281 |
// Conversion-by-region Chart1 bar.
|
|
|
282 |
barChart('lmsConvChart',
|
|
|
283 |
rows.map(function (r) { return r.regionCode; }),
|
|
|
284 |
rows.map(function (r) { return r.convPct; }),
|
|
|
285 |
rows.map(function (r) { return r.convPct < 20 ? '#e03131' : (r.convPct < 40 ? '#f59f00' : '#2f9e44'); }),
|
|
|
286 |
false,
|
|
|
287 |
function (i) {
|
|
|
288 |
var r = rows[i];
|
|
|
289 |
drill('State funnel', r.regionName, 'Region ' + r.regionCode, 'REGION', r.regionCode);
|
|
|
290 |
});
|
|
|
291 |
|
|
|
292 |
// Dashboards KPIs derived from summary + state + ageing (set by initDashboards).
|
|
|
293 |
buildDashKpis(rows);
|
|
|
294 |
}
|
|
|
295 |
|
|
|
296 |
var DASH = { summary: null, ageing: null };
|
|
|
297 |
|
|
|
298 |
function buildDashKpis(stateRows) {
|
|
|
299 |
if (!DASH.summary) return;
|
|
|
300 |
var s = DASH.summary;
|
|
|
301 |
var contacted = 0;
|
|
|
302 |
(s.funnel || []).forEach(function (f) { if (f.stage === 'CONTACTED') contacted = f.count; });
|
|
|
303 |
// Weighted SLA% across regions.
|
|
|
304 |
var totLeads = 0, slaAcc = 0;
|
|
|
305 |
(stateRows || []).forEach(function (r) { totLeads += r.newCount; slaAcc += r.slaPct * r.newCount; });
|
|
|
306 |
var slaPct = totLeads > 0 ? Math.round(slaAcc / totLeads) : 0;
|
|
|
307 |
var days = 90;
|
|
|
308 |
var kpis = [
|
|
|
309 |
{ label: 'LMS created', value: fmtInt(s.total), foot: 'in period', tone: 'navy' },
|
|
|
310 |
{ label: 'Conversion', value: s.conversionPct + '%', foot: 'onboarded / created', tone: 'good' },
|
|
|
311 |
{ label: 'SLA compliance', value: slaPct + '%', foot: 'first contact in SLA', tone: slaPct < 60 ? 'red' : 'navy' },
|
|
|
312 |
{ label: 'Calls/day (BGC)', value: fmtInt(Math.round(contacted / days)), foot: 'proxy · contacted ÷ days', tone: 'amber' },
|
|
|
313 |
{ label: 'Ageing > 7 days', value: DASH.ageing ? fmtInt(DASH.ageing.total) : '—', foot: 'stuck open leads', tone: 'red' },
|
|
|
314 |
{ label: 'Onboarded value', value: fmtInr(s.onboardedValue), foot: 'sum of potential', tone: 'good' }
|
|
|
315 |
];
|
|
|
316 |
var kh = '';
|
|
|
317 |
kpis.forEach(function (k) {
|
|
|
318 |
kh += '<div class="lms-kpi tone-' + k.tone + '">' +
|
|
|
319 |
'<div class="k-label">' + esc(k.label) + '</div>' +
|
|
|
320 |
'<div class="k-value">' + k.value + '</div>' +
|
|
|
321 |
'<div class="k-foot">' + esc(k.foot) + '</div></div>';
|
|
|
322 |
});
|
|
|
323 |
setHtml('lmsDashKpis', kh);
|
|
|
324 |
}
|
|
|
325 |
|
|
|
326 |
function renderHeatmap(data) {
|
|
|
327 |
var weeks = (data && data.weeks) ? data.weeks : [];
|
|
|
328 |
var bms = (data && data.bms) ? data.bms : [];
|
|
|
329 |
if (!bms.length) { setHtml('lmsHeatmap', '<div class="lms-ph">No SLA data for the selected period.</div>'); return; }
|
|
|
330 |
var h = '<table class="heat"><thead><tr><th></th>';
|
|
|
331 |
weeks.forEach(function (w) { h += '<th>' + esc(w) + '</th>'; });
|
|
|
332 |
h += '</tr></thead><tbody>';
|
|
|
333 |
bms.forEach(function (bm) {
|
|
|
334 |
h += '<tr><td class="bm">' + esc(bm.bmName) + '</td>';
|
|
|
335 |
(bm.cells || []).forEach(function (c) {
|
|
|
336 |
if (c.pct === null || c.pct === undefined) {
|
|
|
337 |
h += '<td class="cell empty">–</td>';
|
|
|
338 |
} else {
|
|
|
339 |
h += '<td class="cell" style="background:' + heatColor(c.pct) + '">' + c.pct + '</td>';
|
|
|
340 |
}
|
|
|
341 |
});
|
|
|
342 |
h += '</tr>';
|
|
|
343 |
});
|
|
|
344 |
h += '</tbody></table>';
|
|
|
345 |
setHtml('lmsHeatmap', h);
|
|
|
346 |
}
|
|
|
347 |
|
|
|
348 |
function heatColor(p) {
|
|
|
349 |
if (p >= 90) return '#2f9e44';
|
|
|
350 |
if (p >= 75) return '#82c91e';
|
|
|
351 |
if (p >= 60) return '#f59f00';
|
|
|
352 |
if (p >= 40) return '#e8590c';
|
|
|
353 |
return '#e03131';
|
|
|
354 |
}
|
|
|
355 |
|
|
|
356 |
function renderAgeing(data) {
|
|
|
357 |
DASH.ageing = data;
|
|
|
358 |
var rows = (data && data.rows) ? data.rows : [];
|
|
|
359 |
if (!rows.length) { setHtml('lmsAgeing', '<div class="lms-ph">No ageing leads. 🎉</div>'); return; }
|
|
|
360 |
var h = '<table class="lms-tbl"><thead><tr><th>Stuck at stage</th><th>Leads > ' + (data.days || 7) + 'd</th></tr></thead><tbody>';
|
|
|
361 |
rows.forEach(function (r) {
|
|
|
362 |
h += '<tr class="row-click" data-agestage="' + esc(r.stage) + '">' +
|
|
|
363 |
'<td>' + stagePill(r.stage, r.label) + '</td>' +
|
|
|
364 |
'<td><b>' + fmtInt(r.count) + '</b></td></tr>';
|
|
|
365 |
});
|
|
|
366 |
h += '</tbody></table>';
|
|
|
367 |
setHtml('lmsAgeing', h);
|
|
|
368 |
}
|
|
|
369 |
|
|
|
370 |
function refreshDashboards() {
|
|
|
371 |
readFilters();
|
|
|
372 |
doGetAjaxRequestHandler(api('/lms/dashboard/summary' + filterQS('?')), function (resp) {
|
|
|
373 |
DASH.summary = unwrap(resp);
|
|
|
374 |
// re-run KPI build after state loads; also refresh command-style summary if present
|
|
|
375 |
if (document.getElementById('lmsStateFunnel')) fetchState();
|
|
|
376 |
});
|
|
|
377 |
doGetAjaxRequestHandler(api('/lms/dashboard/sla-heatmap' + filterQS('?')), function (resp) { renderHeatmap(unwrap(resp)); });
|
|
|
378 |
doGetAjaxRequestHandler(api('/lms/dashboard/ageing'), function (resp) { renderAgeing(unwrap(resp)); });
|
|
|
379 |
}
|
|
|
380 |
|
|
|
381 |
function fetchState() {
|
|
|
382 |
doGetAjaxRequestHandler(api('/lms/dashboard/state-funnel' + filterQS('?')), function (resp) { renderStateFunnel(unwrap(resp)); });
|
|
|
383 |
}
|
|
|
384 |
|
|
|
385 |
// ---- DOM helpers ------------------------------------------------------------
|
|
|
386 |
function setHtml(id, html) { var el = document.getElementById(id); if (el) el.innerHTML = html; }
|
|
|
387 |
|
|
|
388 |
// ---- delegated clicks (drill) ----------------------------------------------
|
|
|
389 |
$(document).off('click.lmsdash');
|
|
|
390 |
$(document).on('click.lmsdash', '.lms-dash .lms-kpi[data-kpikey]', function () {
|
|
|
391 |
var key = $(this).data('kpikey');
|
|
|
392 |
var b = kpiBucket(key);
|
|
|
393 |
drill('Command Center', $(this).find('.k-label').text(), 'KPI drill', b.t, b.k);
|
|
|
394 |
});
|
|
|
395 |
$(document).on('click.lmsdash', '.lms-dash .att[data-attkey]', function () {
|
|
|
396 |
drill('Needs attention', $(this).data('atttitle'), 'Attention queue', 'ATTENTION', $(this).data('attkey'));
|
|
|
397 |
});
|
|
|
398 |
$(document).on('click.lmsdash', '.lms-dash table.lms-tbl tr.row-click[data-leadid]', function () {
|
|
|
399 |
var id = $(this).data('leadid');
|
|
|
400 |
closeDrawer();
|
|
|
401 |
doGetAjaxRequestHandler(api('/leadRecord?leadId=' + id), function (r) { $('#main-content').html(r); });
|
|
|
402 |
});
|
|
|
403 |
$(document).on('click.lmsdash', '.lms-dash tr.row-click[data-region]', function () {
|
|
|
404 |
drill('State funnel', $(this).data('regionname'), 'Region ' + $(this).data('region'), 'REGION', $(this).data('region'));
|
|
|
405 |
});
|
|
|
406 |
$(document).on('click.lmsdash', '.lms-dash tr.row-click[data-agestage]', function () {
|
|
|
407 |
drill('Ageing', $(this).find('.stage').text(), 'Stuck leads', 'STAGE', $(this).data('agestage'));
|
|
|
408 |
});
|
|
|
409 |
// Filter changes.
|
|
|
410 |
$(document).on('change.lmsdash', '#lmsFilterRegion, #lmsFilterFrom, #lmsFilterTo', function () {
|
|
|
411 |
if (document.getElementById('lmsKpis') && document.getElementById('lmsPipeline')) {
|
|
|
412 |
refreshCommandCenter();
|
|
|
413 |
} else {
|
|
|
414 |
refreshDashboards();
|
|
|
415 |
}
|
|
|
416 |
});
|
|
|
417 |
|
|
|
418 |
// ---- public API the .vm fragments call --------------------------------------
|
|
|
419 |
window.LmsDash = {
|
|
|
420 |
initCommandCenter: function (cfg) {
|
|
|
421 |
cfg = cfg || {};
|
|
|
422 |
STATE.regionId = cfg.regionId || '';
|
|
|
423 |
STATE.from = cfg.from || '';
|
|
|
424 |
STATE.to = cfg.to || '';
|
|
|
425 |
ensureDrawer();
|
|
|
426 |
if (cfg.summary) renderSummary(cfg.summary);
|
|
|
427 |
loadPipeline();
|
|
|
428 |
},
|
|
|
429 |
initDashboards: function (cfg) {
|
|
|
430 |
cfg = cfg || {};
|
|
|
431 |
STATE.regionId = cfg.regionId || '';
|
|
|
432 |
STATE.from = cfg.from || '';
|
|
|
433 |
STATE.to = cfg.to || '';
|
|
|
434 |
ensureDrawer();
|
|
|
435 |
DASH.summary = cfg.summary || null;
|
|
|
436 |
DASH.ageing = cfg.ageing || null;
|
|
|
437 |
if (cfg.stateFunnel) renderStateFunnel(cfg.stateFunnel);
|
|
|
438 |
if (cfg.slaHeatmap) renderHeatmap(cfg.slaHeatmap);
|
|
|
439 |
if (cfg.ageing) renderAgeing(cfg.ageing);
|
|
|
440 |
},
|
|
|
441 |
refreshCommandCenter: refreshCommandCenter,
|
|
|
442 |
refreshDashboards: refreshDashboards
|
|
|
443 |
};
|
|
|
444 |
|
|
|
445 |
// ---- sidebar menu entry points ----------------------------------------------
|
|
|
446 |
// The LMS console is a full standalone page (own top-bar + 6 tabs, sts-mockup UI). To keep
|
|
|
447 |
// the app's left sidebar + header visible, embed it in an iframe inside #main-content
|
|
|
448 |
// (style/script isolated), deep-linked to the relevant tab.
|
|
|
449 |
function openLmsConsole(hash) {
|
|
|
450 |
var url = api('/lms/dashboard') + (hash || '');
|
|
|
451 |
$('#main-content').html(
|
|
|
452 |
'<iframe title="LMS Console" src="' + url + '" ' +
|
|
|
453 |
'style="width:100%;height:calc(100vh - 92px);min-height:620px;border:0;display:block"></iframe>'
|
|
|
454 |
);
|
|
|
455 |
}
|
|
|
456 |
$(document).on('click', '.lms-command-center', function (e) {
|
|
|
457 |
if (e && e.preventDefault) e.preventDefault();
|
|
|
458 |
openLmsConsole('#cmd');
|
|
|
459 |
});
|
|
|
460 |
$(document).on('click', '.lms-dashboards', function (e) {
|
|
|
461 |
if (e && e.preventDefault) e.preventDefault();
|
|
|
462 |
openLmsConsole('#dash');
|
|
|
463 |
});
|
|
|
464 |
})();
|