Subversion Repositories SmartDukaan

Rev

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

Rev Author Line No. Line
36621 ranu 1
// ============================================================
2
// BEAT PLAN APP — Map-First Route Planner
3
// ============================================================
4
 
5
var map, infoWindow, homeMarker, previewLine;
6
var markers = {};
7
var routeLines = [];
8
var distLabels = [];
9
 
10
var state = {
11
	authUserId: 0,
12
	categoryId: 4,
13
	homeLat: null, homeLng: null, homeName: '',
14
	currentDay: 1,
15
	days: [], // [{dayNumber, startLat, startLng, startName, visits:[], endAction, stayName, stayLat, stayLng, totalKm, totalMins}]
16
	partners: [],
17
	calMonth: null,
18
	calData: null,
19
	calSelectedBeat: null,
20
	calPickedDates: []
21
};
22
 
23
var VISIT_MINS = 30;
24
var AVG_SPEED = 30;
25
var DAY_LIMIT = 540;
26
var ROAD_FACTOR = 1.3;
27
var COLORS = ['#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b', '#06b6d4', '#ec4899', '#334155', '#14b8a6', '#dc2626'];
28
 
29
// ============ TOP BAR ============
30
$('#bp-level').on('change', function () {
31
	var level = $(this).val();
32
	$('#bp-auth-user').html('<option value="">Select User</option>');
33
	if (!level) return;
34
 
35
	$.ajax({
36
		url: context + "/beatPlan/getAuthUsers", type: "GET", dataType: "json",
37
		data: {categoryId: $('#bp-category').val(), escalationType: level},
38
		success: function (r) {
39
			var data = r.response || r;
40
			var html = '<option value="">Select User</option>';
41
			data.forEach(function (u) {
42
				html += '<option value="' + u.id + '">' + u.name + '</option>';
43
			});
44
			$('#bp-auth-user').html(html);
45
		}
46
	});
47
});
48
 
49
$('#bp-load').on('click', function () {
50
	var uid = $('#bp-auth-user').val();
51
	if (!uid) {
52
		alert('Select a user');
53
		return;
54
	}
55
	state.authUserId = parseInt(uid);
56
	state.categoryId = parseInt($('#bp-category').val());
36632 ranu 57
    state.mode = 'list'; // 'list' or 'create' or 'view'
36621 ranu 58
	$('#bp-user-label').text($('#bp-auth-user option:selected').text());
59
 
36632 ranu 60
    // Load home location
36621 ranu 61
	$.ajax({
62
		url: context + "/beatPlan/getBaseLocation", type: "GET", dataType: "json",
63
		data: {authUserId: uid},
64
		success: function (r) {
65
			var data = r.response || r;
66
			if (data.locationName) {
67
				state.homeLat = parseFloat(data.latitude);
68
				state.homeLng = parseFloat(data.longitude);
69
				state.homeName = data.locationName;
36632 ranu 70
            }
71
            showBeatList();
72
        }
73
    });
36621 ranu 74
});
75
 
36632 ranu 76
// ============ BEAT LIST VIEW ============
77
function showBeatList() {
78
    state.mode = 'list';
79
    $('#bottom-bar').hide();
80
 
81
    // Load all beats for this user
82
    var now = new Date();
83
    var month = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
84
 
85
    $.ajax({
86
        url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
87
        data: {authUserId: state.authUserId, month: month},
88
        success: function (r) {
89
            var data = r.response || r;
90
            var beats = data.scheduledBeats || [];
91
 
92
            var html = '<div style="margin-bottom:10px;">';
93
            html += '<button id="btn-new-beat" style="width:100%;padding:10px;border:2px dashed #475569;border-radius:8px;background:none;color:#60a5fa;cursor:pointer;font-size:13px;font-weight:600;font-family:inherit;">+ New Beat</button>';
94
            html += '</div>';
95
 
96
            if (beats.length === 0) {
97
                html += '<p style="color:#64748b;font-size:12px;text-align:center;padding:20px;">No beats created yet.</p>';
98
            }
99
 
100
            // Deduplicate by beatName
101
            var seen = {};
102
            beats.forEach(function (b) {
103
                var key = b.beatName || b.planGroupId;
104
                if (!seen[key]) {
105
                    seen[key] = {beat: b, count: 0};
106
                }
107
                seen[key].count++;
108
            });
109
 
110
            Object.keys(seen).forEach(function (key) {
111
                var item = seen[key];
112
                var b = item.beat;
113
                var totalV = 0, totalKm = 0;
114
                b.days.forEach(function (d) {
115
                    totalV += d.visitCount || 0;
116
                    totalKm += d.totalKm || 0;
117
                });
118
 
119
                var statusCls = b.status === 'running' ? 'status-running' : b.status === 'completed' ? 'status-completed' : 'status-scheduled';
120
                var statusLabel = b.status === 'running' ? 'Live' : b.status === 'completed' ? 'Done' : b.status === 'unscheduled' ? 'Unscheduled' : 'Scheduled';
121
 
122
                html += '<div class="beat-card beat-list-item" data-plangroup="' + b.planGroupId + '" style="cursor:pointer;">';
123
                html += '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + b.beatColor + ';"></span> ' + b.beatName + ' <span class="status-badge ' + statusCls + '">' + statusLabel + '</span></div>';
124
                html += '<div class="beat-meta">' + b.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km';
125
                if (item.count > 1) html += ' | ' + item.count + 'x scheduled';
126
                html += '</div>';
127
                html += '</div>';
128
            });
129
 
130
            $('#route-list').html(html);
131
        }
132
    });
133
}
134
 
135
// Click on existing beat → view its route on map
136
$(document).on('click', '.beat-list-item', function () {
137
    var pg = $(this).data('plangroup');
138
    viewBeat(pg);
139
});
140
 
