Subversion Repositories SmartDukaan

Rev

Rev 36688 | Rev 36709 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
36645 vikas 1
<section class="wrapper">
2
    <div class="row">
3
        <div class="col-lg-12">
4
            <h3 class="page-header"><i class="icon_document_alt"></i> $username - Visit Details ($reportDate)</h3>
5
            <ol class="breadcrumb">
6
                <li><i class="fa fa-home"></i><a href="${rc.contextPath}/dashboard">Home</a></li>
7
                <li><a href="javascript:void(0);" class="beat-report">Beat Report</a></li>
8
                <li><i class="fa fa-user"></i>$username</li>
9
            </ol>
10
        </div>
11
    </div>
12
    <br>
13
    <div class="row col-lg-12" style="float: right;">
36664 vikas 14
        <div class="col-lg-2">
15
            <label>Level</label>
16
            <select id="user-detail-level" class="form-control" data-selected="$!userLevel">
17
                <option value="">All Levels</option>
18
                #foreach($et in $escalationTypes)
19
                    <option value="$et" #if($et.name() == $userLevel)selected#end>$et</option>
20
                #end
21
            </select>
22
        </div>
36645 vikas 23
        <div class="col-lg-3">
36664 vikas 24
            <label>User</label>
25
            <select id="user-detail-user" class="form-control" data-selected-auth="$!authUserId">
26
                <option value="$!authUserId">$username</option>
27
            </select>
28
        </div>
29
        <div class="col-lg-3">
36645 vikas 30
            <label>Select Date</label>
31
            <div class="input-group">
32
                <input type="date" id="user-detail-date" class="form-control" value="$reportDate" data-userid="$userId">
33
                <span class="input-group-btn">
34
                    <button type="button" class="btn btn-info user-detail-refresh" data-userid="$userId" style="margin-left:10px;">
35
                        <i class="fa fa-refresh"></i> Refresh
36
                    </button>
36664 vikas 37
                    <button type="button" class="btn btn-success live-tracking-btn" data-userid="$userId" data-username="$username" style="margin-left:5px;">
38
                        <i class="fa fa-map-marker"></i> Live Tracking
39
                    </button>
36645 vikas 40
                </span>
41
            </div>
42
        </div>
43
    </div>
44
    <br><br><br>
45
 
46
    <div class="col-lg-12">
47
        <div class="row" style="margin-bottom:15px;">
48
            <div class="col-lg-3">
49
                <div class="panel panel-default" style="text-align:center; padding:10px;">
50
                    <h4 style="margin:0; color:#1abc9c; font-weight:bold;">$visits.size()</h4>
51
                    <small>Total Visits</small>
52
                </div>
53
            </div>
54
            #set($completedCount = 0)
55
            #set($pendingCount = 0)
56
            #set($deferredCount = 0)
57
            #foreach($v in $visits)
58
                #if($v.getMarkType() == "CHECKIN-CHECKOUT" || $v.getMarkType() == "CHECKOUT")
59
                    #set($completedCount = $completedCount + 1)
60
                #elseif($v.getMarkType() == "PENDING" || $v.getMarkType() == "CHECKIN")
61
                    #set($pendingCount = $pendingCount + 1)
62
                #elseif($v.getMarkType() == "DEFERRED")
63
                    #set($deferredCount = $deferredCount + 1)
64
                #end
65
            #end
66
            <div class="col-lg-3">
67
                <div class="panel panel-default" style="text-align:center; padding:10px;">
68
                    <h4 style="margin:0; color:#27ae60; font-weight:bold;">$completedCount</h4>
69
                    <small>Completed</small>
70
                </div>
71
            </div>
72
            <div class="col-lg-3">
73
                <div class="panel panel-default" style="text-align:center; padding:10px;">
74
                    <h4 style="margin:0; color:#e67e22; font-weight:bold;">$pendingCount</h4>
75
                    <small>Pending</small>
76
                </div>
77
            </div>
78
            <div class="col-lg-3">
79
                <div class="panel panel-default" style="text-align:center; padding:10px;">
80
                    <h4 style="margin:0; color:#e74c3c; font-weight:bold;">$deferredCount</h4>
81
                    <small>Deferred</small>
82
                </div>
83
            </div>
84
        </div>
85
 
86
        <table class="table table-border table-condensed table-bordered table-striped" id="userDetailTable" style="width:100%">
