Subversion Repositories SmartDukaan

Rev

Rev 37125 | Rev 37692 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
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)}
37651 vikas 216
  /* Brand-wise value capture. auto-fill keeps it compact on a laptop and collapses to one column
217
     on a phone, which is where Path B (field encounter) is actually used. */
218
  .brandgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(148px,1fr));gap:8px;margin-top:2px}
219
  .brandcell{display:flex;align-items:center;gap:6px;background:#f7f8fc;border:1px solid var(--line);
220
    border-radius:8px;padding:4px 8px}
221
  .brandcell .bname{font-size:11.5px;font-weight:700;color:var(--ink);white-space:nowrap;min-width:58px}
222
  .brandcell .bval{border:0;background:transparent;padding:3px 0;height:auto;font-size:12.5px;text-align:right}
223
  .brandcell .bval:focus{outline:0;box-shadow:none}
224
  .brandcell:focus-within{border-color:var(--brand);background:#fff}
37125 vikas 225
  .resolvebox{background:#f5f7ff;border:1px solid #dfe4f7;border-radius:11px;padding:13px 15px;display:flex;align-items:center;gap:13px}
226
  .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}
227
  .resolvebox .rt{font-size:11px;color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:.04em}
228
  .resolvebox .rn{font-size:15px;font-weight:750;margin-top:1px}
229
  .resolvebox .rc{font-size:11.5px;color:var(--muted);margin-top:2px}
230
  .trail{position:relative;padding-left:22px}
231
  .trail:before{content:"";position:absolute;left:7px;top:4px;bottom:4px;width:2px;background:var(--line)}
232
  .tevt{position:relative;padding:0 0 14px}
233
  .tevt:last-child{padding-bottom:0}
234
  .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)}
235
  .tevt.sys .tdot{border-color:var(--soft);color:var(--soft)}
236
  .tevt.win .tdot{border-color:var(--good);color:var(--good)}
237
  .tevt.call .tdot{border-color:var(--coral);color:var(--coral)}
238
  .tevt .tt{font-size:13px;font-weight:650}
239
  .tevt .tm{font-size:11.5px;color:var(--soft);margin-top:1px}
240
  .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}
241
 
242
  /* record stepper */
243
  .rec-stepper{display:flex;align-items:center;gap:0;margin:2px 0 4px;flex-wrap:wrap}
244
  .rec-step{display:flex;align-items:center;gap:8px;flex:1;min-width:96px;position:relative}
245
  .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)}