141
function viewBeat(planGroupId) {
142
    state.mode = 'view';
143
    $('#bottom-bar').hide();
144
 
145
    // Load partners first (for coordinates), then load beat visits
146
    $.ajax({
147
        url: context + "/beatPlan/getPartners", type: "GET", dataType: "json",
148
        data: {
149
            authUserId: state.authUserId,
150
            categoryId: state.categoryId,
151
            startLat: state.homeLat,
152
            startLng: state.homeLng
153
        },
154
        success: function (r) {
155
            var pData = r.response || r;
156
            state.partners = pData.partners || [];
157
 
158
            // Build partner lookup by fofoId
159
            var partnerMap = {};
160
            state.partners.forEach(function (p) {
161
                partnerMap[p.fofoId] = p;
162
            });
163
 
164
            // Load beat visits
165
            $.ajax({
166
                url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
167
                data: {authUserId: state.authUserId, month: '2020-01'},
168
                success: function (r2) {
169
                    var calData = r2.response || r2;
170
                    var beat = (calData.scheduledBeats || []).find(function (b) {
171
                        return b.planGroupId === planGroupId;
172
                    });
173
                    if (!beat) {
174
                        alert('Beat not found');
175
                        return;
176
                    }
177
 
178
                    // Reconstruct state.days from beat data for map drawing
179
                    // We need actual visit fofoIds — fetch from beat_plan records
180
                    $.ajax({
181
                        url: context + "/beatPlan/getBeatVisits", type: "GET", dataType: "json",
182
                        data: {planGroupId: planGroupId},
183
                        success: function (r3) {
184
                            var visits = (r3.response || r3) || [];
185
 
186
                            // Group visits by dayNumber
187
                            var dayVisitsMap = {};
188
                            visits.forEach(function (v) {
189
                                if (!dayVisitsMap[v.dayNumber]) dayVisitsMap[v.dayNumber] = [];
190
                                dayVisitsMap[v.dayNumber].push(v);
191
                            });
192
 
193
                            // Build days for route display
194
                            state.days = [];
195
                            beat.days.forEach(function (d) {
196
                                var dayVisits = (dayVisitsMap[d.dayNumber] || []).sort(function (a, b) {
197
                                    return a.sequenceOrder - b.sequenceOrder;
198
                                });
199
                                var startLat = state.homeLat, startLng = state.homeLng, startName = state.homeName;
200
 
201
                                // If not first day, start from previous day's last visit
202
                                if (state.days.length > 0) {
203
                                    var prevDay = state.days[state.days.length - 1];
204
                                    if (prevDay.visits.length > 0 && prevDay.endAction === 'DAYBREAK') {
205
                                        var lastV = prevDay.visits[prevDay.visits.length - 1];
206
                                        startLat = lastV.lat;
207
                                        startLng = lastV.lng;
208
                                        startName = lastV.code || lastV.name;
209
                                    }
210
                                }
211
 
212
                                var builtVisits = [];
213
                                dayVisits.forEach(function (v) {
36642 ranu 214
                                    if (v.visitType === 'lead') {
215
                                        // Lead visit — get lead geo from API (sync for simplicity)
216
                                        var leadVisit = {
217
                                            id: v.fofoId, type: 'lead',
218
                                            name: 'LEAD #' + v.fofoId,
219
                                            code: 'LEAD',
220
                                            lat: null, lng: null,
221
                                            isLead: true
222
                                        };
223
                                        $.ajax({
224
                                            url: context + '/lead-geo/check/' + v.fofoId,
225
                                            method: 'GET',
226
                                            async: false,
227
                                            success: function (resp) {
228
                                                var geoData = resp.response || resp;
229
                                                if (geoData && geoData.hasApprovedGeo) {
230
                                                    leadVisit.lat = geoData.latitude;
231
                                                    leadVisit.lng = geoData.longitude;
232
                                                }
233
                                            }
36632 ranu 234
                                        });
36642 ranu 235
                                        // Also get lead name
236
                                        $.ajax({
237
                                            url: context + '/getLead?leadId=' + v.fofoId,
238
                                            method: 'GET',
239
                                            async: false,
240
                                            success: function (resp) {
241
                                                var lead = resp.response || resp;
242
                                                if (lead && lead.firstName) {
243
                                                    leadVisit.name = 'LEAD - ' + lead.firstName + ' ' + (lead.lastName || '');
244
                                                }
245
                                            }
246
                                        });
247
                                        builtVisits.push(leadVisit);
248
                                    } else {
249
                                        var p = partnerMap[v.fofoId];
250
                                        if (p) {
251
                                            builtVisits.push({
252
                                                id: p.fofoId, type: 'partner',
253
                                                name: p.code + ' - ' + (p.outletName || p.businessName || ''),
254
                                                code: p.code,
255
                                                lat: p.latitude ? parseFloat(p.latitude) : null,
256
                                                lng: p.longitude ? parseFloat(p.longitude) : null
257
                                            });
258
                                        }
36632 ranu 259
                                    }
260
                                });
261
 
262
                                state.days.push({
263
                                    dayNumber: d.dayNumber,
264
                                    startLat: startLat,
265
                                    startLng: startLng,
266
                                    startName: startName,
267
                                    visits: builtVisits,
268
                                    endAction: d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME',
269
                                    totalKm: d.totalKm || 0,
270
                                    totalMins: d.totalMins || 0
271
                                });
272
                            });
273
 
274
                            state.currentDay = 1;
275
 
276
                            // Render left panel
277
                            var html = '<div style="margin-bottom:8px;"><button class="btn-back-to-list" style="font-size:11px;padding:4px 10px;border:1px solid #475569;border-radius:4px;background:none;color:#94a3b8;cursor:pointer;font-family:inherit;">← Back to list</button></div>';
278
                            html += '<div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:10px;">' + beat.beatName + '</div>';
279
                            $('#route-list').html(html);
280
 
281
                            // Use renderRoutePanel for consistent display (but without remove/daybreak buttons)
282
                            state.mode = 'view';
283
                            renderRoutePanel();
284
                            // Prepend the back button + title before route days
285
                            $('#route-list').prepend('<div style="margin-bottom:8px;"><button class="btn-back-to-list" style="font-size:11px;padding:4px 10px;border:1px solid #475569;border-radius:4px;background:none;color:#94a3b8;cursor:pointer;font-family:inherit;">← Back to list</button></div><div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:10px;">' + beat.beatName + '</div>');
286
 
287
                            // Init map with all partners + highlight beat route
288
                            initMap();
36642 ranu 289
                            // Add lead markers to map
290
                            state.days.forEach(function (d) {
291
                                d.visits.forEach(function (v, i) {
292
                                    if (v.isLead && v.lat && v.lng) {
293
                                        var m = new google.maps.Marker({
294
                                            map: map,
295
                                            position: {lat: v.lat, lng: v.lng},
296
                                            title: v.name,
297
                                            label: {text: 'L', color: '#fff', fontSize: '11px', fontWeight: '700'},
298
                                            icon: {
299
                                                path: google.maps.SymbolPath.CIRCLE,
300
                                                scale: 16,
301
                                                fillColor: '#e67e22',
302
                                                fillOpacity: 0.95,
303
                                                strokeColor: '#fff',
304
                                                strokeWeight: 2
305
                                            }
306
                                        });
307
                                        markers[v.id] = m;
308
                                    }
309
                                });
310
                            });
36632 ranu 311
                            resetMarkerColors();
312
                            drawRoute();
313
                        }
314
                    });
315
                }
316
            });
317
        }
318
    });
319
}
320
 
321
// Back to list
322
$(document).on('click', '.btn-back-to-list', function () {
323
    showBeatList();
324
    // Clear map
325
    if (map) {
326
        for (var key in markers) {
327
            markers[key].setMap(null);
328
        }
329
        markers = {};
330
        clearRoute();
331
    }
332
});
333
 
334
// New Beat button
335
$(document).on('click', '#btn-new-beat', function () {
336
    var beatTitle = prompt('Enter beat name:', '');
337
    if (!beatTitle || !beatTitle.trim()) return;
338
 
339
    state.beatTitle = beatTitle.trim();
340
    state.mode = 'create';
341
 
342
    if (!state.homeLat) {
343
        showHomeModal();
344
    } else {
345
        loadPartners();
346
    }
347
});
348
 
36621 ranu 349
// ============ HOME LOCATION MODAL ============
350
var homeMap, homeMapMarker;
351
 
352
function showHomeModal() {
353
	$('#modal-home').addClass('show');
354
	setTimeout(function () {
355
		if (!homeMap) {
356
			homeMap = new google.maps.Map(document.getElementById('home-map'), {
357
				center: {lat: 28.6, lng: 77.2},
358
				zoom: 6
359
			});
360
			var ac = new google.maps.places.Autocomplete(document.getElementById('home-search'), {componentRestrictions: {country: 'in'}});
361
			homeMapMarker = new google.maps.Marker({map: homeMap, draggable: true});
362
 
363
			ac.addListener('place_changed', function () {
364
				var p = ac.getPlace();
365
				if (!p.geometry) return;
366
				homeMap.setCenter(p.geometry.location);
367
				homeMap.setZoom(14);
368
				homeMapMarker.setPosition(p.geometry.location);
369
				state.homeLat = p.geometry.location.lat();
370
				state.homeLng = p.geometry.location.lng();
371
				state.homeName = p.name || p.formatted_address || '';
372
			});
373
			homeMapMarker.addListener('dragend', function () {
374
				state.homeLat = homeMapMarker.getPosition().lat();
375
				state.homeLng = homeMapMarker.getPosition().lng();
376
			});
377
			homeMap.addListener('click', function (e) {
378
				homeMapMarker.setPosition(e.latLng);
379
				state.homeLat = e.latLng.lat();
380
				state.homeLng = e.latLng.lng();
381
			});
382
		}
383
	}, 200);
384
}
385
 
386
$('#home-cancel').on('click', function () {
387
	$('#modal-home').removeClass('show');
388
});
389
$('#home-confirm').on('click', function () {
390
	if (!state.homeLat) {
391
		alert('Please select a location');
392
		return;
393
	}
394
	$('#modal-home').removeClass('show');
395
	$.ajax({
396
		url: context + "/beatPlan/saveBaseLocation", type: "POST",
397
		data: {
398
			authUserId: state.authUserId,
399
			locationName: state.homeName || 'Home',
400
			latitude: state.homeLat,
401
			longitude: state.homeLng,
402
			address: $('#home-search').val()
403
		}
404
	});
405
	loadPartners();
406
});
407
 
408
// ============ LOAD PARTNERS + INIT MAP ============
409
function loadPartners() {
410
	$.ajax({
411
		url: context + "/beatPlan/getPartners", type: "GET", dataType: "json",
412
		data: {
413
			authUserId: state.authUserId,
414
			categoryId: state.categoryId,
415
			startLat: state.homeLat,
416
			startLng: state.homeLng
417
		},
418
		success: function (r) {
419
			var data = r.response || r;
420
			state.partners = data.partners || [];
421
			initDay1();
422
			initMap();
423
		}
424
	});
425
}
426
 
427
function initDay1() {
428
	state.days = [{
429
		dayNumber: 1, startLat: state.homeLat, startLng: state.homeLng, startName: state.homeName,
430
		visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
431
	}];
432
	state.currentDay = 1;
36632 ranu 433
    if (state.mode === 'create') $('#bottom-bar').show();
36621 ranu 434
	renderRoutePanel();
435
}
436
 
