Rev 36710 | Rev 36729 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
<section class="wrapper"><div class="row"><div class="col-lg-12"><h3 class="page-header"><i class="icon_document_alt"></i> $username - Visit Details ($reportDate)</h3><ol class="breadcrumb"><li><i class="fa fa-home"></i><a href="${rc.contextPath}/dashboard">Home</a></li><li><a href="javascript:void(0);" class="beat-report">Beat Report</a></li><li><i class="fa fa-user"></i>$username</li></ol></div></div><br><div class="row col-lg-12" style="float: right;"><div class="col-lg-2"><label>Level</label><select id="user-detail-level" class="form-control" data-selected="$!userLevel"><option value="">All Levels</option>#foreach($et in $escalationTypes)<option value="$et" #if($et.name() == $userLevel)selected#end>$et</option>#end</select></div><div class="col-lg-3"><label>User</label><select id="user-detail-user" class="form-control" data-selected-auth="$!authUserId"><option value="$!authUserId">$username</option></select></div><div class="col-lg-3"><label>Select Date</label><div class="input-group"><input type="date" id="user-detail-date" class="form-control" value="$reportDate" data-userid="$userId"><span class="input-group-btn"><button type="button" class="btn btn-info user-detail-refresh" data-userid="$userId" style="margin-left:10px;"><i class="fa fa-refresh"></i> Refresh</button><button type="button" class="btn btn-success live-tracking-btn" data-userid="$userId" data-username="$username" style="margin-left:5px;"><i class="fa fa-map-marker"></i> Live Tracking</button></span></div></div></div><br><br><br><div class="col-lg-12"><div class="row" style="margin-bottom:15px;"><div class="col-lg-3"><div class="panel panel-default" style="text-align:center; padding:10px;"><h4 style="margin:0; color:#1abc9c; font-weight:bold;">$visits.size()</h4><small>Total Visits</small></div></div>#set($completedCount = 0)#set($pendingCount = 0)#set($deferredCount = 0)#foreach($v in $visits)#if($v.getMarkType() == "CHECKIN-CHECKOUT" || $v.getMarkType() == "CHECKOUT")#set($completedCount = $completedCount + 1)#elseif($v.getMarkType() == "PENDING" || $v.getMarkType() == "CHECKIN")#set($pendingCount = $pendingCount + 1)#elseif($v.getMarkType() == "DEFERRED")#set($deferredCount = $deferredCount + 1)#end#end<div class="col-lg-3"><div class="panel panel-default" style="text-align:center; padding:10px;"><h4 style="margin:0; color:#27ae60; font-weight:bold;">$completedCount</h4><small>Completed</small></div></div><div class="col-lg-3"><div class="panel panel-default" style="text-align:center; padding:10px;"><h4 style="margin:0; color:#e67e22; font-weight:bold;">$pendingCount</h4><small>Pending</small></div></div><div class="col-lg-3"><div class="panel panel-default" style="text-align:center; padding:10px;"><h4 style="margin:0; color:#e74c3c; font-weight:bold;">$deferredCount</h4><small>Deferred</small></div></div></div><table class="table table-border table-condensed table-bordered table-striped" id="userDetailTable" style="width:100%"><thead><tr ><th>#</th><th>Agenda/Partner Name</th><th>Task Type</th><th>Status</th><th>Address</th><th>Check In Time</th><th>Check Out Time</th><th>Time Spent at Outlet</th><th>Transit Time</th><th>Distance (km)</th><th>Estimated Time</th><th>Description</th></tr></thead><tbody>#if($punchIn)<tr style="background:#d5f5e3;"><td>--</td><td><strong>PUNCH IN</strong></td><td>$!punchIn.getTaskType()</td><td><span class="label label-info">PUNCHIN</span> #if($punchIn.getAttachment() && $punchIn.getAttachment() != "")<a href="javascript:void(0);" class="view-attachment-link" data-attachment="$punchIn.getAttachment()" data-checkin-latlng="$!punchIn.getCheckInLatLng()" data-visit-location="$!punchIn.getVisitLocation()" data-tracking-id="$punchIn.getId()" title="View Attachment"><i class="fa fa-paperclip" style="color:#27ae60; margin-left:5px;"></i></a>#end</td><td>$!punchIn.getAddress()</td><td>$!punchIn.getCheckInTime()</td><td>--</td><td>--</td><td>--</td><td>$!punchIn.getTotalDistance()</td><td>$!punchIn.getEstimatedTime()</td><td>--</td></tr>#end#set($idx = 1)#foreach($v in $visits)<tr><td>$idx</td><td>$!v.getTaskName()</td><td>$!v.getTaskType()</td><td>#if($v.getMarkType() == "CHECKIN-CHECKOUT" || $v.getMarkType() == "CHECKOUT")<span class="label label-success">$v.getMarkType()</span>#elseif($v.getMarkType() == "PENDING" || $v.getMarkType() == "CHECKIN")<span class="label label-warning">$v.getMarkType()</span>#elseif($v.getMarkType() == "DEFERRED")<span class="label label-danger">$v.getMarkType()</span>#else<span class="label label-default">$!v.getMarkType()</span>#end<a href="javascript:void(0);" class="beat-history-link" data-userid="$userId" data-taskid="$v.getTaskId()" data-tasktype="$!v.getTaskType()" data-taskname="$!v.getTaskName()" title="View History" style="margin-left:5px;"><i class="fa fa-history" style="color:#3498db; font-size:14px;"></i></a>#if($v.getAttachment() && $v.getAttachment() != "")<a href="javascript:void(0);" class="view-attachment-link" data-attachment="$v.getAttachment()" data-checkin-latlng="$!v.getCheckInLatLng()" data-visit-location="$!v.getVisitLocation()" data-tracking-id="$v.getId()" title="View Attachment"><i class="fa fa-paperclip" style="color:#27ae60; margin-left:3px; font-size:14px;"></i></a>#end</td><td>$!v.getAddress()</td><td>$!v.getCheckInTime()</td><td>$!v.getCheckOutTime()</td><td>$!v.getTimeSpent()</td><td>$!v.getTransitTime()</td><td>$!v.getTotalDistance()</td><td>$!v.getEstimatedTime()</td><td>#if($v.getTaskDescription() && $v.getTaskDescription() != "")#if($v.getTaskDescription().length() > 50)<span class="desc-short">$v.getTaskDescription().substring(0, 50)... <a href="javascript:void(0);" class="read-more-link" style="color:#3498db;">read more</a></span><span class="desc-full" style="display:none;">$v.getTaskDescription() <a href="javascript:void(0);" class="read-less-link" style="color:#3498db;">read less</a></span>#else$v.getTaskDescription()#end#else--#end</td></tr>#set($idx = $idx + 1)#end#if($punchOut)<tr style="background:#fadbd8;"><td>--</td><td><strong>PUNCH OUT</strong></td><td>$!punchOut.getTaskType()</td><td><span class="label label-danger">PUNCHOUT</span> #if($punchOut.getAttachment() && $punchOut.getAttachment() != "")<a href="javascript:void(0);" class="view-attachment-link" data-attachment="$punchOut.getAttachment()" data-checkin-latlng="$!punchOut.getCheckInLatLng()" data-visit-location="$!punchOut.getVisitLocation()" data-tracking-id="$punchOut.getId()" title="View Attachment"><i class="fa fa-paperclip" style="color:#27ae60; margin-left:5px;"></i></a>#end</td><td>$!punchOut.getAddress()</td><td>$!punchOut.getCheckInTime()</td><td>--</td><td>--</td><td>--</td><td>$!punchOut.getTotalDistance()</td><td>$!punchOut.getEstimatedTime()</td><td>--</td></tr>#end</tbody><tfoot><tr style="background:#f2f3f4; font-weight:bold;"><td colspan="5" style="text-align:right;">Totals</td><td>--</td><td>--</td><td>$totalTimeSpentStr</td><td>$totalTransitStr</td><td>$totalDistanceStr km | Visits: $visitCount | Leads: $leadCount</td><td></td><td></td></tr></tfoot></table></div></section><!-- Task History Modal --><div class="modal fade" id="taskHistoryModal" tabindex="-1" role="dialog"><div class="modal-dialog modal-lg" role="document" style="width:85%;"><div class="modal-content"><div class="modal-header" style="background:#3498db; color:white;"><button type="button" class="close" data-dismiss="modal" style="color:white;">×</button><h4 class="modal-title" id="taskHistoryModalTitle">Task History</h4></div><div class="modal-body" id="taskHistoryModalBody"><div class="text-center"><i class="fa fa-spinner fa-spin fa-2x"></i> Loading...</div></div><div class="modal-footer"><button type="button" class="btn btn-default" data-dismiss="modal">Close</button></div></div></div></div><!-- Live Tracking Modal --><div class="modal fade" id="liveTrackingModal" 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="liveTrackingModalTitle"><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="liveTrackingInfo" style="padding:8px 15px; background:#f8f9fa; border-bottom:1px solid #ddd; font-size:13px;"></div><div id="liveTrackingMap" 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="autoRefreshToggle" style="margin-right:5px;"> Auto-refresh (30s)</label>*#<button type="button" class="btn btn-primary" id="refreshTrackingBtn"><i class="fa fa-refresh"></i> Refresh</button><button type="button" class="btn btn-default" data-dismiss="modal">Close</button></div></div></div></div><!-- Attachment Viewer Modal --><div class="modal fade" id="attachmentViewerModal" tabindex="-1" role="dialog"><div class="modal-dialog" role="document" style="width:600px;"><div class="modal-content"><div class="modal-header" style="background:#e67e22; color:white;"><button type="button" class="close" data-dismiss="modal" style="color:white;">×</button><h4 class="modal-title"><i class="fa fa-paperclip"></i> Attachment</h4></div><div class="modal-body" style="text-align:center; padding:15px;"><img id="attachmentImage" src="" style="max-width:100%; max-height:450px; border:1px solid #ddd; border-radius:4px;" /><div id="attachmentDistanceInfo" style="margin-top:10px; font-size:13px;"></div></div><div class="modal-footer"><button type="button" class="btn btn-success" id="approveCheckinBtn" style="display:none;"><i class="fa fa-check"></i> Approve</button><button type="button" class="btn btn-default" data-dismiss="modal">Close</button></div></div></div></div><script>$('#userDetailTable').DataTable({"scrollX": true,"pageLength": 50,"bPaginate": true,"bLengthChange": true,"bFilter": true,"bInfo": true,"bAutoWidth": false,"ordering": false});$('.view-attachment-link').each(function () {var checkinLatLng = $(this).data('checkin-latlng') || '';var visitLocation = $(this).data('visit-location') || '';if (checkinLatLng && visitLocation && checkinLatLng.toString().indexOf(',') > 0 && visitLocation.toString().indexOf(',') > 0) {var cParts = checkinLatLng.toString().split(',');var vParts = visitLocation.toString().split(',');var cLat = parseFloat(cParts[0]), cLng = parseFloat(cParts[1]);var vLat = parseFloat(vParts[0]), vLng = parseFloat(vParts[1]);if (!isNaN(cLat) && !isNaN(cLng) && !isNaN(vLat) && !isNaN(vLng) && !(cLat === 0 && cLng === 0) && !(vLat === 0 && vLng === 0)) {if (haversineMeters(cLat, cLng, vLat, vLng) > 50) {$(this).find('i').css('color', '#e74c3c');}}}});(function () {var level = $('#user-detail-level').data('selected');var selectedAuth = $('#user-detail-user').data('selected-auth');if (level && selectedAuth) {$.ajax({url: context + "/beatPlan/getAuthUsers", type: "GET", dataType: "json",data: { categoryId: 4, escalationType: level },success: function (r) {var data = r.response || r;var html = '<option value="">Select User</option>';data.forEach(function (u) {html += '<option value="' + u.id + '"' + (u.id == selectedAuth ? ' selected' : '') + '>' + u.name + '</option>';});$('#user-detail-user').html(html);}});}})();$(document).on('click', '.read-more-link', function () {$(this).closest('td').find('.desc-short').hide();$(this).closest('td').find('.desc-full').show();});$(document).on('click', '.read-less-link', function () {$(this).closest('td').find('.desc-full').hide();$(this).closest('td').find('.desc-short').show();});$(document).off('click.beatdetail').off('click.livetrack');$(document).on('click.beatdetail', '.beat-history-link', function () {var userId = $(this).data('userid');var taskId = $(this).data('taskid');var taskType = $(this).data('tasktype');var taskName = $(this).data('taskname');$('#taskHistoryModalTitle').text('History - ' + taskName);$('#taskHistoryModalBody').html('<div class="text-center"><i class="fa fa-spinner fa-spin fa-2x"></i> Loading...</div>');$('#taskHistoryModal').modal('show');doGetAjaxRequestHandler(context + "/beat-report/task-history?userId=" + userId + "&taskId=" + taskId + "&taskType=" + encodeURIComponent(taskType),function (response) {$('#taskHistoryModalBody').html(response);});});var liveTrackingMap = null;var liveTrackingUserId = null;var liveTrackingInterval = null;function getMarkerColor(markType) {if (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 createTrackingMarkerIcon(color, label) {var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="42" height="60" viewBox="0 0 42 60">' +'<path d="M21 0C9.402 0 0 9.402 0 21c0 15.75 21 39 21 39s21-23.25 21-39C42 9.402 32.598 0 21 0z" fill="' + color + '"/>' +'<circle cx="21" cy="21" r="10" fill="white"/>' +'<text x="21" y="26" text-anchor="middle" font-size="14" font-weight="bold" fill="' + color + '">' + label + '</text></svg>';return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);}function loadLiveTracking(userId, username) {var selectedDate = $('#user-detail-date').val();$.ajax({url: context + "/beat-report/live-tracking",type: "GET",dataType: "json",data: { userId: userId, date: selectedDate },success: function (data) {renderLiveTrackingMap(data, username);},error: function () {$('#liveTrackingInfo').html('<span style="color:red;">Failed to load tracking data.</span>');}});}function clearMapOverlays() {if (!liveTrackingMap) return;if (liveTrackingMap._markers) {liveTrackingMap._markers.forEach(function (m) { m.setMap(null); });}if (liveTrackingMap._polyline) {liveTrackingMap._polyline.setMap(null);}if (liveTrackingMap._gpsDots) {liveTrackingMap._gpsDots.forEach(function (m) { m.setMap(null); });}if (liveTrackingMap._stopMarkers) {liveTrackingMap._stopMarkers.forEach(function (m) { m.setMap(null); });}if (liveTrackingMap._currentMarker) {liveTrackingMap._currentMarker.setMap(null);}if (liveTrackingMap._directionsRenderer) {liveTrackingMap._directionsRenderer.setMap(null);}liveTrackingMap._markers = [];liveTrackingMap._gpsDots = [];liveTrackingMap._stopMarkers = [];}function formatEpoch(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 formatDuration(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 haversineMeters(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 detectStops(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 (haversineMeters(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 renderLiveTrackingMap(data, username) {var visits = data.visits || [];var pathPoints = data.pathPoints || [];if (!liveTrackingMap) {liveTrackingMap = new google.maps.Map(document.getElementById('liveTrackingMap'), {zoom: 12,center: { lat: 20.5937, lng: 78.9629 },mapTypeControl: true,streetViewControl: false});}clearMapOverlays();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);var 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: liveTrackingMap,icon: {url: createTrackingMarkerIcon(getMarkerColor(v.markType), label),scaledSize: new google.maps.Size(42, 60),anchor: new google.maps.Point(21, 60)},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:' + getMarkerColor(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(liveTrackingMap, m); });})(marker, infoContent);liveTrackingMap._markers.push(marker);}// Draw actual road path between visits using Directions APIif (visitCoords.length >= 2) {var origin = visitCoords[0];var destination = visitCoords[visitCoords.length - 1];var waypoints = [];for (var wi = 1; wi < visitCoords.length - 1; wi++) {waypoints.push({ location: visitCoords[wi], stopover: true });}var directionsService = new google.maps.DirectionsService();var directionsRenderer = new google.maps.DirectionsRenderer({map: liveTrackingMap,suppressMarkers: true,polylineOptions: {strokeColor: '#8e44ad',strokeOpacity: 0.7,strokeWeight: 4},preserveViewport: true});liveTrackingMap._directionsRenderer = directionsRenderer;directionsService.route({origin: origin,destination: destination,waypoints: waypoints,travelMode: google.maps.TravelMode.DRIVING,optimizeWaypoints: false}, function (result, status) {if (status === 'OK') {directionsRenderer.setDirections(result);}});}// Draw GPS tracked path with red dots + connecting linevar totalGpsDistanceM = 0;var accuracySum = 0;var accuracyCount = 0;var stopsDetected = [];if (pathPoints.length > 0) {var validPoints = [];for (var j = 0; j < pathPoints.length; j++) {var pt = pathPoints[j];var pLat = parseFloat(pt.lat);var 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;}// Calculate total GPS distancefor (var d = 1; d < validPoints.length; d++) {totalGpsDistanceM += haversineMeters(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 }; });// Blue connecting lineliveTrackingMap._polyline = new google.maps.Polyline({path: pathCoords,geodesic: true,strokeColor: '#e74c3c',strokeOpacity: 0.7,strokeWeight: 3,zIndex: 50,map: liveTrackingMap});// Direction arrows for each GPS pointliveTrackingMap._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: liveTrackingMap,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> ' + formatEpoch(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(liveTrackingMap, marker); });})(dot, gp, gi);liveTrackingMap._gpsDots.push(dot);}// Detect stationary stops (within 50m for >5 min)stopsDetected = detectStops(validPoints, 50, 5 * 60 * 1000);liveTrackingMap._stopMarkers = [];for (var si = 0; si < stopsDetected.length; si++) {var stop = stopsDetected[si];var stopMarker = new google.maps.Marker({position: { lat: stop.lat, lng: stop.lng },map: liveTrackingMap,icon: {path: google.maps.SymbolPath.CIRCLE,scale: 20,fillColor: '#f39c12',fillOpacity: 0.9,strokeColor: '#e67e22',strokeWeight: 2},label: { text: formatDuration(stop.duration), fontSize: '12px', fontWeight: 'bold', color: '#000' },title: 'Stopped for ' + formatDuration(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>' + formatDuration(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(liveTrackingMap, marker); });})(stopMarker, stop);liveTrackingMap._stopMarkers.push(stopMarker);}// Last known location marker (bike icon)var lastPoint = pathCoords[pathCoords.length - 1];liveTrackingMap._currentMarker = new google.maps.Marker({position: lastPoint,map: liveTrackingMap,icon: {url: '${rc.contextPath}/resources/images/Bike-Icon-SD.png',scaledSize: new google.maps.Size(56, 56),anchor: new google.maps.Point(28, 28)},title: 'Last Known Location',zIndex: 999});var lastGp = validPoints[validPoints.length - 1];var currentIw = 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: ' + formatEpoch(lastGp.ts) + '<br>' +'<i class="fa fa-crosshairs"></i> Accuracy: ' + (lastGp.accuracy || 'N/A') + 'm</div>'});liveTrackingMap._currentMarker.addListener('click', function () {currentIw.open(liveTrackingMap, liveTrackingMap._currentMarker);});}}if (hasPoints) {liveTrackingMap.fitBounds(bounds);}// Info barvar 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 === '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 = '';var 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>';$('#liveTrackingInfo').html(infoHtml);// Low data warningif (pathPoints.length > 0 && pathPoints.length < 10) {$('#liveTrackingInfo').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 ensureGoogleMaps(callback) {if (window.google && window.google.maps) {callback();return;}var s = document.createElement('script');s.src = 'https://maps.googleapis.com/maps/api/js?key=AIzaSyAckO0y4Z6WhBOuMjNjioWLSYZDhGEvGBc&libraries=geometry&v=weekly';s.onload = callback;document.head.appendChild(s);}var liveTrackingUsername = '';/*function startAutoRefresh() {stopAutoRefresh();if ($('#autoRefreshToggle').is(':checked') && liveTrackingUserId) {liveTrackingInterval = setInterval(function () {loadLiveTracking(liveTrackingUserId, liveTrackingUsername);}, 30000);}}function stopAutoRefresh() {if (liveTrackingInterval) {clearInterval(liveTrackingInterval);liveTrackingInterval = null;}}*/$(document).on('click.livetrack', '.live-tracking-btn', function () {liveTrackingUserId = $(this).data('userid');liveTrackingUsername = $(this).data('username');$('#liveTrackingModalTitle').html('<img src="${rc.contextPath}/resources/images/Bike-Icon-SD.png" style="height:24px; vertical-align:middle; margin-right:6px;"> Live Tracking - ' + liveTrackingUsername);$('#liveTrackingInfo').html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');$('#liveTrackingModal').modal('show');setTimeout(function () {liveTrackingMap = null;ensureGoogleMaps(function () {loadLiveTracking(liveTrackingUserId, liveTrackingUsername);startAutoRefresh();});}, 300);});$(document).on('click.livetrack', '#refreshTrackingBtn', function () {if (liveTrackingUserId) {$('#liveTrackingInfo').html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Refreshing...</div>');loadLiveTracking(liveTrackingUserId, liveTrackingUsername);}});$(document).on('change', '#autoRefreshToggle', function () {if ($(this).is(':checked')) {startAutoRefresh();} else {stopAutoRefresh();}});$('#liveTrackingModal').on('hidden.bs.modal', function () {stopAutoRefresh();liveTrackingUserId = null;});// Attachment Viewer$(document).on('click', '.view-attachment-link', function () {var attachment = $(this).data('attachment');var checkinLatLng = $(this).data('checkin-latlng') || '';var visitLocation = $(this).data('visit-location') || '';var trackingId = $(this).data('tracking-id');var imgUrl = context + '/download-attachment?documentId=' + attachment;$('#attachmentImage').attr('src', imgUrl);$('#approveCheckinBtn').hide().removeData('tracking-id');$('#attachmentDistanceInfo').html('');var distanceM = -1;if (checkinLatLng && visitLocation && checkinLatLng.indexOf(',') > 0 && visitLocation.indexOf(',') > 0) {var cParts = checkinLatLng.toString().split(',');var vParts = visitLocation.toString().split(',');var cLat = parseFloat(cParts[0]);var cLng = parseFloat(cParts[1]);var vLat = parseFloat(vParts[0]);var vLng = parseFloat(vParts[1]);if (!isNaN(cLat) && !isNaN(cLng) && !isNaN(vLat) && !isNaN(vLng) && !(cLat === 0 && cLng === 0) && !(vLat === 0 && vLng === 0)) {distanceM = haversineMeters(cLat, cLng, vLat, vLng);}}if (distanceM > 50) {$('#attachmentDistanceInfo').html('<span class="label label-danger" style="font-size:12px; padding:4px 10px;">' +'<i class="fa fa-exclamation-triangle"></i> Checkin Distance: ' + Math.round(distanceM) + 'm (exceeds 50m radius)</span>');$('#approveCheckinBtn').show().data('tracking-id', trackingId);} else if (distanceM >= 0) {$('#attachmentDistanceInfo').html('<span class="label label-success" style="font-size:12px; padding:4px 10px;">' +'<i class="fa fa-check-circle"></i> Checkin Distance: ' + Math.round(distanceM) + 'm (within 50m radius)</span>');}$('#attachmentViewerModal').modal('show');});$(document).on('click', '#approveCheckinBtn', function () {var btn = $(this);var trackingId = btn.data('tracking-id');if (!trackingId) return;btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Approving...');$.ajax({url: context + '/beat-report/approve-checkin',type: 'POST',data: { trackingId: trackingId },success: function () {btn.html('<i class="fa fa-check"></i> Approved').removeClass('btn-success').addClass('btn-default').prop('disabled', true);alert('Checkin approved successfully');},error: function () {btn.prop('disabled', false).html('<i class="fa fa-check"></i> Approve');alert('Failed to approve checkin');}});});</script>