| Line 135... |
Line 135... |
| 135 |
.hide{display:none}
|
135 |
.hide{display:none}
|
| 136 |
.loading{padding:70px;text-align:center;color:var(--muted)}
|
136 |
.loading{padding:70px;text-align:center;color:var(--muted)}
|
| 137 |
.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}
|
137 |
.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}
|
| 138 |
@keyframes spin{to{transform:rotate(360deg)}}
|
138 |
@keyframes spin{to{transform:rotate(360deg)}}
|
| 139 |
.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}
|
139 |
.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}
|
| - |
|
140 |
/* image lightbox — stacks above the journey modal */
|
| - |
|
141 |
.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}
|
| - |
|
142 |
.img-back.show{display:flex}
|
| - |
|
143 |
.img-back img{max-width:95%;max-height:92vh;border-radius:10px;box-shadow:0 16px 60px rgba(0,0,0,.5);background:#fff}
|
| - |
|
144 |
.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}
|
| - |
|
145 |
.img-x:hover{background:rgba(255,255,255,.3)}
|
| 140 |
.modal-back.show{display:flex}
|
146 |
.modal-back.show{display:flex}
|
| 141 |
.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}
|
147 |
.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}
|
| 142 |
.modal h3{margin:0 0 2px;font-size:17px;text-align:center}
|
148 |
.modal h3{margin:0 0 2px;font-size:17px;text-align:center}
|
| 143 |
.modal > .desc{text-align:center}
|
149 |
.modal > .desc{text-align:center}
|
| 144 |
.modal .x{position:absolute;top:14px;right:16px;cursor:pointer;color:var(--soft);font-size:22px;line-height:1;border:0;background:none}
|
150 |
.modal .x{position:absolute;top:14px;right:16px;cursor:pointer;color:var(--soft);font-size:22px;line-height:1;border:0;background:none}
|
| Line 151... |
Line 157... |
| 151 |
/* journey timeline (exec drilldown) */
|
157 |
/* journey timeline (exec drilldown) */
|
| 152 |
.tl{position:relative;margin:14px 2px 4px;padding:4px 0}
|
158 |
.tl{position:relative;margin:14px 2px 4px;padding:4px 0}
|
| 153 |
.tl::before{content:"";position:absolute;left:50%;top:6px;bottom:6px;width:2px;background:#e9edf5;transform:translateX(-50%)}
|
159 |
.tl::before{content:"";position:absolute;left:50%;top:6px;bottom:6px;width:2px;background:#e9edf5;transform:translateX(-50%)}
|
| 154 |
.tlgap{position:relative;display:flex;justify-content:center;margin:-3px 0}
|
160 |
.tlgap{position:relative;display:flex;justify-content:center;margin:-3px 0}
|
| 155 |
.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}
|
161 |
.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}
|
| 156 |
.tlrow{position:relative;display:flex;margin:14px 0}
|
162 |
.tlrow{position:relative;display:flex;justify-content:space-between;align-items:flex-start;margin:14px 0}
|
| 157 |
.tlrow.left{justify-content:flex-start}
|
- |
|
| 158 |
.tlrow.right{justify-content:flex-end}
|
- |
|
| 159 |
.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}
|
163 |
.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}
|
| 160 |
.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}
|
164 |
.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}
|
| 161 |
.tlrow.left .tlcard{flex-direction:row-reverse;text-align:right}
|
165 |
.tlrow.left .tlcard{flex-direction:row-reverse;text-align:right}
|
| 162 |
.tlrow.right .tlcard{flex-direction:row;text-align:left}
|
166 |
.tlrow.right .tlcard{flex-direction:row;text-align:left}
|
| - |
|
167 |
/* remark on the opposite half, hugging the centre line */
|
| - |
|
168 |
.tlremark{width:calc(50% - 26px);display:flex;align-items:flex-start;padding-top:6px}
|
| - |
|
169 |
.tlrow.right .tlremark{justify-content:flex-end}
|
| - |
|
170 |
.tlrow.left .tlremark{justify-content:flex-start}
|
| - |
|
171 |
.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}
|
| - |
|
172 |
.tlrow.right .tlremark .rk{text-align:right}
|
| - |
|
173 |
.tlrow.left .tlremark .rk{text-align:left}
|
| - |
|
174 |
.tlremark .rkl{font-size:9px;text-transform:uppercase;letter-spacing:.05em;color:var(--soft);font-weight:700;margin-bottom:3px}
|
| 163 |
.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}
|
175 |
.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}
|
| 164 |
.tlcard .tm small{font-weight:600;font-size:8.5px;opacity:.9;text-transform:uppercase;letter-spacing:.04em;margin-top:3px}
|
176 |
.tlcard .tm small{font-weight:600;font-size:8.5px;opacity:.9;text-transform:uppercase;letter-spacing:.04em;margin-top:3px}
|
| 165 |
.tlcard .bd{padding:8px 11px;flex:1;min-width:0}
|
177 |
.tlcard .bd{padding:8px 11px;flex:1;min-width:0}
|
| 166 |
.tlcard .bd .ti{font-size:12.5px;font-weight:700;color:var(--ink);line-height:1.25;word-break:break-word}
|
178 |
.tlcard .bd .ti{font-size:12.5px;font-weight:700;color:var(--ink);line-height:1.25;word-break:break-word}
|
| 167 |
.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}
|
179 |
.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}
|
| Line 174... |
Line 186... |
| 174 |
.tlchip{font-size:10.5px;font-weight:600;color:var(--muted);background:#f1f3f9;border-radius:6px;padding:2px 7px;white-space:nowrap}
|
186 |
.tlchip{font-size:10.5px;font-weight:600;color:var(--muted);background:#f1f3f9;border-radius:6px;padding:2px 7px;white-space:nowrap}
|
| 175 |
.exectabs{display:flex;gap:6px;justify-content:center;margin:12px 0 4px}
|
187 |
.exectabs{display:flex;gap:6px;justify-content:center;margin:12px 0 4px}
|
| 176 |
.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}
|
188 |
.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}
|
| 177 |
.exectab.active{background:var(--brand);color:#fff;border-color:var(--brand)}
|
189 |
.exectab.active{background:var(--brand);color:#fff;border-color:var(--brand)}
|
| 178 |
#bjMap{width:100%;height:460px;border-radius:12px;border:1px solid var(--line);margin-top:10px;background:#eef1f7;overflow:hidden}
|
190 |
#bjMap{width:100%;height:460px;border-radius:12px;border:1px solid var(--line);margin-top:10px;background:#eef1f7;overflow:hidden}
|
| - |
|
191 |
.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}
|
| - |
|
192 |
.bjlegend .lg{font-weight:700}
|
| - |
|
193 |
.bjlegend .sw{display:inline-block;width:24px;vertical-align:middle;margin-right:7px}
|
| - |
|
194 |
.bjlegend .ln{border-top:4px solid #e03131}
|
| - |
|
195 |
.bjlegend .dt{border-top:4px dotted #2f9e44}
|
| 179 |
.valwrap{display:flex;justify-content:center;margin:14px 0 6px}
|
196 |
.valwrap{display:flex;justify-content:center;margin:14px 0 6px}
|
| 180 |
.valimg{max-width:100%;max-height:440px;border-radius:12px;border:1px solid var(--line);cursor:zoom-in}
|
197 |
.valimg{max-width:100%;max-height:440px;border-radius:12px;border:1px solid var(--line);cursor:zoom-in}
|
| 181 |
.valph{width:100%;padding:48px;text-align:center;color:var(--soft);border:1px dashed #cfd6e6;border-radius:12px;background:#f4f6fb;font-size:14px}
|
198 |
.valph{width:100%;padding:48px;text-align:center;color:var(--soft);border:1px dashed #cfd6e6;border-radius:12px;background:#f4f6fb;font-size:14px}
|
| 182 |
.valactions{display:flex;justify-content:center;gap:10px;margin-top:8px}
|
199 |
.valactions{display:flex;justify-content:center;gap:10px;margin-top:8px}
|
| 183 |
@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}}
|
200 |
@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}}
|
| 184 |
@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}}
|
201 |
@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}}
|
| 185 |
</style>
|
202 |
</style>
|
| 186 |
</head>
|
203 |
</head>
|
| 187 |
<body>
|
204 |
<body>
|
| 188 |
<div class="wrap">
|
205 |
<div class="wrap">
|
| 189 |
<header class="top">
|
206 |
<header class="top">
|
| Line 207... |
Line 224... |
| 207 |
<span id="bj-period-range" class="hide">
|
224 |
<span id="bj-period-range" class="hide">
|
| 208 |
<input class="ctl" type="date" id="bj-start" value="$monthStart">
|
225 |
<input class="ctl" type="date" id="bj-start" value="$monthStart">
|
| 209 |
<input class="ctl" type="date" id="bj-end" value="$today">
|
226 |
<input class="ctl" type="date" id="bj-end" value="$today">
|
| 210 |
</span>
|
227 |
</span>
|
| 211 |
<button class="btn-r" id="bj-apply">Apply</button>
|
228 |
<button class="btn-r" id="bj-apply">Apply</button>
|
| - |
|
229 |
<button class="btn-r" id="bj-export" style="background:#2f9e44" title="Download an Excel report for the selected date / range">⬇ Export Excel</button>
|
| 212 |
</div>
|
230 |
</div>
|
| 213 |
</header>
|
231 |
</header>
|
| 214 |
|
232 |
|
| 215 |
<div class="lenses">
|
233 |
<div class="lenses">
|
| 216 |
<button class="lens active" data-lens="today" id="lens-today">Today (Live)</button>
|
234 |
<button class="lens active" data-lens="today" id="lens-today">Today (Live)</button>
|
| Line 222... |
Line 240... |
| 222 |
|
240 |
|
| 223 |
<div class="modal-back" id="bj-modal-back">
|
241 |
<div class="modal-back" id="bj-modal-back">
|
| 224 |
<div class="modal" id="bj-modal"></div>
|
242 |
<div class="modal" id="bj-modal"></div>
|
| 225 |
</div>
|
243 |
</div>
|
| 226 |
|
244 |
|
| - |
|
245 |
<div class="img-back" id="bj-img-back">
|
| - |
|
246 |
<button class="img-x" id="bj-img-x" title="Close">×</button>
|
| - |
|
247 |
<img id="bj-img" alt="Attachment">
|
| - |
|
248 |
</div>
|
| - |
|
249 |
|
| 227 |
<script>
|
250 |
<script>
|
| 228 |
var CTX = '${rc.contextPath}';
|
251 |
var CTX = '${rc.contextPath}';
|
| 229 |
var BASE = CTX + '/beat-journey';
|
252 |
var BASE = CTX + '/beat-journey';
|
| 230 |
var LENS = 'today';
|
253 |
var LENS = 'today';
|
| 231 |
var TODAY = '$today';
|
254 |
var TODAY = '$today';
|
| Line 429... |
Line 452... |
| 429 |
function openValidate(trackingId){
|
452 |
function openValidate(trackingId){
|
| 430 |
var list=(window._att&&window._att.geofence)||[], item=null;
|
453 |
var list=(window._att&&window._att.geofence)||[], item=null;
|
| 431 |
for(var i=0;i<list.length;i++){ if(list[i].trackingId===trackingId){ item=list[i]; break; } }
|
454 |
for(var i=0;i<list.length;i++){ if(list[i].trackingId===trackingId){ item=list[i]; break; } }
|
| 432 |
if(!item){ return; }
|
455 |
if(!item){ return; }
|
| 433 |
var img = (item.attachment && window._mediaUrl)
|
456 |
var img = (item.attachment && window._mediaUrl)
|
| 434 |
? '<img class="valimg" src="'+esc(window._mediaUrl+item.attachment)+'" onclick="window.open(this.src,\'_blank\')" onerror="this.outerHTML=\'<div class="valph">📷 Photo unavailable</div>\'">'
|
457 |
? '<img class="valimg" src="'+esc(window._mediaUrl+item.attachment)+'" onclick="bjOpenImage(this.src)" onerror="this.outerHTML=\'<div class="valph">📷 Photo unavailable</div>\'">'
|
| 435 |
: '<div class="valph">📷 No photo on this check-in</div>';
|
458 |
: '<div class="valph">📷 No photo on this check-in</div>';
|
| 436 |
el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">×</button><h3>Validate check-in</h3>'+
|
459 |
el('bj-modal').innerHTML='<button class="x" onclick="closeModal()">×</button><h3>Validate check-in</h3>'+
|
| 437 |
'<p class="desc">'+esc(item.name)+' · '+esc(item.detail||'')+'</p>'+
|
460 |
'<p class="desc">'+esc(item.name)+' · '+esc(item.detail||'')+'</p>'+
|
| 438 |
'<div class="valwrap">'+img+'</div>'+
|
461 |
'<div class="valwrap">'+img+'</div>'+
|
| 439 |
'<div class="valactions">'+
|
462 |
'<div class="valactions">'+
|
| Line 632... |
Line 655... |
| 632 |
'<div class="exectabs">'+
|
655 |
'<div class="exectabs">'+
|
| 633 |
'<button class="exectab'+(tab==='timeline'?' active':'')+'" onclick="renderExec(\'timeline\')">📝 Timeline</button>'+
|
656 |
'<button class="exectab'+(tab==='timeline'?' active':'')+'" onclick="renderExec(\'timeline\')">📝 Timeline</button>'+
|
| 634 |
'<button class="exectab'+(tab==='map'?' active':'')+'" onclick="renderExec(\'map\')">📍 Live Location</button>'+
|
657 |
'<button class="exectab'+(tab==='map'?' active':'')+'" onclick="renderExec(\'map\')">📍 Live Location</button>'+
|
| 635 |
'</div>';
|
658 |
'</div>';
|
| 636 |
if(tab==='map'){
|
659 |
if(tab==='map'){
|
| - |
|
660 |
el('bj-modal').innerHTML=head+'<div id="bjRouteLegend" class="bjlegend"></div>'+
|
| 637 |
el('bj-modal').innerHTML=head+'<div id="bjMap"><div class="loading"><div class="spinner"></div>Loading map…</div></div>';
|
661 |
'<div id="bjMap"><div class="loading"><div class="spinner"></div>Loading map…</div></div>';
|
| 638 |
bjShowMap(ex.userId, ex.date);
|
662 |
bjShowMap(ex.userId, ex.date);
|
| 639 |
} else {
|
663 |
} else {
|
| 640 |
el('bj-modal').innerHTML=head+buildTimelineHtml(d, ex.date);
|
664 |
el('bj-modal').innerHTML=head+buildTimelineHtml(d, ex.date);
|
| 641 |
}
|
665 |
}
|
| 642 |
}
|
666 |
}
|
| Line 652... |
Line 676... |
| 652 |
else if(x.checkInTime){ visited.push(x); }
|
676 |
else if(x.checkInTime){ visited.push(x); }
|
| 653 |
else { pending.push(x); }
|
677 |
else { pending.push(x); }
|
| 654 |
});
|
678 |
});
|
| 655 |
visited.sort(function(a,b){ return tlSecs(a.checkInTime)-tlSecs(b.checkInTime); });
|
679 |
visited.sort(function(a,b){ return tlSecs(a.checkInTime)-tlSecs(b.checkInTime); });
|
| 656 |
|
680 |
|
| 657 |
var distKm=0, inStoreSec=0, transitSec=0, leadsN=0;
|
681 |
var distKm=0, inStoreSec=0, leadsN=0;
|
| 658 |
all.forEach(function(x){
|
682 |
all.forEach(function(x){
|
| 659 |
var tt=(x.taskType||'').toLowerCase();
|
683 |
var tt=(x.taskType||'').toLowerCase();
|
| 660 |
if(tt==='attendance') return;
|
684 |
if(tt==='attendance') return;
|
| 661 |
if(x.totalDistance) distKm += (parseFloat(x.totalDistance)||0);
|
685 |
if(x.totalDistance) distKm += (parseFloat(x.totalDistance)||0);
|
| 662 |
inStoreSec += toSec(x.timeSpent);
|
686 |
inStoreSec += toSec(x.timeSpent);
|
| 663 |
transitSec += toSec(x.transitTime);
|
- |
|
| 664 |
if(tt==='lead') leadsN++;
|
687 |
if(tt==='lead') leadsN++;
|
| 665 |
});
|
688 |
});
|
| 666 |
var workSec = (att && att.checkInTime && att.checkOutTime) ? (toSec(att.checkOutTime)-toSec(att.checkInTime)) : (att?toSec(att.timeSpent):0);
|
689 |
var workSec = (att && att.checkInTime && att.checkOutTime) ? (toSec(att.checkOutTime)-toSec(att.checkInTime)) : (att?toSec(att.timeSpent):0);
|
| 667 |
if(workSec<0) workSec=0;
|
690 |
if(workSec<0) workSec=0;
|
| - |
|
691 |
// Travel = working − in-store, so In-store + Travel = Working hours exactly
|
| - |
|
692 |
// (summing per-leg transit under-counts the idle/waiting between stops).
|
| - |
|
693 |
var transitSec = Math.max(0, workSec - inStoreSec);
|
| 668 |
|
694 |
|
| 669 |
var seq=[];
|
695 |
var seq=[];
|
| 670 |
if(att && att.checkInTime) seq.push(punchEntry('in', att.checkInTime));
|
696 |
if(att && att.checkInTime) seq.push(punchEntry('in', att.checkInTime));
|
| 671 |
visited.forEach(function(x){ seq.push(x); });
|
697 |
visited.forEach(function(x){ seq.push(x); });
|
| 672 |
pending.forEach(function(x){ seq.push(x); });
|
698 |
pending.forEach(function(x){ seq.push(x); });
|
| Line 760... |
Line 786... |
| 760 |
var m=new google.maps.Marker({position:pos,map:map,zIndex:zi||100,icon:icon,title:title||''});
|
786 |
var m=new google.maps.Marker({position:pos,map:map,zIndex:zi||100,icon:icon,title:title||''});
|
| 761 |
if(info){ var iw=new google.maps.InfoWindow({content:info}); m.addListener('click',function(){ iw.open(map,m); }); }
|
787 |
if(info){ var iw=new google.maps.InfoWindow({content:info}); m.addListener('click',function(){ iw.open(map,m); }); }
|
| 762 |
return m;
|
788 |
return m;
|
| 763 |
}
|
789 |
}
|
| 764 |
|
790 |
|
| 765 |
// 1) GPS trail + last-known location (same bike/rider icon as Beat Report)
|
791 |
// 1) last-known location marker (the GPS trail itself is drawn as the "taken"
|
| - |
|
792 |
// route in bjDrawRoutes, straight from location_tracking_path).
|
| 766 |
var pts=(data.path||[]).map(function(p){ return {lat:p.lat,lng:p.lng}; });
|
793 |
var pts=(data.path||[]).map(function(p){ return {lat:p.lat,lng:p.lng}; });
|
| 767 |
if(pts.length){
|
794 |
if(pts.length){
|
| 768 |
new google.maps.Polyline({path:pts,geodesic:true,strokeColor:'#e03131',strokeOpacity:.7,strokeWeight:3,zIndex:40,map:map});
|
- |
|
| 769 |
pts.forEach(function(p){ bounds.extend(p); has=true; });
|
795 |
pts.forEach(function(p){ bounds.extend(p); has=true; });
|
| 770 |
var lastPt=pts[pts.length-1];
|
796 |
var lastPt=pts[pts.length-1];
|
| 771 |
var lk=new google.maps.Marker({position:lastPt,map:map,zIndex:999,title:'Last Known Location',
|
797 |
var lk=new google.maps.Marker({position:lastPt,map:map,zIndex:999,title:'Last Known Location',
|
| 772 |
icon:{url:CTX+'/resources/images/salesman.png',scaledSize:new google.maps.Size(40,40),anchor:new google.maps.Point(20,20)}});
|
798 |
icon:{url:CTX+'/resources/images/salesman.png',scaledSize:new google.maps.Size(40,40),anchor:new google.maps.Point(20,20)}});
|
| 773 |
var lkIw=new google.maps.InfoWindow({content:'<div style="font-size:12px"><b>📍 Last known location</b></div>'});
|
799 |
var lkIw=new google.maps.InfoWindow({content:'<div style="font-size:12px"><b>📍 Last known location</b></div>'});
|
| Line 799... |
Line 825... |
| 799 |
pin({lat:data.punchOut.lat,lng:data.punchOut.lng}, bjPin('#7048e8','X'), 300,
|
825 |
pin({lat:data.punchOut.lat,lng:data.punchOut.lng}, bjPin('#7048e8','X'), 300,
|
| 800 |
'Punch out'+(data.punchOut.time?' · '+data.punchOut.time:''),
|
826 |
'Punch out'+(data.punchOut.time?' · '+data.punchOut.time:''),
|
| 801 |
'<div style="font-size:12px"><b>⚑ Punch out</b>'+(data.punchOut.time?'<br>'+esc(data.punchOut.time):'')+'</div>');
|
827 |
'<div style="font-size:12px"><b>⚑ Punch out</b>'+(data.punchOut.time?'<br>'+esc(data.punchOut.time):'')+'</div>');
|
| 802 |
}
|
828 |
}
|
| 803 |
|
829 |
|
| - |
|
830 |
// 5) routes — actual road taken vs Google-suggested optimal, for comparison.
|
| - |
|
831 |
// Checkpoints in journey order: punch in → visits (by check-in time) → punch out.
|
| - |
|
832 |
var cps=[];
|
| - |
|
833 |
if(data.punchIn) cps.push({lat:data.punchIn.lat,lng:data.punchIn.lng});
|
| - |
|
834 |
(data.visits||[]).slice()
|
| - |
|
835 |
.filter(function(v){ return v.lat && v.lng; })
|
| - |
|
836 |
.sort(function(a,b){ return tlSecs(a.checkInTime)-tlSecs(b.checkInTime); })
|
| - |
|
837 |
.forEach(function(v){ cps.push({lat:v.lat,lng:v.lng}); });
|
| - |
|
838 |
if(data.punchOut) cps.push({lat:data.punchOut.lat,lng:data.punchOut.lng});
|
| - |
|
839 |
bjDrawRoutes(map, cps, pts);
|
| - |
|
840 |
|
| 804 |
if(has){
|
841 |
if(has){
|
| 805 |
map.fitBounds(bounds);
|
842 |
map.fitBounds(bounds);
|
| 806 |
google.maps.event.addListenerOnce(map,'idle',function(){ if(map.getZoom()>16) map.setZoom(16); });
|
843 |
google.maps.event.addListenerOnce(map,'idle',function(){ if(map.getZoom()>16) map.setZoom(16); });
|
| 807 |
} else {
|
844 |
} else {
|
| 808 |
box.innerHTML='<div class="loading">No GPS / location recorded for this day.</div>';
|
845 |
box.innerHTML='<div class="loading">No GPS / location recorded for this day.</div>';
|
| 809 |
}
|
846 |
}
|
| 810 |
}
|
847 |
}
|
| - |
|
848 |
// TAKEN route = the recorded GPS trail (location_tracking_path) routed along the
|
| - |
|
849 |
// road network so it follows actual roads rather than straight hops between sparse
|
| - |
|
850 |
// pings. SUGGESTED route = the Google-optimal road route through the journey's
|
| - |
|
851 |
// checkpoints (green dotted). Both are drawn and compared by distance.
|
| - |
|
852 |
function bjDrawRoutes(map, cps, pathPts){
|
| - |
|
853 |
var legend=bjRouteLegend();
|
| - |
|
854 |
|
| - |
|
855 |
// taken — road route traced through the recorded breadcrumb (in order)
|
| - |
|
856 |
if(pathPts && pathPts.length>=2){
|
| - |
|
857 |
var ds1=new google.maps.DirectionsService();
|
| - |
|
858 |
var sample=bjSample(pathPts, 25); // Directions cap: 25 incl. ends
|
| - |
|
859 |
var o=sample[0], d=sample[sample.length-1];
|
| - |
|
860 |
var vias=sample.slice(1,-1).map(function(p){ return {location:new google.maps.LatLng(p.lat,p.lng), stopover:false}; });
|
| - |
|
861 |
bjRouteRequest(ds1, o, d, vias, false, function(res){
|
| - |
|
862 |
bjRenderRoute(map, res, {color:'#e03131', dashed:false, z:55});
|
| - |
|
863 |
legend.set('taken', bjRouteKm(res));
|
| - |
|
864 |
}, function(status){
|
| - |
|
865 |
console.warn('Directions (taken) failed:', status);
|
| - |
|
866 |
// fallback: plot the raw breadcrumb (straight hops) and measure along it
|
| - |
|
867 |
new google.maps.Polyline({path:pathPts,geodesic:false,strokeColor:'#e03131',strokeOpacity:.9,strokeWeight:4,zIndex:55,map:map});
|
| - |
|
868 |
var ll=pathPts.map(function(p){ return new google.maps.LatLng(p.lat,p.lng); });
|
| - |
|
869 |
legend.set('taken', google.maps.geometry.spherical.computeLength(ll)/1000);
|
| - |
|
870 |
});
|
| - |
|
871 |
} else {
|
| - |
|
872 |
legend.set('taken', false);
|
| - |
|
873 |
}
|
| - |
|
874 |
|
| - |
|
875 |
// suggested — Google-optimal route through punch-in → visits → punch-out
|
| - |
|
876 |
if(cps.length>=2){
|
| - |
|
877 |
var ds=new google.maps.DirectionsService();
|
| - |
|
878 |
var origin=cps[0], dest=cps[cps.length-1], mids=cps.slice(1,-1), capped=false;
|
| - |
|
879 |
if(mids.length>23){ mids=mids.slice(0,23); capped=true; } // Directions allows 23 waypoints
|
| - |
|
880 |
var wpts=mids.map(function(p){ return {location:new google.maps.LatLng(p.lat,p.lng), stopover:true}; });
|
| - |
|
881 |
bjRouteRequest(ds, origin, dest, wpts, true, function(res){
|
| - |
|
882 |
bjRenderRoute(map, res, {color:'#2f9e44', dashed:true, z:50});
|
| - |
|
883 |
legend.set('suggested', bjRouteKm(res));
|
| - |
|
884 |
}, function(status){ console.warn('Directions (suggested) failed:', status); legend.set('suggested', false); });
|
| - |
|
885 |
if(capped) legend.note('Suggested route limited to first 23 stops');
|
| - |
|
886 |
} else {
|
| - |
|
887 |
legend.set('suggested', false);
|
| - |
|
888 |
}
|
| - |
|
889 |
}
|
| - |
|
890 |
// Evenly downsample a list to at most `max` items, always keeping first and last.
|
| - |
|
891 |
function bjSample(arr, max){
|
| - |
|
892 |
if(arr.length<=max) return arr.slice();
|
| - |
|
893 |
var out=[], step=(arr.length-1)/(max-1);
|
| - |
|
894 |
for(var i=0;i<max;i++) out.push(arr[Math.round(i*step)]);
|
| - |
|
895 |
return out;
|
| - |
|
896 |
}
|
| - |
|
897 |
function bjRouteRequest(ds, origin, dest, wpts, optimize, ok, err){
|
| - |
|
898 |
ds.route({
|
| - |
|
899 |
origin:new google.maps.LatLng(origin.lat,origin.lng),
|
| - |
|
900 |
destination:new google.maps.LatLng(dest.lat,dest.lng),
|
| - |
|
901 |
waypoints:wpts, optimizeWaypoints:optimize,
|
| - |
|
902 |
travelMode:google.maps.TravelMode.DRIVING
|
| - |
|
903 |
}, function(res, status){
|
| - |
|
904 |
if(status===google.maps.DirectionsStatus.OK && res && res.routes && res.routes.length) ok(res);
|
| - |
|
905 |
else err(status);
|
| - |
|
906 |
});
|
| - |
|
907 |
}
|
| - |
|
908 |
function bjRenderRoute(map, res, opt){
|
| - |
|
909 |
var poly={strokeColor:opt.color, strokeWeight:opt.dashed?0:5, strokeOpacity:opt.dashed?0:0.85, zIndex:opt.z};
|
| - |
|
910 |
if(opt.dashed){
|
| - |
|
911 |
poly.icons=[{icon:{path:'M 0,-1 0,1', strokeColor:opt.color, strokeOpacity:1, strokeWeight:4, scale:3}, offset:'0', repeat:'14px'}];
|
| - |
|
912 |
}
|
| - |
|
913 |
new google.maps.DirectionsRenderer({map:map, directions:res, suppressMarkers:true,
|
| - |
|
914 |
suppressInfoWindows:true, preserveViewport:true, polylineOptions:poly});
|
| - |
|
915 |
}
|
| - |
|
916 |
function bjRouteKm(res){
|
| - |
|
917 |
var m=0; (res.routes[0].legs||[]).forEach(function(l){ m+=(l.distance?l.distance.value:0); }); return m/1000;
|
| - |
|
918 |
}
|
| - |
|
919 |
// Route-comparison panel rendered into the modal (#bjRouteLegend), above the map.
|
| - |
|
920 |
// Each distance fills in as its route resolves. value: number = km, false =
|
| - |
|
921 |
// unavailable, undefined = still loading.
|
| - |
|
922 |
function bjRouteLegend(){
|
| - |
|
923 |
var div=el('bjRouteLegend');
|
| - |
|
924 |
if(!div) return { set:function(){}, note:function(){} };
|
| - |
|
925 |
var taken, sugg, noteTxt='';
|
| - |
|
926 |
function val(v){ return typeof v==='number' ? '<b>'+v.toFixed(1)+' km</b>' : (v===false ? 'n/a' : '…'); }
|
| - |
|
927 |
function render(){
|
| - |
|
928 |
var cmp='';
|
| - |
|
929 |
if(typeof taken==='number' && typeof sugg==='number'){
|
| - |
|
930 |
cmp = taken>sugg+0.05
|
| - |
|
931 |
? '<span style="color:#e03131">+'+(taken-sugg).toFixed(1)+' km longer than suggested</span>'
|
| - |
|
932 |
: '<span style="color:#2f9e44">on / better than the suggested route</span>';
|
| - |
|
933 |
}
|
| - |
|
934 |
div.innerHTML=
|
| - |
|
935 |
'<span class="lg">Route comparison</span>'+
|
| - |
|
936 |
'<span><span class="sw ln"></span>Taken (GPS)</span>'+
|
| - |
|
937 |
'<span><span class="sw dt"></span>Suggested</span>'+
|
| - |
|
938 |
cmp+
|
| - |
|
939 |
(noteTxt?'<span style="color:#e8590c">'+noteTxt+'</span>':'');
|
| - |
|
940 |
}
|
| - |
|
941 |
render();
|
| - |
|
942 |
return { set:function(k,v){ if(k==='taken')taken=v; else sugg=v; render(); }, note:function(t){ noteTxt=t; render(); } };
|
| - |
|
943 |
}
|
| 811 |
function tlSecs(t){ if(!t) return 1e9; var p=t.split(':'); return (+p[0])*3600+(+(p[1]||0))*60+(+(p[2]||0)); }
|
944 |
function tlSecs(t){ if(!t) return 1e9; var p=t.split(':'); return (+p[0])*3600+(+(p[1]||0))*60+(+(p[2]||0)); }
|
| 812 |
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)); }
|
945 |
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)); }
|
| 813 |
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); }
|
946 |
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); }
|
| 814 |
function jstat(v,l){ return '<div class="js"><div class="jv">'+v+'</div><div class="jl">'+l+'</div></div>'; }
|
947 |
function jstat(v,l){ return '<div class="js"><div class="jv">'+v+'</div><div class="jl">'+l+'</div></div>'; }
|
| 815 |
function hhmm(t){ return t? t.slice(0,5):''; }
|
948 |
function hhmm(t){ return t? t.slice(0,5):''; }
|
| 816 |
function tlThumb(att){
|
949 |
function tlThumb(att){
|
| 817 |
if(att && window._mediaUrl) return '<img class="tlthumb" src="'+esc(window._mediaUrl+att)+'" onclick="window.open(this.src,\'_blank\')" onerror="tlImgErr(this)">';
|
950 |
if(att && window._mediaUrl) return '<img class="tlthumb" src="'+esc(window._mediaUrl+att)+'" onclick="bjOpenImage(this.src)" onerror="tlImgErr(this)">';
|
| 818 |
return '<div class="tlph" title="No photo">📷</div>';
|
951 |
return '<div class="tlph" title="No photo">📷</div>';
|
| 819 |
}
|
952 |
}
|
| 820 |
function tlImgErr(im){ var d=document.createElement('div'); d.className='tlph'; d.title='Photo unavailable'; d.innerHTML='📷'; if(im.parentNode) im.parentNode.replaceChild(d, im); }
|
953 |
function tlImgErr(im){ var d=document.createElement('div'); d.className='tlph'; d.title='Photo unavailable'; d.innerHTML='📷'; if(im.parentNode) im.parentNode.replaceChild(d, im); }
|
| 821 |
function punchEntry(kind, time){ return {_punch:kind, checkInTime:time, markType:(kind==='in'?'PUNCHIN':'PUNCHOUT'), taskType:'attendance'}; }
|
954 |
function punchEntry(kind, time){ return {_punch:kind, checkInTime:time, markType:(kind==='in'?'PUNCHIN':'PUNCHOUT'), taskType:'attendance'}; }
|
| 822 |
function tlStyle(x){
|
955 |
function tlStyle(x){
|
| Line 829... |
Line 962... |
| 829 |
return {c:'#3b5bdb', l:(mt||'-'), ic:'●'};
|
962 |
return {c:'#3b5bdb', l:(mt||'-'), ic:'●'};
|
| 830 |
}
|
963 |
}
|
| 831 |
function tlTitle(x){
|
964 |
function tlTitle(x){
|
| 832 |
if(x._punch) return x._punch==='in' ? 'Journey start (Punch in)' : 'Journey end (Punch out)';
|
965 |
if(x._punch) return x._punch==='in' ? 'Journey start (Punch in)' : 'Journey end (Punch out)';
|
| 833 |
var n=x.taskName||'';
|
966 |
var n=x.taskName||'';
|
| - |
|
967 |
var taskType=(x.taskType||'').toLowerCase();
|
| - |
|
968 |
if(taskType==='franchisee-visit') {
|
| - |
|
969 |
var pipe = n.lastIndexOf('|');
|
| 834 |
var pipe=n.lastIndexOf('|'); if(pipe>=0) n=n.slice(pipe+1); // drop leading agenda tags
|
970 |
if (pipe >= 0) n = n.slice(pipe + 1); // drop leading agenda tags
|
| 835 |
n=n.replace(/\s*VISIT\b.*$/i,'').replace(/\s*visit\s*$/i,'').replace(/\bnull\b/gi,'').replace(/\s{2,}/g,' ').trim();
|
971 |
n = n.replace(/\s*VISIT\b.*$/i, '').replace(/\s*visit\s*$/i, '').replace(/\bnull\b/gi, '').replace(/\s{2,}/g, ' ').trim();
|
| - |
|
972 |
}
|
| 836 |
return n || '(stop)';
|
973 |
return n || '(stop)';
|
| 837 |
}
|
974 |
}
|
| 838 |
function tlRow(x, side){
|
975 |
function tlRow(x, side){
|
| 839 |
var s=tlStyle(x);
|
976 |
var s=tlStyle(x);
|
| 840 |
var tt=(x.taskType||'').toLowerCase();
|
977 |
var tt=(x.taskType||'').toLowerCase();
|
| Line 855... |
Line 992... |
| 855 |
var chips='';
|
992 |
var chips='';
|
| 856 |
if(x.battery!=null) chips += '<span class="tlchip">🔋 '+x.battery+'%</span>';
|
993 |
if(x.battery!=null) chips += '<span class="tlchip">🔋 '+x.battery+'%</span>';
|
| 857 |
if(x.locOff!=null) chips += '<span class="tlchip">📴 '+x.locOff+'</span>';
|
994 |
if(x.locOff!=null) chips += '<span class="tlchip">📴 '+x.locOff+'</span>';
|
| 858 |
extra = '<div class="tlextra">'+tlThumb(x.attachment)+chips+'</div>';
|
995 |
extra = '<div class="tlextra">'+tlThumb(x.attachment)+chips+'</div>';
|
| 859 |
}
|
996 |
}
|
| 860 |
return '<div class="tlrow '+side+'">'+
|
- |
|
| 861 |
'<div class="node" style="background:'+s.c+'">'+s.ic+'</div>'+
|
- |
|
| 862 |
'<div class="tlcard">'+
|
997 |
var card='<div class="tlcard">'+
|
| 863 |
'<div class="tm" style="background:'+s.c+'">'+timeTop+'<small>'+tlabel+'</small></div>'+
|
998 |
'<div class="tm" style="background:'+s.c+'">'+timeTop+'<small>'+tlabel+'</small></div>'+
|
| 864 |
'<div class="bd"><div class="ti">'+esc(tlTitle(x))+'</div>'+
|
999 |
'<div class="bd"><div class="ti">'+esc(tlTitle(x))+'</div>'+
|
| 865 |
'<span class="stt" style="background:'+s.c+'">'+s.l+'</span>'+
|
1000 |
'<span class="stt" style="background:'+s.c+'">'+s.l+'</span>'+
|
| 866 |
'<div class="mt">'+meta+'</div>'+extra+'</div>'+
|
1001 |
'<div class="mt">'+meta+'</div>'+extra+'</div>'+
|
| 867 |
'</div></div>';
|
1002 |
'</div>';
|
| - |
|
1003 |
// remark sits on the opposite half of the timeline (empty spacer when none)
|
| - |
|
1004 |
var rk=(!x._punch && x.remarks) ? '<div class="rk"><div class="rkl">Remark</div>'+esc(x.remarks)+'</div>' : '';
|
| - |
|
1005 |
var remark='<div class="tlremark">'+rk+'</div>';
|
| - |
|
1006 |
var halves = side==='left' ? (card+remark) : (remark+card);
|
| - |
|
1007 |
return '<div class="tlrow '+side+'">'+
|
| - |
|
1008 |
'<div class="node" style="background:'+s.c+'">'+s.ic+'</div>'+
|
| - |
|
1009 |
halves+'</div>';
|
| 868 |
}
|
1010 |
}
|
| 869 |
function closeModal(){ el('bj-modal-back').classList.remove('show'); }
|
1011 |
function closeModal(){ el('bj-modal-back').classList.remove('show'); }
|
| - |
|
1012 |
// Image lightbox — opens an attachment larger, stacked above the journey modal.
|
| - |
|
1013 |
function bjOpenImage(src){ if(!src) return; el('bj-img').src=src; el('bj-img-back').classList.add('show'); }
|
| - |
|
1014 |
function bjCloseImage(){ el('bj-img-back').classList.remove('show'); el('bj-img').removeAttribute('src'); }
|
| 870 |
|
1015 |
|
| 871 |
/* ============================ APPROVAL ============================ */
|
1016 |
/* ============================ APPROVAL ============================ */
|
| 872 |
function loadApproval(){
|
1017 |
function loadApproval(){
|
| 873 |
spinner();
|
1018 |
spinner();
|
| 874 |
var from=el('bj-start').value, to=el('bj-end').value;
|
1019 |
var from=el('bj-start').value, to=el('bj-end').value;
|
| Line 891... |
Line 1036... |
| 891 |
function approve(trackingId){
|
1036 |
function approve(trackingId){
|
| 892 |
fetch(CTX+'/beat-report/approve-checkin',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'trackingId='+trackingId})
|
1037 |
fetch(CTX+'/beat-report/approve-checkin',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'trackingId='+trackingId})
|
| 893 |
.then(function(r){ return r.json(); }).then(function(){ var row=el('appr-'+trackingId); if(row) row.parentNode.removeChild(row); }).catch(function(){ alert('Approve failed'); });
|
1038 |
.then(function(r){ return r.json(); }).then(function(){ var row=el('appr-'+trackingId); if(row) row.parentNode.removeChild(row); }).catch(function(){ alert('Approve failed'); });
|
| 894 |
}
|
1039 |
}
|
| 895 |
|
1040 |
|
| - |
|
1041 |
/* ============================ EXPORT ============================ */
|
| - |
|
1042 |
// Download the Employee Task Report (.xlsx). Today lens exports the single
|
| - |
|
1043 |
// selected day; Period lens exports the whole start..end range. Both carry the
|
| - |
|
1044 |
// current level / team scope (qs()). The download is served as an attachment, so
|
| - |
|
1045 |
// navigating to the URL triggers the file save without leaving the page.
|
| - |
|
1046 |
function exportReport(){
|
| - |
|
1047 |
var start, end;
|
| - |
|
1048 |
if(LENS==='period'){ start=el('bj-start').value; end=el('bj-end').value; }
|
| - |
|
1049 |
else { start=el('bj-date').value; end=start; }
|
| - |
|
1050 |
var u = BASE + '/export?startDate=' + encodeURIComponent(start) + '&endDate=' + encodeURIComponent(end) + qs();
|
| - |
|
1051 |
window.location.href = u;
|
| - |
|
1052 |
}
|
| - |
|
1053 |
|
| 896 |
/* ============================ INIT ============================ */
|
1054 |
/* ============================ INIT ============================ */
|
| 897 |
document.querySelectorAll('.lens').forEach(function(b){ b.addEventListener('click', function(){ setLens(b.getAttribute('data-lens')); }); });
|
1055 |
document.querySelectorAll('.lens').forEach(function(b){ b.addEventListener('click', function(){ setLens(b.getAttribute('data-lens')); }); });
|
| 898 |
el('bj-apply').addEventListener('click', load);
|
1056 |
el('bj-apply').addEventListener('click', load);
|
| - |
|
1057 |
el('bj-export').addEventListener('click', exportReport);
|
| 899 |
el('bj-level').addEventListener('change', onLevelChange);
|
1058 |
el('bj-level').addEventListener('change', onLevelChange);
|
| 900 |
if(el('bj-user')) el('bj-user').addEventListener('change', load);
|
1059 |
if(el('bj-user')) el('bj-user').addEventListener('change', load);
|
| 901 |
el('bj-date').addEventListener('change', updateTodayLabel);
|
1060 |
el('bj-date').addEventListener('change', updateTodayLabel);
|
| 902 |
el('bj-modal-back').addEventListener('click', function(e){ if(e.target===el('bj-modal-back')) closeModal(); });
|
1061 |
el('bj-modal-back').addEventListener('click', function(e){ if(e.target===el('bj-modal-back')) closeModal(); });
|
| - |
|
1062 |
el('bj-img-back').addEventListener('click', function(e){ if(e.target===el('bj-img-back')) bjCloseImage(); });
|
| - |
|
1063 |
el('bj-img-x').addEventListener('click', bjCloseImage);
|
| - |
|
1064 |
// Esc closes the lightbox first (if open), otherwise the journey modal.
|
| - |
|
1065 |
document.addEventListener('keydown', function(e){
|
| - |
|
1066 |
if(e.key!=='Escape') return;
|
| - |
|
1067 |
if(el('bj-img-back').classList.contains('show')) bjCloseImage();
|
| - |
|
1068 |
else if(el('bj-modal-back').classList.contains('show')) closeModal();
|
| - |
|
1069 |
});
|
| 903 |
updateTodayLabel();
|
1070 |
updateTodayLabel();
|
| 904 |
load();
|
1071 |
load();
|
| 905 |
</script>
|
1072 |
</script>
|
| 906 |
</body>
|
1073 |
</body>
|
| 907 |
</html>
|
1074 |
</html>
|