437
function initMap() {
438
	var center = {lat: state.homeLat, lng: state.homeLng};
439
	map = new google.maps.Map(document.getElementById('beat-map'), {
440
		center: center, zoom: 9,
441
		styles: [
442
			{elementType: 'geometry', stylers: [{color: '#1d2c4d'}]},
443
			{elementType: 'labels.text.fill', stylers: [{color: '#8ec3b9'}]},
444
			{elementType: 'labels.text.stroke', stylers: [{color: '#1a3646'}]},
445
			{featureType: 'road', elementType: 'geometry', stylers: [{color: '#304a7d'}]},
446
			{featureType: 'water', elementType: 'geometry', stylers: [{color: '#0e1626'}]}
447
		]
448
	});
449
	infoWindow = new google.maps.InfoWindow();
450
 
451
	// Home marker
452
	homeMarker = new google.maps.Marker({
453
		map: map, position: center, title: 'Home: ' + state.homeName,
454
		icon: {
455
			url: 'https://maps.google.com/mapfiles/kml/shapes/homegardenbusiness.png',
456
			scaledSize: new google.maps.Size(32, 32)
457
		},
458
		zIndex: 100
459
	});
460
 
461
	// Partner markers with name labels
462
	for (var key in markers) {
463
		markers[key].setMap(null);
464
	}
465
	markers = {};
466
 
467
	state.partners.forEach(function (p) {
468
		if (!p.latitude || !p.longitude) return;
469
		var lat = parseFloat(p.latitude), lng = parseFloat(p.longitude);
470
		if (isNaN(lat) || isNaN(lng)) return;
471
 
472
		var m = new google.maps.Marker({
473
			map: map, position: {lat: lat, lng: lng},
474
			title: p.code + ' - ' + (p.outletName || p.businessName || ''),
475
			label: {text: p.code || '', color: '#fff', fontSize: '10px', fontWeight: '600'},
476
			icon: {
477
				path: google.maps.SymbolPath.CIRCLE,
478
				scale: 14,
479
				fillColor: '#ef4444',
480
				fillOpacity: 0.9,
481
				strokeColor: '#fff',
482
				strokeWeight: 1
483
			}
484
		});
485
 
486
		m.addListener('click', function () {
487
			showMarkerPopup(p, m);
488
		});
489
		m.addListener('mouseover', function () {
490
			showPreviewLine(p);
491
		});
492
		m.addListener('mouseout', function () {
493
			hidePreviewLine();
494
		});
495
 
496
		markers[p.fofoId] = m;
497
	});
498
 
499
	fitBounds();
500
}
501
 
502
// ============ HOVER PREVIEW ============
503
function showPreviewLine(partner) {
504
	if (!partner.latitude || !partner.longitude) return;
505
	var day = curDay();
506
	var lastPt = day.visits.length > 0
507
		? {lat: day.visits[day.visits.length - 1].lat, lng: day.visits[day.visits.length - 1].lng}
508
		: {lat: day.startLat, lng: day.startLng};
509
	if (!lastPt.lat) return;
510
 
511
	var endPt = {lat: parseFloat(partner.latitude), lng: parseFloat(partner.longitude)};
512
	if (previewLine) previewLine.setMap(null);
513
	previewLine = new google.maps.Polyline({
514
		path: [lastPt, endPt],
515
		geodesic: true,
516
		strokeColor: '#60a5fa',
517
		strokeOpacity: 0.5,
518
		strokeWeight: 2,
519
		strokePattern: [10, 5],
520
		icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
521
		map: map
522
	});
523
}
524
 
525
function hidePreviewLine() {
526
	if (previewLine) {
527
		previewLine.setMap(null);
528
		previewLine = null;
529
	}
530
}
531
 
532
// ============ MARKER CLICK POPUP ============
533
function showMarkerPopup(partner, marker) {
534
	var day = curDay();
535
	var alreadyAdded = day.visits.some(function (v) {
536
		return v.id === partner.fofoId;
537
	});
538
	if (alreadyAdded) {
539
		infoWindow.setContent('<div class="marker-popup"><h4>' + partner.code + '</h4><p>Already in today\'s route</p></div>');
540
		infoWindow.open(map, marker);
541
		return;
542
	}
543
 
544
	var lastPt = day.visits.length > 0
545
		? {lat: day.visits[day.visits.length - 1].lat, lng: day.visits[day.visits.length - 1].lng}
546
		: {lat: day.startLat, lng: day.startLng};
547
 
548
	var dist = 0, travelMins = 0;
549
	if (lastPt.lat && partner.latitude) {
550
		dist = haversine(lastPt.lat, lastPt.lng, parseFloat(partner.latitude), parseFloat(partner.longitude)) * ROAD_FACTOR;
551
		travelMins = (dist / AVG_SPEED) * 60;
552
	}
553
 
554
	var name = partner.outletName || partner.businessName || '';
555
	var addr = partner.address || '';
556
 
557
	var html = '<div class="marker-popup">';
558
	html += '<h4>' + partner.code + ' - ' + name + '</h4>';
559
	html += '<div class="popup-meta">' + addr + '</div>';
560
	html += '<div class="popup-distance">~' + dist.toFixed(1) + ' km | ~' + fmtMins(travelMins) + ' travel + ' + VISIT_MINS + 'min visit</div>';
561
	html += '<button class="popup-next" onclick="addVisit(' + partner.fofoId + ',\'next\')">Next Visit</button>';
562
	html += '<button class="popup-last" onclick="addVisit(' + partner.fofoId + ',\'last\')">Last Visit</button>';
563
	html += '</div>';
564
 
565
	infoWindow.setContent(html);
566
	infoWindow.open(map, marker);
567
}
568
 
569
// ============ ADD VISIT ============
570
function addVisit(fofoId, action) {
571
	infoWindow.close();
572
	var p = state.partners.find(function (x) {
573
		return x.fofoId === fofoId;
574
	});
575
	if (!p) return;
576
 
577
	var day = curDay();
578
	var visit = {
579
		id: p.fofoId, type: 'partner',
580
		name: p.code + ' - ' + (p.outletName || p.businessName || ''),
581
		code: p.code,
582
		lat: p.latitude ? parseFloat(p.latitude) : null,
583
		lng: p.longitude ? parseFloat(p.longitude) : null
584
	};
585
 
586
	day.visits.push(visit);
587
 
588
	// Update marker to green
589
	if (markers[fofoId]) {
590
		markers[fofoId].setIcon({
591
			path: google.maps.SymbolPath.CIRCLE, scale: 14,
592
			fillColor: '#22c55e', fillOpacity: 0.9, strokeColor: '#fff', strokeWeight: 2
593
		});
594
		markers[fofoId].setLabel({text: '' + day.visits.length, color: '#fff', fontSize: '11px', fontWeight: '700'});
595
	}
596
 
597
	recalcDay();
598
	drawRoute();
599
	renderRoutePanel();
600
 
601
	if (action === 'last') {
602
		// Last visit — end day, go home
603
		day.endAction = 'HOME';
604
		day.stayLat = state.homeLat;
605
		day.stayLng = state.homeLng;
606
		day.stayName = state.homeName;
607
		drawReturnHome();
608
		renderRoutePanel();
609
	} else if (action === 'nextday') {
610
		// Continue next day — end current day, start new day from here
611
		day.endAction = 'CONTINUE';
612
		day.stayLat = visit.lat;
613
		day.stayLng = visit.lng;
614
		day.stayName = visit.name;
615
		startNextDay(visit.lat, visit.lng, visit.name);
616
	}
617
	// 'next' — just added, continue on same day
618
}
619
 
