Subversion Repositories SmartDukaan

Rev

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

Rev Author Line No. Line
36877 vikas 1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
<title>Beat Journey</title>
7
<style>
8
  :root{
9
    --bg:#eef1f7; --card:#ffffff; --ink:#1d2433; --muted:#69748c; --soft:#97a1b5;
10
    --line:#e6e9f1; --line2:#eef1f6;
11
    --brand:#3b5bdb; --brand2:#5c7cfa; --teal:#0ca678; --cyan:#1098ad;
12
    --amber:#f59f00; --orange:#e8590c; --red:#e03131; --violet:#7048e8; --pink:#d6336c;
13
    --good:#2f9e44; --warn:#f08c00; --bad:#e03131;
14
  }
15
  *{box-sizing:border-box}
16
  html,body{margin:0;background:var(--bg);color:var(--ink);
17
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;font-size:14px}
18
  .wrap{max-width:1320px;margin:0 auto;padding:20px 24px 90px}
19
  a{color:var(--brand)}
20
  header.top{display:flex;align-items:center;justify-content:space-between;gap:18px;flex-wrap:wrap;margin-bottom:16px}
21
  .brand .crumb{font-size:12px;color:var(--soft);font-weight:600}
22
  .brand h1{margin:3px 0 0;font-size:23px;font-weight:740;letter-spacing:-.01em}
23
  .brand h1 .live{font-size:11px;font-weight:700;color:var(--teal);background:#e6fcf5;border:1px solid #c3fae8;border-radius:999px;padding:3px 9px;vertical-align:middle;margin-left:10px}
24
  .brand h1 .live i{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--teal);margin-right:5px;animation:pulse 1.6s infinite}
25
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
26
  .hd-right{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
27
  .ctl{background:var(--card);border:1px solid var(--line);border-radius:9px;padding:8px 12px;font-size:13px;color:var(--ink);font-weight:600}
28
  .ctl .lbl{color:var(--soft);font-weight:500;margin-right:5px}
29
  select.ctl,input.ctl{appearance:none;cursor:pointer;height:36px}
30
  input.ctl{cursor:auto}
31
  .btn-r{background:var(--brand);border:0;color:#fff;font-weight:650;padding:9px 15px;border-radius:9px;cursor:pointer;font-size:13px;height:36px}
32
  .lenses{display:flex;gap:4px;border-bottom:1px solid var(--line);margin-bottom:20px}
33
  .lens{padding:11px 4px;margin-right:22px;font-size:14px;font-weight:650;color:var(--muted);cursor:pointer;border:0;background:transparent;border-bottom:2.5px solid transparent;margin-bottom:-1px}
34
  .lens.active{color:var(--brand);border-bottom-color:var(--brand)}
35
  .lens .b{display:inline-block;background:#eef1fb;color:var(--brand);font-size:11px;font-weight:700;border-radius:999px;padding:1px 7px;margin-left:6px}
36917 vikas 36
  .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}
36929 vikas 37
  .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);display:flex;flex-direction:column}
36905 vikas 38
  .kpi.clickable{cursor:pointer;transition:border-color .12s,box-shadow .12s,transform .12s}
39
  .kpi.clickable:hover{border-color:#c7d0e8;box-shadow:0 3px 10px rgba(20,30,60,.09);transform:translateY(-1px)}
36877 vikas 40
  .kpi .top{display:flex;align-items:center;gap:8px}
41
  .kpi .ic{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;flex:none}
42
  .kpi .label{font-size:11.5px;color:var(--muted);font-weight:600;line-height:1.2}
36929 vikas 43
  .kpi .val{font-size:26px;font-weight:760;margin-top:auto;padding-top:9px;letter-spacing:-.02em;line-height:1}
36877 vikas 44
  .kpi .val small{font-size:13px;color:var(--soft);font-weight:600}
45
  .kpi .foot{font-size:11.5px;color:var(--soft);margin-top:7px}
36929 vikas 46
  .kpi .brow{display:flex;align-items:flex-start;text-align:center}
47
  .kpi .brow.blabs{margin-top:11px}
48
  .kpi .brow.bvals{margin-top:auto;padding-top:4px}
49
  .kpi .blab{flex:1;font-size:11px;color:var(--soft);font-weight:600;line-height:1.25}
50
  .kpi .bval{flex:1;font-size:24px;font-weight:760;letter-spacing:-.02em;line-height:1}
51
  .kpi .bval small{font-size:12px;color:var(--soft);font-weight:600}
52
  .kpi .bsep{color:var(--line);font-weight:600;align-self:center}
36877 vikas 53
  .grid{display:grid;gap:16px}
54
  .g2{grid-template-columns:1fr 1fr}
55
  .g3{grid-template-columns:1.55fr 1fr}
56
  .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)}
57
  .card h2{margin:0 0 3px;font-size:15px;font-weight:720;letter-spacing:-.01em}
58
  .card .desc{color:var(--muted);font-size:12.5px;margin:0 0 14px}
59
  .sectionlabel{font-size:11.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--soft);font-weight:700;margin:24px 2px 12px}
60
  .funnel{display:flex;gap:9px;align-items:stretch}
