Subversion Repositories SmartDukaan

Rev

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

Rev 36682 Rev 36685
Line 217... Line 217...
217
</div>
217
</div>
218
 
218
 
219
<script>
219
<script>
220
    $('#userDetailTable').DataTable({
220
    $('#userDetailTable').DataTable({
221
        "scrollX": true,
221
        "scrollX": true,
-
 
222
        "pageLength": 50,
222
        "bPaginate": true,
223
        "bPaginate": true,
223
        "bLengthChange": true,
224
        "bLengthChange": true,
224
        "bFilter": true,
225
        "bFilter": true,
225
        "bInfo": true,
226
        "bInfo": true,
226
        "bAutoWidth": false
227
        "bAutoWidth": false
Line 298... Line 299...
298
                $('#liveTrackingInfo').html('<span style="color:red;">Failed to load tracking data.</span>');
299
                $('#liveTrackingInfo').html('<span style="color:red;">Failed to load tracking data.</span>');
299
            }
300
            }
300
        });
301
        });
301
    }
302
    }
302
 
303
 
-
 
304
    function clearMapOverlays() {
-
 
305
        if (!liveTrackingMap) return;
-
 
306
        if (liveTrackingMap._markers) {
-
 
307
            liveTrackingMap._markers.forEach(function (m) { m.setMap(null); });
-
 
308
        }
-
 
309
        if (liveTrackingMap._polyline) {
-
 
310
            liveTrackingMap._polyline.setMap(null);
-
 
311
        }
-
 
312
        if (liveTrackingMap._gpsDots) {
-
 
313
            liveTrackingMap._gpsDots.forEach(function (m) { m.setMap(null); });
-
 
314
        }
-
 
315
        if (liveTrackingMap._stopMarkers) {
-
 
316
            liveTrackingMap._stopMarkers.forEach(function (m) { m.setMap(null); });
-
 
317
        }
-
 
318
        if (liveTrackingMap._currentMarker) {
-
 
319
            liveTrackingMap._currentMarker.setMap(null);
-
 
320
        }
-
 
321
        if (liveTrackingMap._directionsRenderer) {
-
 
322
            liveTrackingMap._directionsRenderer.setMap(null);
-
 
323
        }
-
 
324
        liveTrackingMap._markers = [];
-
 
325
        liveTrackingMap._gpsDots = [];
-
 
326
        liveTrackingMap._stopMarkers = [];
-
 
327
    }
-
 
328
 
-
 
329
    function formatEpoch(ts) {
-
 
330
        if (!ts) return 'N/A';
-
 
331
        var n = parseFloat(ts);
-
 
332
        if (isNaN(n)) return ts;
-
 
333
        if (n < 1e12) n = n * 1000;
-
 
334
        var d = new Date(n);
-
 
335
        return d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0') + ':' + d.getSeconds().toString().padStart(2,'0');
-
 
336
    }
-
 
337
 
-
 
338
    function formatDuration(ms) {
-
 
339
        var sec = Math.floor(ms / 1000);
-
 
340
        var h = Math.floor(sec / 3600);
-
 
341
        var m = Math.floor((sec % 3600) / 60);
-
 
342
        var s = sec % 60;
-
 
343
        if (h > 0) return h + 'h ' + m + 'm';
-
 
344
        if (m > 0) return m + 'm ' + s + 's';
-
 
345
        return s + 's';
-
 
346
    }
-
 
347
 
-
 
348
    function haversineMeters(lat1, lon1, lat2, lon2) {
-
 
349
        var R = 6371000;
-
 
350
        var dLat = (lat2 - lat1) * Math.PI / 180;
-
 
351
        var dLon = (lon2 - lon1) * Math.PI / 180;
-
 
352
        var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
-
 
353
                Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
-
 
354
                Math.sin(dLon/2) * Math.sin(dLon/2);
-
 
355
        return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
-
 
356
    }
-
 
357
 
-
 
