Subversion Repositories SmartDukaan

Rev

Rev 36693 | Rev 36696 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 36693 Rev 36695
Line 16... Line 16...
16
            <th>Total Visits</th>
16
            <th>Total Visits</th>
17
            <th>Completed Visits</th>
17
            <th>Completed Visits</th>
18
            <th>Pending Visits</th>
18
            <th>Pending Visits</th>
19
            <th>Deferred</th>
19
            <th>Deferred</th>
20
            <th>Battery</th>
20
            <th>Battery</th>
21
            <th>Loc Off</th>
-
 
22
            <th>Map</th>
21
            <th>Map</th>
23
            <th>Live Tracking</th>
-
 
24
        </tr>
22
        </tr>
25
        </thead>
23
        </thead>
26
        <tbody>
24
        <tbody>
27
        #set($rowIndex = 0)
25
        #set($rowIndex = 0)
28
        #foreach($row in $reportRows)
26
        #foreach($row in $reportRows)
Line 33... Line 31...
33
                <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>
31
                <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>
34
                <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>
32
                <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>
35
                <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>
33
                <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>
36
                <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>
34
                <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>
37
                <td>#if($row.get("batteryLevel"))$row.get("batteryLevel")%#else<span style="color:#ccc;">--</span>#end</td>
35
                <td>#if($row.get("batteryLevel"))$row.get("batteryLevel")%#else<span style="color:#ccc;">--</span>#end</td>
38
                <td>#if($row.get("locationToggleCount") > 0)<span class="label label-danger"><i class="fa fa-power-off"></i> $row.get("locationToggleCount")</span>#else<span style="color:#27ae60;"><i class="fa fa-check-circle"></i></span>#end</td>
-
 
39
                <td><a href="javascript:void(0);" class="beat-map-link" data-row="$rowIndex" style="cursor:pointer;"><i class="fa fa-map-marker" style="color:#1abc9c; font-size:18px;"></i></a></td>
36
                <td><a href="javascript:void(0);" class="beat-map-link" data-row="$rowIndex" style="cursor:pointer;"><i class="fa fa-map-marker" style="color:#1abc9c; font-size:18px;"></i></a></td>
40
                <td><a href="javascript:void(0);" class="data-live-tracking-btn" data-userid="$row.get('userId')" data-username="$row.get('username')" style="cursor:pointer;"><img src="${rc.contextPath}/resources/images/Bike-Icon-SD.png" style="height:22px; vertical-align:middle;" title="Live Tracking"></a></td>
-
 
41
            </tr>
37
            </tr>
42
 
38
 
43
            <script>
39
            <script>
44
                window.beatReportData = window.beatReportData || {};
40
                window.beatReportData = window.beatReportData || {};
45
                window.beatReportData[$rowIndex] = {
41
                window.beatReportData[$rowIndex] = {
Line 294... Line 290...
294
        }
290
        }
295
        $('#beatDetailTableBody').html(tbody);
291
        $('#beatDetailTableBody').html(tbody);
296
        $('#beatDetailModal').modal('show');
292
        $('#beatDetailModal').modal('show');
297
    });
293
    });
298
 
294
 
299
    // --- Live Tracking ---
-
 
300
    var dataLiveMap = null;
-
 
301
    var dataLiveUserId = null;
-
 
302
    var dataLiveUsername = '';
-
 
303
    var dataLiveInterval = null;
-
 
304
 
-
 
305
    function dataGetMarkerColor(markType) {
-
 
306
        if (markType === 'CHECKIN-CHECKOUT' || markType === 'CHECKOUT') return '#0F9D58';
-
 
307
        if (markType === 'PENDING' || markType === 'CHECKIN') return '#4285F4';
-
 
308
        if (markType === 'DEFERRED') return '#DB4437';
-
 
309
        if (markType === 'PUNCHIN') return '#1abc9c';
-
 
310
        if (markType === 'PUNCHOUT') return '#e74c3c';
-
 
311
        return '#F4B400';
-
 
312
    }
-
 
313
 
-
 
314
    function dataCreateMarkerIcon(color, label) {
-
 
315
        var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="40" viewBox="0 0 28 40">' +
-
 
316
            '<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 + '"/>' +
-
 
317
            '<circle cx="14" cy="14" r="7" fill="white"/>' +
-
 
318
            '<text x="14" y="18" text-anchor="middle" font-size="10" font-weight="bold" fill="' + color + '">' + label + '</text></svg>';
-
 
319
        return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
-
 
320
    }