246
  .rec-step.done .dot{background:var(--good);border-color:var(--good);color:#fff}
247
  .rec-step.cur .dot{background:var(--navy);border-color:var(--navy);color:#fff;box-shadow:0 0 0 4px #e7ebfa}
248
  .rec-step .sl{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.02em;color:var(--muted)}
249
  .rec-step.cur .sl{color:var(--navy)}
250
  .rec-step .bar{position:absolute;left:26px;right:0;top:13px;height:2px;background:var(--line);z-index:-1}
251
  .rec-step.done .bar{background:var(--good)}
252
 
253
  /* matrix */
254
  .matrix{width:100%;border-collapse:separate;border-spacing:0;font-size:12.5px}
255
  .matrix th,.matrix td{padding:9px 8px;border-bottom:1px solid var(--line2);text-align:center}
256
  .matrix th:first-child,.matrix td:first-child{text-align:left;font-weight:650}
257
  .matrix thead th{background:#f8f9fc;color:var(--muted);font-size:10.5px;text-transform:uppercase;letter-spacing:.03em}
258
  .mk-y{color:var(--good);font-weight:800}
259
  .mk-n{color:#ccd2e0}
260
  .mk-s{color:var(--orange);font-weight:800;font-size:11px}
261
 
262
  @media (max-width:1040px){
263
    .kpis{grid-template-columns:repeat(3,1fr)}
264
    .g2,.g3{grid-template-columns:1fr}
265
    .form-grid{grid-template-columns:1fr}.fld.full{grid-column:1}
266
    .search{display:none}
267
  }
268
</style>
269
</head>
270
<body>
271
 
272
<!-- ============ APP BAR ============ -->
273
<div class="appbar">
274
  <div class="inner">
275
    <div class="logo"><span class="mk">SD</span><div>SmartDukaan · LMS<small>LEAD MANAGEMENT SYSTEM</small></div></div>
276
    <div class="search">
277
      <span class="si">&#128269;</span>
278
      <input id="lmsSearch" placeholder="Search LMS ID, business, phone, retailer…  e.g. LMS-UPW-26-000123">
279
      <span class="hint">/ to focus</span>
280
    </div>
281
    <button class="abtn coral" onclick="window.open(CTX+'/getOpenLead','_blank')">+ New Lead</button>
282
    <div class="role">
283
      <div class="av">SD</div>
284
      <div class="who"><b>Lead Dashboard</b><br><span>live · real data</span></div>
285
    </div>
286
  </div>
287
</div>
288
 
289
<!-- ============ TABS ============ -->
290
<div class="tabbar">
291
  <div class="inner">
292
    <button class="tab active" onclick="go('cmd',this)">Command Center</button>
293
    <button class="tab" onclick="go('record',this)">Lead Record</button>
294
    <button class="tab" onclick="go('create',this)">Create Lead</button>
295
    <button class="tab" onclick="go('assign',this)">Auto-Assignment</button>
296
    <button class="tab" onclick="go('dash',this)">Dashboards</button>
297
    <button class="tab" onclick="go('audit',this)">Recordings &amp; Audit</button>
298
  </div>
299
</div>
300
 
301
<div class="wrap">
302
 
303
  <!-- ======================= COMMAND CENTER ========================= -->
304
  <div id="cmd">
305
    <div class="pagehead">
306
      <div>
307
        <div class="crumb">Sales · Lead Pipeline</div>
308
        <h1 class="page">Command Center</h1>
309
        <div class="sub">Every lead you own or oversee — from creation to onboarding. SLA clock runs on all NEW records.</div>
310
      </div>
311
      <div class="hd-right">
312
        <select class="ctl" id="fRegion">
313
          <option value="">All regions</option>
314
          #foreach($r in $regions)
315
            <option value="$r.getId()">$!r.getName()#if($r.getRegionCode()) ($r.getRegionCode())#end</option>
316
          #end
317
        </select>
318
        <input type="date" class="ctl" id="fFrom" value="$!fromDate">
319
        <input type="date" class="ctl" id="fTo" value="$!toDate">
320
      </div>
321
    </div>
322
 
323
    <div class="kpis" id="ccKpis"></div>
324
 
325
    <div class="sectionlabel">Lifecycle Funnel — selected period</div>
326
    <div class="card"><div class="funnel" id="ccFunnel"></div></div>
327
 
328
    <div class="grid g3">
329
      <div class="card">
330
        <h2>Lead pipeline</h2>
331
        <p class="desc">Click any row &rarr; full LMS record &amp; trail. Rows shaded by SLA urgency.</p>
332
        <table>
333
          <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>
334
          <tbody id="ccPipeline"><tr><td colspan="7"><div class="ph">Loading…</div></td></tr></tbody>
335
        </table>
336
      </div>
337
      <div class="card">
338
        <h2>Needs attention now</h2>
339
        <p class="desc">Actionable queues · click to work them.</p>
340
        <div id="ccAttention"></div>
341
      </div>
342
    </div>
343
  </div>
344
 
345
  <!-- ======================= LEAD RECORD ========================= -->
346
  <div id="record" class="hide">
347
    <div class="pagehead">
348
      <div>
349
        <div class="crumb">Command Center · Lead Record</div>
350
        <h1 class="page">Lead Record</h1>
351
        <div class="sub">The single source of truth for a lead — stepper, immutable trail, SLA &amp; disposition. Loads the real record.</div>
352
      </div>
353
      <div class="hd-right">
354
        <div class="lookup">
355
          <input id="recLookup" placeholder="Lead # (e.g. 123)" inputmode="numeric">
356
          <button class="btn sm" onclick="loadRecord()">Load</button>
357
        </div>
358
      </div>
359
    </div>
360
    <div class="recordHost" id="recordHost"><div class="card"><div class="ph">Loading the most recent lead…</div></div></div>
361
    <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>
362
  </div>
363
 
364
  <!-- ======================= CREATE LEAD ========================= -->
365
  <div id="create" class="hide">
366
    <div class="pagehead">
367
      <div>
368
        <div class="crumb">Create · New Lead</div>
369
        <h1 class="page">Create Lead</h1>
37651 vikas 370
        <div class="sub">Only what an agent can know on a first call. LMS ID is generated on submit and the lead is auto-assigned from region. Address, store photo and geo-pin come from the capture link, which is created automatically and copied to your clipboard.</div>
37125 vikas 371
      </div>
372
      <div class="hd-right">
373
        <div class="ctl" style="padding:4px;display:flex;gap:4px">
374
          <button class="btn coral sm" id="pathA" style="border-radius:7px" onclick="setPath('A',this)">Path A · BGC outbound</button>
375
          <button class="btn ghost sm" id="pathB" style="border-radius:7px" onclick="setPath('B',this)">Path B · Field encounter</button>
376
        </div>
377
      </div>
378
    </div>
379
 
380
    <div class="grid g3">
381
      <div class="card">
382
        <h2>Lead details</h2>
383
        <p class="desc" id="createDesc">Path A — captured by BGC agent during an outbound call, after consent.</p>
384
        <div class="form-grid">
385
          <div class="fld"><label>Retailer name <span class="m">*</span></label><input class="inp" id="cRetailer" placeholder="Contact person"></div>
386
          <div class="fld"><label>Business name <span class="m">*</span></label><input class="inp" id="cBusiness" placeholder="Shop / trade name"></div>
387
          <div class="fld"><label>Contact number <span class="m">*</span></label><input class="inp mono" id="cContact" placeholder="10-digit mobile" inputmode="numeric"></div>
37651 vikas 388
          <div class="fld full">
389
            <label>Monthly business value — brand wise <span class="auto" id="cValueTotal">total ₹0</span></label>
390
            <div class="brandgrid" id="cBrandGrid">
391
              #foreach($b in $leadBrands)
392
                <div class="brandcell">
393
                  <span class="bname">$b</span>
394
                  <input class="inp bval mono" data-brand="$b" placeholder="0" inputmode="numeric"
395
                         oninput="brandTotal()">
396
                </div>
397
              #end
398
            </div>
399
            <div class="hinttext">Type in rupees per month — 100000 or 1L, 50k both work. Leave a brand blank if it is not stocked.</div>
400
          </div>
37125 vikas 401
          <div class="fld"><label>Interest status</label><select class="inp" id="cInterest"><option>Interested</option><option>Callback</option><option>Not Interested</option></select></div>
402
          <div class="fld full">
403
            <label>Region / State <span class="m">*</span> <span class="auto">drives auto-assignment</span></label>
404
            <select class="inp" id="cRegion" onchange="resolveBM(this.value)">
405
              <option value="">Select a region…</option>
406
              #foreach($r in $regions)
407
                <option value="$r.getId()">$!r.getName()#if($r.getRegionCode()) ($r.getRegionCode())#end</option>
408
              #end
409
            </select>
410
          </div>
411
        </div>
412
        <div style="display:flex;gap:9px;margin-top:16px;justify-content:flex-end">
413
          <button class="btn coral" onclick="submitLead()">Submit &amp; generate LMS ID</button>
414
        </div>
415
      </div>
416
 
417
      <div>
418
        <div class="card">
419
          <h2>Auto-assignment preview</h2>
420
          <p class="desc">On Region select, the engine resolves the BM/RSM and shows it for confirmation before submit.</p>
421
          <div class="resolvebox" id="resolveBox">
422
            <div class="ra" id="raAv">—</div>
423
            <div>
424
              <div class="rt">Resolved BM / RSM · <span id="rRegion">—</span></div>
425
              <div class="rn" id="rName">Pick a region…</div>
426
              <div class="rc" id="rEsc">The owning BM/RSM is resolved from the region (cs.position).</div>
427
            </div>
428
          </div>
429
          <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>
430
        </div>
431
        <div class="card">
432
          <h2>What happens on submit</h2>
433
          <div class="trail" style="margin-top:4px">
434
            <div class="tevt win"><div class="tdot">1</div><div class="tt">Validate fields &amp; generate LMS ID</div><div class="tm">immutable, searchable</div></div>
435
            <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>
436
            <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>
437
            <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>
438
          </div>
439
        </div>
440
      </div>
441
    </div>
442
  </div>
443
 
444
  <!-- ======================= AUTO-ASSIGNMENT ========================= -->
445
  <div id="assign" class="hide">
446
    <div class="pagehead">
447
      <div>
448
        <div class="crumb">Sales Ops · Routing</div>
449
        <h1 class="page">Auto-Assignment Engine</h1>
450
        <div class="sub">Region &rarr; BM/RSM mapping resolves ownership at submit — from the live cs.position table. HOLD when a region has no active BM.</div>
451
      </div>
452
    </div>
453
 
454
    <div class="kpis" style="grid-template-columns:repeat(4,1fr)" id="assignKpis"></div>
455
 
456
    <div class="grid g3">
457
      <div class="card">
458
        <h2>Region &rarr; BM/RSM mapping</h2>
459
        <p class="desc">The live routing table — resolved for each region right now.</p>
460
        <table>
461
          <thead><tr><th>Region / State</th><th>Assigned BM / RSM</th><th>Status</th></tr></thead>
462
          <tbody id="assignMap"><tr><td colspan="3"><div class="ph">Resolving…</div></td></tr></tbody>
463
        </table>
464
      </div>
465
      <div>
466
        <div class="card">
467
          <h2>Engine rules</h2>
468
          <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>
469
          <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>
470
          <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 &rarr; LMS held; Sales Head notified.</div></div></div>
471
          <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>
472
        </div>
473
        <div class="card" style="background:#fff5f5;border-color:#ffd9d5">
474
          <h2 style="color:#c92a2a">HOLD queue</h2>
475
          <p class="desc" style="margin-bottom:8px">LMS with no active BM — waiting until one is mapped.</p>
476
          <div id="holdQueue"><div class="ph">Loading…</div></div>
477
        </div>
478
      </div>
479
    </div>
480
  </div>
481
 
482
  <!-- ======================= DASHBOARDS ========================= -->
483
  <div id="dash" class="hide">
484
    <div class="pagehead">
485
      <div>
486
        <div class="crumb">Management · Cross-region roll-up</div>
487
        <h1 class="page">Dashboards</h1>
488
        <div class="sub">Funnel, SLA compliance, ageing &amp; servicing across all regions — derived from the lead record.</div>
489
      </div>
490
      <div class="hd-right">
491
        <span class="ctl"><span class="lbl">Period</span><span id="dashPeriod">—</span></span>
492
        <button class="btn ghost sm" onclick="toast('Export uses the app report suite',1)">Export</button>
493
      </div>
494
    </div>
495
 
496
    <div class="kpis" id="dashKpis"></div>
497
 
498
    <div class="sectionlabel">State funnel — NEW &rarr; ONBOARDED by region</div>
499
    <div class="card">
500
      <table>
501
        <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>
502
        <tbody id="dashState"></tbody>
503
      </table>
504
    </div>
505
 
506
    <div class="grid g2">
507
      <div class="card">
508
        <h2>SLA compliance — owner heat-map</h2>
509
        <p class="desc">% first contact within SLA, by BM (rows) across recent weeks (cols). Red = chronic breach.</p>
510
        <div id="dashHeat"></div>
511
        <div class="note">SLA = first contact within 5 hrs of creation (SOP §11). Reminder T-1h; escalation to BM at T+5h.</div>
512
      </div>
513
      <div class="card">
514
        <h2>Standard dashboards</h2>
515
        <p class="desc">SOP §18.2 report suite — Live = from the lead record; Partial/Pending need their source system.</p>
516
        <table><tbody style="font-size:12.5px">
517
          <tr class="clickrow" data-rep="State Funnel"><td>&#128200; <b>State Funnel</b><span class="rbadge live">Live</span></td><td style="color:#69748c">New &rarr; onboarded by region, BM, ASM</td></tr>
518
          <tr class="clickrow" data-rep="SLA Compliance"><td>&#9878; <b>SLA Compliance</b><span class="rbadge live">Live</span></td><td style="color:#69748c">% within SLA, owner heat-map</td></tr>
519
          <tr class="clickrow" data-rep="Ageing"><td>&#8987; <b>Ageing</b><span class="rbadge live">Live</span></td><td style="color:#69748c">Stuck by stage, days-since-action</td></tr>
520
          <tr class="clickrow" data-rep="Management Roll-up"><td>&#128081; <b>Management Roll-up</b><span class="rbadge live">Live</span></td><td style="color:#69748c">Cross-region leads, onboardings, value</td></tr>
521
          <tr class="clickrow" data-rep="BGC Daily"><td>&#128202; <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>
522
          <tr class="clickrow" data-rep="Audit Hygiene"><td>&#129529; <b>Audit Hygiene</b><span class="rbadge partial">Partial</span></td><td style="color:#69748c">Missing dispositions / HOLD; DND &amp; recordings pending</td></tr>
523
          <tr class="clickrow" data-rep="RBM Servicing"><td>&#128260; <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>
524
        </tbody></table>
525
      </div>
526
    </div>
527
 
528
    <div class="sectionlabel">What to fix</div>
529
    <div class="callout">
530
      <h2>Recommended actions — from this period's data</h2>
531
      <p class="desc">Each links to the section above.</p>
532
      <div id="dashFix"></div>
533
    </div>
534
  </div>
535
 
536
  <!-- ======================= RECORDINGS & AUDIT ========================= -->
537
  <div id="audit" class="hide">
538
    <div class="pagehead">
539
      <div>
540
        <div class="crumb">Audit · Access Control</div>
541
        <h1 class="page">Recordings &amp; Audit</h1>
542
        <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>
543
      </div>
544
    </div>
545
 
546
    <div class="kpis" style="grid-template-columns:repeat(5,1fr)" id="auditKpis"></div>
547
 
548
    <div class="grid g2">
549
      <div class="card">
550
        <h2>Access &amp; recording visibility matrix</h2>
551
        <p class="desc">What each role can open. Recording access is scoped by role + reporting hierarchy (SOP §15).</p>
552
        <table class="matrix">
553
          <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>
554
          <tbody>
555
            <tr><td>Individual (BGC/ASM)</td><td class="mk-y">&#10003;</td><td class="mk-n">&mdash;</td><td class="mk-n">&mdash;</td><td class="mk-n">&mdash;</td><td class="mk-s">own only</td></tr>
556
            <tr><td>Reporting Manager</td><td class="mk-y">&#10003;</td><td class="mk-y">&#10003;</td><td class="mk-n">&mdash;</td><td class="mk-n">&mdash;</td><td class="mk-s">reports'</td></tr>
557
            <tr><td>BM / RSM</td><td class="mk-y">&#10003;</td><td class="mk-y">&#10003;</td><td class="mk-y">&#10003;</td><td class="mk-n">&mdash;</td><td class="mk-s">state-wide</td></tr>
558
            <tr><td>Audit Team</td><td class="mk-y">&#10003;</td><td class="mk-y">&#10003;</td><td class="mk-y">&#10003;</td><td class="mk-y">&#10003;</td><td class="mk-s">all · read-only</td></tr>
559
            <tr><td>Upper Management</td><td class="mk-y">&#10003;</td><td class="mk-y">&#10003;</td><td class="mk-y">&#10003;</td><td class="mk-y">&#10003;</td><td class="mk-s">via BI drill</td></tr>
560
          </tbody>
561
        </table>
562
        <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>
563
      </div>
564
 
565
      <div class="card">
566
        <h2>Weekly hygiene flags <span style="color:#97a1b5;font-weight:500;font-size:12px">· live</span></h2>
567
        <p class="desc">Derived from the lead record — click to list the leads.</p>
568
        <div id="auditHygiene"><div class="ph">Loading…</div></div>
569
      </div>
570
    </div>
571
 
572
    <div class="grid g2">
573
      <div class="card">
574
        <h2>Audit &amp; compliance program (SOP §17)</h2>
575
        <div class="att" style="margin-bottom:8px"><div class="ic" style="background:#e6fcf5;color:#0ca678">&#127925;</div><div class="ab"><div class="atitle">Weekly recording sampling</div><div class="adet">Across BGC, ASM &amp; RBM for quality &amp; process adherence.</div></div></div>
576
        <div class="att" style="margin-bottom:8px"><div class="ic" style="background:#e6fcf5;color:#0ca678">&#129529;</div><div class="ab"><div class="atitle">5% deep audit of onboarded</div><div class="adet">Verify data integrity &amp; process compliance.</div></div></div>
577
        <div class="att" style="margin-bottom:8px"><div class="ic" style="background:#fff0f0;color:#e03131">&#128683;</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>
578
        <div class="att" style="margin-bottom:0"><div class="ic" style="background:#eef1f6;color:#69748c">&#128451;</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>
579
      </div>
580
      <div class="card">
581
        <h2>Recording repository <span style="color:#97a1b5;font-weight:500;font-size:12px">· pending source</span></h2>
582
        <p class="desc">Each recording would be tagged with LMS ID, user, timestamp, duration &amp; disposition.</p>
583
        <div class="ph">No recording repository connected yet — this arrives with the C2C telephony bridge (Phase 2/3).</div>
584
      </div>
585
    </div>
586
  </div>
587
 
588
</div>
589
 
590
<!-- drawer + toast -->
591
<div class="drawer-ov" id="ov" onclick="closeDrawer()"></div>
592
<div class="drawer" id="drawer">
593
  <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()">&times;</button></div>
594
  <div class="dbody" id="dBody"></div>
595
</div>
596
<div class="toastwrap" id="toasts"></div>
597
 
598
<script>
599
  var CTX = "${rc.contextPath}";
600
  var SEED = { summary:$summaryJson, state:$stateFunnelJson, heat:$slaHeatmapJson, ageing:$ageingJson };
601
  var STATE = { regionId:'', from:'$!fromDate', to:'$!toDate' };
602
  var DATA = { summary:null, state:null, heat:null, ageing:null };
603
 
604
  function byId(x){ return document.getElementById(x); }
605
  function esc(s){ if(s==null) return ''; return (''+s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
606
  function fmtInt(n){ if(n==null) return '0'; return (''+n).replace(/\B(?=(\d{3})+(?!\d))/g,','); }
607
  function fmtInr(v){ if(v==null||v===''||isNaN(v)||Number(v)<=0) return '&#8377;0'; v=Number(v);
608
    if(v>=1e7) return '&#8377;'+(v/1e7).toFixed(1)+'<small> Cr</small>'; if(v>=1e5) return '&#8377;'+(v/1e5).toFixed(1)+'<small> L</small>';
609
    if(v>=1e3) return '&#8377;'+Math.round(v/1e3)+'<small>K</small>'; return '&#8377;'+Math.round(v); }
610
  function inrShort(v){ if(v==null||isNaN(v)||Number(v)<=0) return '&#8377;0'; v=Number(v);
611
    if(v>=1e7) return '&#8377;'+(v/1e7).toFixed(1)+'Cr'; if(v>=1e5) return '&#8377;'+(v/1e5).toFixed(1)+'L';
612
    if(v>=1e3) return '&#8377;'+Math.round(v/1e3)+'k'; return '&#8377;'+Math.round(v); }
613
 
614
  // ---- tabs
615
  var SCREENS=['cmd','record','create','assign','dash','audit'];
616
  var LOADED={};
617
  function go(id,btn){ SCREENS.forEach(function(s){ byId(s).classList.toggle('hide',s!==id); });
618
    document.querySelectorAll('.tab').forEach(function(t){ t.classList.remove('active'); }); if(btn) btn.classList.add('active');
619
    if(id==='dash') byId('dashPeriod').textContent=(STATE.from||'')+' → '+(STATE.to||'');
620
    if(id==='record' && !LOADED.record){ LOADED.record=true; loadRecentRecord(); }
621
    if(id==='assign' && !LOADED.assign){ LOADED.assign=true; buildAssign(); }
622
    if(id==='audit'){ buildAudit(); } }
623
 
624
  // ---- toast
625
  function toast(msg,ok){ var w=byId('toasts'); var t=document.createElement('div'); t.className='toast';
626
    t.innerHTML=(ok?'<span class="ok">&#10003;</span>':'')+esc(msg); w.appendChild(t);
627
    setTimeout(function(){ t.style.opacity='0'; t.style.transition='opacity .3s'; setTimeout(function(){ t.remove(); },300); },2600); }
628
 
629
  // ---- filters + fetch
630
  function qs(){ var q=''; if(STATE.regionId)q+='&regionId='+encodeURIComponent(STATE.regionId);
631
    if(STATE.from)q+='&from='+encodeURIComponent(STATE.from); if(STATE.to)q+='&to='+encodeURIComponent(STATE.to); return q; }
37651 vikas 632
  function getJSON(path,cb,onErr){ fetch(CTX+path,{credentials:'same-origin',headers:{'X-Requested-With':'XMLHttpRequest'}})
633
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
634
    .then(function(j){ cb((j&&j.response!==undefined)?j.response:j); })
635
    .catch(function(e){ console.error('LMS fetch failed',path,e);
636
      if(typeof onErr==='function'){ onErr(e); } else { toast('Could not load data'); } }); }
37125 vikas 637
 
638
  // ---- stage helpers
639
  var STCLASS={NEW:'st-new',ASSIGNED:'st-assigned',CONTACTED:'st-contacted',QUALIFIED:'st-qualified',
640
    BEAT_PLANNED:'st-beat',VISITED:'st-visited',ONBOARDED:'st-onboarded',ACTIVE:'st-active',NOT_INTERESTED:'st-ni',DROPPED:'st-dead'};
641
  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>'; }
642
  function slaCell(state){ switch(state){
643
    case'MET': return '<span class="sla ok"><span class="slabar"><i style="width:100%;background:#2f9e44"></i></span><span class="clk">met</span></span>';
644
    case'RUNNING': return '<span class="sla ok"><span class="slabar"><i style="width:50%;background:#1f2a5e"></i></span><span class="clk">running</span></span>';
645
    case'AT_RISK': return '<span class="sla warn"><span class="slabar"><i style="width:85%;background:#f08c00"></i></span><span class="clk">&lt; 1 hr</span></span>';
646
    case'BREACHED': return '<span class="sla bad"><span class="slabar"><i style="width:100%;background:#e03131"></i></span><span class="clk">breached</span></span>';
647
    default: return '<span style="color:#97a1b5;font-size:12px">&mdash;</span>'; } }
648
 
649
  // ---- COMMAND CENTER
650
  var CC_KPI={ 'new':{i:'&#128229;',bg:'#eef1fb',fg:'#3b5bdb'}, awaiting:{i:'&#9203;',bg:'#fff4e6',fg:'#f08c00',v:'#f08c00'},
651
    sla:{i:'&#9888;',bg:'#fff0f0',fg:'#e03131',v:'#e03131'}, contacted:{i:'&#128222;',bg:'#e3fafc',fg:'#0c8599'},
652
    qualified:{i:'&#10003;',bg:'#e6fcf5',fg:'#0ca678'}, onboarded:{i:'&#127942;',bg:'#ebfbee',fg:'#2f9e44'} };
653
  var ATT_META={ BREACHED_NO_CONTACT:{i:'&#9888;',bg:'#fff0f0',fg:'#e03131',c:'badflag'}, HOLD:{i:'&#128100;',bg:'#fff0f0',fg:'#e03131',c:'badflag'},
654
    SLA_UNDER_1H:{i:'&#9203;',bg:'#fff4e6',fg:'#e8590c',c:'warnflag'}, MISSING_DISP:{i:'&#128172;',bg:'#fff4e6',fg:'#e8590c',c:'warnflag'},
655
    QUALIFIED_NO_BEAT:{i:'&#128337;',bg:'#eef1f6',fg:'#69748c',c:''}, ONBOARDED_PENDING:{i:'&#128260;',bg:'#f3f0ff',fg:'#7048e8',c:''} };
656
 
657
  function renderSummary(d){ DATA.summary=d;
658
    var kh=''; (d.kpis||[]).forEach(function(k){ var m=CC_KPI[k.key]||{i:'&#128202;',bg:'#eef1f6',fg:'#69748c'};
659
      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>'+
660
        '<div class="label">'+esc(k.label)+'</div></div><div class="val"'+(m.v?' style="color:'+m.v+'"':'')+'>'+fmtInt(k.value)+'</div>'+
661
        '<div class="foot">'+esc(k.foot)+'</div></div>'; });
662
    byId('ccKpis').innerHTML=kh;
663
    renderFunnel(d.funnel||[]);
664
    var ah=''; (d.attention||[]).forEach(function(a){ var m=ATT_META[a.key]||{i:'&#128202;',bg:'#eef1f6',fg:'#69748c',c:''};
665
      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>'+
666
        '<div class="ab"><div class="atitle">'+esc(a.title)+'</div><div class="adet">'+esc(a.detail)+'</div></div>'+
667
        '<div class="acount '+m.c+'">'+fmtInt(a.count)+'</div></div>'; });
668
    byId('ccAttention').innerHTML=ah;
669
    buildDashKpis(); buildFix();
670
  }
671
 
672
  function renderFunnel(funnel){
673
    var byStage={}; funnel.forEach(function(s){ byStage[s.stage]=s; });
674
    var seq=[['ALL','New'],['ASSIGNED','Assigned'],['CONTACTED','Contacted'],['QUALIFIED','Qualified'],['VISITED','Visited'],['ONBOARDED','Onboarded']];
675
    var h=''; var prev=null;
676
    seq.forEach(function(p,idx){ var s=byStage[p[0]]; if(!s) return; var last=(idx===seq.length-1);
677
      var cap; if(idx===0){ cap='<div class="keep">created</div>'; }
678
      else if(last){ cap='<div class="keep">'+s.pct+'% conversion</div>'; }
679
      else { var drop=prev?prev.count-s.count:0; cap=drop>0?('<div class="drop">&minus;'+fmtInt(drop)+'</div>'):('<div class="keep">'+s.pct+'%</div>'); }
680
      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>'+
681
        '<div class="fbar" style="width:'+Math.max(s.pct,3)+'%"></div>'+cap+(last?'':'<span class="arrow">&#10148;</span>')+'</div>';
682
      prev=s; });
683
    byId('ccFunnel').innerHTML=h||'<div class="ph">No leads in scope.</div>';
684
  }
685
 
686
  function loadPipeline(){ byId('ccPipeline').innerHTML='<tr><td colspan="7"><div class="ph">Loading…</div></td></tr>';
687
    getJSON('/lms/dashboard/leads?bucketType=ALL'+qs(),function(d){ var leads=(d&&d.leads)?d.leads:[];
688
      if(!leads.length){ byId('ccPipeline').innerHTML='<tr><td colspan="7"><div class="ph">No leads in scope.</div></td></tr>'; return; }
689
      var h=''; leads.slice(0,12).forEach(function(l){ var rc=l.slaState==='BREACHED'?'rowbad':(l.slaState==='AT_RISK'?'rowwarn':'');
690
        h+='<tr class="clickrow '+rc+'" data-leadid="'+l.id+'">'+
691
          '<td><span class="idpill">'+esc(l.lmsCode)+'</span></td>'+
692
          '<td><b>'+esc(l.name||'—')+'</b>'+(l.outlet?'<br><span style="color:#97a1b5;font-size:11px">'+esc(l.outlet)+'</span>':'')+'</td>'+
693
          '<td>'+(l.path==='B'?'<span class="tag b">B · Field</span>':'<span class="tag a">A · BGC</span>')+'</td>'+
694
          '<td>'+stagePill(l.stage,l.stageLabel)+'</td>'+
695
          '<td>'+esc(l.owner||'—')+'</td>'+
696
          '<td>'+slaCell(l.slaState)+'</td>'+
697
          '<td class="num">'+inrShort(l.value)+'</td></tr>'; });
698
      byId('ccPipeline').innerHTML=h;
699
    }); }
700
 
701
  function kpiBucket(k){ switch(k){case'new':return['STAGE','NEW'];case'awaiting':return['STAGE','ASSIGNED'];
702
    case'sla':return['ATTENTION','BREACHED_NO_CONTACT'];case'contacted':return['STAGE','CONTACTED'];
703
    case'qualified':return['STAGE','QUALIFIED'];case'onboarded':return['STAGE','ONBOARDED'];default:return['ALL','']; } }
704
 
705
  // ---- DASHBOARDS
706
  var DASH_KPI=[ {i:'&#128229;',bg:'#eef1fb',fg:'#3b5bdb'}, {i:'&#128200;',bg:'#e6fcf5',fg:'#0ca678',v:'#0ca678'},
707
    {i:'&#9878;',bg:'#fff4e6',fg:'#f08c00',v:'#f08c00'}, {i:'&#128222;',bg:'#e3fafc',fg:'#0c8599'},
708
    {i:'&#8987;',bg:'#fff0f0',fg:'#e03131',v:'#e03131'}, {i:'&#128176;',bg:'#ebfbee',fg:'#2f9e44'} ];
709
  function pctColor(p){ return p>=25?'#2f9e44':(p>=18?'#f08c00':(p>=10?'#e8590c':'#e03131')); }
710
  function slaColor(p){ return p>=85?'#2f9e44':(p>=70?'#f08c00':'#e03131'); }
711
 
712
  function renderState(d){ DATA.state=d; var rows=(d&&d.rows)?d.rows:[];
713
    var h=''; rows.forEach(function(r){ var cls=r.convPct<15?'rowbad':(r.slaPct<60?'rowwarn':'');
714
      h+='<tr class="clickrow '+cls+'" data-region="'+esc(r.regionCode)+'" data-rn="'+esc(r.regionName)+'">'+
715
        '<td><b>'+esc(r.regionName)+'</b> <span class="idpill">'+esc(r.regionCode)+'</span></td>'+
716
        '<td class="num">'+fmtInt(r.newCount)+'</td><td class="num">'+fmtInt(r.contacted)+'</td><td class="num">'+fmtInt(r.qualified)+'</td>'+
717
        '<td class="num">'+fmtInt(r.visited)+'</td><td class="num">'+fmtInt(r.onboarded)+'</td>'+
718
        '<td class="num"><b style="color:'+pctColor(r.convPct)+'">'+r.convPct+'%</b></td>'+
719
        '<td class="num"><b style="color:'+slaColor(r.slaPct)+'">'+r.slaPct+'%</b></td></tr>'; });
720
    byId('dashState').innerHTML=h||'<tr><td colspan="8"><div class="ph">No regional data.</div></td></tr>';
721
    buildDashKpis(); buildFix();
722
  }
723
 
724
  function renderHeat(d){ DATA.heat=d; var weeks=(d&&d.weeks)?d.weeks:[],bms=(d&&d.bms)?d.bms:[];
725
    if(!bms.length){ byId('dashHeat').innerHTML='<div class="ph">No SLA data for the selected period.</div>'; return; }
726
    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'; }
727
    var h='<table class="heat"><thead><tr><td style="background:none;border:0"></td>';
728
    weeks.forEach(function(w){ h+='<td style="background:none;border:0;color:#97a1b5;font-size:10px;font-weight:700">'+esc(w)+'</td>'; });
729
    h+='</tr></thead><tbody>';
730
    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>';
731
      (bm.cells||[]).forEach(function(c){ if(c.pct==null){ h+='<td style="background:#eef1f6;color:#97a1b5">–</td>'; }
732
        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>'; });
733
    h+='</tbody></table>'; byId('dashHeat').innerHTML=h;
734
  }
735
 
736
  function renderAgeing(d){ DATA.ageing=d; buildDashKpis(); buildFix(); }
737
 
738
  function buildDashKpis(){ var s=DATA.summary; if(!s) return;
739
    var contacted=0; (s.funnel||[]).forEach(function(f){ if(f.stage==='CONTACTED')contacted=f.count; });
740
    var tot=0,acc=0; ((DATA.state&&DATA.state.rows)||[]).forEach(function(r){ tot+=r.newCount; acc+=r.slaPct*r.newCount; });
741
    var sla=tot>0?Math.round(acc/tot):0;
742
    var vals=[ {v:fmtInt(s.total),f:'in period'}, {v:s.conversionPct+'<small>%</small>',f:'new &rarr; onboarded'},
743
      {v:sla+'<small>%</small>',f:'first contact ≤ 5h'}, {v:fmtInt(Math.round(contacted/90)),f:'proxy · no telephony'},
744
      {v:DATA.ageing?fmtInt(DATA.ageing.total):'—',f:'stuck by stage'}, {v:fmtInr(s.onboardedValue),f:'est. potential'} ];
745
    var labels=['LMS created','Conversion','SLA compliance','Calls / day (BGC)','Ageing &gt; 7 days','Onboarded value'];
746
    var h=''; vals.forEach(function(x,i){ var m=DASH_KPI[i];
747
      h+='<div class="kpi"><div class="top"><div class="ic" style="background:'+m.bg+';color:'+m.fg+'">'+m.i+'</div>'+
748
        '<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>'; });
749
    byId('dashKpis').innerHTML=h;
750
  }
751
 
752
  function buildFix(){ var rows=((DATA.state&&DATA.state.rows)||[]).slice();
753
    var out='';
754
    if(rows.length){ var worst=rows.slice().sort(function(a,b){return a.convPct-b.convPct;})[0];
755
      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>'+
756
        '<div class="fd"><b>'+fmtInt(worst.newCount)+' created, only '+worst.convPct+'% onboarded and '+worst.slaPct+'% SLA</b> — the weakest region this period.</div>'+
757
        '<div class="act">&rarr; Review its stuck leads; check BM mapping / HOLD queue.</div></div></div>';
758
      var lowSla=rows.slice().sort(function(a,b){return a.slaPct-b.slaPct;})[0];
759
      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>'+
760
        '<div class="fd"><b>'+lowSla.slaPct+'% on-time first contact</b> — first-contact discipline is breaking down.</div>'+
761
        '<div class="act">&rarr; Enforce ASM mapping (≤2h) + coaching.</div></div></div>'; } }
762
    var age=DATA.ageing?DATA.ageing.total:0;
763
    out+='<div class="fix"><div class="sev" style="background:#f59f00"></div><div class="fb"><div class="ft">'+fmtInt(age)+' leads ageing &gt; 7 days</div>'+
764
      '<div class="fd"><b>Stuck open leads by stage</b> — qualified leads not being added to beats lose revival odds.</div>'+
765
      '<div class="act">&rarr; Nudge ASMs to beat-plan qualified LMS within 48h.</div></div></div>';
766
    byId('dashFix').innerHTML=out;
767
  }
768
 
769
  // ---- drill drawer
770
  function openDrawer(crumb,title,sub,html){ byId('dCrumb').textContent=crumb||''; byId('dTitle').textContent=title||'';
771
    byId('dSub').textContent=sub||''; byId('dBody').innerHTML=html||''; byId('ov').classList.add('open'); byId('drawer').classList.add('open'); }
772
  function closeDrawer(){ byId('ov').classList.remove('open'); byId('drawer').classList.remove('open'); }
773
  document.addEventListener('keydown',function(e){ if(e.key==='Escape')closeDrawer();
774
    if(e.key==='/'&&document.activeElement.tagName!=='INPUT'){ e.preventDefault(); byId('lmsSearch').focus(); } });
775
 
776
  function drawerLeadTable(leads){ if(!leads||!leads.length) return '<div class="ph">No leads in this bucket.</div>';
777
    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>';
778
    leads.forEach(function(l){ h+='<tr class="clickrow" data-leadid="'+l.id+'"><td><span class="idpill">'+esc(l.lmsCode)+'</span></td>'+
779
      '<td><b>'+esc(l.name||'—')+'</b>'+(l.outlet?'<br><span style="color:#97a1b5;font-size:11px">'+esc(l.outlet)+'</span>':'')+'</td>'+
780
      '<td>'+stagePill(l.stage,l.stageLabel)+'</td><td>'+esc(l.owner||'—')+'</td><td>'+slaCell(l.slaState)+'</td>'+
781
      '<td class="num">'+inrShort(l.value)+'</td></tr>'; });
782
    return h+'</tbody></table></div>'; }
783
  function drill(crumb,title,sub,bt,bk){ openDrawer(crumb,title,'Loading…','<div class="ph">Loading…</div>');
784
    var q='?bucketType='+encodeURIComponent(bt)+(bk?'&bucketKey='+encodeURIComponent(bk):'')+qs();
785
    getJSON('/lms/dashboard/leads'+q,function(d){ var leads=(d&&d.leads)?d.leads:[];
786
      openDrawer(crumb,title,(sub||'')+' · '+leads.length+' lead(s)',drawerLeadTable(leads)); }); }
787
 
37651 vikas 788
  // ---- app-bar search: LMS id / retailer / business / phone / city -> results in the drawer
789
  var searchTimer=null, searchSeq=0;
790
  function runSearch(){ var q=(byId('lmsSearch').value||'').trim();
791
    if(q.length<2){ toast('Type at least 2 characters'); return; }
792
    var seq=++searchSeq;
793
    openDrawer('Search','\u201c'+q+'\u201d','Searching…','<div class="ph">Searching…</div>');
794
    getJSON('/lms/dashboard/search?q='+encodeURIComponent(q),function(d){
795
      if(seq!==searchSeq) return;                       // a newer search already won
796
      var leads=(d&&d.leads)?d.leads:[];
797
      openDrawer('Search','\u201c'+q+'\u201d',leads.length+' match(es)',drawerLeadTable(leads)); },
798
      function(){ if(seq!==searchSeq) return;
799
        openDrawer('Search','\u201c'+q+'\u201d','Search failed','<div class="ph">Could not run the search.</div>'); }); }
800
  byId('lmsSearch').addEventListener('keydown',function(e){
801
    if(e.key==='Enter'){ e.preventDefault(); clearTimeout(searchTimer); runSearch(); }
802
    else if(e.key==='Escape'){ this.blur(); closeDrawer(); } });
803
  byId('lmsSearch').addEventListener('input',function(){ clearTimeout(searchTimer);
804
    var v=(this.value||'').trim(); if(v.length<2) return;
805
    searchTimer=setTimeout(runSearch,400); });
806
 
37125 vikas 807
  var REPORT_NOTE={ 'State Funnel':'Live from the lead record — grouped by region, BM and ASM.',
808
    'SLA Compliance':'Live — first_contacted_at vs first_contact_due, owner heat-map.',
809
    'Ageing':'Live — open leads untouched > 7 days, by stage.',
810
    'Management Roll-up':'Live — cross-region leads, onboardings and estimated value.',
811
    'BGC Daily':'Partial — created + dispositions are real; call volume is a PROXY (contacted ÷ days). No telephony bridge yet.',
812
    'Audit Hygiene':'Partial — missing-disposition & HOLD are shown; DND breaches & recording flags need the call/recording log.',
813
    'RBM Servicing':'Pending data — repeat-order / complaint / resolution-TAT need order + ticket data outside the lead record.' };
814
 
815
  // ---- delegated clicks
816
  document.addEventListener('click',function(e){
817
    var kpi=e.target.closest('.kpi[data-kpi]'); if(kpi){ var b=kpiBucket(kpi.getAttribute('data-kpi'));
818
      drill('Command Center',kpi.querySelector('.label').textContent,'KPI drill',b[0],b[1]); return; }
819
    var fs=e.target.closest('.fstep[data-stage]'); if(fs){ var st=fs.getAttribute('data-stage');
820
      drill('Lifecycle funnel',fs.getAttribute('data-label'),'Reached this stage',st==='ALL'?'ALL':'STAGE',st==='ALL'?'':st); return; }
821
    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; }
822
    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; }
823
    var rep=e.target.closest('tr[data-rep]'); if(rep){ var nm=rep.getAttribute('data-rep');
824
      openDrawer('Report suite',nm,'SOP §18.2','<div class="card"><div class="dnote">'+esc(REPORT_NOTE[nm]||'')+'</div></div>'); return; }
825
    var lead=e.target.closest('[data-leadid]'); if(lead){ var lid=lead.getAttribute('data-leadid');
826
      closeDrawer(); LOADED.record=true; byId('recLookup').value=lid; injectRecord(lid);
827
      go('record',document.querySelectorAll('.tab')[1]); return; }
828
  });
829
 
830
  // ---- filters
831
  function refresh(){ STATE.regionId=byId('fRegion').value||''; STATE.from=byId('fFrom').value||''; STATE.to=byId('fTo').value||'';
832
    getJSON('/lms/dashboard/summary?_=1'+qs(),renderSummary); loadPipeline();
833
    getJSON('/lms/dashboard/state-funnel?_=1'+qs(),renderState);
834
    getJSON('/lms/dashboard/sla-heatmap?_=1'+qs(),renderHeat);
835
    getJSON('/lms/dashboard/ageing',renderAgeing);
836
    byId('dashPeriod').textContent=(STATE.from||'')+' → '+(STATE.to||''); }
837
  ['fRegion','fFrom','fTo'].forEach(function(id){ byId(id).addEventListener('change',refresh); });
838
 
839
  // ====================== LEAD RECORD (native sts-mockup layout, real data) ======================
840
  var STEPS=[['NEW','New'],['ASSIGNED','Assigned'],['CONTACTED','Contacted'],['QUALIFIED','Qualified'],
841
    ['BEAT_PLANNED','Beat-plan'],['VISITED','Visited'],['ONBOARDED','Onboarded'],['ACTIVE','Active']];
842
  function fmtDT(s){ if(!s) return '—'; var d=new Date(s); if(isNaN(d)) return esc(s);
843
    var mo=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
844
    function p(n){ return (n<10?'0':'')+n; }
845
    return p(d.getDate())+' '+mo[d.getMonth()]+' '+d.getFullYear()+' · '+p(d.getHours())+':'+p(d.getMinutes()); }
846
  function kvRow(k,v){ return '<div class="k">'+k+'</div><div class="v">'+(v==null||v===''?'—':v)+'</div>'; }
847
  function trailType(t){ if(t==='TELEPHONIC'||t==='CALL') return 'call'; if(!t) return 'sys'; return ''; }
848
 
849
  function injectRecord(id){ byId('recordHost').innerHTML='<div class="ph">Loading lead #'+esc(id)+'…</div>';
850
    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); }); }
851
  function loadRecord(){ var v=(byId('recLookup').value||'').replace(/\D/g,''); if(!v){ toast('Enter a lead #'); return; } injectRecord(v); }
852
  function loadRecentRecord(){ getJSON('/lms/dashboard/leads?bucketType=ALL'+qs(),function(d){ var leads=(d&&d.leads)?d.leads:[];
853
    if(leads.length){ byId('recLookup').value=leads[0].id; injectRecord(leads[0].id); }
854
    else { byId('recordHost').innerHTML='<div class="ph">No leads to show. Create one from the Create Lead tab.</div>'; } }); }
855
 
856
  function renderRecord(d){
857
    // header
858
    var head='<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:14px">'+
859
      '<div><div class="crumb">'+(d.lmsCode?('<span class="idpill">'+esc(d.lmsCode)+'</span>'):('Lead #'+d.id))+'</div>'+
860
      '<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>'+
861
      '<div style="display:flex;gap:8px;align-items:center">'+stagePill(d.stage,d.stageLabel)+
862
      ' <span class="stage" style="background:#eef1f6;color:#69748c">SLA: '+esc(d.slaState||'NA')+'</span>'+
863
      (d.assignmentStatus==='HOLD'?' <span class="stage st-ni">HOLD</span>':'')+'</div></div>';
864
    // stepper
865
    var idx=(d.stageIndex==null?-1:d.stageIndex);
866
    var step='<div class="card" style="padding:16px 22px"><div class="rec-stepper">';
867
    if(d.terminal){ step+='<div style="font-weight:700;color:#e8590c">'+esc(d.stageLabel)+' · terminal state</div>'; }
868
    else { STEPS.forEach(function(s,i){ var cls=i<idx?'done':(i===idx?'cur':''); var last=(i===STEPS.length-1);
869
      step+='<div class="rec-step '+cls+'"><div class="dot">'+(i<idx?'&#10003;':(i+1))+'</div><div class="sl">'+esc(s[1])+'</div>'+(last?'':'<div class="bar"></div>')+'</div>'; }); }
870
    step+='</div></div>';
871
    // details
872
    var details='<div class="card"><h2>Record</h2><p class="desc">Mandatory fields captured at creation. ID, dates &amp; assignment are system-set.</p>'+
873
      '<div class="grid g2" style="gap:20px"><div class="kv">'+
874
        kvRow('LMS ID', d.lmsCode?('<span class="mono">'+esc(d.lmsCode)+'</span>'):'—')+
875
        kvRow('Created', fmtDT(d.created)+(d.createdBy?(' · '+esc(d.createdBy)):''))+
876
        kvRow('Creation path', d.path?('<span class="tag '+(d.path==='B'?'b':'a')+'">'+(d.path==='B'?'B · Field':'A · BGC')+'</span>'):'—')+
877
        kvRow('Retailer', esc(d.name))+
878
        kvRow('Business', esc(d.outlet))+
879
        kvRow('Contact', d.contact?('<span class="mono">'+esc(d.contact)+'</span>'):'—')+
880
      '</div><div class="kv">'+
881
        kvRow('Location', esc(d.location))+
882
        kvRow('Region', esc(d.regionCode))+
883
        kvRow('Business value', inrShort(d.value))+
884
        kvRow('Source', esc(d.source))+
885
        kvRow('Last disposition', d.disposition?esc(d.disposition):'—')+
886
        kvRow('Owner BM/RSM', esc(d.ownerBm))+
887
      '</div></div><div class="divider"></div>'+
888
      '<div class="kv" style="grid-template-columns:130px 1fr 130px 1fr">'+
889
        kvRow('Working owner', esc(d.owner))+kvRow('Current stage', stagePill(d.stage,d.stageLabel))+
890
        kvRow('First contact', d.firstContactedAt?fmtDT(d.firstContactedAt):'<span style="color:#e8590c">not yet</span>')+
891
        kvRow('Unreachable tries', d.unreachableCount==null?'0':d.unreachableCount)+
892
      '</div></div>';
893
    // SLA card
894
    var slaClr=d.slaState==='MET'?'#2f9e44':(d.slaState==='BREACHED'?'#e03131':(d.slaState==='AT_RISK'?'#f08c00':'#69748c'));
895
    var sla='<div class="card"><h2>First-contact SLA</h2><p class="desc">5-hr commitment from creation (SOP §11).</p>'+
896
      '<div style="font-size:26px;font-weight:800;color:'+slaClr+'">'+esc(d.slaState||'NA')+'</div>'+
897
      '<div class="kv" style="margin-top:10px">'+kvRow('Due by', d.firstContactDue?fmtDT(d.firstContactDue):'—')+
898
      kvRow('First contact', d.firstContactedAt?fmtDT(d.firstContactedAt):'<span style="color:#e8590c">not yet</span>')+'</div></div>';
899
    // trail
900
    var trail='<div class="card"><h2>Trail <span style="color:#97a1b5;font-weight:500;font-size:12px">· append-only</span></h2>';
901
    if(!d.trail||!d.trail.length){ trail+='<div class="ph">No activity yet.</div>'; }
902
    else { trail+='<div class="trail">'; d.trail.forEach(function(t){ trail+='<div class="tevt '+trailType(t.type)+'"><div class="tdot">&#9679;</div>'+
903
      '<div class="tt">'+(t.type?esc(t.type):'Update')+'</div>'+
904
      '<div class="tm">'+esc(t.actor||'System')+' · '+fmtDT(t.when)+(t.scheduled?(' · scheduled '+fmtDT(t.scheduled)):'')+'</div>'+
905
      (t.body?('<div class="td">'+esc(t.body)+'</div>'):'')+'</div>'; }); trail+='</div>'; }
906
    trail+='</div>';
907
 
37651 vikas 908
    var openBtn='<div style="text-align:right;margin-bottom:12px"><button class="btn coral sm" onclick="window.open(CTX+\'/dashboard?leadRecord='+d.id+'\',\'_blank\')">Open in app to act &rarr;</button></div>';
37125 vikas 909
    byId('recordHost').innerHTML=head+step+openBtn+'<div class="grid g3"><div>'+details+trail+'</div><div>'+sla+'</div></div>';
910
  }
911
 
912
  // ====================== CREATE LEAD (real /lms/resolve-region + /createLead) ======================
913
  var CREATE_PATH='A';
914
  function setPath(p,btn){ CREATE_PATH=p;
915
    byId('pathA').className='btn '+(p==='A'?'coral':'ghost')+' sm'; byId('pathB').className='btn '+(p==='B'?'coral':'ghost')+' sm';
37651 vikas 916
    byId('createDesc').textContent=(p==='A')?'Path A — captured by a BGC agent during an outbound call, after consent. Owner is the region BM.':'Path B — field encounter. Owner is you, the creator.';
917
  }
37125 vikas 918
  function initials(n){ n=(n||'').trim(); if(!n) return '—'; var p=n.split(/\s+/); return ((p[0]||'')[0]||'').toUpperCase()+((p[1]||'')[0]||'').toUpperCase(); }
919
  function resolveBM(regionId){ if(!regionId){ byId('rName').textContent='Pick a region…'; byId('rRegion').textContent='—'; byId('raAv').textContent='—';
920
      byId('rEsc').textContent='The owning BM/RSM is resolved from the region (cs.position).'; return; }
921
    getJSON('/lms/resolve-region?regionId='+encodeURIComponent(regionId),function(d){ d=d||{};
922
      byId('rRegion').textContent=d.regionCode||''; var nm=d.bmName||'';
923
      if(d.assignmentStatus==='HOLD' || !nm){ byId('rName').textContent='No active BM/RSM'; byId('raAv').textContent='!';
924
        byId('raAv').style.background='#e03131'; byId('rEsc').innerHTML='Region has no active owner &rarr; <b style="color:#e03131">HOLD queue</b>, Sales Head notified.'; }
925
      else { byId('rName').textContent=nm; byId('raAv').textContent=initials(nm); byId('raAv').style.background='#1f2a5e';
926
        byId('rEsc').textContent='Auto-resolved from region · escalation contact copied on the assignment email.'; } }); }
37651 vikas 927
  // ---- brand-wise business value ------------------------------------------------------------
928
  // Agents write "1L", "50k", "1.5 lakh" as readily as digits, and rejecting those would just push
929
  // them back to typing a single lump sum. Parsed here so the stored value is always plain rupees.
930
  function parseMoney(raw){
931
    if(raw===null||raw===undefined) return 0;
932
    var t=(''+raw).trim().toLowerCase().replace(/[₹,\s]/g,'');
933
    if(!t) return 0;
934
    var m=t.match(/^([0-9]*\.?[0-9]+)(k|l|lac|lacs|lakh|lakhs|cr|crore|crores)?$/);
935
    if(!m) return 0;
936
    var n=parseFloat(m[1]); if(isNaN(n)) return 0;
937
    switch(m[2]){
938
      case 'k': return Math.round(n*1000);
939
      case 'l': case 'lac': case 'lacs': case 'lakh': case 'lakhs': return Math.round(n*100000);
940
      case 'cr': case 'crore': case 'crores': return Math.round(n*10000000);
941
      default: return Math.round(n);
942
    }
943
  }
944
  function collectBrands(){
945
    var out=[];
946
    document.querySelectorAll('#cBrandGrid .bval').forEach(function(el){
947
      var v=parseMoney(el.value);
948
      if(v>0) out.push({brand:el.getAttribute('data-brand'), value:v});
949
    });
950
    return out;
951
  }
952
  function brandTotal(){
953
    var t=0; collectBrands().forEach(function(b){ t+=b.value; });
954
    var el=byId('cValueTotal');
955
    if(el) el.textContent='total '+(t>0?fmtMoney(t):'₹0');
956
    return t;
957
  }
958
  function fmtMoney(n){
959
    if(n>=10000000) return '₹'+(n/10000000).toFixed(n%10000000?2:0)+'Cr';
960
    if(n>=100000)   return '₹'+(n/100000).toFixed(n%100000?2:0)+'L';
961
    if(n>=1000)     return '₹'+(n/1000).toFixed(n%1000?1:0)+'K';
962
    return '₹'+n;
963
  }
964
 
37125 vikas 965
  function val(id){ var e=byId(id); return e?e.value:''; }
37651 vikas 966
  function submitLead(){ var fn=val('cRetailer').trim(), biz=val('cBusiness').trim();
967
    var phone=(val('cContact')||'').replace(/\D/g,'').slice(-10);
968
    var brands=collectBrands(); var value=brandTotal();
37125 vikas 969
    var region=val('cRegion'); var path=CREATE_PATH;
37651 vikas 970
    if(!fn){ toast('Retailer name is required'); return; }
971
    if(phone.length!==10){ toast('A valid 10-digit contact is required'); return; }
972
    if(!region){ toast('Pick a region'); return; }
973
    // No address check on either path any more: the capture link is what establishes location,
974
    // and it is generated for every lead regardless of path.
975
    var payload={ firstName:fn, mobile:phone, outletName:biz||'', source:'LMS Dashboard',
976
      regionId:parseInt(region,10), creationPath:path };
977
    // potential is the sum, so the funnel's value column keeps working, while the per-brand split
978
    // is what actually tells a BM what the counter is worth.
37125 vikas 979
    if(value && value>0) payload.potential=value;
37651 vikas 980
    if(brands.length) payload.leadBrands=brands;
981
    // /lms/createLead, NOT the shared /createLead: this one honours regionId and creationPath, so
982
    // the lead actually gets a stage, an LMS code, an owner and an SLA clock.
983
    fetch(CTX+'/lms/createLead',{method:'POST',credentials:'same-origin',
984
        headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},
985
        body:JSON.stringify(payload)})
986
      .then(function(r){ return r.text(); })
987
      .then(function(t){
988
        var d=null; try{ d=JSON.parse(t); }catch(e){ d=null; }
989
        var res=(d&&d.response)?d.response:d;
990
        if(!res || res.ok!==true){
991
          // A refusal carries the reason as a bare string under `response` (badRequest("...")),
992
          // whereas success is an object. Surface the reason, not a generic failure.
993
          toast((typeof res==='string' && res) || (res&&res.message) || 'Create failed');
994
          return;
995
        }
996
        var held=(res.assignmentStatus==='HOLD');
997
        toast(held ? ('Lead '+res.lmsCode+' created · no BM for that region, parked in HOLD')
998
                   : ('Lead '+res.lmsCode+' created · assigned to '+(res.ownerName||'the region BM')+' · SLA started'), 1);
999
        ['cRetailer','cBusiness','cContact'].forEach(function(x){ var e=byId(x); if(e) e.value=''; });
1000
        document.querySelectorAll('#cBrandGrid .bval').forEach(function(el){ el.value=''; });
1001
        brandTotal();
1002
 
1003
        // Hand the capture link over immediately — the whole point of generating it on create is
1004
        // that the agent is still on the call with the retailer.
1005
        if(res.geoLink){
1006
          if(navigator.clipboard && navigator.clipboard.writeText){
1007
            navigator.clipboard.writeText(res.geoLink).then(function(){
1008
              alert('Capture link copied to your clipboard — send it to the retailer now:\n\n'+res.geoLink);
1009
            }, function(){ prompt('Send this capture link to the retailer:', res.geoLink); });
1010
          } else {
1011
            prompt('Send this capture link to the retailer:', res.geoLink);
1012
          }
1013
        }
1014
 
1015
        byId('recLookup').value=res.leadId; injectRecord(res.leadId);
1016
        go('record',document.querySelectorAll('.tab')[1]); refresh(); })
37125 vikas 1017
      .catch(function(e){ toast('Create failed'); }); }
1018
 
1019
  // ====================== AUTO-ASSIGNMENT (real resolve per region) ======================
37651 vikas 1020
  function buildAssign(){ var opts=byId('cRegion').querySelectorAll('option'); var holdCount=0, assignedCount=0, pending=0;
1021
    var body=byId('assignMap'); body.innerHTML='';
1022
    // Rows are created up-front in region order, then filled in as each resolve lands — otherwise
1023
    // the table would be ordered by whichever request happened to answer first.
37125 vikas 1024
    opts.forEach(function(o){ if(!o.value) return; pending++;
37651 vikas 1025
      var label=o.textContent;
1026
      var tr=document.createElement('tr');
1027
      tr.innerHTML='<td><b>'+esc(label)+'</b></td><td colspan="2"><span class="ph">Resolving…</span></td>';
1028
      body.appendChild(tr);
1029
      function done(){ if(--pending===0){ renderAssignKpis(assignedCount,holdCount); } }
1030
      getJSON('/lms/resolve-region?regionId='+encodeURIComponent(o.value),function(d){ d=d||{};
37125 vikas 1031
        var hold=(d.assignmentStatus==='HOLD' || !d.bmName);
1032
        if(hold) holdCount++; else assignedCount++;
1033
        tr.innerHTML='<td><b>'+esc(label)+'</b></td>'+
1034
          '<td>'+(hold?'<span class="warnflag">— vacant —</span>':esc(d.bmName))+'</td>'+
1035
          '<td>'+(hold?'<span class="stage st-ni">HOLD</span>':'<span class="stage st-active">Active</span>')+'</td>';
37651 vikas 1036
        done();
1037
      },function(){ tr.innerHTML='<td><b>'+esc(label)+'</b></td><td colspan="2"><span class="warnflag">could not resolve</span></td>';
1038
        done(); }); });
37125 vikas 1039
    // HOLD queue leads
1040
    getJSON('/lms/dashboard/leads?bucketType=ATTENTION&bucketKey=HOLD',function(d){ var leads=(d&&d.leads)?d.leads:[];
1041
      if(!leads.length){ byId('holdQueue').innerHTML='<div class="ph">No leads in HOLD. 🎉</div>'; return; }
1042
      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+'">'+
1043
        '<span class="idpill">'+esc(l.lmsCode)+'</span> <span>'+esc(l.name||l.outlet||'—')+'</span></div>'; });
1044
      byId('holdQueue').innerHTML=h; }); }