358
    function detectStops(points, radiusM, minTimeMs) {
-
 
359
        var stops = [];
-
 
360
        var i = 0;
-
 
361
        while (i < points.length) {
-
 
362
            var cluster = [points[i]];
-
 
363
            var cLat = points[i].lat, cLng = points[i].lng;
-
 
364
            var j = i + 1;
-
 
365
            while (j < points.length) {
-
 
366
                if (haversineMeters(cLat, cLng, points[j].lat, points[j].lng) <= radiusM) {
-
 
367
                    cluster.push(points[j]);
-
 
368
                    j++;
-
 
369
                } else {
-
 
370
                    break;
-
 
371
                }
-
 
372
            }
-
 
373
            if (cluster.length >= 2) {
-
 
374
                var t0 = parseFloat(cluster[0].ts);
-
 
375
                var t1 = parseFloat(cluster[cluster.length - 1].ts);
-
 
376
                if (t0 < 1e12) t0 *= 1000;
-
 
377
                if (t1 < 1e12) t1 *= 1000;
-
 
378
                var duration = t1 - t0;
-
 
379
                if (duration >= minTimeMs) {
-
 
380
                    var avgLat = 0, avgLng = 0;
-
 
381
                    cluster.forEach(function(p) { avgLat += p.lat; avgLng += p.lng; });
-
 
382
                    stops.push({
-
 
383
                        lat: avgLat / cluster.length,
-
 
384
                        lng: avgLng / cluster.length,
-
 
385
                        startTime: t0,
-
 
386
                        endTime: t1,
-
 
387
                        duration: duration,
-
 
388
                        pointCount: cluster.length
-
 
389
                    });
-
 
390
                }
-
 
391
            }
-
 
392
            i = j;
-
 
393
        }
-
 
394
        return stops;
-
 
395
    }
-
 
396
 
303
    function renderLiveTrackingMap(data, username) {
397
    function renderLiveTrackingMap(data, username) {
304
        var visits = data.visits || [];
398
        var visits = data.visits || [];
305
        var pathPoints = data.pathPoints || [];
399
        var pathPoints = data.pathPoints || [];
306
 
400
 
307
        if (!liveTrackingMap) {
401
        if (!liveTrackingMap) {
Line 311... Line 405...
311
                mapTypeControl: true,
405
                mapTypeControl: true,
312
                streetViewControl: false
406
                streetViewControl: false
313
            });
407
            });
314
        }
408
        }
315
 
409
 
316
        if (liveTrackingMap._markers) {
-
 
317
            liveTrackingMap._markers.forEach(function (m) { m.setMap(null); });
-
 
318
        }
-
 
319
        if (liveTrackingMap._polyline) {
-
 
320
            liveTrackingMap._polyline.setMap(null);
-
 
321
        }
-
 
322
        if (liveTrackingMap._visitRoute) {
-
 
323
            liveTrackingMap._visitRoute.setMap(null);
-
 
324
        }
-
 
325
        if (liveTrackingMap._currentMarker) {
-
 
326
            liveTrackingMap._currentMarker.setMap(null);
-
 
327
        }
-
 
328
        liveTrackingMap._markers = [];
410
        clearMapOverlays();
329
 
411
 
330
        var bounds = new google.maps.LatLngBounds();
412
        var bounds = new google.maps.LatLngBounds();
331
        var hasPoints = false;
413
        var hasPoints = false;
332
        var visitIdx = 0;
414
        var visitIdx = 0;
-
 
415
        var visitCoords = [];
333
 
416
 