87
            <thead>
36656 vikas 88
            <tr >
36645 vikas 89
                <th>#</th>
36666 vikas 90
                <th>Agenda/Partner Name</th>
36645 vikas 91
                <th>Task Type</th>
92
                <th>Status</th>
93
                <th>Address</th>
94
                <th>Check In Time</th>
95
                <th>Check Out Time</th>
36666 vikas 96
                <th>Time Spent at Outlet</th>
36645 vikas 97
                <th>Transit Time</th>
98
                <th>Distance (km)</th>
36666 vikas 99
                <th>Estimated Time</th>
36645 vikas 100
            </tr>
101
            </thead>
102
            <tbody>
36656 vikas 103
            #if($punchIn)
104
                <tr style="background:#d5f5e3;">
105
                    <td>--</td>
106
                    <td><strong>PUNCH IN</strong></td>
107
                    <td>$!punchIn.getTaskType()</td>
108
                    <td><span class="label label-info">PUNCHIN</span> #if($punchIn.getAttachment() && $punchIn.getAttachment() != "")<a href="https://partners.smartdukaan.com/document/$punchIn.getAttachment()" target="_blank" title="View Attachment"><i class="fa fa-paperclip" style="color:#e67e22; margin-left:5px;"></i></a>#end</td>
109
                    <td>$!punchIn.getAddress()</td>
110
                    <td>$!punchIn.getCheckInTime()</td>
111
                    <td>--</td>
112
                    <td>--</td>
113
                    <td>--</td>
114
                    <td>$!punchIn.getTotalDistance()</td>
36666 vikas 115
                    <td>$!punchIn.getEstimatedTime()</td>
36656 vikas 116
                </tr>
117
            #end
36645 vikas 118
            #set($idx = 1)
119
            #foreach($v in $visits)
120
                <tr>
121
                    <td>$idx</td>
122
                    <td>$!v.getTaskName()</td>
123
                    <td>$!v.getTaskType()</td>
124
                    <td>
125
                        #if($v.getMarkType() == "CHECKIN-CHECKOUT" || $v.getMarkType() == "CHECKOUT")
126
                            <span class="label label-success">$v.getMarkType()</span>
127
                        #elseif($v.getMarkType() == "PENDING" || $v.getMarkType() == "CHECKIN")
128
                            <span class="label label-warning">$v.getMarkType()</span>
129
                        #elseif($v.getMarkType() == "DEFERRED")
130
                            <span class="label label-danger">$v.getMarkType()</span>
131
                        #else
132
                            <span class="label label-default">$!v.getMarkType()</span>
133
                        #end
36656 vikas 134
                        <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>
135
                        #if($v.getAttachment() && $v.getAttachment() != "")<a href="https://partners.smartdukaan.com/document/$v.getAttachment()" target="_blank" title="View Attachment"><i class="fa fa-paperclip" style="color:#e67e22; margin-left:3px; font-size:14px;"></i></a>#end
36645 vikas 136
                    </td>
137
                    <td>$!v.getAddress()</td>
138
                    <td>$!v.getCheckInTime()</td>
139
                    <td>$!v.getCheckOutTime()</td>
140
                    <td>$!v.getTimeSpent()</td>
141
                    <td>$!v.getTransitTime()</td>
142
                    <td>$!v.getTotalDistance()</td>
36666 vikas 143
                    <td>$!v.getEstimatedTime()</td>
36645 vikas 144
                </tr>
145
                #set($idx = $idx + 1)
146
            #end
36656 vikas 147
            #if($punchOut)
148
                <tr style="background:#fadbd8;">
149
                    <td>--</td>
150
                    <td><strong>PUNCH OUT</strong></td>
151
                    <td>$!punchOut.getTaskType()</td>
152
                    <td><span class="label label-danger">PUNCHOUT</span> #if($punchOut.getAttachment() && $punchOut.getAttachment() != "")<a href="https://partners.smartdukaan.com/document/$punchOut.getAttachment()" target="_blank" title="View Attachment"><i class="fa fa-paperclip" style="color:#e67e22; margin-left:5px;"></i></a>#end</td>
153
                    <td>$!punchOut.getAddress()</td>
154
                    <td>$!punchOut.getCheckInTime()</td>
155
                    <td>--</td>
156
                    <td>--</td>
