| 37125 |
vikas |
1 |
## LMS Dashboard — STANDALONE full page. Faithful copy of sts-mockup.html design (app bar + tabs + hand-drawn
|
|
|
2 |
## funnel/heat-map, NO charts), wired to REAL lead data. Rendered as a complete HTML doc (layout.vm = $screen_content).
|
|
|
3 |
## Served at GET /lms/dashboard. Seeds: regions[], summaryJson, stateFunnelJson, slaHeatmapJson, ageingJson, fromDate, toDate.
|
|
|
4 |
<!DOCTYPE html>
|
|
|
5 |
<html lang="en">
|
|
|
6 |
<head>
|
|
|
7 |
<meta charset="utf-8">
|
|
|
8 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
9 |
<title>LMS — Lead Dashboard</title>
|
|
|
10 |
<style>
|
|
|
11 |
:root{
|
|
|
12 |
--bg:#eef1f7; --card:#ffffff; --ink:#1d2433; --muted:#69748c; --soft:#97a1b5;
|
|
|
13 |
--line:#e6e9f1; --line2:#eef1f6;
|
|
|
14 |
--navy:#1f2a5e; --navy2:#2f3d78;
|
|
|
15 |
--brand:#3b5bdb; --brand2:#5c7cfa; --teal:#0ca678; --cyan:#1098ad;
|
|
|
16 |
--amber:#f59f00; --orange:#e8590c; --red:#e03131; --coral:#f0483e; --violet:#7048e8; --pink:#d6336c;
|
|
|
17 |
--good:#2f9e44; --warn:#f08c00; --bad:#e03131;
|
|
|
18 |
}
|
|
|
19 |
*{box-sizing:border-box}
|
|
|
20 |
html,body{margin:0;background:var(--bg);color:var(--ink);
|
|
|
21 |
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;font-size:14px}
|
|
|
22 |
.wrap{max-width:1340px;margin:0 auto;padding:0 24px 90px}
|
|
|
23 |
a{color:var(--brand);text-decoration:none}
|
|
|
24 |
code,.mono{font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
|
25 |
|
|
|
26 |
/* ===== app bar ===== */
|
|
|
27 |
.appbar{position:sticky;top:0;z-index:40;background:var(--navy);color:#fff;margin:0;padding:0 24px}
|
|
|
28 |
.appbar .inner{max-width:1340px;margin:0 auto;display:flex;align-items:center;gap:18px;height:58px}
|
|
|
29 |
.logo{display:flex;align-items:center;gap:10px;font-weight:800;letter-spacing:-.01em;font-size:16px}
|
|
|
30 |
.logo .mk{width:30px;height:30px;border-radius:8px;background:linear-gradient(135deg,var(--coral),#ff7a5c);display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:800}
|
|
|
31 |
.logo small{display:block;font-size:10px;font-weight:600;color:#aab3d6;letter-spacing:.06em}
|
|
|
32 |
.search{flex:1;max-width:560px;position:relative}
|
|
|
33 |
.search input{width:100%;background:#2b3768;border:1px solid #3a488a;color:#fff;border-radius:9px;padding:9px 12px 9px 34px;font-size:13px;font-weight:500}
|
|
|
34 |
.search input::placeholder{color:#9aa4cc}
|
|
|
35 |
.search .si{position:absolute;left:11px;top:9px;color:#9aa4cc;font-size:14px}
|
|
|
36 |
.search .hint{position:absolute;right:10px;top:8px;font-size:10px;color:#9aa4cc;border:1px solid #3a488a;border-radius:5px;padding:2px 6px}
|
|
|
37 |
.abtn{background:#2b3768;border:1px solid #3a488a;color:#fff;border-radius:9px;padding:8px 11px;font-size:13px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:7px}
|
|
|
38 |
.abtn.coral{background:var(--coral);border-color:var(--coral)}
|
|
|
39 |
.role{display:flex;align-items:center;gap:9px}
|
|
|
40 |
.role .av{width:31px;height:31px;border-radius:50%;background:#3a488a;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px}
|
|
|
41 |
.role .who{font-size:12px;line-height:1.25}
|
|
|
42 |
.role .who b{font-weight:700}
|
|
|
43 |
.role .who span{color:#aab3d6}
|
|
|
44 |
|
|
|
45 |
/* ===== tabs ===== */
|
|
|
46 |
.tabbar{position:sticky;top:58px;z-index:39;background:var(--card);border-bottom:1px solid var(--line);margin:0 0 20px;padding:0 24px;box-shadow:0 1px 2px rgba(20,30,60,.03)}
|
|
|
47 |
.tabbar .inner{max-width:1340px;margin:0 auto;display:flex;gap:2px;overflow-x:auto}
|
|
|
48 |
.tab{padding:13px 4px;margin-right:24px;font-size:13.5px;font-weight:650;color:var(--muted);cursor:pointer;border:0;background:transparent;border-bottom:2.5px solid transparent;margin-bottom:-1px;white-space:nowrap}
|
|
|
49 |
.tab:hover{color:var(--ink)}
|
|
|
50 |
.tab.active{color:var(--navy);border-bottom-color:var(--coral)}
|
|
|
51 |
|
|
|
52 |
/* ===== generic ===== */
|
|
|
53 |
.crumb{font-size:12px;color:var(--soft);font-weight:600}
|
|
|
54 |
h1.page{margin:2px 0 2px;font-size:23px;font-weight:760;letter-spacing:-.01em}
|
|
|
55 |
.pagehead{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:18px}
|
|
|
56 |
.pagehead .sub{color:var(--muted);font-size:13px;margin-top:2px}
|
|
|
57 |
.hd-right{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
|
|
|
58 |
.ctl{background:var(--card);border:1px solid var(--line);border-radius:9px;padding:8px 12px;font-size:13px;color:var(--ink);font-weight:600}
|
|
|
59 |
.ctl .lbl{color:var(--soft);font-weight:500;margin-right:5px}
|
|
|
60 |
select.ctl{appearance:none;cursor:pointer;padding-right:26px;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6'><path d='M1 1l4 4 4-4' stroke='%2369748c' fill='none' stroke-width='1.5'/></svg>");background-repeat:no-repeat;background-position:right 10px center}
|
|
|
61 |
.btn{background:var(--navy);border:0;color:#fff;font-weight:650;padding:9px 15px;border-radius:9px;cursor:pointer;font-size:13px}
|
|
|
62 |
.btn.coral{background:var(--coral)}
|
|
|
63 |
.btn.ghost{background:#fff;color:var(--ink);border:1px solid var(--line)}
|
|
|
64 |
.btn.sm{padding:6px 11px;font-size:12px}
|
|
|
65 |
|
|
|
66 |
.sectionlabel{font-size:11.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--soft);font-weight:700;margin:24px 2px 12px}
|
|
|
67 |
.grid{display:grid;gap:16px}
|
|
|
68 |
.g2{grid-template-columns:1fr 1fr}
|
|
|
69 |
.g3{grid-template-columns:1.6fr 1fr}
|
|
|
70 |
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:17px 19px;margin-bottom:16px;box-shadow:0 1px 2px rgba(20,30,60,.04)}
|
|
|
71 |
.card h2{margin:0 0 3px;font-size:15px;font-weight:720;letter-spacing:-.01em}
|
|
|
72 |
.card .desc{color:var(--muted);font-size:12.5px;margin:0 0 14px}
|
|
|
73 |
|
|
|
74 |
/* kpis */
|
|
|
75 |
.kpis{display:grid;grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:8px}
|
|
|
76 |
.kpi{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:14px 15px;box-shadow:0 1px 2px rgba(20,30,60,.04)}
|
|
|
77 |
.kpi .top{display:flex;align-items:center;gap:8px}
|
|
|
78 |
.kpi .ic{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;flex:none}
|
|
|
79 |
.kpi .label{font-size:11.5px;color:var(--muted);font-weight:600;line-height:1.2}
|
|
|
80 |
.kpi .val{font-size:26px;font-weight:760;margin-top:9px;letter-spacing:-.02em;line-height:1}
|
|
|
81 |
.kpi .val small{font-size:13px;color:var(--soft);font-weight:600}
|
|
|
82 |
.kpi .foot{font-size:11.5px;color:var(--soft);margin-top:7px}
|
|
|
83 |
|
|
|
84 |
/* funnel */
|
|
|
85 |
.funnel{display:flex;gap:8px;align-items:stretch;flex-wrap:wrap}
|
|
|
86 |
.fstep{flex:1;min-width:118px;border:1px solid var(--line);border-radius:11px;padding:12px 11px;position:relative;text-align:center;background:#fbfcfe}
|
|
|
87 |
.fstep .fn{font-size:10.5px;text-transform:uppercase;letter-spacing:.03em;color:var(--muted);font-weight:700}
|
|
|
88 |
.fstep .fv{font-size:24px;font-weight:770;margin-top:6px}
|
|
|
89 |
.fstep .fbar{height:5px;border-radius:3px;margin-top:9px;background:linear-gradient(90deg,var(--navy),var(--brand))}
|
|
|
90 |
.fstep .drop{font-size:10.5px;color:var(--red);margin-top:6px;font-weight:600}
|
|
|
91 |
.fstep .keep{font-size:10.5px;color:var(--good);margin-top:6px;font-weight:600}
|
|
|
92 |
.fstep .arrow{position:absolute;right:-11px;top:50%;transform:translateY(-50%);z-index:2;color:#c5cce0;font-size:14px}
|
|
|
93 |
|
|
|
94 |
/* table */
|
|
|
95 |
table{width:100%;border-collapse:collapse;font-size:13px}
|
|
|
96 |
th,td{text-align:left;padding:9px 9px;border-bottom:1px solid var(--line2)}
|
|
|
97 |
th{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:700;white-space:nowrap}
|
|
|
98 |
th.num,td.num{text-align:right}
|
|
|
99 |
tbody tr:hover{background:#f7f9fd}
|
|
|
100 |
.clickrow{cursor:pointer}
|
|
|
101 |
.rowwarn{background:#fff9f0}
|
|
|
102 |
.rowbad{background:#fff5f5}
|
|
|
103 |
.warnflag{color:var(--orange);font-weight:700}
|
|
|
104 |
.badflag{color:var(--red);font-weight:700}
|
|
|
105 |
|
|
|
106 |
/* pills / stages */
|
|
|
107 |
.idpill{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px;font-weight:700;color:var(--navy);background:#eef1fb;border:1px solid #dfe4f7;border-radius:6px;padding:2px 7px;white-space:nowrap}
|
|
|
108 |
.stage{display:inline-flex;align-items:center;gap:6px;font-weight:700;font-size:11px;padding:3px 9px;border-radius:999px;text-transform:uppercase;letter-spacing:.02em}
|
|
|
109 |
.st-new{background:#eef1fb;color:#3b5bdb}
|
|
|
110 |
.st-assigned{background:#e7f5ff;color:#1971c2}
|
|
|
111 |
.st-contacted{background:#e3fafc;color:#0c8599}
|
|
|
112 |
.st-qualified{background:#e6fcf5;color:#0ca678}
|
|
|
113 |
.st-beat{background:#f3f0ff;color:#7048e8}
|
|
|
114 |
.st-visited{background:#fff0f6;color:#c2255c}
|
|
|
115 |
.st-onboarded{background:#ebfbee;color:#2f9e44}
|
|
|
116 |
.st-active{background:#d3f9d8;color:#2b8a3e}
|
|
|
117 |
.st-ni{background:#fff4e6;color:#e8590c}
|
|
|
118 |
.st-dead{background:#f1f3f5;color:#868e96}
|
|
|
119 |
.tag{display:inline-block;font-size:10.5px;font-weight:700;padding:2px 7px;border-radius:5px}
|
|
|
120 |
.tag.a{background:#fff0ef;color:var(--coral)}
|
|
|
121 |
.tag.b{background:#e6fcf5;color:var(--teal)}
|
|
|
122 |
|
|
|
123 |
/* SLA pills */
|
|
|
124 |
.sla{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700}
|
|
|
125 |
.sla .clk{font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
|
126 |
.sla.ok{color:var(--good)} .sla.warn{color:var(--orange)} .sla.bad{color:var(--red)}
|
|
|
127 |
.slabar{display:inline-block;width:52px;height:6px;border-radius:4px;background:#eef1f6;overflow:hidden;vertical-align:middle}
|
|
|
128 |
.slabar>i{display:block;height:100%}
|
|
|
129 |
|
|
|
130 |
/* attention */
|
|
|
131 |
.att{display:flex;align-items:center;gap:12px;padding:11px 12px;border:1px solid var(--line);border-radius:11px;margin-bottom:9px}
|
|
|
132 |
.att .ic{width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:14px;flex:none}
|
|
|
133 |
.att .ab{flex:1}
|
|
|
134 |
.att .atitle{font-size:13px;font-weight:650}
|
|
|
135 |
.att .adet{font-size:12px;color:var(--muted);margin-top:2px}
|
|
|
136 |
.att .acount{font-size:18px;font-weight:780}
|
|
|
137 |
|
|
|
138 |
/* tiles */
|
|
|
139 |
.tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:11px}
|
|
|
140 |
.tile{background:#f8f9fc;border:1px solid var(--line2);border-radius:11px;padding:12px}
|
|
|
141 |
.tile .t{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:600}
|
|
|
142 |
.tile .v{font-size:21px;font-weight:760;margin-top:5px}
|
|
|
143 |
.tile .v small{font-size:12px;color:var(--soft);font-weight:600}
|
|
|
144 |
|
|
|
145 |
.hide{display:none !important}
|
|
|
146 |
|
|
|
147 |
/* interactivity */
|
|
|
148 |
.kpi,.fstep,.att,#dash tbody tr,.heat td{cursor:pointer}
|
|
|
149 |
.kpi,.fstep{transition:transform .12s,box-shadow .12s}
|
|
|
150 |
.kpi:hover,.fstep:hover{box-shadow:0 4px 14px rgba(20,30,60,.10);transform:translateY(-1px)}
|
|
|
151 |
.att:hover{border-color:#c7d0e8}
|
|
|
152 |
|
|
|
153 |
.drawer-ov{position:fixed;inset:0;background:rgba(20,26,50,.42);z-index:70;opacity:0;pointer-events:none;transition:opacity .2s}
|
|
|
154 |
.drawer-ov.open{opacity:1;pointer-events:auto}
|
|
|
155 |
.drawer{position:fixed;top:0;right:0;height:100%;width:min(580px,96vw);background:#f7f8fc;z-index:71;box-shadow:-14px 0 46px rgba(10,15,40,.22);transform:translateX(100%);transition:transform .26s cubic-bezier(.4,0,.2,1);display:flex;flex-direction:column}
|
|
|
156 |
.drawer.open{transform:none}
|
|
|
157 |
.drawer .dh{padding:17px 20px;background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:flex-start;gap:12px;flex:none}
|
|
|
158 |
.drawer .crumb2{font-size:10.5px;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--coral)}
|
|
|
159 |
.drawer h3{margin:2px 0 1px;font-size:18px;font-weight:770;letter-spacing:-.01em}
|
|
|
160 |
.drawer .dsub{color:var(--muted);font-size:12.5px}
|
|
|
161 |
.drawer .x{margin-left:auto;cursor:pointer;font-size:22px;line-height:1;color:var(--soft);background:none;border:0;padding:0 2px}
|
|
|
162 |
.drawer .dbody{padding:16px 18px 40px;overflow:auto;flex:1}
|
|
|
163 |
.drawer .card{margin-bottom:12px}
|
|
|
164 |
.dstat{display:grid;grid-template-columns:repeat(3,1fr);gap:9px;margin-bottom:12px}
|
|
|
165 |
.dstat .box{background:#fff;border:1px solid var(--line);border-radius:11px;padding:11px 12px}
|
|
|
166 |
.dstat .t{font-size:10px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:700}
|
|
|
167 |
.dstat .v{font-size:20px;font-weight:770;margin-top:3px}
|
|
|
168 |
.dnote{font-size:12px;color:var(--muted);line-height:1.55;margin-top:8px}
|
|
|
169 |
|
|
|
170 |
.toastwrap{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:90;display:flex;flex-direction:column;gap:8px;align-items:center}
|
|
|
171 |
.toast{background:#1f2a5e;color:#fff;padding:11px 16px;border-radius:11px;font-size:13px;font-weight:600;box-shadow:0 10px 34px rgba(10,15,40,.32);display:flex;align-items:center;gap:10px;animation:tin .18s ease-out}
|
|
|
172 |
.toast .ok{color:#69db7c;font-weight:800}
|
|
|
173 |
@keyframes tin{from{opacity:0;transform:translateY(10px)}}
|
|
|
174 |
|
|
|
175 |
/* heatmap */
|
|
|
176 |
.heat td{width:34px;height:30px;text-align:center;font-size:11px;font-weight:700;color:#fff;border:2px solid #fff;border-radius:6px}
|
|
|
177 |
|
|
|
178 |
/* callout */
|
|
|
179 |
.callout{background:linear-gradient(180deg,#fbfcff,#f4f7ff);border:1px solid #dfe6fb;border-radius:14px;padding:18px 20px;margin-top:6px}
|
|
|
180 |
.callout h2{margin:0 0 3px;font-size:16px}
|
|
|
181 |
.fix{display:flex;gap:13px;padding:13px 0;border-bottom:1px solid #e6ebf8}
|
|
|
182 |
.fix:last-child{border-bottom:0}
|
|
|
183 |
.fix .sev{width:6px;border-radius:4px;flex:none}
|
|
|
184 |
.fix .fb{flex:1}
|
|
|
185 |
.fix .ft{font-size:14px;font-weight:700}
|
|
|
186 |
.fix .fd{font-size:13px;color:#3a4459;margin-top:4px;line-height:1.5}
|
|
|
187 |
.fix .fd b{color:var(--ink)}
|
|
|
188 |
.fix .act{font-size:12.5px;color:var(--good);margin-top:6px;font-weight:650}
|
|
|
189 |
|
|
|
190 |
.note{font-size:11.5px;color:var(--soft);margin-top:10px;line-height:1.6}
|
|
|
191 |
.ph{font-size:12.5px;color:var(--soft);font-style:italic;padding:16px 0;text-align:center}
|
|
|
192 |
.rbadge{font-size:9.5px;font-weight:700;padding:1px 6px;border-radius:10px;margin-left:6px;vertical-align:middle}
|
|
|
193 |
.rbadge.live{background:#e6f7ee;color:var(--good)}.rbadge.partial{background:#fff3dc;color:var(--warn)}.rbadge.soon{background:#eef0f5;color:var(--soft)}
|
|
|
194 |
|
|
|
195 |
/* record / kv / trail */
|
|
|
196 |
.kv{display:grid;grid-template-columns:130px 1fr;gap:7px 12px;font-size:13px}
|
|
|
197 |
.kv .k{color:var(--soft);font-weight:600}
|
|
|
198 |
.kv .v{font-weight:600}
|
|
|
199 |
.divider{height:1px;background:var(--line);margin:14px 0}
|
|
|
200 |
.recordHost{min-height:120px}
|
|
|
201 |
.lookup{display:flex;gap:8px;align-items:center;margin-bottom:14px}
|
|
|
202 |
.lookup input{border:1px solid var(--line);border-radius:9px;padding:9px 11px;font-size:13px;width:280px}
|
|
|
203 |
|
|
|
204 |
/* form */
|
|
|
205 |
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 16px}
|
|
|
206 |
.fld{display:flex;flex-direction:column;gap:5px}
|
|
|
207 |
.fld.full{grid-column:1/3}
|
|
|
208 |
.fld label{font-size:11.5px;font-weight:700;color:var(--muted)}
|
|
|
209 |
.fld label .m{color:var(--red)}
|
|
|
210 |
.fld label .auto{float:right;font-size:10px;font-weight:700;color:var(--brand);background:#eef1fb;border-radius:5px;padding:1px 6px;text-transform:uppercase;letter-spacing:.03em}
|
|
|
211 |
.inp{border:1px solid var(--line);border-radius:9px;padding:9px 11px;font-size:13px;font-weight:500;background:#fff;color:var(--ink);width:100%}
|
|
|
212 |
.inp:focus{outline:none;border-color:var(--brand2);box-shadow:0 0 0 3px #eaefff}
|
|
|
213 |
.inp[readonly],.inp.ro{background:#f5f6fa;color:var(--muted);font-weight:600}
|
|
|
214 |
select.inp{appearance:none;cursor:pointer;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6'><path d='M1 1l4 4 4-4' stroke='%2369748c' fill='none' stroke-width='1.5'/></svg>");background-repeat:no-repeat;background-position:right 11px center}
|
|
|
215 |
.hinttext{font-size:11px;color:var(--soft)}
|
|
|
216 |
.resolvebox{background:#f5f7ff;border:1px solid #dfe4f7;border-radius:11px;padding:13px 15px;display:flex;align-items:center;gap:13px}
|
|
|
217 |
.resolvebox .ra{width:38px;height:38px;border-radius:10px;background:var(--navy);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;flex:none}
|
|
|
218 |
.resolvebox .rt{font-size:11px;color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:.04em}
|
|
|
219 |
.resolvebox .rn{font-size:15px;font-weight:750;margin-top:1px}
|
|
|
220 |
.resolvebox .rc{font-size:11.5px;color:var(--muted);margin-top:2px}
|
|
|
221 |
.trail{position:relative;padding-left:22px}
|
|
|
222 |
.trail:before{content:"";position:absolute;left:7px;top:4px;bottom:4px;width:2px;background:var(--line)}
|
|
|
223 |
.tevt{position:relative;padding:0 0 14px}
|
|
|
224 |
.tevt:last-child{padding-bottom:0}
|
|
|
225 |
.tevt .tdot{position:absolute;left:-22px;top:2px;width:16px;height:16px;border-radius:50%;background:#fff;border:2px solid var(--brand);display:flex;align-items:center;justify-content:center;font-size:8px;color:var(--brand)}
|
|
|
226 |
.tevt.sys .tdot{border-color:var(--soft);color:var(--soft)}
|
|
|
227 |
.tevt.win .tdot{border-color:var(--good);color:var(--good)}
|
|
|
228 |
.tevt.call .tdot{border-color:var(--coral);color:var(--coral)}
|
|
|
229 |
.tevt .tt{font-size:13px;font-weight:650}
|
|
|
230 |
.tevt .tm{font-size:11.5px;color:var(--soft);margin-top:1px}
|
|
|
231 |
.tevt .td{font-size:12.5px;color:var(--muted);margin-top:4px;background:#f8f9fc;border:1px solid var(--line2);border-radius:8px;padding:8px 10px}
|
|
|
232 |
|
|
|
233 |
/* record stepper */
|
|
|
234 |
.rec-stepper{display:flex;align-items:center;gap:0;margin:2px 0 4px;flex-wrap:wrap}
|
|
|
235 |
.rec-step{display:flex;align-items:center;gap:8px;flex:1;min-width:96px;position:relative}
|
|
|
236 |
.rec-step .dot{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;flex:none;border:2px solid var(--line);background:#fff;color:var(--soft)}
|
|
|
237 |
.rec-step.done .dot{background:var(--good);border-color:var(--good);color:#fff}
|
|
|
238 |
.rec-step.cur .dot{background:var(--navy);border-color:var(--navy);color:#fff;box-shadow:0 0 0 4px #e7ebfa}
|
|
|
239 |
.rec-step .sl{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.02em;color:var(--muted)}
|
|
|
240 |
.rec-step.cur .sl{color:var(--navy)}
|
|
|
241 |
.rec-step .bar{position:absolute;left:26px;right:0;top:13px;height:2px;background:var(--line);z-index:-1}
|
|
|
242 |
.rec-step.done .bar{background:var(--good)}
|
|
|
243 |
|
|
|
244 |
/* matrix */
|
|
|
245 |
.matrix{width:100%;border-collapse:separate;border-spacing:0;font-size:12.5px}
|
|
|
246 |
.matrix th,.matrix td{padding:9px 8px;border-bottom:1px solid var(--line2);text-align:center}
|
|
|
247 |
.matrix th:first-child,.matrix td:first-child{text-align:left;font-weight:650}
|
|
|
248 |
.matrix thead th{background:#f8f9fc;color:var(--muted);font-size:10.5px;text-transform:uppercase;letter-spacing:.03em}
|
|
|
249 |
.mk-y{color:var(--good);font-weight:800}
|
|
|
250 |
.mk-n{color:#ccd2e0}
|
|
|
251 |
.mk-s{color:var(--orange);font-weight:800;font-size:11px}
|
|
|
252 |
|
|
|
253 |
@media (max-width:1040px){
|
|
|
254 |
.kpis{grid-template-columns:repeat(3,1fr)}
|
|
|
255 |
.g2,.g3{grid-template-columns:1fr}
|
|
|
256 |
.form-grid{grid-template-columns:1fr}.fld.full{grid-column:1}
|
|
|
257 |
.search{display:none}
|
|
|
258 |
}
|
|
|
259 |
</style>
|
|
|
260 |
</head>
|
|
|
261 |
<body>
|
|
|
262 |
|
|
|
263 |
<!-- ============ APP BAR ============ -->
|
|
|
264 |
<div class="appbar">
|
|
|
265 |
<div class="inner">
|
|
|
266 |
<div class="logo"><span class="mk">SD</span><div>SmartDukaan · LMS<small>LEAD MANAGEMENT SYSTEM</small></div></div>
|
|
|
267 |
<div class="search">
|
|
|
268 |
<span class="si">🔍</span>
|
|
|
269 |
<input id="lmsSearch" placeholder="Search LMS ID, business, phone, retailer… e.g. LMS-UPW-26-000123">
|
|
|
270 |
<span class="hint">/ to focus</span>
|
|
|
271 |
</div>
|
|
|
272 |
<button class="abtn coral" onclick="window.open(CTX+'/getOpenLead','_blank')">+ New Lead</button>
|
|
|
273 |
<div class="role">
|
|
|
274 |
<div class="av">SD</div>
|
|
|
275 |
<div class="who"><b>Lead Dashboard</b><br><span>live · real data</span></div>
|
|
|
276 |
</div>
|
|
|
277 |
</div>
|
|
|
278 |
</div>
|
|
|
279 |
|
|
|
280 |
<!-- ============ TABS ============ -->
|
|
|
281 |
<div class="tabbar">
|
|
|
282 |
<div class="inner">
|
|
|
283 |
<button class="tab active" onclick="go('cmd',this)">Command Center</button>
|
|
|
284 |
<button class="tab" onclick="go('record',this)">Lead Record</button>
|
|
|
285 |
<button class="tab" onclick="go('create',this)">Create Lead</button>
|
|
|
286 |
<button class="tab" onclick="go('assign',this)">Auto-Assignment</button>
|
|
|
287 |
<button class="tab" onclick="go('dash',this)">Dashboards</button>
|
|
|
288 |
<button class="tab" onclick="go('audit',this)">Recordings & Audit</button>
|
|
|
289 |
</div>
|
|
|
290 |
</div>
|
|
|
291 |
|
|
|
292 |
<div class="wrap">
|
|
|
293 |
|
|
|
294 |
<!-- ======================= COMMAND CENTER ========================= -->
|
|
|
295 |
<div id="cmd">
|
|
|
296 |
<div class="pagehead">
|
|
|
297 |
<div>
|
|
|
298 |
<div class="crumb">Sales · Lead Pipeline</div>
|
|
|
299 |
<h1 class="page">Command Center</h1>
|
|
|
300 |
<div class="sub">Every lead you own or oversee — from creation to onboarding. SLA clock runs on all NEW records.</div>
|
|
|
301 |
</div>
|
|
|
302 |
<div class="hd-right">
|
|
|
303 |
<select class="ctl" id="fRegion">
|
|
|
304 |
<option value="">All regions</option>
|
|
|
305 |
#foreach($r in $regions)
|
|
|
306 |
<option value="$r.getId()">$!r.getName()#if($r.getRegionCode()) ($r.getRegionCode())#end</option>
|
|
|
307 |
#end
|
|
|
308 |
</select>
|
|
|
309 |
<input type="date" class="ctl" id="fFrom" value="$!fromDate">
|
|
|
310 |
<input type="date" class="ctl" id="fTo" value="$!toDate">
|
|
|
311 |
</div>
|
|
|
312 |
</div>
|
|
|
313 |
|
|
|
314 |
<div class="kpis" id="ccKpis"></div>
|
|
|
315 |
|
|
|
316 |
<div class="sectionlabel">Lifecycle Funnel — selected period</div>
|
|
|
317 |
<div class="card"><div class="funnel" id="ccFunnel"></div></div>
|
|
|
318 |
|
|
|
319 |
<div class="grid g3">
|
|
|
320 |
<div class="card">
|
|
|
321 |
<h2>Lead pipeline</h2>
|
|
|
322 |
<p class="desc">Click any row → full LMS record & trail. Rows shaded by SLA urgency.</p>
|
|
|
323 |
<table>
|
|
|
324 |
<thead><tr><th>LMS ID</th><th>Business / Retailer</th><th>Path</th><th>Stage</th><th>Owner</th><th>SLA</th><th class="num">Value</th></tr></thead>
|
|
|
325 |
<tbody id="ccPipeline"><tr><td colspan="7"><div class="ph">Loading…</div></td></tr></tbody>
|
|
|
326 |
</table>
|
|
|
327 |
</div>
|
|
|
328 |
<div class="card">
|
|
|
329 |
<h2>Needs attention now</h2>
|
|
|
330 |
<p class="desc">Actionable queues · click to work them.</p>
|
|
|
331 |
<div id="ccAttention"></div>
|
|
|
332 |
</div>
|
|
|
333 |
</div>
|
|
|
334 |
</div>
|
|
|
335 |
|
|
|
336 |
<!-- ======================= LEAD RECORD ========================= -->
|
|
|
337 |
<div id="record" class="hide">
|
|
|
338 |
<div class="pagehead">
|
|
|
339 |
<div>
|
|
|
340 |
<div class="crumb">Command Center · Lead Record</div>
|
|
|
341 |
<h1 class="page">Lead Record</h1>
|
|
|
342 |
<div class="sub">The single source of truth for a lead — stepper, immutable trail, SLA & disposition. Loads the real record.</div>
|
|
|
343 |
</div>
|
|
|
344 |
<div class="hd-right">
|
|
|
345 |
<div class="lookup">
|
|
|
346 |
<input id="recLookup" placeholder="Lead # (e.g. 123)" inputmode="numeric">
|
|
|
347 |
<button class="btn sm" onclick="loadRecord()">Load</button>
|
|
|
348 |
</div>
|
|
|
349 |
</div>
|
|
|
350 |
</div>
|
|
|
351 |
<div class="recordHost" id="recordHost"><div class="card"><div class="ph">Loading the most recent lead…</div></div></div>
|
|
|
352 |
<div class="note" style="margin-left:2px">Read-only preview of the real record. Stage/disposition actions run in the app — use <b>Open in app to act</b>.</div>
|
|
|
353 |
</div>
|
|
|
354 |
|
|
|
355 |
<!-- ======================= CREATE LEAD ========================= -->
|
|
|
356 |
<div id="create" class="hide">
|
|
|
357 |
<div class="pagehead">
|
|
|
358 |
<div>
|
|
|
359 |
<div class="crumb">Create · New Lead</div>
|
|
|
360 |
<h1 class="page">Create Lead</h1>
|
|
|
361 |
<div class="sub">Mandatory fields must be filled before submit. LMS ID is generated only on successful submit — auto-assigned from region.</div>
|
|
|
362 |
</div>
|
|
|
363 |
<div class="hd-right">
|
|
|
364 |
<div class="ctl" style="padding:4px;display:flex;gap:4px">
|
|
|
365 |
<button class="btn coral sm" id="pathA" style="border-radius:7px" onclick="setPath('A',this)">Path A · BGC outbound</button>
|
|
|
366 |
<button class="btn ghost sm" id="pathB" style="border-radius:7px" onclick="setPath('B',this)">Path B · Field encounter</button>
|
|
|
367 |
</div>
|
|
|
368 |
</div>
|
|
|
369 |
</div>
|
|
|
370 |
|
|
|
371 |
<div class="grid g3">
|
|
|
372 |
<div class="card">
|
|
|
373 |
<h2>Lead details</h2>
|
|
|
374 |
<p class="desc" id="createDesc">Path A — captured by BGC agent during an outbound call, after consent.</p>
|
|
|
375 |
<div class="form-grid">
|
|
|
376 |
<div class="fld"><label>Retailer name <span class="m">*</span></label><input class="inp" id="cRetailer" placeholder="Contact person"></div>
|
|
|
377 |
<div class="fld"><label>Business name <span class="m">*</span></label><input class="inp" id="cBusiness" placeholder="Shop / trade name"></div>
|
|
|
378 |
<div class="fld full"><label>Location <span class="m">*</span> <span class="hinttext" style="float:right" id="cLocHint">geo-pin optional on Path A</span></label><input class="inp" id="cLocation" placeholder="Full address"></div>
|
|
|
379 |
<div class="fld"><label>Contact number <span class="m">*</span></label><input class="inp mono" id="cContact" placeholder="10-digit mobile" inputmode="numeric"></div>
|
|
|
380 |
<div class="fld"><label>Business value</label><input class="inp" id="cValue" placeholder="e.g. 85000 (₹/month)"></div>
|
|
|
381 |
<div class="fld"><label>Interest status</label><select class="inp" id="cInterest"><option>Interested</option><option>Callback</option><option>Not Interested</option></select></div>
|
|
|
382 |
<div class="fld"><label>Category / brands</label><input class="inp" id="cBrands" placeholder="Samsung, Vivo…"></div>
|
|
|
383 |
<div class="fld full">
|
|
|
384 |
<label>Region / State <span class="m">*</span> <span class="auto">drives auto-assignment</span></label>
|
|
|
385 |
<select class="inp" id="cRegion" onchange="resolveBM(this.value)">
|
|
|
386 |
<option value="">Select a region…</option>
|
|
|
387 |
#foreach($r in $regions)
|
|
|
388 |
<option value="$r.getId()">$!r.getName()#if($r.getRegionCode()) ($r.getRegionCode())#end</option>
|
|
|
389 |
#end
|
|
|
390 |
</select>
|
|
|
391 |
</div>
|
|
|
392 |
</div>
|
|
|
393 |
<div style="display:flex;gap:9px;margin-top:16px;justify-content:flex-end">
|
|
|
394 |
<button class="btn coral" onclick="submitLead()">Submit & generate LMS ID</button>
|
|
|
395 |
</div>
|
|
|
396 |
</div>
|
|
|
397 |
|
|
|
398 |
<div>
|
|
|
399 |
<div class="card">
|
|
|
400 |
<h2>Auto-assignment preview</h2>
|
|
|
401 |
<p class="desc">On Region select, the engine resolves the BM/RSM and shows it for confirmation before submit.</p>
|
|
|
402 |
<div class="resolvebox" id="resolveBox">
|
|
|
403 |
<div class="ra" id="raAv">—</div>
|
|
|
404 |
<div>
|
|
|
405 |
<div class="rt">Resolved BM / RSM · <span id="rRegion">—</span></div>
|
|
|
406 |
<div class="rn" id="rName">Pick a region…</div>
|
|
|
407 |
<div class="rc" id="rEsc">The owning BM/RSM is resolved from the region (cs.position).</div>
|
|
|
408 |
</div>
|
|
|
409 |
</div>
|
|
|
410 |
<div class="note">If a region has no active BM/RSM, the LMS enters a <b>HOLD queue</b> and the Sales Head is notified.</div>
|
|
|
411 |
</div>
|
|
|
412 |
<div class="card">
|
|
|
413 |
<h2>What happens on submit</h2>
|
|
|
414 |
<div class="trail" style="margin-top:4px">
|
|
|
415 |
<div class="tevt win"><div class="tdot">1</div><div class="tt">Validate fields & generate LMS ID</div><div class="tm">immutable, searchable</div></div>
|
|
|
416 |
<div class="tevt sys"><div class="tdot">2</div><div class="tt">Set stage = Assigned</div><div class="tm">BM/RSM resolved from region</div></div>
|
|
|
417 |
<div class="tevt sys"><div class="tdot">3</div><div class="tt">Dispatch assignment email</div><div class="tm">BM/RSM + CC Reporting Mgr</div></div>
|
|
|
418 |
<div class="tevt call"><div class="tdot">4</div><div class="tt">Start 5 hr SLA timer</div><div class="tm">first-contact clock begins</div></div>
|
|
|
419 |
</div>
|
|
|
420 |
</div>
|
|
|
421 |
</div>
|
|
|
422 |
</div>
|
|
|
423 |
</div>
|
|
|
424 |
|
|
|
425 |
<!-- ======================= AUTO-ASSIGNMENT ========================= -->
|
|
|
426 |
<div id="assign" class="hide">
|
|
|
427 |
<div class="pagehead">
|
|
|
428 |
<div>
|
|
|
429 |
<div class="crumb">Sales Ops · Routing</div>
|
|
|
430 |
<h1 class="page">Auto-Assignment Engine</h1>
|
|
|
431 |
<div class="sub">Region → BM/RSM mapping resolves ownership at submit — from the live cs.position table. HOLD when a region has no active BM.</div>
|
|
|
432 |
</div>
|
|
|
433 |
</div>
|
|
|
434 |
|
|
|
435 |
<div class="kpis" style="grid-template-columns:repeat(4,1fr)" id="assignKpis"></div>
|
|
|
436 |
|
|
|
437 |
<div class="grid g3">
|
|
|
438 |
<div class="card">
|
|
|
439 |
<h2>Region → BM/RSM mapping</h2>
|
|
|
440 |
<p class="desc">The live routing table — resolved for each region right now.</p>
|
|
|
441 |
<table>
|
|
|
442 |
<thead><tr><th>Region / State</th><th>Assigned BM / RSM</th><th>Status</th></tr></thead>
|
|
|
443 |
<tbody id="assignMap"><tr><td colspan="3"><div class="ph">Resolving…</div></td></tr></tbody>
|
|
|
444 |
</table>
|
|
|
445 |
</div>
|
|
|
446 |
<div>
|
|
|
447 |
<div class="card">
|
|
|
448 |
<h2>Engine rules</h2>
|
|
|
449 |
<div class="att" style="margin-bottom:8px"><div class="ic" style="background:#eef1fb;color:#3b5bdb">1</div><div class="ab"><div class="atitle">Resolve at submit</div><div class="adet">Region drives ownership; BM/RSM shown for confirmation.</div></div></div>
|
|
|
450 |
<div class="att" style="margin-bottom:8px"><div class="ic" style="background:#fff4e6;color:#e8590c">2</div><div class="ab"><div class="atitle">Tier fallback</div><div class="adet">L4 → L5 → L3 within the region (cs.position).</div></div></div>
|
|
|
451 |
<div class="att" style="margin-bottom:8px"><div class="ic" style="background:#fff0f0;color:#e03131">3</div><div class="ab"><div class="atitle">HOLD queue</div><div class="adet">No active BM/RSM → LMS held; Sales Head notified.</div></div></div>
|
|
|
452 |
<div class="att" style="margin-bottom:0"><div class="ic" style="background:#f3f0ff;color:#7048e8">4</div><div class="ab"><div class="atitle">Manual reassignment</div><div class="adet">BM/RSM/Sales Head can override — always logged.</div></div></div>
|
|
|
453 |
</div>
|
|
|
454 |
<div class="card" style="background:#fff5f5;border-color:#ffd9d5">
|
|
|
455 |
<h2 style="color:#c92a2a">HOLD queue</h2>
|
|
|
456 |
<p class="desc" style="margin-bottom:8px">LMS with no active BM — waiting until one is mapped.</p>
|
|
|
457 |
<div id="holdQueue"><div class="ph">Loading…</div></div>
|
|
|
458 |
</div>
|
|
|
459 |
</div>
|
|
|
460 |
</div>
|
|
|
461 |
</div>
|
|
|
462 |
|
|
|
463 |
<!-- ======================= DASHBOARDS ========================= -->
|
|
|
464 |
<div id="dash" class="hide">
|
|
|
465 |
<div class="pagehead">
|
|
|
466 |
<div>
|
|
|
467 |
<div class="crumb">Management · Cross-region roll-up</div>
|
|
|
468 |
<h1 class="page">Dashboards</h1>
|
|
|
469 |
<div class="sub">Funnel, SLA compliance, ageing & servicing across all regions — derived from the lead record.</div>
|
|
|
470 |
</div>
|
|
|
471 |
<div class="hd-right">
|
|
|
472 |
<span class="ctl"><span class="lbl">Period</span><span id="dashPeriod">—</span></span>
|
|
|
473 |
<button class="btn ghost sm" onclick="toast('Export uses the app report suite',1)">Export</button>
|
|
|
474 |
</div>
|
|
|
475 |
</div>
|
|
|
476 |
|
|
|
477 |
<div class="kpis" id="dashKpis"></div>
|
|
|
478 |
|
|
|
479 |
<div class="sectionlabel">State funnel — NEW → ONBOARDED by region</div>
|
|
|
480 |
<div class="card">
|
|
|
481 |
<table>
|
|
|
482 |
<thead><tr><th>Region</th><th class="num">New</th><th class="num">Contacted</th><th class="num">Qualified</th><th class="num">Visited</th><th class="num">Onboarded</th><th class="num">Conv%</th><th class="num">SLA%</th></tr></thead>
|
|
|
483 |
<tbody id="dashState"></tbody>
|
|
|
484 |
</table>
|
|
|
485 |
</div>
|
|
|
486 |
|
|
|
487 |
<div class="grid g2">
|
|
|
488 |
<div class="card">
|
|
|
489 |
<h2>SLA compliance — owner heat-map</h2>
|
|
|
490 |
<p class="desc">% first contact within SLA, by BM (rows) across recent weeks (cols). Red = chronic breach.</p>
|
|
|
491 |
<div id="dashHeat"></div>
|
|
|
492 |
<div class="note">SLA = first contact within 5 hrs of creation (SOP §11). Reminder T-1h; escalation to BM at T+5h.</div>
|
|
|
493 |
</div>
|
|
|
494 |
<div class="card">
|
|
|
495 |
<h2>Standard dashboards</h2>
|
|
|
496 |
<p class="desc">SOP §18.2 report suite — Live = from the lead record; Partial/Pending need their source system.</p>
|
|
|
497 |
<table><tbody style="font-size:12.5px">
|
|
|
498 |
<tr class="clickrow" data-rep="State Funnel"><td>📈 <b>State Funnel</b><span class="rbadge live">Live</span></td><td style="color:#69748c">New → onboarded by region, BM, ASM</td></tr>
|
|
|
499 |
<tr class="clickrow" data-rep="SLA Compliance"><td>⚖ <b>SLA Compliance</b><span class="rbadge live">Live</span></td><td style="color:#69748c">% within SLA, owner heat-map</td></tr>
|
|
|
500 |
<tr class="clickrow" data-rep="Ageing"><td>⌛ <b>Ageing</b><span class="rbadge live">Live</span></td><td style="color:#69748c">Stuck by stage, days-since-action</td></tr>
|
|
|
501 |
<tr class="clickrow" data-rep="Management Roll-up"><td>👑 <b>Management Roll-up</b><span class="rbadge live">Live</span></td><td style="color:#69748c">Cross-region leads, onboardings, value</td></tr>
|
|
|
502 |
<tr class="clickrow" data-rep="BGC Daily"><td>📊 <b>BGC Daily</b><span class="rbadge partial">Partial</span></td><td style="color:#69748c">Created + dispositions real; calls are a proxy (no telephony)</td></tr>
|
|
|
503 |
<tr class="clickrow" data-rep="Audit Hygiene"><td>🧹 <b>Audit Hygiene</b><span class="rbadge partial">Partial</span></td><td style="color:#69748c">Missing dispositions / HOLD; DND & recordings pending</td></tr>
|
|
|
504 |
<tr class="clickrow" data-rep="RBM Servicing"><td>🔄 <b>RBM Servicing</b><span class="rbadge soon">Pending data</span></td><td style="color:#69748c">Repeat-order / complaint / TAT need order data</td></tr>
|
|
|
505 |
</tbody></table>
|
|
|
506 |
</div>
|
|
|
507 |
</div>
|
|
|
508 |
|
|
|
509 |
<div class="sectionlabel">What to fix</div>
|
|
|
510 |
<div class="callout">
|
|
|
511 |
<h2>Recommended actions — from this period's data</h2>
|
|
|
512 |
<p class="desc">Each links to the section above.</p>
|
|
|
513 |
<div id="dashFix"></div>
|
|
|
514 |
</div>
|
|
|
515 |
</div>
|
|
|
516 |
|
|
|
517 |
<!-- ======================= RECORDINGS & AUDIT ========================= -->
|
|
|
518 |
<div id="audit" class="hide">
|
|
|
519 |
<div class="pagehead">
|
|
|
520 |
<div>
|
|
|
521 |
<div class="crumb">Audit · Access Control</div>
|
|
|
522 |
<h1 class="page">Recordings & Audit</h1>
|
|
|
523 |
<div class="sub">Recordings are gated by role + reporting hierarchy; every open is logged. Hygiene flags are live; the recording repo is a pending source system.</div>
|
|
|
524 |
</div>
|
|
|
525 |
</div>
|
|
|
526 |
|
|
|
527 |
<div class="kpis" style="grid-template-columns:repeat(5,1fr)" id="auditKpis"></div>
|
|
|
528 |
|
|
|
529 |
<div class="grid g2">
|
|
|
530 |
<div class="card">
|
|
|
531 |
<h2>Access & recording visibility matrix</h2>
|
|
|
532 |
<p class="desc">What each role can open. Recording access is scoped by role + reporting hierarchy (SOP §15).</p>
|
|
|
533 |
<table class="matrix">
|
|
|
534 |
<thead><tr><th>Role</th><th>Own LMS</th><th>Reports'</th><th>State-wide</th><th>All records</th><th>Recordings</th></tr></thead>
|
|
|
535 |
<tbody>
|
|
|
536 |
<tr><td>Individual (BGC/ASM)</td><td class="mk-y">✓</td><td class="mk-n">—</td><td class="mk-n">—</td><td class="mk-n">—</td><td class="mk-s">own only</td></tr>
|
|
|
537 |
<tr><td>Reporting Manager</td><td class="mk-y">✓</td><td class="mk-y">✓</td><td class="mk-n">—</td><td class="mk-n">—</td><td class="mk-s">reports'</td></tr>
|
|
|
538 |
<tr><td>BM / RSM</td><td class="mk-y">✓</td><td class="mk-y">✓</td><td class="mk-y">✓</td><td class="mk-n">—</td><td class="mk-s">state-wide</td></tr>
|
|
|
539 |
<tr><td>Audit Team</td><td class="mk-y">✓</td><td class="mk-y">✓</td><td class="mk-y">✓</td><td class="mk-y">✓</td><td class="mk-s">all · read-only</td></tr>
|
|
|
540 |
<tr><td>Upper Management</td><td class="mk-y">✓</td><td class="mk-y">✓</td><td class="mk-y">✓</td><td class="mk-y">✓</td><td class="mk-s">via BI drill</td></tr>
|
|
|
541 |
</tbody>
|
|
|
542 |
</table>
|
|
|
543 |
<div class="note">Recording repository (signed URLs, DND enforcement, 24-mo retention) is a Phase-2 source system — not yet wired to real data.</div>
|
|
|
544 |
</div>
|
|
|
545 |
|
|
|
546 |
<div class="card">
|
|
|
547 |
<h2>Weekly hygiene flags <span style="color:#97a1b5;font-weight:500;font-size:12px">· live</span></h2>
|
|
|
548 |
<p class="desc">Derived from the lead record — click to list the leads.</p>
|
|
|
549 |
<div id="auditHygiene"><div class="ph">Loading…</div></div>
|
|
|
550 |
</div>
|
|
|
551 |
</div>
|
|
|
552 |
|
|
|
553 |
<div class="grid g2">
|
|
|
554 |
<div class="card">
|
|
|
555 |
<h2>Audit & compliance program (SOP §17)</h2>
|
|
|
556 |
<div class="att" style="margin-bottom:8px"><div class="ic" style="background:#e6fcf5;color:#0ca678">🎵</div><div class="ab"><div class="atitle">Weekly recording sampling</div><div class="adet">Across BGC, ASM & RBM for quality & process adherence.</div></div></div>
|
|
|
557 |
<div class="att" style="margin-bottom:8px"><div class="ic" style="background:#e6fcf5;color:#0ca678">🧹</div><div class="ab"><div class="atitle">5% deep audit of onboarded</div><div class="adet">Verify data integrity & process compliance.</div></div></div>
|
|
|
558 |
<div class="att" style="margin-bottom:8px"><div class="ic" style="background:#fff0f0;color:#e03131">🚫</div><div class="ab"><div class="atitle">DND enforcement</div><div class="adet">Audit maintains the list; repeat calls auto-flag the user.</div></div></div>
|
|
|
559 |
<div class="att" style="margin-bottom:0"><div class="ic" style="background:#eef1f6;color:#69748c">🗃</div><div class="ab"><div class="atitle">Retention policy</div><div class="adet">24 months for recordings · 5 years for LMS records.</div></div></div>
|
|
|
560 |
</div>
|
|
|
561 |
<div class="card">
|
|
|
562 |
<h2>Recording repository <span style="color:#97a1b5;font-weight:500;font-size:12px">· pending source</span></h2>
|
|
|
563 |
<p class="desc">Each recording would be tagged with LMS ID, user, timestamp, duration & disposition.</p>
|
|
|
564 |
<div class="ph">No recording repository connected yet — this arrives with the C2C telephony bridge (Phase 2/3).</div>
|
|
|
565 |
</div>
|
|
|
566 |
</div>
|
|
|
567 |
</div>
|
|
|
568 |
|
|
|
569 |
</div>
|
|
|
570 |
|
|
|
571 |
<!-- drawer + toast -->
|
|
|
572 |
<div class="drawer-ov" id="ov" onclick="closeDrawer()"></div>
|
|
|
573 |
<div class="drawer" id="drawer">
|
|
|
574 |
<div class="dh"><div><div class="crumb2" id="dCrumb"></div><h3 id="dTitle"></h3><div class="dsub" id="dSub"></div></div><button class="x" onclick="closeDrawer()">×</button></div>
|
|
|
575 |
<div class="dbody" id="dBody"></div>
|
|
|
576 |
</div>
|
|
|
577 |
<div class="toastwrap" id="toasts"></div>
|
|
|
578 |
|
|
|
579 |
<script>
|
|
|
580 |
var CTX = "${rc.contextPath}";
|
|
|
581 |
var SEED = { summary:$summaryJson, state:$stateFunnelJson, heat:$slaHeatmapJson, ageing:$ageingJson };
|
|
|
582 |
var STATE = { regionId:'', from:'$!fromDate', to:'$!toDate' };
|
|
|
583 |
var DATA = { summary:null, state:null, heat:null, ageing:null };
|
|
|
584 |
|
|
|
585 |
function byId(x){ return document.getElementById(x); }
|
|
|
586 |
function esc(s){ if(s==null) return ''; return (''+s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];}); }
|
|
|
587 |
function fmtInt(n){ if(n==null) return '0'; return (''+n).replace(/\B(?=(\d{3})+(?!\d))/g,','); }
|
|
|
588 |
function fmtInr(v){ if(v==null||v===''||isNaN(v)||Number(v)<=0) return '₹0'; v=Number(v);
|
|
|
589 |
if(v>=1e7) return '₹'+(v/1e7).toFixed(1)+'<small> Cr</small>'; if(v>=1e5) return '₹'+(v/1e5).toFixed(1)+'<small> L</small>';
|
|
|
590 |
if(v>=1e3) return '₹'+Math.round(v/1e3)+'<small>K</small>'; return '₹'+Math.round(v); }
|
|
|
591 |
function inrShort(v){ if(v==null||isNaN(v)||Number(v)<=0) return '₹0'; v=Number(v);
|
|
|
592 |
if(v>=1e7) return '₹'+(v/1e7).toFixed(1)+'Cr'; if(v>=1e5) return '₹'+(v/1e5).toFixed(1)+'L';
|
|
|
593 |
if(v>=1e3) return '₹'+Math.round(v/1e3)+'k'; return '₹'+Math.round(v); }
|
|
|
594 |
|
|
|
595 |
// ---- tabs
|
|
|
596 |
var SCREENS=['cmd','record','create','assign','dash','audit'];
|
|
|
597 |
var LOADED={};
|
|
|
598 |
function go(id,btn){ SCREENS.forEach(function(s){ byId(s).classList.toggle('hide',s!==id); });
|
|
|
599 |
document.querySelectorAll('.tab').forEach(function(t){ t.classList.remove('active'); }); if(btn) btn.classList.add('active');
|
|
|
600 |
if(id==='dash') byId('dashPeriod').textContent=(STATE.from||'')+' → '+(STATE.to||'');
|
|
|
601 |
if(id==='record' && !LOADED.record){ LOADED.record=true; loadRecentRecord(); }
|
|
|
602 |
if(id==='assign' && !LOADED.assign){ LOADED.assign=true; buildAssign(); }
|
|
|
603 |
if(id==='audit'){ buildAudit(); } }
|
|
|
604 |
|
|
|
605 |
// ---- toast
|
|
|
606 |
function toast(msg,ok){ var w=byId('toasts'); var t=document.createElement('div'); t.className='toast';
|
|
|
607 |
t.innerHTML=(ok?'<span class="ok">✓</span>':'')+esc(msg); w.appendChild(t);
|
|
|
608 |
setTimeout(function(){ t.style.opacity='0'; t.style.transition='opacity .3s'; setTimeout(function(){ t.remove(); },300); },2600); }
|
|
|
609 |
|
|
|
610 |
// ---- filters + fetch
|
|
|
611 |
function qs(){ var q=''; if(STATE.regionId)q+='®ionId='+encodeURIComponent(STATE.regionId);
|
|
|
612 |
if(STATE.from)q+='&from='+encodeURIComponent(STATE.from); if(STATE.to)q+='&to='+encodeURIComponent(STATE.to); return q; }
|
|
|
613 |
function getJSON(path,cb){ fetch(CTX+path,{credentials:'same-origin',headers:{'X-Requested-With':'XMLHttpRequest'}})
|
|
|
614 |
.then(function(r){ return r.json(); }).then(function(j){ cb((j&&j.response!==undefined)?j.response:j); })
|
|
|
615 |
.catch(function(e){ console.error('LMS fetch failed',path,e); toast('Could not load data'); }); }
|
|
|
616 |
|
|
|
617 |
// ---- stage helpers
|
|
|
618 |
var STCLASS={NEW:'st-new',ASSIGNED:'st-assigned',CONTACTED:'st-contacted',QUALIFIED:'st-qualified',
|
|
|
619 |
BEAT_PLANNED:'st-beat',VISITED:'st-visited',ONBOARDED:'st-onboarded',ACTIVE:'st-active',NOT_INTERESTED:'st-ni',DROPPED:'st-dead'};
|
|
|
620 |
function stagePill(st,lb){ if(!st) return '<span class="stage st-new">—</span>'; return '<span class="stage '+(STCLASS[st]||'st-new')+'">'+esc(lb||st)+'</span>'; }
|
|
|
621 |
function slaCell(state){ switch(state){
|
|
|
622 |
case'MET': return '<span class="sla ok"><span class="slabar"><i style="width:100%;background:#2f9e44"></i></span><span class="clk">met</span></span>';
|
|
|
623 |
case'RUNNING': return '<span class="sla ok"><span class="slabar"><i style="width:50%;background:#1f2a5e"></i></span><span class="clk">running</span></span>';
|
|
|
624 |
case'AT_RISK': return '<span class="sla warn"><span class="slabar"><i style="width:85%;background:#f08c00"></i></span><span class="clk">< 1 hr</span></span>';
|
|
|
625 |
case'BREACHED': return '<span class="sla bad"><span class="slabar"><i style="width:100%;background:#e03131"></i></span><span class="clk">breached</span></span>';
|
|
|
626 |
default: return '<span style="color:#97a1b5;font-size:12px">—</span>'; } }
|
|
|
627 |
|
|
|
628 |
// ---- COMMAND CENTER
|
|
|
629 |
var CC_KPI={ 'new':{i:'📥',bg:'#eef1fb',fg:'#3b5bdb'}, awaiting:{i:'⏳',bg:'#fff4e6',fg:'#f08c00',v:'#f08c00'},
|
|
|
630 |
sla:{i:'⚠',bg:'#fff0f0',fg:'#e03131',v:'#e03131'}, contacted:{i:'📞',bg:'#e3fafc',fg:'#0c8599'},
|
|
|
631 |
qualified:{i:'✓',bg:'#e6fcf5',fg:'#0ca678'}, onboarded:{i:'🏆',bg:'#ebfbee',fg:'#2f9e44'} };
|
|
|
632 |
var ATT_META={ BREACHED_NO_CONTACT:{i:'⚠',bg:'#fff0f0',fg:'#e03131',c:'badflag'}, HOLD:{i:'👤',bg:'#fff0f0',fg:'#e03131',c:'badflag'},
|
|
|
633 |
SLA_UNDER_1H:{i:'⏳',bg:'#fff4e6',fg:'#e8590c',c:'warnflag'}, MISSING_DISP:{i:'💬',bg:'#fff4e6',fg:'#e8590c',c:'warnflag'},
|
|
|
634 |
QUALIFIED_NO_BEAT:{i:'🕑',bg:'#eef1f6',fg:'#69748c',c:''}, ONBOARDED_PENDING:{i:'🔄',bg:'#f3f0ff',fg:'#7048e8',c:''} };
|
|
|
635 |
|
|
|
636 |
function renderSummary(d){ DATA.summary=d;
|
|
|
637 |
var kh=''; (d.kpis||[]).forEach(function(k){ var m=CC_KPI[k.key]||{i:'📊',bg:'#eef1f6',fg:'#69748c'};
|
|
|
638 |
kh+='<div class="kpi" data-kpi="'+esc(k.key)+'"><div class="top"><div class="ic" style="background:'+m.bg+';color:'+m.fg+'">'+m.i+'</div>'+
|
|
|
639 |
'<div class="label">'+esc(k.label)+'</div></div><div class="val"'+(m.v?' style="color:'+m.v+'"':'')+'>'+fmtInt(k.value)+'</div>'+
|
|
|
640 |
'<div class="foot">'+esc(k.foot)+'</div></div>'; });
|
|
|
641 |
byId('ccKpis').innerHTML=kh;
|
|
|
642 |
renderFunnel(d.funnel||[]);
|
|
|
643 |
var ah=''; (d.attention||[]).forEach(function(a){ var m=ATT_META[a.key]||{i:'📊',bg:'#eef1f6',fg:'#69748c',c:''};
|
|
|
644 |
ah+='<div class="att" data-att="'+esc(a.key)+'" data-attt="'+esc(a.title)+'"><div class="ic" style="background:'+m.bg+';color:'+m.fg+'">'+m.i+'</div>'+
|
|
|
645 |
'<div class="ab"><div class="atitle">'+esc(a.title)+'</div><div class="adet">'+esc(a.detail)+'</div></div>'+
|
|
|
646 |
'<div class="acount '+m.c+'">'+fmtInt(a.count)+'</div></div>'; });
|
|
|
647 |
byId('ccAttention').innerHTML=ah;
|
|
|
648 |
buildDashKpis(); buildFix();
|
|
|
649 |
}
|
|
|
650 |
|
|
|
651 |
function renderFunnel(funnel){
|
|
|
652 |
var byStage={}; funnel.forEach(function(s){ byStage[s.stage]=s; });
|
|
|
653 |
var seq=[['ALL','New'],['ASSIGNED','Assigned'],['CONTACTED','Contacted'],['QUALIFIED','Qualified'],['VISITED','Visited'],['ONBOARDED','Onboarded']];
|
|
|
654 |
var h=''; var prev=null;
|
|
|
655 |
seq.forEach(function(p,idx){ var s=byStage[p[0]]; if(!s) return; var last=(idx===seq.length-1);
|
|
|
656 |
var cap; if(idx===0){ cap='<div class="keep">created</div>'; }
|
|
|
657 |
else if(last){ cap='<div class="keep">'+s.pct+'% conversion</div>'; }
|
|
|
658 |
else { var drop=prev?prev.count-s.count:0; cap=drop>0?('<div class="drop">−'+fmtInt(drop)+'</div>'):('<div class="keep">'+s.pct+'%</div>'); }
|
|
|
659 |
h+='<div class="fstep" data-stage="'+p[0]+'" data-label="'+esc(p[1])+'"><div class="fn">'+esc(p[1])+'</div><div class="fv">'+fmtInt(s.count)+'</div>'+
|
|
|
660 |
'<div class="fbar" style="width:'+Math.max(s.pct,3)+'%"></div>'+cap+(last?'':'<span class="arrow">➤</span>')+'</div>';
|
|
|
661 |
prev=s; });
|
|
|
662 |
byId('ccFunnel').innerHTML=h||'<div class="ph">No leads in scope.</div>';
|
|
|
663 |
}
|
|
|
664 |
|
|
|
665 |
function loadPipeline(){ byId('ccPipeline').innerHTML='<tr><td colspan="7"><div class="ph">Loading…</div></td></tr>';
|
|
|
666 |
getJSON('/lms/dashboard/leads?bucketType=ALL'+qs(),function(d){ var leads=(d&&d.leads)?d.leads:[];
|
|
|
667 |
if(!leads.length){ byId('ccPipeline').innerHTML='<tr><td colspan="7"><div class="ph">No leads in scope.</div></td></tr>'; return; }
|
|
|
668 |
var h=''; leads.slice(0,12).forEach(function(l){ var rc=l.slaState==='BREACHED'?'rowbad':(l.slaState==='AT_RISK'?'rowwarn':'');
|
|
|
669 |
h+='<tr class="clickrow '+rc+'" data-leadid="'+l.id+'">'+
|
|
|
670 |
'<td><span class="idpill">'+esc(l.lmsCode)+'</span></td>'+
|
|
|
671 |
'<td><b>'+esc(l.name||'—')+'</b>'+(l.outlet?'<br><span style="color:#97a1b5;font-size:11px">'+esc(l.outlet)+'</span>':'')+'</td>'+
|
|
|
672 |
'<td>'+(l.path==='B'?'<span class="tag b">B · Field</span>':'<span class="tag a">A · BGC</span>')+'</td>'+
|
|
|
673 |
'<td>'+stagePill(l.stage,l.stageLabel)+'</td>'+
|
|
|
674 |
'<td>'+esc(l.owner||'—')+'</td>'+
|
|
|
675 |
'<td>'+slaCell(l.slaState)+'</td>'+
|
|
|
676 |
'<td class="num">'+inrShort(l.value)+'</td></tr>'; });
|
|
|
677 |
byId('ccPipeline').innerHTML=h;
|
|
|
678 |
}); }
|
|
|
679 |
|
|
|
680 |
function kpiBucket(k){ switch(k){case'new':return['STAGE','NEW'];case'awaiting':return['STAGE','ASSIGNED'];
|
|
|
681 |
case'sla':return['ATTENTION','BREACHED_NO_CONTACT'];case'contacted':return['STAGE','CONTACTED'];
|
|
|
682 |
case'qualified':return['STAGE','QUALIFIED'];case'onboarded':return['STAGE','ONBOARDED'];default:return['ALL','']; } }
|
|
|
683 |
|
|
|
684 |
// ---- DASHBOARDS
|
|
|
685 |
var DASH_KPI=[ {i:'📥',bg:'#eef1fb',fg:'#3b5bdb'}, {i:'📈',bg:'#e6fcf5',fg:'#0ca678',v:'#0ca678'},
|
|
|
686 |
{i:'⚖',bg:'#fff4e6',fg:'#f08c00',v:'#f08c00'}, {i:'📞',bg:'#e3fafc',fg:'#0c8599'},
|
|
|
687 |
{i:'⌛',bg:'#fff0f0',fg:'#e03131',v:'#e03131'}, {i:'💰',bg:'#ebfbee',fg:'#2f9e44'} ];
|
|
|
688 |
function pctColor(p){ return p>=25?'#2f9e44':(p>=18?'#f08c00':(p>=10?'#e8590c':'#e03131')); }
|
|
|
689 |
function slaColor(p){ return p>=85?'#2f9e44':(p>=70?'#f08c00':'#e03131'); }
|
|
|
690 |
|
|
|
691 |
function renderState(d){ DATA.state=d; var rows=(d&&d.rows)?d.rows:[];
|
|
|
692 |
var h=''; rows.forEach(function(r){ var cls=r.convPct<15?'rowbad':(r.slaPct<60?'rowwarn':'');
|
|
|
693 |
h+='<tr class="clickrow '+cls+'" data-region="'+esc(r.regionCode)+'" data-rn="'+esc(r.regionName)+'">'+
|
|
|
694 |
'<td><b>'+esc(r.regionName)+'</b> <span class="idpill">'+esc(r.regionCode)+'</span></td>'+
|
|
|
695 |
'<td class="num">'+fmtInt(r.newCount)+'</td><td class="num">'+fmtInt(r.contacted)+'</td><td class="num">'+fmtInt(r.qualified)+'</td>'+
|
|
|
696 |
'<td class="num">'+fmtInt(r.visited)+'</td><td class="num">'+fmtInt(r.onboarded)+'</td>'+
|
|
|
697 |
'<td class="num"><b style="color:'+pctColor(r.convPct)+'">'+r.convPct+'%</b></td>'+
|
|
|
698 |
'<td class="num"><b style="color:'+slaColor(r.slaPct)+'">'+r.slaPct+'%</b></td></tr>'; });
|
|
|
699 |
byId('dashState').innerHTML=h||'<tr><td colspan="8"><div class="ph">No regional data.</div></td></tr>';
|
|
|
700 |
buildDashKpis(); buildFix();
|
|
|
701 |
}
|
|
|
702 |
|
|
|
703 |
function renderHeat(d){ DATA.heat=d; var weeks=(d&&d.weeks)?d.weeks:[],bms=(d&&d.bms)?d.bms:[];
|
|
|
704 |
if(!bms.length){ byId('dashHeat').innerHTML='<div class="ph">No SLA data for the selected period.</div>'; return; }
|
|
|
705 |
function hc(p){ if(p>=90)return'#40c057';if(p>=80)return'#82c91e';if(p>=70)return'#fab005';if(p>=60)return'#fd7e14';if(p>=50)return'#fa5252';return'#e03131'; }
|
|
|
706 |
var h='<table class="heat"><thead><tr><td style="background:none;border:0"></td>';
|
|
|
707 |
weeks.forEach(function(w){ h+='<td style="background:none;border:0;color:#97a1b5;font-size:10px;font-weight:700">'+esc(w)+'</td>'; });
|
|
|
708 |
h+='</tr></thead><tbody>';
|
|
|
709 |
bms.forEach(function(bm){ h+='<tr><td style="background:none;color:#69748c;font-size:11px;text-align:left;width:auto;border:0">'+esc(bm.bmName)+'</td>';
|
|
|
710 |
(bm.cells||[]).forEach(function(c){ if(c.pct==null){ h+='<td style="background:#eef1f6;color:#97a1b5">–</td>'; }
|
|
|
711 |
else { var col=hc(c.pct),tc=(c.pct>=70&&c.pct<80)?'#333':'#fff'; h+='<td style="background:'+col+';color:'+tc+'">'+c.pct+'</td>'; } }); h+='</tr>'; });
|
|
|
712 |
h+='</tbody></table>'; byId('dashHeat').innerHTML=h;
|
|
|
713 |
}
|
|
|
714 |
|
|
|
715 |
function renderAgeing(d){ DATA.ageing=d; buildDashKpis(); buildFix(); }
|
|
|
716 |
|
|
|
717 |
function buildDashKpis(){ var s=DATA.summary; if(!s) return;
|
|
|
718 |
var contacted=0; (s.funnel||[]).forEach(function(f){ if(f.stage==='CONTACTED')contacted=f.count; });
|
|
|
719 |
var tot=0,acc=0; ((DATA.state&&DATA.state.rows)||[]).forEach(function(r){ tot+=r.newCount; acc+=r.slaPct*r.newCount; });
|
|
|
720 |
var sla=tot>0?Math.round(acc/tot):0;
|
|
|
721 |
var vals=[ {v:fmtInt(s.total),f:'in period'}, {v:s.conversionPct+'<small>%</small>',f:'new → onboarded'},
|
|
|
722 |
{v:sla+'<small>%</small>',f:'first contact ≤ 5h'}, {v:fmtInt(Math.round(contacted/90)),f:'proxy · no telephony'},
|
|
|
723 |
{v:DATA.ageing?fmtInt(DATA.ageing.total):'—',f:'stuck by stage'}, {v:fmtInr(s.onboardedValue),f:'est. potential'} ];
|
|
|
724 |
var labels=['LMS created','Conversion','SLA compliance','Calls / day (BGC)','Ageing > 7 days','Onboarded value'];
|
|
|
725 |
var h=''; vals.forEach(function(x,i){ var m=DASH_KPI[i];
|
|
|
726 |
h+='<div class="kpi"><div class="top"><div class="ic" style="background:'+m.bg+';color:'+m.fg+'">'+m.i+'</div>'+
|
|
|
727 |
'<div class="label">'+labels[i]+'</div></div><div class="val"'+(m.v?' style="color:'+m.v+'"':'')+'>'+x.v+'</div><div class="foot">'+x.f+'</div></div>'; });
|
|
|
728 |
byId('dashKpis').innerHTML=h;
|
|
|
729 |
}
|
|
|
730 |
|
|
|
731 |
function buildFix(){ var rows=((DATA.state&&DATA.state.rows)||[]).slice();
|
|
|
732 |
var out='';
|
|
|
733 |
if(rows.length){ var worst=rows.slice().sort(function(a,b){return a.convPct-b.convPct;})[0];
|
|
|
734 |
out+='<div class="fix"><div class="sev" style="background:#e03131"></div><div class="fb"><div class="ft">'+esc(worst.regionName)+' is leaking at the top</div>'+
|
|
|
735 |
'<div class="fd"><b>'+fmtInt(worst.newCount)+' created, only '+worst.convPct+'% onboarded and '+worst.slaPct+'% SLA</b> — the weakest region this period.</div>'+
|
|
|
736 |
'<div class="act">→ Review its stuck leads; check BM mapping / HOLD queue.</div></div></div>';
|
|
|
737 |
var lowSla=rows.slice().sort(function(a,b){return a.slaPct-b.slaPct;})[0];
|
|
|
738 |
if(lowSla && lowSla.regionCode!==worst.regionCode){ out+='<div class="fix"><div class="sev" style="background:#f59f00"></div><div class="fb"><div class="ft">'+esc(lowSla.regionName)+' SLA slipping</div>'+
|
|
|
739 |
'<div class="fd"><b>'+lowSla.slaPct+'% on-time first contact</b> — first-contact discipline is breaking down.</div>'+
|
|
|
740 |
'<div class="act">→ Enforce ASM mapping (≤2h) + coaching.</div></div></div>'; } }
|
|
|
741 |
var age=DATA.ageing?DATA.ageing.total:0;
|
|
|
742 |
out+='<div class="fix"><div class="sev" style="background:#f59f00"></div><div class="fb"><div class="ft">'+fmtInt(age)+' leads ageing > 7 days</div>'+
|
|
|
743 |
'<div class="fd"><b>Stuck open leads by stage</b> — qualified leads not being added to beats lose revival odds.</div>'+
|
|
|
744 |
'<div class="act">→ Nudge ASMs to beat-plan qualified LMS within 48h.</div></div></div>';
|
|
|
745 |
byId('dashFix').innerHTML=out;
|
|
|
746 |
}
|
|
|
747 |
|
|
|
748 |
// ---- drill drawer
|
|
|
749 |
function openDrawer(crumb,title,sub,html){ byId('dCrumb').textContent=crumb||''; byId('dTitle').textContent=title||'';
|
|
|
750 |
byId('dSub').textContent=sub||''; byId('dBody').innerHTML=html||''; byId('ov').classList.add('open'); byId('drawer').classList.add('open'); }
|
|
|
751 |
function closeDrawer(){ byId('ov').classList.remove('open'); byId('drawer').classList.remove('open'); }
|
|
|
752 |
document.addEventListener('keydown',function(e){ if(e.key==='Escape')closeDrawer();
|
|
|
753 |
if(e.key==='/'&&document.activeElement.tagName!=='INPUT'){ e.preventDefault(); byId('lmsSearch').focus(); } });
|
|
|
754 |
|
|
|
755 |
function drawerLeadTable(leads){ if(!leads||!leads.length) return '<div class="ph">No leads in this bucket.</div>';
|
|
|
756 |
var h='<div class="card"><table><thead><tr><th>LMS ID</th><th>Business</th><th>Stage</th><th>Owner</th><th>SLA</th><th class="num">Value</th></tr></thead><tbody>';
|
|
|
757 |
leads.forEach(function(l){ h+='<tr class="clickrow" data-leadid="'+l.id+'"><td><span class="idpill">'+esc(l.lmsCode)+'</span></td>'+
|
|
|
758 |
'<td><b>'+esc(l.name||'—')+'</b>'+(l.outlet?'<br><span style="color:#97a1b5;font-size:11px">'+esc(l.outlet)+'</span>':'')+'</td>'+
|
|
|
759 |
'<td>'+stagePill(l.stage,l.stageLabel)+'</td><td>'+esc(l.owner||'—')+'</td><td>'+slaCell(l.slaState)+'</td>'+
|
|
|
760 |
'<td class="num">'+inrShort(l.value)+'</td></tr>'; });
|
|
|
761 |
return h+'</tbody></table></div>'; }
|
|
|
762 |
function drill(crumb,title,sub,bt,bk){ openDrawer(crumb,title,'Loading…','<div class="ph">Loading…</div>');
|
|
|
763 |
var q='?bucketType='+encodeURIComponent(bt)+(bk?'&bucketKey='+encodeURIComponent(bk):'')+qs();
|
|
|
764 |
getJSON('/lms/dashboard/leads'+q,function(d){ var leads=(d&&d.leads)?d.leads:[];
|
|
|
765 |
openDrawer(crumb,title,(sub||'')+' · '+leads.length+' lead(s)',drawerLeadTable(leads)); }); }
|
|
|
766 |
|
|
|
767 |
var REPORT_NOTE={ 'State Funnel':'Live from the lead record — grouped by region, BM and ASM.',
|
|
|
768 |
'SLA Compliance':'Live — first_contacted_at vs first_contact_due, owner heat-map.',
|
|
|
769 |
'Ageing':'Live — open leads untouched > 7 days, by stage.',
|
|
|
770 |
'Management Roll-up':'Live — cross-region leads, onboardings and estimated value.',
|
|
|
771 |
'BGC Daily':'Partial — created + dispositions are real; call volume is a PROXY (contacted ÷ days). No telephony bridge yet.',
|
|
|
772 |
'Audit Hygiene':'Partial — missing-disposition & HOLD are shown; DND breaches & recording flags need the call/recording log.',
|
|
|
773 |
'RBM Servicing':'Pending data — repeat-order / complaint / resolution-TAT need order + ticket data outside the lead record.' };
|
|
|
774 |
|
|
|
775 |
// ---- delegated clicks
|
|
|
776 |
document.addEventListener('click',function(e){
|
|
|
777 |
var kpi=e.target.closest('.kpi[data-kpi]'); if(kpi){ var b=kpiBucket(kpi.getAttribute('data-kpi'));
|
|
|
778 |
drill('Command Center',kpi.querySelector('.label').textContent,'KPI drill',b[0],b[1]); return; }
|
|
|
779 |
var fs=e.target.closest('.fstep[data-stage]'); if(fs){ var st=fs.getAttribute('data-stage');
|
|
|
780 |
drill('Lifecycle funnel',fs.getAttribute('data-label'),'Reached this stage',st==='ALL'?'ALL':'STAGE',st==='ALL'?'':st); return; }
|
|
|
781 |
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; }
|
|
|
782 |
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; }
|
|
|
783 |
var rep=e.target.closest('tr[data-rep]'); if(rep){ var nm=rep.getAttribute('data-rep');
|
|
|
784 |
openDrawer('Report suite',nm,'SOP §18.2','<div class="card"><div class="dnote">'+esc(REPORT_NOTE[nm]||'')+'</div></div>'); return; }
|
|
|
785 |
var lead=e.target.closest('[data-leadid]'); if(lead){ var lid=lead.getAttribute('data-leadid');
|
|
|
786 |
closeDrawer(); LOADED.record=true; byId('recLookup').value=lid; injectRecord(lid);
|
|
|
787 |
go('record',document.querySelectorAll('.tab')[1]); return; }
|
|
|
788 |
});
|
|
|
789 |
|
|
|
790 |
// ---- filters
|
|
|
791 |
function refresh(){ STATE.regionId=byId('fRegion').value||''; STATE.from=byId('fFrom').value||''; STATE.to=byId('fTo').value||'';
|
|
|
792 |
getJSON('/lms/dashboard/summary?_=1'+qs(),renderSummary); loadPipeline();
|
|
|
793 |
getJSON('/lms/dashboard/state-funnel?_=1'+qs(),renderState);
|
|
|
794 |
getJSON('/lms/dashboard/sla-heatmap?_=1'+qs(),renderHeat);
|
|
|
795 |
getJSON('/lms/dashboard/ageing',renderAgeing);
|
|
|
796 |
byId('dashPeriod').textContent=(STATE.from||'')+' → '+(STATE.to||''); }
|
|
|
797 |
['fRegion','fFrom','fTo'].forEach(function(id){ byId(id).addEventListener('change',refresh); });
|
|
|
798 |
|
|
|
799 |
// ====================== LEAD RECORD (native sts-mockup layout, real data) ======================
|
|
|
800 |
var STEPS=[['NEW','New'],['ASSIGNED','Assigned'],['CONTACTED','Contacted'],['QUALIFIED','Qualified'],
|
|
|
801 |
['BEAT_PLANNED','Beat-plan'],['VISITED','Visited'],['ONBOARDED','Onboarded'],['ACTIVE','Active']];
|
|
|
802 |
function fmtDT(s){ if(!s) return '—'; var d=new Date(s); if(isNaN(d)) return esc(s);
|
|
|
803 |
var mo=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
|
804 |
function p(n){ return (n<10?'0':'')+n; }
|
|
|
805 |
return p(d.getDate())+' '+mo[d.getMonth()]+' '+d.getFullYear()+' · '+p(d.getHours())+':'+p(d.getMinutes()); }
|
|
|
806 |
function kvRow(k,v){ return '<div class="k">'+k+'</div><div class="v">'+(v==null||v===''?'—':v)+'</div>'; }
|
|
|
807 |
function trailType(t){ if(t==='TELEPHONIC'||t==='CALL') return 'call'; if(!t) return 'sys'; return ''; }
|
|
|
808 |
|
|
|
809 |
function injectRecord(id){ byId('recordHost').innerHTML='<div class="ph">Loading lead #'+esc(id)+'…</div>';
|
|
|
810 |
getJSON('/lms/dashboard/record?leadId='+encodeURIComponent(id),function(d){ if(!d||!d.id){ byId('recordHost').innerHTML='<div class="ph">Lead #'+esc(id)+' not found.</div>'; return; } renderRecord(d); }); }
|
|
|
811 |
function loadRecord(){ var v=(byId('recLookup').value||'').replace(/\D/g,''); if(!v){ toast('Enter a lead #'); return; } injectRecord(v); }
|
|
|
812 |
function loadRecentRecord(){ getJSON('/lms/dashboard/leads?bucketType=ALL'+qs(),function(d){ var leads=(d&&d.leads)?d.leads:[];
|
|
|
813 |
if(leads.length){ byId('recLookup').value=leads[0].id; injectRecord(leads[0].id); }
|
|
|
814 |
else { byId('recordHost').innerHTML='<div class="ph">No leads to show. Create one from the Create Lead tab.</div>'; } }); }
|
|
|
815 |
|
|
|
816 |
function renderRecord(d){
|
|
|
817 |
// header
|
|
|
818 |
var head='<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:14px">'+
|
|
|
819 |
'<div><div class="crumb">'+(d.lmsCode?('<span class="idpill">'+esc(d.lmsCode)+'</span>'):('Lead #'+d.id))+'</div>'+
|
|
|
820 |
'<h1 class="page" style="margin-top:4px">'+esc(d.name||'—')+(d.outlet?' <span style="font-weight:500;color:#59668a;font-size:18px">· '+esc(d.outlet)+'</span>':'')+'</h1></div>'+
|
|
|
821 |
'<div style="display:flex;gap:8px;align-items:center">'+stagePill(d.stage,d.stageLabel)+
|
|
|
822 |
' <span class="stage" style="background:#eef1f6;color:#69748c">SLA: '+esc(d.slaState||'NA')+'</span>'+
|
|
|
823 |
(d.assignmentStatus==='HOLD'?' <span class="stage st-ni">HOLD</span>':'')+'</div></div>';
|
|
|
824 |
// stepper
|
|
|
825 |
var idx=(d.stageIndex==null?-1:d.stageIndex);
|
|
|
826 |
var step='<div class="card" style="padding:16px 22px"><div class="rec-stepper">';
|
|
|
827 |
if(d.terminal){ step+='<div style="font-weight:700;color:#e8590c">'+esc(d.stageLabel)+' · terminal state</div>'; }
|
|
|
828 |
else { STEPS.forEach(function(s,i){ var cls=i<idx?'done':(i===idx?'cur':''); var last=(i===STEPS.length-1);
|
|
|
829 |
step+='<div class="rec-step '+cls+'"><div class="dot">'+(i<idx?'✓':(i+1))+'</div><div class="sl">'+esc(s[1])+'</div>'+(last?'':'<div class="bar"></div>')+'</div>'; }); }
|
|
|
830 |
step+='</div></div>';
|
|
|
831 |
// details
|
|
|
832 |
var details='<div class="card"><h2>Record</h2><p class="desc">Mandatory fields captured at creation. ID, dates & assignment are system-set.</p>'+
|
|
|
833 |
'<div class="grid g2" style="gap:20px"><div class="kv">'+
|
|
|
834 |
kvRow('LMS ID', d.lmsCode?('<span class="mono">'+esc(d.lmsCode)+'</span>'):'—')+
|
|
|
835 |
kvRow('Created', fmtDT(d.created)+(d.createdBy?(' · '+esc(d.createdBy)):''))+
|
|
|
836 |
kvRow('Creation path', d.path?('<span class="tag '+(d.path==='B'?'b':'a')+'">'+(d.path==='B'?'B · Field':'A · BGC')+'</span>'):'—')+
|
|
|
837 |
kvRow('Retailer', esc(d.name))+
|
|
|
838 |
kvRow('Business', esc(d.outlet))+
|
|
|
839 |
kvRow('Contact', d.contact?('<span class="mono">'+esc(d.contact)+'</span>'):'—')+
|
|
|
840 |
'</div><div class="kv">'+
|
|
|
841 |
kvRow('Location', esc(d.location))+
|
|
|
842 |
kvRow('Region', esc(d.regionCode))+
|
|
|
843 |
kvRow('Business value', inrShort(d.value))+
|
|
|
844 |
kvRow('Source', esc(d.source))+
|
|
|
845 |
kvRow('Last disposition', d.disposition?esc(d.disposition):'—')+
|
|
|
846 |
kvRow('Owner BM/RSM', esc(d.ownerBm))+
|
|
|
847 |
'</div></div><div class="divider"></div>'+
|
|
|
848 |
'<div class="kv" style="grid-template-columns:130px 1fr 130px 1fr">'+
|
|
|
849 |
kvRow('Working owner', esc(d.owner))+kvRow('Current stage', stagePill(d.stage,d.stageLabel))+
|
|
|
850 |
kvRow('First contact', d.firstContactedAt?fmtDT(d.firstContactedAt):'<span style="color:#e8590c">not yet</span>')+
|
|
|
851 |
kvRow('Unreachable tries', d.unreachableCount==null?'0':d.unreachableCount)+
|
|
|
852 |
'</div></div>';
|
|
|
853 |
// SLA card
|
|
|
854 |
var slaClr=d.slaState==='MET'?'#2f9e44':(d.slaState==='BREACHED'?'#e03131':(d.slaState==='AT_RISK'?'#f08c00':'#69748c'));
|
|
|
855 |
var sla='<div class="card"><h2>First-contact SLA</h2><p class="desc">5-hr commitment from creation (SOP §11).</p>'+
|
|
|
856 |
'<div style="font-size:26px;font-weight:800;color:'+slaClr+'">'+esc(d.slaState||'NA')+'</div>'+
|
|
|
857 |
'<div class="kv" style="margin-top:10px">'+kvRow('Due by', d.firstContactDue?fmtDT(d.firstContactDue):'—')+
|
|
|
858 |
kvRow('First contact', d.firstContactedAt?fmtDT(d.firstContactedAt):'<span style="color:#e8590c">not yet</span>')+'</div></div>';
|
|
|
859 |
// trail
|
|
|
860 |
var trail='<div class="card"><h2>Trail <span style="color:#97a1b5;font-weight:500;font-size:12px">· append-only</span></h2>';
|
|
|
861 |
if(!d.trail||!d.trail.length){ trail+='<div class="ph">No activity yet.</div>'; }
|
|
|
862 |
else { trail+='<div class="trail">'; d.trail.forEach(function(t){ trail+='<div class="tevt '+trailType(t.type)+'"><div class="tdot">●</div>'+
|
|
|
863 |
'<div class="tt">'+(t.type?esc(t.type):'Update')+'</div>'+
|
|
|
864 |
'<div class="tm">'+esc(t.actor||'System')+' · '+fmtDT(t.when)+(t.scheduled?(' · scheduled '+fmtDT(t.scheduled)):'')+'</div>'+
|
|
|
865 |
(t.body?('<div class="td">'+esc(t.body)+'</div>'):'')+'</div>'; }); trail+='</div>'; }
|
|
|
866 |
trail+='</div>';
|
|
|
867 |
|
|
|
868 |
var openBtn='<div style="text-align:right;margin-bottom:12px"><button class="btn coral sm" onclick="window.open(CTX+\'/leadRecord?leadId='+d.id+'\',\'_blank\')">Open in app to act →</button></div>';
|
|
|
869 |
byId('recordHost').innerHTML=head+step+openBtn+'<div class="grid g3"><div>'+details+trail+'</div><div>'+sla+'</div></div>';
|
|
|
870 |
}
|
|
|
871 |
|
|
|
872 |
// ====================== CREATE LEAD (real /lms/resolve-region + /createLead) ======================
|
|
|
873 |
var CREATE_PATH='A';
|
|
|
874 |
function setPath(p,btn){ CREATE_PATH=p;
|
|
|
875 |
byId('pathA').className='btn '+(p==='A'?'coral':'ghost')+' sm'; byId('pathB').className='btn '+(p==='B'?'coral':'ghost')+' sm';
|
|
|
876 |
byId('createDesc').textContent=(p==='A')?'Path A — captured by BGC agent during an outbound call, after consent.':'Path B — field encounter; geo-pin/location is mandatory, owner = creator.';
|
|
|
877 |
byId('cLocHint').textContent=(p==='A')?'geo-pin optional on Path A':'location mandatory on Path B'; }
|
|
|
878 |
function initials(n){ n=(n||'').trim(); if(!n) return '—'; var p=n.split(/\s+/); return ((p[0]||'')[0]||'').toUpperCase()+((p[1]||'')[0]||'').toUpperCase(); }
|
|
|
879 |
function resolveBM(regionId){ if(!regionId){ byId('rName').textContent='Pick a region…'; byId('rRegion').textContent='—'; byId('raAv').textContent='—';
|
|
|
880 |
byId('rEsc').textContent='The owning BM/RSM is resolved from the region (cs.position).'; return; }
|
|
|
881 |
getJSON('/lms/resolve-region?regionId='+encodeURIComponent(regionId),function(d){ d=d||{};
|
|
|
882 |
byId('rRegion').textContent=d.regionCode||''; var nm=d.bmName||'';
|
|
|
883 |
if(d.assignmentStatus==='HOLD' || !nm){ byId('rName').textContent='No active BM/RSM'; byId('raAv').textContent='!';
|
|
|
884 |
byId('raAv').style.background='#e03131'; byId('rEsc').innerHTML='Region has no active owner → <b style="color:#e03131">HOLD queue</b>, Sales Head notified.'; }
|
|
|
885 |
else { byId('rName').textContent=nm; byId('raAv').textContent=initials(nm); byId('raAv').style.background='#1f2a5e';
|
|
|
886 |
byId('rEsc').textContent='Auto-resolved from region · escalation contact copied on the assignment email.'; } }); }
|
|
|
887 |
function val(id){ var e=byId(id); return e?e.value:''; }
|
|
|
888 |
function submitLead(){ var fn=val('cRetailer').trim(), biz=val('cBusiness').trim(), loc=val('cLocation').trim();
|
|
|
889 |
var phone=(val('cContact')||'').replace(/\D/g,'').slice(-10); var value=parseFloat((val('cValue')||'').replace(/[^0-9.]/g,''));
|
|
|
890 |
var region=val('cRegion'); var path=CREATE_PATH;
|
|
|
891 |
if(!fn){ toast('Retailer name is required'); return; } if(phone.length!==10){ toast('A valid 10-digit contact is required'); return; }
|
|
|
892 |
if(!region){ toast('Pick a region'); return; } if(path==='B' && !loc){ toast('Path B needs a location'); return; }
|
|
|
893 |
var payload={ firstName:fn, lastName:'', mobile:phone, address:loc||'', city:'', state:'', source:'LMS Dashboard',
|
|
|
894 |
remark:'Created from LMS dashboard', outletName:biz||'', regionId:parseInt(region,10), creationPath:path };
|
|
|
895 |
if(value && value>0) payload.potential=value;
|
|
|
896 |
fetch(CTX+'/createLead',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},body:JSON.stringify(payload)})
|
|
|
897 |
.then(function(r){ return r.text(); }).then(function(t){ t=(''+t).trim(); var id=parseInt(t,10);
|
|
|
898 |
if(!isNaN(id) && String(id)===t){ toast('Lead created · auto-assigned · SLA started',1);
|
|
|
899 |
['cRetailer','cBusiness','cLocation','cContact','cValue','cBrands'].forEach(function(x){ byId(x).value=''; });
|
|
|
900 |
byId('recLookup').value=id; injectRecord(id); go('record',document.querySelectorAll('.tab')[1]); refresh(); }
|
|
|
901 |
else { toast(t||'Lead created'); } })
|
|
|
902 |
.catch(function(e){ toast('Create failed'); }); }
|
|
|
903 |
|
|
|
904 |
// ====================== AUTO-ASSIGNMENT (real resolve per region) ======================
|
|
|
905 |
function buildAssign(){ var opts=byId('cRegion').querySelectorAll('option'); var rows=[]; var holdCount=0, assignedCount=0, pending=0;
|
|
|
906 |
byId('assignMap').innerHTML='';
|
|
|
907 |
opts.forEach(function(o){ if(!o.value) return; pending++;
|
|
|
908 |
var id=o.value, label=o.textContent;
|
|
|
909 |
getJSON('/lms/resolve-region?regionId='+encodeURIComponent(id),function(d){ d=d||{}; pending--;
|
|
|
910 |
var hold=(d.assignmentStatus==='HOLD' || !d.bmName);
|
|
|
911 |
if(hold) holdCount++; else assignedCount++;
|
|
|
912 |
var tr=document.createElement('tr');
|
|
|
913 |
tr.innerHTML='<td><b>'+esc(label)+'</b></td>'+
|
|
|
914 |
'<td>'+(hold?'<span class="warnflag">— vacant —</span>':esc(d.bmName))+'</td>'+
|
|
|
915 |
'<td>'+(hold?'<span class="stage st-ni">HOLD</span>':'<span class="stage st-active">Active</span>')+'</td>';
|
|
|
916 |
byId('assignMap').appendChild(tr);
|
|
|
917 |
if(pending===0){ renderAssignKpis(assignedCount,holdCount); } }); });
|
|
|
918 |
// HOLD queue leads
|
|
|
919 |
getJSON('/lms/dashboard/leads?bucketType=ATTENTION&bucketKey=HOLD',function(d){ var leads=(d&&d.leads)?d.leads:[];
|
|
|
920 |
if(!leads.length){ byId('holdQueue').innerHTML='<div class="ph">No leads in HOLD. 🎉</div>'; return; }
|
|
|
921 |
var h=''; leads.slice(0,8).forEach(function(l){ h+='<div style="display:flex;align-items:center;gap:10px;font-size:12.5px;padding:5px 0" class="clickrow" data-leadid="'+l.id+'">'+
|
|
|
922 |
'<span class="idpill">'+esc(l.lmsCode)+'</span> <span>'+esc(l.name||l.outlet||'—')+'</span></div>'; });
|
|
|
923 |
byId('holdQueue').innerHTML=h; }); }
|
|
|
924 |
function renderAssignKpis(assigned,hold){ var k=[
|
|
|
925 |
{i:'📧',bg:'#eef1fb',fg:'#3b5bdb',l:'Regions mapped',v:assigned,f:'active BM/RSM'},
|
|
|
926 |
{i:'⏸',bg:'#fff0f0',fg:'#e03131',l:'Regions on HOLD',v:hold,f:'no active BM',vc:'#e03131'},
|
|
|
927 |
{i:'👤',bg:'#fff4e6',fg:'#e8590c',l:'HOLD leads',v:(DATA.summary&&attn('HOLD'))||0,f:'awaiting a BM',vc:'#e8590c'},
|
|
|
928 |
{i:'⚡',bg:'#ebfbee',fg:'#2f9e44',l:'Resolve tiers',v:'L4→L5→L3',f:'cs.position'} ];
|
|
|
929 |
var h=''; k.forEach(function(x){ h+='<div class="kpi"><div class="top"><div class="ic" style="background:'+x.bg+';color:'+x.fg+'">'+x.i+'</div>'+
|
|
|
930 |
'<div class="label">'+esc(x.l)+'</div></div><div class="val"'+(x.vc?' style="color:'+x.vc+'"':'')+'>'+(typeof x.v==='number'?fmtInt(x.v):x.v)+'</div><div class="foot">'+esc(x.f)+'</div></div>'; });
|
|
|
931 |
byId('assignKpis').innerHTML=h; }
|
|
|
932 |
|
|
|
933 |
function attn(key){ var s=DATA.summary; if(!s||!s.attention) return 0; var f=0; s.attention.forEach(function(a){ if(a.key===key)f=a.count; }); return f; }
|
|
|
934 |
|
|
|
935 |
// ====================== RECORDINGS & AUDIT (live hygiene from attention) ======================
|
|
|
936 |
function buildAudit(){
|
|
|
937 |
// KPIs — missing dispositions is live; recording metrics are pending source.
|
|
|
938 |
var miss=attn('MISSING_DISP'), br=attn('BREACHED_NO_CONTACT'), hold=attn('HOLD');
|
|
|
939 |
var kp=[ {i:'🎵',bg:'#fff0ef',fg:'#f0483e',l:'Recordings (24 mo)',v:'—',f:'pending source'},
|
|
|
940 |
{i:'💬',bg:'#fff4e6',fg:'#e8590c',l:'Missing dispositions',v:miss,f:'live',vc:'#e8590c'},
|
|
|
941 |
{i:'🚫',bg:'#fff0f0',fg:'#e03131',l:'DND breaches',v:'—',f:'needs call log'},
|
|
|
942 |
{i:'👁',bg:'#eef1fb',fg:'#3b5bdb',l:'Recording opens',v:'—',f:'pending source'},
|
|
|
943 |
{i:'🧹',bg:'#e6fcf5',fg:'#0ca678',l:'Onboarded (deep audit)',v:fmtInt(onboardedCount()),f:'5% sampled'} ];
|
|
|
944 |
var h=''; kp.forEach(function(x){ h+='<div class="kpi"><div class="top"><div class="ic" style="background:'+x.bg+';color:'+x.fg+'">'+x.i+'</div>'+
|
|
|
945 |
'<div class="label">'+esc(x.l)+'</div></div><div class="val"'+(x.vc?' style="color:'+x.vc+'"':'')+'>'+(typeof x.v==='number'?fmtInt(x.v):x.v)+'</div><div class="foot">'+esc(x.f)+'</div></div>'; });
|
|
|
946 |
byId('auditKpis').innerHTML=h;
|
|
|
947 |
// hygiene flags (clickable via data-att)
|
|
|
948 |
var flags=[ {k:'MISSING_DISP',i:'💬',bg:'#fff4e6',fg:'#e8590c',t:'LMS with missing dispositions',d:'Next action stays locked until dispositioned',c:'warnflag',v:miss},
|
|
|
949 |
{k:'BREACHED_NO_CONTACT',i:'⚠',bg:'#fff0f0',fg:'#e03131',t:'SLA breaches — first contact',d:'Escalated to BM/RSM per matrix',c:'badflag',v:br},
|
|
|
950 |
{k:'HOLD',i:'👤',bg:'#fff0f0',fg:'#e03131',t:'Unassigned records (HOLD)',d:'Region awaiting a BM',c:'badflag',v:hold} ];
|
|
|
951 |
var fh=''; flags.forEach(function(a){ fh+='<div class="att" data-att="'+a.k+'" data-attt="'+esc(a.t)+'"><div class="ic" style="background:'+a.bg+';color:'+a.fg+'">'+a.i+'</div>'+
|
|
|
952 |
'<div class="ab"><div class="atitle">'+esc(a.t)+'</div><div class="adet">'+esc(a.d)+'</div></div><div class="acount '+a.c+'">'+fmtInt(a.v)+'</div></div>'; });
|
|
|
953 |
byId('auditHygiene').innerHTML=fh; }
|
|
|
954 |
function onboardedCount(){ var s=DATA.summary; if(!s||!s.funnel) return 0; var c=0; s.funnel.forEach(function(f){ if(f.stage==='ONBOARDED')c=f.count; }); return c; }
|
|
|
955 |
|
|
|
956 |
// ---- initial paint from seeds, pipeline loads live
|
|
|
957 |
renderSummary(SEED.summary); renderState(SEED.state); renderHeat(SEED.heat); renderAgeing(SEED.ageing);
|
|
|
958 |
byId('dashPeriod').textContent=(STATE.from||'')+' → '+(STATE.to||'');
|
|
|
959 |
loadPipeline();
|
|
|
960 |
|
|
|
961 |
// ---- deep-link: a URL hash such as dash or record opens that tab
|
|
|
962 |
(function(){ var TABIDX={cmd:0,record:1,create:2,assign:3,dash:4,audit:5};
|
|
|
963 |
var h=(location.hash||'').replace('#','').trim();
|
|
|
964 |
if(h && TABIDX[h]!=null){ go(h, document.querySelectorAll('.tab')[TABIDX[h]]); }
|
|
|
965 |
window.addEventListener('hashchange',function(){ var k=(location.hash||'').replace('#','').trim();
|
|
|
966 |
if(k && TABIDX[k]!=null){ go(k, document.querySelectorAll('.tab')[TABIDX[k]]); } }); })();
|
|
|
967 |
</script>
|
|
|
968 |
</body>
|
|
|
969 |
</html>
|