Subversion Repositories SmartDukaan

Rev

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

Rev Author Line No. Line
36645 vikas 1
<script>
2
    if (!window.google || !window.google.maps) {
3
        var s = document.createElement('script');
36693 vikas 4
        s.src = 'https://maps.googleapis.com/maps/api/js?key=AIzaSyAckO0y4Z6WhBOuMjNjioWLSYZDhGEvGBc&libraries=geometry&v=weekly';
36645 vikas 5
        document.head.appendChild(s);
6
    }
7
</script>
8
 
9
<div class="col-lg-12">
36712 vikas 10
    <table class="table table-border table-condensed table-bordered table-hover" id="beatReportTable" style="width:100%">
36645 vikas 11
        <thead>
12
        <tr>
13
            <th>User Name</th>
36761 ranu 14
            <th>Beat</th>
15
            <th>Day</th>
16
            <th>Planned</th>
36709 vikas 17
            <th>Punch In/Out</th>
36645 vikas 18
            <th>Total Visits</th>
19
            <th>Completed Visits</th>
20
            <th>Pending Visits</th>
21
            <th>Deferred</th>
36761 ranu 22
            <th>Action</th>
36645 vikas 23
        </tr>
24
        </thead>
25
        <tbody>
26
        #set($rowIndex = 0)
27
        #foreach($row in $reportRows)
28
            <tr>
29
                <td><a href="javascript:void(0);" class="beat-user-detail-link" data-userid="$row.get('userId')" style="color:#2c3e50; font-weight:bold; text-decoration:underline; cursor:pointer;">$row.get("username")</a></td>
36761 ranu 30
                #set($userBeats = $row.get("beats"))
31
                #if($userBeats && $userBeats.size() > 0)
32
                    <td>
33
                        #foreach($b in $userBeats)
34
                            <span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#if($b.get(
35
                                'beatColor'))$b.get('beatColor')#else#3498db#end;margin-right:5px;"></span>$b.get(
36
                            "beatName")<br>
37
                        #end
38
                    </td>
39
                    <td>
40
                        #foreach($b in $userBeats)
41
                            $b.get("dayNumber")<br>
42
                        #end
43
                    </td>
44
                    <td>
45
                        #foreach($b in $userBeats)
46
                            <strong>$b.get("totalCount")</strong><br>
47
                        #end
48
                    </td>
49
                #else
36793 ranu 50
                    <td style="color:#999; text-align:center; font-style:italic;">No beat scheduled</td>
51
                    <td style="color:#999;">--</td>
52
                    <td style="color:#999;">--</td>
36761 ranu 53
                #end
36709 vikas 54
                <td>
55
                    #if($row.get("punchInTime") != "")<span class="label label-success">$row.get("punchInTime")</span>#else<span style="color:#ccc;">--</span>#end
56
                    #if($row.get("punchOutTime") != "")<span class="label label-danger">$row.get("punchOutTime")</span>#else<span style="color:#ccc;">--</span>#end
57
                </td>
36645 vikas 58
                <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>
59
                <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>
60
                <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>
61
                <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>
36761 ranu 62
                <td style="white-space:nowrap;">
63
                    <a href="javascript:void(0);" class="beat-map-link" data-row="$rowIndex" title="Map"
64
                       style="cursor:pointer; margin-right:6px;"><i class="fa fa-map-marker"
65
                                                                    style="color:#1abc9c; font-size:18px;"></i></a>
66
                    #if($userBeats && $userBeats.size() > 0)
67
                        #foreach($b in $userBeats)
68
                            <button class="btn btn-xs btn-info beat-view-calendar" title="View Calendar"
69
                                    data-userid="$b.get('authUserId')"
70
                                    data-username="$row.get('username').replace('"', '\"')"><i class="fa fa-eye"></i>
71
                            </button>
72
                            <button class="btn btn-xs btn-warning beat-edit-on-date" title="Edit Beat"
73
                                    data-userid="$b.get('authUserId')"
74
                                    data-username="$row.get('username').replace('"', '\"')"
75
                                    data-beatid="$b.get('beatId')"
76
                                    data-date="$b.get('scheduleDate')"><i class="fa fa-edit"></i></button>
77
                            <button class="btn btn-xs btn-success beat-assign-visit" title="Assign Visit"
78
                                    data-userid="$b.get('authUserId')"
79
                                    data-username="$row.get('username').replace('"', '\"')"
80
                                    data-beatid="$b.get('beatId')"
81
                                    data-date="$b.get('scheduleDate')"><i class="fa fa-plus-circle"></i></button>
82
                            <br>
83
                        #end
84
                    #end
85
                </td>
36645 vikas 86
            </tr>
87
            #set($rowIndex = $rowIndex + 1)
88
        #end
89
        </tbody>
90
    </table>
91
</div>
92
 
36793 ranu 93
<!--
94
  Per-row visit lists are emitted as a single script block OUTSIDE the table.
95
  Embedding them between <tr> rows triggers HTML foster-parenting (browsers
96
  move misplaced non-row content out of the table during parsing), which
97
  fragments the tbody and causes DataTables to throw
98
  "Cannot set properties of undefined (setting '_DT_CellIndex')".
99
-->
100
<script>
101
    window.beatReportData = window.beatReportData || {};
102
        #set($rowIndex = 0)
103
        #foreach($row in $reportRows)
104
        window.beatReportData[$rowIndex] = {
105
            username: "$row.get('username').replace('"', '\"')",
106
            total: [
107
                #foreach($lt in $row.get("totalList"))
108
                    {
109
                        taskName: "$!lt.getTaskName().replace('"', '\"')",
110
                        markType: "$!lt.getMarkType()",
111
                        address: "$!lt.getAddress().replace('"', '\"').replace("'", "\'")",
112
                        checkInTime: "$!lt.getCheckInTime()",
113
                        checkOutTime: "$!lt.getCheckOutTime()",
114
                        timeSpent: "$!lt.getTimeSpent()",
115
                        taskType: "$!lt.getTaskType()",
116
                        visitLocation: "$!lt.getVisitLocation()",
117
                        checkInLatLng: "$!lt.getCheckInLatLng()",
118
                        taskDescription: "$!lt.getTaskDescription().replace('"', '\"')",
119
                        attachment: "$!lt.getAttachment()"
120
                    },
121
                #end
122
            ],
123
            completed: [
124
                #foreach($lt in $row.get("completedList"))
125
                    {
126
                        taskName: "$!lt.getTaskName().replace('"', '\"')",
127
                        markType: "$!lt.getMarkType()",
128
                        address: "$!lt.getAddress().replace('"', '\"').replace("'", "\'")",
129
                        checkInTime: "$!lt.getCheckInTime()",
130
                        checkOutTime: "$!lt.getCheckOutTime()",
131
                        timeSpent: "$!lt.getTimeSpent()",
132
                        taskType: "$!lt.getTaskType()",
133
                        visitLocation: "$!lt.getVisitLocation()",
134
                        checkInLatLng: "$!lt.getCheckInLatLng()",
135
                        taskDescription: "$!lt.getTaskDescription().replace('"', '\"')",
136
                        attachment: "$!lt.getAttachment()"
137
                    },
138
                #end
139
            ],
140
            pending: [
141
                #foreach($lt in $row.get("pendingList"))
142
                    {
143
                        taskName: "$!lt.getTaskName().replace('"', '\"')",
144
                        markType: "$!lt.getMarkType()",
145
                        address: "$!lt.getAddress().replace('"', '\"').replace("'", "\'")",
146
                        checkInTime: "$!lt.getCheckInTime()",
147
                        checkOutTime: "$!lt.getCheckOutTime()",
148
                        timeSpent: "$!lt.getTimeSpent()",
149
                        taskType: "$!lt.getTaskType()",
150
                        visitLocation: "$!lt.getVisitLocation()",
151
                        checkInLatLng: "$!lt.getCheckInLatLng()",
152
                        taskDescription: "$!lt.getTaskDescription().replace('"', '\"')",
153
                        attachment: "$!lt.getAttachment()"
154
                    },
155
                #end
156
            ],
157
            deferred: [
158
                #foreach($lt in $row.get("deferredList"))
159
                    {
160
                        taskName: "$!lt.getTaskName().replace('"', '\"')",
161
                        markType: "$!lt.getMarkType()",
162
                        address: "$!lt.getAddress().replace('"', '\"').replace("'", "\'")",
163
                        checkInTime: "$!lt.getCheckInTime()",
164
                        checkOutTime: "$!lt.getCheckOutTime()",
165
                        timeSpent: "$!lt.getTimeSpent()",
166
                        taskType: "$!lt.getTaskType()",
167
                        visitLocation: "$!lt.getVisitLocation()",
168
                        checkInLatLng: "$!lt.getCheckInLatLng()",
169
                        taskDescription: "$!lt.getTaskDescription().replace('"', '\"')",
170
                        attachment: "$!lt.getAttachment()"
171
                    },
172
                #end
173
            ]
174
        };