157
                    <td>--</td>
158
                    <td>$!punchOut.getTotalDistance()</td>
36666 vikas 159
                    <td>$!punchOut.getEstimatedTime()</td>
36656 vikas 160
                </tr>
161
            #end
36645 vikas 162
            </tbody>
36656 vikas 163
            <tfoot>
164
                <tr style="background:#f2f3f4; font-weight:bold;">
165
                    <td colspan="5" style="text-align:right;">Totals</td>
166
                    <td>--</td>
167
                    <td>--</td>
168
                    <td>$totalTimeSpentStr</td>
169
                    <td>$totalTransitStr</td>
170
                    <td>$totalDistanceStr km | Visits: $visitCount | Leads: $leadCount</td>
36666 vikas 171
                    <td></td>
36656 vikas 172
                </tr>
173
            </tfoot>
36645 vikas 174
        </table>
175
    </div>
176
</section>
177
 
36656 vikas 178
<!-- Task History Modal -->
179
<div class="modal fade" id="taskHistoryModal" tabindex="-1" role="dialog">
180
    <div class="modal-dialog modal-lg" role="document" style="width:85%;">
181
        <div class="modal-content">
182
            <div class="modal-header" style="background:#3498db; color:white;">
183
                <button type="button" class="close" data-dismiss="modal" style="color:white;">&times;</button>
184
                <h4 class="modal-title" id="taskHistoryModalTitle">Task History</h4>
185
            </div>
186
            <div class="modal-body" id="taskHistoryModalBody">
187
                <div class="text-center"><i class="fa fa-spinner fa-spin fa-2x"></i> Loading...</div>
188
            </div>
189
            <div class="modal-footer">
190
                <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
191
            </div>
192
        </div>
193
    </div>
194
</div>
195
 
36664 vikas 196
<!-- Live Tracking Modal -->
197
<div class="modal fade" id="liveTrackingModal" tabindex="-1" role="dialog">
198
    <div class="modal-dialog modal-lg" role="document" style="width:90%;">
199
        <div class="modal-content">
200
            <div class="modal-header" style="background:#27ae60; color:white;">
201
                <button type="button" class="close" data-dismiss="modal" style="color:white;">&times;</button>
36690 vikas 202
                <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>
36664 vikas 203
            </div>
204
            <div class="modal-body" style="padding:0;">
205
                <div id="liveTrackingInfo" style="padding:8px 15px; background:#f8f9fa; border-bottom:1px solid #ddd; font-size:13px;"></div>
206
                <div id="liveTrackingMap" style="height:500px; width:100%;"></div>
207
            </div>
208
            <div class="modal-footer">
36682 vikas 209
                <label style="float:left; margin-top:7px; font-weight:normal; font-size:13px;">
210
                    <input type="checkbox" id="autoRefreshToggle" checked style="margin-right:5px;"> Auto-refresh (30s)
211
                </label>
36664 vikas 212
                <button type="button" class="btn btn-primary" id="refreshTrackingBtn"><i class="fa fa-refresh"></i> Refresh</button>
213
                <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
214
            </div>
215
        </div>
216
    </div>
217
</div>
218
 
36645 vikas 219
<script>
220
    $('#userDetailTable').DataTable({
221
        "scrollX": true,
36685 vikas 222
        "pageLength": 50,
36645 vikas 223
        "bPaginate": true,
224
        "bLengthChange": true,
225
        "bFilter": true,
226
        "bInfo": true,
227
        "bAutoWidth": false
228
    });
36656 vikas 229
 
36664 vikas 230
    (function () {
231
        var level = $('#user-detail-level').data('selected');
232
        var selectedAuth = $('#user-detail-user').data('selected-auth');
233
        if (level && selectedAuth) {
234
            $.ajax({
235
                url: context + "/beatPlan/getAuthUsers", type: "GET", dataType: "json",
236
                data: { categoryId: 4, escalationType: level },
237
                success: function (r) {
238
                    var data = r.response || r;
239
                    var html = '<option value="">Select User</option>';
240
                    data.forEach(function (u) {
241
                        html += '<option value="' + u.id + '"' + (u.id == selectedAuth ? ' selected' : '') + '>' + u.name + '</option>';
242
                    });
243
                    $('#user-detail-user').html(html);
244
                }
245
            });
246
        }
247
    })();
248
 