334
        for (var i = 0; i < visits.length; i++) {
417
        for (var i = 0; i < visits.length; i++) {
335
            var v = visits[i];
418
            var v = visits[i];
336
            if (!v.lat || !v.lng) continue;
419
            if (!v.lat || !v.lng) continue;
337
            var lat = parseFloat(v.lat);
420
            var lat = parseFloat(v.lat);
Line 339... Line 422...
339
            if (isNaN(lat) || isNaN(lng) || (lat === 0 && lng === 0)) continue;
422
            if (isNaN(lat) || isNaN(lng) || (lat === 0 && lng === 0)) continue;
340
 
423
 
341
            hasPoints = true;
424
            hasPoints = true;
342
            var pos = { lat: lat, lng: lng };
425
            var pos = { lat: lat, lng: lng };
343
            bounds.extend(pos);
426
            bounds.extend(pos);
-
 
427
            visitCoords.push(pos);
344
 
428
 
345
            var label = v.markType === 'PUNCHIN' ? 'P' : (v.markType === 'PUNCHOUT' ? 'X' : String(++visitIdx));
429
            var label = v.markType === 'PUNCHIN' ? 'P' : (v.markType === 'PUNCHOUT' ? 'X' : String(++visitIdx));
346
            var marker = new google.maps.Marker({
430
            var marker = new google.maps.Marker({
347
                position: pos,
431
                position: pos,
348
                map: liveTrackingMap,
432
                map: liveTrackingMap,
349
                icon: {
433
                icon: {
350
                    url: createTrackingMarkerIcon(getMarkerColor(v.markType), label),
434
                    url: createTrackingMarkerIcon(getMarkerColor(v.markType), label),
351
                    scaledSize: new google.maps.Size(28, 40),
435
                    scaledSize: new google.maps.Size(28, 40),
352
                    anchor: new google.maps.Point(14, 40)
436
                    anchor: new google.maps.Point(14, 40)
353
                },
437
                },
354
                title: v.taskName + ' (' + v.markType + ')'
438
                title: v.taskName + ' (' + v.markType + ')',
-
 
439
                zIndex: 100
355
            });
440
            });
356
 
441
 
357
            var infoContent = '<div style="font-size:13px; max-width:250px;">' +
442
            var infoContent = '<div style="font-size:13px; max-width:280px;">' +
358
                '<strong>' + (v.taskName || '') + '</strong><br>' +
443
                '<strong>' + (v.taskName || '') + '</strong><br>' +
359
                '<span class="label" style="background:' + getMarkerColor(v.markType) + '; color:white;">' + v.markType + '</span><br>' +
444
                '<span class="label" style="background:' + getMarkerColor(v.markType) + '; color:white;">' + v.markType + '</span>' +
-
 
445
                (v.taskType ? ' <span class="label label-default">' + v.taskType + '</span>' : '') + '<br>' +
360
                (v.checkInTime ? '<i class="fa fa-sign-in"></i> ' + v.checkInTime + '<br>' : '') +
446
                (v.checkInTime ? '<i class="fa fa-sign-in" style="color:#27ae60;"></i> In: <strong>' + v.checkInTime + '</strong><br>' : '') +
361
                (v.checkOutTime ? '<i class="fa fa-sign-out"></i> ' + v.checkOutTime + '<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>' : '') +
362
                (v.address ? '<i class="fa fa-map-marker"></i> ' + v.address : '') +
451
                (v.address ? '<i class="fa fa-map-marker" style="color:#e74c3c;"></i> ' + v.address : '') +
363
                '</div>';
452
                '</div>';
364
            (function (m, c) {
453
            (function (m, c) {
365
                var iw = new google.maps.InfoWindow({ content: c });
454
                var iw = new google.maps.InfoWindow({ content: c });
366
                m.addListener('click', function () { iw.open(liveTrackingMap, m); });
455
                m.addListener('click', function () { iw.open(liveTrackingMap, m); });
367
            })(marker, infoContent);
456
            })(marker, infoContent);
368
 
457
 
369
            liveTrackingMap._markers.push(marker);
458
            liveTrackingMap._markers.push(marker);
370
        }
459
        }
371
 
460
 
372
        // Draw visit-to-visit route (dashed line with arrows)
461
        // Draw actual road path between visits using Directions API
373
        var visitCoords = [];