61
  .fstep{flex:1;border:1px solid var(--line);border-radius:11px;padding:13px 12px;position:relative;text-align:center;background:#fbfcfe}
36905 vikas 62
  .fstep.clickable{cursor:pointer;transition:border-color .12s,box-shadow .12s,transform .12s}
63
  .fstep.clickable:hover{border-color:#c7d0e8;box-shadow:0 3px 10px rgba(20,30,60,.09);transform:translateY(-1px)}
36916 vikas 64
  .fstep.active{border-color:var(--brand);background:#eef1fb;box-shadow:0 0 0 1px var(--brand) inset}
65
  .boardfilter{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted);margin:0 0 10px;padding:7px 11px;background:#eef1fb;border:1px solid #d7deef;border-radius:8px}
66
  .boardfilter b{color:#28304a}
67
  .boardfilter .clearf{margin-left:auto;border:1px solid #c7d0e8;background:#fff;color:#3b5bdb;border-radius:7px;padding:3px 10px;font-size:12px;font-weight:600;cursor:pointer}
68
  .boardfilter .clearf:hover{background:#f1f4ff}
36877 vikas 69
  .fstep .fn{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);font-weight:600}
70
  .fstep .fv{font-size:26px;font-weight:770;margin-top:6px}
71
  .fstep .fbar{height:5px;border-radius:3px;margin-top:10px;background:linear-gradient(90deg,var(--brand),var(--brand2))}
72
  .fstep .drop{font-size:11px;color:var(--red);margin-top:7px;font-weight:600}
73
  .fstep .keep{font-size:11px;color:var(--good);margin-top:7px;font-weight:600}
74
  .fstep .arrow{position:absolute;right:-12px;top:50%;transform:translateY(-50%);z-index:2;color:#c5cce0;font-size:15px}
36929 vikas 75
  .fstep .ssplit{display:flex;align-items:flex-start;margin-top:11px}
76
  .fstep .scol{flex:1}
77
  .fstep .scol .sl{font-size:11px;color:var(--soft);font-weight:600;line-height:1.25}
78
  .fstep .scol .sv{font-size:23px;font-weight:760;letter-spacing:-.02em;line-height:1;margin-top:7px;color:#1b2436}
79
  .fstep .scol .sv .u{font-size:11px;color:var(--soft);font-weight:600;margin-left:2px}
80
  .fstep .scol .sv.good{color:#2f9e44}
81
  .fstep .scol .sv.bad{color:#e8590c}
36877 vikas 82
  .leak{display:flex;gap:10px;margin-top:12px}
83
  .leakbox{flex:1;border:1px solid var(--line2);border-radius:10px;padding:10px 12px;background:#f8f9fc}
84
  .leakbox .lt{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:600}
85
  .leakbox .lv{font-size:20px;font-weight:760;margin-top:4px}
86
  table{width:100%;border-collapse:collapse;font-size:13px}
87
  th,td{text-align:left;padding:9px 9px;border-bottom:1px solid var(--line2)}
88
  th{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:700;white-space:nowrap;cursor:pointer}
89
  th.num,td.num{text-align:right}
90
  tbody tr:hover{background:#f7f9fd}
91
  tr.clickable{cursor:pointer}
92
  .lvl{display:inline-block;font-size:10.5px;font-weight:700;padding:2px 6px;border-radius:5px;background:#eef1fb;color:var(--brand)}
93
  .pill-state{display:inline-flex;align-items:center;gap:6px;font-weight:600;font-size:12.5px;padding:3px 9px;border-radius:999px}
94
  .ps-store{background:#e7f5ff;color:#1971c2}
95
  .ps-travel{background:#e6fcf5;color:#0ca678}
96
  .ps-done{background:#ebfbee;color:#2f9e44}
97
  .ps-idle{background:#fff4e6;color:#e8590c}
98
  .ps-no{background:#fff0f0;color:#e03131}
99
  .meter{display:inline-block;width:54px;height:7px;border-radius:4px;background:#eef1f6;vertical-align:middle;margin-left:7px;overflow:hidden}
100
  .meter > i{display:block;height:100%;border-radius:4px}
101
  .warnflag{color:var(--orange);font-weight:700}
102
  .badflag{color:var(--red);font-weight:700}
37060 vikas 103
  .leavebtn{border:1px solid #ffd8a8;background:#fff4e6;color:#e8590c;font-weight:650;font-size:11px;padding:2px 9px;border-radius:6px;cursor:pointer;white-space:nowrap}
104
  .leavebtn:hover{background:#ffe8cc}
105
  .leavebtn:disabled{opacity:.6;cursor:default}
36877 vikas 106
  .rowwarn{background:#fff9f0}
107
  .rowbad{background:#fff5f5}
36897 vikas 108
  .bstat{display:inline-block;font-size:10.5px;font-weight:700;padding:3px 9px;border-radius:999px;white-space:nowrap}
109
  .bstat.ok{background:#ebfbee;color:#2f9e44}
110
  .bstat.warn{background:#fff4e6;color:#e8590c}
111
  .bstat.no{background:#fff0f0;color:#e03131}
112
  .bdot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px;vertical-align:middle}
113
  .ptime{display:inline-block;background:#ebfbee;color:#2f9e44;font-weight:700;font-size:11px;padding:2px 7px;border-radius:6px;font-variant-numeric:tabular-nums}
36913 vikas 114
  .potime{display:inline-block;background:#ebfbee;color: #D60303A8;font-weight:700;font-size:11px;padding:2px 7px;border-radius:6px;font-variant-numeric:tabular-nums}
36897 vikas 115
  td.cnt{font-weight:700;font-variant-numeric:tabular-nums}
116
  .cgreen{color:#2f9e44}.camber{color:#f08c00}.cred{color:#e03131}.cmuted{color:#69748c}
36917 vikas 117
  tr.boardtot td{background:#f4f6fb;border-bottom:2px solid #cdd3df;font-weight:700}
36897 vikas 118
  .actbtn{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:7px;border:0;cursor:pointer;color:#fff;font-size:12px;margin-left:3px}
119
  .actbtn.map{background:#12b886}.actbtn.view{background:#3b5bdb}
120
  .actbtn.edit{background:#f08c00}.actbtn.add{background:#2f9e44;font-size:15px}
121
  .actbtn:active{transform:translateY(1px)}
36877 vikas 122
  .att{display:flex;align-items:center;gap:12px;padding:11px 12px;border:1px solid var(--line);border-radius:11px;margin-bottom:9px}
123
  .att .ic{width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:14px;flex:none}
124
  .att .ab{flex:1}
125
  .att .atitle{font-size:13px;font-weight:650}
126
  .att .adet{font-size:12px;color:var(--muted);margin-top:2px}
127
  .att .acount{font-size:18px;font-weight:780}
36882 vikas 128
  .att.clickable{cursor:pointer;transition:border-color .12s,box-shadow .12s}
129
  .att.clickable:hover{border-color:#c7d0e8;box-shadow:0 2px 8px rgba(20,30,60,.08)}
130
  .att .chev{color:#c2cadb;font-size:13px;margin-left:8px}
36941 vikas 131
  .attrow{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px}
132
  .attrow .att{margin-bottom:0}
133
  @media (max-width:1000px){.attrow{grid-template-columns:repeat(2,1fr)}}
36877 vikas 134
  .barrow{display:grid;grid-template-columns:60px 1fr 90px;align-items:center;gap:11px;margin:8px 0}
135
  .barrow .name{font-size:13px;font-weight:600}
136
  .track{background:#f1f3f9;border-radius:7px;height:20px;position:relative;overflow:hidden}
137
  .fill{height:100%;border-radius:7px;display:flex;align-items:center;justify-content:flex-end;padding-right:6px;font-size:10.5px;color:#fff;font-weight:650;min-width:2px}
138
  .barrow .meta{font-size:11.5px;color:var(--muted);text-align:right}
139
  .donutwrap{display:flex;align-items:center;gap:20px}
140
  .donut{width:138px;height:138px;border-radius:50%;flex:none;position:relative;
141
    -webkit-mask:radial-gradient(circle 42px at center,transparent 98%,#000 100%);mask:radial-gradient(circle 42px at center,transparent 98%,#000 100%)}
142
  .donut .center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;-webkit-mask:none;mask:none}
143
  .legend{display:flex;flex-direction:column;gap:9px;font-size:12.5px}
144
  .legend .it{display:flex;align-items:flex-start;gap:9px}
36913 vikas 145
  .legend .it.clickable{cursor:pointer;border-radius:8px;padding:4px 6px;margin:-4px -6px;transition:background .12s}
146
  .legend .it.clickable:hover{background:#f1f3f9}
147
  .barrow.clickable{cursor:pointer}
148
  .barrow.clickable:hover .track{outline:2px solid #c7d0e8;outline-offset:1px;border-radius:7px}
149
  .donut.clickable{cursor:pointer}
36877 vikas 150
  .dot{width:11px;height:11px;border-radius:3px;flex:none;margin-top:3px}
151
  .tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:11px}
152
  .tile{background:#f8f9fc;border:1px solid var(--line2);border-radius:11px;padding:12px}
153
  .tile .t{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--soft);font-weight:600}
154
  .tile .v{font-size:21px;font-weight:760;margin-top:5px}
155
  .tile .v small{font-size:12px;color:var(--soft);font-weight:600}
156
  .util{display:flex;height:30px;border-radius:8px;overflow:hidden}
157
  .util div{display:flex;align-items:center;justify-content:center;font-size:11.5px;font-weight:650;color:#fff}
158
  .utlegend{display:flex;gap:18px;margin-top:12px;font-size:12px;color:var(--muted);flex-wrap:wrap}
159
  .utlegend span{display:inline-flex;align-items:center;gap:7px}
160
  .callout{background:linear-gradient(180deg,#fbfcff,#f4f7ff);border:1px solid #dfe6fb;border-radius:14px;padding:18px 20px;margin-top:6px}
161
  .callout h2{margin:0 0 3px;font-size:16px}
162
  .callout .desc{color:var(--muted);font-size:12.5px;margin-bottom:14px}
163
  .fix{display:flex;gap:13px;padding:13px 0;border-bottom:1px solid #e6ebf8}
164
  .fix:last-child{border-bottom:0}
165
  .fix .sev{width:6px;border-radius:4px;flex:none}
166
  .fix .fb{flex:1}
167
  .fix .ft{font-size:14px;font-weight:700}
168
  .fix .fd{font-size:13px;color:#3a4459;margin-top:4px;line-height:1.5}
169
  .fix .fd b{color:var(--ink)}
170
  .fix .act{font-size:12.5px;color:var(--good);margin-top:6px;font-weight:650}
171
  .hide{display:none}
172
  .loading{padding:70px;text-align:center;color:var(--muted)}
173
  .spinner{width:30px;height:30px;border:3px solid #e3e8f5;border-top-color:var(--brand);border-radius:50%;animation:spin 1s linear infinite;margin:0 auto 12px}
174
  @keyframes spin{to{transform:rotate(360deg)}}
175
  .modal-back{position:fixed;inset:0;background:rgba(15,22,40,.45);display:none;align-items:flex-start;justify-content:center;z-index:50;padding:40px 16px;overflow:auto}
36903 vikas 176
  /* image lightbox — stacks above the journey modal */
177
  .img-back{position:fixed;inset:0;background:rgba(8,12,24,.82);display:none;align-items:center;justify-content:center;z-index:60;padding:30px}
178
  .img-back.show{display:flex}
179
  .img-back img{max-width:95%;max-height:92vh;border-radius:10px;box-shadow:0 16px 60px rgba(0,0,0,.5);background:#fff}
180
  .img-x{position:absolute;top:18px;right:22px;width:40px;height:40px;border-radius:50%;border:0;background:rgba(255,255,255,.18);color:#fff;font-size:26px;line-height:1;cursor:pointer}
181
  .img-x:hover{background:rgba(255,255,255,.3)}
36877 vikas 182
  .modal-back.show{display:flex}
36882 vikas 183
  .modal{background:#fff;border-radius:14px;max-width:780px;width:100%;padding:20px 22px;box-shadow:0 20px 60px rgba(10,20,50,.3);position:relative}
184
  .modal h3{margin:0 0 2px;font-size:17px;text-align:center}
185
  .modal > .desc{text-align:center}
37039 vikas 186
  .modal .x,.rh-panel .x,.od-panel .x{position:absolute;top:14px;right:16px;cursor:pointer;color:var(--soft);font-size:22px;line-height:1;border:0;background:none}
36882 vikas 187
  /* journey totals bar */
188
  .jstats{display:flex;gap:9px;margin:12px 0 2px;flex-wrap:wrap}
189
  .js{flex:1;min-width:104px;background:#f8f9fc;border:1px solid var(--line2);border-radius:10px;padding:9px 10px;text-align:center}
190
  .js .jv{font-size:15.5px;font-weight:760;color:var(--ink);font-variant-numeric:tabular-nums}
191
  .js .jl{font-size:10px;color:var(--soft);text-transform:uppercase;letter-spacing:.04em;margin-top:3px;font-weight:600}
36877 vikas 192
  .btn-ok{background:var(--good);border:0;color:#fff;font-weight:650;padding:6px 12px;border-radius:7px;cursor:pointer;font-size:12px}
37174 vikas 193
  .btn-no{background:var(--bad);border:0;color:#fff;font-weight:650;padding:6px 12px;border-radius:7px;cursor:pointer;font-size:12px}
36882 vikas 194
  /* journey timeline (exec drilldown) */
195
  .tl{position:relative;margin:14px 2px 4px;padding:4px 0}
196
  .tl::before{content:"";position:absolute;left:50%;top:6px;bottom:6px;width:2px;background:#e9edf5;transform:translateX(-50%)}
197
  .tlgap{position:relative;display:flex;justify-content:center;margin:-3px 0}
198
  .tlgap .gappill{background:#fff;border:1px solid var(--line);border-radius:999px;padding:3px 11px;font-size:11px;font-weight:600;color:var(--muted);z-index:2;box-shadow:0 1px 2px rgba(20,30,60,.05);font-variant-numeric:tabular-nums}
36955 vikas 199
  .tlgap .gappill.overrun{border-color:var(--red);color:var(--red)}
36903 vikas 200
  .tlrow{position:relative;display:flex;justify-content:space-between;align-items:flex-start;margin:14px 0}
36882 vikas 201
  .tlrow .node{position:absolute;left:50%;top:8px;transform:translateX(-50%);width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:13px;z-index:2;box-shadow:0 0 0 4px #fff}
202
  .tlcard{width:calc(50% - 26px);background:#fff;border:1px solid var(--line);border-radius:11px;box-shadow:0 2px 10px rgba(20,30,60,.07);display:flex;overflow:hidden}
203
  .tlrow.left .tlcard{flex-direction:row-reverse;text-align:right}
204
  .tlrow.right .tlcard{flex-direction:row;text-align:left}
36903 vikas 205
  /* remark on the opposite half, hugging the centre line */
206
  .tlremark{width:calc(50% - 26px);display:flex;align-items:flex-start;padding-top:6px}
207
  .tlrow.right .tlremark{justify-content:flex-end}
208
  .tlrow.left .tlremark{justify-content:flex-start}
209
  .tlremark .rk{max-width:100%;background:#fbfcfe;border:1px solid var(--line2);border-left:3px solid #c7d0e8;border-radius:8px;padding:7px 10px;font-size:11.5px;color:#4a5468;line-height:1.45;word-break:break-word}
210
  .tlrow.right .tlremark .rk{text-align:right}
211
  .tlrow.left .tlremark .rk{text-align:left}
212
  .tlremark .rkl{font-size:9px;text-transform:uppercase;letter-spacing:.05em;color:var(--soft);font-weight:700;margin-bottom:3px}
37038 vikas 213
  .tlremark .rkhist{display:inline-block;margin-top:6px;background:none;border:0;padding:0;color:var(--brand);font-size:10.5px;font-weight:700;cursor:pointer}
214
  .tlremark .rkhist:hover{text-decoration:underline}
215
  /* past-discussions drawer */
216
  .rh-back{position:fixed;inset:0;background:rgba(15,22,40,.42);z-index:1200;display:none}
217
  .rh-back.show{display:block}
218
  .rh-panel{position:absolute;top:0;right:0;height:100%;width:min(400px,92vw);background:#fff;box-shadow:-8px 0 30px rgba(20,30,60,.18);display:flex;flex-direction:column;padding:18px 18px 14px;transform:translateX(100%);transition:transform .22s ease}
219
  .rh-back.show .rh-panel{transform:translateX(0)}
220
  .rh-panel h3{margin:0 0 2px;font-size:15px;color:var(--ink)}
221
  .rh-panel .desc{margin:0;font-size:12px;color:var(--soft)}
222
  .rh-list{flex:1;overflow-y:auto;margin-top:12px;display:flex;flex-direction:column;gap:9px}
223
  .rhitem{background:#fbfcfe;border:1px solid var(--line2);border-left:3px solid #c7d0e8;border-radius:9px;padding:9px 11px}
224
  .rhtop{display:flex;justify-content:space-between;align-items:center;gap:8px}
225
  .rhdate{font-size:11px;font-weight:700;color:var(--muted);font-variant-numeric:tabular-nums}
226
  .rhb{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;padding:1px 6px;border-radius:5px;white-space:nowrap}
227
  .rhb.ok{background:#e7f7ec;color:#1a7a3c}
228
  .rhb.def{background:#fff4e6;color:#e8590c}
229
  .rhexec{font-size:10px;color:var(--soft);font-weight:600;margin:3px 0 4px}
230
  .rhtext{font-size:12px;color:#4a5468;line-height:1.45;word-break:break-word;white-space:pre-wrap}
231
  .rhloading,.rhempty{color:var(--soft);font-size:12px;text-align:center;padding:24px 8px}
232
  .rh-more{margin-top:10px;background:var(--brand);color:#fff;border:0;border-radius:8px;padding:9px;font-weight:650;font-size:12.5px;cursor:pointer;display:none}
233
  .rh-more:disabled{opacity:.6;cursor:default}
37044 vikas 234
  /* orders drawer (stacks above the remark drawer) */
37039 vikas 235
  .od-back{position:fixed;inset:0;background:rgba(15,22,40,.42);z-index:1300;display:none}
236
  .od-back.show{display:block}
237
  .od-panel{position:absolute;top:0;right:0;height:100%;width:min(440px,94vw);background:#fff;box-shadow:-8px 0 30px rgba(20,30,60,.18);display:flex;flex-direction:column;padding:18px 18px 14px;transform:translateX(100%);transition:transform .22s ease}
238
  .od-back.show .od-panel{transform:translateX(0)}
239
  .od-panel h3{margin:0 0 2px;font-size:15px;color:var(--ink)}
240
  .od-panel .desc{margin:0;font-size:12px;color:var(--soft)}
241
  .od-body{flex:1;overflow-y:auto;margin-top:12px}
37044 vikas 242
  .odorder{border:1px solid var(--line);border-radius:11px;padding:11px;margin-bottom:12px;box-shadow:0 1px 2px rgba(20,30,60,.04)}
243
  .odorder .odsum{margin-bottom:8px}
37039 vikas 244
  .odsum{display:flex;flex-wrap:wrap;gap:8px 18px;padding:10px 12px;background:#fbfcfe;border:1px solid var(--line2);border-radius:9px;margin-bottom:12px}
245
  .odsum .k{font-size:9px;text-transform:uppercase;letter-spacing:.05em;color:var(--soft);font-weight:700}
246
  .odsum .v{font-size:13px;font-weight:700;color:var(--ink);font-variant-numeric:tabular-nums}
247
  .odtable{width:100%;border-collapse:collapse;font-size:11.5px}
248
  .odtable th{text-align:left;color:var(--soft);font-weight:700;font-size:9px;text-transform:uppercase;letter-spacing:.03em;padding:5px 6px;border-bottom:1px solid var(--line2)}
249
  .odtable td{padding:6px;border-bottom:1px solid #eef1f7;color:#4a5468;vertical-align:top}
250
  .odtable td.num{text-align:right;font-variant-numeric:tabular-nums}
251
  .odloading,.odempty{color:var(--soft);font-size:12px;text-align:center;padding:24px 8px}
36882 vikas 252
  .tlcard .tm{flex:none;width:62px;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;font-weight:750;font-size:13px;padding:6px 4px;line-height:1.05}
253
  .tlcard .tm small{font-weight:600;font-size:8.5px;opacity:.9;text-transform:uppercase;letter-spacing:.04em;margin-top:3px}
254
  .tlcard .bd{padding:8px 11px;flex:1;min-width:0}
255
  .tlcard .bd .ti{font-size:12.5px;font-weight:700;color:var(--ink);line-height:1.25;word-break:break-word}
256
  .tlcard .bd .stt{display:inline-block;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;padding:1px 6px;border-radius:5px;margin-top:5px;color:#fff}
36941 vikas 257
  .tlcard .bd .stt.unplanned{background:#fff4e6;color:#e8590c;border:1px solid #ffd8a8;margin-left:5px}
36882 vikas 258
  .tlcard .bd .mt{font-size:11px;color:var(--muted);margin-top:6px}
37044 vikas 259
  .tlcard .bd .tlags{margin-top:6px}
260
  .tlag{display:inline-block;font-size:9.5px;font-weight:700;color:#5646c9;background:#eeeafc;border:1px solid #ddd5f7;border-radius:5px;padding:1px 6px;margin:2px 4px 0 0}
261
  .tlrow.left .tlcard .bd .tlag{margin:2px 0 0 4px}
36882 vikas 262
  .tlextra{display:flex;align-items:center;gap:7px;margin-top:8px;flex-wrap:wrap}
263
  .tlrow.left .tlextra{justify-content:flex-end}
264
  .tlrow.right .tlextra{justify-content:flex-start}
265
  .tlthumb{width:46px;height:46px;border-radius:8px;object-fit:cover;border:1px solid var(--line);cursor:pointer;background:#f1f3f9}
266
  .tlph{width:46px;height:46px;border-radius:8px;border:1px dashed #cfd6e6;background:#f4f6fb;display:flex;align-items:center;justify-content:center;color:#b3bcce;font-size:18px}
267
  .tlchip{font-size:10.5px;font-weight:600;color:var(--muted);background:#f1f3f9;border-radius:6px;padding:2px 7px;white-space:nowrap}
37039 vikas 268
  .tlbadge{font-size:10.5px;font-weight:700;border:0;border-radius:6px;padding:2px 8px;white-space:nowrap;cursor:pointer;line-height:1.5}
269
  .tlbadge.ord{background:#e7f0ff;color:#1c56c9}
270
  .tlbadge.ord:hover{background:#d4e4ff}
271
  .tlbadge.sch{background:#fff4e6;color:#c25709;cursor:default}
36897 vikas 272
  .exectabs{display:flex;gap:6px;justify-content:center;margin:12px 0 4px}
273
  .exectab{padding:6px 14px;border-radius:8px;font-size:12.5px;font-weight:650;color:var(--muted);cursor:pointer;border:1px solid var(--line);background:#fff}
274
  .exectab.active{background:var(--brand);color:#fff;border-color:var(--brand)}
275
  #bjMap{width:100%;height:460px;border-radius:12px;border:1px solid var(--line);margin-top:10px;background:#eef1f7;overflow:hidden}
36903 vikas 276
  .bjlegend{display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin-top:12px;padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:#f8f9fc;font-size:12.5px;min-height:20px}
277
  .bjlegend .lg{font-weight:700}
278
  .bjlegend .sw{display:inline-block;width:24px;vertical-align:middle;margin-right:7px}
279
  .bjlegend .ln{border-top:4px solid #e03131}
280
  .bjlegend .dt{border-top:4px dotted #2f9e44}
37174 vikas 281
  .valwrap{display:flex;justify-content:center;align-items:flex-start;gap:16px;flex-wrap:wrap;margin:14px 0 6px}
282
  .valfig{display:flex;flex-direction:column;align-items:center;gap:6px;margin:0;max-width:100%}
283
  .valcap{font-size:11px;font-weight:650;color:var(--soft);text-transform:uppercase;letter-spacing:.03em}
36897 vikas 284
  .valimg{max-width:100%;max-height:440px;border-radius:12px;border:1px solid var(--line);cursor:zoom-in}
285
  .valph{width:100%;padding:48px;text-align:center;color:var(--soft);border:1px dashed #cfd6e6;border-radius:12px;background:#f4f6fb;font-size:14px}
286
  .valactions{display:flex;justify-content:center;gap:10px;margin-top:8px}
36877 vikas 287
  @media (max-width:1000px){.kpis{grid-template-columns:repeat(3,1fr)}.g2,.g3{grid-template-columns:1fr}.funnel{flex-wrap:wrap}.fstep{min-width:46%}.fstep .arrow{display:none}}
36903 vikas 288
  @media (max-width:560px){.tl::before{left:20px}.tlrow.left,.tlrow.right{justify-content:flex-end}.tlrow .node{left:20px}.tlcard{width:calc(100% - 44px)}.tlrow.left .tlcard{flex-direction:row;text-align:left}.tlremark{display:none}}
36877 vikas 289
</style>
290
</head>
291
<body>
292
<div class="wrap">
293
  <header class="top">
294
    <div class="brand">
295
      <div class="crumb"><a href="${rc.contextPath}/dashboard">&larr; Dashboard</a> &middot; Field Sales &middot; Beat / Monthly PJP</div>
296
      <h1>Beat Journey <span class="live"><i></i>Live</span></h1>
297
    </div>
298
    <div class="hd-right">
299
      <select class="ctl" id="bj-level">
300
        <option value="">All levels below me</option>
301
        #foreach($lvl in $escalationTypes)
302
          <option value="$lvl">$lvl</option>
303
        #end
304
      </select>
36897 vikas 305
      #if($topLevel)
306
      <select class="ctl hide" id="bj-user">
307
        <option value="">All users at level</option>
308
      </select>
309
      #end
36877 vikas 310
      <input class="ctl" type="date" id="bj-date" value="$today">
311
      <span id="bj-period-range" class="hide">
312
        <input class="ctl" type="date" id="bj-start" value="$monthStart">
313
        <input class="ctl" type="date" id="bj-end" value="$today">
314
      </span>
315
      <button class="btn-r" id="bj-apply">Apply</button>
36903 vikas 316
      <button class="btn-r" id="bj-export" style="background:#2f9e44" title="Download an Excel report for the selected date / range">&#11015; Export Excel</button>
36877 vikas 317
    </div>
318
  </header>
319
 
320
  <div class="lenses">
321
    <button class="lens active" data-lens="today" id="lens-today">Today (Live)</button>
322
    <button class="lens" data-lens="period" id="lens-period">Period (Analytics)</button>
323
  </div>
324
 
325
  <div id="bj-content"><div class="loading"><div class="spinner"></div>Loading&hellip;</div></div>
326
</div>
327
 
328
<div class="modal-back" id="bj-modal-back">
329
  <div class="modal" id="bj-modal"></div>
330
</div>
331
 
36903 vikas 332
<div class="img-back" id="bj-img-back">
333
  <button class="img-x" id="bj-img-x" title="Close">&times;</button>
334
  <img id="bj-img" alt="Attachment">
335
</div>
336
 
37038 vikas 337
<!-- past-discussions drawer — slides in from the right, above the journey modal -->
338
<div class="rh-back" id="bj-rh-back" onclick="if(event.target===this)closeRemarkHistory()">
339
  <div class="rh-panel" id="bj-rh-panel">
340
    <button class="x" onclick="closeRemarkHistory()">&times;</button>
341
    <h3 id="bj-rh-title"></h3>
342
    <p class="desc" id="bj-rh-sub"></p>
343
    <div class="rh-list" id="bj-rh-list"></div>
344
    <button class="rh-more" id="bj-rh-more" onclick="bjLoadRemarkHistory()">Load more</button>
345
  </div>
346
</div>
347
 
37044 vikas 348
<!-- orders drawer — slides in from the right, above the remark drawer -->
37039 vikas 349
<div class="od-back" id="bj-od-back" onclick="if(event.target===this)closeOrderDetail()">
350
  <div class="od-panel" id="bj-od-panel">
351
    <button class="x" onclick="closeOrderDetail()">&times;</button>
352
    <h3 id="bj-od-title"></h3>
353
    <p class="desc" id="bj-od-sub"></p>
354
    <div class="od-body" id="bj-od-body"></div>
355
  </div>
356
</div>
357
 
36877 vikas 358
<script>
359
var CTX = '${rc.contextPath}';
360
var BASE = CTX + '/beat-journey';
361
var LENS = 'today';
36897 vikas 362
var TODAY = '$today';
36962 vikas 363
var CAN_EDIT_BEAT = $canEditBeat;
36877 vikas 364
 
365
function el(id){ return document.getElementById(id); }
36897 vikas 366
// The first lens reads "Today (Live)" for today's date, otherwise it shows the
367
// selected date (the tab is still the live/single-day view, just for that day).
368
function fmtDateLabel(d){
369
  var p=(d||'').split('-'); if(p.length!==3) return d;
370
  var mo=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
371
  return parseInt(p[2],10)+' '+(mo[parseInt(p[1],10)-1]||p[1])+' '+p[0];
372
}
373
function updateTodayLabel(){
374
  var b=el('bj-date'), lbl=el('lens-today'); if(!lbl) return;
375
  var d=b?b.value:'';
376
  lbl.innerHTML = (!d || d===TODAY) ? 'Today (Live)' : esc(fmtDateLabel(d));
377
}
36877 vikas 378
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
379
function num(n){ return (n==null?0:n).toLocaleString(); }
380
function adhColor(p){ if(p>=80)return '#2f9e44'; if(p>=60)return '#f08c00'; if(p>=40)return '#e8590c'; return '#e03131'; }
381
function pct(a,b){ return b? Math.round(100*a/b):0; }
382
function qs(){
383
  var lvl = el('bj-level').value;
384
  var s = '';
385
  if(lvl) s += '&escalationType=' + encodeURIComponent(lvl);
36897 vikas 386
  var usr = el('bj-user');           // only present for L5/Final
387
  if(usr && usr.value) s += '&teamOfAuthUserId=' + encodeURIComponent(usr.value);
36877 vikas 388
  return s;
389
}
36897 vikas 390
// L5/Final: when a level is picked, list its users so a specific manager's team
391
// can be selected. Selecting a user scopes the whole view to that user's team.
392
function onLevelChange(){
393
  var usr = el('bj-user');
394
  if(!usr){ load(); return; }       // not top-level: just reload on level change
395
  var lvl = el('bj-level').value;
396
  usr.innerHTML = '<option value="">All users at level</option>';
397
  if(!lvl){ usr.classList.add('hide'); load(); return; }
398
  fetch(BASE + '/users?escalationType=' + encodeURIComponent(lvl), {headers:{'Accept':'application/json'}})
399
    .then(function(r){ return r.json(); })
400
    .then(function(d){
401
      var us = d.users || [];
402
      var h = '<option value="">All ' + esc(lvl) + ' users</option>';
403
      us.forEach(function(u){ h += '<option value="' + u.id + '">' + esc(u.name) + '</option>'; });
404
      usr.innerHTML = h;
405
      usr.classList.toggle('hide', us.length === 0);
406
      load();
407
    })
408
    .catch(function(){ usr.classList.add('hide'); load(); });
409
}
36877 vikas 410
function spinner(){ el('bj-content').innerHTML = '<div class="loading"><div class="spinner"></div>Loading&hellip;</div>'; }
411
function fail(e){ el('bj-content').innerHTML = '<div class="loading">Could not load data ('+esc(e&&e.message||e)+'). Are you signed in?</div>'; }
412
 
413
function setLens(name){
414
  LENS = name;
415
  var tabs = document.querySelectorAll('.lens');
416
  for(var i=0;i<tabs.length;i++){ tabs[i].classList.toggle('active', tabs[i].getAttribute('data-lens')===name); }
417
  el('bj-date').classList.toggle('hide', name!=='today');
418
  el('bj-period-range').classList.toggle('hide', name==='today');
419
  load();
420
}
421
function load(){
422
  if(LENS==='today') return loadToday();
423
  if(LENS==='period') return loadPeriod();
424
  return loadApproval();
425
}
426
 
427
/* ============================ TODAY ============================ */
428
function loadToday(){
36897 vikas 429
  updateTodayLabel();
36877 vikas 430
  spinner();
431
  fetch(BASE + '/today?date=' + encodeURIComponent(el('bj-date').value) + qs(), {headers:{'Accept':'application/json'}})
432
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
433
    .then(renderToday).catch(fail);
434
}
36905 vikas 435
function kpi(ic, bg, fg, label, val, foot, valColor, click){
436
  return '<div class="kpi'+(click?' clickable':'')+'"'+(click?' onclick="'+click+'" title="Click for details"':'')+'><div class="top"><div class="ic" style="background:'+bg+';color:'+fg+'">'+ic+'</div>'+
36929 vikas 437
    '<div class="label">'+label+'</div></div>'+
438
    '<div class="foot">'+foot+'</div>'+
439
    '<div class="val"'+(valColor?' style="color:'+valColor+'"':'')+'>'+val+'</div></div>';
36877 vikas 440
}
36929 vikas 441
// Multi-column breakdown card: centered title, a row of sub-labels, the number under each.
442
// cols = [[label, value], ...]; unit (optional) is appended after each number, e.g. 'min'.
443
function kpiCols(ic, bg, fg, title, cols, fg2, click, unit){
444
  var lab='', val='', u=unit?' <small>'+unit+'</small>':'';
445
  for(var i=0;i<cols.length;i++){
446
    var sep=i?'<span class="bsep">&middot;</span>':'';
447
    lab+=sep+'<div class="blab">'+cols[i][0]+'</div>';
448
    val+=(i?'<span class="bsep">&nbsp;</span>':'')+'<div class="bval"'+(fg2?' style="color:'+fg2+'"':'')+'>'+num(cols[i][1])+u+'</div>';
449
  }
450
  return '<div class="kpi kpicols'+(click?' clickable':'')+'"'+(click?' onclick="'+click+'" title="Click for details"':'')+'>'+
451
    '<div class="top"><div class="ic" style="background:'+bg+';color:'+fg+'">'+ic+'</div><div class="label">'+title+'</div></div>'+
452
    '<div class="brow blabs">'+lab+'</div>'+
453
    '<div class="brow bvals">'+val+'</div></div>';
454
}
455
// Period journey summary as a row of cards (old funnel layout). Each card is a
36941 vikas 456
// group (Total Visits / Avg Time Spent / Idle-Break / Deferred / Completed),
457
// planned and self-assigned merged into one value. col=[[label,value,colorClass],...].
36929 vikas 458
function summaryCard(title, cols, last){
459
  var body='';
460
  for(var i=0;i<cols.length;i++){
461
    body+='<div class="scol"><div class="sl">'+cols[i][0]+'</div>'+
462
          '<div class="sv'+(cols[i][2]?' '+cols[i][2]:'')+'">'+cols[i][1]+'</div></div>';
463
  }
464
  return '<div class="fstep"><div class="fn">'+title+'</div><div class="ssplit">'+body+'</div>'+
465
    (last?'':'<span class="arrow">&#10148;</span>')+'</div>';
466
}
467
function summaryCards(s){
468
  if(!s) return '';
469
  var mn=function(v){return num(v)+'<span class="u">min</span>';};
36941 vikas 470
  // planned + self-assigned merged into single totals
471
  var totalVisits=(s.plannedVisits||0)+(s.selfVisits||0);
472
  var completed=(s.completedPlanned||0)+(s.completedSelf||0);
473
  var deferred=(s.deferredPlanned||0)+(s.deferredSelf||0);
474
  return summaryCard('Total Visits',[['Visits',num(totalVisits),'']])+
475
    summaryCard('Average Time Spent',[['Per visit',mn(s.avgTimeAllMin),'']])+
36929 vikas 476
    summaryCard('Idle / Break',[['Total Average',mn(s.idleAvgMin),'']])+
36941 vikas 477
    summaryCard('Completed Visits',[['Completed',num(completed),'good']])+
478
    summaryCard('Deferred',[['Deferred',num(deferred),deferred>0?'bad':'']],true);
36929 vikas 479
}
36905 vikas 480
function fstep(name, val, w, sub, drop, last, key){
36916 vikas 481
  return '<div class="fstep'+(key?' clickable" data-fkey="'+key+'" onclick="funnelClick(\''+key+'\')" title="Click to filter the board to these executives':'')+'"><div class="fn">'+name+'</div><div class="fv">'+num(val)+'</div>'+
36913 vikas 482
    '<div class="fbar" style="width:'+Math.min(w,100)+'%"></div><div class="'+(drop?'drop':'keep')+'">'+sub+'</div>'+
36877 vikas 483
    (last?'':'<span class="arrow">&#10148;</span>')+'</div>';
484
}
36913 vikas 485
// Funnel drop sub-text: positive = fell out (red); negative = above planned
486
// (leads push visits past planned stops) shown green; zero = on plan.
487
function fdrop(p, badLabel){
488
  if(p>0) return '&minus;'+p+'% '+badLabel;
489
  if(p<0) return '+'+(-p)+'% above planned';
490
  return 'on plan';
491
}
36877 vikas 492
function leakbox(t, v, color){ return '<div class="leakbox"><div class="lt">'+t+'</div><div class="lv"'+(color?' style="color:'+color+'"':'')+'>'+num(v)+'</div></div>'; }
493
 
494
function renderToday(d){
495
  var k=d.kpis, f=d.funnel, s=d.stops, a=d.attention;
36897 vikas 496
  window._canApprove = !!d.canApprove;
37060 vikas 497
  window._canMarkLeave = !!d.canMarkLeave;
36897 vikas 498
  window._mediaUrl = d.mediaUrl || window._mediaUrl || '';
36905 vikas 499
  window._board = d.board || [];          // funnel drill-down filters this
500
  window._boardDate = d.date || '';
501
  window._leadsList = d.leadsList || [];  // leads drill-down
36917 vikas 502
  window._execList = d.execList || [];    // main-card drill-downs
36877 vikas 503
  var h='';
504
  h+='<div class="kpis">';
36917 vikas 505
  h+=kpi('&#128101;','#eef1fb','#3b5bdb','Total Executives',num(k.totalExecs),'in your team',null,"openCards('total')");
506
  h+=kpi('&#128197;','#ebfbee','#2f9e44','Scheduled PJP',num(k.scheduledPjp)+' <small>/'+k.totalExecs+'</small>',k.unscheduled+' unscheduled',null,"openCards('scheduled')");
507
  h+=kpi('&#9998;','#fff4e6','#e8590c','Agenda Filled',num(k.agendaFilled)+' <small>/'+k.scheduledPjp+'</small>',(k.scheduledPjp-k.agendaFilled)+' pending',null,"openCards('agenda')");
508
  h+=kpi('&#10060;','#fff0f6','#d6336c','Unscheduled',num(k.unscheduled),'no PJP today',null,"openCards('unscheduled')");
36877 vikas 509
  h+='</div>';
510
 
36941 vikas 511
  h+='<div class="card"><h2>Needs attention now</h2><p class="desc">Things a manager should clear today.</p>'+attentionList(a)+'</div>';
512
 
36877 vikas 513
  h+='<div class="sectionlabel">Today\'s Journey Funnel</div><div class="card"><div class="funnel">';
36917 vikas 514
  var sp=f.scheduledPjp||1, pin=f.punchedIn||1;
515
  h+=fstep('Punched In',f.punchedIn,pct(f.punchedIn,sp),f.punchedIn+' of '+f.scheduledPjp+' scheduled',false,false,'punchedIn');
516
  h+=fstep('Traveling',f.travelling,pct(f.travelling,pin),'on the road',false,false,'travelling');
517
  h+=fstep('In Store',f.inStore,pct(f.inStore,pin),'at a stop',false,false,'inStore');
37020 vikas 518
  h+=fstep('Idle / Break',f.idle,pct(f.idle,pin),'&lt; 1 km in 15 min',true,false,'idle');
36917 vikas 519
  h+=fstep('Punched Out',f.punchedOut,pct(f.punchedOut,pin),'journey ended',false,true,'punchedOut');
36877 vikas 520
  h+='</div></div>';
521
 
36916 vikas 522
  window._boardFilter = null;             // funnel-card filter (null = all rows)
523
  h+='<div class="card"><h2>Live executive board</h2><p class="desc">Each executive\'s journey right now. Click a row for the day\'s visit detail.</p><div id="bj-board">'+boardCard()+'</div></div>';
37248 vikas 524
 
525
  window._visitFlags = d.visitFlags || [];
526
  h+='<div class="sectionlabel">Visit-quality flags</div>';
527
  h+='<div class="card"><h2>Visit-quality flags</h2><p class="desc">Every rule scored on today\'s visits. Click a row with hits to see the flagged visits.</p>'+flagsTable(window._visitFlags)+'</div>';
528
 
36877 vikas 529
  el('bj-content').innerHTML=h;
530
}
531
function stateClass(st){ return st==='IN_STORE'?'ps-store':st==='TRAVELLING'?'ps-travel':st==='DONE'?'ps-done':st==='NOT_STARTED'?'ps-no':'ps-idle'; }
36897 vikas 532
function js(s){ return String(s==null?'':s).replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
533
// Open the Beat Planner (map-first planner window) for this executive/beat/day,
534
// in a new tab. Edit pre-selects the beat; add opens it to assign/add a visit.
535
function openPlanner(authUserId, beatId, date, mode){
536
  var u = CTX + '/beatPlanWindow?autoUserId=' + encodeURIComponent(authUserId) + '&editDate=' + encodeURIComponent(date);
537
  if(beatId && beatId>0) u += '&editBeatId=' + encodeURIComponent(beatId);
538
  if(mode==='add') u += '&assign=1';
539
  window.open(u, '_blank');
540
}
36916 vikas 541
// Funnel-card click: period cards (p_*) open their drill-down popup; today's
542
// funnel cards filter the Live executive board in place. Clicking the active
543
// card again clears the filter.
544
function funnelClick(key){
545
  if(key && key.indexOf('p_')===0) return openFunnel(key);
546
  setBoardFilter(window._boardFilter===key ? null : key);
547
}
548
// Apply / clear the funnel filter and re-render just the board card. Also marks
549
// the active funnel card so it's clear which cohort the board is showing.
550
function setBoardFilter(key){
551
  window._boardFilter = key || null;
552
  var c=el('bj-board'); if(c) c.innerHTML=boardCard();
553
  document.querySelectorAll('.fstep[data-fkey]').forEach(function(s){
554
    s.classList.toggle('active', !!key && s.getAttribute('data-fkey')===key);
555
  });
556
}
557
// Board card content: an optional "filtered by …" bar plus the table, showing
558
// only the rows that reached the selected funnel stage (Scheduled = all rows).
559
function boardCard(){
560
  var rows=window._board||[], date=window._boardDate||'', key=window._boardFilter;
37248 vikas 561
  var view=(rows||[]).slice(), m=key?FUNNEL_META[key]:null;   // copy — never reorder the source
562
  if(m) view=view.filter(m.f);
563
  if(boardSortKey) view.sort(function(a,b){
564
    var x=boardSortVal(a,boardSortKey), y=boardSortVal(b,boardSortKey);
565
    if(typeof x==='string') return boardSortAsc? (''+x).localeCompare(''+y):(''+y).localeCompare(''+x);
566
    return boardSortAsc? x-y : y-x;
567
  });
36916 vikas 568
  var bar='';
569
  if(m){
570
    bar='<div class="boardfilter">Showing <b>'+esc(m.title)+'</b> &middot; '+view.length+
571
        ' of '+rows.length+'<button class="clearf" onclick="setBoardFilter(null)">&times; Clear filter</button></div>';
572
  }
573
  return bar+boardTable(view, date);
574
}
36941 vikas 575
// Merge planned partner visits and self-assigned leads into one set of tallies.
576
// Total reconciles with the parts: Total = Completed + Pending + Deferred, where
577
// Total is the plan (or activity total if it ran past the plan).
578
function boardCounts(r){
579
  var completed=r.completedVisits||0, deferred=r.deferred||0;
580
  var total=Math.max(r.planned||0, completed+deferred);
581
  return {total:total, completed:completed, deferred:deferred, pending:total-completed-deferred};
582
}
37020 vikas 583
// seconds -> compact "1h 20m" / "45m" for the board's time columns.
584
function bhm(sec){ sec=Math.max(0,Math.round(sec||0)); var h=Math.floor(sec/3600), m=Math.round((sec%3600)/60); return h?(h+'h '+m+'m'):(m+'m'); }
37248 vikas 585
// Click-to-sort for the live board (mirrors the Period scorecard's sortCard). The
586
// numeric columns are derived (boardCounts) and times are raw seconds, so we sort on
587
// the underlying values here — not the rendered "1h 20m"/count strings.
588
var boardSortKey=null, boardSortAsc=false;
589
function boardSortVal(r, key){
590
  switch(key){
591
    case 'agenda':  return r.agendaFilled?1:0;
592
    case 'name':    return r.name||'';
593
    case 'punch':   return r.punchInTime||'';   // HH:MM:SS string; blanks group together
594
    case 'total': case 'completed': case 'pending': case 'deferred':
595
                    return boardCounts(r)[key];
596
    case 'transit': return r.transitSecs||0;
597
    case 'inStore': return r.inStoreSecs||0;
598
    case 'state':   return r.stateLabel||'';
599
    default:        return 0;
600
  }
601
}
602
function sortBoard(key){
603
  if(boardSortKey===key) boardSortAsc=!boardSortAsc; else { boardSortKey=key; boardSortAsc=false; }
604
  var c=el('bj-board'); if(c) c.innerHTML=boardCard();   // re-render; sort applied inside boardCard()
605
}
606
// Sort-direction caret, shown only on the active column.
607
function bsArrow(key){ return boardSortKey===key ? (boardSortAsc?' &#9650;':' &#9660;') : ''; }
36877 vikas 608
function boardTable(rows, date){
609
  if(!rows||!rows.length) return '<p class="desc">No executives scheduled or active for this date.</p>';
36897 vikas 610
  var h='<div style="overflow-x:auto"><table><thead><tr>'+
37248 vikas 611
    '<th onclick="sortBoard(\'agenda\')">Agenda'+bsArrow('agenda')+'</th>'+
612
    '<th onclick="sortBoard(\'name\')">Executive'+bsArrow('name')+'</th>'+
613
    '<th onclick="sortBoard(\'punch\')">Punch In/Out'+bsArrow('punch')+'</th>'+
614
    '<th class="num" title="Planned partner visits + self-assigned leads" onclick="sortBoard(\'total\')">Total'+bsArrow('total')+'</th>'+
615
    '<th class="num" onclick="sortBoard(\'completed\')">Completed'+bsArrow('completed')+'</th>'+
616
    '<th class="num" onclick="sortBoard(\'pending\')">Pending'+bsArrow('pending')+'</th>'+
617
    '<th class="num" onclick="sortBoard(\'deferred\')">Deferred'+bsArrow('deferred')+'</th>'+
618
    '<th class="num" title="Transit time between stops so far today" onclick="sortBoard(\'transit\')">Transit'+bsArrow('transit')+'</th>'+
619
    '<th class="num" title="Time spent inside stores so far today" onclick="sortBoard(\'inStore\')">In-store'+bsArrow('inStore')+'</th>'+
620
    '<th onclick="sortBoard(\'state\')">State'+bsArrow('state')+'</th><th>Action</th>'+
36897 vikas 621
    '</tr></thead><tbody>';
36917 vikas 622
  // column totals, computed up front so the Total row can lead the table
37020 vikas 623
  var t={total:0,completed:0,pending:0,deferred:0,transit:0,inStore:0};
36877 vikas 624
  rows.forEach(function(r){
36941 vikas 625
    var c=boardCounts(r);
626
    t.total+=c.total; t.completed+=c.completed; t.pending+=c.pending; t.deferred+=c.deferred;
37020 vikas 627
    t.transit+=(r.transitSecs||0); t.inStore+=(r.inStoreSecs||0);
36917 vikas 628
  });
629
  h+='<tr class="boardtot">'+
630
     '<td></td><td><b>Total</b><br><span style="color:#97a1b5;font-size:11px">'+rows.length+' executives</span></td><td></td>'+
36941 vikas 631
     '<td class="num cnt cmuted">'+t.total+'</td>'+
632
     '<td class="num cnt cgreen">'+t.completed+'</td>'+
633
     '<td class="num cnt">'+t.pending+'</td>'+
37020 vikas 634
     '<td class="num cnt camber">'+t.deferred+'</td>'+
635
     '<td class="num cnt cmuted">'+bhm(t.transit)+'</td>'+
636
     '<td class="num cnt cmuted">'+bhm(t.inStore)+'</td><td></td><td></td>'+
36917 vikas 637
     '</tr>';
638
  rows.forEach(function(r){
36897 vikas 639
    var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (r.idleAway?'rowwarn':'');
640
    var A = r.userId+",'"+js(r.name)+"','"+date+"'";   // openExec args
641
    var go = 'onclick="openExec('+A+')"';
36941 vikas 642
    var c=boardCounts(r);
36897 vikas 643
    h+='<tr class="clickable '+rowcls+'" '+go+'>'+
644
       '<td style="text-align:center">'+(r.agendaFilled?'<span style="color:#2f9e44;font-size:15px">&#10003;</span>':'<span class="badflag" style="font-size:15px">&#10007;</span>')+'</td>'+
37248 vikas 645
       '<td><b>'+esc(r.name)+'</b>'+flagBadge(r)+'<br>'+esc(r.beatName||'-')+'</td>'+
36897 vikas 646
       '<td style="white-space:nowrap">'+(r.punchInTime?'<span class="ptime">'+esc(r.punchInTime)+'</span>':'<span class="badflag">-</span>')+
647
         ' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
36941 vikas 648
       '<td class="num cnt cmuted">'+c.total+'</td>'+
649
       '<td class="num cnt cgreen">'+c.completed+'</td>'+
650
       '<td class="num cnt">'+c.pending+'</td>'+
651
       '<td class="num cnt camber">'+c.deferred+'</td>'+
37020 vikas 652
       '<td class="num cnt cmuted">'+(r.transitSecs?bhm(r.transitSecs):'<span style="color:#c5cce0">&mdash;</span>')+'</td>'+
653
       '<td class="num cnt cmuted">'+(r.inStoreSecs?bhm(r.inStoreSecs):'<span style="color:#c5cce0">&mdash;</span>')+'</td>'+
36905 vikas 654
       '<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
36897 vikas 655
       '<td style="white-space:nowrap" onclick="event.stopPropagation()">'+
656
         '<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">&#128205;</button>'+
657
         '<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">&#128065;</button>'+
36962 vikas 658
         (CAN_EDIT_BEAT
659
           ? '<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">&#9998;</button>'+
660
             '<button class="actbtn add" title="Assign / add visit in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'add\')">&#43;</button>'
661
           : '')+
36897 vikas 662
       '</td></tr>';
36877 vikas 663
  });
36917 vikas 664
  return h+'</tbody></table></div>';
36877 vikas 665
}
36905 vikas 666
// Funnel drill-down: each stage shows the executives who reached it AND the
667
// drop-off cohort that fell out at this step (the "−N …" sub-line). Filters
668
// mirror the backend funnel conditions exactly (rows carry the per-stage flags).
669
var FUNNEL_META = {
36917 vikas 670
  punchedIn:  {title:'Punched In',   desc:'Started the day (punch-in recorded)', f:function(r){ return r.punchedIn; }},
37020 vikas 671
  travelling: {title:'Traveling',    desc:'On the road between stops',           f:function(r){ return r.state==='TRAVELLING'; }},
36917 vikas 672
  inStore:    {title:'In Store',     desc:'Currently checked in at a stop',      f:function(r){ return r.state==='IN_STORE'; }},
37020 vikas 673
  idle:       {title:'Idle / Break', desc:'Punched in but < 1 km covered in the last 15 min', f:function(r){ return r.state==='IDLE'; }},
36917 vikas 674
  punchedOut: {title:'Punched Out',  desc:'Ended the day (punch-out recorded)',  f:function(r){ return r.punchedOut; }}
36905 vikas 675
};
676
function funnelRows(list, date){
677
  if(!list.length) return '<p class="desc">None.</p>';
678
  var h='<table><thead><tr><th>Executive / Beat</th><th>Punch In/Out</th><th class="num">Completed</th><th class="num">Planned</th><th>State</th></tr></thead><tbody>';
679
  list.slice().sort(function(a,b){ return (''+a.name).localeCompare(b.name); }).forEach(function(r){
680
    var A=r.userId+",'"+js(r.name)+"','"+date+"'";
681
    h+='<tr class="clickable" onclick="openExec('+A+')">'+
682
       '<td><b>'+esc(r.name)+'</b><br><span style="color:#97a1b5;font-size:11px">'+esc(r.beatName||'-')+'</span></td>'+
683
       '<td style="white-space:nowrap">'+(r.punchInTime?'<span class="ptime">'+esc(r.punchInTime)+'</span>':'<span class="badflag">-</span>')+
684
         ' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOutTime?esc(r.punchOutTime):'--')+'</span></td>'+
685
       '<td class="num cnt cgreen">'+r.completedVisits+'</td>'+
686
       '<td class="num">'+r.planned+'</td>'+
687
       '<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td></tr>';
688
  });
689
  return h+'</tbody></table>';
690
}
691
function openFunnel(key){
36913 vikas 692
  if(key && key.indexOf('p_')===0) return openPFunnel(key);   // period funnel
36905 vikas 693
  var m=FUNNEL_META[key]; if(!m) return;
694
  var board=window._board||[], date=window._boardDate||'';
695
  var reached=board.filter(m.f);
696
  var dropped=m.drop ? board.filter(m.drop) : [];
697
  var mb=el('bj-modal-back'), md=el('bj-modal');
698
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(m.title)+'</h3>'+
699
        '<p class="desc">'+reached.length+' '+(reached.length===1?'executive':'executives')+' &middot; '+esc(m.desc)+'</p>'+
700
        funnelRows(reached, date);
701
  if(m.drop){
702
    h+='<div class="sectionlabel" style="margin-top:18px;color:#e8590c">'+esc(m.dropLabel)+' &middot; '+dropped.length+'</div>'+
703
       funnelRows(dropped, date);
704
  }
705
  h+='<p class="desc" style="margin-top:10px">Click a row to open that executive\'s journey detail.</p>';
706
  md.innerHTML=h; mb.classList.add('show');
707
}
36917 vikas 708
// Main-card drill-downs: roster of executives behind each top KPI card.
709
// 'drop' (optional) adds a second, highlighted section for the complementary
710
// cohort — e.g. who's scheduled but hasn't filled the agenda.
711
var CARD_META = {
712
  total:       {title:'Total Executives', desc:'Everyone in your team',        f:function(r){ return true; }},
713
  scheduled:   {title:'Scheduled PJP',     desc:'Executives with a beat today', f:function(r){ return r.hasBeat; }},
714
  agenda:      {title:'Agenda Filled',     desc:'Beats with the agenda filled', f:function(r){ return r.agendaFilled; },
715
                dropLabel:'Agenda not filled (scheduled)', drop:function(r){ return r.hasBeat && !r.agendaFilled; }},
37060 vikas 716
  unscheduled: {title:'Unscheduled',       desc:'No PJP scheduled today',       f:function(r){ return !r.hasBeat && !r.onLeave; }}
36917 vikas 717
};
718
// Renders an executive roster table (used by the card drill-downs).
719
function execRows(list, date){
720
  if(!list.length) return '<p class="desc">None.</p>';
721
  var h='<table><thead><tr><th>Executive</th><th>Beat</th><th>Agenda</th><th>Status</th></tr></thead><tbody>';
722
  list.forEach(function(r){
723
    var A=r.userId+",'"+js(r.name)+"','"+date+"'";
724
    var go = r.userId ? ' class="clickable" onclick="openExec('+A+')"' : '';
725
    var agenda = !r.hasBeat ? '<span style="color:#97a1b5">&mdash;</span>'
726
      : (r.agendaFilled ? '<span style="color:#2f9e44">&#10003;</span>' : '<span class="badflag">&#10007;</span>');
37076 vikas 727
    // Execs who aren't clearly working today can be marked on leave: agenda not
728
    // filled or not punched in (covers unscheduled). Manager / super-user only;
729
    // server-enforced. Already-on-leave rows show no button.
730
    var leaveBtn = (window._canMarkLeave && r.authUserId && !r.onLeave && (!r.agendaFilled || !r.punchedIn))
37060 vikas 731
      ? ' <button class="leavebtn" onclick="event.stopPropagation();markLeave('+r.authUserId+",'"+js(r.name)+"',this)\">Mark leave</button>" : '';
36917 vikas 732
    h+='<tr'+go+'>'+
733
       '<td><b>'+esc(r.name)+'</b></td>'+
734
       '<td>'+esc(r.beatName||'-')+'</td>'+
735
       '<td style="text-align:center">'+agenda+'</td>'+
37060 vikas 736
       '<td>'+esc(r.statusLabel||'')+leaveBtn+'</td></tr>';
36917 vikas 737
  });
738
  return h+'</tbody></table>';
739
}
740
function openCards(key){
741
  var m=CARD_META[key]; if(!m) return;
37076 vikas 742
  window._openCardKey=key;   // remembered so markLeave can re-render the same view
36917 vikas 743
  var all=(window._execList||[]), date=window._boardDate||'';
744
  var list=all.filter(m.f);
745
  var dropped=m.drop ? all.filter(m.drop) : [];
746
  var mb=el('bj-modal-back'), md=el('bj-modal');
747
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(m.title)+'</h3>'+
748
        '<p class="desc">'+list.length+' '+(list.length===1?'executive':'executives')+' &middot; '+esc(m.desc)+'</p>'+
749
        execRows(list, date);
750
  if(m.drop){
751
    h+='<div class="sectionlabel" style="margin-top:18px;color:#e8590c">'+esc(m.dropLabel)+' &middot; '+dropped.length+'</div>'+
752
       execRows(dropped, date);
753
  }
754
  h+='<p class="desc" style="margin-top:10px">Click a row to open that executive\'s journey detail.</p>';
755
  md.innerHTML=h; mb.classList.add('show');
756
}
36905 vikas 757
// Leads drill-down: lists every lead visit logged today (matches the KPI count).
758
function openLeads(){
759
  var list=window._leadsList||[], date=window._boardDate||'';
760
  var mb=el('bj-modal-back'), md=el('bj-modal');
761
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>Leads on route</h3>'+
762
        '<p class="desc">'+list.length+' lead '+(list.length===1?'visit':'visits')+' logged'+(date?' on '+esc(date):'')+'</p>';
763
  if(!list.length){ h+='<p class="desc">No leads logged for this date.</p>'; }
764
  else {
765
    h+='<table><thead><tr><th>Executive</th><th>Lead</th><th>Check-in</th><th>Status</th></tr></thead><tbody>';
766
    list.slice().sort(function(a,b){ return (''+a.exec).localeCompare(b.exec); }).forEach(function(r){
767
      var A=r.userId+",'"+js(r.exec)+"','"+date+"'";
768
      var go = r.userId ? ' class="clickable" onclick="openExec('+A+')"' : '';
769
      h+='<tr'+go+'>'+
770
         '<td><b>'+esc(r.exec)+'</b></td>'+
771
         '<td>'+esc(r.lead||'-')+'</td>'+
772
         '<td style="white-space:nowrap">'+(r.checkInTime?'<span class="ptime">'+esc(hhmm(r.checkInTime))+'</span>':'<span class="badflag">-</span>')+'</td>'+
773
         '<td>'+esc(r.status||'')+'</td></tr>';
774
    });
775
    h+='</tbody></table><p class="desc" style="margin-top:10px">Click a row to open that executive\'s journey detail.</p>';
776
  }
777
  md.innerHTML=h; mb.classList.add('show');
778
}
36913 vikas 779
/* ===================== PERIOD (ANALYTICS) DRILL-DOWNS ===================== */
780
function pModal(html){ el('bj-modal').innerHTML=html; el('bj-modal-back').classList.add('show'); }
781
function pRange(){ return (el('bj-start')?el('bj-start').value:'')+' to '+(el('bj-end')?el('bj-end').value:''); }
782
 
783
// Scorecard list popup (used by KPI cards + funnel stages). Rows drill to the
784
// executive's day-by-day. sortKey sorts descending.
785
function openPScore(sortKey, title, desc){
786
  var rows=(((window._period||{}).scorecard)||[]).slice();
787
  if(sortKey) rows.sort(function(a,b){ return (b[sortKey]||0)-(a[sortKey]||0); });
788
  pModal('<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(title)+'</h3>'+
789
    '<p class="desc">'+esc(desc)+' &middot; '+esc(pRange())+' &middot; click a row for the day-by-day</p>'+pScoreTable(rows));
790
}
791
function pScoreTable(rows){
792
  if(!rows.length) return '<p class="desc">No executives in scope.</p>';
37020 vikas 793
  var h='<table><thead><tr><th>Executive</th><th class="num">Plan</th><th class="num">Done</th><th class="num">Adher.</th>'+
794
    '<th class="num" title="Avg lead visits per active (punched-in) day">Lead/day</th>'+
795
    '<th class="num" title="Avg franchisee visits per active (punched-in) day">Fr/day</th>'+
37028 vikas 796
    '<th class="num">Disc.</th><th class="num">Hrs</th><th class="num">Leads</th></tr></thead><tbody>';
36913 vikas 797
  rows.forEach(function(r){
798
    var c=adhColor(r.adherencePct);
799
    h+='<tr class="clickable" onclick="openExecPeriod('+r.authUserId+',\''+js(r.name)+'\')">'+
37020 vikas 800
       '<td><span class="lvl">'+esc(r.level)+'</span> <b>'+esc(r.name)+'</b></td>'+
36913 vikas 801
       '<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
802
       '<td class="num"><b style="color:'+c+'">'+r.adherencePct+'%</b></td>'+
37020 vikas 803
       '<td class="num">'+(r.avgLeadVisitsPerDay||'-')+'</td>'+
804
       '<td class="num">'+(r.avgFranchiseeVisitsPerDay||'-')+'</td>'+
36913 vikas 805
       '<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'-')+(r.discussionMin?'m':'')+'</td>'+
37028 vikas 806
       '<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td></tr>';
36913 vikas 807
  });
808
  return h+'</tbody></table>';
809
}
810
// Funnel stage → the executives behind it (the scorecard, focused).
811
function openPFunnel(key){
812
  var map={ p_planned:['planned','Planned stops','Planned partner + lead stops per executive'],
813
            p_punchedin:['done','Planned on punch-in days','Executives, planned vs done'],
814
            p_checkedin:['done','Checked in','Visits checked in per executive'],
815
            p_checkedout:['done','Checked out (done)','Visits completed per executive'] };
816
  var m=map[key]; if(m) openPScore(m[0],m[1],m[2]);
817
}
818
// Executive day-by-day (scorecard row drill). Fetches /exec-period; each day
819
// opens the journey detail / map modal.
820
function openExecPeriod(authUserId, name){
821
  pModal('<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(name)+'</h3>'+
822
    '<p class="desc">Day-by-day &middot; '+esc(pRange())+'</p><div class="loading"><div class="spinner"></div>Loading&hellip;</div>');
823
  fetch(BASE+'/exec-period?authUserId='+authUserId+'&startDate='+encodeURIComponent(el('bj-start').value)+'&endDate='+encodeURIComponent(el('bj-end').value),{headers:{'Accept':'application/json'}})
824
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
825
    .then(function(d){
826
      var rows=d.rows||[], uid=d.userId, nm=d.name||name;
827
      var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(nm)+'</h3>'+
828
        '<p class="desc">'+rows.length+' day'+(rows.length===1?'':'s')+' &middot; '+esc(pRange())+(uid?' &middot; click a day for the journey detail':'')+'</p>';
829
      if(!rows.length){ h+='<p class="desc">No scheduled or active days in this range.</p>'; }
830
      else {
831
        h+='<table><thead><tr><th>Date</th><th>Punch In/Out</th><th class="num">Planned</th><th class="num">Completed</th><th class="num">Deferred</th><th class="num">Total</th></tr></thead><tbody>';
832
        rows.forEach(function(r){
833
          var go = uid ? ' class="clickable" onclick="openExec('+uid+',\''+js(nm)+'\',\''+r.date+'\')"' : '';
834
          h+='<tr'+go+'><td>'+esc(r.date)+'</td>'+
835
             '<td style="white-space:nowrap">'+(r.punchIn?'<span class="ptime">'+esc(hhmm(r.punchIn))+'</span>':'<span class="badflag">-</span>')+
836
               ' <span style="color:#97a1b5;font-size:11px">/ '+(r.punchOut?esc(hhmm(r.punchOut)):'--')+'</span></td>'+
837
             '<td class="num">'+r.planned+'</td><td class="num cnt cgreen">'+r.done+'</td>'+
838
             '<td class="num cnt cred">'+r.deferred+'</td><td class="num cnt cmuted">'+r.total+'</td></tr>';
839
        });
840
        h+='</tbody></table>';
841
      }
842
      el('bj-modal').innerHTML=h;
843
    })
844
    .catch(function(){ el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">&times;</button><p class="desc">Could not load day-by-day.</p>'; });
845
}
846
// PJP coverage drill — execs with / without a plan (optionally one level).
847
function openCoverage(level){
848
  var all=((window._period||{}).coverageDetail)||[];
849
  var list=level ? all.filter(function(r){ return r.level===level; }) : all;
850
  var without=list.filter(function(r){ return !r.hasPlan; }), withP=list.filter(function(r){ return r.hasPlan; });
851
  function tbl(rs){
852
    if(!rs.length) return '<p class="desc">None.</p>';
853
    var h='<table><tbody>';
854
    rs.forEach(function(r){ h+='<tr class="clickable" onclick="openExecPeriod('+r.authUserId+',\''+js(r.name)+'\')"><td style="font-weight:600">'+esc(r.name)+'</td><td><span class="lvl">'+esc(r.level)+'</span></td></tr>'; });
855
    return h+'</tbody></table>';
856
  }
857
  pModal('<button class="x" onclick="closeModal()">&times;</button><h3>PJP coverage'+(level?' &middot; '+esc(level):'')+'</h3>'+
858
    '<p class="desc">'+withP.length+' of '+list.length+' have a monthly plan &middot; '+esc(pRange())+'</p>'+
859
    '<div class="sectionlabel" style="color:#e03131">No plan &middot; '+without.length+'</div>'+tbl(without)+
860
    '<div class="sectionlabel" style="color:#2f9e44;margin-top:16px">Has a plan &middot; '+withP.length+'</div>'+tbl(withP));
861
}
862
// Deferrals drill — all, or just system / genuine (from the donut segments).
863
function openDeferrals(filter){
864
  var all=((window._period||{}).deferralsList)||[];
865
  var list = filter==='system' ? all.filter(function(r){ return r.system; })
866
           : filter==='genuine' ? all.filter(function(r){ return !r.system; }) : all;
867
  var title = filter==='system' ? 'Deferrals — system-flagged' : filter==='genuine' ? 'Deferrals — genuine reason' : 'Deferrals';
868
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(title)+'</h3>'+
869
        '<p class="desc">'+list.length+' deferral'+(list.length===1?'':'s')+' &middot; '+esc(pRange())+'</p>';
870
  if(!list.length){ h+='<p class="desc">None in this range.</p>'; }
871
  else {
872
    h+='<table><thead><tr><th>Executive</th><th>Partner</th><th>Reason</th><th>Date</th><th>Status</th></tr></thead><tbody>';
873
    list.forEach(function(r){
874
      h+='<tr><td style="font-weight:600;white-space:nowrap">'+esc(r.exec)+'</td>'+
875
         '<td>'+esc(r.partner||'-')+'</td>'+
876
         '<td style="color:'+(r.system?'#f08c00':'#2f9e44')+'">'+esc(r.reason||'-')+'</td>'+
877
         '<td style="white-space:nowrap">'+esc(r.date||'')+'</td>'+
878
         '<td>'+esc(r.status||'')+'</td></tr>';
879
    });
880
    h+='</tbody></table>';
881
  }
882
  pModal(h);
883
}
36929 vikas 884
// Auto "missed" drill — the three execution-gap causes behind the card, plus
885
// the underlying deferral records (system-flagged "beat missed" / "no punch-in").
886
function openMissed(){
887
  var k=(window._period||{}).kpis||{};
888
  var rows=[
889
    ['Unscheduled', k.autoMissedUnscheduled||0, 'Executives (sales roles) with no PJP scheduled in this range'],
890
    ['Unfilled agenda', k.autoMissedUnfilledAgenda||0, 'Scheduled beat-days where the agenda was never filled'],
891
    ['Missed punch', k.autoMissedMissedPunchin||0, 'Scheduled beat-days the owner never punched in on']
892
  ];
893
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>Auto &ldquo;missed&rdquo;</h3>'+
894
        '<p class="desc">Why planned execution broke down &middot; '+esc(pRange())+'</p>'+
895
        '<table><thead><tr><th>Cause</th><th class="num">Count</th><th>What it means</th></tr></thead><tbody>';
896
  rows.forEach(function(r){
897
    h+='<tr><td style="font-weight:600;white-space:nowrap">'+esc(r[0])+'</td>'+
898
       '<td class="num cnt cred">'+num(r[1])+'</td>'+
899
       '<td style="color:#69748c">'+esc(r[2])+'</td></tr>';
900
  });
901
  h+='<tr class="boardtot"><td><b>Total</b></td><td class="num cnt"><b>'+num(k.autoMissed||0)+'</b></td><td></td></tr>';
902
  h+='</tbody></table>'+
903
     '<p class="desc" style="margin-top:12px">See also the <a href="javascript:void(0)" onclick="openDeferrals()" style="color:#3b5bdb">deferral records</a> for the partner-level &ldquo;beat missed&rdquo; / &ldquo;no punch-in&rdquo; entries.</p>';
904
  pModal(h);
905
}
36913 vikas 906
// Leads drill (period) — every lead created in the range.
907
function openPLeads(){
908
  var list=((window._period||{}).leadsList)||[];
909
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>Leads on route</h3>'+
910
        '<p class="desc">'+list.length+' lead'+(list.length===1?'':'s')+' created &middot; '+esc(pRange())+'</p>';
911
  if(!list.length){ h+='<p class="desc">No leads created in this range.</p>'; }
912
  else {
913
    h+='<table><thead><tr><th>Executive</th><th>Lead</th><th>City</th><th>Date</th><th>Status</th></tr></thead><tbody>';
914
    list.forEach(function(r){
915
      h+='<tr><td style="font-weight:600;white-space:nowrap">'+esc(r.exec)+'</td>'+
916
         '<td>'+esc(r.lead||'-')+'</td><td>'+esc(r.city||'')+'</td>'+
917
         '<td style="white-space:nowrap">'+esc(r.date||'')+'</td><td>'+esc(r.status||'')+'</td></tr>';
918
    });
919
    h+='</tbody></table>';
920
  }
921
  pModal(h);
922
}
36877 vikas 923
function names(list,n){ if(!list||!list.length) return 'None'; var s=list.slice(0,n).join(', '); if(list.length>n) s+=' +'+(list.length-n); return s; }
36882 vikas 924
var ATT_META = {
925
  notPunchedIn:    {bg:'#fff0f0', fg:'#e03131', ic:'&#9888;',   title:'Not punched in',                  sub:'executives with a beat, not started', cls:'badflag'},
36941 vikas 926
  idleStale:       {bg:'#fff4e6', fg:'#e8590c', ic:'&#9201;',   title:'Idle &mdash; off-store &gt; 59 min', sub:'stationary &gt;20 m from any store',  cls:'warnflag'},
36882 vikas 927
  geofence:        {bg:'#fff0f0', fg:'#e03131', ic:'&#128205;', title:'Check-in outside store geofence',  sub:'&gt;50 m from partner\'s saved location', cls:'badflag'},
928
  locationOff:     {bg:'#fff4e6', fg:'#e8590c', ic:'&#128244;', title:'Location turned off mid-journey',  sub:'GPS toggled off',                     cls:'warnflag'},
929
  deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'&#8635;',   title:'Deferred, awaiting re-action',     sub:'shops deferred today',                cls:''}
930
};
37248 vikas 931
// ---- Visit-quality flags: a today-scoped replica of the 14-row monitor table ----
932
// higher rate = worse, so redder as the flagged share climbs.
933
function flagRateColor(p){ if(p>=50)return '#e03131'; if(p>=25)return '#e8590c'; if(p>=10)return '#f08c00'; return '#2f9e44'; }
934
function flagStatusColor(s){
935
  switch(s){
936
    case 'action':      return '#e03131';
937
    case 'systemic':    return '#e8590c';
938
    case 'partial':     return '#f08c00';
939
    case 'needs floor': return '#1098ad';
940
    case 'too broad':   return '#7048e8';
941
    case 'contained':   return '#2f9e44';
942
    default:            return '#97a1b5';   // "never fires" / "no data"
943
  }
944
}
945
function flagStatusChip(s){
946
  var c=flagStatusColor(s);
947
  return '<span style="display:inline-block;font-size:10.5px;font-weight:700;padding:2px 8px;border-radius:999px;background:'+c+'1a;color:'+c+'">'+esc(s)+'</span>';
948
}
949
function flagsTable(list){
950
  if(!list||!list.length) return '<p class="desc">No flag data for this date.</p>';
951
  var h='<div style="overflow-x:auto"><table><thead><tr>'+
952
    '<th style="width:34px">#</th><th>Rule</th><th class="num">Flagged</th><th class="num">Of</th><th class="num">Rate</th><th>Status</th>'+
953
    '</tr></thead><tbody>';
954
  list.forEach(function(r){
955
    var hits=r.count||0, denom=r.denom||0, rate=pct(hits,denom);
956
    var open = hits>0 ? ' class="clickable" onclick="openFlag(\''+r.code+'\')"' : '';
957
    var cntColor = hits>0 ? flagRateColor(rate) : '#97a1b5';
958
    h+='<tr'+open+'>'+
959
       '<td style="font-weight:700;color:#97a1b5">'+esc(r.num)+'</td>'+
960
       '<td>'+esc(r.title)+'</td>'+
961
       '<td class="num cnt" style="font-weight:700;color:'+cntColor+'">'+num(hits)+'</td>'+
962
       '<td class="num cnt cmuted">'+num(denom)+'</td>'+
963
       '<td class="num cnt cmuted">'+(denom?rate+'%':'&mdash;')+'</td>'+
964
       '<td>'+flagStatusChip(r.status)+(hits>0?' <span class="chev">&#9656;</span>':'')+'</td>'+
965
       '</tr>';
966
  });
967
  return h+'</tbody></table></div>';
968
}
37277 vikas 969
// Minutes-of-day from the first HH:MM in a flag detail string (-1 if none) — for sorting.
970
function flagTimeVal(detail){
971
  var m=String(detail||'').match(/(\d{1,2}):(\d{2})/);
972
  return m ? (parseInt(m[1],10)*60+parseInt(m[2],10)) : -1;
973
}
37248 vikas 974
function openFlag(code){
975
  var list=window._visitFlags||[], r=null;
976
  for(var i=0;i<list.length;i++){ if(list[i].code===code){ r=list[i]; break; } }
977
  if(!r) return;
978
  var items=r.items||[], date=window._boardDate||'';
37277 vikas 979
  // Time-based rules (first check-in / last checkout) read best with the latest time on top.
980
  if(code==='r7a' || code==='r7b'){
981
    items=items.slice().sort(function(a,b){ return flagTimeVal(b.detail)-flagTimeVal(a.detail); });
982
  }
37248 vikas 983
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(r.num)+' &middot; '+esc(r.title)+'</h3>'+
984
        '<p class="desc">'+items.length+' flagged '+(items.length===1?'visit':'visits')+
985
        (r.denom?' of '+num(r.denom)+' &middot; '+pct(r.count,r.denom)+'%':'')+' &middot; '+flagStatusChip(r.status)+'</p>';
986
  if(!items.length){ h+='<p class="desc">Nothing flagged today.</p>'; }
987
  else{
988
    h+='<p class="desc">Click a row to open that executive\'s journey detail.</p><table><tbody>';
989
    items.forEach(function(x){
990
      var canOpen=x.userId>0;
991
      var go=canOpen?' class="clickable" onclick="openExec('+x.userId+",'"+js(x.name)+"','"+date+"')\"":'';
992
      h+='<tr'+go+'><td style="font-weight:600;white-space:nowrap">'+esc(x.name)+'</td>'+
993
         '<td style="color:#69748c">'+esc(x.detail||'')+'</td></tr>';
994
    });
995
    h+='</tbody></table>';
996
  }
997
  el('bj-modal').innerHTML=h; el('bj-modal-back').classList.add('show');
998
}
999
// Small red flag badge for the board's executive cell (count of rules tripped today).
37277 vikas 1000
// Clickable: opens a per-executive breakdown of which rules they tripped.
37248 vikas 1001
function flagBadge(r){
1002
  var f=r.flags||[]; if(!f.length) return '';
37277 vikas 1003
  return ' <span onclick="event.stopPropagation();openExecFlags('+r.userId+',\''+js(r.name)+'\')"'
1004
    +' title="'+f.length+' visit-quality flag'+(f.length>1?'s':'')+' — click for detail"'
1005
    +' style="cursor:pointer;display:inline-block;font-size:10px;font-weight:700;color:#e03131;background:#fff0f0;border:1px solid #ffc9c9;border-radius:999px;padding:0 6px;vertical-align:middle">&#9873; '+f.length+'</span>';
37248 vikas 1006
}
37277 vikas 1007
// Per-executive flag drill-down: which visit-quality rules this exec tripped today, with the detail.
1008
function openExecFlags(userId, name){
1009
  var row=(window._board||[]).find(function(x){ return x.userId===userId; });
1010
  var codes=(row&&row.flags)||[];
1011
  var byCode={}; (window._visitFlags||[]).forEach(function(f){ byCode[f.code]=f; });
1012
  var date=window._boardDate||'';
1013
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(name)+'</h3>'+
1014
        '<p class="desc">'+codes.length+' visit-quality flag'+(codes.length===1?'':'s')+' today</p>';
1015
  if(!codes.length){ h+='<p class="desc">No flags.</p>'; }
1016
  else{
1017
    h+='<table><tbody>';
1018
    codes.forEach(function(code){
1019
      var f=byCode[code]; if(!f) return;
1020
      var items=f.items||[];
1021
      var item=items.filter(function(it){ return it.userId===userId; })[0] || items.filter(function(it){ return it.name===name; })[0];
1022
      h+='<tr><td style="font-weight:600;white-space:nowrap">'+esc(f.num)+' &middot; '+esc(f.title)+'</td>'+
1023
         '<td style="color:#69748c">'+esc(item?item.detail:'')+'</td></tr>';
1024
    });
1025
    h+='</tbody></table>';
1026
  }
1027
  if(userId>0){
1028
    h+='<p class="desc" style="margin-top:10px"><a href="#" onclick="closeModal();openExec('+userId+',\''+js(name)+'\',\''+date+'\');return false;">Open journey detail &rarr;</a></p>';
1029
  }
1030
  el('bj-modal').innerHTML=h; el('bj-modal-back').classList.add('show');
1031
}
36877 vikas 1032
function attentionList(a){
36882 vikas 1033
  window._att = a;
1034
  var order=['notPunchedIn','idleStale','geofence','locationOff','deferredAwaiting'];
36941 vikas 1035
  var h='<div class="attrow">';
36882 vikas 1036
  order.forEach(function(k){
1037
    var m=ATT_META[k], list=a[k]||[], n=list.length;
1038
    var detail = n ? names(list.map(function(x){return x.name;}),3) : m.sub;
1039
    var cls = n ? ' att clickable' : ' att';
1040
    var click = n ? ' onclick="openAttention(\''+k+'\')"' : '';
1041
    h+='<div class="'+cls.trim()+'"'+click+'><div class="ic" style="background:'+m.bg+';color:'+m.fg+'">'+m.ic+'</div>'+
1042
       '<div class="ab"><div class="atitle">'+m.title+'</div><div class="adet">'+detail+'</div></div>'+
1043
       '<div class="acount '+(n?m.cls:'')+'">'+n+'</div>'+(n?'<span class="chev">&#9656;</span>':'')+'</div>';
1044
  });
36941 vikas 1045
  return h+'</div>';
36877 vikas 1046
}
36882 vikas 1047
function openAttention(key){
1048
  var a=window._att||{}, list=a[key]||[], m=ATT_META[key]||{title:'Detail'};
1049
  var mb=el('bj-modal-back'), md=el('bj-modal');
36897 vikas 1050
  // Off-site check-ins can be approved here, but only by L5/Final (canApprove).
1051
  var showApprove = (key==='geofence' && window._canApprove);
36882 vikas 1052
  var h='<button class="x" onclick="closeModal()">&times;</button><h3>'+m.title+'</h3>'+
36897 vikas 1053
        '<p class="desc">'+list.length+' '+(list.length===1?'item':'items')+
1054
        (showApprove?' &middot; approving updates the partner\'s saved coordinates':'')+'</p>';
36882 vikas 1055
  if(!list.length){ h+='<p class="desc">Nothing here.</p>'; }
1056
  else{
36941 vikas 1057
    var date=window._boardDate||'';
1058
    h+='<p class="desc">Click a row to open that executive\'s journey detail.</p><table><tbody>';
36882 vikas 1059
    list.forEach(function(x){
36897 vikas 1060
      var act='';
36941 vikas 1061
      // approve button gets its own cell that swallows the click so it doesn't drill down
1062
      if(showApprove) act='<td class="num" style="white-space:nowrap" onclick="event.stopPropagation()">'+(x.trackingId?'<button class="btn-ok" onclick="openValidate('+x.trackingId+')">Validate &amp; Approve</button>':'')+'</td>';
1063
      var canOpen = x.userId>0;
1064
      var go = canOpen ? ' class="clickable" onclick="openExec('+x.userId+",'"+js(x.name)+"','"+date+"')\"" : '';
1065
      h+='<tr'+go+'><td style="font-weight:600;white-space:nowrap">'+esc(x.name)+'</td>'+
36897 vikas 1066
         '<td style="color:#69748c">'+esc(x.detail||'')+'</td>'+act+'</tr>';
36882 vikas 1067
    });
1068
    h+='</tbody></table>';
1069
  }
1070
  md.innerHTML=h; mb.classList.add('show');
1071
}
36897 vikas 1072
// Step 2: review the check-in photo/document before approving.
1073
function openValidate(trackingId){
1074
  var list=(window._att&&window._att.geofence)||[], item=null;
1075
  for(var i=0;i<list.length;i++){ if(list[i].trackingId===trackingId){ item=list[i]; break; } }
1076
  if(!item){ return; }
37174 vikas 1077
  var img = bjValImg(item.attachment) || '<div class="valph">&#128247; No photo on this check-in</div>';
36897 vikas 1078
  el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">&times;</button><h3>Validate check-in</h3>'+
1079
    '<p class="desc">'+esc(item.name)+' &middot; '+esc(item.detail||'')+'</p>'+
37174 vikas 1080
    '<div class="valwrap">'+
1081
      '<figure class="valfig"><figcaption class="valcap">This check-in</figcaption>'+img+'</figure>'+
1082
      // Previous visit photo (same store) — hidden until the lazy fetch fills it in.
1083
      '<figure class="valfig" id="bj-prevfig" style="display:none"><figcaption class="valcap" id="bj-prevcap">Previous visit</figcaption><div id="bj-previmg"></div></figure>'+
1084
    '</div>'+
36897 vikas 1085
    '<div class="valactions">'+
1086
      '<button class="exectab" onclick="openAttention(\'geofence\')">&larr; Back</button>'+
37174 vikas 1087
      '<button class="btn-no" onclick="rejectValidated('+trackingId+',this)">Reject</button>'+
36897 vikas 1088
      '<button class="btn-ok" onclick="approveValidated('+trackingId+',this)">Approve</button>'+
1089
    '</div>';
37174 vikas 1090
  bjLoadPrevCheckin(trackingId);
36897 vikas 1091
}
37174 vikas 1092
// Build the <img> markup for a check-in attachment (empty string when there's none).
1093
function bjValImg(att){
1094
  return (att && window._mediaUrl)
1095
    ? '<img class="valimg" src="'+esc(window._mediaUrl+att)+'" onclick="bjOpenImage(this.src)" onerror="this.outerHTML=\'<div class=&quot;valph&quot;>&#128247; Photo unavailable</div>\'">'
1096
    : '';
1097
}
1098
// Lazily fetch the most recent OTHER check-in photo at the same store and, if one
1099
// exists, reveal it beside the current photo so the reviewer can compare shopfronts.
1100
function bjLoadPrevCheckin(trackingId){
1101
  fetch(CTX+'/beat-journey/prev-checkin?trackingId='+trackingId,{headers:{'Accept':'application/json'}})
1102
    .then(function(r){ return r.ok ? r.json() : null; })
1103
    .then(function(d){
1104
      if(!d || !d.attachment) return;
1105
      var fig=el('bj-prevfig'); if(!fig) return;
1106
      var im=bjValImg(d.attachment); if(!im) return;
1107
      el('bj-previmg').innerHTML=im;
1108
      var when=[d.date,d.time].filter(Boolean).join(' ');
1109
      el('bj-prevcap').innerHTML='Previous visit'+(when?' &middot; '+esc(when):'');
1110
      fig.style.display='';
1111
    })
1112
    .catch(function(){});
1113
}
36897 vikas 1114
function approveValidated(trackingId, btn){
1115
  btn.disabled=true; btn.textContent='Approving…';
1116
  fetch(CTX+'/beat-journey/approve-checkin',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'trackingId='+trackingId})
1117
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
1118
    .then(function(){
1119
      if(window._att && window._att.geofence){ window._att.geofence = window._att.geofence.filter(function(x){ return x.trackingId!==trackingId; }); }
1120
      if(window._att && window._att.geofence && window._att.geofence.length) openAttention('geofence');
1121
      else closeModal();
1122
    })
1123
    .catch(function(){ btn.disabled=false; btn.textContent='Approve'; alert('Approve failed (not authorised, or server error).'); });
1124
}
37174 vikas 1125
// Reject the off-site check-in: coords are left untouched (unlike approve), and the
1126
// executive, their reporting manager and ops are emailed. Optional reason is prompted.
1127
function rejectValidated(trackingId, btn){
1128
  var reason = prompt('Reject this off-site check-in?\nThe executive and their manager will be emailed.\nOptional reason:', '');
1129
  if(reason===null) return;   // cancelled
1130
  btn.disabled=true; btn.textContent='Rejecting…';
1131
  var body='trackingId='+trackingId+'&reason='+encodeURIComponent(reason||'');
1132
  fetch(CTX+'/beat-journey/reject-checkin',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body})
1133
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
1134
    .then(function(){
1135
      if(window._att && window._att.geofence){ window._att.geofence = window._att.geofence.filter(function(x){ return x.trackingId!==trackingId; }); }
1136
      if(window._att && window._att.geofence && window._att.geofence.length) openAttention('geofence');
1137
      else closeModal();
1138
    })
1139
    .catch(function(){ btn.disabled=false; btn.textContent='Reject'; alert('Reject failed (not authorised, or server error).'); });
1140
}
36877 vikas 1141
 
37076 vikas 1142
// Mark an executive on leave for the board date (unscheduled, or scheduled but not
1143
// started / agenda unfilled). Manager / super-user only (server-enforced).
1144
// Optimistically flips the row to "On leave" and re-renders the open drill-down.
37060 vikas 1145
function markLeave(authUserId, name, btn){
1146
  if(!authUserId) return;
1147
  var reason = prompt('Mark '+name+' on leave for '+(window._boardDate||'today')+'?\nOptional reason:', '');
1148
  if(reason===null) return;   // cancelled
1149
  btn.disabled=true; btn.textContent='Marking…';
1150
  var body='authUserId='+authUserId+'&date='+encodeURIComponent(window._boardDate||'')+'&reason='+encodeURIComponent(reason||'');
1151
  fetch(CTX+'/beat-journey/mark-leave',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body})
1152
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
1153
    .then(function(){
1154
      (window._execList||[]).forEach(function(x){ if(x.authUserId===authUserId){ x.onLeave=true; x.statusLabel='On leave'; } });
37076 vikas 1155
      openCards(window._openCardKey||'unscheduled');   // re-render the open drill-down (row now shows "On leave", no button)
37060 vikas 1156
      loadToday();                // refresh KPI tiles / counts in the background
1157
    })
1158
    .catch(function(){ btn.disabled=false; btn.textContent='Mark leave'; alert('Could not mark leave (not authorised, or server error).'); });
1159
}
1160
 
36877 vikas 1161
/* ============================ PERIOD ============================ */
1162
function loadPeriod(){
1163
  spinner();
1164
  var u = BASE + '/data?startDate=' + encodeURIComponent(el('bj-start').value) + '&endDate=' + encodeURIComponent(el('bj-end').value) + qs();
1165
  fetch(u, {headers:{'Accept':'application/json'}})
1166
    .then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
1167
    .then(renderPeriod).catch(fail);
1168
}
1169
function tile(t,v){ return '<div class="tile"><div class="t">'+t+'</div><div class="v">'+v+'</div></div>'; }
1170
function renderPeriod(d){
36913 vikas 1171
  window._period = d;          // drill-downs read scorecard / lists from here
36877 vikas 1172
  var k=d.kpis, f=d.funnel, df=d.deferral, u=d.utilization, t=d.travel, o=d.outcomes;
1173
  var neverTouched = Math.max((f.plannedStops||0)-(f.checkedIn||0)-(df.total||0),0);
1174
  var h='';
1175
  h+='<div class="kpis">';
36929 vikas 1176
  h+=kpi('&#9878;','#fff4e6','#f08c00','Adherence',k.adherencePct+'<small>%</small>',num(k.doneStops)+' of '+num(k.plannedStops)+' planned visits done',adhColor(k.adherencePct),"openPScore('adherencePct','Adherence','Planned visits done vs planned, per executive')");
36913 vikas 1177
  h+=kpi('&#128506;','#e7f5ff','#1098ad','PJP coverage L1',k.coverageL1Pct+'<small>%</small>',k.coverageL1With+'/'+k.coverageL1Total+' have a plan','#1098ad',"openCoverage('L1')");
36929 vikas 1178
  h+=kpiCols('&#128694;','#ebfbee','#2f9e44','Average Visit (per day)',
36941 vikas 1179
    [['Franchisee',k.avgPartnersPerActiveDay],['Lead',k.avgLeadsPerActiveDay]],
36929 vikas 1180
    null,"openPScore('done','Visits / active day','Visits completed per executive')");
1181
  h+=kpiCols('&#128172;','#eef1fb','#3b5bdb','Average discussion (per executive)',
1182
    [['Partner',k.avgDiscussionPartnerMin],['Lead',k.avgDiscussionLeadMin]],
1183
    k.avgDiscussionMin>0&&k.avgDiscussionMin<30?'#e8590c':null,"openPScore('discussionMin','Average discussion minutes','Avg in-store discussion per executive')",'min');
1184
  h+=kpiCols('&#8635;','#fff0f0','#e03131','Auto "missed"',
1185
    [['Unscheduled',k.autoMissedUnscheduled],['Unfilled agenda',k.autoMissedUnfilledAgenda],['Missed punch',k.autoMissedMissedPunchin]],
1186
    '#e03131',"openMissed()");
36913 vikas 1187
  h+=kpi('&#9733;','#fff0f6','#d6336c','Leads on route',num(k.leadsCreated),'this period',null,"openPLeads()");
36877 vikas 1188
  h+='</div>';
1189
 
36941 vikas 1190
  h+='<div class="sectionlabel">Journey Summary</div><div class="card"><p class="desc">Planned (PJP) and self-assigned visits combined.</p><div class="funnel">'+summaryCards(d.summary)+'</div></div>';
36877 vikas 1191
 
1192
  h+='<div class="sectionlabel">Adherence &amp; Coverage</div><div class="grid g3">';
1193
  h+='<div class="card"><h2>Per-executive scorecard <span style="color:#97a1b5;font-weight:500;font-size:12px">&middot; '+d.scorecard.length+' executives</span></h2><p class="desc">The coaching list &mdash; click a header to sort. Reds are where to intervene.</p>'+scorecardTable(d.scorecard)+'</div>';
1194
  h+='<div class="card"><h2>Coverage by sales level</h2><p class="desc">Filled = executives with a plan; light = total at that level.</p>'+coverageBars(d.coverageByLevel)+utilBlock(u)+'</div>';
1195
  h+='</div>';
1196
 
1197
  h+='<div class="sectionlabel">Deferrals, Travel &amp; Outcomes</div><div class="grid g2">';
1198
  h+='<div class="card"><h2>Deferral nature</h2><p class="desc">Deliberate, or just auto-flagged missed beats?</p>'+deferDonut(df)+'</div>';
1199
  h+='<div class="card"><h2>Travel &amp; route quality</h2><p class="desc">Straight-line distance per leg between consecutive stops.</p>'+travelBlock(t)+'</div>';
1200
  h+='</div>';
1201
 
1202
  h+='<div class="sectionlabel">Outcomes &amp; Visit Quality</div><div class="card"><p class="desc">Captured on the check-out form &mdash; the substance of each store conversation.</p>'+
1203
     '<div class="tiles" style="grid-template-columns:repeat(4,1fr)">'+
1204
     tile('Partner remark filled', o.remarkFilledPct+'<small>%</small>')+
1205
     tile('Avg partner rating', (o.avgRating||0)+'<small>/5</small>')+
1206
     tile('Audit checklist done', o.auditDonePct+'<small>%</small>')+
1207
     tile('Next-visit follow-up set', o.nextVisitPct+'<small>%</small>')+
1208
     '</div></div>';
1209
 
1210
  h+='<div class="sectionlabel">What to fix</div>'+findingsBlock(d.findings);
1211
  h+=sourcesCard();
1212
  el('bj-content').innerHTML=h;
1213
  window._scorecard = d.scorecard;
1214
}
1215
var sortKey=null, sortAsc=false;
1216
function sortCard(key){
1217
  if(sortKey===key) sortAsc=!sortAsc; else { sortKey=key; sortAsc=false; }
1218
  window._scorecard.sort(function(a,b){ var x=a[key],y=b[key];
1219
    if(typeof x==='string') return sortAsc? (''+x).localeCompare(y):(''+y).localeCompare(x);
1220
    return sortAsc? x-y : y-x; });
1221
  document.getElementById('scTbl').outerHTML = scorecardTable(window._scorecard);
1222
}
1223
function scorecardTable(rows){
1224
  if(!rows||!rows.length) return '<p class="desc">No beat owners in scope for this period.</p>';
1225
  var h='<table id="scTbl"><thead><tr>'+
1226
    '<th onclick="sortCard(\'name\')">Executive</th>'+
1227
    '<th class="num" onclick="sortCard(\'planned\')">Plan</th>'+
1228
    '<th class="num" onclick="sortCard(\'done\')">Done</th>'+
1229
    '<th class="num" onclick="sortCard(\'adherencePct\')">Adher.</th>'+
37028 vikas 1230
    '<th class="num" onclick="sortCard(\'avgLeadVisitsPerDay\')" title="Avg lead visits per active (punched-in) day">Lead/day</th>'+
1231
    '<th class="num" onclick="sortCard(\'avgFranchiseeVisitsPerDay\')" title="Avg franchisee visits per active (punched-in) day">Fr/day</th>'+
36877 vikas 1232
    '<th class="num" onclick="sortCard(\'discussionMin\')">Disc.</th>'+
1233
    '<th class="num" onclick="sortCard(\'workingHrs\')">Hrs</th>'+
37028 vikas 1234
    '<th class="num" onclick="sortCard(\'leads\')">Leads</th></tr></thead><tbody>';
36877 vikas 1235
  rows.forEach(function(r){
1236
    var c=adhColor(r.adherencePct);
37028 vikas 1237
    h+='<tr class="clickable" onclick="openExecPeriod('+r.authUserId+',\''+js(r.name)+'\')"><td><span class="lvl">'+esc(r.level)+'</span>'+esc(r.name)+'</td>'+
36877 vikas 1238
       '<td class="num">'+r.planned+'</td><td class="num">'+r.done+'</td>'+
1239
       '<td class="num"><b style="color:'+c+'">'+r.adherencePct+'%</b><span class="meter"><i style="width:'+Math.min(100,r.adherencePct)+'%;background:'+c+'"></i></span></td>'+
37028 vikas 1240
       '<td class="num">'+(r.avgLeadVisitsPerDay||'-')+'</td>'+
1241
       '<td class="num">'+(r.avgFranchiseeVisitsPerDay||'-')+'</td>'+
36882 vikas 1242
       '<td class="num'+(r.discussionMin>0&&r.discussionMin<10?' warnflag':'')+'">'+(r.discussionMin||'-')+(r.discussionMin?'m':'')+'</td>'+
37028 vikas 1243
       '<td class="num">'+r.workingHrs+'</td><td class="num">'+r.leads+'</td></tr>';
36877 vikas 1244
  });
1245
  return h+'</tbody></table>';
1246
}
1247
function coverageBars(rows){
1248
  if(!rows||!rows.length) return '<p class="desc">No coverage data.</p>';
1249
  var max=0; rows.forEach(function(r){ if(r.totalUsers>max)max=r.totalUsers; }); if(!max)max=1;
1250
  var h=''; rows.forEach(function(r){
1251
    var w=pct(r.withPjp,max), wt=pct(r.totalUsers,max);
36913 vikas 1252
    h+='<div class="barrow clickable" onclick="openCoverage(\''+js(r.level)+'\')" title="Click to see these executives"><div class="name">'+esc(r.level)+'</div><div class="track">'+
36877 vikas 1253
       '<div style="position:absolute;inset:0;width:'+wt+'%;background:#e3e8f5;border-radius:7px"></div>'+
1254
       '<div class="fill" style="width:'+w+'%;position:relative;background:linear-gradient(90deg,#1098ad,#22b8cf)">'+(r.withPjp||'')+'</div></div>'+
1255
       '<div class="meta">'+r.withPjp+'/'+r.totalUsers+' &middot; '+r.coveragePct+'%</div></div>';
1256
  });
1257
  return h;
1258
}
1259
function utilBlock(u){
1260
  return '<h2 style="margin-top:18px">Working-hour utilization</h2><p class="desc">Where the average working day goes.</p>'+
1261
    '<div class="util">'+
1262
    '<div style="width:'+u.inStorePct+'%;background:#2f9e44">In-store '+u.inStorePct+'%</div>'+
1263
    '<div style="width:'+u.travelPct+'%;background:#3b5bdb">Travel '+u.travelPct+'%</div>'+
1264
    '<div style="width:'+u.idlePct+'%;background:#e03131">Idle '+u.idlePct+'%</div></div>'+
1265
    '<div class="utlegend"><span><span class="dot" style="background:#2f9e44;margin:0"></span>Selling '+u.inStoreHrs+' h</span>'+
1266
    '<span><span class="dot" style="background:#3b5bdb;margin:0"></span>Travel '+u.travelHrs+' h</span>'+
1267
    '<span><span class="dot" style="background:#e03131;margin:0"></span>Idle '+u.idleHrs+' h</span></div>';
1268
}
1269
function deferDonut(df){
1270
  var sysPct=df.systemPct||0;
1271
  var g='conic-gradient(#f59f00 0 '+sysPct+'%, #2f9e44 '+sysPct+'% 100%)';
36913 vikas 1272
  return '<div class="donutwrap"><div class="donut clickable" onclick="openDeferrals()" title="Click to see all deferrals" style="background:'+g+'"><div class="center">'+
36877 vikas 1273
    '<div style="font-size:24px;font-weight:780">'+num(df.total)+'</div><div style="font-size:11px;color:#97a1b5">deferrals</div></div></div>'+
1274
    '<div class="legend">'+
36913 vikas 1275
    '<div class="it clickable" onclick="openDeferrals(\'system\')"><span class="dot" style="background:#f59f00"></span><div><b>'+df.systemGenerated+'</b> system auto-flagged ('+sysPct+'%)<br><span style="color:#97a1b5;font-size:11.5px">"beat missed", "no punch-in"</span></div></div>'+
1276
    '<div class="it clickable" onclick="openDeferrals(\'genuine\')"><span class="dot" style="background:#2f9e44"></span><div><b>'+df.human+'</b> genuine reason ('+df.humanPct+'%)</div></div>'+
36877 vikas 1277
    '<div class="it"><span class="dot" style="background:#3b5bdb"></span><div><b>'+df.actioned+'</b> acted on by a head'+(df.avgDaysToAction?'<br><span style="color:#97a1b5;font-size:11.5px">avg '+df.avgDaysToAction+' days to action</span>':'')+'</div></div>'+
36913 vikas 1278
    '</div></div><p class="desc" style="margin-top:13px">Click the chart for all deferrals, or a legend row for system / genuine. "System" vs "genuine" is read from the deferral reason.</p>';
36877 vikas 1279
}
1280
function travelBlock(t){
1281
  var rows=t.legBuckets||[], max=0; rows.forEach(function(r){ if(r.count>max)max=r.count; }); if(!max)max=1;
1282
  var colors={'0-2 km':'#2f9e44','2-5 km':'#2f9e44','5-15 km':'#82c91e','15-50 km':'#f59f00','50 km+':'#e03131'};
1283
  var h=''; rows.forEach(function(r){
1284
    h+='<div class="barrow"><div class="name">'+esc(r.bucket)+'</div><div class="track"><div class="fill" style="width:'+pct(r.count,max)+'%;background:'+(colors[r.bucket]||'#3b5bdb')+'">'+r.count+'</div></div><div class="meta"></div></div>';
1285
  });
1286
  h+='<div class="tiles" style="margin-top:14px">'+
1287
     tile('Avg leg', t.avgLegKm+'<small> km</small>')+
1288
     tile('Legs &gt; 15 km', t.pctOver15+'<small>%</small>')+
1289
     tile('Actual / estimate', (t.actualVsEstimateRatio||0)+'<small>&times;</small>')+'</div>';
1290
  return h;
1291
}
1292
function findingsBlock(items){
1293
  if(!items||!items.length) return '<div class="callout"><h2>Nothing flagged</h2><p class="desc">No issues crossed the alert thresholds for this period.</p></div>';
1294
  var sev={high:'#e03131',med:'#f59f00',low:'#2f9e44'};
1295
  var h='<div class="callout"><h2>Recommended management actions</h2><p class="desc">Generated from this period\'s data.</p>';
1296
  items.forEach(function(x){
1297
    h+='<div class="fix"><div class="sev" style="background:'+(sev[x.severity]||'#3b5bdb')+'"></div><div class="fb">'+
1298
       '<div class="ft">'+esc(x.title)+'</div><div class="fd">'+esc(x.detail)+'</div>'+
1299
       '<div class="act">&rarr; '+esc(x.action)+'</div></div></div>';
1300
  });
1301
  return h+'</div>';
1302
}
1303
function sourcesCard(){
1304
  return '<div class="sectionlabel">Where the numbers come from</div><div class="card">'+
1305
    '<p class="desc">Nothing new needs to be captured &mdash; everything here is built from what the field app already records on each journey.</p>'+
1306
    '<ul style="margin:0;padding-left:18px;line-height:1.95;font-size:13px;color:#3a4459">'+
1307
    '<li>The <b>journey timeline</b> (punch-in, check-ins/outs, deferrals, punch-out) comes from what the executive taps in the app.</li>'+
1308
    '<li><b>Adherence</b> compares the partners planned on each beat against the stores actually visited.</li>'+
1309
    '<li><b>Time, travel and distance</b> use the in-store and travel durations logged per visit.</li>'+
1310
    '<li><b>Geofence / fake-visit checks</b> compare the check-in GPS against the partner\'s saved shop location (flagged beyond ~50 m).</li>'+
1311
    '<li><b>Visit outcomes</b> (remarks, rating, audit, next visit) come from the check-out form.</li>'+
1312
    '<li><b>Coverage and the manager view</b> follow the existing L1&ndash;L5 sales hierarchy, so each manager sees only their team.</li>'+
1313
    '</ul></div>';
1314
}
1315
 
1316
/* ============================ EXEC DRILLDOWN ============================ */
36897 vikas 1317
function openExec(userId, name, date, tab){
36877 vikas 1318
  if(!userId){ return; }
36897 vikas 1319
  window._exec = {userId:userId, name:name, date:date, detail:null, tab:(tab||'timeline')};
36877 vikas 1320
  var mb=el('bj-modal-back'), m=el('bj-modal');
1321
  m.innerHTML='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(name)+'</h3><p class="desc">Journey detail &middot; '+esc(date)+'</p><div class="loading"><div class="spinner"></div>Loading&hellip;</div>';
1322
  mb.classList.add('show');
36882 vikas 1323
  fetch(CTX+'/beat-journey/journey-detail?userId='+userId+'&date='+encodeURIComponent(date),{headers:{'Accept':'application/json'}})
36877 vikas 1324
    .then(function(r){ return r.json(); }).then(function(d){
36882 vikas 1325
      window._mediaUrl = d.mediaUrl || '';
36897 vikas 1326
      window._exec.detail = d;
1327
      renderExec(window._exec.tab);
1328
    }).catch(function(){ el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">&times;</button><p class="desc">Could not load detail.</p>'; });
1329
}
1330
function renderExec(tab){
1331
  var ex=window._exec; if(!ex) return;
1332
  var d=ex.detail||{};
1333
  var head='<button class="x" onclick="closeModal()">&times;</button><h3>'+esc(ex.name)+'</h3>'+
1334
    '<p class="desc">Journey detail &middot; '+esc(ex.date)+' &middot; battery '+(d.batteryLevel==null?'-':d.batteryLevel+'%')+' &middot; loc-off '+(d.locationToggleCount||0)+'</p>'+
1335
    '<div class="exectabs">'+
1336
      '<button class="exectab'+(tab==='timeline'?' active':'')+'" onclick="renderExec(\'timeline\')">&#128221; Timeline</button>'+
1337
      '<button class="exectab'+(tab==='map'?' active':'')+'" onclick="renderExec(\'map\')">&#128205; Live Location</button>'+
1338
    '</div>';
1339
  if(tab==='map'){
36903 vikas 1340
    el('bj-modal').innerHTML=head+'<div id="bjRouteLegend" class="bjlegend"></div>'+
1341
      '<div id="bjMap"><div class="loading"><div class="spinner"></div>Loading map&hellip;</div></div>';
36897 vikas 1342
    bjShowMap(ex.userId, ex.date);
1343
  } else {
1344
    el('bj-modal').innerHTML=head+buildTimelineHtml(d, ex.date);
1345
  }
1346
}
1347
function buildTimelineHtml(d, date){
1348
  var all=(d.visits||[]).slice();
1349
  // Sequence like the field report: Punch in -> visited stops (by check-in time)
1350
  // -> PENDING/not-visited -> Punch out. The attendance row carries both punch-in
1351
  // & punch-out times; split it into the two bookends.
1352
  var att=null, visited=[], pending=[];
1353
  all.forEach(function(x){
1354
    var tt=(x.taskType||'').toLowerCase(), mt=(x.markType||'').toUpperCase();
1355
    if(tt==='attendance' || mt.indexOf('PUNCH')>=0){ if(!att) att=x; }
1356
    else if(x.checkInTime){ visited.push(x); }
1357
    else { pending.push(x); }
1358
  });
1359
  visited.sort(function(a,b){ return tlSecs(a.checkInTime)-tlSecs(b.checkInTime); });
36882 vikas 1360
 
36903 vikas 1361
  var distKm=0, inStoreSec=0, leadsN=0;
36897 vikas 1362
  all.forEach(function(x){
1363
    var tt=(x.taskType||'').toLowerCase();
1364
    if(tt==='attendance') return;
1365
    if(x.totalDistance) distKm += (parseFloat(x.totalDistance)||0);
1366
    inStoreSec += toSec(x.timeSpent);
1367
    if(tt==='lead') leadsN++;
1368
  });
1369
  var workSec = (att && att.checkInTime && att.checkOutTime) ? (toSec(att.checkOutTime)-toSec(att.checkInTime)) : (att?toSec(att.timeSpent):0);
1370
  if(workSec<0) workSec=0;
36903 vikas 1371
  // Travel = working − in-store, so In-store + Travel = Working hours exactly
1372
  // (summing per-leg transit under-counts the idle/waiting between stops).
1373
  var transitSec = Math.max(0, workSec - inStoreSec);
36882 vikas 1374
 
36897 vikas 1375
  var seq=[];
1376
  if(att && att.checkInTime) seq.push(punchEntry('in', att.checkInTime));
1377
  visited.forEach(function(x){ seq.push(x); });
1378
  pending.forEach(function(x){ seq.push(x); });
1379
  if(att && att.checkOutTime) seq.push(punchEntry('out', att.checkOutTime));
36882 vikas 1380
 
36897 vikas 1381
  var h='<div class="jstats">'+
1382
    jstat(fmtSec(workSec),'Working hours')+
1383
    jstat(fmtSec(inStoreSec),'In-store time')+
1384
    jstat(fmtSec(transitSec),'Travel time')+
1385
    jstat(distKm.toFixed(2)+' km','Distance Covered')+
36905 vikas 1386
    jstat(visited.length+(leadsN?' / '+leadsN+'L':''),'Visits / Leads')+
36897 vikas 1387
    '</div>';
1388
  if(!seq.length){ return h+'<p class="desc">No tracking events for this day.</p>'; }
1389
  h+='<div class="tl">';
1390
  var ri=0;
1391
  seq.forEach(function(x){
1392
    if(!x._punch && x.checkInTime){
1393
      var tr=(x.transitTime && x.transitTime!=='00:00:00') ? x.transitTime : null;
1394
      var km=(x.totalDistance && parseFloat(x.totalDistance)>0) ? (parseFloat(x.totalDistance).toFixed(2)+' km') : null;
36955 vikas 1395
      var eta=(x.estimatedTime && x.estimatedTime!=='00:00:00') ? x.estimatedTime : null;
1396
      if(tr || km || eta){
1397
        var parts=[]; if(tr) parts.push('&#128663; '+esc(tr)); if(km) parts.push(esc(km)); if(eta) parts.push('ETA '+esc(eta));
1398
        // flag the leg red when actual transit exceeds 1.5x the ETA (same rule as the export)
1399
        var overrun = eta && toSec(tr) > 1.5*toSec(eta);
1400
        h+='<div class="tlgap"><span class="gappill'+(overrun?' overrun':'')+'"'+(overrun?' title="Travel overran 1.5x the ETA"':'')+'>'+parts.join(' &middot; ')+'</span></div>';
36877 vikas 1401
      }
36897 vikas 1402
    }
1403
    h+=tlRow(x, ri%2===0?'left':'right'); ri++;
1404
  });
1405
  h+='</div>';
1406
  return h;
36877 vikas 1407
}
36897 vikas 1408
 
1409
/* ---- live location map (reuses /beat-report/live-tracking pathPoints) ---- */
1410
function bjLoadMaps(cb){
1411
  if(window.google && window.google.maps){ cb(); return; }
1412
  window._mapsCbs = window._mapsCbs || [];
1413
  window._mapsCbs.push(cb);
1414
  if(window._mapsLoading) return;
1415
  window._mapsLoading=true;
1416
  window.__bjMapsReady=function(){ (window._mapsCbs||[]).forEach(function(f){ try{f();}catch(e){} }); window._mapsCbs=[]; };
1417
  var s=document.createElement('script');
1418
  s.src='https://maps.googleapis.com/maps/api/js?key=AIzaSyAckO0y4Z6WhBOuMjNjioWLSYZDhGEvGBc&libraries=geometry&v=weekly&callback=__bjMapsReady';
1419
  s.async=true; document.head.appendChild(s);
1420
}
1421
function bjShowMap(userId, date){
1422
  bjLoadMaps(function(){
1423
    fetch(CTX+'/beat-journey/track-map?userId='+userId+'&date='+encodeURIComponent(date),{headers:{'Accept':'application/json'}})
1424
      .then(function(r){ return r.json(); }).then(function(data){ bjDrawMap(data); })
1425
      .catch(function(){ var e=document.getElementById('bjMap'); if(e) e.innerHTML='<div class="loading">Could not load location.</div>'; });
1426
  });
1427
}
1428
function bjMarkColor(mt){
1429
  mt=(mt||'').toUpperCase();
1430
  if(mt.indexOf('CHECKOUT')>=0) return '#2f9e44';
1431
  if(mt==='CHECKIN') return '#f59f00';
1432
  if(mt==='DEFERRED') return '#e8590c';
1433
  if(mt==='PENDING') return '#97a1b5';
1434
  return '#3b5bdb';
1435
}
1436
function bjMarkInfo(v,color){
1437
  return '<div style="font-size:12px;max-width:240px"><b>'+esc(v.name||'')+'</b><br>'+
1438
    '<span style="background:'+color+';color:#fff;border-radius:4px;padding:1px 6px;font-size:10px">'+esc(v.markType||'')+'</span>'+
1439
    (v.checkInTime?'<br>In: <b>'+esc(v.checkInTime)+'</b>':'')+
1440
    (v.checkOutTime?'<br>Out: <b>'+esc(v.checkOutTime)+'</b>':'')+
1441
    (v.timeSpent?'<br>Time spent: <b>'+esc(v.timeSpent)+'</b>':'')+
1442
    (v.address?'<br>'+esc(v.address):'')+'</div>';
1443
}
1444
// Teardrop pin (same shape as Beat Report) with a white disc + label.
1445
function bjPin(color, label){
1446
  var svg='<svg xmlns="http://www.w3.org/2000/svg" width="28" height="40" viewBox="0 0 28 40">'+
1447
    '<path d="M14 0C6.268 0 0 6.268 0 14c0 10.5 14 26 14 26s14-15.5 14-26C28 6.268 21.732 0 14 0z" fill="'+color+'"/>'+
1448
    '<circle cx="14" cy="14" r="7" fill="#ffffff"/>'+
1449
    '<text x="14" y="18" text-anchor="middle" font-size="10" font-weight="bold" fill="'+color+'">'+label+'</text></svg>';
1450
  return {url:'data:image/svg+xml;charset=UTF-8,'+encodeURIComponent(svg), scaledSize:new google.maps.Size(28,40), anchor:new google.maps.Point(14,40)};
1451
}
1452
// Larger pin sized to fit a duration label (idle stops).
1453
function bjStopPin(label){
1454
  var s=String(label), d=Math.max(40, 14+s.length*7), H=d+10, r=d/2;
1455
  var svg='<svg xmlns="http://www.w3.org/2000/svg" width="'+d+'" height="'+H+'" viewBox="0 0 '+d+' '+H+'">'+
1456
    '<polygon points="'+(r-7)+','+(d-7)+' '+(r+7)+','+(d-7)+' '+r+','+H+'" fill="#e8590c"/>'+
1457
    '<circle cx="'+r+'" cy="'+r+'" r="'+(r-2)+'" fill="#e8590c" stroke="#ffffff" stroke-width="2"/>'+
1458
    '<text x="'+r+'" y="'+(r+4)+'" text-anchor="middle" font-size="12" font-weight="bold" fill="#ffffff">'+s+'</text></svg>';
1459
  return {url:'data:image/svg+xml;charset=UTF-8,'+encodeURIComponent(svg), scaledSize:new google.maps.Size(d,H), anchor:new google.maps.Point(r,H)};
1460
}
37248 vikas 1461
// Numbered visit pin — a rounded badge (with pointer) that widens to fit a multi-
1462
// char label like "4→1" (planned→actual). Mirrors bjStopPin's scaling approach.
1463
function bjSeqPin(color, label){
1464
  var s=String(label), w=Math.max(28, 12 + s.length*8), H=40, bh=24, rx=8;
1465
  var svg='<svg xmlns="http://www.w3.org/2000/svg" width="'+w+'" height="'+H+'" viewBox="0 0 '+w+' '+H+'">'+
1466
    '<rect x="0.5" y="0.5" width="'+(w-1)+'" height="'+bh+'" rx="'+rx+'" fill="'+color+'" stroke="#ffffff"/>'+
1467
    '<polygon points="'+(w/2-6)+','+bh+' '+(w/2+6)+','+bh+' '+(w/2)+','+H+'" fill="'+color+'"/>'+
1468
    '<text x="'+(w/2)+'" y="'+(bh/2+5)+'" text-anchor="middle" font-size="13" font-weight="bold" fill="#ffffff">'+s+'</text></svg>';
1469
  return {url:'data:image/svg+xml;charset=UTF-8,'+encodeURIComponent(svg),
1470
          scaledSize:new google.maps.Size(w,H), anchor:new google.maps.Point(w/2,H)};
1471
}
36897 vikas 1472
function fmtDur2(sec){ sec=sec||0; var m=Math.floor(sec/60), s=sec%60; return m+'m'+(s>0?' '+s+'s':''); }
1473
function bjDrawMap(data){
1474
  var box=document.getElementById('bjMap'); if(!box) return;
1475
  box.innerHTML='';
1476
  var map=new google.maps.Map(box,{zoom:12,center:{lat:20.5937,lng:78.9629},mapTypeControl:true,streetViewControl:false});
1477
  var bounds=new google.maps.LatLngBounds(), has=false;
1478
  function pin(pos,icon,zi,title,info){
1479
    has=true; bounds.extend(pos);
1480
    var m=new google.maps.Marker({position:pos,map:map,zIndex:zi||100,icon:icon,title:title||''});
1481
    if(info){ var iw=new google.maps.InfoWindow({content:info}); m.addListener('click',function(){ iw.open(map,m); }); }
1482
    return m;
1483
  }
1484
 
36903 vikas 1485
  // 1) last-known location marker (the GPS trail itself is drawn as the "taken"
1486
  // route in bjDrawRoutes, straight from location_tracking_path).
36897 vikas 1487
  var pts=(data.path||[]).map(function(p){ return {lat:p.lat,lng:p.lng}; });
1488
  if(pts.length){
1489
    pts.forEach(function(p){ bounds.extend(p); has=true; });
1490
    var lastPt=pts[pts.length-1];
1491
    var lk=new google.maps.Marker({position:lastPt,map:map,zIndex:999,title:'Last Known Location',
1492
      icon:{url:CTX+'/resources/images/salesman.png',scaledSize:new google.maps.Size(40,40),anchor:new google.maps.Point(20,20)}});
1493
    var lkIw=new google.maps.InfoWindow({content:'<div style="font-size:12px"><b>&#128205; Last known location</b></div>'});
1494
    lk.addListener('click',function(){ lkIw.open(map,lk); });
1495
  }
1496
 
1497
  // 2) idle stops (>=10 min within 50 m)
1498
  (data.idleStops||[]).forEach(function(s){
1499
    var lbl=fmtDur2(s.seconds);
1500
    pin({lat:s.lat,lng:s.lng}, bjStopPin(lbl), 80, 'Idle '+lbl,
1501
      '<div style="font-size:12px"><b>&#9201; Idle stop</b><br>Stayed ~'+esc(lbl)+' within 50 m</div>');
1502
  });
1503
 
37248 vikas 1504
  // 3) every visit — each pin shows planned→actual. planned # = position in the
1505
  // assigned order the backend returns (id/insert order). actual # = position among
1506
  // VISITED stops ordered by check-in time (same key the route/timeline use).
1507
  // Pending / deferred stops (no check-in) show just the planned # and sit below.
1508
  var visitsArr = data.visits||[];
1509
  visitsArr.slice()
1510
    .filter(function(v){ return !!v.checkInTime; })
1511
    .sort(function(a,b){ return tlSecs(a.checkInTime)-tlSecs(b.checkInTime); })
1512
    .forEach(function(v,i){ v._actualNo = i+1; });   // tags the shared object refs
1513
  visitsArr.forEach(function(v,i){
1514
    var color=bjMarkColor(v.markType), visited=!!v.checkInTime, planned=i+1;
1515
    var label = visited ? (planned+'→'+v._actualNo) : String(planned);
1516
    pin({lat:v.lat,lng:v.lng}, bjSeqPin(color, label), visited?100:60,
36897 vikas 1517
      (v.name||'')+' ('+v.markType+')', bjMarkInfo(v,color));
1518
  });
1519
 
1520
  // 4) punch in / punch out
1521
  if(data.punchIn){
1522
    pin({lat:data.punchIn.lat,lng:data.punchIn.lng}, bjPin('#2f9e44','P'), 300,
1523
      'Punch in'+(data.punchIn.time?' · '+data.punchIn.time:''),
1524
      '<div style="font-size:12px"><b>&#9873; Punch in</b>'+(data.punchIn.time?'<br>'+esc(data.punchIn.time):'')+'</div>');
1525
  }
1526
  if(data.punchOut){
1527
    pin({lat:data.punchOut.lat,lng:data.punchOut.lng}, bjPin('#7048e8','X'), 300,
1528
      'Punch out'+(data.punchOut.time?' · '+data.punchOut.time:''),
1529
      '<div style="font-size:12px"><b>&#9873; Punch out</b>'+(data.punchOut.time?'<br>'+esc(data.punchOut.time):'')+'</div>');
1530
  }
1531
 
36903 vikas 1532
  // 5) routes — actual road taken vs Google-suggested optimal, for comparison.
37028 vikas 1533
  // Suggested-route checkpoints = the whole day's stops (planned + visited), not
1534
  // just the ones already visited, so the suggestion shows from punch-in onward
1535
  // (and even before the first check-in). optimizeWaypoints reorders them, so the
1536
  // input order only needs to keep punch-in first / punch-out last; visited stops
1537
  // are listed by check-in time and still-pending stops after them.
36903 vikas 1538
  var cps=[];
1539
  if(data.punchIn) cps.push({lat:data.punchIn.lat,lng:data.punchIn.lng});
1540
  (data.visits||[]).slice()
37028 vikas 1541
    .filter(function(v){ return v.lat && v.lng; })                   // every stop with coords
36903 vikas 1542
    .sort(function(a,b){ return tlSecs(a.checkInTime)-tlSecs(b.checkInTime); })
1543
    .forEach(function(v){ cps.push({lat:v.lat,lng:v.lng}); });
1544
  if(data.punchOut) cps.push({lat:data.punchOut.lat,lng:data.punchOut.lng});
36929 vikas 1545
  // Actual time window of the GPS trail (first → last breadcrumb timestamp),
1546
  // shown on the taken-route click detail alongside the driving estimate.
1547
  var takenTime='';
1548
  var dp=data.path||[];
1549
  if(dp.length>=2 && dp[0].ts && dp[dp.length-1].ts && dp[dp.length-1].ts>dp[0].ts){
1550
    var t0=dp[0].ts, t1=dp[dp.length-1].ts;
1551
    takenTime=bjClock(t0)+' &rarr; '+bjClock(t1)+' ('+bjElapsed(t1-t0)+')';
1552
  }
1553
  bjDrawRoutes(map, cps, pts, takenTime, dp);
36903 vikas 1554
 
36897 vikas 1555
  if(has){
1556
    map.fitBounds(bounds);
1557
    google.maps.event.addListenerOnce(map,'idle',function(){ if(map.getZoom()>16) map.setZoom(16); });
1558
  } else {
1559
    box.innerHTML='<div class="loading">No GPS / location recorded for this day.</div>';
1560
  }
1561
}
36903 vikas 1562
// TAKEN route = the recorded GPS trail (location_tracking_path) routed along the
1563
// road network so it follows actual roads rather than straight hops between sparse
1564
// pings. SUGGESTED route = the Google-optimal road route through the journey's
1565
// checkpoints (green dotted). Both are drawn and compared by distance.
36929 vikas 1566
function bjDrawRoutes(map, cps, pathPts, takenTime, timedPts){
36903 vikas 1567
  var legend=bjRouteLegend();
1568
 
37032 vikas 1569
  // De-jitter the breadcrumb first: stationary GPS drift (e.g. under a roof)
1570
  // scatters points around a fixed spot, which makes the road-snapped route weave
1571
  // back and forth and inflates the taken distance. Collapsing sub-JITTER_MIN_M
1572
  // hops removes those clusters before we draw or measure the trail.
1573
  var trail=bjCleanTrail(pathPts, 30);
1574
 
36903 vikas 1575
  // taken — road route traced through the recorded breadcrumb (in order)
37032 vikas 1576
  if(trail && trail.length>=2){
36903 vikas 1577
    var ds1=new google.maps.DirectionsService();
37032 vikas 1578
    var sample=bjSample(trail, 25);                           // Directions cap: 25 incl. ends
36903 vikas 1579
    var o=sample[0], d=sample[sample.length-1];
1580
    var vias=sample.slice(1,-1).map(function(p){ return {location:new google.maps.LatLng(p.lat,p.lng), stopover:false}; });
1581
    bjRouteRequest(ds1, o, d, vias, false, function(res){
36929 vikas 1582
      bjRenderRoute(map, res, {color:'#e03131', dashed:false, z:55, label:'Route taken (actual GPS)', time:takenTime, timed:timedPts});
36903 vikas 1583
      legend.set('taken', bjRouteKm(res));
1584
    }, function(status){
1585
      console.warn('Directions (taken) failed:', status);
37032 vikas 1586
      // fallback: plot the cleaned breadcrumb (straight hops), with arrows, and measure along it
1587
      var poly=new google.maps.Polyline({path:trail,geodesic:false,strokeColor:'#e03131',strokeOpacity:.9,strokeWeight:4,zIndex:55,map:map,
36929 vikas 1588
        icons:[{icon:{path:google.maps.SymbolPath.FORWARD_CLOSED_ARROW,strokeColor:'#e03131',fillColor:'#e03131',fillOpacity:1,scale:3},offset:'3%',repeat:'120px'}]});
37032 vikas 1589
      var km=google.maps.geometry.spherical.computeLength(trail.map(function(p){ return new google.maps.LatLng(p.lat,p.lng); }))/1000;
36929 vikas 1590
      bjRouteClick(map, poly, '#e03131', 'Route taken (actual GPS)', km, '', takenTime, timedPts);
1591
      legend.set('taken', km);
36903 vikas 1592
    });
1593
  } else {
1594
    legend.set('taken', false);
1595
  }
1596
 
1597
  // suggested — Google-optimal route through punch-in → visits → punch-out
1598
  if(cps.length>=2){
1599
    var ds=new google.maps.DirectionsService();
1600
    var origin=cps[0], dest=cps[cps.length-1], mids=cps.slice(1,-1), capped=false;
1601
    if(mids.length>23){ mids=mids.slice(0,23); capped=true; }  // Directions allows 23 waypoints
1602
    var wpts=mids.map(function(p){ return {location:new google.maps.LatLng(p.lat,p.lng), stopover:true}; });
1603
    bjRouteRequest(ds, origin, dest, wpts, true, function(res){
36929 vikas 1604
      bjRenderRoute(map, res, {color:'#2f9e44', dashed:true, z:50, label:'Suggested route (optimal)'});
36903 vikas 1605
      legend.set('suggested', bjRouteKm(res));
1606
    }, function(status){ console.warn('Directions (suggested) failed:', status); legend.set('suggested', false); });
1607
    if(capped) legend.note('Suggested route limited to first 23 stops');
1608
  } else {
1609
    legend.set('suggested', false);
1610
  }
1611
}
37032 vikas 1612
// Collapse stationary GPS jitter: keep the first point, then only points that
1613
// moved at least `minM` metres from the last kept one; the true final point is
1614
// always kept so the trail ends at the last known location. This removes the
1615
// dense drift clusters (weak signal indoors) that otherwise draw back-and-forth
1616
// loops and inflate the taken distance, while preserving real movement.
1617
function bjCleanTrail(pts, minM){
1618
  if(!pts || pts.length<2) return pts ? pts.slice() : [];
1619
  var out=[pts[0]], last=pts[0];
1620
  for(var i=1;i<pts.length-1;i++){
1621
    var p=pts[i];
1622
    var d=google.maps.geometry.spherical.computeDistanceBetween(
1623
      new google.maps.LatLng(last.lat,last.lng), new google.maps.LatLng(p.lat,p.lng));
1624
    if(d>=minM){ out.push(p); last=p; }
1625
  }
1626
  out.push(pts[pts.length-1]);   // keep the final fix regardless
1627
  return out;
1628
}
36903 vikas 1629
// Evenly downsample a list to at most `max` items, always keeping first and last.
1630
function bjSample(arr, max){
1631
  if(arr.length<=max) return arr.slice();
1632
  var out=[], step=(arr.length-1)/(max-1);
1633
  for(var i=0;i<max;i++) out.push(arr[Math.round(i*step)]);
1634
  return out;
1635
}
1636
function bjRouteRequest(ds, origin, dest, wpts, optimize, ok, err){
1637
  ds.route({
1638
    origin:new google.maps.LatLng(origin.lat,origin.lng),
1639
    destination:new google.maps.LatLng(dest.lat,dest.lng),
1640
    waypoints:wpts, optimizeWaypoints:optimize,
1641
    travelMode:google.maps.TravelMode.DRIVING
1642
  }, function(res, status){
1643
    if(status===google.maps.DirectionsStatus.OK && res && res.routes && res.routes.length) ok(res);
1644
    else err(status);
1645
  });
1646
}
36929 vikas 1647
// Draw a route as a custom polyline (so we can add direction arrows + a click
1648
// info window). Forward arrows are repeated along the line; dashed routes also
1649
// get the dash pattern. Clicking the line opens its distance / duration detail.
36903 vikas 1650
function bjRenderRoute(map, res, opt){
36929 vikas 1651
  var path=res.routes[0].overview_path;
1652
  var arrow={icon:{path:google.maps.SymbolPath.FORWARD_CLOSED_ARROW, strokeColor:opt.color,
1653
                   strokeOpacity:1, strokeWeight:1, fillColor:opt.color, fillOpacity:1, scale:opt.dashed?2.6:3.2},
1654
             offset:'3%', repeat:opt.dashed?'95px':'120px'};
1655
  var icons=[arrow];
36903 vikas 1656
  if(opt.dashed){
36929 vikas 1657
    icons.unshift({icon:{path:'M 0,-1 0,1', strokeColor:opt.color, strokeOpacity:1, strokeWeight:4, scale:3}, offset:'0', repeat:'14px'});
36903 vikas 1658
  }
36929 vikas 1659
  var poly=new google.maps.Polyline({path:path, map:map, zIndex:opt.z,
1660
    strokeColor:opt.color, strokeOpacity:opt.dashed?0:0.85, strokeWeight:opt.dashed?0:5, icons:icons});
1661
  bjRouteClick(map, poly, opt.color, opt.label||(opt.dashed?'Suggested route':'Route taken'),
1662
    bjRouteKm(res), bjRouteDur(res), opt.time, opt.timed);
36903 vikas 1663
}
36929 vikas 1664
// Attach a click handler that opens an info window with the route's details at
1665
// the clicked point. Reused by both the routed line and the breadcrumb fallback.
1666
// `time` (optional) is the actual GPS time window, e.g. "09:28 → 16:03 (6h 35m)".
1667
// `timed` (optional) are the raw breadcrumbs with timestamps; when present, the
1668
// captured time at the nearest breadcrumb to the click is shown.
1669
function bjRouteClick(map, poly, color, label, km, dur, time, timed){
1670
  var iw=new google.maps.InfoWindow();
1671
  poly.addListener('click', function(e){
1672
    var here='';
1673
    if(timed && timed.length){
1674
      var np=bjNearestPt(timed, e.latLng);
1675
      if(np && np.ts) here='&#128205; At this point: <b>'+bjClockS(np.ts)+'</b><br>';
1676
    }
1677
    iw.setContent('<div style="font-size:12px;line-height:1.5"><b style="color:'+color+'">&#10148; '+esc(label)+'</b><br>'+
1678
      here+(time?'Time: <b>'+time+'</b><br>':'')+
1679
      'Distance: <b>'+km.toFixed(1)+' km</b>'+(dur?'<br>Est. drive: <b>'+esc(dur)+'</b>':'')+'</div>');
1680
    iw.setPosition(e.latLng); iw.open(map);
1681
  });
1682
}
1683
// Nearest timestamped breadcrumb to a clicked LatLng (great-circle distance).
1684
function bjNearestPt(timed, latLng){
1685
  var best=null, bd=Infinity;
1686
  for(var i=0;i<timed.length;i++){
1687
    var d=google.maps.geometry.spherical.computeDistanceBetween(latLng, new google.maps.LatLng(timed[i].lat, timed[i].lng));
1688
    if(d<bd){ bd=d; best=timed[i]; }
1689
  }
1690
  return best;
1691
}
1692
// Format an epoch-millis timestamp as a local HH:MM clock, and a millisecond
1693
// span as a compact "Nh Nm" / "Nm" elapsed string (for the GPS time window).
1694
function bjClock(ts){
1695
  var d=new Date(ts), h=d.getHours(), m=d.getMinutes();
1696
  return (h<10?'0':'')+h+':'+(m<10?'0':'')+m;
1697
}
1698
function bjClockS(ts){
1699
  var d=new Date(ts), h=d.getHours(), m=d.getMinutes(), s=d.getSeconds();
1700
  return (h<10?'0':'')+h+':'+(m<10?'0':'')+m+':'+(s<10?'0':'')+s;
1701
}
1702
function bjElapsed(ms){
1703
  var s=Math.round(ms/1000), h=Math.floor(s/3600), m=Math.round((s%3600)/60);
1704
  return h ? (h+'h '+m+'m') : (m+'m');
1705
}
36903 vikas 1706
function bjRouteKm(res){
1707
  var m=0; (res.routes[0].legs||[]).forEach(function(l){ m+=(l.distance?l.distance.value:0); }); return m/1000;
1708
}
36929 vikas 1709
function bjRouteDur(res){
1710
  var s=0; (res.routes[0].legs||[]).forEach(function(l){ s+=(l.duration?l.duration.value:0); });
1711
  if(!s) return '';
1712
  var h=Math.floor(s/3600), m=Math.round((s%3600)/60);
1713
  return h ? (h+'h '+m+'m') : (m+'m');
1714
}
36903 vikas 1715
// Route-comparison panel rendered into the modal (#bjRouteLegend), above the map.
1716
// Each distance fills in as its route resolves. value: number = km, false =
1717
// unavailable, undefined = still loading.
1718
function bjRouteLegend(){
1719
  var div=el('bjRouteLegend');
1720
  if(!div) return { set:function(){}, note:function(){} };
1721
  var taken, sugg, noteTxt='';
1722
  function val(v){ return typeof v==='number' ? '<b>'+v.toFixed(1)+' km</b>' : (v===false ? 'n/a' : '…'); }
1723
  function render(){
1724
    var cmp='';
1725
    if(typeof taken==='number' && typeof sugg==='number'){
1726
      cmp = taken>sugg+0.05
1727
        ? '<span style="color:#e03131">+'+(taken-sugg).toFixed(1)+' km longer than suggested</span>'
1728
        : '<span style="color:#2f9e44">on / better than the suggested route</span>';
1729
    }
1730
    div.innerHTML=
1731
      '<span class="lg">Route comparison</span>'+
1732
      '<span><span class="sw ln"></span>Taken (GPS)</span>'+
1733
      '<span><span class="sw dt"></span>Suggested</span>'+
1734
      cmp+
1735
      (noteTxt?'<span style="color:#e8590c">'+noteTxt+'</span>':'');
1736
  }
1737
  render();
1738
  return { set:function(k,v){ if(k==='taken')taken=v; else sugg=v; render(); }, note:function(t){ noteTxt=t; render(); } };
1739
}
36882 vikas 1740
function tlSecs(t){ if(!t) return 1e9; var p=t.split(':'); return (+p[0])*3600+(+(p[1]||0))*60+(+(p[2]||0)); }
1741
function toSec(t){ if(!t||t==='00:00:00') return 0; var p=t.split(':'); return (+p[0])*3600+(+(p[1]||0))*60+(+(p[2]||0)); }
1742
function fmtSec(s){ s=Math.max(0,Math.round(s)); var h=Math.floor(s/3600), m=Math.floor((s%3600)/60), ss=s%60; function z(n){return (n<10?'0':'')+n;} return z(h)+':'+z(m)+':'+z(ss); }
1743
function jstat(v,l){ return '<div class="js"><div class="jv">'+v+'</div><div class="jl">'+l+'</div></div>'; }
1744
function hhmm(t){ return t? t.slice(0,5):''; }
37039 vikas 1745
// Best-effort: pull a "when will they order" hint out of a free-text remark.
1746
// English + Hinglish. Display-only; returns '' when nothing recognizable is found.
1747
function bjScheduledFromRemark(remark){
1748
  if(!remark) return '';
1749
  var t = String(remark).toLowerCase();
1750
  // day-after (before tomorrow/kal, since "parso" is distinct)
1751
  if(/\b(day[\s-]?after(\s+tomorrow)?|parso)\b/.test(t)) return 'day after';
1752
  if(/\b(tomorrow|kal|tmrw|tmr)\b/.test(t)) return 'tomorrow';
1753
  if(/\b(today|aaj|abhi)\b/.test(t)) return 'today';
1754
  // range "N-M days" / "N-M din" (check before single N)
1755
  var range = t.match(/\b(\d{1,2})\s*[-–to]+\s*(\d{1,2})\s*(days?|din)\b/);
1756
  if(range) return 'in '+range[1]+'–'+range[2]+' days';
1757
  var nd = t.match(/\b(?:in\s+)?(\d{1,2})\s*(days?|din)\b/);
1758
  if(nd) return 'in '+nd[1]+' day'+(nd[1]==='1'?'':'s');
1759
  if(/\b(this\s*week|is\s*week|is\s*haft[ae])\b/.test(t)) return 'this week';
1760
  if(/\b(next\s*week|agle\s*haft[ae]|agla\s*week)\b/.test(t)) return 'next week';
1761
  // explicit "DD Mon" / "DD Month"
1762
  var mon = t.match(/\b(\d{1,2})\s+(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\b/);
1763
  if(mon){
1764
    var M={jan:'Jan',feb:'Feb',mar:'Mar',apr:'Apr',may:'May',jun:'Jun',jul:'Jul',aug:'Aug',sep:'Sep',oct:'Oct',nov:'Nov',dec:'Dec'};
1765
    return parseInt(mon[1],10)+' '+M[mon[2]];
1766
  }
1767
  // "DD/MM" or "DD-MM" (reject year-like / out-of-range month)
1768
  var dm = t.match(/\b(\d{1,2})[\/\-](\d{1,2})\b(?!\d)/);
1769
  if(dm && parseInt(dm[2],10)>=1 && parseInt(dm[2],10)<=12){
1770
    var mo=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
1771
    return parseInt(dm[1],10)+' '+mo[parseInt(dm[2],10)-1];
1772
  }
1773
  // weekday names (English + common Hinglish)
1774
  var wk = t.match(/\b(monday|tuesday|wednesday|thursday|friday|saturday|sunday|somwar|mangalwar|budhwar|guruwar|shukrawar|shaniwar|raviwar|itwar)\b/);
1775
  if(wk){
1776
    var W={monday:'Mon',somwar:'Mon',tuesday:'Tue',mangalwar:'Tue',wednesday:'Wed',budhwar:'Wed',
1777
           thursday:'Thu',guruwar:'Thu',friday:'Fri',shukrawar:'Fri',saturday:'Sat',shaniwar:'Sat',
1778
           sunday:'Sun',raviwar:'Sun',itwar:'Sun'};
1779
    return W[wk[1]];
1780
  }
1781
  return '';
1782
}
36882 vikas 1783
function tlThumb(att){
36903 vikas 1784
  if(att && window._mediaUrl) return '<img class="tlthumb" src="'+esc(window._mediaUrl+att)+'" onclick="bjOpenImage(this.src)" onerror="tlImgErr(this)">';
37044 vikas 1785
  return '';   // no image -> render nothing (no placeholder box)
36882 vikas 1786
}
1787
function tlImgErr(im){ var d=document.createElement('div'); d.className='tlph'; d.title='Photo unavailable'; d.innerHTML='&#128247;'; if(im.parentNode) im.parentNode.replaceChild(d, im); }
1788
function punchEntry(kind, time){ return {_punch:kind, checkInTime:time, markType:(kind==='in'?'PUNCHIN':'PUNCHOUT'), taskType:'attendance'}; }
1789
function tlStyle(x){
1790
  var mt=(x.markType||'').toUpperCase(), tt=(x.taskType||'').toLowerCase();
1791
  if(x._punch || tt==='attendance' || mt.indexOf('PUNCH')>=0) return {c:'#7048e8', l:(x._punch==='out'?'Punch out':'Punch in'), ic:'&#9873;'};
1792
  if(mt.indexOf('CHECKOUT')>=0) return {c:'#2f9e44', l:'Visited', ic:'&#10003;'};
37034 vikas 1793
  if(mt==='CHECKIN') return {c:'#1098ad', l:'In', ic:'&#128205;'};
37044 vikas 1794
  if(mt==='DEFERRED') return {c:'#e8590c', l:'Deferred', ic:'&#9888;'};
36882 vikas 1795
  if(mt==='PENDING' || !x.checkInTime) return {c:'#97a1b5', l:'Pending', ic:'&#9675;'};
1796
  return {c:'#3b5bdb', l:(mt||'-'), ic:'&#9679;'};
1797
}
1798
function tlTitle(x){
1799
  if(x._punch) return x._punch==='in' ? 'Journey start (Punch in)' : 'Journey end (Punch out)';
1800
  var n=x.taskName||'';
36903 vikas 1801
  var taskType=(x.taskType||'').toLowerCase();
1802
  if(taskType==='franchisee-visit') {
1803
    var pipe = n.lastIndexOf('|');
1804
    if (pipe >= 0) n = n.slice(pipe + 1);      // drop leading agenda tags
1805
    n = n.replace(/\s*VISIT\b.*$/i, '').replace(/\s*visit\s*$/i, '').replace(/\bnull\b/gi, '').replace(/\s{2,}/g, ' ').trim();
1806
  }
36882 vikas 1807
  return n || '(stop)';
1808
}
37044 vikas 1809
// Agenda chips: task_name is "agenda1, agenda2 | Outlet" — show the prefix (partner visits only).
1810
function tlAgendas(x){
1811
  if(x._punch || (x.taskType||'').toLowerCase()!=='franchisee-visit') return '';
1812
  var n=x.taskName||'', pipe=n.lastIndexOf(' | ');
1813
  var part = pipe>0 ? n.slice(0,pipe) : '';
1814
  if(!part) return '';
1815
  var out='';
1816
  part.split(',').forEach(function(a){ var t=a.trim(); if(t) out += '<span class="tlag">'+esc(t)+'</span>'; });
1817
  return out ? '<div class="tlags">'+out+'</div>' : '';
1818
}
36882 vikas 1819
function tlRow(x, side){
1820
  var s=tlStyle(x);
1821
  var tt=(x.taskType||'').toLowerCase();
1822
  var timeTop = x.checkInTime ? hhmm(x.checkInTime) : '-';
1823
  var tlabel = x._punch ? (x._punch==='in'?'punch in':'punch out') : (x.checkInTime?'check-in':'pending');
1824
  var meta;
1825
  if(x._punch){
1826
    meta = (x._punch==='in'?'Punch in':'Punch out') + ' &middot; ' + hhmm(x.checkInTime);
1827
  } else {
1828
    var io = (x.checkInTime?hhmm(x.checkInTime):'-') + ' &rarr; ' + (x.checkOutTime?hhmm(x.checkOutTime):'-');
1829
    var dur = (x.timeSpent && x.timeSpent!=='00:00:00') ? ' &middot; '+esc(x.timeSpent) : '';
1830
    var typeLabel = tt==='lead' ? 'Lead' : (tt==='office-visit'?'Office':'Partner');
1831
    meta = x.checkInTime ? (typeLabel+' &middot; '+io+dur) : (typeLabel+' &middot; not visited');
1832
  }
1833
  // image (or placeholder) + battery + location-toggle on real stops
1834
  var extra='';
1835
  if(!x._punch){
1836
    var chips='';
1837
    if(x.battery!=null) chips += '<span class="tlchip">&#128267; '+x.battery+'%</span>';
1838
    if(x.locOff!=null) chips += '<span class="tlchip">&#128244; '+x.locOff+'</span>';
37044 vikas 1839
    // orders placed that day (any agenda): one badge -> drawer lists them all.
1840
    // No order + billing agenda -> a parsed "scheduled" chip instead.
1841
    if(x.orders && x.orders.length){
1842
      var n=x.orders.length;
1843
      chips += '<button type="button" class="tlbadge ord" title="View orders placed this day"'
1844
             + ' onclick="bjOpenOrderList('+x.taskId+')">&#129534; '+n+' order'+(n>1?'s':'')+'</button>';
1845
    } else if(x.billingAgenda){
1846
      var sch = bjScheduledFromRemark(x.remarks);
1847
      if(sch) chips += '<span class="tlbadge sch" title="Scheduled (from remark)">&#128197; '+esc(sch)+'</span>';
37039 vikas 1848
    }
37044 vikas 1849
    var thumb=tlThumb(x.attachment);
1850
    if(thumb || chips) extra = '<div class="tlextra">'+thumb+chips+'</div>';
36882 vikas 1851
  }
36903 vikas 1852
  var card='<div class="tlcard">'+
36882 vikas 1853
        '<div class="tm" style="background:'+s.c+'">'+timeTop+'<small>'+tlabel+'</small></div>'+
1854
        '<div class="bd"><div class="ti">'+esc(tlTitle(x))+'</div>'+
1855
          '<span class="stt" style="background:'+s.c+'">'+s.l+'</span>'+
36941 vikas 1856
          (tt==='lead' ? '<span class="stt unplanned">Unplanned</span>' : '')+
37044 vikas 1857
          tlAgendas(x)+
36882 vikas 1858
          '<div class="mt">'+meta+'</div>'+extra+'</div>'+
36903 vikas 1859
      '</div>';
1860
  // remark sits on the opposite half of the timeline (empty spacer when none)
37038 vikas 1861
  var rk='';
1862
  if(!x._punch && x.remarks){
1863
    rk='<div class="rk"><div class="rkl">Remark</div>'+esc(x.remarks);
1864
    if(x.taskId && x.taskId>0){
1865
      // one big-string timeline -> global fn + primitive args (partner name escaped for the attribute)
1866
      rk+='<button type="button" class="rkhist" onclick="bjOpenRemarkHistory('+x.taskId+',\''+js(x.taskType||'')+'\',\''+js(tlTitle(x))+'\')">&#128220; Past discussions</button>';
1867
    }
1868
    rk+='</div>';
1869
  }
36903 vikas 1870
  var remark='<div class="tlremark">'+rk+'</div>';
1871
  var halves = side==='left' ? (card+remark) : (remark+card);
1872
  return '<div class="tlrow '+side+'">'+
1873
      '<div class="node" style="background:'+s.c+'">'+s.ic+'</div>'+
1874
      halves+'</div>';
36882 vikas 1875
}
36877 vikas 1876
function closeModal(){ el('bj-modal-back').classList.remove('show'); }
36903 vikas 1877
// Image lightbox — opens an attachment larger, stacked above the journey modal.
1878
function bjOpenImage(src){ if(!src) return; el('bj-img').src=src; el('bj-img-back').classList.add('show'); }
1879
function bjCloseImage(){ el('bj-img-back').classList.remove('show'); el('bj-img').removeAttribute('src'); }
36877 vikas 1880
 
37038 vikas 1881
/* ===================== REMARK HISTORY DRAWER ===================== */
1882
// All prior discussions for one partner/lead (task_id + task_type), across every
1883
// executive and date, newest first. Paged via /beat-journey/remark-history.
1884
function bjOpenRemarkHistory(taskId, taskType, name){
1885
  if(!taskId || taskId<=0) return;
1886
  window._rh = { taskId:taskId, taskType:taskType, name:name, offset:0, limit:8, items:[], loading:false, total:0 };
1887
  el('bj-rh-title').innerHTML='&#128220; '+esc(name);
1888
  el('bj-rh-sub').textContent='Past discussions across all executives';
1889
  el('bj-rh-list').innerHTML='<div class="rhloading"><div class="spinner"></div>Loading&hellip;</div>';
1890
  el('bj-rh-more').style.display='none';
1891
  el('bj-rh-back').classList.add('show');
1892
  bjLoadRemarkHistory();
1893
}
1894
function closeRemarkHistory(){ el('bj-rh-back').classList.remove('show'); window._rh=null; }
1895
function bjLoadRemarkHistory(){
1896
  var s=window._rh; if(!s || s.loading) return;          // block re-entrant fetches
1897
  s.loading=true;
1898
  var more=el('bj-rh-more'); more.disabled=true; more.textContent='Loading…';
1899
  var u=CTX+'/beat-journey/remark-history?taskId='+s.taskId
1900
        +'&taskType='+encodeURIComponent(s.taskType||'')
1901
        +'&offset='+s.offset+'&limit='+s.limit;
1902
  fetch(u,{headers:{'Accept':'application/json'}})
1903
    .then(function(r){ return r.json(); })
1904
    .then(function(d){
1905
      var st=window._rh; if(!st || st.taskId!==s.taskId) return;  // drawer changed mid-flight
1906
      st.total=d.total||0;
1907
      (d.items||[]).forEach(function(it){ st.items.push(it); });
1908
      st.offset += (d.items||[]).length;
1909
      st.loading=false;
1910
      bjRenderRemarkHistory(!!d.hasMore);
1911
    })
1912
    .catch(function(){
1913
      var st=window._rh; if(st) st.loading=false;
1914
      el('bj-rh-list').innerHTML='<div class="rhempty">Could not load past discussions.</div>';
1915
      el('bj-rh-more').style.display='none';
1916
    });
1917
}
1918
function bjRenderRemarkHistory(hasMore){
1919
  var s=window._rh; if(!s) return;
1920
  if(!s.items.length){
1921
    el('bj-rh-list').innerHTML='<div class="rhempty">No earlier discussions for this partner.</div>';
1922
    el('bj-rh-more').style.display='none';
1923
    return;
1924
  }
1925
  var h='';
1926
  s.items.forEach(function(it){
1927
    var mt=(it.markType||'').toUpperCase();
1928
    var badge = mt.indexOf('CHECKOUT')>=0 ? '<span class="rhb ok">visited</span>'
1929
              : (mt==='DEFERRED' ? '<span class="rhb def">deferred</span>' : '');
1930
    var when = esc(it.date||'')+(it.time?' &middot; '+esc(hhmm(it.time)):'');
1931
    h+='<div class="rhitem">'
1932
      +'<div class="rhtop"><span class="rhdate">'+when+'</span>'+badge+'</div>'
1933
      +'<div class="rhexec">'+esc(it.exec||'')+'</div>'
1934
      +'<div class="rhtext">'+esc(it.remark||'')+'</div></div>';
1935
  });
1936
  el('bj-rh-list').innerHTML=h;
1937
  var more=el('bj-rh-more');
1938
  more.disabled=false; more.textContent='Load more';
1939
  more.style.display = hasMore ? 'block' : 'none';
1940
}
1941
 
37044 vikas 1942
/* ===================== ORDERS DRAWER ===================== */
1943
// All retail orders a partner placed on the visit day, each with its line items.
1944
function bjOpenOrderList(fofoId){
1945
  if(!fofoId) return;
1946
  var date=(window._exec && window._exec.date) || '';
1947
  window._od = { fofoId:fofoId, date:date };
1948
  el('bj-od-title').innerHTML='&#129534; Orders';
1949
  el('bj-od-sub').textContent='Orders placed on '+date;
37039 vikas 1950
  el('bj-od-body').innerHTML='<div class="odloading"><div class="spinner"></div>Loading&hellip;</div>';
1951
  el('bj-od-back').classList.add('show');
37044 vikas 1952
  fetch(CTX+'/beat-journey/order-list?fofoId='+encodeURIComponent(fofoId)+'&date='+encodeURIComponent(date),
37039 vikas 1953
        {headers:{'Accept':'application/json'}})
1954
    .then(function(r){ return r.json(); })
1955
    .then(function(d){
37044 vikas 1956
      var st=window._od; if(!st || st.fofoId!==fofoId) return;   // drawer changed mid-flight
1957
      bjRenderOrderList(d);
37039 vikas 1958
    })
37044 vikas 1959
    .catch(function(){ el('bj-od-body').innerHTML='<div class="odempty">Could not load orders.</div>'; });
37039 vikas 1960
}
1961
function closeOrderDetail(){ el('bj-od-back').classList.remove('show'); window._od=null; }
37044 vikas 1962
function bjRenderOrderList(d){
1963
  var orders=d.orders||[];
1964
  if(!orders.length){ el('bj-od-body').innerHTML='<div class="odempty">No orders placed on this day.</div>'; return; }
37039 vikas 1965
  var money=function(n){ return '₹'+(Math.round(Number(n)||0)).toLocaleString(); };
37044 vikas 1966
  el('bj-od-sub').textContent=orders.length+' order'+(orders.length>1?'s':'')+' placed on '+(window._od?window._od.date:'');
1967
  var h='';
1968
  orders.forEach(function(o){
1969
    h+='<div class="odorder"><div class="odsum">'
1970
      +'<div><div class="k">Invoice</div><div class="v">'+esc(o.invoice||('#'+o.orderId))+'</div></div>'
1971
      +'<div><div class="k">Amount</div><div class="v">'+money(o.amount)+'</div></div>'
1972
      +'<div><div class="k">Time</div><div class="v">'+esc(o.time?hhmm(o.time):'—')+'</div></div>'
1973
      +'</div>';
1974
    var items=o.items||[];
1975
    if(items.length){
1976
      h+='<table class="odtable"><thead><tr><th>Item</th><th class="num">Qty</th><th class="num">MOP</th></tr></thead><tbody>';
1977
      items.forEach(function(it){
1978
        var name=[it.brand,it.model,it.color].filter(function(s){return s;}).map(esc).join(' &middot; ');
1979
        h+='<tr><td>'+(name||'&mdash;')+'</td><td class="num">'+(it.qty||0)+'</td><td class="num">'+money(it.mop)+'</td></tr>';
1980
      });
1981
      h+='</tbody></table>';
1982
    } else { h+='<div class="odempty">No line items.</div>'; }
1983
    h+='</div>';
37039 vikas 1984
  });
1985
  el('bj-od-body').innerHTML=h;
1986
}
1987
 
36877 vikas 1988
/* ============================ APPROVAL ============================ */
1989
function loadApproval(){
1990
  spinner();
1991
  var from=el('bj-start').value, to=el('bj-end').value;
1992
  var u=CTX+'/beat-report/approval/data?fromDate='+encodeURIComponent(from)+'&toDate='+encodeURIComponent(to)+qs();
1993
  fetch(u,{headers:{'Accept':'application/json'}}).then(function(r){ return r.json(); }).then(function(d){
1994
    var rows=d.rows||[];
1995
    var h='<div class="card"><h2>Location Approval <span style="color:#97a1b5;font-weight:500;font-size:12px">&middot; '+rows.length+' off-site check-ins</span></h2>'+
1996
      '<p class="desc">Check-ins recorded &gt;50 m from the partner\'s saved location, across your downline. Approving updates the partner\'s coordinates.</p>';
1997
    if(!rows.length){ h+='<p class="desc">Nothing pending in this window.</p></div>'; el('bj-content').innerHTML=h; return; }
1998
    h+='<table><thead><tr><th>Executive</th><th>Store</th><th>Date</th><th>Check-in</th><th class="num">Distance</th><th class="num">Action</th></tr></thead><tbody>';
1999
    rows.forEach(function(r){
2000
      h+='<tr id="appr-'+r.trackingId+'"><td>'+esc(r.username)+'</td><td>'+esc(r.taskName)+'</td><td>'+esc(r.taskDate)+'</td><td>'+esc(r.checkInTime)+'</td>'+
2001
         '<td class="num '+(r.distanceM>200?'badflag':'warnflag')+'">'+r.distanceM+' m</td>'+
2002
         '<td class="num"><button class="btn-ok" onclick="approve('+r.trackingId+')">Approve</button></td></tr>';
2003
    });
2004
    h+='</tbody></table></div>';
2005
    el('bj-content').innerHTML=h;
2006
  }).catch(fail);
2007
}
2008
function approve(trackingId){
2009
  fetch(CTX+'/beat-report/approve-checkin',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'trackingId='+trackingId})
2010
    .then(function(r){ return r.json(); }).then(function(){ var row=el('appr-'+trackingId); if(row) row.parentNode.removeChild(row); }).catch(function(){ alert('Approve failed'); });
2011
}
2012
 
36903 vikas 2013
/* ============================ EXPORT ============================ */
2014
// Download the Employee Task Report (.xlsx). Today lens exports the single
2015
// selected day; Period lens exports the whole start..end range. Both carry the
2016
// current level / team scope (qs()). The download is served as an attachment, so
2017
// navigating to the URL triggers the file save without leaving the page.
2018
function exportReport(){
36913 vikas 2019
  var start, end, path;
2020
  if(LENS==='period'){
2021
    // Period lens: per-employee totals + a Raw Data sheet (separate endpoint).
2022
    start=el('bj-start').value; end=el('bj-end').value; path='/export-period';
2023
  } else {
2024
    // Today lens: the day-grouped Employee Task Report (unchanged).
2025
    start=el('bj-date').value; end=start; path='/export';
2026
  }
2027
  var u = BASE + path + '?startDate=' + encodeURIComponent(start) + '&endDate=' + encodeURIComponent(end) + qs();
36903 vikas 2028
  window.location.href = u;
2029
}
2030
 
36877 vikas 2031
/* ============================ INIT ============================ */
2032
document.querySelectorAll('.lens').forEach(function(b){ b.addEventListener('click', function(){ setLens(b.getAttribute('data-lens')); }); });
2033
el('bj-apply').addEventListener('click', load);
36903 vikas 2034
el('bj-export').addEventListener('click', exportReport);
36897 vikas 2035
el('bj-level').addEventListener('change', onLevelChange);
2036
if(el('bj-user')) el('bj-user').addEventListener('change', load);
2037
el('bj-date').addEventListener('change', updateTodayLabel);
36877 vikas 2038
el('bj-modal-back').addEventListener('click', function(e){ if(e.target===el('bj-modal-back')) closeModal(); });
36903 vikas 2039
el('bj-img-back').addEventListener('click', function(e){ if(e.target===el('bj-img-back')) bjCloseImage(); });
2040
el('bj-img-x').addEventListener('click', bjCloseImage);
2041
// Esc closes the lightbox first (if open), otherwise the journey modal.
2042
document.addEventListener('keydown', function(e){
2043
  if(e.key!=='Escape') return;
2044
  if(el('bj-img-back').classList.contains('show')) bjCloseImage();
2045
  else if(el('bj-modal-back').classList.contains('show')) closeModal();
2046
});
36897 vikas 2047
updateTodayLabel();
36877 vikas 2048
load();
2049
</script>
2050
</body>
2051
</html>