249
    $(document).off('click.beatdetail').off('click.livetrack');
250
 
251
    $(document).on('click.beatdetail', '.beat-history-link', function () {
36656 vikas 252
        var userId = $(this).data('userid');
253
        var taskId = $(this).data('taskid');
254
        var taskType = $(this).data('tasktype');
255
        var taskName = $(this).data('taskname');
256
 
257
        $('#taskHistoryModalTitle').text('History - ' + taskName);
258
        $('#taskHistoryModalBody').html('<div class="text-center"><i class="fa fa-spinner fa-spin fa-2x"></i> Loading...</div>');
259
        $('#taskHistoryModal').modal('show');
260
 
261
        doGetAjaxRequestHandler(context + "/beat-report/task-history?userId=" + userId + "&taskId=" + taskId + "&taskType=" + encodeURIComponent(taskType),
262
            function (response) {
263
                $('#taskHistoryModalBody').html(response);
264
            });
265
    });
36664 vikas 266
 
267
    var liveTrackingMap = null;
268
    var liveTrackingUserId = null;
269
    var liveTrackingInterval = null;
270
 
271
    function getMarkerColor(markType) {
272
        if (markType === 'CHECKIN-CHECKOUT' || markType === 'CHECKOUT') return '#0F9D58';
273
        if (markType === 'PENDING' || markType === 'CHECKIN') return '#4285F4';
274
        if (markType === 'DEFERRED') return '#DB4437';
275
        if (markType === 'PUNCHIN') return '#1abc9c';
276
        if (markType === 'PUNCHOUT') return '#e74c3c';
277
        return '#F4B400';
278
    }
279
 
280
    function createTrackingMarkerIcon(color, label) {
281
        var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="40" viewBox="0 0 28 40">' +
282
            '<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 + '"/>' +
283
            '<circle cx="14" cy="14" r="7" fill="white"/>' +
284
            '<text x="14" y="18" text-anchor="middle" font-size="10" font-weight="bold" fill="' + color + '">' + label + '</text></svg>';
285
        return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
286
    }
287
 
288
    function loadLiveTracking(userId, username) {
289
        var selectedDate = $('#user-detail-date').val();
290
        $.ajax({
291
            url: context + "/beat-report/live-tracking",
292
            type: "GET",
293
            dataType: "json",
294
            data: { userId: userId, date: selectedDate },
295
            success: function (data) {
296
                renderLiveTrackingMap(data, username);
297
            },
298
            error: function () {
299
                $('#liveTrackingInfo').html('<span style="color:red;">Failed to load tracking data.</span>');
300
            }
301
        });
302
    }
303
 
36685 vikas 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
 
36664 vikas 397
    function renderLiveTrackingMap(data, username) {
398
        var visits = data.visits || [];
399
        var pathPoints = data.pathPoints || [];
400
 
401
        if (!liveTrackingMap) {
402
            liveTrackingMap = new google.maps.Map(document.getElementById('liveTrackingMap'), {
403
                zoom: 12,
404
                center: { lat: 20.5937, lng: 78.9629 },
405
                mapTypeControl: true,
406
                streetViewControl: false
407
            });
408
        }
409
 
36685 vikas 410
        clearMapOverlays();
36664 vikas 411
 
412
        var bounds = new google.maps.LatLngBounds();
413
        var hasPoints = false;
414
        var visitIdx = 0;
36685 vikas 415
        var visitCoords = [];
36664 vikas 416
 
417
        for (var i = 0; i < visits.length; i++) {
418
            var v = visits[i];
419
            if (!v.lat || !v.lng) continue;
420
            var lat = parseFloat(v.lat);
421
            var lng = parseFloat(v.lng);
422
            if (isNaN(lat) || isNaN(lng) || (lat === 0 && lng === 0)) continue;
423
 
424
            hasPoints = true;
425
            var pos = { lat: lat, lng: lng };
426
            bounds.extend(pos);
36685 vikas 427
            visitCoords.push(pos);
36664 vikas 428
 
429
            var label = v.markType === 'PUNCHIN' ? 'P' : (v.markType === 'PUNCHOUT' ? 'X' : String(++visitIdx));
430
            var marker = new google.maps.Marker({
431
                position: pos,
432
                map: liveTrackingMap,
433
                icon: {
434
                    url: createTrackingMarkerIcon(getMarkerColor(v.markType), label),
435
                    scaledSize: new google.maps.Size(28, 40),
436
                    anchor: new google.maps.Point(14, 40)
437
                },
36685 vikas 438
                title: v.taskName + ' (' + v.markType + ')',
439
                zIndex: 100
36664 vikas 440
            });
441
 
36685 vikas 442
            var infoContent = '<div style="font-size:13px; max-width:280px;">' +
36664 vikas 443
                '<strong>' + (v.taskName || '') + '</strong><br>' +
36685 vikas 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>' +
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 : '') +
36664 vikas 452
                '</div>';
453
            (function (m, c) {
454
                var iw = new google.maps.InfoWindow({ content: c });
455
                m.addListener('click', function () { iw.open(liveTrackingMap, m); });
456
            })(marker, infoContent);
457
 
458
            liveTrackingMap._markers.push(marker);
459
        }