-
 
321
 
-
 
322
    function dataFormatEpoch(ts) {
-
 
323
        if (!ts) return 'N/A';
-
 
324
        var n = parseFloat(ts);
-
 
325
        if (isNaN(n)) return ts;
-
 
326
        if (n < 1e12) n = n * 1000;
-
 
327
        var d = new Date(n);
-
 
328
        return d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0') + ':' + d.getSeconds().toString().padStart(2,'0');
-
 
329
    }
-
 
330
 
-
 
331
    function dataFormatDuration(ms) {
-
 
332
        var sec = Math.floor(ms / 1000);
-
 
333
        var h = Math.floor(sec / 3600);
-
 
334
        var m = Math.floor((sec % 3600) / 60);
-
 
335
        var s = sec % 60;
-
 
336
        if (h > 0) return h + 'h ' + m + 'm';
-
 
337
        if (m > 0) return m + 'm ' + s + 's';
-
 
338
        return s + 's';
-
 
339
    }
-
 
340
 
-
 
341
    function dataHaversine(lat1, lon1, lat2, lon2) {
-
 
342
        var R = 6371000;
-
 
343
        var dLat = (lat2 - lat1) * Math.PI / 180;
-
 
344
        var dLon = (lon2 - lon1) * Math.PI / 180;
-
 
345
        var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
-
 
346
                Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
-
 
347
                Math.sin(dLon/2) * Math.sin(dLon/2);
-
 
348
        return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
-
 
349
    }
-
 
350
 
-
 
351
    function dataDetectStops(points, radiusM, minTimeMs) {
-
 
352
        var stops = [];
-
 
353
        var i = 0;
-
 
354
        while (i < points.length) {
-
 
355
            var cluster = [points[i]];
-
 
356
            var cLat = points[i].lat, cLng = points[i].lng;
-
 
357
            var j = i + 1;
-
 
358
            while (j < points.length) {
-
 
359
                if (dataHaversine(cLat, cLng, points[j].lat, points[j].lng) <= radiusM) {
-
 
360
                    cluster.push(points[j]);
-
 
361
                    j++;
-
 
362
                } else {
-
 
363
                    break;
-
 
364
                }
-
 
365
            }
-
 
366
            if (cluster.length >= 2) {
-
 
367
                var t0 = parseFloat(cluster[0].ts);
-
 
368
                var t1 = parseFloat(cluster[cluster.length - 1].ts);
-
 
369
                if (t0 < 1e12) t0 *= 1000;
-
 
370
                if (t1 < 1e12) t1 *= 1000;
-
 
371
                var duration = t1 - t0;
-
 
372
                if (duration >= minTimeMs) {
-
 
373
                    var avgLat = 0, avgLng = 0;
-
 
374
                    cluster.forEach(function(p) { avgLat += p.lat; avgLng += p.lng; });
-
 
375
                    stops.push({
-
 
376
                        lat: avgLat / cluster.length, lng: avgLng / cluster.length,
-
 
377
                        startTime: t0, endTime: t1, duration: duration, pointCount: cluster.length
-
 
378
                    });
-
 
379
                }
-
 
380
            }
-
 
381
            i = j;
-
 
382
        }
-
 
383
        return stops;
-
 
384
    }
-
 
385
 
-
 
386
    function dataClearOverlays() {
-
 
387
        if (!dataLiveMap) return;
-
 
388
        if (dataLiveMap._markers) dataLiveMap._markers.forEach(function(m) { m.setMap(null); });
-
 
389
        if (dataLiveMap._polyline) dataLiveMap._polyline.setMap(null);
-
 
390
        if (dataLiveMap._gpsDots) dataLiveMap._gpsDots.forEach(function(m) { m.setMap(null); });
-
 
391
        if (dataLiveMap._stopMarkers) dataLiveMap._stopMarkers.forEach(function(m) { m.setMap(null); });
-
 
392
        if (dataLiveMap._currentMarker) dataLiveMap._currentMarker.setMap(null);
-
 
393
        if (dataLiveMap._directionsRenderer) dataLiveMap._directionsRenderer.setMap(null);
-
 
394
        dataLiveMap._markers = [];
-
 
395
        dataLiveMap._gpsDots = [];
-
 
396
        dataLiveMap._stopMarkers = [];
-
 
397
    }