620
function drawReturnHome() {
621
	var day = curDay();
622
	if (day.visits.length === 0) return;
623
	var lastVisit = day.visits[day.visits.length - 1];
624
	if (!lastVisit.lat || !state.homeLat) return;
625
 
626
	var line = new google.maps.Polyline({
627
		path: [{lat: lastVisit.lat, lng: lastVisit.lng}, {lat: state.homeLat, lng: state.homeLng}],
628
		geodesic: true, strokeColor: '#22c55e', strokeOpacity: 0.6, strokeWeight: 2,
629
		icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
630
		map: map
631
	});
632
	routeLines.push(line);
633
 
634
	// Add return distance to day total
635
	var retDist = haversine(lastVisit.lat, lastVisit.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
636
	day.totalKm += retDist;
637
	day.totalMins += (retDist / AVG_SPEED) * 60;
638
	updateBottomBar();
639
}
640
 
641
function startNextDay(startLat, startLng, startName) {
642
	var nextNum = state.days.length + 1;
643
	state.days.push({
644
		dayNumber: nextNum, startLat: startLat, startLng: startLng, startName: startName,
645
		visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
646
	});
647
	state.currentDay = nextNum;
648
	updateBottomBar();
649
	renderRoutePanel();
650
	// Reset marker colors for unvisited
651
	resetMarkerColors();
652
}
653
 
654
function resetMarkerColors() {
655
	var allVisited = {};
656
	state.days.forEach(function (d) {
657
		d.visits.forEach(function (v) {
658
			allVisited[v.id] = true;
659
		});
660
	});
661
 
662
	state.partners.forEach(function (p) {
663
		if (!markers[p.fofoId]) return;
664
		if (allVisited[p.fofoId]) {
665
			markers[p.fofoId].setIcon({
666
				path: google.maps.SymbolPath.CIRCLE,
667
				scale: 14,
668
				fillColor: '#22c55e',
669
				fillOpacity: 0.9,
670
				strokeColor: '#fff',
671
				strokeWeight: 1
672
			});
673
		} else {
674
			markers[p.fofoId].setIcon({
675
				path: google.maps.SymbolPath.CIRCLE,
676
				scale: 14,
677
				fillColor: '#ef4444',
678
				fillOpacity: 0.9,
679
				strokeColor: '#fff',
680
				strokeWeight: 1
681
			});
682
			markers[p.fofoId].setLabel({text: p.code || '', color: '#fff', fontSize: '10px', fontWeight: '600'});
683
		}
684
	});
685
 
686
	// Re-label current day visits
687
	var day = curDay();
688
	day.visits.forEach(function (v, i) {
689
		if (markers[v.id]) markers[v.id].setLabel({
690
			text: '' + (i + 1),
691
			color: '#fff',
692
			fontSize: '11px',
693
			fontWeight: '700'
694
		});
695
	});
696
}
697
 
698
// ============ ROUTE DRAWING ============
699
function clearRoute() {
700
	routeLines.forEach(function (l) {
701
		l.setMap(null);
702
	});
703
	routeLines = [];
704
	distLabels.forEach(function (l) {
705
		l.setMap(null);
706
	});
707
	distLabels = [];
708
}
709
 
36632 ranu 710
var DAY_ROUTE_COLORS = ['#3b82f6', '#a855f7', '#06b6d4', '#ec4899', '#f59e0b', '#14b8a6'];
711
 
36621 ranu 712
function drawRoute() {
713
	clearRoute();
714
 
36632 ranu 715
    // Draw routes for ALL days, not just current
716
    state.days.forEach(function (day, dayIdx) {
717
        var isCurrent = day.dayNumber === state.currentDay;
718
        var routeColor = DAY_ROUTE_COLORS[dayIdx % DAY_ROUTE_COLORS.length];
719
        var opacity = isCurrent ? 0.9 : 0.5;
720
        var weight = isCurrent ? 3 : 2;
36621 ranu 721
 
36632 ranu 722
        var pts = [{lat: day.startLat, lng: day.startLng}];
723
        day.visits.forEach(function (v) {
724
            if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
725
        });
726
 
727
        // Draw visit route lines for this day
728
        for (var i = 0; i < pts.length - 1; i++) {
729
            var line = new google.maps.Polyline({
730
                path: [pts[i], pts[i + 1]], geodesic: true,
731
                strokeColor: routeColor, strokeOpacity: opacity, strokeWeight: weight, map: map
732
            });
733
            routeLines.push(line);
734
 
735
            // Only show distance labels on current day
736
            if (isCurrent) {
737
                var d = haversine(pts[i].lat, pts[i].lng, pts[i + 1].lat, pts[i + 1].lng) * ROAD_FACTOR;
738
                var mid = {lat: (pts[i].lat + pts[i + 1].lat) / 2, lng: (pts[i].lng + pts[i + 1].lng) / 2};
739
                var lbl = new google.maps.Marker({
740
                    position: mid, map: map, icon: {path: google.maps.SymbolPath.CIRCLE, scale: 0},
741
                    label: {text: d.toFixed(1) + 'km', color: '#93c5fd', fontSize: '10px', fontWeight: '600'}
742
                });
743
                distLabels.push(lbl);
744
            }
745
        }
746
 
747
        // Draw return-to-home line (not on DAYBREAK days)
748
        if (day.visits.length > 0 && day.endAction !== 'DAYBREAK') {
749
            var lastV = day.visits[day.visits.length - 1];
750
            if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
751
                var retLine = new google.maps.Polyline({
752
                    path: [{lat: lastV.lat, lng: lastV.lng}, {lat: state.homeLat, lng: state.homeLng}],
753
                    geodesic: true, strokeColor: '#22c55e', strokeOpacity: isCurrent ? 0.4 : 0.2, strokeWeight: 2,
754
                    icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
755
                    map: map
756
                });
757
                routeLines.push(retLine);
758
            }
759
        }
760
    });
36621 ranu 761
}
762
 
763
function recalcDay() {
764
	var day = curDay();
765
	var pts = [{lat: day.startLat, lng: day.startLng}];
766
	day.visits.forEach(function (v) {
767
		if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
768
	});
769
 
770
	var km = 0;
771
	for (var i = 0; i < pts.length - 1; i++) km += haversine(pts[i].lat, pts[i].lng, pts[i + 1].lat, pts[i + 1].lng) * ROAD_FACTOR;
772
	day.totalKm = km;
773
	day.totalMins = (km / AVG_SPEED) * 60 + day.visits.length * VISIT_MINS;
774
	updateBottomBar();
775
}
776
 
777
function updateBottomBar() {
778
	var day = curDay();
779
	$('#bb-day-label').text('Day ' + day.dayNumber);
780
	$('#bb-stops').text(day.visits.length + ' stops');
781
	$('#bb-distance').text(day.totalKm.toFixed(1) + ' km');
782
	$('#bb-time').text(fmtMins(day.totalMins));
783
 
784
	var pct = Math.min((day.totalMins / DAY_LIMIT) * 100, 100);
785
	var col = pct < 78 ? '#22c55e' : pct < 89 ? '#f59e0b' : '#ef4444';
786
	$('#bb-progress').css({width: pct + '%', background: col});
787
}
788
 