460
 
36685 vikas 461
        // Draw actual road path between visits using Directions API
462
        if (visitCoords.length >= 2) {
463
            var origin = visitCoords[0];
464
            var destination = visitCoords[visitCoords.length - 1];
465
            var waypoints = [];
466
            for (var wi = 1; wi < visitCoords.length - 1; wi++) {
467
                waypoints.push({ location: visitCoords[wi], stopover: true });
468
            }
469
 
470
            var directionsService = new google.maps.DirectionsService();
471
            var directionsRenderer = new google.maps.DirectionsRenderer({
472
                map: liveTrackingMap,
473
                suppressMarkers: true,
474
                polylineOptions: {
475
                    strokeColor: '#8e44ad',
476
                    strokeOpacity: 0.7,
477
                    strokeWeight: 4
478
                },
479
                preserveViewport: true
36682 vikas 480
            });
36685 vikas 481
            liveTrackingMap._directionsRenderer = directionsRenderer;
482
 
483
            directionsService.route({
484
                origin: origin,
485
                destination: destination,
486
                waypoints: waypoints,
487
                travelMode: google.maps.TravelMode.DRIVING,
488
                optimizeWaypoints: false
489
            }, function (result, status) {
490
                if (status === 'OK') {
491
                    directionsRenderer.setDirections(result);
492
                }
493
            });
36682 vikas 494
        }
495
 
36685 vikas 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
 
