Subversion Repositories SmartDukaan

Rev

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 / SLA
     bars 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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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">&times;</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 tiles
        var 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 + clickable
        var 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 queue
        var 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 &gt; ' + (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 present
            if (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');
    });
})();