175
            #set($rowIndex = $rowIndex + 1)
176
        #end
177
</script>
178
 
36645 vikas 179
<!-- Map Modal -->
180
<div class="modal fade" id="beatMapModal" tabindex="-1" role="dialog">
181
    <div class="modal-dialog modal-lg" role="document" style="width:90%;">
182
        <div class="modal-content">
183
            <div class="modal-header" style="background:#1abc9c; color:white;">
184
                <button type="button" class="close" data-dismiss="modal" style="color:white;">&times;</button>
185
                <h4 class="modal-title" id="beatMapModalTitle">Visit Locations</h4>
186
            </div>
187
            <div class="modal-body" style="padding:0;">
188
                <div id="beatMapContainer" style="height:500px; width:100%;"></div>
189
                <div id="beatMapInfo" style="padding:6px 15px; background:#fff3cd; border-top:1px solid #ddd; color:#856404; display:none;"></div>
190
                <div style="padding:8px 15px; background:#f5f5f5; border-top:1px solid #ddd;">
191
                    <span style="margin-right:15px;"><i class="fa fa-circle" style="color:#4285F4;"></i> Check In</span>
192
                    <span style="margin-right:15px;"><i class="fa fa-circle" style="color:#0F9D58;"></i> Completed</span>
193
                    <span style="margin-right:15px;"><i class="fa fa-circle" style="color:#F4B400;"></i> Pending</span>
194
                    <span><i class="fa fa-circle" style="color:#DB4437;"></i> Deferred</span>
195
                </div>
196
            </div>
197
            <div class="modal-footer">
198
                <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
199
            </div>
200
        </div>
201
    </div>
202
</div>
203
 
204
<!-- Detail Popup Modal -->
205
<div class="modal fade" id="beatDetailModal" tabindex="-1" role="dialog">
206
    <div class="modal-dialog modal-lg" role="document">
207
        <div class="modal-content">
208
            <div class="modal-header" style="background:#1abc9c; color:white;">
209
                <button type="button" class="close" data-dismiss="modal" style="color:white;">&times;</button>
210
                <h4 class="modal-title" id="beatDetailModalTitle">Visit Details</h4>
211
            </div>
212
            <div class="modal-body">
213
                <table class="table table-bordered table-condensed table-striped" id="beatDetailTable" style="width:100%">
214
                    <thead>
215
                    <tr>
216
                        <th>#</th>
36669 vikas 217
                        <th>Agenda/Partner Name</th>
36645 vikas 218
                        <th>Type</th>
219
                        <th>Status</th>
220
                        <th>Address</th>
221
                        <th>Check In</th>
222
                        <th>Check Out</th>
223
                        <th>Time Spent</th>
224
                        <th>Description</th>
36736 ranu 225
                        <th>ETA</th>
36645 vikas 226
                        <th>Attachment</th>
227
                    </tr>
228
                    </thead>
229
                    <tbody id="beatDetailTableBody"></tbody>
230
                </table>
231
            </div>
232
            <div class="modal-footer">
233
                <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
234
            </div>
235
        </div>
236
    </div>
237
</div>
238
 
239
<script>
240
    $('#beatReportTable').DataTable({
241
        "scrollX": true,
36685 vikas 242
        "pageLength": 50,
36645 vikas 243
        "bPaginate": true,
244
        "bLengthChange": true,
245
        "bFilter": true,
246
        "bInfo": true,
247
        "bAutoWidth": false
248
    });
249
 
250
    var beatMapMarkerColors = {
251
        'PENDING': '#F4B400',
252
        'DEFERRED': '#DB4437',
253
        'CHECKIN': '#4285F4',
36783 vikas 254
        'CHECKIN_CHECKOUT': '#0F9D58',
36645 vikas 255
        'CHECKIN-CHECKOUT': '#0F9D58',
256
        'CHECKOUT': '#0F9D58'
257
    };
258
 
259
    function getMarkerColor(markType) {
260
        return beatMapMarkerColors[markType] || '#999999';
261
    }
262
 
263
    function createColoredMarkerIcon(color, label) {
264
        var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="42" viewBox="0 0 32 42">' +
265
            '<path d="M16 0C7.2 0 0 7.2 0 16c0 12 16 26 16 26s16-14 16-26C32 7.2 24.8 0 16 0z" fill="' + color + '"/>' +
266
            '<circle cx="16" cy="15" r="10" fill="white"/>' +
267
            '<text x="16" y="20" text-anchor="middle" font-size="12" font-weight="bold" fill="' + color + '">' + label + '</text>' +
268
            '</svg>';
269
        return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
270
    }
271
 
272
    function getLatLng(item) {
273
        var loc = item.visitLocation || item.checkInLatLng || '';
274
        if (!loc || loc === '0.0000,0.0000') return null;
275
        var parts = loc.split(',');
276
        if (parts.length !== 2) return null;
277
        var lat = parseFloat(parts[0].trim());
278
        var lng = parseFloat(parts[1].trim());
279
        if (isNaN(lat) || isNaN(lng) || (lat === 0 && lng === 0)) return null;
280
        return { lat: lat, lng: lng };
281
    }
282
 