462
        if (visitCoords.length >= 2) {
374
        for (var vi = 0; vi < visits.length; vi++) {
-
 
375
            var vv = visits[vi];
463
            var origin = visitCoords[0];
376
            if (!vv.lat || !vv.lng) continue;
-
 
377
            var vLat = parseFloat(vv.lat);
464
            var destination = visitCoords[visitCoords.length - 1];
378
            var vLng = parseFloat(vv.lng);
465
            var waypoints = [];
379
            if (isNaN(vLat) || isNaN(vLng) || (vLat === 0 && vLng === 0)) continue;
466
            for (var wi = 1; wi < visitCoords.length - 1; wi++) {
380
            visitCoords.push({ lat: vLat, lng: vLng });
467
                waypoints.push({ location: visitCoords[wi], stopover: true });
381
        }
468
            }
-
 
469
 
382
        if (visitCoords.length > 1) {
470
            var directionsService = new google.maps.DirectionsService();
383
            liveTrackingMap._visitRoute = new google.maps.Polyline({
471
            var directionsRenderer = new google.maps.DirectionsRenderer({
384
                path: visitCoords,
472
                map: liveTrackingMap,
385
                geodesic: true,
473
                suppressMarkers: true,
-
 
474
                polylineOptions: {
386
                strokeColor: '#8e44ad',
475
                    strokeColor: '#8e44ad',
387
                strokeOpacity: 0,
476
                    strokeOpacity: 0.7,
388
                strokeWeight: 3,
477
                    strokeWeight: 4
389
                icons: [{
478
                },
-
 
479
                preserveViewport: true
-
 
480
            });
390
                    icon: { path: 'M 0,-1 0,1', strokeOpacity: 0.6, strokeWeight: 3, scale: 3 },
481
            liveTrackingMap._directionsRenderer = directionsRenderer;
-
 
482
 
-
 
483
            directionsService.route({
391
                    offset: '0',
484
                origin: origin,
392
                    repeat: '16px'
485
                destination: destination,
393
                }, {
486
                waypoints: waypoints,
394
                    icon: { path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW, strokeOpacity: 0.8, scale: 3 },
487
                travelMode: google.maps.TravelMode.DRIVING,
395
                    offset: '50%',
488
                optimizeWaypoints: false
396
                    repeat: '200px'
489
            }, function (result, status) {
397
                }],
490
                if (status === 'OK') {
-
 
491
                    directionsRenderer.setDirections(result);
398
                map: liveTrackingMap
492
                }
399
            });
493
            });
400
        }
494
        }
401
 
495
 
-
 
496
        // Draw GPS tracked path with red dots + connecting line
-
 
497
        var totalGpsDistanceM = 0;
-
 
498
        var accuracySum = 0;
-
 
499
        var accuracyCount = 0;
-
 
500
        var stopsDetected = [];
-
 
501
 
