Rev 36806 | Blame | Compare with Previous | Last modification | View Log | RSS feed
<script>if (!window.google || !window.google.maps) {var s = document.createElement('script');s.src = 'https://maps.googleapis.com/maps/api/js?key=AIzaSyAckO0y4Z6WhBOuMjNjioWLSYZDhGEvGBc&libraries=geometry&v=weekly';document.head.appendChild(s);}</script><div class="col-lg-12"><table class="table table-border table-condensed table-bordered table-hover" id="beatReportTable" style="width:100%"><thead><tr><th>Status</th><th>User Name</th><th>Beat</th><th>Day</th><th>Planned</th><th>Punch In/Out</th><th>Total Visits</th><th>Completed Visits</th><th>Pending Visits</th><th>Deferred</th><th>Action</th></tr></thead><tbody>#set($rowIndex = 0)#foreach($row in $reportRows)#set($status = $row.get("status"))#if($status == "ON_TRACK")#set($rowBg = "#eafaf1")#set($badgeBg = "#27ae60")#set($badgeText = "On track")#elseif($status == "AGENDA_PENDING")#set($rowBg = "#fff8e1")#set($badgeBg = "#f1c40f")#set($badgeText = "Agenda not filled")#else#set($rowBg = "#fdecea")#set($badgeBg = "#e74c3c")#set($badgeText = "No beat")#end<tr style="background:$rowBg;"><td style="white-space:nowrap;"><spanstyle="display:inline-block;padding:3px 8px;border-radius:3px;background:$badgeBg;color:#fff;font-size:11px;font-weight:600;">$badgeText</span></td><td><a href="javascript:void(0);" class="beat-user-detail-link" data-userid="$row.get('userId')" style="color:#2c3e50; font-weight:bold; text-decoration:underline; cursor:pointer;">$row.get("username")</a></td>#set($userBeats = $row.get("beats"))#if($userBeats && $userBeats.size() > 0)<td>#foreach($b in $userBeats)<span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#if($b.get('beatColor'))$b.get('beatColor')#else#3498db#end;margin-right:5px;"></span>$b.get("beatName")<br>#end</td><td>#foreach($b in $userBeats)$b.get("dayNumber")<br>#end</td><td>#foreach($b in $userBeats)<strong>$b.get("totalCount")</strong><br>#end</td>#else<td style="color:#999; text-align:center; font-style:italic;">No beat scheduled</td><td style="color:#999;">--</td><td style="color:#999;">--</td>#end<td>#if($row.get("punchInTime") != "")<span class="label label-success">$row.get("punchInTime")</span>#else<span style="color:#ccc;">--</span>#end#if($row.get("punchOutTime") != "")<span class="label label-danger">$row.get("punchOutTime")</span>#else<span style="color:#ccc;">--</span>#end</td><td><a href="javascript:void(0);" class="beat-detail-link" data-row="$rowIndex" data-type="total" style="color:#1abc9c; font-weight:bold; text-decoration:underline; cursor:pointer;">$row.get("total")</a></td><td><a href="javascript:void(0);" class="beat-detail-link" data-row="$rowIndex" data-type="completed" style="color:#1abc9c; font-weight:bold; text-decoration:underline; cursor:pointer;">$row.get("completed")</a></td><td><a href="javascript:void(0);" class="beat-detail-link" data-row="$rowIndex" data-type="pending" style="color:#e67e22; font-weight:bold; text-decoration:underline; cursor:pointer;">$row.get("pending")</a></td><td><a href="javascript:void(0);" class="beat-detail-link" data-row="$rowIndex" data-type="deferred" style="color:#e74c3c; font-weight:bold; text-decoration:underline; cursor:pointer;">$row.get("deferred")</a></td><td style="white-space:nowrap;"><a href="javascript:void(0);" class="beat-map-link" data-row="$rowIndex" title="Map"style="cursor:pointer; margin-right:6px;"><i class="fa fa-map-marker"style="color:#1abc9c; font-size:18px;"></i></a>#if($userBeats && $userBeats.size() > 0)#foreach($b in $userBeats)<button class="btn btn-xs btn-info beat-view-calendar" title="View Calendar"data-userid="$b.get('authUserId')"data-username="$row.get('username').replace('"', '\"')"><i class="fa fa-eye"></i></button>#if($canEditBeat)<button class="btn btn-xs btn-warning beat-edit-on-date" title="Edit Beat"data-userid="$b.get('authUserId')"data-username="$row.get('username').replace('"', '\"')"data-beatid="$b.get('beatId')"data-date="$b.get('scheduleDate')"><i class="fa fa-edit"></i></button><button class="btn btn-xs btn-success beat-assign-visit" title="Assign Visit"data-userid="$b.get('authUserId')"data-username="$row.get('username').replace('"', '\"')"data-beatid="$b.get('beatId')"data-date="$b.get('scheduleDate')"><i class="fa fa-plus-circle"></i></button>#end<br>#end#end</td></tr>#set($rowIndex = $rowIndex + 1)#end</tbody></table></div><!--Per-row visit lists are emitted as a single script block OUTSIDE the table.Embedding them between <tr> rows triggers HTML foster-parenting (browsersmove misplaced non-row content out of the table during parsing), whichfragments the tbody and causes DataTables to throw"Cannot set properties of undefined (setting '_DT_CellIndex')".--><script>window.beatReportData = window.beatReportData || {};#set($rowIndex = 0)#foreach($row in $reportRows)window.beatReportData[$rowIndex] = {username: "$row.get('username').replace('"', '\"')",total: [#foreach($lt in $row.get("totalList")){taskName: "$!lt.getTaskName().replace('"', '\"')",markType: "$!lt.getMarkType()",address: "$!lt.getAddress().replace('"', '\"').replace("'", "\'")",checkInTime: "$!lt.getCheckInTime()",checkOutTime: "$!lt.getCheckOutTime()",timeSpent: "$!lt.getTimeSpent()",taskType: "$!lt.getTaskType()",visitLocation: "$!lt.getVisitLocation()",checkInLatLng: "$!lt.getCheckInLatLng()",taskDescription: "$!lt.getTaskDescription().replace('"', '\"')",attachment: "$!lt.getAttachment()"},#end],completed: [#foreach($lt in $row.get("completedList")){taskName: "$!lt.getTaskName().replace('"', '\"')",markType: "$!lt.getMarkType()",address: "$!lt.getAddress().replace('"', '\"').replace("'", "\'")",checkInTime: "$!lt.getCheckInTime()",checkOutTime: "$!lt.getCheckOutTime()",timeSpent: "$!lt.getTimeSpent()",taskType: "$!lt.getTaskType()",visitLocation: "$!lt.getVisitLocation()",checkInLatLng: "$!lt.getCheckInLatLng()",taskDescription: "$!lt.getTaskDescription().replace('"', '\"')",attachment: "$!lt.getAttachment()"},#end],pending: [#foreach($lt in $row.get("pendingList")){taskName: "$!lt.getTaskName().replace('"', '\"')",markType: "$!lt.getMarkType()",address: "$!lt.getAddress().replace('"', '\"').replace("'", "\'")",checkInTime: "$!lt.getCheckInTime()",checkOutTime: "$!lt.getCheckOutTime()",timeSpent: "$!lt.getTimeSpent()",taskType: "$!lt.getTaskType()",visitLocation: "$!lt.getVisitLocation()",checkInLatLng: "$!lt.getCheckInLatLng()",taskDescription: "$!lt.getTaskDescription().replace('"', '\"')",attachment: "$!lt.getAttachment()"},#end],deferred: [#foreach($lt in $row.get("deferredList")){taskName: "$!lt.getTaskName().replace('"', '\"')",markType: "$!lt.getMarkType()",address: "$!lt.getAddress().replace('"', '\"').replace("'", "\'")",checkInTime: "$!lt.getCheckInTime()",checkOutTime: "$!lt.getCheckOutTime()",timeSpent: "$!lt.getTimeSpent()",taskType: "$!lt.getTaskType()",visitLocation: "$!lt.getVisitLocation()",checkInLatLng: "$!lt.getCheckInLatLng()",taskDescription: "$!lt.getTaskDescription().replace('"', '\"')",attachment: "$!lt.getAttachment()"},#end]};#set($rowIndex = $rowIndex + 1)#end</script><!-- Map Modal --><div class="modal fade" id="beatMapModal" tabindex="-1" role="dialog"><div class="modal-dialog modal-lg" role="document" style="width:90%;"><div class="modal-content"><div class="modal-header" style="background:#1abc9c; color:white;"><button type="button" class="close" data-dismiss="modal" style="color:white;">×</button><h4 class="modal-title" id="beatMapModalTitle">Visit Locations</h4></div><div class="modal-body" style="padding:0;"><div id="beatMapContainer" style="height:500px; width:100%;"></div><div id="beatMapInfo" style="padding:6px 15px; background:#fff3cd; border-top:1px solid #ddd; color:#856404; display:none;"></div><div style="padding:8px 15px; background:#f5f5f5; border-top:1px solid #ddd;"><span style="margin-right:15px;"><i class="fa fa-circle" style="color:#4285F4;"></i> Check In</span><span style="margin-right:15px;"><i class="fa fa-circle" style="color:#0F9D58;"></i> Completed</span><span style="margin-right:15px;"><i class="fa fa-circle" style="color:#F4B400;"></i> Pending</span><span><i class="fa fa-circle" style="color:#DB4437;"></i> Deferred</span></div></div><div class="modal-footer"><button type="button" class="btn btn-default" data-dismiss="modal">Close</button></div></div></div></div><!-- Detail Popup Modal --><div class="modal fade" id="beatDetailModal" tabindex="-1" role="dialog"><div class="modal-dialog modal-lg" role="document"><div class="modal-content"><div class="modal-header" style="background:#1abc9c; color:white;"><button type="button" class="close" data-dismiss="modal" style="color:white;">×</button><h4 class="modal-title" id="beatDetailModalTitle">Visit Details</h4></div><div class="modal-body"><table class="table table-bordered table-condensed table-striped" id="beatDetailTable" style="width:100%"><thead><tr><th>#</th><th>Agenda/Partner Name</th><th>Type</th><th>Status</th><th>Address</th><th>Check In</th><th>Check Out</th><th>Time Spent</th><th>Description</th><th>ETA</th><th>Attachment</th></tr></thead><tbody id="beatDetailTableBody"></tbody></table></div><div class="modal-footer"><button type="button" class="btn btn-default" data-dismiss="modal">Close</button></div></div></div></div><script>$('#beatReportTable').DataTable({"scrollX": true,"pageLength": 50,"bPaginate": true,"bLengthChange": true,"bFilter": true,"bInfo": true,"bAutoWidth": false});var beatMapMarkerColors = {'PENDING': '#F4B400','DEFERRED': '#DB4437','CHECKIN': '#4285F4','CHECKIN_CHECKOUT': '#0F9D58','CHECKIN-CHECKOUT': '#0F9D58','CHECKOUT': '#0F9D58'};function getMarkerColor(markType) {return beatMapMarkerColors[markType] || '#999999';}function createColoredMarkerIcon(color, label) {var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="42" viewBox="0 0 32 42">' +'<path d="M16 0C7.2 0 0 7.2 0 16c0 12 16 26 16 26s16-14 16-26C32 7.2 24.8 0 16 0z" fill="' + color + '"/>' +'<circle cx="16" cy="15" r="10" fill="white"/>' +'<text x="16" y="20" text-anchor="middle" font-size="12" font-weight="bold" fill="' + color + '">' + label + '</text>' +'</svg>';return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);}function getLatLng(item) {var loc = item.visitLocation || item.checkInLatLng || '';if (!loc || loc === '0.0000,0.0000') return null;var parts = loc.split(',');if (parts.length !== 2) return null;var lat = parseFloat(parts[0].trim());var lng = parseFloat(parts[1].trim());if (isNaN(lat) || isNaN(lng) || (lat === 0 && lng === 0)) return null;return { lat: lat, lng: lng };}$(document).on('click', '.beat-map-link', function () {var rowIndex = $(this).data('row');var data = window.beatReportData[rowIndex];if (!data) return;var items = data.total || [];var username = data.username;$('#beatMapModalTitle').text(username + ' - Visit Locations (' + items.length + ')');if (typeof google === 'undefined' || !google.maps) {alert('Google Maps is still loading. Please try again in a moment.');return;}$('#beatMapModal').modal('show');setTimeout(function () {var mapDiv = document.getElementById('beatMapContainer');var bounds = new google.maps.LatLngBounds();var hasMarkers = false;var map = new google.maps.Map(mapDiv, {zoom: 10,center: { lat: 20.5937, lng: 78.9629 },mapTypeId: 'roadmap'});var infoWindow = new google.maps.InfoWindow();for (var i = 0; i < items.length; i++) {var item = items[i];var pos = getLatLng(item);if (!pos) continue;hasMarkers = true;bounds.extend(pos);var color = getMarkerColor(item.markType);var marker = new google.maps.Marker({position: pos,map: map,title: item.taskName || item.markType,icon: {url: createColoredMarkerIcon(color, (i + 1)),scaledSize: new google.maps.Size(32, 42),anchor: new google.maps.Point(16, 42)}});(function (m, itm, idx) {google.maps.event.addListener(m, 'click', function () {var content = '<div style="min-width:200px;">' +'<strong>' + (idx + 1) + '. ' + (itm.taskName || '--') + '</strong><br>' +'<b>Status:</b> ' + (itm.markType || '--') + '<br>' +'<b>Address:</b> ' + (itm.address || '--') + '<br>' +'<b>Check In:</b> ' + (itm.checkInTime || '--') + '<br>' +'<b>Check Out:</b> ' + (itm.checkOutTime || '--') + '<br>' +'<b>Time Spent:</b> ' + (itm.timeSpent || '--') +'</div>';infoWindow.setContent(content);infoWindow.open(map, m);});})(marker, item, i);}var markerCount = 0;for (var j = 0; j < items.length; j++) { if (getLatLng(items[j])) markerCount++; }var infoDiv = document.getElementById('beatMapInfo');if (markerCount < items.length) {infoDiv.innerHTML = '<i class="fa fa-exclamation-triangle"></i> Showing ' + markerCount + ' of ' + items.length + ' visits on map. ' + (items.length - markerCount) + ' visits have no location data.';infoDiv.style.display = 'block';} else {infoDiv.style.display = 'none';}if (hasMarkers) {map.fitBounds(bounds);if (markerCount === 1) {map.setZoom(14);}}}, 400);});$(document).on('click', '.beat-detail-link', function () {var rowIndex = $(this).data('row');var type = $(this).data('type');var data = window.beatReportData[rowIndex];if (!data) return;var items = data[type] || [];var username = data.username;var typeLabel = type.charAt(0).toUpperCase() + type.slice(1);$('#beatDetailModalTitle').text(username + ' - ' + typeLabel + ' Visits (' + items.length + ')');var tbody = '';for (var i = 0; i < items.length; i++) {var item = items[i];tbody += '<tr>' +'<td>' + (i + 1) + '</td>' +'<td>' + (item.taskName || '--') + '</td>' +'<td>' + (item.taskType || '--') + '</td>' +'<td>' + (item.markType || '--') + '</td>' +'<td>' + (item.address || '--') + '</td>' +'<td>' + (item.checkInTime || '--') + '</td>' +'<td>' + (item.checkOutTime || '--') + '</td>' +'<td>' + (item.timeSpent || '--') + '</td>' +'<td>' + (item.taskDescription || '--') + '</td>' +'<td>' + (item.estimatedTime || '--') + '</td>' +'<td>' + (item.attachment ? '<a href="https://partners.smartdukaan.com/document/' + item.attachment + '" target="_blank"><i class="fa fa-paperclip"></i> View</a>' : '--') + '</td>' +'</tr>';}if (items.length === 0) {tbody = '<tr><td colspan="10" class="text-center">No records found</td></tr>';}$('#beatDetailTableBody').html(tbody);$('#beatDetailModal').modal('show');});// --- Live Tracking ---var dataLiveMap = null;var dataLiveUserId = null;var dataLiveUsername = '';var dataLiveInterval = null;function dataGetMarkerColor(markType) {if (markType === 'CHECKIN-CHECKOUT' || markType === 'CHECKIN_CHECKOUT' || markType === 'CHECKOUT') return '#0F9D58';if (markType === 'PENDING' || markType === 'CHECKIN') return '#4285F4';if (markType === 'DEFERRED') return '#DB4437';if (markType === 'PUNCHIN') return '#1abc9c';if (markType === 'PUNCHOUT') return '#e74c3c';return '#F4B400';}function dataCreateMarkerIcon(color, label) {var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="40" viewBox="0 0 28 40">' +'<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 + '"/>' +'<circle cx="14" cy="14" r="7" fill="white"/>' +'<text x="14" y="18" text-anchor="middle" font-size="10" font-weight="bold" fill="' + color + '">' + label + '</text></svg>';return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);}function dataFormatEpoch(ts) {if (!ts) return 'N/A';var n = parseFloat(ts);if (isNaN(n)) return ts;if (n < 1e12) n = n * 1000;var d = new Date(n);return d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0') + ':' + d.getSeconds().toString().padStart(2,'0');}function dataFormatDuration(ms) {var sec = Math.floor(ms / 1000);var h = Math.floor(sec / 3600);var m = Math.floor((sec % 3600) / 60);var s = sec % 60;if (h > 0) return h + 'h ' + m + 'm';if (m > 0) return m + 'm ' + s + 's';return s + 's';}function dataHaversine(lat1, lon1, lat2, lon2) {var R = 6371000;var dLat = (lat2 - lat1) * Math.PI / 180;var dLon = (lon2 - lon1) * Math.PI / 180;var a = Math.sin(dLat/2) * Math.sin(dLat/2) +Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *Math.sin(dLon/2) * Math.sin(dLon/2);return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));}function dataDetectStops(points, radiusM, minTimeMs) {var stops = [];var i = 0;while (i < points.length) {var cluster = [points[i]];var cLat = points[i].lat, cLng = points[i].lng;var j = i + 1;while (j < points.length) {if (dataHaversine(cLat, cLng, points[j].lat, points[j].lng) <= radiusM) {cluster.push(points[j]);j++;} else {break;}}if (cluster.length >= 2) {var t0 = parseFloat(cluster[0].ts);var t1 = parseFloat(cluster[cluster.length - 1].ts);if (t0 < 1e12) t0 *= 1000;if (t1 < 1e12) t1 *= 1000;var duration = t1 - t0;if (duration >= minTimeMs) {var avgLat = 0, avgLng = 0;cluster.forEach(function(p) { avgLat += p.lat; avgLng += p.lng; });stops.push({lat: avgLat / cluster.length, lng: avgLng / cluster.length,startTime: t0, endTime: t1, duration: duration, pointCount: cluster.length});}}i = j;}return stops;}function dataClearOverlays() {if (!dataLiveMap) return;if (dataLiveMap._markers) dataLiveMap._markers.forEach(function(m) { m.setMap(null); });if (dataLiveMap._polyline) dataLiveMap._polyline.setMap(null);if (dataLiveMap._gpsDots) dataLiveMap._gpsDots.forEach(function(m) { m.setMap(null); });if (dataLiveMap._stopMarkers) dataLiveMap._stopMarkers.forEach(function(m) { m.setMap(null); });if (dataLiveMap._currentMarker) dataLiveMap._currentMarker.setMap(null);if (dataLiveMap._directionsRenderer) dataLiveMap._directionsRenderer.setMap(null);dataLiveMap._markers = [];dataLiveMap._gpsDots = [];dataLiveMap._stopMarkers = [];}function dataLoadLiveTracking(userId, username) {var selectedDate = $('#beat-report-date').val();$.ajax({url: context + "/beat-report/live-tracking",type: "GET", dataType: "json",data: { userId: userId, date: selectedDate },success: function(data) { dataRenderMap(data, username); },error: function() { $('#dataLiveInfo').html('<span style="color:red;">Failed to load tracking data.</span>'); }});}function dataRenderMap(data, username) {var visits = data.visits || [];var pathPoints = data.pathPoints || [];if (!dataLiveMap) {dataLiveMap = new google.maps.Map(document.getElementById('dataLiveMap'), {zoom: 12, center: { lat: 20.5937, lng: 78.9629 },mapTypeControl: true, streetViewControl: false});}dataClearOverlays();var bounds = new google.maps.LatLngBounds();var hasPoints = false;var visitIdx = 0;var visitCoords = [];for (var i = 0; i < visits.length; i++) {var v = visits[i];if (!v.lat || !v.lng) continue;var lat = parseFloat(v.lat), lng = parseFloat(v.lng);if (isNaN(lat) || isNaN(lng) || (lat === 0 && lng === 0)) continue;hasPoints = true;var pos = { lat: lat, lng: lng };bounds.extend(pos);visitCoords.push(pos);var label = v.markType === 'PUNCHIN' ? 'P' : (v.markType === 'PUNCHOUT' ? 'X' : String(++visitIdx));var marker = new google.maps.Marker({position: pos, map: dataLiveMap,icon: { url: dataCreateMarkerIcon(dataGetMarkerColor(v.markType), label), scaledSize: new google.maps.Size(28, 40), anchor: new google.maps.Point(14, 40) },title: v.taskName + ' (' + v.markType + ')', zIndex: 100});var infoContent = '<div style="font-size:13px; max-width:280px;">' +'<strong>' + (v.taskName || '') + '</strong><br>' +'<span class="label" style="background:' + dataGetMarkerColor(v.markType) + '; color:white;">' + v.markType + '</span>' +(v.taskType ? ' <span class="label label-default">' + v.taskType + '</span>' : '') + '<br>' +(v.checkInTime ? '<i class="fa fa-sign-in" style="color:#27ae60;"></i> In: <strong>' + v.checkInTime + '</strong><br>' : '') +(v.checkOutTime ? '<i class="fa fa-sign-out" style="color:#e74c3c;"></i> Out: <strong>' + v.checkOutTime + '</strong><br>' : '') +(v.timeSpent ? '<i class="fa fa-clock-o" style="color:#f39c12;"></i> Time Spent: <strong>' + v.timeSpent + '</strong><br>' : '') +(v.transitTime ? '<i class="fa fa-car" style="color:#3498db;"></i> Transit: <strong>' + v.transitTime + '</strong><br>' : '') +(v.totalDistance && v.totalDistance !== '0.0' ? '<i class="fa fa-road" style="color:#8e44ad;"></i> Distance: <strong>' + v.totalDistance + ' km</strong><br>' : '') +(v.address ? '<i class="fa fa-map-marker" style="color:#e74c3c;"></i> ' + v.address : '') + '</div>';(function(m, c) {var iw = new google.maps.InfoWindow({ content: c });m.addListener('click', function() { iw.open(dataLiveMap, m); });})(marker, infoContent);dataLiveMap._markers.push(marker);}if (visitCoords.length >= 2) {var origin = visitCoords[0], destination = visitCoords[visitCoords.length - 1];var waypoints = [];for (var wi = 1; wi < visitCoords.length - 1; wi++) waypoints.push({ location: visitCoords[wi], stopover: true });var ds = new google.maps.DirectionsService();var dr = new google.maps.DirectionsRenderer({map: dataLiveMap, suppressMarkers: true,polylineOptions: { strokeColor: '#8e44ad', strokeOpacity: 0.7, strokeWeight: 4 },preserveViewport: true});dataLiveMap._directionsRenderer = dr;ds.route({ origin: origin, destination: destination, waypoints: waypoints, travelMode: google.maps.TravelMode.DRIVING, optimizeWaypoints: false },function(result, status) { if (status === 'OK') dr.setDirections(result); });}var totalGpsDistanceM = 0, accuracySum = 0, accuracyCount = 0, stopsDetected = [];if (pathPoints.length > 0) {var validPoints = [];for (var j = 0; j < pathPoints.length; j++) {var pt = pathPoints[j];var pLat = parseFloat(pt.lat), pLng = parseFloat(pt.lng);if (isNaN(pLat) || isNaN(pLng) || (pLat === 0 && pLng === 0)) continue;var acc = parseFloat(pt.accuracy);if (!isNaN(acc)) { accuracySum += acc; accuracyCount++; }validPoints.push({ lat: pLat, lng: pLng, ts: pt.timestamp, accuracy: pt.accuracy });bounds.extend({ lat: pLat, lng: pLng });hasPoints = true;}for (var d = 1; d < validPoints.length; d++) {totalGpsDistanceM += dataHaversine(validPoints[d-1].lat, validPoints[d-1].lng, validPoints[d].lat, validPoints[d].lng);}if (validPoints.length > 0) {var pathCoords = validPoints.map(function(p) { return { lat: p.lat, lng: p.lng }; });dataLiveMap._polyline = new google.maps.Polyline({path: pathCoords, geodesic: true, strokeColor: '#e74c3c', strokeOpacity: 0.7, strokeWeight: 3, zIndex: 50, map: dataLiveMap});dataLiveMap._gpsDots = [];for (var gi = 0; gi < validPoints.length; gi++) {var gp = validPoints[gi];var heading = 0;if (gi < validPoints.length - 1) {heading = google.maps.geometry.spherical.computeHeading(new google.maps.LatLng(gp.lat, gp.lng), new google.maps.LatLng(validPoints[gi+1].lat, validPoints[gi+1].lng));} else if (gi > 0) {heading = google.maps.geometry.spherical.computeHeading(new google.maps.LatLng(validPoints[gi-1].lat, validPoints[gi-1].lng), new google.maps.LatLng(gp.lat, gp.lng));}var dot = new google.maps.Marker({position: { lat: gp.lat, lng: gp.lng }, map: dataLiveMap,icon: { path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW, scale: 3, fillColor: '#e74c3c', fillOpacity: 1, strokeColor: '#b71c1c', strokeWeight: 1, rotation: heading },zIndex: 80});(function(marker, point, idx) {var iw = new google.maps.InfoWindow({content: '<div style="font-size:12px;"><strong>GPS Point #' + (idx+1) + '</strong><br>' +'<i class="fa fa-clock-o"></i> ' + dataFormatEpoch(point.ts) + '<br>' +'<i class="fa fa-crosshairs"></i> Accuracy: ' + (point.accuracy || 'N/A') + 'm<br>' +'<i class="fa fa-map-marker"></i> ' + point.lat.toFixed(6) + ', ' + point.lng.toFixed(6) + '</div>'});marker.addListener('click', function() { iw.open(dataLiveMap, marker); });})(dot, gp, gi);dataLiveMap._gpsDots.push(dot);}stopsDetected = dataDetectStops(validPoints, 50, 5 * 60 * 1000);dataLiveMap._stopMarkers = [];for (var si = 0; si < stopsDetected.length; si++) {var stop = stopsDetected[si];var sm = new google.maps.Marker({position: { lat: stop.lat, lng: stop.lng }, map: dataLiveMap,icon: { path: google.maps.SymbolPath.CIRCLE, scale: 14, fillColor: '#f39c12', fillOpacity: 0.9, strokeColor: '#e67e22', strokeWeight: 2 },label: { text: dataFormatDuration(stop.duration), fontSize: '9px', fontWeight: 'bold', color: '#000' },title: 'Stopped for ' + dataFormatDuration(stop.duration), zIndex: 200});(function(marker, s) {var iw = new google.maps.InfoWindow({content: '<div style="font-size:13px;"><strong><i class="fa fa-pause-circle" style="color:#f39c12;"></i> Stationary Stop</strong><br>' +'<i class="fa fa-clock-o"></i> Duration: <strong>' + dataFormatDuration(s.duration) + '</strong><br>' +'<i class="fa fa-sign-in"></i> From: <strong>' + new Date(s.startTime).toLocaleTimeString() + '</strong><br>' +'<i class="fa fa-sign-out"></i> To: <strong>' + new Date(s.endTime).toLocaleTimeString() + '</strong><br>' +'<i class="fa fa-map-marker"></i> ' + s.lat.toFixed(6) + ', ' + s.lng.toFixed(6) + '<br>' +'<i class="fa fa-dot-circle-o"></i> Points in cluster: ' + s.pointCount + '</div>'});marker.addListener('click', function() { iw.open(dataLiveMap, marker); });})(sm, stop);dataLiveMap._stopMarkers.push(sm);}var lastPoint = pathCoords[pathCoords.length - 1];dataLiveMap._currentMarker = new google.maps.Marker({position: lastPoint, map: dataLiveMap,icon: { url: '${rc.contextPath}/resources/images/Bike-Icon-SD.png', scaledSize: new google.maps.Size(40, 40), anchor: new google.maps.Point(20, 20) },title: 'Last Known Location', zIndex: 999});var lastGp = validPoints[validPoints.length - 1];var curIw = new google.maps.InfoWindow({content: '<div style="font-size:13px;"><strong><i class="fa fa-crosshairs" style="color:#27ae60;"></i> Last Known Location</strong><br>' +'<i class="fa fa-clock-o"></i> Time: ' + dataFormatEpoch(lastGp.ts) + '<br>' +'<i class="fa fa-crosshairs"></i> Accuracy: ' + (lastGp.accuracy || 'N/A') + 'm</div>'});dataLiveMap._currentMarker.addListener('click', function() { curIw.open(dataLiveMap, dataLiveMap._currentMarker); });}}if (hasPoints) dataLiveMap.fitBounds(bounds);var totalVisits = visits.filter(function(v) { return v.markType !== 'PUNCHIN' && v.markType !== 'PUNCHOUT'; }).length;var completedVisits = visits.filter(function(v) { return v.markType === 'CHECKIN-CHECKOUT' || v.markType === 'CHECKIN_CHECKOUT' || v.markType === 'CHECKOUT'; }).length;var now = new Date();var timeStr = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0') + ':' + now.getSeconds().toString().padStart(2,'0');var avgAccuracy = accuracyCount > 0 ? (accuracySum / accuracyCount).toFixed(0) : null;var accuracyLabel = '', accuracyBadge = '';if (avgAccuracy) {if (avgAccuracy <= 10) { accuracyLabel = 'High'; accuracyBadge = 'background:#27ae60;'; }else if (avgAccuracy <= 30) { accuracyLabel = 'Good'; accuracyBadge = 'background:#2ecc71;'; }else if (avgAccuracy <= 100) { accuracyLabel = 'Average'; accuracyBadge = 'background:#f39c12;'; }else { accuracyLabel = 'Low'; accuracyBadge = 'background:#e74c3c;'; }}var totalGpsKm = (totalGpsDistanceM / 1000).toFixed(2);var infoHtml = '<strong>' + username + '</strong> | Visits: <strong>' + totalVisits + '</strong> | Completed: <strong>' + completedVisits + '</strong>';if (pathPoints.length > 0) {infoHtml += ' | GPS Distance: <strong>' + totalGpsKm + ' km</strong>';infoHtml += ' | Points: <strong>' + pathPoints.length + '</strong>';if (accuracyLabel) infoHtml += ' | Accuracy: <span class="label" style="' + accuracyBadge + ' color:white; padding:2px 8px; border-radius:3px; font-size:11px;">' + accuracyLabel + '</span> (' + avgAccuracy + 'm)';if (stopsDetected.length > 0) infoHtml += ' | <span style="color:#f39c12;"><i class="fa fa-pause-circle"></i></span> Stops: <strong>' + stopsDetected.length + '</strong>';infoHtml += ' | <img src="${rc.contextPath}/resources/images/Bike-Icon-SD.png" style="height:16px; vertical-align:middle;"> Last Known';} else {infoHtml += ' | <span style="color:#999;"><i class="fa fa-exclamation-triangle"></i> No GPS data yet</span>';}infoHtml += ' | <span style="color:#e74c3c;"><i class="fa fa-arrow-right" style="font-size:10px;"></i></span> GPS Trail';if (visitCoords.length > 1) infoHtml += ' | <span style="background:#8e44ad; display:inline-block; width:14px; height:3px; vertical-align:middle; margin:0 2px;"></span> Route';var locToggle = data.locationToggleCount || 0;if (locToggle > 0) infoHtml += ' | <span class="label" style="background:#e74c3c; color:white; padding:2px 8px; border-radius:3px; font-size:11px;"><i class="fa fa-power-off"></i> Loc Off: ' + locToggle + '</span>';infoHtml += ' | <span style="color:#999;">Updated: ' + timeStr + '</span>';$('#dataLiveInfo').html(infoHtml);if (pathPoints.length > 0 && pathPoints.length < 10) {$('#dataLiveInfo').append('<div style="margin-top:4px; padding:4px 8px; background:#fff3cd; border:1px solid #ffc107; border-radius:3px; font-size:11px; color:#856404;">' +'<i class="fa fa-exclamation-triangle"></i> Received less data points from device due to: Low Battery level, GPS/location switched off, or phone automatically switched off the location service.</div>');}}function dataStartAutoRefresh() {dataStopAutoRefresh();if ($('#dataAutoRefresh').is(':checked') && dataLiveUserId) {dataLiveInterval = setInterval(function() { dataLoadLiveTracking(dataLiveUserId, dataLiveUsername); }, 30000);}}function dataStopAutoRefresh() {if (dataLiveInterval) { clearInterval(dataLiveInterval); dataLiveInterval = null; }}$(document).on('click', '.data-live-tracking-btn', function() {dataLiveUserId = $(this).data('userid');dataLiveUsername = $(this).data('username');$('#dataLiveTitle').html('<img src="${rc.contextPath}/resources/images/Bike-Icon-SD.png" style="height:24px; vertical-align:middle; margin-right:6px;"> Live Tracking - ' + dataLiveUsername);$('#dataLiveInfo').html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');$('#dataLiveModal').modal('show');setTimeout(function() {dataLiveMap = null;if (window.google && window.google.maps) {dataLoadLiveTracking(dataLiveUserId, dataLiveUsername);dataStartAutoRefresh();} else {var s = document.createElement('script');s.src = 'https://maps.googleapis.com/maps/api/js?key=AIzaSyAckO0y4Z6WhBOuMjNjioWLSYZDhGEvGBc&libraries=geometry&v=weekly';s.onload = function() { dataLoadLiveTracking(dataLiveUserId, dataLiveUsername); dataStartAutoRefresh(); };document.head.appendChild(s);}}, 300);});$(document).on('click', '#dataRefreshBtn', function() {if (dataLiveUserId) {$('#dataLiveInfo').html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Refreshing...</div>');dataLoadLiveTracking(dataLiveUserId, dataLiveUsername);}});$(document).on('change', '#dataAutoRefresh', function() {$(this).is(':checked') ? dataStartAutoRefresh() : dataStopAutoRefresh();});$('#dataLiveModal').on('hidden.bs.modal', function() { dataStopAutoRefresh(); dataLiveUserId = null; });</script><!-- Live Tracking Modal --><div class="modal fade" id="dataLiveModal" tabindex="-1" role="dialog"><div class="modal-dialog modal-lg" role="document" style="width:90%;"><div class="modal-content"><div class="modal-header" style="background:#27ae60; color:white;"><button type="button" class="close" data-dismiss="modal" style="color:white;">×</button><h4 class="modal-title" id="dataLiveTitle"><img src="${rc.contextPath}/resources/images/Bike-Icon-SD.png" style="height:24px; vertical-align:middle; margin-right:6px;"> Live Tracking</h4></div><div class="modal-body" style="padding:0;"><div id="dataLiveInfo" style="padding:8px 15px; background:#f8f9fa; border-bottom:1px solid #ddd; font-size:13px;"></div><div id="dataLiveMap" style="height:500px; width:100%;"></div></div><div class="modal-footer"><label style="float:left; margin-top:7px; font-weight:normal; font-size:13px;"><input type="checkbox" id="dataAutoRefresh" checked style="margin-right:5px;"> Auto-refresh (30s)</label><button type="button" class="btn btn-primary" id="dataRefreshBtn"><i class="fa fa-refresh"></i> Refresh</button><button type="button" class="btn btn-default" data-dismiss="modal">Close</button></div></div></div></div><!-- Beat calendar modal (iframe loads /beatPlanWindow with auto-selected user) --><div class="modal fade" id="reportBeatCalendarModal" tabindex="-1" style="overflow-y:auto;"><div class="modal-dialog" style="width:95%; max-width:1300px; margin-top:20px;"><div class="modal-content"><div class="modal-header"><button type="button" class="close" data-dismiss="modal"onclick="$('#reportBeatCalendarFrame').attr('src','');">×</button><h4 class="modal-title" id="reportBeatCalendarTitle">Calendar</h4></div><div class="modal-body" style="padding:0;"><iframe id="reportBeatCalendarFrame" style="width:100%; height:80vh; border:none;"></iframe></div></div></div></div><script>$(document).on('click', '.beat-view-calendar', function () {var userId = $(this).data('userid');var userName = $(this).data('username') || '';var url = context + '/beatPlanWindow?autoUserId=' + userId+ '&autoUserName=' + encodeURIComponent(userName);$('#reportBeatCalendarTitle').text('Calendar - ' + userName);$('#reportBeatCalendarFrame').attr('src', url);$('#reportBeatCalendarModal').modal('show');});$(document).on('click', '.beat-edit-on-date', function () {var userId = $(this).data('userid');var userName = $(this).data('username') || '';var beatId = $(this).data('beatid');var date = $(this).data('date');var url = context + '/beatPlanWindow?autoUserId=' + userId+ '&autoUserName=' + encodeURIComponent(userName)+ '&editBeatId=' + beatId + '&editDate=' + date;$('#reportBeatCalendarTitle').text('Edit Beat - ' + userName + ' on ' + date);$('#reportBeatCalendarFrame').attr('src', url);$('#reportBeatCalendarModal').modal('show');});// ---------- ASSIGN VISIT (mirrors Beat Day View; IDs/classes prefixed with `r` to avoid collisions) ----------var rAvContext = {authUserId: null, userName: '', date: '', beatId: null, allParties: [], agendaOptions: []};var R_AGENDA_MULTISELECT_OPTS = {includeSelectAllOption: false,maxHeight: 220,buttonWidth: '240px',numberDisplayed: 2,nonSelectedText: 'Pick agenda(s)',nSelectedText: ' agendas',allSelectedText: 'All agendas',enableFiltering: true,enableCaseInsensitiveFiltering: true};function rAgendaSelectHtml(p) {var preset = {};(p.existingAgendas || []).forEach(function (a) {preset[a] = true;});var html = '<select multiple class="r-av-party-agenda" data-fofoid="' + p.fofoStoreId + '">';rAvContext.agendaOptions.forEach(function (opt) {html += '<option value="' + opt + '"' + (preset[opt] ? ' selected' : '') + '>' + opt + '</option>';});html += '</select>';return html;}function rRebuildDefaultAgenda() {var $sel = $('#rAvDefaultAgenda');try {$sel.multiselect('destroy');} catch (e) {}var html = '';rAvContext.agendaOptions.forEach(function (opt) {html += '<option value="' + opt + '">' + opt + '</option>';});$sel.html(html);var defaultOpts = $.extend({}, R_AGENDA_MULTISELECT_OPTS);defaultOpts.nonSelectedText = 'Pick agenda(s) to apply';$sel.multiselect(defaultOpts);}function rInitRowMultiselects() {$('.r-av-party-agenda').each(function () {var $sel = $(this);try {$sel.multiselect('destroy');} catch (e) {}$sel.multiselect(R_AGENDA_MULTISELECT_OPTS);});}function rUpdateAvSelectedCount() {var n = $('.r-av-party-chk:checked').length;$('#rAvSelectedCount').text(n + ' selected');$('#rAvSubmit').prop('disabled', n === 0);}function rRenderAvParties(list) {if (!list || list.length === 0) {$('#rAvPartyList').html('<div style="padding:20px; color:#999; text-align:center;">No parties found.</div>');return;}var html = '<table class="table table-condensed table-hover" style="margin-bottom:0; font-size:13px;">';html += '<thead><tr>'+ '<th style="width:30px;"><input type="checkbox" id="rAvSelectAll" title="Select all"></th>'+ '<th>Code</th><th>Outlet</th><th>City</th>'+ '<th style="width:260px;">Agenda</th>'+ '<th style="width:260px;">Description</th>'+ '</tr></thead><tbody>';list.forEach(function (p) {var safeName = (p.outletName || '').replace(/"/g, '"');var existingDesc = (p.existingDescription || '').replace(/"/g, '"');html += '<tr>'+ '<td><input type="checkbox" class="r-av-party-chk" '+ 'data-fofoid="' + p.fofoStoreId + '" '+ 'data-name="' + safeName + '" '+ 'data-lat="' + (p.latitude || '') + '" '+ 'data-lng="' + (p.longitude || '') + '"></td>'+ '<td>' + (p.code || '') + '</td>'+ '<td>' + (p.outletName || '') + '</td>'+ '<td>' + (p.city || '') + '</td>'+ '<td>' + rAgendaSelectHtml(p) + '</td>'+ '<td><textarea class="form-control input-sm r-av-party-desc"'+ ' data-fofoid="' + p.fofoStoreId + '"'+ ' rows="2" placeholder="Optional notes..."'+ ' style="resize:vertical; min-height:34px;">' + existingDesc + '</textarea></td>'+ '</tr>';});html += '</tbody></table>';$('#rAvPartyList').html(html);rInitRowMultiselects();rUpdateAvSelectedCount();}$(document).on('click', '.beat-assign-visit', function () {rAvContext.authUserId = $(this).data('userid');rAvContext.userName = $(this).data('username') || '';rAvContext.date = $(this).data('date');rAvContext.beatId = $(this).data('beatid');rAvContext.allParties = [];$('#rAvTitle').text('Assign Visit - ' + rAvContext.userName + ' on ' + rAvContext.date);$('#rAvSearch').val('');$('#rAvPartyList').html('<div style="padding:20px; color:#999; text-align:center;">Loading parties...</div>');$('#rAvMsg').text('');$('#rAvSelectedCount').text('0 selected');$('#rAvSubmit').prop('disabled', true);$('#rAssignVisitModal').modal('show');$.get(context + '/beatPlan/assignVisit/parties', {authUserId: rAvContext.authUserId,date: rAvContext.date,beatId: rAvContext.beatId}).done(function (r) {var data = r.response || r;rAvContext.allParties = (data.parties || []).filter(function (p) {return !p.inBeat;});rAvContext.agendaOptions = data.agendaOptions || [];if (!data.dtrUserId) {$('#rAvPartyList').html('<div style="padding:20px; color:#c62828; text-align:center;">'+ 'This user has no dtr.users record (cannot create visit tasks).</div>');return;}rRebuildDefaultAgenda();rRenderAvParties(rAvContext.allParties);}).fail(function (xhr) {$('#rAvPartyList').html('<div style="padding:20px; color:#c62828; text-align:center;">'+ 'Error loading parties: ' + (xhr.responseText || xhr.statusText) + '</div>');});});$(document).on('input', '#rAvSearch', function () {var q = $(this).val().toLowerCase().trim();if (!q) {rRenderAvParties(rAvContext.allParties);return;}var filtered = rAvContext.allParties.filter(function (p) {return (p.code || '').toLowerCase().indexOf(q) !== -1|| (p.outletName || '').toLowerCase().indexOf(q) !== -1|| (p.city || '').toLowerCase().indexOf(q) !== -1;});rRenderAvParties(filtered);});$(document).on('change', '#rAvSelectAll', function () {$('.r-av-party-chk').prop('checked', this.checked);rUpdateAvSelectedCount();});$(document).on('change', '.r-av-party-chk', rUpdateAvSelectedCount);$(document).on('click', '#rAvApplyAgenda', function () {var defValues = $('#rAvDefaultAgenda').val() || [];if (defValues.length === 0) {alert('Pick at least one agenda above first');return;}$('.r-av-party-chk:checked').each(function () {var fid = $(this).data('fofoid');var $sel = $('.r-av-party-agenda[data-fofoid="' + fid + '"]');var existing = $sel.val() || [];var merged = existing.slice();defValues.forEach(function (v) {if (merged.indexOf(v) === -1) merged.push(v);});$sel.val(merged);try {$sel.multiselect('refresh');} catch (e) {}});});$(document).on('click', '#rAvSubmit', function () {var picks = [];var missing = 0;$('.r-av-party-chk:checked').each(function () {var fid = parseInt($(this).data('fofoid'));var agendas = $('.r-av-party-agenda[data-fofoid="' + fid + '"]').val() || [];var description = ($('.r-av-party-desc[data-fofoid="' + fid + '"]').val() || '').trim();if (agendas.length === 0) missing++;picks.push({fofoStoreId: fid,outletName: $(this).data('name'),latitude: $(this).data('lat') ? String($(this).data('lat')) : null,longitude: $(this).data('lng') ? String($(this).data('lng')) : null,agendas: agendas,description: description});});if (picks.length === 0) return;if (missing > 0) {if (!confirm(missing + ' selected row(s) have no agenda. Submit anyway with default "Visit"?')) return;}var btn = $(this);btn.prop('disabled', true).text('Assigning...');$('#rAvMsg').text('');$.ajax({url: context + '/beatPlan/assignVisit/submit',type: 'POST',contentType: 'application/json',data: JSON.stringify({authUserId: rAvContext.authUserId,planDate: rAvContext.date,parties: picks}),success: function (r) {var d = r.response || r;$('#rAvMsg').html('<span style="color:#2e7d32;">' + (d.message || 'Saved') + '</span>');btn.prop('disabled', false).text('Save Assignments');setTimeout(function () {$('#rAssignVisitModal').modal('hide');}, 1500);},error: function (xhr) {var msg = 'Failed';try {msg = (JSON.parse(xhr.responseText).response.message) || msg;} catch (e) {}$('#rAvMsg').html('<span style="color:#c62828;">' + msg + '</span>');btn.prop('disabled', false).text('Save Assignments');}});});</script><!-- Assign Visit Modal (mirrors Beat Day View; prefixed IDs to avoid collisions) --><div class="modal fade" id="rAssignVisitModal" tabindex="-1" style="overflow-y:auto;"><div class="modal-dialog" style="width:90%; max-width:900px; margin-top:30px;"><div class="modal-content"><div class="modal-header"><button type="button" class="close" data-dismiss="modal">×</button><h4 class="modal-title" id="rAvTitle">Assign Visit</h4></div><div class="modal-body"><div style="display:flex; gap:10px; align-items:flex-start; margin-bottom:10px;"><div style="flex:1;"><label style="font-size:11px; color:#666; font-weight:normal; margin-bottom:2px;">Agenda - applies to selected rows</label><select id="rAvDefaultAgenda" multiple></select></div><button type="button" class="btn btn-default btn-sm" id="rAvApplyAgenda" style="margin-top:18px;"title="Add the picked agendas into all selected rows (merges with existing)">Apply to Selected</button></div><div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;"><input type="text" id="rAvSearch" class="form-control input-sm"placeholder="Search by code, outlet name or city..." style="flex:1;"><span id="rAvSelectedCount" style="font-size:12px; color:#666;">0 selected</span></div><div id="rAvPartyList"style="max-height:50vh; overflow-y:auto; border:1px solid #e5e5e5; border-radius:4px;"></div><div id="rAvMsg" style="margin-top:10px; font-size:12px;"></div></div><div class="modal-footer"><button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button><button type="button" class="btn btn-success" id="rAvSubmit" disabled>Save Assignments</button></div></div></div></div>