283
    $(document).on('click', '.beat-map-link', function () {
284
        var rowIndex = $(this).data('row');
285
        var data = window.beatReportData[rowIndex];
286
        if (!data) return;
287
 
288
        var items = data.total || [];
289
        var username = data.username;
290
        $('#beatMapModalTitle').text(username + ' - Visit Locations (' + items.length + ')');
291
 
292
        if (typeof google === 'undefined' || !google.maps) {
293
            alert('Google Maps is still loading. Please try again in a moment.');
294
            return;
295
        }
296
 
297
        $('#beatMapModal').modal('show');
298
 
299
        setTimeout(function () {
300
            var mapDiv = document.getElementById('beatMapContainer');
301
            var bounds = new google.maps.LatLngBounds();
302
            var hasMarkers = false;
303
 
304
            var map = new google.maps.Map(mapDiv, {
305
                zoom: 10,
306
                center: { lat: 20.5937, lng: 78.9629 },
307
                mapTypeId: 'roadmap'
308
            });
309
 
310
            var infoWindow = new google.maps.InfoWindow();
311
 
312
            for (var i = 0; i < items.length; i++) {
313
                var item = items[i];
314
                var pos = getLatLng(item);
315
                if (!pos) continue;
316
 
317
                hasMarkers = true;
318
                bounds.extend(pos);
319
 
320
                var color = getMarkerColor(item.markType);
321
                var marker = new google.maps.Marker({
322
                    position: pos,
323
                    map: map,
324
                    title: item.taskName || item.markType,
325
                    icon: {
326
                        url: createColoredMarkerIcon(color, (i + 1)),
327
                        scaledSize: new google.maps.Size(32, 42),
328
                        anchor: new google.maps.Point(16, 42)
329
                    }
330
                });
331
 
332
                (function (m, itm, idx) {
333
                    google.maps.event.addListener(m, 'click', function () {
334
                        var content = '<div style="min-width:200px;">' +
335
                            '<strong>' + (idx + 1) + '. ' + (itm.taskName || '--') + '</strong><br>' +
336
                            '<b>Status:</b> ' + (itm.markType || '--') + '<br>' +
337
                            '<b>Address:</b> ' + (itm.address || '--') + '<br>' +
338
                            '<b>Check In:</b> ' + (itm.checkInTime || '--') + '<br>' +
339
                            '<b>Check Out:</b> ' + (itm.checkOutTime || '--') + '<br>' +
340
                            '<b>Time Spent:</b> ' + (itm.timeSpent || '--') +
341
                            '</div>';
342
                        infoWindow.setContent(content);
343
                        infoWindow.open(map, m);
344
                    });
345
                })(marker, item, i);
346
            }
347
 
348
            var markerCount = 0;
349
            for (var j = 0; j < items.length; j++) { if (getLatLng(items[j])) markerCount++; }
350
 
351
            var infoDiv = document.getElementById('beatMapInfo');
352
            if (markerCount < items.length) {
353
                infoDiv.innerHTML = '<i class="fa fa-exclamation-triangle"></i> Showing ' + markerCount + ' of ' + items.length + ' visits on map. ' + (items.length - markerCount) + ' visits have no location data.';
354
                infoDiv.style.display = 'block';
355
            } else {
356
                infoDiv.style.display = 'none';
357
            }
358
 
359
            if (hasMarkers) {
360
                map.fitBounds(bounds);
361
                if (markerCount === 1) {
362
                    map.setZoom(14);
363
                }
364
            }
365
        }, 400);
366
    });
367
 
368
    $(document).on('click', '.beat-detail-link', function () {
369
        var rowIndex = $(this).data('row');
370
        var type = $(this).data('type');
371
        var data = window.beatReportData[rowIndex];
372
        if (!data) return;
373
 
374
        var items = data[type] || [];
375
        var username = data.username;
376
 
377
        var typeLabel = type.charAt(0).toUpperCase() + type.slice(1);
378
        $('#beatDetailModalTitle').text(username + ' - ' + typeLabel + ' Visits (' + items.length + ')');
379
 
380
        var tbody = '';
381
        for (var i = 0; i < items.length; i++) {
382
            var item = items[i];
383
            tbody += '<tr>' +
384
                '<td>' + (i + 1) + '</td>' +
385
                '<td>' + (item.taskName || '--') + '</td>' +
386
                '<td>' + (item.taskType || '--') + '</td>' +
387
                '<td>' + (item.markType || '--') + '</td>' +
388
                '<td>' + (item.address || '--') + '</td>' +
389
                '<td>' + (item.checkInTime || '--') + '</td>' +
390
                '<td>' + (item.checkOutTime || '--') + '</td>' +
391
                '<td>' + (item.timeSpent || '--') + '</td>' +
392
                '<td>' + (item.taskDescription || '--') + '</td>' +
36666 vikas 393
                '<td>' + (item.estimatedTime || '--') + '</td>' +
36645 vikas 394
                '<td>' + (item.attachment ? '<a href="https://partners.smartdukaan.com/document/' + item.attachment + '" target="_blank"><i class="fa fa-paperclip"></i> View</a>' : '--') + '</td>' +
395
                '</tr>';
396
        }
397
        if (items.length === 0) {
398
            tbody = '<tr><td colspan="10" class="text-center">No records found</td></tr>';
399
        }
400
        $('#beatDetailTableBody').html(tbody);
401
        $('#beatDetailModal').modal('show');
402
    });
36693 vikas 403
 
36696 vikas 404
    // --- Live Tracking ---
405
    var dataLiveMap = null;
406
    var dataLiveUserId = null;
407
    var dataLiveUsername = '';
408
    var dataLiveInterval = null;
409
 
410
    function dataGetMarkerColor(markType) {
36783 vikas 411
        if (markType === 'CHECKIN-CHECKOUT' || markType === 'CHECKIN_CHECKOUT' || markType === 'CHECKOUT') return '#0F9D58';
36696 vikas 412
        if (markType === 'PENDING' || markType === 'CHECKIN') return '#4285F4';
413
        if (markType === 'DEFERRED') return '#DB4437';
414
        if (markType === 'PUNCHIN') return '#1abc9c';
415
        if (markType === 'PUNCHOUT') return '#e74c3c';
416
        return '#F4B400';
417
    }
418
 
419
    function dataCreateMarkerIcon(color, label) {
420
        var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="40" viewBox="0 0 28 40">' +
421
            '<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 + '"/>' +
422
            '<circle cx="14" cy="14" r="7" fill="white"/>' +
423
            '<text x="14" y="18" text-anchor="middle" font-size="10" font-weight="bold" fill="' + color + '">' + label + '</text></svg>';
424
        return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
425
    }
426
 
427
    function dataFormatEpoch(ts) {
428
        if (!ts) return 'N/A';
429
        var n = parseFloat(ts);
430
        if (isNaN(n)) return ts;
431
        if (n < 1e12) n = n * 1000;
432
        var d = new Date(n);
433
        return d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0') + ':' + d.getSeconds().toString().padStart(2,'0');
434
    }
435
 
436
    function dataFormatDuration(ms) {
437
        var sec = Math.floor(ms / 1000);
438
        var h = Math.floor(sec / 3600);
439
        var m = Math.floor((sec % 3600) / 60);
440
        var s = sec % 60;
441
        if (h > 0) return h + 'h ' + m + 'm';
442
        if (m > 0) return m + 'm ' + s + 's';
443
        return s + 's';
444
    }
445
 
446
    function dataHaversine(lat1, lon1, lat2, lon2) {
447
        var R = 6371000;
448
        var dLat = (lat2 - lat1) * Math.PI / 180;
449
        var dLon = (lon2 - lon1) * Math.PI / 180;
450
        var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
451
                Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
452
                Math.sin(dLon/2) * Math.sin(dLon/2);
453
        return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
454
    }