402
        if (pathPoints.length > 0) {
502
        if (pathPoints.length > 0) {
403
            var pathCoords = [];
503
            var validPoints = [];
404
            for (var j = 0; j < pathPoints.length; j++) {
504
            for (var j = 0; j < pathPoints.length; j++) {
405
                var pt = pathPoints[j];
505
                var pt = pathPoints[j];
406
                var pLat = parseFloat(pt.lat);
506
                var pLat = parseFloat(pt.lat);
407
                var pLng = parseFloat(pt.lng);
507
                var pLng = parseFloat(pt.lng);
408
                if (isNaN(pLat) || isNaN(pLng) || (pLat === 0 && pLng === 0)) continue;
508
                if (isNaN(pLat) || isNaN(pLng) || (pLat === 0 && pLng === 0)) continue;
-
 
509
                var acc = parseFloat(pt.accuracy);
-
 
510
                if (!isNaN(acc)) { accuracySum += acc; accuracyCount++; }
409
                pathCoords.push({ lat: pLat, lng: pLng });
511
                validPoints.push({ lat: pLat, lng: pLng, ts: pt.timestamp, accuracy: pt.accuracy });
410
                bounds.extend({ lat: pLat, lng: pLng });
512
                bounds.extend({ lat: pLat, lng: pLng });
411
                hasPoints = true;
513
                hasPoints = true;
412
            }
514
            }
413
 
515
 
-
 
516
            // Calculate total GPS distance
-
 
517
            for (var d = 1; d < validPoints.length; d++) {
-
 
518
                totalGpsDistanceM += haversineMeters(validPoints[d-1].lat, validPoints[d-1].lng, validPoints[d].lat, validPoints[d].lng);
-
 
519
            }
-
 
520
 
414
            if (pathCoords.length > 0) {
521
            if (validPoints.length > 0) {
-
 
522
                var pathCoords = validPoints.map(function(p) { return { lat: p.lat, lng: p.lng }; });
-
 
523
 
-
 
524
                // Blue connecting line
415
                liveTrackingMap._polyline = new google.maps.Polyline({
525
                liveTrackingMap._polyline = new google.maps.Polyline({
416
                    path: pathCoords,
526
                    path: pathCoords,
417
                    geodesic: true,
527
                    geodesic: true,
418
                    strokeColor: '#3498db',
528
                    strokeColor: '#e74c3c',
419
                    strokeOpacity: 0.8,
529
                    strokeOpacity: 0.7,
420
                    strokeWeight: 3,
530
                    strokeWeight: 3,
-
 
531
                    zIndex: 50,
421
                    map: liveTrackingMap
532
                    map: liveTrackingMap
422
                });
533
                });
423
 
534
 
-
 
535
                // Red dots for each GPS point
-
 
536
                liveTrackingMap._gpsDots = [];
-
 
537
                for (var gi = 0; gi < validPoints.length; gi++) {
-
 
538
                    var gp = validPoints[gi];
-
 
539
                    var dot = new google.maps.Marker({
-
 
540
                        position: { lat: gp.lat, lng: gp.lng },
-
 
541
                        map: liveTrackingMap,
-
 
542
                        icon: {
-
 
543
                            path: google.maps.SymbolPath.CIRCLE,
-
 
544
                            scale: 4,
-
 
545
                            fillColor: '#e74c3c',
-
 
546
                            fillOpacity: 1,
-
 
547
                            strokeColor: '#b71c1c',
-
 
548
                            strokeWeight: 1
-
 
549
                        },
-
 
550
                        zIndex: 80
-
 
551
                    });
-
 
552
                    (function(marker, point, idx) {
-
 
553
                        var iw = new google.maps.InfoWindow({
-
 
554
                            content: '<div style="font-size:12px;">' +
-
 
555
                                '<strong>GPS Point #' + (idx + 1) + '</strong><br>' +
-
 
556
                                '<i class="fa fa-clock-o"></i> ' + formatEpoch(point.ts) + '<br>' +
-
 
557
                                '<i class="fa fa-crosshairs"></i> Accuracy: ' + (point.accuracy || 'N/A') + 'm<br>' +
-
 
558
                                '<i class="fa fa-map-marker"></i> ' + point.lat.toFixed(6) + ', ' + point.lng.toFixed(6) +
-
 
559
                                '</div>'
-
 
560
                        });
-
 
561
                        marker.addListener('click', function() { iw.open(liveTrackingMap, marker); });
-
 
562
                    })(dot, gp, gi);
-
 
563
                    liveTrackingMap._gpsDots.push(dot);
-
 
564
                }
-
 
565
 
-
 
566
                // Detect stationary stops (within 50m for >5 min)
-
 
567
                stopsDetected = detectStops(validPoints, 50, 5 * 60 * 1000);
-
 
568
                liveTrackingMap._stopMarkers = [];
-
 
569
                for (var si = 0; si < stopsDetected.length; si++) {
-
 
570
                    var stop = stopsDetected[si];
-
 
571
                    var stopMarker = new google.maps.Marker({
-
 
572
                        position: { lat: stop.lat, lng: stop.lng },
-
 
573
                        map: liveTrackingMap,
-
 
574
                        icon: {
-
 
575
                            path: google.maps.SymbolPath.CIRCLE,
-
 
576
                            scale: 14,
-
 
577
                            fillColor: '#f39c12',
-
 
578
                            fillOpacity: 0.9,
-
 
579
                            strokeColor: '#e67e22',
-
 
580
                            strokeWeight: 2
-
 
581
                        },
-
 
582
                        label: { text: formatDuration(stop.duration), fontSize: '9px', fontWeight: 'bold', color: '#000' },
-
 
583
                        title: 'Stopped for ' + formatDuration(stop.duration),
-
 
584
                        zIndex: 200
-
 
585
                    });
-
 
586
                    (function(marker, s) {
-
 
587
                        var iw = new google.maps.InfoWindow({
-
 
588
                            content: '<div style="font-size:13px;">' +
-
 
589
                                '<strong><i class="fa fa-pause-circle" style="color:#f39c12;"></i> Stationary Stop</strong><br>' +
-
 
590
                                '<i class="fa fa-clock-o"></i> Duration: <strong>' + formatDuration(s.duration) + '</strong><br>' +
-
 
591
                                '<i class="fa fa-sign-in"></i> From: <strong>' + new Date(s.startTime).toLocaleTimeString() + '</strong><br>' +
-
 
592
                                '<i class="fa fa-sign-out"></i> To: <strong>' + new Date(s.endTime).toLocaleTimeString() + '</strong><br>' +
-
 
593
                                '<i class="fa fa-map-marker"></i> ' + s.lat.toFixed(6) + ', ' + s.lng.toFixed(6) + '<br>' +
-
 
594
                                '<i class="fa fa-dot-circle-o"></i> Points in cluster: ' + s.pointCount +
-
 
595
                                '</div>'
-
 
596
                        });
-
 
597
                        marker.addListener('click', function() { iw.open(liveTrackingMap, marker); });
-
 
598
                    })(stopMarker, stop);
-
 
599
                    liveTrackingMap._stopMarkers.push(stopMarker);
-
 
600
                }
-
 
601
 
-
 
602
                // Last known location marker
424
                var lastPoint = pathCoords[pathCoords.length - 1];
603
                var lastPoint = pathCoords[pathCoords.length - 1];
425
                liveTrackingMap._currentMarker = new google.maps.Marker({
604
                liveTrackingMap._currentMarker = new google.maps.Marker({
426
                    position: lastPoint,
605
                    position: lastPoint,
427
                    map: liveTrackingMap,
606
                    map: liveTrackingMap,
428
                    icon: {
607
                    icon: {
429
                        path: google.maps.SymbolPath.CIRCLE,
608
                        path: google.maps.SymbolPath.CIRCLE,
430
                        scale: 10,
609
                        scale: 10,
431
                        fillColor: '#e74c3c',
610
                        fillColor: '#27ae60',
432
                        fillOpacity: 1,
611
                        fillOpacity: 1,
433
                        strokeColor: '#ffffff',
612
                        strokeColor: '#ffffff',
434
                        strokeWeight: 3
613
                        strokeWeight: 3
435
                    },
614
                    },
436
                    title: 'Last Known Location',
615
                    title: 'Last Known Location',
437
                    zIndex: 999
616
                    zIndex: 999
438
                });
617
                });
439
 
-
 
440
                var lastTs = pathPoints[pathPoints.length - 1].timestamp || 'N/A';
618
                var lastGp = validPoints[validPoints.length - 1];
441
                var currentIw = new google.maps.InfoWindow({
619
                var currentIw = new google.maps.InfoWindow({
442
                    content: '<div style="font-size:13px;"><strong><i class="fa fa-crosshairs"></i> Last Known Location</strong><br>Time: ' + lastTs + '</div>'
620
                    content: '<div style="font-size:13px;"><strong><i class="fa fa-crosshairs" style="color:#27ae60;"></i> Last Known Location</strong><br>' +
-
 
621
                        '<i class="fa fa-clock-o"></i> Time: ' + formatEpoch(lastGp.ts) + '<br>' +
-
 
622
                        '<i class="fa fa-crosshairs"></i> Accuracy: ' + (lastGp.accuracy || 'N/A') + 'm</div>'
443
                });
623
                });
444
                liveTrackingMap._currentMarker.addListener('click', function () {
624
                liveTrackingMap._currentMarker.addListener('click', function () {
445
                    currentIw.open(liveTrackingMap, liveTrackingMap._currentMarker);
625
                    currentIw.open(liveTrackingMap, liveTrackingMap._currentMarker);
446
                });
626
                });
447
            }
627
            }
Line 449... Line 629...
449
 
629
 
450
        if (hasPoints) {
630
        if (hasPoints) {
451
            liveTrackingMap.fitBounds(bounds);
631
            liveTrackingMap.fitBounds(bounds);
452
        }
632
        }
453
 
633
 
-
 
634
        // Info bar
454
        var totalVisits = visits.filter(function (v) { return v.markType !== 'PUNCHIN' && v.markType !== 'PUNCHOUT'; }).length;
635
        var totalVisits = visits.filter(function (v) { return v.markType !== 'PUNCHIN' && v.markType !== 'PUNCHOUT'; }).length;
455
        var completedVisits = visits.filter(function (v) { return v.markType === 'CHECKIN-CHECKOUT' || v.markType === 'CHECKOUT'; }).length;
636
        var completedVisits = visits.filter(function (v) { return v.markType === 'CHECKIN-CHECKOUT' || v.markType === 'CHECKOUT'; }).length;
456
        var now = new Date();
637
        var now = new Date();
457
        var timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0') + ':' + now.getSeconds().toString().padStart(2, '0');
638
        var timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0') + ':' + now.getSeconds().toString().padStart(2, '0');
-
 
639
 
-
 
640
        var avgAccuracy = accuracyCount > 0 ? (accuracySum / accuracyCount).toFixed(0) : null;
-
 
641
        var accuracyLabel = '';
458
        $('#liveTrackingInfo').html(
642
        var accuracyBadge = '';
-
 
643
        if (avgAccuracy) {
-
 
644
            if (avgAccuracy <= 10) { accuracyLabel = 'High'; accuracyBadge = 'background:#27ae60;'; }
-
 
645
            else if (avgAccuracy <= 30) { accuracyLabel = 'Good'; accuracyBadge = 'background:#2ecc71;'; }
-
 
646
            else if (avgAccuracy <= 100) { accuracyLabel = 'Average'; accuracyBadge = 'background:#f39c12;'; }
-
 
647
            else { accuracyLabel = 'Low'; accuracyBadge = 'background:#e74c3c;'; }
-
 
648
        }
-
 
649
 
-
 
650
        var totalGpsKm = (totalGpsDistanceM / 1000).toFixed(2);
-
 
651
 
459
            '<strong>' + username + '</strong> | ' +
652
        var infoHtml = '<strong>' + username + '</strong> | ' +
460
            'Visits: <strong>' + totalVisits + '</strong> | ' +
653
            'Visits: <strong>' + totalVisits + '</strong> | ' +
461
            'Completed: <strong>' + completedVisits + '</strong> | ' +
654
            'Completed: <strong>' + completedVisits + '</strong>';
-
 
655
 
-
 
656
        if (pathPoints.length > 0) {
-
 
657
            infoHtml += ' | GPS Distance: <strong>' + totalGpsKm + ' km</strong>';
462
            'Path Points: <strong>' + pathPoints.length + '</strong>' +
658
            infoHtml += ' | Points: <strong>' + pathPoints.length + '</strong>';
-
 
659
            if (accuracyLabel) {
-
 
660
                infoHtml += ' | Accuracy: <span class="label" style="' + accuracyBadge + ' color:white; padding:2px 8px; border-radius:3px; font-size:11px;">' + accuracyLabel + '</span> (' + avgAccuracy + 'm)';
-
 
661
            }
-
 
662
            if (stopsDetected.length > 0) {
-
 
663
                infoHtml += ' | <span style="color:#f39c12;"><i class="fa fa-pause-circle"></i></span> Stops: <strong>' + stopsDetected.length + '</strong>';
-
 
664
            }
463
            (pathPoints.length > 0 ? ' | <span style="color:#e74c3c;"><i class="fa fa-circle"></i></span> Last Known' : '') +
665
            infoHtml += ' | <span style="color:#27ae60;"><i class="fa fa-circle"></i></span> Last Known';
-
 
666
        } else {
464
            ' | <span style="color:#3498db;">━</span> GPS Path' +
667
            infoHtml += ' | <span style="color:#999;"><i class="fa fa-exclamation-triangle"></i> No GPS data yet</span>';
-
 
668
        }
-
 
669
 
465
            (visitCoords.length > 1 ? ' | <span style="color:#8e44ad;">┅</span> Visit Route' : '') +
670
        infoHtml += ' | <span style="color:#e74c3c;"><i class="fa fa-circle" style="font-size:8px;"></i></span> GPS Trail';
-
 
671
        if (visitCoords.length > 1) {
-
 
672
            infoHtml += ' | <span style="background:#8e44ad; display:inline-block; width:14px; height:3px; vertical-align:middle; margin:0 2px;"></span> Route';
-
 
673
        }
466
            ' | <span style="color:#999;">Updated: ' + timeStr + '</span>'
674
        infoHtml += ' | <span style="color:#999;">Updated: ' + timeStr + '</span>';
-
 
675
 
-
 
676
        $('#liveTrackingInfo').html(infoHtml);
-
 
677
 
-
 
678
        // Low data warning
-
 
679
        if (pathPoints.length > 0 && pathPoints.length < 10) {
-
 
680
            $('#liveTrackingInfo').append(
-
 
681
                '<div style="margin-top:4px; padding:4px 8px; background:#fff3cd; border:1px solid #ffc107; border-radius:3px; font-size:11px; color:#856404;">' +
-
 
682
                '<i class="fa fa-exclamation-triangle"></i> Received less data points from device due to: Low Battery level, ' +
-
 
683
                'GPS/location switched off, or phone automatically switched off the location service.' +
-
 
684
                '</div>'
-
 
685
            );
467
        );
686
        }
468
    }
687
    }
469
 
688
 
470
    function ensureGoogleMaps(callback) {
689
    function ensureGoogleMaps(callback) {
471
        if (window.google && window.google.maps) {
690
        if (window.google && window.google.maps) {
472
            callback();
691
            callback();