789
// ============ ROUTE PANEL ============
790
function renderRoutePanel() {
791
	var html = '';
36632 ranu 792
    if (state.mode === 'create' && state.beatTitle) {
793
        html += '<div style="margin-bottom:8px;"><button class="btn-back-to-list" style="font-size:11px;padding:4px 10px;border:1px solid #475569;border-radius:4px;background:none;color:#94a3b8;cursor:pointer;font-family:inherit;">← Back</button></div>';
794
        html += '<div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:10px;">' + state.beatTitle + '</div>';
795
    }
36621 ranu 796
	state.days.forEach(function (day) {
797
		var isCurrent = day.dayNumber === state.currentDay;
36632 ranu 798
        var isLastDay = day.dayNumber === state.days.length;
36621 ranu 799
		html += '<div class="route-day">';
800
		html += '<div class="route-day-header">';
801
		html += '<span>Day ' + day.dayNumber + (isCurrent ? ' (current)' : '') + '</span>';
802
		html += '<span>' + fmtMins(day.totalMins) + '</span>';
803
		html += '</div>';
804
 
805
		// Start point
36632 ranu 806
        html += '<div class="route-stop route-stop-home"><span class="stop-num">H</span><div class="stop-info"><div class="stop-name">' + (day.startName || 'Home') + '</div></div></div>';
36621 ranu 807
 
808
		// Visits with distance between each
809
		var prevLat = day.startLat, prevLng = day.startLng;
36632 ranu 810
        var isLastStop;
36621 ranu 811
		day.visits.forEach(function (v, i) {
36632 ranu 812
            isLastStop = (i === day.visits.length - 1);
813
 
36621 ranu 814
			var segDist = 0, segTime = 0;
815
			if (prevLat && prevLng && v.lat && v.lng) {
816
				segDist = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
817
				segTime = (segDist / AVG_SPEED) * 60;
818
			}
819
			if (segDist > 0) {
820
				html += '<div class="route-segment"><span class="seg-line"></span><span class="seg-dist">' + segDist.toFixed(1) + ' km | ' + fmtMins(segTime) + '</span></div>';
821
			}
822
 
36642 ranu 823
            var isLeadVisit = v.isLead || v.type === 'lead';
824
            html += '<div class="route-stop route-stop-clickable" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" style="cursor:pointer;' + (isLeadVisit ? 'background:#fff3cd;border-left:3px solid #e67e22;' : '') + '">';
825
            html += '<span class="stop-num" style="' + (isLeadVisit ? 'background:#e67e22;' : '') + '">' + (i + 1) + '</span>';
826
            html += '<div class="stop-info"><div class="stop-name">' + (v.code || '') + (isLeadVisit ? ' <span style="color:#e67e22;font-size:10px;">LEAD VISIT</span>' : '') + '</div>';
36632 ranu 827
            html += '<div class="stop-meta">' + (v.name || '') + '</div>';
828
 
829
            // Show "Last Visit" + "Day Break" buttons on last stop of current day
830
            if (isLastStop && isCurrent && !day.endAction) {
831
                html += '<div style="margin-top:4px;display:flex;gap:4px;">';
832
                html += '<button class="btn-last-visit" data-daynum="' + day.dayNumber + '" style="font-size:10px;padding:2px 8px;border:none;border-radius:3px;background:#22c55e;color:#fff;cursor:pointer;font-weight:600;">Last Visit</button>';
833
                html += '<button class="btn-day-break" data-daynum="' + day.dayNumber + '" style="font-size:10px;padding:2px 8px;border:none;border-radius:3px;background:#f59e0b;color:#0f172a;cursor:pointer;font-weight:600;">Day Break</button>';
834
                html += '</div>';
835
            }
836
 
837
            html += '</div>';
838
            if (state.mode === 'create') {
839
                html += '<span class="stop-remove" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" title="Remove">&times;</span>';
840
            }
36621 ranu 841
			html += '</div>';
842
 
843
			prevLat = v.lat;
844
			prevLng = v.lng;
845
		});
846
 
36632 ranu 847
        // Show return-to-home only when day is complete (Last Visit) or still in progress
848
        if (day.visits.length > 0 && day.endAction !== 'DAYBREAK') {
36621 ranu 849
			var lastV = day.visits[day.visits.length - 1];
850
			var retDist = 0, retTime = 0;
851
			if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
852
				retDist = haversine(lastV.lat, lastV.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
853
				retTime = (retDist / AVG_SPEED) * 60;
854
			}
855
			if (retDist > 0) {
36632 ranu 856
                html += '<div class="route-segment"><span class="seg-line seg-line-home"></span><span class="seg-dist">' + retDist.toFixed(1) + ' km | ' + fmtMins(retTime) + ' (return)</span></div>';
36621 ranu 857
			}
858
			html += '<div class="route-stop route-stop-home"><span class="stop-num">H</span><div class="stop-info"><div class="stop-name">Home</div></div></div>';
859
		}
860
 
36632 ranu 861
        // Day break indicator
862
        if (day.endAction === 'DAYBREAK' && day.visits.length > 0) {
863
            html += '<div style="margin-left:12px;padding:4px 8px;font-size:11px;color:#f59e0b;font-weight:600;">-- Day Break (stays near last visit) --</div>';
864
        }
865
 
36621 ranu 866
		html += '<div class="day-summary">' + day.visits.length + ' stops | ' + day.totalKm.toFixed(1) + ' km</div>';
867
		html += '</div>';
868
	});
869
 
870
	$('#route-list').html(html);
871
}
872
 
873
// Click on stop name in route panel → show popup on map
874
$(document).on('click', '.route-stop-clickable', function (e) {
875
	if ($(e.target).hasClass('stop-remove')) return; // let remove handler handle it
876
	var fofoId = parseInt($(this).data('fofoid'));
877
	if (!fofoId || !markers[fofoId]) return;
878
 
879
	var marker = markers[fofoId];
880
	var p = state.partners.find(function (x) {
881
		return x.fofoId === fofoId;
882
	});
883
	if (!p) return;
884
 
885
	map.panTo(marker.getPosition());
886
	map.setZoom(12);
887
 
888
	var name = p.outletName || p.businessName || '';
889
	var addr = p.address || '';
890
	infoWindow.setContent('<div class="marker-popup"><h4>' + p.code + ' - ' + name + '</h4><div class="popup-meta">' + addr + '</div></div>');
891
	infoWindow.open(map, marker);
892
});
893
 
894
// Remove stop from route panel
895
$(document).on('click', '.stop-remove', function (e) {
896
	e.stopPropagation();
897
	var fofoId = parseInt($(this).data('fofoid'));
898
	var dayNum = parseInt($(this).data('daynum'));
899
 
900
	var day = state.days[dayNum - 1];
901
	if (!day) return;
902
 
903
	// Remove visit from day
904
	day.visits = day.visits.filter(function (v) {
905
		return v.id !== fofoId;
906
	});
907
 
908
	// Reset marker back to red (unvisited)
909
	if (markers[fofoId]) {
910
		var p = state.partners.find(function (x) {
911
			return x.fofoId === fofoId;
912
		});
913
		markers[fofoId].setIcon({
914
			path: google.maps.SymbolPath.CIRCLE, scale: 14,
915
			fillColor: '#ef4444', fillOpacity: 0.9, strokeColor: '#fff', strokeWeight: 1
916
		});
917
		markers[fofoId].setLabel({text: (p ? p.code : '') || '', color: '#fff', fontSize: '10px', fontWeight: '600'});
918
	}
919
 
920
	// Re-number remaining markers for this day
921
	day.visits.forEach(function (v, i) {
922
		if (markers[v.id]) {
923
			markers[v.id].setLabel({text: '' + (i + 1), color: '#fff', fontSize: '11px', fontWeight: '700'});
924
		}
925
	});
926
 
927
	recalcDay();
928
	drawRoute();
929
	renderRoutePanel();
930
	updateBottomBar();
931
});
932
 
36632 ranu 933
// ============ LAST VISIT (from sidebar) — marks beat as complete, return home ============
934
$(document).on('click', '.btn-last-visit', function (e) {
935
    e.stopPropagation();
936
    var dayNum = parseInt($(this).data('daynum'));
937
    var day = state.days[dayNum - 1];
938
    if (!day || day.visits.length === 0) return;
36621 ranu 939
 
36632 ranu 940
    var lastVisit = day.visits[day.visits.length - 1];
941
    if (!confirm('Mark ' + (lastVisit.code || lastVisit.name) + ' as last visit? Beat will end here and return home.')) return;
36621 ranu 942
 
36632 ranu 943
    day.endAction = 'HOME';
944
    drawRoute();
945
    renderRoutePanel();
946
    updateBottomBar();
36621 ranu 947
});
948
 
36632 ranu 949
// ============ DAY BREAK (from sidebar) — end day, next day starts from last visit ============
950
$(document).on('click', '.btn-day-break', function (e) {
951
    e.stopPropagation();
952
    var dayNum = parseInt($(this).data('daynum'));
953
    var day = state.days[dayNum - 1];
954
    if (!day || day.visits.length === 0) return;
36621 ranu 955
 
36632 ranu 956
    var lastVisit = day.visits[day.visits.length - 1];
957
    if (!confirm('Day Break at ' + (lastVisit.code || lastVisit.name) + '? Next day will start from this location.')) return;
958
 
959
    day.endAction = 'DAYBREAK';
960
 
961
    // Next day starts from LAST VISIT location (not home)
962
    var nextNum = state.days.length + 1;
963
    state.days.push({
964
        dayNumber: nextNum,
965
        startLat: lastVisit.lat, startLng: lastVisit.lng, startName: lastVisit.code || lastVisit.name,
966
        visits: [], endAction: null,
967
        stayName: null, stayLat: null, stayLng: null,
968
        totalKm: 0, totalMins: 0
969
    });
970
    state.currentDay = nextNum;
971
 
972
    resetMarkerColors();
973
    drawRoute();
36621 ranu 974
	renderRoutePanel();
36632 ranu 975
    updateBottomBar();
36621 ranu 976
});
977
 