-
 
398
 
-
 
399
    function dataLoadLiveTracking(userId, username) {
-
 
400
        var selectedDate = $('#beat-report-date').val();
-
 
401
        $.ajax({
-
 
402
            url: context + "/beat-report/live-tracking",
-
 
403
            type: "GET", dataType: "json",
-
 
404
            data: { userId: userId, date: selectedDate },
-
 
405
            success: function(data) { dataRenderMap(data, username); },
-
 
406
            error: function() { $('#dataLiveInfo').html('<span style="color:red;">Failed to load tracking data.</span>'); }
-
 
407
        });
-
 
408
    }
-
 
409
 
-
 
410
    function dataRenderMap(data, username) {
-
 
411
        var visits = data.visits || [];
-
 
412
        var pathPoints = data.pathPoints || [];
-
 
413
 
-
 
414
        if (!dataLiveMap) {
-
 
415
            dataLiveMap = new google.maps.Map(document.getElementById('dataLiveMap'), {
-
 
416
                zoom: 12, center: { lat: 20.5937, lng: 78.9629 },
-
 
417
                mapTypeControl: true, streetViewControl: false
-
 
418
            });
-
 
419
        }
-
 
420
        dataClearOverlays();
-
 
421
 
-
 
422
        var bounds = new google.maps.LatLngBounds();
-
 
423
        var hasPoints = false;
-
 
424
        var visitIdx = 0;
-
 
425
        var visitCoords = [];
-
 
426
 
-
 
427
        for (var i = 0; i < visits.length; i++) {
-
 
428
            var v = visits[i];
-
 
429
            if (!v.lat || !v.lng) continue;
-
 
430
            var lat = parseFloat(v.lat), lng = parseFloat(v.lng);
-
 
431
            if (isNaN(lat) || isNaN(lng) || (lat === 0 && lng === 0)) continue;
-
 
432
            hasPoints = true;
-
 
433
            var pos = { lat: lat, lng: lng };
-
 
434
            bounds.extend(pos);
-
 
435
            visitCoords.push(pos);
-
 
436
            var label = v.markType === 'PUNCHIN' ? 'P' : (v.markType === 'PUNCHOUT' ? 'X' : String(++visitIdx));
-
 
437
            var marker = new google.maps.Marker({
-
 
438
                position: pos, map: dataLiveMap,
-
 
439
                icon: { url: dataCreateMarkerIcon(dataGetMarkerColor(v.markType), label), scaledSize: new google.maps.Size(28, 40), anchor: new google.maps.Point(14, 40) },
-
 
440
                title: v.taskName + ' (' + v.markType + ')', zIndex: 100
-
 
441
            });
-
 
442
            var infoContent = '<div style="font-size:13px; max-width:280px;">' +
-
 
443
                '<strong>' + (v.taskName || '') + '</strong><br>' +
-
 
444
                '<span class="label" style="background:' + dataGetMarkerColor(v.markType) + '; color:white;">' + v.markType + '</span>' +
-
 
445
                (v.taskType ? ' <span class="label label-default">' + v.taskType + '</span>' : '') + '<br>' +
-
 
446
                (v.checkInTime ? '<i class="fa fa-sign-in" style="color:#27ae60;"></i> In: <strong>' + v.checkInTime + '</strong><br>' : '') +
-
 
447
                (v.checkOutTime ? '<i class="fa fa-sign-out" style="color:#e74c3c;"></i> Out: <strong>' + v.checkOutTime + '</strong><br>' : '') +
-
 
448
                (v.timeSpent ? '<i class="fa fa-clock-o" style="color:#f39c12;"></i> Time Spent: <strong>' + v.timeSpent + '</strong><br>' : '') +
-
 
449
                (v.transitTime ? '<i class="fa fa-car" style="color:#3498db;"></i> Transit: <strong>' + v.transitTime + '</strong><br>' : '') +
-
 
450
                (v.totalDistance && v.totalDistance !== '0.0' ? '<i class="fa fa-road" style="color:#8e44ad;"></i> Distance: <strong>' + v.totalDistance + ' km</strong><br>' : '') +
-
 
451
                (v.address ? '<i class="fa fa-map-marker" style="color:#e74c3c;"></i> ' + v.address : '') + '</div>';
-
 
452
            (function(m, c) {
-
 
453
                var iw = new google.maps.InfoWindow({ content: c });
-
 
454
                m.addListener('click', function() { iw.open(dataLiveMap, m); });
-
 
455
            })(marker, infoContent);
-
 
456
            dataLiveMap._markers.push(marker);
-
 
457
        }
-
 
458
 
-
 
459
        if (visitCoords.length >= 2) {
-
 
460
            var origin = visitCoords[0], destination = visitCoords[visitCoords.length - 1];
-
 
461
            var waypoints = [];
-
 
462
            for (var wi = 1; wi < visitCoords.length - 1; wi++) waypoints.push({ location: visitCoords[wi], stopover: true });
-
 
463
            var ds = new google.maps.DirectionsService();
-
 
464
            var dr = new google.maps.DirectionsRenderer({
-
 
465
                map: dataLiveMap, suppressMarkers: true,
-
 
466
                polylineOptions: { strokeColor: '#8e44ad', strokeOpacity: 0.7, strokeWeight: 4 },
-
 
467
                preserveViewport: true
-
 
468
            });
-
 
469
            dataLiveMap._directionsRenderer = dr;
-
 
470
            ds.route({ origin: origin, destination: destination, waypoints: waypoints, travelMode: google.maps.TravelMode.DRIVING, optimizeWaypoints: false },
-
 
471
                function(result, status) { if (status === 'OK') dr.setDirections(result); });
-
 
472
        }
-
 
473
 
-
 
474
        var totalGpsDistanceM = 0, accuracySum = 0, accuracyCount = 0, stopsDetected = [];
-
 
475
        if (pathPoints.length > 0) {
-
 
476
            var validPoints = [];
-
 
477
            for (var j = 0; j < pathPoints.length; j++) {
-
 
478
                var pt = pathPoints[j];
-
 
479
                var pLat = parseFloat(pt.lat), pLng = parseFloat(pt.lng);
-
 
480
                if (isNaN(pLat) || isNaN(pLng) || (pLat === 0 && pLng === 0)) continue;
-
 
481
                var acc = parseFloat(pt.accuracy);
-
 
482
                if (!isNaN(acc)) { accuracySum += acc; accuracyCount++; }
-
 
483
                validPoints.push({ lat: pLat, lng: pLng, ts: pt.timestamp, accuracy: pt.accuracy });
-
 
484
                bounds.extend({ lat: pLat, lng: pLng });
-
 
485
                hasPoints = true;
-
 
486
            }
-
 
487
            for (var d = 1; d < validPoints.length; d++) {
-
 
488
                totalGpsDistanceM += dataHaversine(validPoints[d-1].lat, validPoints[d-1].lng, validPoints[d].lat, validPoints[d].lng);
-
 
489
            }
-
 
490
            if (validPoints.length > 0) {
-
 
491
                var pathCoords = validPoints.map(function(p) { return { lat: p.lat, lng: p.lng }; });
-
 
492
                dataLiveMap._polyline = new google.maps.Polyline({
-
 
493
                    path: pathCoords, geodesic: true, strokeColor: '#e74c3c', strokeOpacity: 0.7, strokeWeight: 3, zIndex: 50, map: dataLiveMap
-
 
494
                });
-
 
495
                dataLiveMap._gpsDots = [];
-
 
496
                for (var gi = 0; gi < validPoints.length; gi++) {
-
 
497
                    var gp = validPoints[gi];
-
 
498
                    var heading = 0;
-
 
499
                    if (gi < validPoints.length - 1) {
-
 
500
                        heading = google.maps.geometry.spherical.computeHeading(
-
 
501
                            new google.maps.LatLng(gp.lat, gp.lng), new google.maps.LatLng(validPoints[gi+1].lat, validPoints[gi+1].lng));
-
 
502
                    } else if (gi > 0) {
-
 
503
                        heading = google.maps.geometry.spherical.computeHeading(
-
 
504
                            new google.maps.LatLng(validPoints[gi-1].lat, validPoints[gi-1].lng), new google.maps.LatLng(gp.lat, gp.lng));
-
 
505
                    }
-
 
506
                    var dot = new google.maps.Marker({
-
 
507
                        position: { lat: gp.lat, lng: gp.lng }, map: dataLiveMap,
-
 
508
                        icon: { path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW, scale: 3, fillColor: '#e74c3c', fillOpacity: 1, strokeColor: '#b71c1c', strokeWeight: 1, rotation: heading },
-
 
509
                        zIndex: 80
-
 
510
                    });
-
 
511
                    (function(marker, point, idx) {
-
 
512
                        var iw = new google.maps.InfoWindow({
-
 
513
                            content: '<div style="font-size:12px;"><strong>GPS Point #' + (idx+1) + '</strong><br>' +
-
 
514
                                '<i class="fa fa-clock-o"></i> ' + dataFormatEpoch(point.ts) + '<br>' +
-
 
515
                                '<i class="fa fa-crosshairs"></i> Accuracy: ' + (point.accuracy || 'N/A') + 'm<br>' +
-
 
516
                                '<i class="fa fa-map-marker"></i> ' + point.lat.toFixed(6) + ', ' + point.lng.toFixed(6) + '</div>'
-
 
517
                        });
-
 
518
                        marker.addListener('click', function() { iw.open(dataLiveMap, marker); });
-
 
519
                    })(dot, gp, gi);
-
 
520
                    dataLiveMap._gpsDots.push(dot);
-
 
521
                }
-
 
522
                stopsDetected = dataDetectStops(validPoints, 50, 5 * 60 * 1000);
-
 
523
                dataLiveMap._stopMarkers = [];
-
 
524
                for (var si = 0; si < stopsDetected.length; si++) {
-
 
525
                    var stop = stopsDetected[si];
-
 
526
                    var sm = new google.maps.Marker({
-
 
527
                        position: { lat: stop.lat, lng: stop.lng }, map: dataLiveMap,
-
 
528
                        icon: { path: google.maps.SymbolPath.CIRCLE, scale: 14, fillColor: '#f39c12', fillOpacity: 0.9, strokeColor: '#e67e22', strokeWeight: 2 },
-
 
529
                        label: { text: dataFormatDuration(stop.duration), fontSize: '9px', fontWeight: 'bold', color: '#000' },
-
 
530
                        title: 'Stopped for ' + dataFormatDuration(stop.duration), zIndex: 200
-
 
531
                    });
-
 
532
                    (function(marker, s) {
-
 
533
                        var iw = new google.maps.InfoWindow({
-
 
534
                            content: '<div style="font-size:13px;"><strong><i class="fa fa-pause-circle" style="color:#f39c12;"></i> Stationary Stop</strong><br>' +
-
 
535
                                '<i class="fa fa-clock-o"></i> Duration: <strong>' + dataFormatDuration(s.duration) + '</strong><br>' +
-
 
536
                                '<i class="fa fa-sign-in"></i> From: <strong>' + new Date(s.startTime).toLocaleTimeString() + '</strong><br>' +
-
 
537
                                '<i class="fa fa-sign-out"></i> To: <strong>' + new Date(s.endTime).toLocaleTimeString() + '</strong><br>' +
-
 
538
                                '<i class="fa fa-map-marker"></i> ' + s.lat.toFixed(6) + ', ' + s.lng.toFixed(6) + '<br>' +
-
 
539
                                '<i class="fa fa-dot-circle-o"></i> Points in cluster: ' + s.pointCount + '</div>'
-
 
540
                        });
-
 
541
                        marker.addListener('click', function() { iw.open(dataLiveMap, marker); });
-
 
542
                    })(sm, stop);
-
 
543
                    dataLiveMap._stopMarkers.push(sm);
-
 
544
                }
-
 
545
                var lastPoint = pathCoords[pathCoords.length - 1];
-
 
546
                dataLiveMap._currentMarker = new google.maps.Marker({
-
 
547
                    position: lastPoint, map: dataLiveMap,
-
 
548
                    icon: { url: '${rc.contextPath}/resources/images/Bike-Icon-SD.png', scaledSize: new google.maps.Size(40, 40), anchor: new google.maps.Point(20, 20) },
-
 
549
                    title: 'Last Known Location', zIndex: 999
-
 
550
                });
-
 
551
                var lastGp = validPoints[validPoints.length - 1];
-
 
552
                var curIw = new google.maps.InfoWindow({
-
 
553
                    content: '<div style="font-size:13px;"><strong><i class="fa fa-crosshairs" style="color:#27ae60;"></i> Last Known Location</strong><br>' +
-
 
554
                        '<i class="fa fa-clock-o"></i> Time: ' + dataFormatEpoch(lastGp.ts) + '<br>' +
-
 
555
                        '<i class="fa fa-crosshairs"></i> Accuracy: ' + (lastGp.accuracy || 'N/A') + 'm</div>'
-
 
556
                });
-
 
557
                dataLiveMap._currentMarker.addListener('click', function() { curIw.open(dataLiveMap, dataLiveMap._currentMarker); });
-
 
558
            }
-
 
559
        }
-
 
560
 
-
 
561
        if (hasPoints) dataLiveMap.fitBounds(bounds);
-
 
562
 
-
 
563
        var totalVisits = visits.filter(function(v) { return v.markType !== 'PUNCHIN' && v.markType !== 'PUNCHOUT'; }).length;
-
 
564
        var completedVisits = visits.filter(function(v) { return v.markType === 'CHECKIN-CHECKOUT' || v.markType === 'CHECKOUT'; }).length;
-
 
565
        var now = new Date();
-
 
566
        var timeStr = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0') + ':' + now.getSeconds().toString().padStart(2,'0');
-
 
567
        var avgAccuracy = accuracyCount > 0 ? (accuracySum / accuracyCount).toFixed(0) : null;
-
 
568
        var accuracyLabel = '', accuracyBadge = '';
-
 
569
        if (avgAccuracy) {
-
 
570
            if (avgAccuracy <= 10) { accuracyLabel = 'High'; accuracyBadge = 'background:#27ae60;'; }
-
 
571
            else if (avgAccuracy <= 30) { accuracyLabel = 'Good'; accuracyBadge = 'background:#2ecc71;'; }
-
 
572
            else if (avgAccuracy <= 100) { accuracyLabel = 'Average'; accuracyBadge = 'background:#f39c12;'; }
-
 
573
            else { accuracyLabel = 'Low'; accuracyBadge = 'background:#e74c3c;'; }
-
 
574
        }
-
 
575
        var totalGpsKm = (totalGpsDistanceM / 1000).toFixed(2);
-
 
576
        var infoHtml = '<strong>' + username + '</strong> | Visits: <strong>' + totalVisits + '</strong> | Completed: <strong>' + completedVisits + '</strong>';
-
 
577
        if (pathPoints.length > 0) {
-
 
578
            infoHtml += ' | GPS Distance: <strong>' + totalGpsKm + ' km</strong>';
-
 
579
            infoHtml += ' | Points: <strong>' + pathPoints.length + '</strong>';
-
 
580
            if (accuracyLabel) infoHtml += ' | Accuracy: <span class="label" style="' + accuracyBadge + ' color:white; padding:2px 8px; border-radius:3px; font-size:11px;">' + accuracyLabel + '</span> (' + avgAccuracy + 'm)';
-
 
581
            if (stopsDetected.length > 0) infoHtml += ' | <span style="color:#f39c12;"><i class="fa fa-pause-circle"></i></span> Stops: <strong>' + stopsDetected.length + '</strong>';
-
 
582
            infoHtml += ' | <img src="${rc.contextPath}/resources/images/Bike-Icon-SD.png" style="height:16px; vertical-align:middle;"> Last Known';
-
 
583
        } else {
-
 
584
            infoHtml += ' | <span style="color:#999;"><i class="fa fa-exclamation-triangle"></i> No GPS data yet</span>';
-
 
585
        }
-
 
586
        infoHtml += ' | <span style="color:#e74c3c;"><i class="fa fa-arrow-right" style="font-size:10px;"></i></span> GPS Trail';
-
 
587
        if (visitCoords.length > 1) infoHtml += ' | <span style="background:#8e44ad; display:inline-block; width:14px; height:3px; vertical-align:middle; margin:0 2px;"></span> Route';
-
 
588
        var locToggle = data.locationToggleCount || 0;
-
 
589
        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>';
-
 
590
        infoHtml += ' | <span style="color:#999;">Updated: ' + timeStr + '</span>';
-
 
591
        $('#dataLiveInfo').html(infoHtml);
-
 
592
        if (pathPoints.length > 0 && pathPoints.length < 10) {
-
 
593
            $('#dataLiveInfo').append(
-
 
594
                '<div style="margin-top:4px; padding:4px 8px; background:#fff3cd; border:1px solid #ffc107; border-radius:3px; font-size:11px; color:#856404;">' +
-
 
595
                '<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>');
-
 
596
        }
-
 
597
    }
