| Line 112... |
Line 112... |
| 112 |
.bdot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px;vertical-align:middle}
|
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}
|
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}
|
| 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}
|
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}
|
| 115 |
td.cnt{font-weight:700;font-variant-numeric:tabular-nums}
|
115 |
td.cnt{font-weight:700;font-variant-numeric:tabular-nums}
|
| 116 |
.cgreen{color:#2f9e44}.camber{color:#f08c00}.cred{color:#e03131}.cmuted{color:#69748c}
|
116 |
.cgreen{color:#2f9e44}.camber{color:#f08c00}.cred{color:#e03131}.cmuted{color:#69748c}
|
| - |
|
117 |
|
| - |
|
118 |
/* visit-quality flags — top-offenders panel */
|
| - |
|
119 |
.fsummary{display:flex;gap:24px;flex-wrap:wrap;align-items:flex-end;margin:4px 0 16px}
|
| - |
|
120 |
.fsummary .fstat .fn{font-size:28px;font-weight:780;letter-spacing:-.02em;line-height:1;font-variant-numeric:tabular-nums}
|
| - |
|
121 |
.fsummary .fstat .fn small{font-size:15px;color:var(--soft);font-weight:600}
|
| - |
|
122 |
.fsummary .fstat .fl{font-size:11px;color:var(--muted);font-weight:600;margin-top:6px}
|
| - |
|
123 |
.fsummary .fstat.sep{border-left:1px solid var(--line);padding-left:24px}
|
| - |
|
124 |
.fsummary .fstat .ftop{display:flex;align-items:baseline;gap:8px}
|
| - |
|
125 |
.fpill{display:inline-flex;align-items:center;gap:6px;font-size:10.5px;font-weight:700;padding:3px 9px;border-radius:999px;white-space:nowrap}
|
| - |
|
126 |
.fpill i{width:6px;height:6px;border-radius:50%;flex:none}
|
| - |
|
127 |
.fheroes{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
|
| - |
|
128 |
.fhero{border:1px solid var(--line);border-radius:13px;padding:15px;cursor:pointer;background:linear-gradient(180deg,transparent,color-mix(in srgb,var(--fc) 5%,transparent));transition:border-color .15s}
|
| - |
|
129 |
.fhero:hover{border-color:var(--fc)}
|
| - |
|
130 |
.fhtop{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
| - |
|
131 |
.fhname{font-size:12.5px;font-weight:650;line-height:1.25}
|
| - |
|
132 |
.fhbig{font-size:36px;font-weight:800;letter-spacing:-.03em;line-height:1;margin-top:12px;font-variant-numeric:tabular-nums}
|
| - |
|
133 |
.fhsub{font-size:11.5px;color:var(--muted);margin-top:7px}
|
| - |
|
134 |
.fring{width:44px;height:44px;border-radius:50%;flex:none;display:flex;align-items:center;justify-content:center;-webkit-mask:radial-gradient(circle 14px at center,transparent 98%,#000 100%);mask:radial-gradient(circle 14px at center,transparent 98%,#000 100%)}
|
| - |
|
135 |
.frlbl{font-size:10px;font-weight:800;font-variant-numeric:tabular-nums}
|
| - |
|
136 |
.fsub{font-size:11px;letter-spacing:.09em;text-transform:uppercase;color:var(--soft);font-weight:700;margin:16px 2px 6px}
|
| - |
|
137 |
.fsub.muted{opacity:.7}
|
| - |
|
138 |
.frow{display:grid;grid-template-columns:28px minmax(140px,1fr) 48px 66px 96px;align-items:center;gap:12px;padding:9px 8px;border-radius:9px}
|
| - |
|
139 |
.frow.clickable{cursor:pointer}
|
| - |
|
140 |
.frow.clickable:hover{background:#f7f9fd}
|
| - |
|
141 |
.frow.quiet{opacity:.6}
|
| - |
|
142 |
.fnum{font-size:11px;font-weight:700;color:var(--soft);text-align:center;font-variant-numeric:tabular-nums}
|
| - |
|
143 |
.frname{font-size:13px;font-weight:600}
|
| - |
|
144 |
.frrate{font-size:13.5px;font-weight:750;text-align:right;font-variant-numeric:tabular-nums}
|
| - |
|
145 |
.frfrac{font-size:12px;color:var(--soft);text-align:right;font-variant-numeric:tabular-nums}
|
| - |
|
146 |
@media (max-width:900px){.fheroes{grid-template-columns:1fr}.frow{grid-template-columns:24px 1fr 48px 96px}.frfrac{display:none}}
|
| - |
|
147 |
.bkbtn{position:absolute;top:14px;left:16px;background:none;border:0;color:var(--muted);font:inherit;font-size:13px;font-weight:600;cursor:pointer;padding:4px 8px;border-radius:7px}
|
| - |
|
148 |
.bkbtn:hover{background:#eef1fb;color:var(--brand)}
|
| - |
|
149 |
|
| - |
|
150 |
/* orders list — level filter chips */
|
| - |
|
151 |
.olhead{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:2px}
|
| - |
|
152 |
.olchips{display:inline-flex;gap:3px;background:#eef1fb;border-radius:9px;padding:3px}
|
| - |
|
153 |
.olchip{appearance:none;border:0;background:transparent;color:var(--muted);font:inherit;font-size:12px;font-weight:650;padding:5px 12px;border-radius:7px;cursor:pointer}
|
| - |
|
154 |
.olchip:hover{color:var(--ink)}
|
| - |
|
155 |
.olchip.on{background:var(--brand);color:#fff}
|
| 117 |
tr.boardtot td{background:#f4f6fb;border-bottom:2px solid #cdd3df;font-weight:700}
|
156 |
tr.boardtot td{background:#f4f6fb;border-bottom:2px solid #cdd3df;font-weight:700}
|
| 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}
|
157 |
.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}
|
158 |
.actbtn.map{background:#12b886}.actbtn.view{background:#3b5bdb}
|
| 120 |
.actbtn.edit{background:#f08c00}.actbtn.add{background:#2f9e44;font-size:15px}
|
159 |
.actbtn.edit{background:#f08c00}.actbtn.add{background:#2f9e44;font-size:15px}
|
| 121 |
.actbtn:active{transform:translateY(1px)}
|
160 |
.actbtn:active{transform:translateY(1px)}
|
| Line 267... |
Line 306... |
| 267 |
.tlchip{font-size:10.5px;font-weight:600;color:var(--muted);background:#f1f3f9;border-radius:6px;padding:2px 7px;white-space:nowrap}
|
306 |
.tlchip{font-size:10.5px;font-weight:600;color:var(--muted);background:#f1f3f9;border-radius:6px;padding:2px 7px;white-space:nowrap}
|
| 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}
|
307 |
.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}
|
308 |
.tlbadge.ord{background:#e7f0ff;color:#1c56c9}
|
| 270 |
.tlbadge.ord:hover{background:#d4e4ff}
|
309 |
.tlbadge.ord:hover{background:#d4e4ff}
|
| 271 |
.tlbadge.sch{background:#fff4e6;color:#c25709;cursor:default}
|
310 |
.tlbadge.sch{background:#fff4e6;color:#c25709;cursor:default}
|
| - |
|
311 |
.tlbadge.dwell{background:#fff4e6;color:#e8590c;cursor:default}
|
| - |
|
312 |
.tlbadge.nobill{background:#fff0f0;color:#e03131;cursor:default}
|
| - |
|
313 |
.tlcard.long-dwell{border-color:#ffd8a8;box-shadow:0 2px 12px rgba(232,89,12,.16)}
|
| 272 |
.exectabs{display:flex;gap:6px;justify-content:center;margin:12px 0 4px}
|
314 |
.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}
|
315 |
.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)}
|
316 |
.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}
|
317 |
#bjMap{width:100%;height:460px;border-radius:12px;border:1px solid var(--line);margin-top:10px;background:#eef1f7;overflow:hidden}
|
| 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}
|
318 |
.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}
|
| Line 520... |
Line 562... |
| 520 |
h+='</div></div>';
|
562 |
h+='</div></div>';
|
| 521 |
|
563 |
|
| 522 |
window._boardFilter = null; // funnel-card filter (null = all rows)
|
564 |
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>';
|
565 |
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>';
|
| 524 |
|
566 |
|
| - |
|
567 |
window._ordersList = d.ordersList || [];
|
| - |
|
568 |
h+='<div class="sectionlabel">Billing · Orders today</div><div id="bj-orders">'+ordersCard()+'</div>';
|
| - |
|
569 |
|
| 525 |
window._visitFlags = d.visitFlags || [];
|
570 |
window._visitFlags = d.visitFlags || [];
|
| 526 |
h+='<div class="sectionlabel">Visit-quality flags</div>';
|
571 |
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>';
|
572 |
h+='<div class="card"><h2>Visit-quality flags</h2><p class="desc">Today\'s biggest quality gaps first. Click a rule to see the flagged visits.</p>'+flagsTable(window._visitFlags)+'</div>';
|
| 528 |
|
573 |
|
| 529 |
el('bj-content').innerHTML=h;
|
574 |
el('bj-content').innerHTML=h;
|
| 530 |
}
|
575 |
}
|
| 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'; }
|
576 |
function stateClass(st){ return st==='IN_STORE'?'ps-store':st==='TRAVELLING'?'ps-travel':st==='DONE'?'ps-done':st==='NOT_STARTED'?'ps-no':'ps-idle'; }
|
| 532 |
function js(s){ return String(s==null?'':s).replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
|
577 |
function js(s){ return String(s==null?'':s).replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
|
| Line 580... |
Line 625... |
| 580 |
var total=Math.max(r.planned||0, completed+deferred);
|
625 |
var total=Math.max(r.planned||0, completed+deferred);
|
| 581 |
return {total:total, completed:completed, deferred:deferred, pending:total-completed-deferred};
|
626 |
return {total:total, completed:completed, deferred:deferred, pending:total-completed-deferred};
|
| 582 |
}
|
627 |
}
|
| 583 |
// seconds -> compact "1h 20m" / "45m" for the board's time columns.
|
628 |
// 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'); }
|
629 |
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'); }
|
| - |
|
630 |
// Day-counts for the board's Onboarded / Last-bill columns. null -> em dash.
|
| - |
|
631 |
function daysAgo(n){ return (n==null) ? '<span style="color:#c5cce0">—</span>' : (n<=0?'today':n+'d'); }
|
| - |
|
632 |
// Last-billing recency, coloured by staleness (fresher = greener, stale = red).
|
| - |
|
633 |
function billDaysHtml(n){
|
| - |
|
634 |
if(n==null) return '<span style="color:#c5cce0">—</span>';
|
| - |
|
635 |
var c = n>60?'#e03131':(n>30?'#e8590c':'#2f9e44');
|
| - |
|
636 |
return '<span style="color:'+c+'">'+(n<=0?'today':n+'d')+'</span>';
|
| - |
|
637 |
}
|
| 585 |
// Click-to-sort for the live board (mirrors the Period scorecard's sortCard). The
|
638 |
// 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
|
639 |
// 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.
|
640 |
// the underlying values here — not the rendered "1h 20m"/count strings.
|
| 588 |
var boardSortKey=null, boardSortAsc=false;
|
641 |
var boardSortKey=null, boardSortAsc=false;
|
| 589 |
function boardSortVal(r, key){
|
642 |
function boardSortVal(r, key){
|
| Line 594... |
Line 647... |
| 594 |
case 'total': case 'completed': case 'pending': case 'deferred':
|
647 |
case 'total': case 'completed': case 'pending': case 'deferred':
|
| 595 |
return boardCounts(r)[key];
|
648 |
return boardCounts(r)[key];
|
| 596 |
case 'transit': return r.transitSecs||0;
|
649 |
case 'transit': return r.transitSecs||0;
|
| 597 |
case 'inStore': return r.inStoreSecs||0;
|
650 |
case 'inStore': return r.inStoreSecs||0;
|
| 598 |
case 'state': return r.stateLabel||'';
|
651 |
case 'state': return r.stateLabel||'';
|
| - |
|
652 |
case 'onboard': return r.onboardDays==null?1e9:r.onboardDays;
|
| - |
|
653 |
case 'bill': return r.lastBillDays==null?1e9:r.lastBillDays;
|
| 599 |
default: return 0;
|
654 |
default: return 0;
|
| 600 |
}
|
655 |
}
|
| 601 |
}
|
656 |
}
|
| 602 |
function sortBoard(key){
|
657 |
function sortBoard(key){
|
| 603 |
if(boardSortKey===key) boardSortAsc=!boardSortAsc; else { boardSortKey=key; boardSortAsc=false; }
|
658 |
if(boardSortKey===key) boardSortAsc=!boardSortAsc; else { boardSortKey=key; boardSortAsc=false; }
|
| Line 615... |
Line 670... |
| 615 |
'<th class="num" onclick="sortBoard(\'completed\')">Completed'+bsArrow('completed')+'</th>'+
|
670 |
'<th class="num" onclick="sortBoard(\'completed\')">Completed'+bsArrow('completed')+'</th>'+
|
| 616 |
'<th class="num" onclick="sortBoard(\'pending\')">Pending'+bsArrow('pending')+'</th>'+
|
671 |
'<th class="num" onclick="sortBoard(\'pending\')">Pending'+bsArrow('pending')+'</th>'+
|
| 617 |
'<th class="num" onclick="sortBoard(\'deferred\')">Deferred'+bsArrow('deferred')+'</th>'+
|
672 |
'<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>'+
|
673 |
'<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>'+
|
674 |
'<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>'+
|
675 |
'<th onclick="sortBoard(\'state\')">State'+bsArrow('state')+'</th>'+
|
| - |
|
676 |
'<th class="num" title="Days since this exec last onboarded a new partner" onclick="sortBoard(\'onboard\')">Onboarded'+bsArrow('onboard')+'</th>'+
|
| - |
|
677 |
'<th class="num" title="Days since this exec\'s partners last billed" onclick="sortBoard(\'bill\')">Last bill'+bsArrow('bill')+'</th>'+
|
| - |
|
678 |
'<th>Action</th>'+
|
| 621 |
'</tr></thead><tbody>';
|
679 |
'</tr></thead><tbody>';
|
| 622 |
// column totals, computed up front so the Total row can lead the table
|
680 |
// column totals, computed up front so the Total row can lead the table
|
| 623 |
var t={total:0,completed:0,pending:0,deferred:0,transit:0,inStore:0};
|
681 |
var t={total:0,completed:0,pending:0,deferred:0,transit:0,inStore:0};
|
| 624 |
rows.forEach(function(r){
|
682 |
rows.forEach(function(r){
|
| 625 |
var c=boardCounts(r);
|
683 |
var c=boardCounts(r);
|
| Line 631... |
Line 689... |
| 631 |
'<td class="num cnt cmuted">'+t.total+'</td>'+
|
689 |
'<td class="num cnt cmuted">'+t.total+'</td>'+
|
| 632 |
'<td class="num cnt cgreen">'+t.completed+'</td>'+
|
690 |
'<td class="num cnt cgreen">'+t.completed+'</td>'+
|
| 633 |
'<td class="num cnt">'+t.pending+'</td>'+
|
691 |
'<td class="num cnt">'+t.pending+'</td>'+
|
| 634 |
'<td class="num cnt camber">'+t.deferred+'</td>'+
|
692 |
'<td class="num cnt camber">'+t.deferred+'</td>'+
|
| 635 |
'<td class="num cnt cmuted">'+bhm(t.transit)+'</td>'+
|
693 |
'<td class="num cnt cmuted">'+bhm(t.transit)+'</td>'+
|
| 636 |
'<td class="num cnt cmuted">'+bhm(t.inStore)+'</td><td></td><td></td>'+
|
694 |
'<td class="num cnt cmuted">'+bhm(t.inStore)+'</td><td></td><td></td><td></td><td></td>'+
|
| 637 |
'</tr>';
|
695 |
'</tr>';
|
| 638 |
rows.forEach(function(r){
|
696 |
rows.forEach(function(r){
|
| 639 |
var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (r.idleAway?'rowwarn':'');
|
697 |
var rowcls = r.state==='NOT_STARTED' ? 'rowbad' : (r.idleAway?'rowwarn':'');
|
| 640 |
var A = r.userId+",'"+js(r.name)+"','"+date+"'"; // openExec args
|
698 |
var A = r.userId+",'"+js(r.name)+"','"+date+"'"; // openExec args
|
| 641 |
var go = 'onclick="openExec('+A+')"';
|
699 |
var go = 'onclick="openExec('+A+')"';
|
| Line 650... |
Line 708... |
| 650 |
'<td class="num cnt">'+c.pending+'</td>'+
|
708 |
'<td class="num cnt">'+c.pending+'</td>'+
|
| 651 |
'<td class="num cnt camber">'+c.deferred+'</td>'+
|
709 |
'<td class="num cnt camber">'+c.deferred+'</td>'+
|
| 652 |
'<td class="num cnt cmuted">'+(r.transitSecs?bhm(r.transitSecs):'<span style="color:#c5cce0">—</span>')+'</td>'+
|
710 |
'<td class="num cnt cmuted">'+(r.transitSecs?bhm(r.transitSecs):'<span style="color:#c5cce0">—</span>')+'</td>'+
|
| 653 |
'<td class="num cnt cmuted">'+(r.inStoreSecs?bhm(r.inStoreSecs):'<span style="color:#c5cce0">—</span>')+'</td>'+
|
711 |
'<td class="num cnt cmuted">'+(r.inStoreSecs?bhm(r.inStoreSecs):'<span style="color:#c5cce0">—</span>')+'</td>'+
|
| 654 |
'<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
|
712 |
'<td><span class="pill-state '+stateClass(r.state)+'">'+esc(r.stateLabel)+'</span></td>'+
|
| - |
|
713 |
'<td class="num cnt cmuted">'+daysAgo(r.onboardDays)+'</td>'+
|
| - |
|
714 |
'<td class="num cnt">'+billDaysHtml(r.lastBillDays)+'</td>'+
|
| 655 |
'<td style="white-space:nowrap" onclick="event.stopPropagation()">'+
|
715 |
'<td style="white-space:nowrap" onclick="event.stopPropagation()">'+
|
| 656 |
'<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">📍</button>'+
|
716 |
'<button class="actbtn map" title="Live location" onclick="openExec('+A+',\'map\')">📍</button>'+
|
| 657 |
'<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">👁</button>'+
|
717 |
'<button class="actbtn view" title="Journey detail" onclick="openExec('+A+')">👁</button>'+
|
| 658 |
(CAN_EDIT_BEAT
|
718 |
(CAN_EDIT_BEAT
|
| 659 |
? '<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">✎</button>'+
|
719 |
? '<button class="actbtn edit" title="Edit beat in Beat Planner" onclick="openPlanner('+r.authUserId+','+(r.beatId||0)+',\''+date+'\',\'edit\')">✎</button>'+
|
| Line 926... |
Line 986... |
| 926 |
idleStale: {bg:'#fff4e6', fg:'#e8590c', ic:'⏱', title:'Idle — off-store > 59 min', sub:'stationary >20 m from any store', cls:'warnflag'},
|
986 |
idleStale: {bg:'#fff4e6', fg:'#e8590c', ic:'⏱', title:'Idle — off-store > 59 min', sub:'stationary >20 m from any store', cls:'warnflag'},
|
| 927 |
geofence: {bg:'#fff0f0', fg:'#e03131', ic:'📍', title:'Check-in outside store geofence', sub:'>50 m from partner\'s saved location', cls:'badflag'},
|
987 |
geofence: {bg:'#fff0f0', fg:'#e03131', ic:'📍', title:'Check-in outside store geofence', sub:'>50 m from partner\'s saved location', cls:'badflag'},
|
| 928 |
locationOff: {bg:'#fff4e6', fg:'#e8590c', ic:'📴', title:'Location turned off mid-journey', sub:'GPS toggled off', cls:'warnflag'},
|
988 |
locationOff: {bg:'#fff4e6', fg:'#e8590c', ic:'📴', title:'Location turned off mid-journey', sub:'GPS toggled off', cls:'warnflag'},
|
| 929 |
deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'↻', title:'Deferred, awaiting re-action', sub:'shops deferred today', cls:''}
|
989 |
deferredAwaiting:{bg:'#eef1f6', fg:'#69748c', ic:'↻', title:'Deferred, awaiting re-action', sub:'shops deferred today', cls:''}
|
| 930 |
};
|
990 |
};
|
| 931 |
// ---- Visit-quality flags: a today-scoped replica of the 14-row monitor table ----
|
991 |
// ---- Billing: today's orders, filterable by the executive's org level (L1..L5) ----
|
| 932 |
// higher rate = worse, so redder as the flagged share climbs.
|
992 |
window._ordersLevel = window._ordersLevel || ''; // '' = all levels
|
| 933 |
function flagRateColor(p){ if(p>=50)return '#e03131'; if(p>=25)return '#e8590c'; if(p>=10)return '#f08c00'; return '#2f9e44'; }
|
993 |
function setOrdersLevel(L){ window._ordersLevel = (window._ordersLevel===L)?'':L; var c=el('bj-orders'); if(c) c.innerHTML=ordersCard(); }
|
| - |
|
994 |
// Compact Indian-currency: ₹7.8L / ₹1.2Cr / ₹9.5k.
|
| - |
|
995 |
function inr(n){ n=Math.round(n||0);
|
| - |
|
996 |
if(n>=10000000) return '₹'+(n/10000000).toFixed(2).replace(/\.00$/,'')+'Cr';
|
| - |
|
997 |
if(n>=100000) return '₹'+(n/100000).toFixed(2).replace(/\.00$/,'')+'L';
|
| - |
|
998 |
if(n>=1000) return '₹'+(n/1000).toFixed(1).replace(/\.0$/,'')+'k';
|
| - |
|
999 |
return '₹'+n;
|
| - |
|
1000 |
}
|
| - |
|
1001 |
// Today's orders grouped by partner (respecting the level filter), sorted by ₹ total.
|
| - |
|
1002 |
function orderGroups(){
|
| - |
|
1003 |
var list=window._ordersList||[];
|
| - |
|
1004 |
var view = window._ordersLevel ? list.filter(function(o){return o.level===window._ordersLevel;}) : list;
|
| - |
|
1005 |
var groups={}, order=[];
|
| - |
|
1006 |
view.forEach(function(o){
|
| - |
|
1007 |
var k = o.fofoId || ('p:'+o.exec+'|'+o.partner);
|
| - |
|
1008 |
if(!groups[k]){ groups[k]={exec:o.exec,level:o.level,partner:o.partner,fofoId:o.fofoId,count:0,total:0,last:''}; order.push(k); }
|
| - |
|
1009 |
var g=groups[k]; g.count++; g.total+=(o.amount||0);
|
| - |
|
1010 |
if((o.time||'')>g.last) g.last=o.time||'';
|
| - |
|
1011 |
});
|
| - |
|
1012 |
var glist=order.map(function(k){return groups[k];}).sort(function(a,b){return b.total-a.total;});
|
| - |
|
1013 |
return {glist:glist, view:view, total:view.reduce(function(a,o){return a+(o.amount||0);},0)};
|
| - |
|
1014 |
}
|
| - |
|
1015 |
function ordersCard(){
|
| - |
|
1016 |
var levels=['','L1','L2','L3','L4','L5'];
|
| - |
|
1017 |
var chips=levels.map(function(L){
|
| - |
|
1018 |
var on=(window._ordersLevel===L);
|
| - |
|
1019 |
return '<button class="olchip'+(on?' on':'')+'" onclick="setOrdersLevel(\''+L+'\')">'+(L||'All')+'</button>';
|
| - |
|
1020 |
}).join('');
|
| - |
|
1021 |
var h='<div class="card"><div class="olhead"><h2>Orders today</h2><div class="olchips">'+chips+'</div></div>';
|
| - |
|
1022 |
if(!(window._ordersList||[]).length) return h+'<p class="desc">No orders placed today.</p></div>';
|
| - |
|
1023 |
var g=orderGroups(), glist=g.glist;
|
| - |
|
1024 |
if(!glist.length) return h+'<p class="desc">No orders at '+esc(window._ordersLevel)+' today.</p></div>';
|
| - |
|
1025 |
var top=glist.slice(0,3);
|
| - |
|
1026 |
h+='<div class="fsummary">'+
|
| - |
|
1027 |
'<div class="fstat"><div class="fn">'+inr(g.total)+'</div><div class="fl">Billed today</div></div>'+
|
| - |
|
1028 |
'<div class="fstat sep"><div class="fn">'+g.view.length+'</div><div class="fl">Orders</div></div>'+
|
| - |
|
1029 |
'<div class="fstat sep"><div class="fn">'+glist.length+'</div><div class="fl">Partners billed</div></div>'+
|
| - |
|
1030 |
'</div>';
|
| - |
|
1031 |
h+='<div class="fheroes">'; top.forEach(function(gr){ h+=orderHero(gr, g.total); }); h+='</div>';
|
| - |
|
1032 |
h+='<p class="desc" style="margin-top:12px"><a href="#" onclick="openOrdersTable();return false;">View all '+glist.length+' billed partners →</a></p>';
|
| - |
|
1033 |
return h+'</div>';
|
| - |
|
1034 |
}
|
| - |
|
1035 |
function orderHero(g, grand){
|
| - |
|
1036 |
var pct = grand>0 ? Math.round(100*g.total/grand) : 0, c='var(--brand)';
|
| - |
|
1037 |
return '<div class="fhero clickable" onclick="openOrdersTable()" style="--fc:'+c+'">'+
|
| - |
|
1038 |
'<div class="fhtop"><div class="fhname">'+esc(g.partner)+'</div>'+
|
| - |
|
1039 |
'<span class="fring" style="background:conic-gradient('+c+' '+(pct*3.6)+'deg, var(--line2) 0)"><span class="frlbl" style="color:'+c+'">'+pct+'%</span></span></div>'+
|
| - |
|
1040 |
'<div class="fhbig" style="color:'+c+'">'+inr(g.total)+'</div>'+
|
| - |
|
1041 |
'<div class="fhsub">'+g.count+' order'+(g.count>1?'s':'')+' · <span class="lvl">'+esc(g.level||'-')+'</span> '+esc(g.exec)+'</div>'+
|
| - |
|
1042 |
'</div>';
|
| - |
|
1043 |
}
|
| - |
|
1044 |
// Full grouped orders table in a popup; each partner row opens its order detail.
|
| - |
|
1045 |
function openOrdersTable(){
|
| - |
|
1046 |
var g=orderGroups(), glist=g.glist, date=window._boardDate||'';
|
| - |
|
1047 |
var h='<button class="x" onclick="closeModal()">×</button><h3>Orders today</h3>'+
|
| - |
|
1048 |
'<p class="desc">'+g.view.length+' orders from '+glist.length+' partners · '+inr(g.total)+(window._ordersLevel?' · '+esc(window._ordersLevel)+' only':'')+' · click a partner for its orders</p>'+
|
| - |
|
1049 |
'<div style="overflow:auto;max-height:64vh"><table><thead><tr><th>Executive</th><th>Partner</th><th class="num">Orders</th><th class="num">Amount</th><th class="num">Last</th></tr></thead><tbody>';
|
| - |
|
1050 |
glist.forEach(function(gr){
|
| - |
|
1051 |
var go = gr.fofoId ? ' class="clickable" onclick="bjOpenOrderList('+gr.fofoId+",'"+date+"')\"" : '';
|
| - |
|
1052 |
h+='<tr'+go+'><td><span class="lvl">'+esc(gr.level||'-')+'</span> <b>'+esc(gr.exec)+'</b></td>'+
|
| - |
|
1053 |
'<td>'+esc(gr.partner)+'</td>'+
|
| - |
|
1054 |
'<td class="num cnt cmuted">'+gr.count+'</td>'+
|
| - |
|
1055 |
'<td class="num cnt">₹'+num(Math.round(gr.total))+'</td>'+
|
| - |
|
1056 |
'<td class="num cmuted">'+esc((gr.last||'').slice(0,5))+'</td></tr>';
|
| - |
|
1057 |
});
|
| - |
|
1058 |
h+='</tbody></table></div>';
|
| - |
|
1059 |
el('bj-modal').innerHTML=h; el('bj-modal-back').classList.add('show');
|
| - |
|
1060 |
}
|
| - |
|
1061 |
|
| - |
|
1062 |
// ---- Visit-quality flags: "top offenders" panel (severity-first, dashboard palette) ----
|
| - |
|
1063 |
// Internal tuning status -> display severity {label, color}, using the dashboard's own tokens.
|
| 934 |
function flagStatusColor(s){
|
1064 |
function flagSeverity(status){
|
| 935 |
switch(s){
|
1065 |
switch(status){
|
| 936 |
case 'action': return '#e03131';
|
1066 |
case 'action': return {label:'Action', color:'var(--red)'};
|
| 937 |
case 'systemic': return '#e8590c';
|
1067 |
case 'systemic': return {label:'High', color:'var(--orange)'};
|
| 938 |
case 'partial': return '#f08c00';
|
1068 |
case 'partial': return {label:'High', color:'var(--orange)'};
|
| 939 |
case 'needs floor': return '#1098ad';
|
1069 |
case 'needs floor': return {label:'Watch', color:'var(--cyan)'};
|
| 940 |
case 'too broad': return '#7048e8';
|
1070 |
case 'too broad': return {label:'Monitor', color:'var(--violet)'};
|
| 941 |
case 'contained': return '#2f9e44';
|
1071 |
case 'contained': return {label:'Monitor', color:'var(--violet)'};
|
| 942 |
default: return '#97a1b5'; // "never fires" / "no data"
|
1072 |
default: return {label:'No data', color:'var(--soft)'}; // no data / never fires
|
| 943 |
}
|
1073 |
}
|
| 944 |
}
|
1074 |
}
|
| - |
|
1075 |
function flagStatusColor(s){ return flagSeverity(s).color; }
|
| 945 |
function flagStatusChip(s){
|
1076 |
function flagStatusChip(s){
|
| 946 |
var c=flagStatusColor(s);
|
1077 |
var v=flagSeverity(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>';
|
1078 |
return '<span class="fpill" style="color:'+v.color+';background:color-mix(in srgb,'+v.color+' 13%,transparent)"><i style="background:'+v.color+'"></i>'+esc(v.label)+'</span>';
|
| - |
|
1079 |
}
|
| - |
|
1080 |
function frate(r){ return r.denom>0 ? Math.round(100*r.count/r.denom) : 0; }
|
| - |
|
1081 |
function ffiring(r){ return (r.count||0)>0; }
|
| - |
|
1082 |
function flagRing(r){
|
| - |
|
1083 |
var p=frate(r), c=flagStatusColor(r.status);
|
| - |
|
1084 |
return '<span class="fring" style="background:conic-gradient('+c+' '+(p*3.6)+'deg, var(--line2) 0)"><span class="frlbl" style="color:'+c+'">'+p+'%</span></span>';
|
| - |
|
1085 |
}
|
| - |
|
1086 |
function flagHero(r){
|
| - |
|
1087 |
var c=flagStatusColor(r.status);
|
| - |
|
1088 |
return '<div class="fhero clickable" onclick="openFlag(\''+r.code+'\')" style="--fc:'+c+'">'+
|
| - |
|
1089 |
'<div class="fhtop"><div class="fhname">'+esc(r.title)+'</div>'+flagRing(r)+'</div>'+
|
| - |
|
1090 |
'<div class="fhbig" style="color:'+c+'">'+num(r.count)+'</div>'+
|
| - |
|
1091 |
'<div class="fhsub">of '+num(r.denom)+' · '+flagStatusChip(r.status)+'</div>'+
|
| - |
|
1092 |
'</div>';
|
| - |
|
1093 |
}
|
| - |
|
1094 |
function flagRow(r, quiet){
|
| - |
|
1095 |
var c=flagStatusColor(r.status), p=frate(r);
|
| - |
|
1096 |
var attr = quiet ? ' class="frow quiet"' : ' class="frow clickable" onclick="openFlag(\''+r.code+'\')"';
|
| - |
|
1097 |
return '<div'+attr+'>'+
|
| - |
|
1098 |
'<div class="fnum">'+esc(r.num)+'</div>'+
|
| - |
|
1099 |
'<div class="frname">'+esc(r.title)+'</div>'+
|
| - |
|
1100 |
'<div class="frrate" style="color:'+(ffiring(r)?c:'var(--soft)')+'">'+(r.denom?p+'%':'—')+'</div>'+
|
| - |
|
1101 |
'<div class="frfrac">'+(r.denom?num(r.count)+' / '+num(r.denom):'—')+'</div>'+
|
| - |
|
1102 |
'<div>'+flagStatusChip(r.status)+'</div>'+
|
| - |
|
1103 |
'</div>';
|
| 948 |
}
|
1104 |
}
|
| 949 |
function flagsTable(list){
|
1105 |
function flagsTable(list){
|
| 950 |
if(!list||!list.length) return '<p class="desc">No flag data for this date.</p>';
|
1106 |
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>';
|
1107 |
var fires=list.filter(ffiring);
|
| 954 |
list.forEach(function(r){
|
1108 |
var flagged=list.reduce(function(a,r){return a+(r.count||0);},0);
|
| 955 |
var hits=r.count||0, denom=r.denom||0, rate=pct(hits,denom);
|
1109 |
var top=fires.slice().sort(function(a,b){return (b.count||0)-(a.count||0);}).slice(0,3);
|
| 956 |
var open = hits>0 ? ' class="clickable" onclick="openFlag(\''+r.code+'\')"' : '';
|
1110 |
var topK={}; top.forEach(function(r){topK[r.code]=1;});
|
| 957 |
var cntColor = hits>0 ? flagRateColor(rate) : '#97a1b5';
|
1111 |
var rest=fires.filter(function(r){return !topK[r.code];}).sort(function(a,b){return frate(b)-frate(a);});
|
| 958 |
h+='<tr'+open+'>'+
|
1112 |
var quiet=list.filter(function(r){return !ffiring(r);});
|
| 959 |
'<td style="font-weight:700;color:#97a1b5">'+esc(r.num)+'</td>'+
|
1113 |
var worst=fires.slice().sort(function(a,b){return frate(b)-frate(a);})[0];
|
| - |
|
1114 |
|
| 960 |
'<td>'+esc(r.title)+'</td>'+
|
1115 |
var h='<div class="fsummary">'+
|
| 961 |
'<td class="num cnt" style="font-weight:700;color:'+cntColor+'">'+num(hits)+'</td>'+
|
1116 |
'<div class="fstat"><div class="fn">'+num(flagged)+'</div><div class="fl">Flagged visits today</div></div>'+
|
| 962 |
'<td class="num cnt cmuted">'+num(denom)+'</td>'+
|
- |
|
| 963 |
'<td class="num cnt cmuted">'+(denom?rate+'%':'—')+'</td>'+
|
1117 |
'<div class="fstat sep"><div class="fn">'+fires.length+' <small>/ '+list.length+'</small></div><div class="fl">Rules firing</div></div>'+
|
| 964 |
'<td>'+flagStatusChip(r.status)+(hits>0?' <span class="chev">▸</span>':'')+'</td>'+
|
1118 |
(worst?'<div class="fstat sep"><div class="ftop"><div class="fn" style="color:'+flagStatusColor(worst.status)+'">'+frate(worst)+'%</div>'+flagStatusChip(worst.status)+'</div><div class="fl">Worst rule · #'+esc(worst.num)+' '+esc(worst.title)+'</div></div>':'')+
|
| 965 |
'</tr>';
|
1119 |
'</div>';
|
| 966 |
});
|
1120 |
|
| - |
|
1121 |
h+='<div class="fheroes">'; top.forEach(function(r){ h+=flagHero(r); }); h+='</div>';
|
| - |
|
1122 |
if(rest.length){ h+='<div class="fsub">Other firing rules</div><div class="frows">'; rest.forEach(function(r){ h+=flagRow(r,false); }); h+='</div>'; }
|
| - |
|
1123 |
if(quiet.length){ h+='<div class="fsub muted">Quiet today</div><div class="frows">'; quiet.forEach(function(r){ h+=flagRow(r,true); }); h+='</div>'; }
|
| 967 |
return h+'</tbody></table></div>';
|
1124 |
return h;
|
| 968 |
}
|
1125 |
}
|
| 969 |
// Minutes-of-day from the first HH:MM in a flag detail string (-1 if none) — for sorting.
|
1126 |
// Minutes-of-day from the first HH:MM in a flag detail string (-1 if none) — for sorting.
|
| 970 |
function flagTimeVal(detail){
|
1127 |
function flagTimeVal(detail){
|
| 971 |
var m=String(detail||'').match(/(\d{1,2}):(\d{2})/);
|
1128 |
var m=String(detail||'').match(/(\d{1,2}):(\d{2})/);
|
| 972 |
return m ? (parseInt(m[1],10)*60+parseInt(m[2],10)) : -1;
|
1129 |
return m ? (parseInt(m[1],10)*60+parseInt(m[2],10)) : -1;
|
| Line 986... |
Line 1143... |
| 986 |
if(!items.length){ h+='<p class="desc">Nothing flagged today.</p>'; }
|
1143 |
if(!items.length){ h+='<p class="desc">Nothing flagged today.</p>'; }
|
| 987 |
else{
|
1144 |
else{
|
| 988 |
h+='<p class="desc">Click a row to open that executive\'s journey detail.</p><table><tbody>';
|
1145 |
h+='<p class="desc">Click a row to open that executive\'s journey detail.</p><table><tbody>';
|
| 989 |
items.forEach(function(x){
|
1146 |
items.forEach(function(x){
|
| 990 |
var canOpen=x.userId>0;
|
1147 |
var canOpen=x.userId>0;
|
| 991 |
var go=canOpen?' class="clickable" onclick="openExec('+x.userId+",'"+js(x.name)+"','"+date+"')\"":'';
|
1148 |
var go=canOpen?' class="clickable" onclick="openExec('+x.userId+",'"+js(x.name)+"','"+date+"','timeline','flag:"+r.code+"')\"":'';
|
| 992 |
h+='<tr'+go+'><td style="font-weight:600;white-space:nowrap">'+esc(x.name)+'</td>'+
|
1149 |
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>';
|
1150 |
'<td style="color:#69748c">'+esc(x.detail||'')+'</td></tr>';
|
| 994 |
});
|
1151 |
});
|
| 995 |
h+='</tbody></table>';
|
1152 |
h+='</tbody></table>';
|
| 996 |
}
|
1153 |
}
|
| Line 1023... |
Line 1180... |
| 1023 |
'<td style="color:#69748c">'+esc(item?item.detail:'')+'</td></tr>';
|
1180 |
'<td style="color:#69748c">'+esc(item?item.detail:'')+'</td></tr>';
|
| 1024 |
});
|
1181 |
});
|
| 1025 |
h+='</tbody></table>';
|
1182 |
h+='</tbody></table>';
|
| 1026 |
}
|
1183 |
}
|
| 1027 |
if(userId>0){
|
1184 |
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 →</a></p>';
|
1185 |
h+='<p class="desc" style="margin-top:10px"><a href="#" onclick="openExec('+userId+',\''+js(name)+'\',\''+date+'\',\'timeline\',\'execflags\');return false;">Open journey detail →</a></p>';
|
| 1029 |
}
|
1186 |
}
|
| 1030 |
el('bj-modal').innerHTML=h; el('bj-modal-back').classList.add('show');
|
1187 |
el('bj-modal').innerHTML=h; el('bj-modal-back').classList.add('show');
|
| 1031 |
}
|
1188 |
}
|
| 1032 |
function attentionList(a){
|
1189 |
function attentionList(a){
|
| 1033 |
window._att = a;
|
1190 |
window._att = a;
|
| Line 1312... |
Line 1469... |
| 1312 |
'<li><b>Coverage and the manager view</b> follow the existing L1–L5 sales hierarchy, so each manager sees only their team.</li>'+
|
1469 |
'<li><b>Coverage and the manager view</b> follow the existing L1–L5 sales hierarchy, so each manager sees only their team.</li>'+
|
| 1313 |
'</ul></div>';
|
1470 |
'</ul></div>';
|
| 1314 |
}
|
1471 |
}
|
| 1315 |
|
1472 |
|
| 1316 |
/* ============================ EXEC DRILLDOWN ============================ */
|
1473 |
/* ============================ EXEC DRILLDOWN ============================ */
|
| - |
|
1474 |
// Back button shown when this journey detail was opened from another popup (a flag
|
| - |
|
1475 |
// drill-down or the per-exec flag list). Returns to that popup. `back` is a string
|
| - |
|
1476 |
// descriptor: 'flag:<code>' or 'execflags'.
|
| - |
|
1477 |
function execBackBtn(){
|
| - |
|
1478 |
return (window._exec && window._exec.back) ? '<button class="bkbtn" onclick="execBack()">← Back</button>' : '';
|
| - |
|
1479 |
}
|
| - |
|
1480 |
function execBack(){
|
| - |
|
1481 |
var ex=window._exec; if(!ex || !ex.back) return;
|
| - |
|
1482 |
if(ex.back.indexOf('flag:')===0){ openFlag(ex.back.slice(5)); }
|
| - |
|
1483 |
else if(ex.back==='execflags'){ openExecFlags(ex.userId, ex.name); }
|
| - |
|
1484 |
}
|
| 1317 |
function openExec(userId, name, date, tab){
|
1485 |
function openExec(userId, name, date, tab, back){
|
| 1318 |
if(!userId){ return; }
|
1486 |
if(!userId){ return; }
|
| 1319 |
window._exec = {userId:userId, name:name, date:date, detail:null, tab:(tab||'timeline')};
|
1487 |
window._exec = {userId:userId, name:name, date:date, detail:null, tab:(tab||'timeline'), back:(back||null)};
|
| 1320 |
var mb=el('bj-modal-back'), m=el('bj-modal');
|
1488 |
var mb=el('bj-modal-back'), m=el('bj-modal');
|
| 1321 |
m.innerHTML='<button class="x" onclick="closeModal()">×</button><h3>'+esc(name)+'</h3><p class="desc">Journey detail · '+esc(date)+'</p><div class="loading"><div class="spinner"></div>Loading…</div>';
|
1489 |
m.innerHTML=execBackBtn()+'<button class="x" onclick="closeModal()">×</button><h3>'+esc(name)+'</h3><p class="desc">Journey detail · '+esc(date)+'</p><div class="loading"><div class="spinner"></div>Loading…</div>';
|
| 1322 |
mb.classList.add('show');
|
1490 |
mb.classList.add('show');
|
| 1323 |
fetch(CTX+'/beat-journey/journey-detail?userId='+userId+'&date='+encodeURIComponent(date),{headers:{'Accept':'application/json'}})
|
1491 |
fetch(CTX+'/beat-journey/journey-detail?userId='+userId+'&date='+encodeURIComponent(date),{headers:{'Accept':'application/json'}})
|
| 1324 |
.then(function(r){ return r.json(); }).then(function(d){
|
1492 |
.then(function(r){ return r.json(); }).then(function(d){
|
| 1325 |
window._mediaUrl = d.mediaUrl || '';
|
1493 |
window._mediaUrl = d.mediaUrl || '';
|
| 1326 |
window._exec.detail = d;
|
1494 |
window._exec.detail = d;
|
| Line 1328... |
Line 1496... |
| 1328 |
}).catch(function(){ el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">×</button><p class="desc">Could not load detail.</p>'; });
|
1496 |
}).catch(function(){ el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">×</button><p class="desc">Could not load detail.</p>'; });
|
| 1329 |
}
|
1497 |
}
|
| 1330 |
function renderExec(tab){
|
1498 |
function renderExec(tab){
|
| 1331 |
var ex=window._exec; if(!ex) return;
|
1499 |
var ex=window._exec; if(!ex) return;
|
| 1332 |
var d=ex.detail||{};
|
1500 |
var d=ex.detail||{};
|
| 1333 |
var head='<button class="x" onclick="closeModal()">×</button><h3>'+esc(ex.name)+'</h3>'+
|
1501 |
var head=execBackBtn()+'<button class="x" onclick="closeModal()">×</button><h3>'+esc(ex.name)+'</h3>'+
|
| 1334 |
'<p class="desc">Journey detail · '+esc(ex.date)+' · battery '+(d.batteryLevel==null?'-':d.batteryLevel+'%')+' · loc-off '+(d.locationToggleCount||0)+'</p>'+
|
1502 |
'<p class="desc">Journey detail · '+esc(ex.date)+' · battery '+(d.batteryLevel==null?'-':d.batteryLevel+'%')+' · loc-off '+(d.locationToggleCount||0)+'</p>'+
|
| 1335 |
'<div class="exectabs">'+
|
1503 |
'<div class="exectabs">'+
|
| 1336 |
'<button class="exectab'+(tab==='timeline'?' active':'')+'" onclick="renderExec(\'timeline\')">📝 Timeline</button>'+
|
1504 |
'<button class="exectab'+(tab==='timeline'?' active':'')+'" onclick="renderExec(\'timeline\')">📝 Timeline</button>'+
|
| 1337 |
'<button class="exectab'+(tab==='map'?' active':'')+'" onclick="renderExec(\'map\')">📍 Live Location</button>'+
|
1505 |
'<button class="exectab'+(tab==='map'?' active':'')+'" onclick="renderExec(\'map\')">📍 Live Location</button>'+
|
| 1338 |
'</div>';
|
1506 |
'</div>';
|
| Line 1817... |
Line 1985... |
| 1817 |
return out ? '<div class="tlags">'+out+'</div>' : '';
|
1985 |
return out ? '<div class="tlags">'+out+'</div>' : '';
|
| 1818 |
}
|
1986 |
}
|
| 1819 |
function tlRow(x, side){
|
1987 |
function tlRow(x, side){
|
| 1820 |
var s=tlStyle(x);
|
1988 |
var s=tlStyle(x);
|
| 1821 |
var tt=(x.taskType||'').toLowerCase();
|
1989 |
var tt=(x.taskType||'').toLowerCase();
|
| - |
|
1990 |
var mt=(x.markType||'').toUpperCase();
|
| - |
|
1991 |
var isPartner = tt==='franchisee-visit';
|
| - |
|
1992 |
var visited = mt.indexOf('CHECKOUT')>=0;
|
| - |
|
1993 |
var overDwell = !x._punch && visited && toSec(x.timeSpent) > 90*60; // #5 over 1h30m at a store
|
| 1822 |
var timeTop = x.checkInTime ? hhmm(x.checkInTime) : '-';
|
1994 |
var timeTop = x.checkInTime ? hhmm(x.checkInTime) : '-';
|
| 1823 |
var tlabel = x._punch ? (x._punch==='in'?'punch in':'punch out') : (x.checkInTime?'check-in':'pending');
|
1995 |
var tlabel = x._punch ? (x._punch==='in'?'punch in':'punch out') : (x.checkInTime?'check-in':'pending');
|
| 1824 |
var meta;
|
1996 |
var meta;
|
| 1825 |
if(x._punch){
|
1997 |
if(x._punch){
|
| 1826 |
meta = (x._punch==='in'?'Punch in':'Punch out') + ' · ' + hhmm(x.checkInTime);
|
1998 |
meta = (x._punch==='in'?'Punch in':'Punch out') + ' · ' + hhmm(x.checkInTime);
|
| Line 1834... |
Line 2006... |
| 1834 |
var extra='';
|
2006 |
var extra='';
|
| 1835 |
if(!x._punch){
|
2007 |
if(!x._punch){
|
| 1836 |
var chips='';
|
2008 |
var chips='';
|
| 1837 |
if(x.battery!=null) chips += '<span class="tlchip">🔋 '+x.battery+'%</span>';
|
2009 |
if(x.battery!=null) chips += '<span class="tlchip">🔋 '+x.battery+'%</span>';
|
| 1838 |
if(x.locOff!=null) chips += '<span class="tlchip">📴 '+x.locOff+'</span>';
|
2010 |
if(x.locOff!=null) chips += '<span class="tlchip">📴 '+x.locOff+'</span>';
|
| - |
|
2011 |
// over 1h30m at this store — the #5 signal called out on the stop itself.
|
| - |
|
2012 |
if(overDwell) chips += '<span class="tlbadge dwell" title="Over 1h30m at a single store">⏱ '+esc(x.timeSpent)+'</span>';
|
| 1839 |
// orders placed that day (any agenda): one badge -> drawer lists them all.
|
2013 |
// orders placed that day (any agenda): one badge -> drawer lists them all.
|
| - |
|
2014 |
// A visited partner with no order gets a "No billing" badge; a scheduled remark
|
| 1840 |
// No order + billing agenda -> a parsed "scheduled" chip instead.
|
2015 |
// still adds its parsed date chip.
|
| 1841 |
if(x.orders && x.orders.length){
|
2016 |
if(x.orders && x.orders.length){
|
| 1842 |
var n=x.orders.length;
|
2017 |
var n=x.orders.length;
|
| 1843 |
chips += '<button type="button" class="tlbadge ord" title="View orders placed this day"'
|
2018 |
chips += '<button type="button" class="tlbadge ord" title="View orders placed this day"'
|
| 1844 |
+ ' onclick="bjOpenOrderList('+x.taskId+')">🧾 '+n+' order'+(n>1?'s':'')+'</button>';
|
2019 |
+ ' onclick="bjOpenOrderList('+x.taskId+')">🧾 '+n+' order'+(n>1?'s':'')+'</button>';
|
| 1845 |
} else if(x.billingAgenda){
|
2020 |
} else {
|
| 1846 |
var sch = bjScheduledFromRemark(x.remarks);
|
2021 |
if(isPartner && visited) chips += '<span class="tlbadge nobill" title="No order placed on this visit">∅ No billing</span>';
|
| 1847 |
if(sch) chips += '<span class="tlbadge sch" title="Scheduled (from remark)">📅 '+esc(sch)+'</span>';
|
2022 |
if(x.billingAgenda){ var sch = bjScheduledFromRemark(x.remarks); if(sch) chips += '<span class="tlbadge sch" title="Scheduled (from remark)">📅 '+esc(sch)+'</span>'; }
|
| 1848 |
}
|
2023 |
}
|
| 1849 |
var thumb=tlThumb(x.attachment);
|
2024 |
var thumb=tlThumb(x.attachment);
|
| 1850 |
if(thumb || chips) extra = '<div class="tlextra">'+thumb+chips+'</div>';
|
2025 |
if(thumb || chips) extra = '<div class="tlextra">'+thumb+chips+'</div>';
|
| 1851 |
}
|
2026 |
}
|
| 1852 |
var card='<div class="tlcard">'+
|
2027 |
var card='<div class="tlcard'+(overDwell?' long-dwell':'')+'">'+
|
| 1853 |
'<div class="tm" style="background:'+s.c+'">'+timeTop+'<small>'+tlabel+'</small></div>'+
|
2028 |
'<div class="tm" style="background:'+s.c+'">'+timeTop+'<small>'+tlabel+'</small></div>'+
|
| 1854 |
'<div class="bd"><div class="ti">'+esc(tlTitle(x))+'</div>'+
|
2029 |
'<div class="bd"><div class="ti">'+esc(tlTitle(x))+'</div>'+
|
| 1855 |
'<span class="stt" style="background:'+s.c+'">'+s.l+'</span>'+
|
2030 |
'<span class="stt" style="background:'+s.c+'">'+s.l+'</span>'+
|
| 1856 |
(tt==='lead' ? '<span class="stt unplanned">Unplanned</span>' : '')+
|
2031 |
(tt==='lead' ? '<span class="stt unplanned">Unplanned</span>' : '')+
|
| 1857 |
tlAgendas(x)+
|
2032 |
tlAgendas(x)+
|
| Line 1939... |
Line 2114... |
| 1939 |
more.style.display = hasMore ? 'block' : 'none';
|
2114 |
more.style.display = hasMore ? 'block' : 'none';
|
| 1940 |
}
|
2115 |
}
|
| 1941 |
|
2116 |
|
| 1942 |
/* ===================== ORDERS DRAWER ===================== */
|
2117 |
/* ===================== ORDERS DRAWER ===================== */
|
| 1943 |
// All retail orders a partner placed on the visit day, each with its line items.
|
2118 |
// All retail orders a partner placed on the visit day, each with its line items.
|
| 1944 |
function bjOpenOrderList(fofoId){
|
2119 |
function bjOpenOrderList(fofoId, date){
|
| 1945 |
if(!fofoId) return;
|
2120 |
if(!fofoId) return;
|
| 1946 |
var date=(window._exec && window._exec.date) || '';
|
2121 |
date = date || (window._exec && window._exec.date) || window._boardDate || '';
|
| 1947 |
window._od = { fofoId:fofoId, date:date };
|
2122 |
window._od = { fofoId:fofoId, date:date };
|
| 1948 |
el('bj-od-title').innerHTML='🧾 Orders';
|
2123 |
el('bj-od-title').innerHTML='🧾 Orders';
|
| 1949 |
el('bj-od-sub').textContent='Orders placed on '+date;
|
2124 |
el('bj-od-sub').textContent='Orders placed on '+date;
|
| 1950 |
el('bj-od-body').innerHTML='<div class="odloading"><div class="spinner"></div>Loading…</div>';
|
2125 |
el('bj-od-body').innerHTML='<div class="odloading"><div class="spinner"></div>Loading…</div>';
|
| 1951 |
el('bj-od-back').classList.add('show');
|
2126 |
el('bj-od-back').classList.add('show');
|