Subversion Repositories SmartDukaan

Rev

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