-
 
598
 
-
 
599
    function dataStartAutoRefresh() {
-
 
600
        dataStopAutoRefresh();
-
 
601
        if ($('#dataAutoRefresh').is(':checked') && dataLiveUserId) {
-
 
602
            dataLiveInterval = setInterval(function() { dataLoadLiveTracking(dataLiveUserId, dataLiveUsername); }, 30000);
-
 
603
        }
-
 
604
    }
-
 
605
    function dataStopAutoRefresh() {
-
 
606
        if (dataLiveInterval) { clearInterval(dataLiveInterval); dataLiveInterval = null; }
-
 
607
    }
-
 
608
 
-
 
609
    $(document).on('click', '.data-live-tracking-btn', function() {
-
 
610
        dataLiveUserId = $(this).data('userid');
-
 
611
        dataLiveUsername = $(this).data('username');
-
 
612
        $('#dataLiveTitle').html('<img src="${rc.contextPath}/resources/images/Bike-Icon-SD.png" style="height:24px; vertical-align:middle; margin-right:6px;"> Live Tracking - ' + dataLiveUsername);
-
 
613
        $('#dataLiveInfo').html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');
-
 
614
        $('#dataLiveModal').modal('show');
-
 
615
        setTimeout(function() {
-
 
616
            dataLiveMap = null;
-
 
617
            if (window.google && window.google.maps) {
-
 
618
                dataLoadLiveTracking(dataLiveUserId, dataLiveUsername);
-
 
619
                dataStartAutoRefresh();
-
 
620
            } else {
-
 
621
                var s = document.createElement('script');
-
 
622
                s.src = 'https://maps.googleapis.com/maps/api/js?key=AIzaSyAckO0y4Z6WhBOuMjNjioWLSYZDhGEvGBc&libraries=geometry&v=weekly';
-
 
623
                s.onload = function() { dataLoadLiveTracking(dataLiveUserId, dataLiveUsername); dataStartAutoRefresh(); };
-
 
624
                document.head.appendChild(s);
-
 
625
            }
-
 
626
        }, 300);
-
 
627
    });