455
 
456
    function dataDetectStops(points, radiusM, minTimeMs) {
457
        var stops = [];
458
        var i = 0;
459
        while (i < points.length) {
460
            var cluster = [points[i]];
461
            var cLat = points[i].lat, cLng = points[i].lng;
462
            var j = i + 1;
463
            while (j < points.length) {
464
                if (dataHaversine(cLat, cLng, points[j].lat, points[j].lng) <= radiusM) {
465
                    cluster.push(points[j]);
466
                    j++;
467
                } else {
468
                    break;
469
                }
470
            }
471
            if (cluster.length >= 2) {
472
                var t0 = parseFloat(cluster[0].ts);
473
                var t1 = parseFloat(cluster[cluster.length - 1].ts);
474
                if (t0 < 1e12) t0 *= 1000;
475
                if (t1 < 1e12) t1 *= 1000;
476
                var duration = t1 - t0;
477
                if (duration >= minTimeMs) {
478
                    var avgLat = 0, avgLng = 0;
479
                    cluster.forEach(function(p) { avgLat += p.lat; avgLng += p.lng; });
480
                    stops.push({
481
                        lat: avgLat / cluster.length, lng: avgLng / cluster.length,
482
                        startTime: t0, endTime: t1, duration: duration, pointCount: cluster.length
483
                    });
484
                }
485
            }
486
            i = j;
487
        }
488
        return stops;
489
    }
490
 
491
    function dataClearOverlays() {
492
        if (!dataLiveMap) return;
493
        if (dataLiveMap._markers) dataLiveMap._markers.forEach(function(m) { m.setMap(null); });
494
        if (dataLiveMap._polyline) dataLiveMap._polyline.setMap(null);
495
        if (dataLiveMap._gpsDots) dataLiveMap._gpsDots.forEach(function(m) { m.setMap(null); });
496
        if (dataLiveMap._stopMarkers) dataLiveMap._stopMarkers.forEach(function(m) { m.setMap(null); });
497
        if (dataLiveMap._currentMarker) dataLiveMap._currentMarker.setMap(null);
498
        if (dataLiveMap._directionsRenderer) dataLiveMap._directionsRenderer.setMap(null);
499
        dataLiveMap._markers = [];
500
        dataLiveMap._gpsDots = [];
501
        dataLiveMap._stopMarkers = [];
502
    }
503
 
504
    function dataLoadLiveTracking(userId, username) {
505
        var selectedDate = $('#beat-report-date').val();
506
        $.ajax({
507
            url: context + "/beat-report/live-tracking",
508
            type: "GET", dataType: "json",
509
            data: { userId: userId, date: selectedDate },
510
            success: function(data) { dataRenderMap(data, username); },
511
            error: function() { $('#dataLiveInfo').html('<span style="color:red;">Failed to load tracking data.</span>'); }
512
        });
513
    }
514
 