978
$('#btn-finish').on('click', function () {
979
	if (state.days.every(function (d) {
980
		return d.visits.length === 0;
981
	})) {
982
		alert('No visits added');
983
		return;
984
	}
985
 
36632 ranu 986
    var beatTitle = state.beatTitle || 'Beat';
36621 ranu 987
 
988
	// Build plan data and save to DB first
989
	var daysData = [];
990
	state.days.forEach(function (day) {
991
		if (day.visits.length === 0) return;
992
		daysData.push({
993
			dayNumber: day.dayNumber,
994
			startLocationName: day.startName,
995
			startLatitude: day.startLat ? day.startLat.toString() : null,
996
			startLongitude: day.startLng ? day.startLng.toString() : null,
997
			endAction: day.endAction,
998
			stayLocationName: day.stayName,
999
			stayLatitude: day.stayLat ? day.stayLat.toString() : null,
1000
			stayLongitude: day.stayLng ? day.stayLng.toString() : null,
1001
			totalDistanceKm: day.totalKm,
1002
			totalTimeMins: Math.round(day.totalMins),
1003
			visits: day.visits.map(function (v) {
1004
				return {id: v.id, type: v.type || 'partner'};
1005
			})
1006
		});
1007
	});
1008
 
1009
	// Dates will be assigned later via calendar — pass nulls for now
1010
	var dates = daysData.map(function () {
1011
		return null;
1012
	});
1013
	var planData = JSON.stringify({days: daysData, dates: dates, beatName: beatTitle.trim()});
1014
 
1015
	$('#btn-finish').prop('disabled', true).text('Saving...');
1016
 
1017
	$.ajax({
1018
		url: context + "/beatPlan/submitPlan",
1019
		type: "POST",
1020
		data: {authUserId: state.authUserId, planData: planData},
1021
		success: function (response) {
1022
			var data = (typeof response === 'string' ? JSON.parse(response) : response);
1023
			var result = data.response || data;
1024
			$('#btn-finish').prop('disabled', false).text('Finish Plan');
1025
 
1026
			if (result.status) {
1027
				state.savedPlanGroupId = result.planGroupId;
1028
				$('#bottom-bar').hide();
1029
 
36632 ranu 1030
                if (result.duplicate) {
1031
                    alert('This beat already exists (same visits).');
1032
                } else {
1033
                    alert('Beat "' + beatTitle + '" saved successfully!');
1034
                }
1035
 
1036
                // Go back to beat list
1037
                showBeatList();
36621 ranu 1038
			} else {
1039
				alert('Error saving beat plan');
1040
			}
1041
		},
1042
		error: function (xhr) {
1043
			$('#btn-finish').prop('disabled', false).text('Finish Plan');
1044
			alert('Error: ' + (xhr.responseText || xhr.statusText));
1045
		}
1046
	});
1047
});
1048
 
1049
// ============ PANEL TABS ============
1050
$('.panel-tab').on('click', function () {
1051
	var tab = $(this).data('tab');
1052
	$('.panel-tab').removeClass('active');
1053
	$(this).addClass('active');
1054
	$('#panel-route').toggle(tab === 'route');
1055
	$('#panel-calendar').toggle(tab === 'calendar');
1056
	$('#cal-grid-container').toggle(tab === 'calendar');
36632 ranu 1057
    $('#bottom-bar').toggle(tab === 'route' && state.mode === 'create' && state.days && state.days.length > 0);
36621 ranu 1058
 
1059
	if (tab === 'calendar' && !state.calMonth) {
1060
		var now = new Date();
1061
		state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
1062
		loadCalendarData();
1063
	}
1064
});
1065
 
1066
// ============ CALENDAR ============
1067
$('#cal-prev').on('click', function () {
1068
	navMonth(-1);
1069
});
1070
$('#cal-next').on('click', function () {
1071
	navMonth(1);
1072
});
1073
 
1074
function navMonth(delta) {
1075
	var p = state.calMonth.split('-');
1076
	var d = new Date(parseInt(p[0]), parseInt(p[1]) - 1 + delta, 1);
1077
	state.calMonth = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2);
1078
	loadCalendarData();
1079
}
1080
 
1081
function loadCalendarData() {
1082
	if (!state.authUserId) return;
1083
	$('#cal-month-label').text(state.calMonth);
1084
 
1085
	$.ajax({
1086
		url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
1087
		data: {authUserId: state.authUserId, month: state.calMonth},
1088
		success: function (r) {
1089
			var data = r.response || r;
1090
			state.calData = data;
1091
			renderCalGrid(data);
1092
			renderBeatCards(data.scheduledBeats);
1093
		}
1094
	});
1095
}
1096
 
1097
function renderCalGrid(data) {
1098
	var p = state.calMonth.split('-');
1099
	var year = parseInt(p[0]), month = parseInt(p[1]) - 1;
1100
	var first = new Date(year, month, 1);
1101
	var last = new Date(year, month + 1, 0);
1102
	var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
1103
	$('#cal-month-label').text(months[month] + ' ' + year);
1104
 
1105
	var holidayMap = {};
1106
	(data.holidays || []).forEach(function (h) {
1107
		holidayMap[h.date] = h.occasion;
1108
	});
1109
	var blockedSet = {};
1110
	(data.blockedDates || []).forEach(function (d) {
1111
		blockedSet[d] = true;
1112
	});
1113
 
1114
	var beatDateMap = {};
1115
	(data.scheduledBeats || []).forEach(function (b) {
1116
		(b.days || []).forEach(function (d) {
1117
			if (d.planDate) {
1118
				if (!beatDateMap[d.planDate]) beatDateMap[d.planDate] = [];
1119
				beatDateMap[d.planDate].push({
1120
					name: b.beatName,
1121
					color: b.beatColor,
1122
					day: d.dayNumber,
1123
					status: b.status,
36632 ranu 1124
                    visits: d.visitCount,
1125
                    planGroupId: b.planGroupId
36621 ranu 1126
				});
1127
			}
1128
		});
1129
	});
1130
 
1131
	var today = fmtDate(new Date());
1132
	var startOff = (first.getDay() + 6) % 7;
1133
	var calStart = new Date(year, month, 1 - startOff);
1134
 
1135
	var html = '<table class="cal-grid"><tr><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th><th>Sun</th></tr>';
1136
	var d = new Date(calStart);
1137
	for (var w = 0; w < 6; w++) {
1138
		var hasDay = false;
1139
		var row = '<tr>';
1140
		for (var wd = 0; wd < 7; wd++) {
1141
			var ds = fmtDate(d);
1142
			var inMonth = d.getMonth() === month;
1143
			var sun = d.getDay() === 0;
1144
			var hol = !!holidayMap[ds];
1145
			var isToday = ds === today;
1146
			var beats = beatDateMap[ds] || [];
1147
			var isPicked = state.calPickedDates.indexOf(ds) > -1;
1148
 
36632 ranu 1149
            var isPast = ds < today;
1150
            var hasBeats = beats.length > 0;
1151
 
36621 ranu 1152
			var cls = [];
36632 ranu 1153
            if (hol && !sun) cls.push('holiday', 'blocked');
1154
            else if (isPast) cls.push('blocked', 'past');
1155
            if (sun && !isPast) cls.push('sunday'); // Sundays styled but not blocked
1156
            if (sun && isPast) cls.push('blocked', 'past');
36621 ranu 1157
			if (isToday) cls.push('today');
1158
			if (!inMonth) cls.push('blocked');
1159
 
1160
			var style = inMonth ? '' : 'opacity:0.3;';
36632 ranu 1161
            if (isPast && inMonth) style += 'opacity:0.5;';
36621 ranu 1162
 
36632 ranu 1163
            row += '<td class="' + cls.join(' ') + '" data-date="' + ds + '" data-hasbeat="' + (hasBeats ? '1' : '0') + '" style="' + style + '">';
36621 ranu 1164
			row += '<div class="cal-date">' + d.getDate() + '</div>';
1165
			if (hol) row += '<div class="cal-holiday-label">' + holidayMap[ds] + '</div>';
1166
 
1167
			beats.forEach(function (b) {
1168
				var chipCls = 'cal-chip';
1169
				if (b.status === 'running') chipCls += ' running';
1170
				if (b.status === 'completed') chipCls += ' completed';
36632 ranu 1171
                var removeBtn = (!isPast && b.status !== 'running' && b.status !== 'completed')
1172
                    ? '<span class="cal-chip-remove" data-date="' + ds + '" data-plangroup="' + (b.planGroupId || '') + '" title="Remove">&times;</span>'
1173
                    : '';
1174
                row += '<span class="' + chipCls + '" style="background:' + b.color + ';position:relative;">'
1175
                    + b.name + ' D' + b.day + (b.status === 'running' ? ' [live]' : '') + ' (' + b.visits + ')'
1176
                    + removeBtn + '</span>';
36621 ranu 1177
			});
1178
 
1179
			if (isPicked) {
1180
				var idx = state.calPickedDates.indexOf(ds) + 1;
1181
				row += '<span class="cal-chip" style="background:#3b82f6;border:1px dashed #93c5fd;">Day ' + idx + '</span>';
1182
			}
1183
 
1184
			row += '</td>';
1185
			if (inMonth) hasDay = true;
1186
			d.setDate(d.getDate() + 1);
1187
		}
1188
		row += '</tr>';
1189
		if (hasDay) html += row;
1190
	}
1191
	html += '</table>';
1192
	$('#cal-grid-content').html(html);
1193
}
1194
 