36664 vikas 502
        if (pathPoints.length > 0) {
36685 vikas 503
            var validPoints = [];
36664 vikas 504
            for (var j = 0; j < pathPoints.length; j++) {
505
                var pt = pathPoints[j];
506
                var pLat = parseFloat(pt.lat);
507
                var pLng = parseFloat(pt.lng);
508
                if (isNaN(pLat) || isNaN(pLng) || (pLat === 0 && pLng === 0)) continue;
36685 vikas 509
                var acc = parseFloat(pt.accuracy);
510
                if (!isNaN(acc)) { accuracySum += acc; accuracyCount++; }
511
                validPoints.push({ lat: pLat, lng: pLng, ts: pt.timestamp, accuracy: pt.accuracy });
36664 vikas 512
                bounds.extend({ lat: pLat, lng: pLng });
513
                hasPoints = true;
514
            }
515
 
36685 vikas 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
 
521
            if (validPoints.length > 0) {
522
                var pathCoords = validPoints.map(function(p) { return { lat: p.lat, lng: p.lng }; });
523
 
524
                // Blue connecting line
36664 vikas 525
                liveTrackingMap._polyline = new google.maps.Polyline({
526
                    path: pathCoords,
527
                    geodesic: true,
36685 vikas 528
                    strokeColor: '#e74c3c',
529
                    strokeOpacity: 0.7,
36664 vikas 530
                    strokeWeight: 3,
36685 vikas 531
                    zIndex: 50,
36664 vikas 532
                    map: liveTrackingMap
533
                });
534
 
36688 vikas 535
                // Direction arrows for each GPS point
36685 vikas 536
                liveTrackingMap._gpsDots = [];
537
                for (var gi = 0; gi < validPoints.length; gi++) {
538
                    var gp = validPoints[gi];
36688 vikas 539
                    var heading = 0;
540
                    if (gi < validPoints.length - 1) {
541
                        heading = google.maps.geometry.spherical.computeHeading(
542
                            new google.maps.LatLng(gp.lat, gp.lng),
543
                            new google.maps.LatLng(validPoints[gi + 1].lat, validPoints[gi + 1].lng)
544
                        );
545
                    } else if (gi > 0) {
546
                        heading = google.maps.geometry.spherical.computeHeading(
547
                            new google.maps.LatLng(validPoints[gi - 1].lat, validPoints[gi - 1].lng),
548
                            new google.maps.LatLng(gp.lat, gp.lng)
549
                        );
550
                    }
36685 vikas 551
                    var dot = new google.maps.Marker({
552
                        position: { lat: gp.lat, lng: gp.lng },
553
                        map: liveTrackingMap,
554
                        icon: {
36688 vikas 555
                            path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
556
                            scale: 3,
36685 vikas 557
                            fillColor: '#e74c3c',
558
                            fillOpacity: 1,
559
                            strokeColor: '#b71c1c',
36688 vikas 560
                            strokeWeight: 1,
561
                            rotation: heading
36685 vikas 562
                        },
563
                        zIndex: 80
564
                    });
565
                    (function(marker, point, idx) {
566
                        var iw = new google.maps.InfoWindow({
567
                            content: '<div style="font-size:12px;">' +
568
                                '<strong>GPS Point #' + (idx + 1) + '</strong><br>' +
569
                                '<i class="fa fa-clock-o"></i> ' + formatEpoch(point.ts) + '<br>' +
570
                                '<i class="fa fa-crosshairs"></i> Accuracy: ' + (point.accuracy || 'N/A') + 'm<br>' +
571
                                '<i class="fa fa-map-marker"></i> ' + point.lat.toFixed(6) + ', ' + point.lng.toFixed(6) +
572
                                '</div>'
573
                        });
574
                        marker.addListener('click', function() { iw.open(liveTrackingMap, marker); });
575
                    })(dot, gp, gi);
576
                    liveTrackingMap._gpsDots.push(dot);
577
                }
578
 
579
                // Detect stationary stops (within 50m for >5 min)
580
                stopsDetected = detectStops(validPoints, 50, 5 * 60 * 1000);
581
                liveTrackingMap._stopMarkers = [];
582
                for (var si = 0; si < stopsDetected.length; si++) {
583
                    var stop = stopsDetected[si];
584
                    var stopMarker = new google.maps.Marker({
585
                        position: { lat: stop.lat, lng: stop.lng },
586
                        map: liveTrackingMap,
587
                        icon: {
588
                            path: google.maps.SymbolPath.CIRCLE,
589
                            scale: 14,
590
                            fillColor: '#f39c12',
591
                            fillOpacity: 0.9,
592
                            strokeColor: '#e67e22',
593
                            strokeWeight: 2
594
                        },
595
                        label: { text: formatDuration(stop.duration), fontSize: '9px', fontWeight: 'bold', color: '#000' },
596
                        title: 'Stopped for ' + formatDuration(stop.duration),
597
                        zIndex: 200
598
                    });
599
                    (function(marker, s) {
600
                        var iw = new google.maps.InfoWindow({
601
                            content: '<div style="font-size:13px;">' +
602
                                '<strong><i class="fa fa-pause-circle" style="color:#f39c12;"></i> Stationary Stop</strong><br>' +
603
                                '<i class="fa fa-clock-o"></i> Duration: <strong>' + formatDuration(s.duration) + '</strong><br>' +
604
                                '<i class="fa fa-sign-in"></i> From: <strong>' + new Date(s.startTime).toLocaleTimeString() + '</strong><br>' +
605
                                '<i class="fa fa-sign-out"></i> To: <strong>' + new Date(s.endTime).toLocaleTimeString() + '</strong><br>' +
606
                                '<i class="fa fa-map-marker"></i> ' + s.lat.toFixed(6) + ', ' + s.lng.toFixed(6) + '<br>' +
607
                                '<i class="fa fa-dot-circle-o"></i> Points in cluster: ' + s.pointCount +
608
                                '</div>'
609
                        });
610
                        marker.addListener('click', function() { iw.open(liveTrackingMap, marker); });
611
                    })(stopMarker, stop);
612
                    liveTrackingMap._stopMarkers.push(stopMarker);
613
                }
614
 
36688 vikas 615
                // Last known location marker (bike icon)
36664 vikas 616
                var lastPoint = pathCoords[pathCoords.length - 1];
617
                liveTrackingMap._currentMarker = new google.maps.Marker({
618
                    position: lastPoint,
619
                    map: liveTrackingMap,
620
                    icon: {
36690 vikas 621
                        url: '${rc.contextPath}/resources/images/Bike-Icon-SD.png',
36688 vikas 622
                        scaledSize: new google.maps.Size(40, 40),
623
                        anchor: new google.maps.Point(20, 20)
36664 vikas 624
                    },
625
                    title: 'Last Known Location',
626
                    zIndex: 999
627
                });
36685 vikas 628
                var lastGp = validPoints[validPoints.length - 1];
36664 vikas 629
                var currentIw = new google.maps.InfoWindow({
36685 vikas 630
                    content: '<div style="font-size:13px;"><strong><i class="fa fa-crosshairs" style="color:#27ae60;"></i> Last Known Location</strong><br>' +
631
                        '<i class="fa fa-clock-o"></i> Time: ' + formatEpoch(lastGp.ts) + '<br>' +
632
                        '<i class="fa fa-crosshairs"></i> Accuracy: ' + (lastGp.accuracy || 'N/A') + 'm</div>'
36664 vikas 633
                });
634
                liveTrackingMap._currentMarker.addListener('click', function () {
635
                    currentIw.open(liveTrackingMap, liveTrackingMap._currentMarker);
636
                });
637
            }
638
        }