515
    function dataRenderMap(data, username) {
516
        var visits = data.visits || [];
517
        var pathPoints = data.pathPoints || [];
518
 
519
        if (!dataLiveMap) {
520
            dataLiveMap = new google.maps.Map(document.getElementById('dataLiveMap'), {
521
                zoom: 12, center: { lat: 20.5937, lng: 78.9629 },
522
                mapTypeControl: true, streetViewControl: false
523
            });
524
        }
525
        dataClearOverlays();
526
 
527
        var bounds = new google.maps.LatLngBounds();
528
        var hasPoints = false;
529
        var visitIdx = 0;
530
        var visitCoords = [];
531
 
532
        for (var i = 0; i < visits.length; i++) {
533
            var v = visits[i];
534
            if (!v.lat || !v.lng) continue;
535
            var lat = parseFloat(v.lat), lng = parseFloat(v.lng);
536
            if (isNaN(lat) || isNaN(lng) || (lat === 0 && lng === 0)) continue;
537
            hasPoints = true;
538
            var pos = { lat: lat, lng: lng };
539
            bounds.extend(pos);
540
            visitCoords.push(pos);
541
            var label = v.markType === 'PUNCHIN' ? 'P' : (v.markType === 'PUNCHOUT' ? 'X' : String(++visitIdx));
542
            var marker = new google.maps.Marker({
543
                position: pos, map: dataLiveMap,
544
                icon: { url: dataCreateMarkerIcon(dataGetMarkerColor(v.markType), label), scaledSize: new google.maps.Size(28, 40), anchor: new google.maps.Point(14, 40) },
545
                title: v.taskName + ' (' + v.markType + ')', zIndex: 100
546
            });
547
            var infoContent = '<div style="font-size:13px; max-width:280px;">' +
548
                '<strong>' + (v.taskName || '') + '</strong><br>' +
549
                '<span class="label" style="background:' + dataGetMarkerColor(v.markType) + '; color:white;">' + v.markType + '</span>' +
550
                (v.taskType ? ' <span class="label label-default">' + v.taskType + '</span>' : '') + '<br>' +
551
                (v.checkInTime ? '<i class="fa fa-sign-in" style="color:#27ae60;"></i> In: <strong>' + v.checkInTime + '</strong><br>' : '') +
552
                (v.checkOutTime ? '<i class="fa fa-sign-out" style="color:#e74c3c;"></i> Out: <strong>' + v.checkOutTime + '</strong><br>' : '') +
553
                (v.timeSpent ? '<i class="fa fa-clock-o" style="color:#f39c12;"></i> Time Spent: <strong>' + v.timeSpent + '</strong><br>' : '') +
554
                (v.transitTime ? '<i class="fa fa-car" style="color:#3498db;"></i> Transit: <strong>' + v.transitTime + '</strong><br>' : '') +
555
                (v.totalDistance && v.totalDistance !== '0.0' ? '<i class="fa fa-road" style="color:#8e44ad;"></i> Distance: <strong>' + v.totalDistance + ' km</strong><br>' : '') +
556
                (v.address ? '<i class="fa fa-map-marker" style="color:#e74c3c;"></i> ' + v.address : '') + '</div>';
557
            (function(m, c) {
558
                var iw = new google.maps.InfoWindow({ content: c });
559
                m.addListener('click', function() { iw.open(dataLiveMap, m); });
560
            })(marker, infoContent);
561
            dataLiveMap._markers.push(marker);
562
        }
563
 
564
        if (visitCoords.length >= 2) {
565
            var origin = visitCoords[0], destination = visitCoords[visitCoords.length - 1];
566
            var waypoints = [];
567
            for (var wi = 1; wi < visitCoords.length - 1; wi++) waypoints.push({ location: visitCoords[wi], stopover: true });
568
            var ds = new google.maps.DirectionsService();
569
            var dr = new google.maps.DirectionsRenderer({
570
                map: dataLiveMap, suppressMarkers: true,
571
                polylineOptions: { strokeColor: '#8e44ad', strokeOpacity: 0.7, strokeWeight: 4 },
572
                preserveViewport: true
573
            });
574
            dataLiveMap._directionsRenderer = dr;
575
            ds.route({ origin: origin, destination: destination, waypoints: waypoints, travelMode: google.maps.TravelMode.DRIVING, optimizeWaypoints: false },
576
                function(result, status) { if (status === 'OK') dr.setDirections(result); });
577
        }
578
 
579
        var totalGpsDistanceM = 0, accuracySum = 0, accuracyCount = 0, stopsDetected = [];
580
        if (pathPoints.length > 0) {
581
            var validPoints = [];
582
            for (var j = 0; j < pathPoints.length; j++) {
583
                var pt = pathPoints[j];
584
                var pLat = parseFloat(pt.lat), pLng = parseFloat(pt.lng);
585
                if (isNaN(pLat) || isNaN(pLng) || (pLat === 0 && pLng === 0)) continue;
586
                var acc = parseFloat(pt.accuracy);
587
                if (!isNaN(acc)) { accuracySum += acc; accuracyCount++; }
588
                validPoints.push({ lat: pLat, lng: pLng, ts: pt.timestamp, accuracy: pt.accuracy });
589
                bounds.extend({ lat: pLat, lng: pLng });
590
                hasPoints = true;
591
            }
592
            for (var d = 1; d < validPoints.length; d++) {
593
                totalGpsDistanceM += dataHaversine(validPoints[d-1].lat, validPoints[d-1].lng, validPoints[d].lat, validPoints[d].lng);
594
            }
595
            if (validPoints.length > 0) {
596
                var pathCoords = validPoints.map(function(p) { return { lat: p.lat, lng: p.lng }; });
597
                dataLiveMap._polyline = new google.maps.Polyline({
598
                    path: pathCoords, geodesic: true, strokeColor: '#e74c3c', strokeOpacity: 0.7, strokeWeight: 3, zIndex: 50, map: dataLiveMap
599
                });
600
                dataLiveMap._gpsDots = [];
601
                for (var gi = 0; gi < validPoints.length; gi++) {
602
                    var gp = validPoints[gi];
603
                    var heading = 0;
604
                    if (gi < validPoints.length - 1) {
605
                        heading = google.maps.geometry.spherical.computeHeading(
606
                            new google.maps.LatLng(gp.lat, gp.lng), new google.maps.LatLng(validPoints[gi+1].lat, validPoints[gi+1].lng));
607
                    } else if (gi > 0) {
608
                        heading = google.maps.geometry.spherical.computeHeading(
609
                            new google.maps.LatLng(validPoints[gi-1].lat, validPoints[gi-1].lng), new google.maps.LatLng(gp.lat, gp.lng));
610
                    }
611
                    var dot = new google.maps.Marker({
612
                        position: { lat: gp.lat, lng: gp.lng }, map: dataLiveMap,
613
                        icon: { path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW, scale: 3, fillColor: '#e74c3c', fillOpacity: 1, strokeColor: '#b71c1c', strokeWeight: 1, rotation: heading },
614
                        zIndex: 80
615
                    });
616
                    (function(marker, point, idx) {
617
                        var iw = new google.maps.InfoWindow({
618
                            content: '<div style="font-size:12px;"><strong>GPS Point #' + (idx+1) + '</strong><br>' +
619
                                '<i class="fa fa-clock-o"></i> ' + dataFormatEpoch(point.ts) + '<br>' +
620
                                '<i class="fa fa-crosshairs"></i> Accuracy: ' + (point.accuracy || 'N/A') + 'm<br>' +
621
                                '<i class="fa fa-map-marker"></i> ' + point.lat.toFixed(6) + ', ' + point.lng.toFixed(6) + '</div>'
622
                        });
623
                        marker.addListener('click', function() { iw.open(dataLiveMap, marker); });
624
                    })(dot, gp, gi);
625
                    dataLiveMap._gpsDots.push(dot);
626
                }
627
                stopsDetected = dataDetectStops(validPoints, 50, 5 * 60 * 1000);
628
                dataLiveMap._stopMarkers = [];
629
                for (var si = 0; si < stopsDetected.length; si++) {
630
                    var stop = stopsDetected[si];
631
                    var sm = new google.maps.Marker({
632
                        position: { lat: stop.lat, lng: stop.lng }, map: dataLiveMap,
633
                        icon: { path: google.maps.SymbolPath.CIRCLE, scale: 14, fillColor: '#f39c12', fillOpacity: 0.9, strokeColor: '#e67e22', strokeWeight: 2 },
634
                        label: { text: dataFormatDuration(stop.duration), fontSize: '9px', fontWeight: 'bold', color: '#000' },
635
                        title: 'Stopped for ' + dataFormatDuration(stop.duration), zIndex: 200
636
                    });
637
                    (function(marker, s) {
638
                        var iw = new google.maps.InfoWindow({
639
                            content: '<div style="font-size:13px;"><strong><i class="fa fa-pause-circle" style="color:#f39c12;"></i> Stationary Stop</strong><br>' +
640
                                '<i class="fa fa-clock-o"></i> Duration: <strong>' + dataFormatDuration(s.duration) + '</strong><br>' +
641
                                '<i class="fa fa-sign-in"></i> From: <strong>' + new Date(s.startTime).toLocaleTimeString() + '</strong><br>' +
642
                                '<i class="fa fa-sign-out"></i> To: <strong>' + new Date(s.endTime).toLocaleTimeString() + '</strong><br>' +
643
                                '<i class="fa fa-map-marker"></i> ' + s.lat.toFixed(6) + ', ' + s.lng.toFixed(6) + '<br>' +
644
                                '<i class="fa fa-dot-circle-o"></i> Points in cluster: ' + s.pointCount + '</div>'
645
                        });
646
                        marker.addListener('click', function() { iw.open(dataLiveMap, marker); });
647
                    })(sm, stop);
648
                    dataLiveMap._stopMarkers.push(sm);
649
                }
650
                var lastPoint = pathCoords[pathCoords.length - 1];
651
                dataLiveMap._currentMarker = new google.maps.Marker({
652
                    position: lastPoint, map: dataLiveMap,
653
                    icon: { url: '${rc.contextPath}/resources/images/Bike-Icon-SD.png', scaledSize: new google.maps.Size(40, 40), anchor: new google.maps.Point(20, 20) },
654
                    title: 'Last Known Location', zIndex: 999
655
                });
656
                var lastGp = validPoints[validPoints.length - 1];
657
                var curIw = new google.maps.InfoWindow({
658
                    content: '<div style="font-size:13px;"><strong><i class="fa fa-crosshairs" style="color:#27ae60;"></i> Last Known Location</strong><br>' +
659
                        '<i class="fa fa-clock-o"></i> Time: ' + dataFormatEpoch(lastGp.ts) + '<br>' +
660
                        '<i class="fa fa-crosshairs"></i> Accuracy: ' + (lastGp.accuracy || 'N/A') + 'm</div>'
661
                });
662
                dataLiveMap._currentMarker.addListener('click', function() { curIw.open(dataLiveMap, dataLiveMap._currentMarker); });
663
            }
664
        }
665
 
666
        if (hasPoints) dataLiveMap.fitBounds(bounds);
667
 
668
        var totalVisits = visits.filter(function(v) { return v.markType !== 'PUNCHIN' && v.markType !== 'PUNCHOUT'; }).length;
36783 vikas 669
        var completedVisits = visits.filter(function(v) { return v.markType === 'CHECKIN-CHECKOUT' || v.markType === 'CHECKIN_CHECKOUT' || v.markType === 'CHECKOUT'; }).length;
36696 vikas 670
        var now = new Date();
671
        var timeStr = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0') + ':' + now.getSeconds().toString().padStart(2,'0');
672
        var avgAccuracy = accuracyCount > 0 ? (accuracySum / accuracyCount).toFixed(0) : null;
673
        var accuracyLabel = '', accuracyBadge = '';
674
        if (avgAccuracy) {
675
            if (avgAccuracy <= 10) { accuracyLabel = 'High'; accuracyBadge = 'background:#27ae60;'; }
676
            else if (avgAccuracy <= 30) { accuracyLabel = 'Good'; accuracyBadge = 'background:#2ecc71;'; }
677
            else if (avgAccuracy <= 100) { accuracyLabel = 'Average'; accuracyBadge = 'background:#f39c12;'; }
678
            else { accuracyLabel = 'Low'; accuracyBadge = 'background:#e74c3c;'; }
679
        }
680
        var totalGpsKm = (totalGpsDistanceM / 1000).toFixed(2);
681
        var infoHtml = '<strong>' + username + '</strong> | Visits: <strong>' + totalVisits + '</strong> | Completed: <strong>' + completedVisits + '</strong>';
682
        if (pathPoints.length > 0) {
683
            infoHtml += ' | GPS Distance: <strong>' + totalGpsKm + ' km</strong>';
684
            infoHtml += ' | Points: <strong>' + pathPoints.length + '</strong>';
685
            if (accuracyLabel) infoHtml += ' | Accuracy: <span class="label" style="' + accuracyBadge + ' color:white; padding:2px 8px; border-radius:3px; font-size:11px;">' + accuracyLabel + '</span> (' + avgAccuracy + 'm)';
686
            if (stopsDetected.length > 0) infoHtml += ' | <span style="color:#f39c12;"><i class="fa fa-pause-circle"></i></span> Stops: <strong>' + stopsDetected.length + '</strong>';
687
            infoHtml += ' | <img src="${rc.contextPath}/resources/images/Bike-Icon-SD.png" style="height:16px; vertical-align:middle;"> Last Known';
688
        } else {
689
            infoHtml += ' | <span style="color:#999;"><i class="fa fa-exclamation-triangle"></i> No GPS data yet</span>';
690
        }
691
        infoHtml += ' | <span style="color:#e74c3c;"><i class="fa fa-arrow-right" style="font-size:10px;"></i></span> GPS Trail';
692
        if (visitCoords.length > 1) infoHtml += ' | <span style="background:#8e44ad; display:inline-block; width:14px; height:3px; vertical-align:middle; margin:0 2px;"></span> Route';
693
        var locToggle = data.locationToggleCount || 0;
694
        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>';
695
        infoHtml += ' | <span style="color:#999;">Updated: ' + timeStr + '</span>';
696
        $('#dataLiveInfo').html(infoHtml);
697
        if (pathPoints.length > 0 && pathPoints.length < 10) {
698
            $('#dataLiveInfo').append(
699
                '<div style="margin-top:4px; padding:4px 8px; background:#fff3cd; border:1px solid #ffc107; border-radius:3px; font-size:11px; color:#856404;">' +
700
                '<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>');
701
        }
702
    }