-
 
628
 
-
 
629
    $(document).on('click', '#dataRefreshBtn', function() {
-
 
630
        if (dataLiveUserId) {
-
 
631
            $('#dataLiveInfo').html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Refreshing...</div>');
-
 
632
            dataLoadLiveTracking(dataLiveUserId, dataLiveUsername);
-
 
633
        }
-
 
634
    });
-
 
635
    $(document).on('change', '#dataAutoRefresh', function() {
-
 
636
        $(this).is(':checked') ? dataStartAutoRefresh() : dataStopAutoRefresh();
-
 
637
    });
-
 
638
    $('#dataLiveModal').on('hidden.bs.modal', function() { dataStopAutoRefresh(); dataLiveUserId = null; });
-
 
639
</script>
295
</script>
640
 
-
 
641
<!-- Live Tracking Modal -->
-
 
642
<div class="modal fade" id="dataLiveModal" tabindex="-1" role="dialog">
-
 
643
    <div class="modal-dialog modal-lg" role="document" style="width:90%;">
-
 
644
        <div class="modal-content">
-
 
645
            <div class="modal-header" style="background:#27ae60; color:white;">
-
 
646
                <button type="button" class="close" data-dismiss="modal" style="color:white;">&times;</button>
-
 
647
                <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>
-
 
648
            </div>
-
 
649
            <div class="modal-body" style="padding:0;">
-
 
650
                <div id="dataLiveInfo" style="padding:8px 15px; background:#f8f9fa; border-bottom:1px solid #ddd; font-size:13px;"></div>
-
 
651
                <div id="dataLiveMap" style="height:500px; width:100%;"></div>
-
 
652
            </div>
-
 
653
            <div class="modal-footer">
-
 
654
                <label style="float:left; margin-top:7px; font-weight:normal; font-size:13px;">
-
 
655
                    <input type="checkbox" id="dataAutoRefresh" checked style="margin-right:5px;"> Auto-refresh (30s)
-
 
656
                </label>
-
 
657
                <button type="button" class="btn btn-primary" id="dataRefreshBtn"><i class="fa fa-refresh"></i> Refresh</button>
-
 
658
                <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
-
 
659
            </div>
-
 
660
        </div>
-
 
661
    </div>
-
 
662
</div>
-