639
 
640
        if (hasPoints) {
641
            liveTrackingMap.fitBounds(bounds);
642
        }
643
 
36685 vikas 644
        // Info bar
36664 vikas 645
        var totalVisits = visits.filter(function (v) { return v.markType !== 'PUNCHIN' && v.markType !== 'PUNCHOUT'; }).length;
646
        var completedVisits = visits.filter(function (v) { return v.markType === 'CHECKIN-CHECKOUT' || v.markType === 'CHECKOUT'; }).length;
36682 vikas 647
        var now = new Date();
648
        var timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0') + ':' + now.getSeconds().toString().padStart(2, '0');
36685 vikas 649
 
650
        var avgAccuracy = accuracyCount > 0 ? (accuracySum / accuracyCount).toFixed(0) : null;
651
        var accuracyLabel = '';
652
        var accuracyBadge = '';
653
        if (avgAccuracy) {
654
            if (avgAccuracy <= 10) { accuracyLabel = 'High'; accuracyBadge = 'background:#27ae60;'; }
655
            else if (avgAccuracy <= 30) { accuracyLabel = 'Good'; accuracyBadge = 'background:#2ecc71;'; }
656
            else if (avgAccuracy <= 100) { accuracyLabel = 'Average'; accuracyBadge = 'background:#f39c12;'; }
657
            else { accuracyLabel = 'Low'; accuracyBadge = 'background:#e74c3c;'; }
658
        }
659
 
660
        var totalGpsKm = (totalGpsDistanceM / 1000).toFixed(2);
661
 
662
        var infoHtml = '<strong>' + username + '</strong> | ' +
36664 vikas 663
            'Visits: <strong>' + totalVisits + '</strong> | ' +
36685 vikas 664
            'Completed: <strong>' + completedVisits + '</strong>';
665
 
666
        if (pathPoints.length > 0) {
667
            infoHtml += ' | GPS Distance: <strong>' + totalGpsKm + ' km</strong>';
668
            infoHtml += ' | Points: <strong>' + pathPoints.length + '</strong>';
669
            if (accuracyLabel) {
670
                infoHtml += ' | Accuracy: <span class="label" style="' + accuracyBadge + ' color:white; padding:2px 8px; border-radius:3px; font-size:11px;">' + accuracyLabel + '</span> (' + avgAccuracy + 'm)';
671
            }
672
            if (stopsDetected.length > 0) {
673
                infoHtml += ' | <span style="color:#f39c12;"><i class="fa fa-pause-circle"></i></span> Stops: <strong>' + stopsDetected.length + '</strong>';
674
            }
36690 vikas 675
            infoHtml += ' | <img src="${rc.contextPath}/resources/images/Bike-Icon-SD.png" style="height:16px; vertical-align:middle;"> Last Known';
36685 vikas 676
        } else {
677
            infoHtml += ' | <span style="color:#999;"><i class="fa fa-exclamation-triangle"></i> No GPS data yet</span>';
678
        }
679
 