703
 
704
    function dataStartAutoRefresh() {
705
        dataStopAutoRefresh();
706
        if ($('#dataAutoRefresh').is(':checked') && dataLiveUserId) {
707
            dataLiveInterval = setInterval(function() { dataLoadLiveTracking(dataLiveUserId, dataLiveUsername); }, 30000);
708
        }
709
    }
710
    function dataStopAutoRefresh() {
711
        if (dataLiveInterval) { clearInterval(dataLiveInterval); dataLiveInterval = null; }
712
    }
713
 
714
    $(document).on('click', '.data-live-tracking-btn', function() {
715
        dataLiveUserId = $(this).data('userid');
716
        dataLiveUsername = $(this).data('username');
717
        $('#dataLiveTitle').html('<img src="${rc.contextPath}/resources/images/Bike-Icon-SD.png" style="height:24px; vertical-align:middle; margin-right:6px;"> Live Tracking - ' + dataLiveUsername);
718
        $('#dataLiveInfo').html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');
719
        $('#dataLiveModal').modal('show');
720
        setTimeout(function() {
721
            dataLiveMap = null;
722
            if (window.google && window.google.maps) {
723
                dataLoadLiveTracking(dataLiveUserId, dataLiveUsername);
724
                dataStartAutoRefresh();
725
            } else {
726
                var s = document.createElement('script');
727
                s.src = 'https://maps.googleapis.com/maps/api/js?key=AIzaSyAckO0y4Z6WhBOuMjNjioWLSYZDhGEvGBc&libraries=geometry&v=weekly';
728
                s.onload = function() { dataLoadLiveTracking(dataLiveUserId, dataLiveUsername); dataStartAutoRefresh(); };
729
                document.head.appendChild(s);
730
            }
731
        }, 300);
732
    });
733
 
734
    $(document).on('click', '#dataRefreshBtn', function() {
735
        if (dataLiveUserId) {
736
            $('#dataLiveInfo').html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Refreshing...</div>');
737
            dataLoadLiveTracking(dataLiveUserId, dataLiveUsername);
738
        }
739
    });
740
    $(document).on('change', '#dataAutoRefresh', function() {
741
        $(this).is(':checked') ? dataStartAutoRefresh() : dataStopAutoRefresh();
742
    });
743
    $('#dataLiveModal').on('hidden.bs.modal', function() { dataStopAutoRefresh(); dataLiveUserId = null; });
36645 vikas 744
</script>
36696 vikas 745
 
746
<!-- Live Tracking Modal -->
747
<div class="modal fade" id="dataLiveModal" tabindex="-1" role="dialog">
748
    <div class="modal-dialog modal-lg" role="document" style="width:90%;">
749
        <div class="modal-content">
750
            <div class="modal-header" style="background:#27ae60; color:white;">
751
                <button type="button" class="close" data-dismiss="modal" style="color:white;">&times;</button>
752
                <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>
753
            </div>
754
            <div class="modal-body" style="padding:0;">
755
                <div id="dataLiveInfo" style="padding:8px 15px; background:#f8f9fa; border-bottom:1px solid #ddd; font-size:13px;"></div>
756
                <div id="dataLiveMap" style="height:500px; width:100%;"></div>
757
            </div>
758
            <div class="modal-footer">
759
                <label style="float:left; margin-top:7px; font-weight:normal; font-size:13px;">
760
                    <input type="checkbox" id="dataAutoRefresh" checked style="margin-right:5px;"> Auto-refresh (30s)
761
                </label>
762
                <button type="button" class="btn btn-primary" id="dataRefreshBtn"><i class="fa fa-refresh"></i> Refresh</button>
763
                <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
764
            </div>
765
        </div>
766
    </div>
767
</div>
36761 ranu 768
 
769
<!-- Beat calendar modal (iframe loads /beatPlanWindow with auto-selected user) -->
770
<div class="modal fade" id="reportBeatCalendarModal" tabindex="-1" style="overflow-y:auto;">
771
    <div class="modal-dialog" style="width:95%; max-width:1300px; margin-top:20px;">
772
        <div class="modal-content">
773
            <div class="modal-header">
774
                <button type="button" class="close" data-dismiss="modal"