1045
  function renderAssignKpis(assigned,hold){ var k=[
1046
      {i:'&#128231;',bg:'#eef1fb',fg:'#3b5bdb',l:'Regions mapped',v:assigned,f:'active BM/RSM'},
1047
      {i:'&#9208;',bg:'#fff0f0',fg:'#e03131',l:'Regions on HOLD',v:hold,f:'no active BM',vc:'#e03131'},
1048
      {i:'&#128100;',bg:'#fff4e6',fg:'#e8590c',l:'HOLD leads',v:(DATA.summary&&attn('HOLD'))||0,f:'awaiting a BM',vc:'#e8590c'},
1049
      {i:'&#9889;',bg:'#ebfbee',fg:'#2f9e44',l:'Resolve tiers',v:'L4→L5→L3',f:'cs.position'} ];
1050
    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>'+
1051
      '<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>'; });
1052
    byId('assignKpis').innerHTML=h; }
1053
 
1054
  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; }
1055
 
1056
  // ====================== RECORDINGS & AUDIT (live hygiene from attention) ======================
1057
  function buildAudit(){
1058
    // KPIs — missing dispositions is live; recording metrics are pending source.
1059
    var miss=attn('MISSING_DISP'), br=attn('BREACHED_NO_CONTACT'), hold=attn('HOLD');
1060
    var kp=[ {i:'&#127925;',bg:'#fff0ef',fg:'#f0483e',l:'Recordings (24 mo)',v:'—',f:'pending source'},
1061
      {i:'&#128172;',bg:'#fff4e6',fg:'#e8590c',l:'Missing dispositions',v:miss,f:'live',vc:'#e8590c'},
1062
      {i:'&#128683;',bg:'#fff0f0',fg:'#e03131',l:'DND breaches',v:'—',f:'needs call log'},
1063
      {i:'&#128065;',bg:'#eef1fb',fg:'#3b5bdb',l:'Recording opens',v:'—',f:'pending source'},
1064
      {i:'&#129529;',bg:'#e6fcf5',fg:'#0ca678',l:'Onboarded (deep audit)',v:fmtInt(onboardedCount()),f:'5% sampled'} ];
1065
    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>'+
1066
      '<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>'; });