36688 vikas 680
        infoHtml += ' | <span style="color:#e74c3c;"><i class="fa fa-arrow-right" style="font-size:10px;"></i></span> GPS Trail';
36685 vikas 681
        if (visitCoords.length > 1) {
682
            infoHtml += ' | <span style="background:#8e44ad; display:inline-block; width:14px; height:3px; vertical-align:middle; margin:0 2px;"></span> Route';
683
        }
36690 vikas 684
        var locToggle = data.locationToggleCount || 0;
685
        if (locToggle > 0) {
686
            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>';
687
        }
688
 
36685 vikas 689
        infoHtml += ' | <span style="color:#999;">Updated: ' + timeStr + '</span>';
690
 
691
        $('#liveTrackingInfo').html(infoHtml);
692
 
693
        // Low data warning
694
        if (pathPoints.length > 0 && pathPoints.length < 10) {
695
            $('#liveTrackingInfo').append(
696
                '<div style="margin-top:4px; padding:4px 8px; background:#fff3cd; border:1px solid #ffc107; border-radius:3px; font-size:11px; color:#856404;">' +
697
                '<i class="fa fa-exclamation-triangle"></i> Received less data points from device due to: Low Battery level, ' +
698
                'GPS/location switched off, or phone automatically switched off the location service.' +
699
                '</div>'
700
            );
701
        }
36664 vikas 702
    }
703
 
704
    function ensureGoogleMaps(callback) {
705
        if (window.google && window.google.maps) {
706
            callback();
707
            return;
708
        }
709
        var s = document.createElement('script');
36688 vikas 710
        s.src = 'https://maps.googleapis.com/maps/api/js?key=AIzaSyAckO0y4Z6WhBOuMjNjioWLSYZDhGEvGBc&libraries=geometry&v=weekly';
36664 vikas 711
        s.onload = callback;
712
        document.head.appendChild(s);
713
    }
714
 
36682 vikas 715
    var liveTrackingUsername = '';
716
 
717
    function startAutoRefresh() {
718
        stopAutoRefresh();
719
        if ($('#autoRefreshToggle').is(':checked') && liveTrackingUserId) {
720
            liveTrackingInterval = setInterval(function () {
721
                loadLiveTracking(liveTrackingUserId, liveTrackingUsername);
722
            }, 30000);
723
        }
724
    }
725
 
726
    function stopAutoRefresh() {
727
        if (liveTrackingInterval) {
728
            clearInterval(liveTrackingInterval);
729
            liveTrackingInterval = null;
730
        }
731
    }
732
 
36664 vikas 733
    $(document).on('click.livetrack', '.live-tracking-btn', function () {
734
        liveTrackingUserId = $(this).data('userid');
36682 vikas 735
        liveTrackingUsername = $(this).data('username');
36690 vikas 736
        $('#liveTrackingModalTitle').html('<img src="${rc.contextPath}/resources/images/Bike-Icon-SD.png" style="height:24px; vertical-align:middle; margin-right:6px;"> Live Tracking - ' + liveTrackingUsername);
36664 vikas 737
        $('#liveTrackingInfo').html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');
738
        $('#liveTrackingModal').modal('show');
739
 
740
        setTimeout(function () {
741
            liveTrackingMap = null;
742
            ensureGoogleMaps(function () {
36682 vikas 743
                loadLiveTracking(liveTrackingUserId, liveTrackingUsername);
744
                startAutoRefresh();
36664 vikas 745
            });
746
        }, 300);
747
    });
748
 
749
    $(document).on('click.livetrack', '#refreshTrackingBtn', function () {
750
        if (liveTrackingUserId) {
751
            $('#liveTrackingInfo').html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Refreshing...</div>');
36682 vikas 752
            loadLiveTracking(liveTrackingUserId, liveTrackingUsername);
36664 vikas 753
        }
754
    });
755
 
36682 vikas 756
    $(document).on('change', '#autoRefreshToggle', function () {
757
        if ($(this).is(':checked')) {
758
            startAutoRefresh();
759
        } else {
760
            stopAutoRefresh();
36664 vikas 761
        }
762
    });
36682 vikas 763
 
764
    $('#liveTrackingModal').on('hidden.bs.modal', function () {
765
        stopAutoRefresh();
766
        liveTrackingUserId = null;
767
    });
36645 vikas 768
</script>