775
                        onclick="$('#reportBeatCalendarFrame').attr('src','');">&times;
776
                </button>
777
                <h4 class="modal-title" id="reportBeatCalendarTitle">Calendar</h4>
778
            </div>
779
            <div class="modal-body" style="padding:0;">
780
                <iframe id="reportBeatCalendarFrame" style="width:100%; height:80vh; border:none;"></iframe>
781
            </div>
782
        </div>
783
    </div>
784
</div>
785
 
786
<script>
787
    $(document).on('click', '.beat-view-calendar', function () {
788
        var userId = $(this).data('userid');
789
        var userName = $(this).data('username') || '';
790
        var url = context + '/beatPlanWindow?autoUserId=' + userId
791
                + '&autoUserName=' + encodeURIComponent(userName);
792
        $('#reportBeatCalendarTitle').text('Calendar - ' + userName);
793
        $('#reportBeatCalendarFrame').attr('src', url);
794
        $('#reportBeatCalendarModal').modal('show');
795
    });
796
 
797
    $(document).on('click', '.beat-edit-on-date', function () {
798
        var userId = $(this).data('userid');
799
        var userName = $(this).data('username') || '';
800
        var beatId = $(this).data('beatid');
801
        var date = $(this).data('date');
802
        var url = context + '/beatPlanWindow?autoUserId=' + userId
803
                + '&autoUserName=' + encodeURIComponent(userName)
804
                + '&editBeatId=' + beatId + '&editDate=' + date;
805
        $('#reportBeatCalendarTitle').text('Edit Beat - ' + userName + ' on ' + date);
806
        $('#reportBeatCalendarFrame').attr('src', url);
807
        $('#reportBeatCalendarModal').modal('show');
808
    });
809
 
810
    // ---------- ASSIGN VISIT (mirrors Beat Day View; IDs/classes prefixed with `r` to avoid collisions) ----------
811
    var rAvContext = {authUserId: null, userName: '', date: '', beatId: null, allParties: [], agendaOptions: []};
812
 
813
    var R_AGENDA_MULTISELECT_OPTS = {
814
        includeSelectAllOption: false,
815
        maxHeight: 220,
816
        buttonWidth: '240px',
817
        numberDisplayed: 2,
818
        nonSelectedText: 'Pick agenda(s)',
819
        nSelectedText: ' agendas',
820
        allSelectedText: 'All agendas',
821
        enableFiltering: true,
822
        enableCaseInsensitiveFiltering: true
823
    };
824
 
825
    function rAgendaSelectHtml(p) {
826
        var preset = {};
827
        (p.existingAgendas || []).forEach(function (a) {
828
            preset[a] = true;
829
        });
830
        var html = '<select multiple class="r-av-party-agenda" data-fofoid="' + p.fofoStoreId + '">';
831
        rAvContext.agendaOptions.forEach(function (opt) {
832
            html += '<option value="' + opt + '"' + (preset[opt] ? ' selected' : '') + '>' + opt + '</option>';
833
        });
834
        html += '</select>';
835
        return html;
836
    }
837
 
838
    function rRebuildDefaultAgenda() {
839
        var $sel = $('#rAvDefaultAgenda');
840
        try {
841
                $sel.multiselect('destroy');
842
        } catch (e) {
843
        }
844
        var html = '';
845
        rAvContext.agendaOptions.forEach(function (opt) {
846
            html += '<option value="' + opt + '">' + opt + '</option>';
847
        });
848
            $sel.html(html);
849
        var defaultOpts = $.extend({}, R_AGENDA_MULTISELECT_OPTS);
850
        defaultOpts.nonSelectedText = 'Pick agenda(s) to apply';
851
            $sel.multiselect(defaultOpts);
852
    }
853
 
854
    function rInitRowMultiselects() {
855
        $('.r-av-party-agenda').each(function () {
856
            var $sel = $(this);
857
            try {
858
                    $sel.multiselect('destroy');
859
            } catch (e) {
860
            }
861
                $sel.multiselect(R_AGENDA_MULTISELECT_OPTS);
862
        });
863
    }
864
 
865
    function rUpdateAvSelectedCount() {
866
        var n = $('.r-av-party-chk:checked').length;
867
        $('#rAvSelectedCount').text(n + ' selected');
868
        $('#rAvSubmit').prop('disabled', n === 0);
869
    }
870
 