1195
function renderBeatCards(beats) {
1196
	var html = '';
1197
	if (!beats || beats.length === 0) {
1198
		html = '<p style="color:#64748b;font-size:12px;padding:20px;">No beats scheduled. Create a beat first.</p>';
36632 ranu 1199
        $('#cal-beat-cards').html(html);
1200
        return;
36621 ranu 1201
	}
36632 ranu 1202
 
1203
    // Deduplicate by beatName — group beats with same name, show count
1204
    var beatGroups = {};
36621 ranu 1205
	(beats || []).forEach(function (b) {
36632 ranu 1206
        var key = b.beatName || b.planGroupId;
1207
        if (!beatGroups[key]) {
1208
            beatGroups[key] = {
1209
                beatName: b.beatName,
1210
                beatColor: b.beatColor,
1211
                instances: []
1212
            };
1213
        }
1214
        beatGroups[key].instances.push(b);
1215
    });
36621 ranu 1216
 
36632 ranu 1217
    Object.keys(beatGroups).forEach(function (key) {
1218
        var group = beatGroups[key];
1219
        var first = group.instances[0];
1220
        var scheduledCount = group.instances.filter(function (b) {
1221
            return b.status === 'scheduled';
1222
        }).length;
1223
        var unscheduledCount = group.instances.filter(function (b) {
1224
            return b.status === 'unscheduled';
1225
        }).length;
1226
        var totalInstances = group.instances.length;
1227
 
36621 ranu 1228
		var totalV = 0, totalKm = 0;
36632 ranu 1229
        first.days.forEach(function (d) {
1230
            totalV += d.visitCount || 0;
1231
            totalKm += d.totalKm || 0;
1232
        });
36621 ranu 1233
 
36632 ranu 1234
        // Use first unscheduled instance for scheduling, or first overall
1235
        var primaryPg = first.planGroupId;
1236
        var unscheduledInstance = group.instances.find(function (b) {
1237
            return b.status === 'unscheduled';
1238
        });
1239
        if (unscheduledInstance) primaryPg = unscheduledInstance.planGroupId;
1240
 
1241
        var statusText = '';
1242
        if (scheduledCount > 0) statusText += scheduledCount + ' scheduled';
1243
        if (unscheduledCount > 0) statusText += (statusText ? ', ' : '') + unscheduledCount + ' unscheduled';
1244
 
1245
        var isScheduling = state.calSelectedBeat === primaryPg && state.calPickedDates.length > 0;
1246
 
1247
        html += '<div class="beat-card" data-plangroup="' + primaryPg + '" data-beatname="' + (group.beatName || '') + '" draggable="true">';
1248
        html += '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + group.beatColor + ';"></span> ' + group.beatName + '</div>';
1249
        html += '<div class="beat-meta">' + first.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km</div>';
1250
        html += '<div class="beat-meta" style="color:#94a3b8;">' + statusText + ' (' + totalInstances + ' total)</div>';
1251
 
1252
        html += '<div class="beat-actions">';
1253
        if (isScheduling) {
1254
            html += '<button class="btn-sm btn-confirm-schedule" data-pg="' + primaryPg + '" data-days="' + first.days.length + '" style="background:#22c55e;color:#fff;">Confirm (' + state.calPickedDates.length + ' dates)</button>';
1255
            html += '<button class="btn-sm btn-cancel-schedule" style="background:#475569;color:#e2e8f0;">Cancel</button>';
1256
        } else {
1257
            html += '<button class="btn-sm btn-schedule" data-pg="' + primaryPg + '" data-days="' + first.days.length + '">Schedule</button>';
1258
        }
1259
        html += '<button class="btn-sm" style="background:#ef4444;color:#fff;" onclick="deleteBeat(\'' + primaryPg + '\')">Delete</button>';
1260
        html += '</div>';
1261
 
1262
        if (isScheduling) {
1263
            html += '<div style="font-size:10px;color:#60a5fa;margin-top:4px;">Dates: ' + state.calPickedDates.join(', ') + '</div>';
1264
            html += '<div style="font-size:10px;color:#94a3b8;">Click calendar cells to adjust</div>';
1265
        }
1266
 
1267
        html += '<div style="font-size:9px;color:#475569;margin-top:4px;">Drag onto calendar to schedule</div>';
36621 ranu 1268
		html += '</div>';
1269
	});
36632 ranu 1270
 
36621 ranu 1271
	$('#cal-beat-cards').html(html);
1272
}
1273
 
1274
// Schedule button — suggest dates, show Confirm button in beat card
1275
$(document).on('click', '.btn-schedule', function (e) {
1276
	e.stopPropagation();
1277
	var pg = $(this).data('pg');
1278
	var days = parseInt($(this).data('days'));
1279
	state.calSelectedBeat = pg;
1280
	state.calPickedDates = [];
1281
 
1282
	$.ajax({
1283
		url: context + "/beatPlan/availableSlots", type: "GET", dataType: "json",
1284
		data: {authUserId: state.authUserId, month: state.calMonth, daysNeeded: days},
1285
		success: function (r) {
1286
			var data = r.response || r;
1287
			state.calPickedDates = data.suggestedDates || [];
1288
			renderCalGrid(state.calData);
1289
			renderBeatCards(state.calData.scheduledBeats);
1290
			if (state.calPickedDates.length < days) {
1291
				alert('Only ' + state.calPickedDates.length + ' slots available this month. Need ' + days + '.');
1292
			}
1293
		}
1294
	});
1295
});
1296
 
36632 ranu 1297
// Click calendar cell during Schedule flow — auto-fill consecutive days from clicked date
1298
$(document).on('click', '#cal-grid-content td', function () {
36621 ranu 1299
	if (!state.calSelectedBeat) return;
1300
	var ds = $(this).data('date');
36632 ranu 1301
    if (!ds || isDateBlocked(ds)) return;
1302
 
1303
    // Find the beat to get days needed
1304
    var beat = (state.calData.scheduledBeats || []).find(function (b) {
1305
        return b.planGroupId === state.calSelectedBeat;
1306
    });
1307
    var daysNeeded = beat ? beat.days.length : 1;
1308
 
1309
    // Auto-fill consecutive slots from clicked date
1310
    var slots = findConsecutiveSlots(ds, daysNeeded);
1311
    if (slots) {
1312
        state.calPickedDates = slots;
1313
        renderCalGrid(state.calData);
1314
        renderBeatCards(state.calData.scheduledBeats);
1315
    }
36621 ranu 1316
});
1317
 
1318
// Cancel scheduling mode
1319
$(document).on('click', '.btn-cancel-schedule', function (e) {
1320
	e.stopPropagation();
1321
	state.calSelectedBeat = null;
1322
	state.calPickedDates = [];
1323
	renderCalGrid(state.calData);
1324
	renderBeatCards(state.calData.scheduledBeats);
1325
});
1326
 
1327
// Confirm schedule — save to server
1328
$(document).on('click', '.btn-confirm-schedule', function (e) {
1329
	e.stopPropagation();
1330
	var pg = $(this).data('pg');
1331
	var daysNeeded = parseInt($(this).data('days'));
1332
 
1333
	if (state.calPickedDates.length < daysNeeded) {
1334
		alert('Need ' + daysNeeded + ' dates but only ' + state.calPickedDates.length + ' selected.');
1335
		return;
1336
	}
1337
 
1338
	var beat = (state.calData.scheduledBeats || []).find(function (b) {
1339
		return b.planGroupId === pg;
1340
	});
1341
	if (!beat) return;
1342
 
1343
	$.ajax({
1344
		url: context + "/beatPlan/scheduleOnCalendar", type: "POST",
1345
		data: {
1346
			planGroupId: pg,
1347
			dates: JSON.stringify(state.calPickedDates.slice(0, daysNeeded)),
1348
			beatName: beat.beatName,
1349
			beatColor: beat.beatColor
1350
		},
1351
		success: function () {
1352
			state.calSelectedBeat = null;
1353
			state.calPickedDates = [];
1354
			loadCalendarData();
1355
		},
1356
		error: function (xhr) {
1357
			alert('Error: ' + (xhr.responseText || xhr.statusText));
1358
		}
1359
	});
1360
});
1361
 
36632 ranu 1362
// ============ DRAG & DROP BEAT TO CALENDAR ============
1363
$(document).on('dragstart', '.beat-card', function (e) {
1364
    var pg = $(this).data('plangroup');
1365
    var beatName = $(this).data('beatname');
1366
    e.originalEvent.dataTransfer.setData('text/plain', pg);
1367
    e.originalEvent.dataTransfer.setData('beatname', beatName || '');
1368
    $(this).css('opacity', '0.5');
1369
});
36621 ranu 1370
 
36632 ranu 1371
$(document).on('dragend', '.beat-card', function () {
1372
    $(this).css('opacity', '1');
1373
});
36621 ranu 1374
 