1067
    byId('auditKpis').innerHTML=h;
1068
    // hygiene flags (clickable via data-att)
1069
    var flags=[ {k:'MISSING_DISP',i:'&#128172;',bg:'#fff4e6',fg:'#e8590c',t:'LMS with missing dispositions',d:'Next action stays locked until dispositioned',c:'warnflag',v:miss},
1070
      {k:'BREACHED_NO_CONTACT',i:'&#9888;',bg:'#fff0f0',fg:'#e03131',t:'SLA breaches — first contact',d:'Escalated to BM/RSM per matrix',c:'badflag',v:br},
1071
      {k:'HOLD',i:'&#128100;',bg:'#fff0f0',fg:'#e03131',t:'Unassigned records (HOLD)',d:'Region awaiting a BM',c:'badflag',v:hold} ];
1072
    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>'+
1073
      '<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>'; });
1074
    byId('auditHygiene').innerHTML=fh; }
1075
  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; }
1076
 
1077
  // ---- initial paint from seeds, pipeline loads live
1078
  renderSummary(SEED.summary); renderState(SEED.state); renderHeat(SEED.heat); renderAgeing(SEED.ageing);
1079
  byId('dashPeriod').textContent=(STATE.from||'')+' → '+(STATE.to||'');
1080
  loadPipeline();
1081
 
1082
  // ---- deep-link: a URL hash such as dash or record opens that tab
1083
  (function(){ var TABIDX={cmd:0,record:1,create:2,assign:3,dash:4,audit:5};
1084
    var h=(location.hash||'').replace('#','').trim();
1085
    if(h && TABIDX[h]!=null){ go(h, document.querySelectorAll('.tab')[TABIDX[h]]); }
1086
    window.addEventListener('hashchange',function(){ var k=(location.hash||'').replace('#','').trim();
1087
      if(k && TABIDX[k]!=null){ go(k, document.querySelectorAll('.tab')[TABIDX[k]]); } }); })();
1088
</script>
1089
</body>
1090
</html>