871
    function rRenderAvParties(list) {
872
        if (!list || list.length === 0) {
873
            $('#rAvPartyList').html('<div style="padding:20px; color:#999; text-align:center;">No parties found.</div>');
874
            return;
875
        }
876
        var html = '<table class="table table-condensed table-hover" style="margin-bottom:0; font-size:13px;">';
877
        html += '<thead><tr>'
878
                + '<th style="width:30px;"><input type="checkbox" id="rAvSelectAll" title="Select all"></th>'
879
                + '<th>Code</th><th>Outlet</th><th>City</th>'
880
                + '<th style="width:260px;">Agenda</th>'
881
                + '<th style="width:260px;">Description</th>'
882
                + '</tr></thead><tbody>';
883
        list.forEach(function (p) {
884
            var safeName = (p.outletName || '').replace(/"/g, '&quot;');
885
            var existingDesc = (p.existingDescription || '').replace(/"/g, '&quot;');
886
            html += '<tr>'
887
                    + '<td><input type="checkbox" class="r-av-party-chk" '
888
                    + 'data-fofoid="' + p.fofoStoreId + '" '
889
                    + 'data-name="' + safeName + '" '
890
                    + 'data-lat="' + (p.latitude || '') + '" '
891
                    + 'data-lng="' + (p.longitude || '') + '"></td>'
892
                    + '<td>' + (p.code || '') + '</td>'
893
                    + '<td>' + (p.outletName || '') + '</td>'
894
                    + '<td>' + (p.city || '') + '</td>'
895
                    + '<td>' + rAgendaSelectHtml(p) + '</td>'
896
                    + '<td><textarea class="form-control input-sm r-av-party-desc"'
897
                    + ' data-fofoid="' + p.fofoStoreId + '"'
898
                    + ' rows="2" placeholder="Optional notes..."'
899
                    + ' style="resize:vertical; min-height:34px;">' + existingDesc + '</textarea></td>'
900
                    + '</tr>';
901
        });
902
        html += '</tbody></table>';
903
        $('#rAvPartyList').html(html);
904
        rInitRowMultiselects();
905
        rUpdateAvSelectedCount();
906
    }
907
 
908
    $(document).on('click', '.beat-assign-visit', function () {
909
        rAvContext.authUserId = $(this).data('userid');
910
        rAvContext.userName = $(this).data('username') || '';
911
        rAvContext.date = $(this).data('date');
912
        rAvContext.beatId = $(this).data('beatid');
913
        rAvContext.allParties = [];
914
 
915
        $('#rAvTitle').text('Assign Visit - ' + rAvContext.userName + ' on ' + rAvContext.date);
916
        $('#rAvSearch').val('');
917
        $('#rAvPartyList').html('<div style="padding:20px; color:#999; text-align:center;">Loading parties...</div>');
918
        $('#rAvMsg').text('');
919
        $('#rAvSelectedCount').text('0 selected');
920
        $('#rAvSubmit').prop('disabled', true);
921
        $('#rAssignVisitModal').modal('show');
922
 
923
        $.get(context + '/beatPlan/assignVisit/parties', {
924
            authUserId: rAvContext.authUserId,
925
            date: rAvContext.date,
926
            beatId: rAvContext.beatId
927
        }).done(function (r) {
928
            var data = r.response || r;
929
            rAvContext.allParties = (data.parties || []).filter(function (p) {
930
                return !p.inBeat;
931
            });
932
            rAvContext.agendaOptions = data.agendaOptions || [];
933
            if (!data.dtrUserId) {
934
                $('#rAvPartyList').html('<div style="padding:20px; color:#c62828; text-align:center;">'
935
                        + 'This user has no dtr.users record (cannot create visit tasks).</div>');
936
                return;
937
            }
938
            rRebuildDefaultAgenda();
939
            rRenderAvParties(rAvContext.allParties);
940
        }).fail(function (xhr) {
941
            $('#rAvPartyList').html('<div style="padding:20px; color:#c62828; text-align:center;">'
942
                    + 'Error loading parties: ' + (xhr.responseText || xhr.statusText) + '</div>');
943
        });
944
    });
945
 
946
    $(document).on('input', '#rAvSearch', function () {
947
        var q = $(this).val().toLowerCase().trim();
948
        if (!q) {
949
            rRenderAvParties(rAvContext.allParties);
950
            return;
951
        }
952
        var filtered = rAvContext.allParties.filter(function (p) {
953
            return (p.code || '').toLowerCase().indexOf(q) !== -1
954
                    || (p.outletName || '').toLowerCase().indexOf(q) !== -1
955
                    || (p.city || '').toLowerCase().indexOf(q) !== -1;
956
        });
957
        rRenderAvParties(filtered);
958
    });
959
 
960
    $(document).on('change', '#rAvSelectAll', function () {
961
        $('.r-av-party-chk').prop('checked', this.checked);
962
        rUpdateAvSelectedCount();
963
    });
964
    $(document).on('change', '.r-av-party-chk', rUpdateAvSelectedCount);
965
 
966
    $(document).on('click', '#rAvApplyAgenda', function () {
967
        var defValues = $('#rAvDefaultAgenda').val() || [];
968
        if (defValues.length === 0) {
969
            alert('Pick at least one agenda above first');
970
            return;
971
        }
972
        $('.r-av-party-chk:checked').each(function () {
973
            var fid = $(this).data('fofoid');
974
            var $sel = $('.r-av-party-agenda[data-fofoid="' + fid + '"]');
975
            var existing = $sel.val() || [];
976
            var merged = existing.slice();
977
            defValues.forEach(function (v) {
978
                if (merged.indexOf(v) === -1) merged.push(v);
979
            });
980
                $sel.val(merged);
981
            try {
982
                    $sel.multiselect('refresh');
983
            } catch (e) {
984
            }
985
        });
986
    });
987
 
988
    $(document).on('click', '#rAvSubmit', function () {
989
        var picks = [];
990
        var missing = 0;
991
        $('.r-av-party-chk:checked').each(function () {
992
            var fid = parseInt($(this).data('fofoid'));
993
            var agendas = $('.r-av-party-agenda[data-fofoid="' + fid + '"]').val() || [];
994
            var description = ($('.r-av-party-desc[data-fofoid="' + fid + '"]').val() || '').trim();
995
            if (agendas.length === 0) missing++;
996
            picks.push({
997
                fofoStoreId: fid,
998
                outletName: $(this).data('name'),
999
                latitude: $(this).data('lat') ? String($(this).data('lat')) : null,
1000
                longitude: $(this).data('lng') ? String($(this).data('lng')) : null,
1001
                agendas: agendas,
1002
                description: description
1003
            });
1004
        });
1005
        if (picks.length === 0) return;
1006
        if (missing > 0) {
1007
            if (!confirm(missing + ' selected row(s) have no agenda. Submit anyway with default "Visit"?')) return;
1008
        }
1009
        var btn = $(this);
1010
        btn.prop('disabled', true).text('Assigning...');
1011
        $('#rAvMsg').text('');
1012
        $.ajax({
1013
            url: context + '/beatPlan/assignVisit/submit',
1014
            type: 'POST',
1015
            contentType: 'application/json',
1016
            data: JSON.stringify({
1017
                authUserId: rAvContext.authUserId,
1018
                planDate: rAvContext.date,
1019
                parties: picks
1020
            }),
1021
            success: function (r) {
1022
                var d = r.response || r;
1023
                $('#rAvMsg').html('<span style="color:#2e7d32;">' + (d.message || 'Saved') + '</span>');
1024
                btn.prop('disabled', false).text('Save Assignments');
1025
                setTimeout(function () {
1026
                    $('#rAssignVisitModal').modal('hide');
1027
                }, 1500);
1028
            },
1029
            error: function (xhr) {
1030
                var msg = 'Failed';
1031
                try {
1032
                    msg = (JSON.parse(xhr.responseText).response.message) || msg;
1033
                } catch (e) {
1034
                }
1035
                $('#rAvMsg').html('<span style="color:#c62828;">' + msg + '</span>');
1036
                btn.prop('disabled', false).text('Save Assignments');
1037
            }
1038
        });
1039
    });
1040
</script>
1041
 
1042
<!-- Assign Visit Modal (mirrors Beat Day View; prefixed IDs to avoid collisions) -->
1043
<div class="modal fade" id="rAssignVisitModal" tabindex="-1" style="overflow-y:auto;">
1044
    <div class="modal-dialog" style="width:90%; max-width:900px; margin-top:30px;">
1045
        <div class="modal-content">
1046
            <div class="modal-header">
1047
                <button type="button" class="close" data-dismiss="modal">&times;</button>
1048
                <h4 class="modal-title" id="rAvTitle">Assign Visit</h4>
1049
            </div>
1050
            <div class="modal-body">
1051
                <div style="display:flex; gap:10px; align-items:flex-start; margin-bottom:10px;">
1052
                    <div style="flex:1;">
1053
                        <label style="font-size:11px; color:#666; font-weight:normal; margin-bottom:2px;">
1054
                            Agenda - applies to selected rows
1055
                        </label>
1056
                        <select id="rAvDefaultAgenda" multiple></select>
1057
                    </div>
1058
                    <button type="button" class="btn btn-default btn-sm" id="rAvApplyAgenda" style="margin-top:18px;"
1059
                            title="Add the picked agendas into all selected rows (merges with existing)">
1060
                        Apply to Selected
1061
                    </button>
1062
                </div>
1063
                <div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">
1064
                    <input type="text" id="rAvSearch" class="form-control input-sm"
1065
                           placeholder="Search by code, outlet name or city..." style="flex:1;">
1066
                    <span id="rAvSelectedCount" style="font-size:12px; color:#666;">0 selected</span>
1067
                </div>
1068
                <div id="rAvPartyList"
1069
                     style="max-height:50vh; overflow-y:auto; border:1px solid #e5e5e5; border-radius:4px;"></div>
1070
                <div id="rAvMsg" style="margin-top:10px; font-size:12px;"></div>
1071
            </div>
1072
            <div class="modal-footer">
1073
                <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
1074
                <button type="button" class="btn btn-success" id="rAvSubmit" disabled>Save Assignments</button>
1075
            </div>
1076
        </div>
1077
    </div>
1078
</div>