36632 ranu 1375
// Calendar cells accept drops
1376
$(document).on('dragover', '#cal-grid-content td', function (e) {
1377
    var ds = $(this).data('date');
1378
    if (!ds) return;
1379
    if (isDateBlocked(ds)) return; // don't allow drop indicator on blocked dates
1380
    e.preventDefault();
1381
    $(this).css('background', '#1e3a5f');
1382
});
1383
 
1384
$(document).on('dragleave', '#cal-grid-content td', function () {
1385
    $(this).css('background', '');
1386
});
1387
 
1388
$(document).on('drop', '#cal-grid-content td', function (e) {
1389
    e.preventDefault();
1390
    $(this).css('background', '');
1391
 
1392
    var pg = e.originalEvent.dataTransfer.getData('text/plain');
1393
    var dropDate = $(this).data('date');
1394
    if (!pg || !dropDate) return;
1395
 
1396
    if (isDateBlocked(dropDate)) {
1397
        alert('Cannot schedule on this date (past, Sunday, or holiday)');
1398
        return;
1399
    }
1400
 
1401
    var beat = (state.calData.scheduledBeats || []).find(function (b) {
1402
        return b.planGroupId === pg;
1403
    });
1404
    if (!beat) return;
1405
    var beatName = beat.beatName || 'Beat';
1406
    var daysNeeded = beat.days ? beat.days.length : 1;
1407
 
1408
    // Auto-fill consecutive available days starting from drop date
1409
    var scheduleDates = findConsecutiveSlots(dropDate, daysNeeded);
1410
 
1411
    if (!scheduleDates) return; // alerts already shown inside findConsecutiveSlots
1412
 
1413
    var dateStr = scheduleDates.map(function (d, i) {
1414
        return 'Day ' + (i + 1) + ': ' + d;
1415
    }).join('\n');
1416
    if (!confirm('Schedule "' + beatName + '" (' + daysNeeded + ' days):\n\n' + dateStr + '\n\nConfirm?')) return;
1417
 
1418
    $.ajax({
1419
        url: context + "/beatPlan/repeatBeat", type: "POST",
1420
        data: {
1421
            sourcePlanGroupId: pg,
1422
            authUserId: state.authUserId,
1423
            dates: JSON.stringify(scheduleDates)
1424
        },
1425
        success: function () {
1426
            loadCalendarData();
1427
        },
1428
        error: function (xhr) {
1429
            alert('Error: ' + (xhr.responseText || xhr.statusText));
1430
        }
1431
    });
1432
});
1433
 
1434
// Find N consecutive available days starting from startDate, skipping Sundays/holidays
1435
// Returns array of date strings, or null if can't fit
1436
function findConsecutiveSlots(startDateStr, daysNeeded) {
1437
    var startDate = new Date(startDateStr);
1438
    var startMonth = startDate.getMonth();
1439
    var dates = [];
1440
    var d = new Date(startDate);
1441
    var sundayAsked = false;
1442
    var includeSundays = false;
1443
 
1444
    while (dates.length < daysNeeded) {
1445
        var ds = fmtDate(d);
1446
 
1447
        // Rule 3: must stay within same month
1448
        if (d.getMonth() !== startMonth) {
1449
            alert('Beat has ' + daysNeeded + ' days but only ' + dates.length + ' available days left in this month starting from ' + startDateStr + '. Try an earlier date.');
1450
            return null;
36621 ranu 1451
		}
36632 ranu 1452
 
1453
        // Past dates — skip
1454
        var today = fmtDate(new Date());
1455
        if (ds <= today) {
1456
            d.setDate(d.getDate() + 1);
1457
            continue;
1458
        }
1459
 
1460
        // Holidays — always skip
1461
        if (isHoliday(ds)) {
1462
            d.setDate(d.getDate() + 1);
1463
            continue;
1464
        }
1465
 
1466
        // Sunday — ask once whether to include
1467
        if (isSunday(ds)) {
1468
            if (!sundayAsked) {
1469
                sundayAsked = true;
1470
                includeSundays = confirm('This beat falls on a Sunday (' + ds + '). Include Sundays in the schedule?');
1471
            }
1472
            if (!includeSundays) {
1473
                d.setDate(d.getDate() + 1);
1474
                continue;
1475
            }
1476
        }
1477
 
1478
        // Already occupied — block
1479
        if (getBeatsOnDate(ds).length > 0) {
1480
            alert('Cannot schedule: ' + ds + ' already has a beat scheduled. Consecutive days not available from ' + startDateStr + '.');
1481
            return null;
1482
        }
1483
 
1484
        dates.push(ds);
1485
        d.setDate(d.getDate() + 1);
1486
    }
1487
 
1488
    return dates;
1489
}
1490
 
1491
function isDateBlocked(ds) {
1492
    var today = fmtDate(new Date());
1493
    if (ds <= today) return true; // past or today
1494
    // Sundays NOT blocked — handled via confirm dialog instead
1495
    // Only block holidays
1496
    if (state.calData && state.calData.blockedDates) {
1497
        var blocked = state.calData.blockedDates;
1498
        // blockedDates from server includes Sundays+holidays, but we only block holidays now
1499
        var d = new Date(ds);
1500
        if (d.getDay() !== 0) { // not Sunday — check holidays
1501
            if (Array.isArray(blocked) && blocked.indexOf(ds) > -1) return true;
1502
            if (typeof blocked === 'object' && blocked[ds]) return true;
1503
        }
1504
    }
1505
    return false;
1506
}
1507
 
1508
function isSunday(ds) {
1509
    return new Date(ds).getDay() === 0;
1510
}
1511
 
1512
function isHoliday(ds) {
1513
    if (!state.calData || !state.calData.holidays) return false;
1514
    return state.calData.holidays.some(function (h) {
1515
        return h.date === ds;
1516
    });
1517
}
1518
 
1519
function getBeatsOnDate(ds) {
1520
    var result = [];
1521
    if (!state.calData || !state.calData.scheduledBeats) return result;
1522
    state.calData.scheduledBeats.forEach(function (b) {
1523
        (b.days || []).forEach(function (d) {
1524
            if (d.planDate === ds) result.push({planGroupId: b.planGroupId, beatName: b.beatName});
1525
        });
1526
    });
1527
    return result;
1528
}
1529
 
1530
// ============ REMOVE BEAT FROM CALENDAR DATE ============
1531
$(document).on('click', '.cal-chip-remove', function (e) {
1532
    e.stopPropagation();
1533
    var date = $(this).data('date');
1534
    var planGroupId = $(this).data('plangroup');
1535
    if (!planGroupId || !date) return;
1536
 
1537
    if (!confirm('Remove this beat from ' + date + '?')) return;
1538
 
1539
    // Delete the entire beat instance (planGroupId)
1540
    $.ajax({
1541
        url: context + "/beatPlan/delete", type: "POST",
1542
        data: {planGroupId: planGroupId},
1543
        success: function () {
1544
            loadCalendarData();
1545
        },
1546
        error: function (xhr) {
1547
            alert('Error: ' + (xhr.responseText || xhr.statusText));
1548
        }
36621 ranu 1549
	});
1550
});
1551
 
1552
// ============ DELETE BEAT ============
1553
function deleteBeat(planGroupId) {
1554
	if (!confirm('Delete this beat? This cannot be undone.')) return;
1555
	$.ajax({
1556
		url: context + "/beatPlan/delete", type: "POST",
1557
		data: {planGroupId: planGroupId},
1558
		success: function () {
1559
			loadCalendarData();
1560
		},
1561
		error: function (xhr) {
1562
			alert('Error: ' + (xhr.responseText || xhr.statusText));
1563
		}
1564
	});
1565
}
1566
 
1567
// ============ HELPERS ============
1568
function curDay() {
1569
	return state.days[state.currentDay - 1];
1570
}
1571
 
1572
function haversine(lat1, lng1, lat2, lng2) {
1573
	var R = 6371, dLat = (lat2 - lat1) * Math.PI / 180, dLng = (lng2 - lng1) * Math.PI / 180;
1574
	var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
1575
	return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1576
}
1577
 
1578
function fmtMins(m) {
1579
	return Math.floor(m / 60) + 'h ' + Math.round(m % 60) + 'm';
1580
}
1581
 
1582
function fmtDate(d) {
1583
	return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
1584
}
1585
 
1586
function fitBounds() {
1587
	if (Object.keys(markers).length > 0) {
1588
		var b = new google.maps.LatLngBounds();
1589
		if (homeMarker) b.extend(homeMarker.getPosition());
1590
		for (var k in markers) b.extend(markers[k].getPosition());
1591
		map.fitBounds(b);
1592
	}
1593
}