Subversion Repositories SmartDukaan

Rev

Rev 36632 | Go to most recent revision | Details | 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());
57
	$('#bp-user-label').text($('#bp-auth-user option:selected').text());
58
 
59
	// Check home location
60
	$.ajax({
61
		url: context + "/beatPlan/getBaseLocation", type: "GET", dataType: "json",
62
		data: {authUserId: uid},
63
		success: function (r) {
64
			var data = r.response || r;
65
			if (data.locationName) {
66
				state.homeLat = parseFloat(data.latitude);
67
				state.homeLng = parseFloat(data.longitude);
68
				state.homeName = data.locationName;
69
				loadPartners();
70
			} else {
71
				showHomeModal();
72
			}
73
		}
74
	});
75
});
76
 
77
// ============ HOME LOCATION MODAL ============
78
var homeMap, homeMapMarker;
79
 
80
function showHomeModal() {
81
	$('#modal-home').addClass('show');
82
	setTimeout(function () {
83
		if (!homeMap) {
84
			homeMap = new google.maps.Map(document.getElementById('home-map'), {
85
				center: {lat: 28.6, lng: 77.2},
86
				zoom: 6
87
			});
88
			var ac = new google.maps.places.Autocomplete(document.getElementById('home-search'), {componentRestrictions: {country: 'in'}});
89
			homeMapMarker = new google.maps.Marker({map: homeMap, draggable: true});
90
 
91
			ac.addListener('place_changed', function () {
92
				var p = ac.getPlace();
93
				if (!p.geometry) return;
94
				homeMap.setCenter(p.geometry.location);
95
				homeMap.setZoom(14);
96
				homeMapMarker.setPosition(p.geometry.location);
97
				state.homeLat = p.geometry.location.lat();
98
				state.homeLng = p.geometry.location.lng();
99
				state.homeName = p.name || p.formatted_address || '';
100
			});
101
			homeMapMarker.addListener('dragend', function () {
102
				state.homeLat = homeMapMarker.getPosition().lat();
103
				state.homeLng = homeMapMarker.getPosition().lng();
104
			});
105
			homeMap.addListener('click', function (e) {
106
				homeMapMarker.setPosition(e.latLng);
107
				state.homeLat = e.latLng.lat();
108
				state.homeLng = e.latLng.lng();
109
			});
110
		}
111
	}, 200);
112
}
113
 
114
$('#home-cancel').on('click', function () {
115
	$('#modal-home').removeClass('show');
116
});
117
$('#home-confirm').on('click', function () {
118
	if (!state.homeLat) {
119
		alert('Please select a location');
120
		return;
121
	}
122
	$('#modal-home').removeClass('show');
123
	$.ajax({
124
		url: context + "/beatPlan/saveBaseLocation", type: "POST",
125
		data: {
126
			authUserId: state.authUserId,
127
			locationName: state.homeName || 'Home',
128
			latitude: state.homeLat,
129
			longitude: state.homeLng,
130
			address: $('#home-search').val()
131
		}
132
	});
133
	loadPartners();
134
});
135
 
136
// ============ LOAD PARTNERS + INIT MAP ============
137
function loadPartners() {
138
	$.ajax({
139
		url: context + "/beatPlan/getPartners", type: "GET", dataType: "json",
140
		data: {
141
			authUserId: state.authUserId,
142
			categoryId: state.categoryId,
143
			startLat: state.homeLat,
144
			startLng: state.homeLng
145
		},
146
		success: function (r) {
147
			var data = r.response || r;
148
			state.partners = data.partners || [];
149
			initDay1();
150
			initMap();
151
		}
152
	});
153
}
154
 
155
function initDay1() {
156
	state.days = [{
157
		dayNumber: 1, startLat: state.homeLat, startLng: state.homeLng, startName: state.homeName,
158
		visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
159
	}];
160
	state.currentDay = 1;
161
	$('#bottom-bar').show();
162
	renderRoutePanel();
163
}
164
 
165
function initMap() {
166
	var center = {lat: state.homeLat, lng: state.homeLng};
167
	map = new google.maps.Map(document.getElementById('beat-map'), {
168
		center: center, zoom: 9,
169
		styles: [
170
			{elementType: 'geometry', stylers: [{color: '#1d2c4d'}]},
171
			{elementType: 'labels.text.fill', stylers: [{color: '#8ec3b9'}]},
172
			{elementType: 'labels.text.stroke', stylers: [{color: '#1a3646'}]},
173
			{featureType: 'road', elementType: 'geometry', stylers: [{color: '#304a7d'}]},
174
			{featureType: 'water', elementType: 'geometry', stylers: [{color: '#0e1626'}]}
175
		]
176
	});
177
	infoWindow = new google.maps.InfoWindow();
178
 
179
	// Home marker
180
	homeMarker = new google.maps.Marker({
181
		map: map, position: center, title: 'Home: ' + state.homeName,
182
		icon: {
183
			url: 'https://maps.google.com/mapfiles/kml/shapes/homegardenbusiness.png',
184
			scaledSize: new google.maps.Size(32, 32)
185
		},
186
		zIndex: 100
187
	});
188
 
189
	// Partner markers with name labels
190
	for (var key in markers) {
191
		markers[key].setMap(null);
192
	}
193
	markers = {};
194
 
195
	state.partners.forEach(function (p) {
196
		if (!p.latitude || !p.longitude) return;
197
		var lat = parseFloat(p.latitude), lng = parseFloat(p.longitude);
198
		if (isNaN(lat) || isNaN(lng)) return;
199
 
200
		var m = new google.maps.Marker({
201
			map: map, position: {lat: lat, lng: lng},
202
			title: p.code + ' - ' + (p.outletName || p.businessName || ''),
203
			label: {text: p.code || '', color: '#fff', fontSize: '10px', fontWeight: '600'},
204
			icon: {
205
				path: google.maps.SymbolPath.CIRCLE,
206
				scale: 14,
207
				fillColor: '#ef4444',
208
				fillOpacity: 0.9,
209
				strokeColor: '#fff',
210
				strokeWeight: 1
211
			}
212
		});
213
 
214
		m.addListener('click', function () {
215
			showMarkerPopup(p, m);
216
		});
217
		m.addListener('mouseover', function () {
218
			showPreviewLine(p);
219
		});
220
		m.addListener('mouseout', function () {
221
			hidePreviewLine();
222
		});
223
 
224
		markers[p.fofoId] = m;
225
	});
226
 
227
	fitBounds();
228
}
229
 
230
// ============ HOVER PREVIEW ============
231
function showPreviewLine(partner) {
232
	if (!partner.latitude || !partner.longitude) return;
233
	var day = curDay();
234
	var lastPt = day.visits.length > 0
235
		? {lat: day.visits[day.visits.length - 1].lat, lng: day.visits[day.visits.length - 1].lng}
236
		: {lat: day.startLat, lng: day.startLng};
237
	if (!lastPt.lat) return;
238
 
239
	var endPt = {lat: parseFloat(partner.latitude), lng: parseFloat(partner.longitude)};
240
	if (previewLine) previewLine.setMap(null);
241
	previewLine = new google.maps.Polyline({
242
		path: [lastPt, endPt],
243
		geodesic: true,
244
		strokeColor: '#60a5fa',
245
		strokeOpacity: 0.5,
246
		strokeWeight: 2,
247
		strokePattern: [10, 5],
248
		icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
249
		map: map
250
	});
251
}
252
 
253
function hidePreviewLine() {
254
	if (previewLine) {
255
		previewLine.setMap(null);
256
		previewLine = null;
257
	}
258
}
259
 
260
// ============ MARKER CLICK POPUP ============
261
function showMarkerPopup(partner, marker) {
262
	var day = curDay();
263
	var alreadyAdded = day.visits.some(function (v) {
264
		return v.id === partner.fofoId;
265
	});
266
	if (alreadyAdded) {
267
		infoWindow.setContent('<div class="marker-popup"><h4>' + partner.code + '</h4><p>Already in today\'s route</p></div>');
268
		infoWindow.open(map, marker);
269
		return;
270
	}
271
 
272
	var lastPt = day.visits.length > 0
273
		? {lat: day.visits[day.visits.length - 1].lat, lng: day.visits[day.visits.length - 1].lng}
274
		: {lat: day.startLat, lng: day.startLng};
275
 
276
	var dist = 0, travelMins = 0;
277
	if (lastPt.lat && partner.latitude) {
278
		dist = haversine(lastPt.lat, lastPt.lng, parseFloat(partner.latitude), parseFloat(partner.longitude)) * ROAD_FACTOR;
279
		travelMins = (dist / AVG_SPEED) * 60;
280
	}
281
 
282
	var name = partner.outletName || partner.businessName || '';
283
	var addr = partner.address || '';
284
 
285
	var html = '<div class="marker-popup">';
286
	html += '<h4>' + partner.code + ' - ' + name + '</h4>';
287
	html += '<div class="popup-meta">' + addr + '</div>';
288
	html += '<div class="popup-distance">~' + dist.toFixed(1) + ' km | ~' + fmtMins(travelMins) + ' travel + ' + VISIT_MINS + 'min visit</div>';
289
	html += '<button class="popup-next" onclick="addVisit(' + partner.fofoId + ',\'next\')">Next Visit</button>';
290
	html += '<button class="popup-last" onclick="addVisit(' + partner.fofoId + ',\'last\')">Last Visit</button>';
291
	html += '<button class="popup-nextday" onclick="addVisit(' + partner.fofoId + ',\'nextday\')">Continue Next Day</button>';
292
	html += '</div>';
293
 
294
	infoWindow.setContent(html);
295
	infoWindow.open(map, marker);
296
}
297
 
298
// ============ ADD VISIT ============
299
function addVisit(fofoId, action) {
300
	infoWindow.close();
301
	var p = state.partners.find(function (x) {
302
		return x.fofoId === fofoId;
303
	});
304
	if (!p) return;
305
 
306
	var day = curDay();
307
	var visit = {
308
		id: p.fofoId, type: 'partner',
309
		name: p.code + ' - ' + (p.outletName || p.businessName || ''),
310
		code: p.code,
311
		lat: p.latitude ? parseFloat(p.latitude) : null,
312
		lng: p.longitude ? parseFloat(p.longitude) : null
313
	};
314
 
315
	day.visits.push(visit);
316
 
317
	// Update marker to green
318
	if (markers[fofoId]) {
319
		markers[fofoId].setIcon({
320
			path: google.maps.SymbolPath.CIRCLE, scale: 14,
321
			fillColor: '#22c55e', fillOpacity: 0.9, strokeColor: '#fff', strokeWeight: 2
322
		});
323
		markers[fofoId].setLabel({text: '' + day.visits.length, color: '#fff', fontSize: '11px', fontWeight: '700'});
324
	}
325
 
326
	recalcDay();
327
	drawRoute();
328
	renderRoutePanel();
329
 
330
	if (action === 'last') {
331
		// Last visit — end day, go home
332
		day.endAction = 'HOME';
333
		day.stayLat = state.homeLat;
334
		day.stayLng = state.homeLng;
335
		day.stayName = state.homeName;
336
		drawReturnHome();
337
		renderRoutePanel();
338
	} else if (action === 'nextday') {
339
		// Continue next day — end current day, start new day from here
340
		day.endAction = 'CONTINUE';
341
		day.stayLat = visit.lat;
342
		day.stayLng = visit.lng;
343
		day.stayName = visit.name;
344
		startNextDay(visit.lat, visit.lng, visit.name);
345
	}
346
	// 'next' — just added, continue on same day
347
}
348
 
349
function drawReturnHome() {
350
	var day = curDay();
351
	if (day.visits.length === 0) return;
352
	var lastVisit = day.visits[day.visits.length - 1];
353
	if (!lastVisit.lat || !state.homeLat) return;
354
 
355
	var line = new google.maps.Polyline({
356
		path: [{lat: lastVisit.lat, lng: lastVisit.lng}, {lat: state.homeLat, lng: state.homeLng}],
357
		geodesic: true, strokeColor: '#22c55e', strokeOpacity: 0.6, strokeWeight: 2,
358
		icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
359
		map: map
360
	});
361
	routeLines.push(line);
362
 
363
	// Add return distance to day total
364
	var retDist = haversine(lastVisit.lat, lastVisit.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
365
	day.totalKm += retDist;
366
	day.totalMins += (retDist / AVG_SPEED) * 60;
367
	updateBottomBar();
368
}
369
 
370
function startNextDay(startLat, startLng, startName) {
371
	var nextNum = state.days.length + 1;
372
	state.days.push({
373
		dayNumber: nextNum, startLat: startLat, startLng: startLng, startName: startName,
374
		visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
375
	});
376
	state.currentDay = nextNum;
377
	updateBottomBar();
378
	renderRoutePanel();
379
	// Reset marker colors for unvisited
380
	resetMarkerColors();
381
}
382
 
383
function resetMarkerColors() {
384
	var allVisited = {};
385
	state.days.forEach(function (d) {
386
		d.visits.forEach(function (v) {
387
			allVisited[v.id] = true;
388
		});
389
	});
390
 
391
	state.partners.forEach(function (p) {
392
		if (!markers[p.fofoId]) return;
393
		if (allVisited[p.fofoId]) {
394
			markers[p.fofoId].setIcon({
395
				path: google.maps.SymbolPath.CIRCLE,
396
				scale: 14,
397
				fillColor: '#22c55e',
398
				fillOpacity: 0.9,
399
				strokeColor: '#fff',
400
				strokeWeight: 1
401
			});
402
		} else {
403
			markers[p.fofoId].setIcon({
404
				path: google.maps.SymbolPath.CIRCLE,
405
				scale: 14,
406
				fillColor: '#ef4444',
407
				fillOpacity: 0.9,
408
				strokeColor: '#fff',
409
				strokeWeight: 1
410
			});
411
			markers[p.fofoId].setLabel({text: p.code || '', color: '#fff', fontSize: '10px', fontWeight: '600'});
412
		}
413
	});
414
 
415
	// Re-label current day visits
416
	var day = curDay();
417
	day.visits.forEach(function (v, i) {
418
		if (markers[v.id]) markers[v.id].setLabel({
419
			text: '' + (i + 1),
420
			color: '#fff',
421
			fontSize: '11px',
422
			fontWeight: '700'
423
		});
424
	});
425
}
426
 
427
// ============ ROUTE DRAWING ============
428
function clearRoute() {
429
	routeLines.forEach(function (l) {
430
		l.setMap(null);
431
	});
432
	routeLines = [];
433
	distLabels.forEach(function (l) {
434
		l.setMap(null);
435
	});
436
	distLabels = [];
437
}
438
 
439
function drawRoute() {
440
	clearRoute();
441
	var day = curDay();
442
	var pts = [{lat: day.startLat, lng: day.startLng}];
443
	day.visits.forEach(function (v) {
444
		if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
445
	});
446
 
447
	for (var i = 0; i < pts.length - 1; i++) {
448
		var line = new google.maps.Polyline({
449
			path: [pts[i], pts[i + 1]],
450
			geodesic: true,
451
			strokeColor: '#3b82f6',
452
			strokeOpacity: 0.9,
453
			strokeWeight: 3,
454
			map: map
455
		});
456
		routeLines.push(line);
457
 
458
		var d = haversine(pts[i].lat, pts[i].lng, pts[i + 1].lat, pts[i + 1].lng) * ROAD_FACTOR;
459
		var mid = {lat: (pts[i].lat + pts[i + 1].lat) / 2, lng: (pts[i].lng + pts[i + 1].lng) / 2};
460
		var lbl = new google.maps.Marker({
461
			position: mid, map: map, icon: {path: google.maps.SymbolPath.CIRCLE, scale: 0},
462
			label: {text: d.toFixed(1) + 'km', color: '#93c5fd', fontSize: '10px', fontWeight: '600'}
463
		});
464
		distLabels.push(lbl);
465
	}
466
}
467
 
468
function recalcDay() {
469
	var day = curDay();
470
	var pts = [{lat: day.startLat, lng: day.startLng}];
471
	day.visits.forEach(function (v) {
472
		if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
473
	});
474
 
475
	var km = 0;
476
	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;
477
	day.totalKm = km;
478
	day.totalMins = (km / AVG_SPEED) * 60 + day.visits.length * VISIT_MINS;
479
	updateBottomBar();
480
}
481
 
482
function updateBottomBar() {
483
	var day = curDay();
484
	$('#bb-day-label').text('Day ' + day.dayNumber);
485
	$('#bb-stops').text(day.visits.length + ' stops');
486
	$('#bb-distance').text(day.totalKm.toFixed(1) + ' km');
487
	$('#bb-time').text(fmtMins(day.totalMins));
488
 
489
	var pct = Math.min((day.totalMins / DAY_LIMIT) * 100, 100);
490
	var col = pct < 78 ? '#22c55e' : pct < 89 ? '#f59e0b' : '#ef4444';
491
	$('#bb-progress').css({width: pct + '%', background: col});
492
}
493
 
494
// ============ ROUTE PANEL ============
495
function renderRoutePanel() {
496
	var html = '';
497
	state.days.forEach(function (day) {
498
		var isCurrent = day.dayNumber === state.currentDay;
499
		html += '<div class="route-day">';
500
		html += '<div class="route-day-header">';
501
		html += '<span>Day ' + day.dayNumber + (isCurrent ? ' (current)' : '') + '</span>';
502
		html += '<span>' + fmtMins(day.totalMins) + '</span>';
503
		html += '</div>';
504
 
505
		// Start point
506
		html += '<div class="route-stop route-stop-home"><span class="stop-num">H</span><div class="stop-info"><div class="stop-name">' + (day.startName || 'Start') + '</div></div></div>';
507
 
508
		// Visits with distance between each
509
		var prevLat = day.startLat, prevLng = day.startLng;
510
		day.visits.forEach(function (v, i) {
511
			// Distance connector from previous point
512
			var segDist = 0, segTime = 0;
513
			if (prevLat && prevLng && v.lat && v.lng) {
514
				segDist = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
515
				segTime = (segDist / AVG_SPEED) * 60;
516
			}
517
			if (segDist > 0) {
518
				html += '<div class="route-segment"><span class="seg-line"></span><span class="seg-dist">' + segDist.toFixed(1) + ' km | ' + fmtMins(segTime) + '</span></div>';
519
			}
520
 
521
			html += '<div class="route-stop route-stop-clickable" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" style="cursor:pointer;">';
522
			html += '<span class="stop-num">' + (i + 1) + '</span>';
523
			html += '<div class="stop-info"><div class="stop-name">' + (v.code || '') + '</div>';
524
			html += '<div class="stop-meta">' + (v.name || '') + '</div></div>';
525
			html += '<span class="stop-remove" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" title="Remove">&times;</span>';
526
			html += '</div>';
527
 
528
			prevLat = v.lat;
529
			prevLng = v.lng;
530
		});
531
 
532
		// Return home distance
533
		if (day.endAction === 'HOME' && day.visits.length > 0) {
534
			var lastV = day.visits[day.visits.length - 1];
535
			var retDist = 0, retTime = 0;
536
			if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
537
				retDist = haversine(lastV.lat, lastV.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
538
				retTime = (retDist / AVG_SPEED) * 60;
539
			}
540
			if (retDist > 0) {
541
				html += '<div class="route-segment"><span class="seg-line seg-line-home"></span><span class="seg-dist">' + retDist.toFixed(1) + ' km | ' + fmtMins(retTime) + '</span></div>';
542
			}
543
			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>';
544
		}
545
 
546
		html += '<div class="day-summary">' + day.visits.length + ' stops | ' + day.totalKm.toFixed(1) + ' km</div>';
547
		html += '</div>';
548
	});
549
 
550
	$('#route-list').html(html);
551
}
552
 
553
// Click on stop name in route panel → show popup on map
554
$(document).on('click', '.route-stop-clickable', function (e) {
555
	if ($(e.target).hasClass('stop-remove')) return; // let remove handler handle it
556
	var fofoId = parseInt($(this).data('fofoid'));
557
	if (!fofoId || !markers[fofoId]) return;
558
 
559
	var marker = markers[fofoId];
560
	var p = state.partners.find(function (x) {
561
		return x.fofoId === fofoId;
562
	});
563
	if (!p) return;
564
 
565
	map.panTo(marker.getPosition());
566
	map.setZoom(12);
567
 
568
	var name = p.outletName || p.businessName || '';
569
	var addr = p.address || '';
570
	infoWindow.setContent('<div class="marker-popup"><h4>' + p.code + ' - ' + name + '</h4><div class="popup-meta">' + addr + '</div></div>');
571
	infoWindow.open(map, marker);
572
});
573
 
574
// Remove stop from route panel
575
$(document).on('click', '.stop-remove', function (e) {
576
	e.stopPropagation();
577
	var fofoId = parseInt($(this).data('fofoid'));
578
	var dayNum = parseInt($(this).data('daynum'));
579
 
580
	var day = state.days[dayNum - 1];
581
	if (!day) return;
582
 
583
	// Remove visit from day
584
	day.visits = day.visits.filter(function (v) {
585
		return v.id !== fofoId;
586
	});
587
 
588
	// Reset marker back to red (unvisited)
589
	if (markers[fofoId]) {
590
		var p = state.partners.find(function (x) {
591
			return x.fofoId === fofoId;
592
		});
593
		markers[fofoId].setIcon({
594
			path: google.maps.SymbolPath.CIRCLE, scale: 14,
595
			fillColor: '#ef4444', fillOpacity: 0.9, strokeColor: '#fff', strokeWeight: 1
596
		});
597
		markers[fofoId].setLabel({text: (p ? p.code : '') || '', color: '#fff', fontSize: '10px', fontWeight: '600'});
598
	}
599
 
600
	// Re-number remaining markers for this day
601
	day.visits.forEach(function (v, i) {
602
		if (markers[v.id]) {
603
			markers[v.id].setLabel({text: '' + (i + 1), color: '#fff', fontSize: '11px', fontWeight: '700'});
604
		}
605
	});
606
 
607
	recalcDay();
608
	drawRoute();
609
	renderRoutePanel();
610
	updateBottomBar();
611
});
612
 
613
// ============ END DAY / FINISH ============
614
$('#btn-end-day').on('click', function () {
615
	var day = curDay();
616
	if (day.visits.length === 0) {
617
		alert('Add at least one stop');
618
		return;
619
	}
620
 
621
	$('#daybreak-info').text('Day ' + day.dayNumber + ': ' + day.visits.length + ' stops, ' + day.totalKm.toFixed(1) + ' km, ' + fmtMins(day.totalMins));
622
	$('#modal-daybreak').addClass('show');
623
});
624
 
625
$('input[name="stay"]').on('change', function () {
626
	$('#hotel-fields').toggle($(this).val() === 'HOTEL');
627
	if ($(this).val() === 'HOTEL') {
628
		setTimeout(function () {
629
			if (!window._hotelMap) {
630
				window._hotelMap = new google.maps.Map(document.getElementById('hotel-map'), {
631
					center: {
632
						lat: 28.6,
633
						lng: 77.2
634
					}, zoom: 6
635
				});
636
				var hm = new google.maps.Marker({map: window._hotelMap, draggable: true});
637
				var hac = new google.maps.places.Autocomplete(document.getElementById('hotel-search'), {componentRestrictions: {country: 'in'}});
638
				hac.addListener('place_changed', function () {
639
					var p = hac.getPlace();
640
					if (!p.geometry) return;
641
					window._hotelMap.setCenter(p.geometry.location);
642
					window._hotelMap.setZoom(14);
643
					hm.setPosition(p.geometry.location);
644
					state._hotelLat = p.geometry.location.lat();
645
					state._hotelLng = p.geometry.location.lng();
646
					state._hotelName = p.name || p.formatted_address;
647
				});
648
				hm.addListener('dragend', function () {
649
					state._hotelLat = hm.getPosition().lat();
650
					state._hotelLng = hm.getPosition().lng();
651
				});
652
			}
653
		}, 200);
654
	}
655
});
656
 
657
$('#daybreak-cancel').on('click', function () {
658
	$('#modal-daybreak').removeClass('show');
659
});
660
$('#daybreak-confirm').on('click', function () {
661
	var day = curDay();
662
	var opt = $('input[name="stay"]:checked').val();
663
	day.endAction = opt;
664
 
665
	if (opt === 'HOME') {
666
		day.stayLat = state.homeLat;
667
		day.stayLng = state.homeLng;
668
		day.stayName = state.homeName;
669
		drawReturnHome();
670
		startNextDay(state.homeLat, state.homeLng, state.homeName);
671
	} else {
672
		if (!state._hotelLat) {
673
			alert('Select hotel location');
674
			return;
675
		}
676
		day.stayLat = state._hotelLat;
677
		day.stayLng = state._hotelLng;
678
		day.stayName = state._hotelName;
679
		startNextDay(state._hotelLat, state._hotelLng, state._hotelName);
680
		state._hotelLat = null;
681
		state._hotelLng = null;
682
	}
683
	$('#modal-daybreak').removeClass('show');
684
	renderRoutePanel();
685
});
686
 
687
$('#btn-finish').on('click', function () {
688
	if (state.days.every(function (d) {
689
		return d.visits.length === 0;
690
	})) {
691
		alert('No visits added');
692
		return;
693
	}
694
 
695
	// Ask for beat title
696
	var beatTitle = prompt('Enter a name for this beat:', '');
697
	if (beatTitle === null) return; // cancelled
698
	if (!beatTitle.trim()) {
699
		alert('Please enter a beat name');
700
		return;
701
	}
702
 
703
	// Build plan data and save to DB first
704
	var daysData = [];
705
	state.days.forEach(function (day) {
706
		if (day.visits.length === 0) return;
707
		daysData.push({
708
			dayNumber: day.dayNumber,
709
			startLocationName: day.startName,
710
			startLatitude: day.startLat ? day.startLat.toString() : null,
711
			startLongitude: day.startLng ? day.startLng.toString() : null,
712
			endAction: day.endAction,
713
			stayLocationName: day.stayName,
714
			stayLatitude: day.stayLat ? day.stayLat.toString() : null,
715
			stayLongitude: day.stayLng ? day.stayLng.toString() : null,
716
			totalDistanceKm: day.totalKm,
717
			totalTimeMins: Math.round(day.totalMins),
718
			visits: day.visits.map(function (v) {
719
				return {id: v.id, type: v.type || 'partner'};
720
			})
721
		});
722
	});
723
 
724
	// Dates will be assigned later via calendar — pass nulls for now
725
	var dates = daysData.map(function () {
726
		return null;
727
	});
728
	var planData = JSON.stringify({days: daysData, dates: dates, beatName: beatTitle.trim()});
729
 
730
	$('#btn-finish').prop('disabled', true).text('Saving...');
731
 
732
	$.ajax({
733
		url: context + "/beatPlan/submitPlan",
734
		type: "POST",
735
		data: {authUserId: state.authUserId, planData: planData},
736
		success: function (response) {
737
			var data = (typeof response === 'string' ? JSON.parse(response) : response);
738
			var result = data.response || data;
739
			$('#btn-finish').prop('disabled', false).text('Finish Plan');
740
 
741
			if (result.status) {
742
				state.savedPlanGroupId = result.planGroupId;
743
				if (result.duplicate) {
744
					alert('This beat already exists (same visits). Switching to calendar.');
745
				}
746
 
747
				// Switch to calendar tab
748
				$('.panel-tab').removeClass('active');
749
				$('.panel-tab[data-tab="calendar"]').addClass('active');
750
				$('#panel-route').hide();
751
				$('#panel-calendar').show();
752
				$('#cal-grid-container').show();
753
				$('#bottom-bar').hide();
754
 
755
				var now = new Date();
756
				state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
757
				loadCalendarData();
758
			} else {
759
				alert('Error saving beat plan');
760
			}
761
		},
762
		error: function (xhr) {
763
			$('#btn-finish').prop('disabled', false).text('Finish Plan');
764
			alert('Error: ' + (xhr.responseText || xhr.statusText));
765
		}
766
	});
767
});
768
 
769
// ============ PANEL TABS ============
770
$('.panel-tab').on('click', function () {
771
	var tab = $(this).data('tab');
772
	$('.panel-tab').removeClass('active');
773
	$(this).addClass('active');
774
	$('#panel-route').toggle(tab === 'route');
775
	$('#panel-calendar').toggle(tab === 'calendar');
776
	$('#cal-grid-container').toggle(tab === 'calendar');
777
	$('#bottom-bar').toggle(tab === 'route' && state.days.length > 0);
778
 
779
	if (tab === 'calendar' && !state.calMonth) {
780
		var now = new Date();
781
		state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
782
		loadCalendarData();
783
	}
784
});
785
 
786
// ============ CALENDAR ============
787
$('#cal-prev').on('click', function () {
788
	navMonth(-1);
789
});
790
$('#cal-next').on('click', function () {
791
	navMonth(1);
792
});
793
 
794
function navMonth(delta) {
795
	var p = state.calMonth.split('-');
796
	var d = new Date(parseInt(p[0]), parseInt(p[1]) - 1 + delta, 1);
797
	state.calMonth = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2);
798
	loadCalendarData();
799
}
800
 
801
function loadCalendarData() {
802
	if (!state.authUserId) return;
803
	$('#cal-month-label').text(state.calMonth);
804
 
805
	$.ajax({
806
		url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
807
		data: {authUserId: state.authUserId, month: state.calMonth},
808
		success: function (r) {
809
			var data = r.response || r;
810
			state.calData = data;
811
			renderCalGrid(data);
812
			renderBeatCards(data.scheduledBeats);
813
		}
814
	});
815
}
816
 
817
function renderCalGrid(data) {
818
	var p = state.calMonth.split('-');
819
	var year = parseInt(p[0]), month = parseInt(p[1]) - 1;
820
	var first = new Date(year, month, 1);
821
	var last = new Date(year, month + 1, 0);
822
	var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
823
	$('#cal-month-label').text(months[month] + ' ' + year);
824
 
825
	var holidayMap = {};
826
	(data.holidays || []).forEach(function (h) {
827
		holidayMap[h.date] = h.occasion;
828
	});
829
	var blockedSet = {};
830
	(data.blockedDates || []).forEach(function (d) {
831
		blockedSet[d] = true;
832
	});
833
 
834
	var beatDateMap = {};
835
	(data.scheduledBeats || []).forEach(function (b) {
836
		(b.days || []).forEach(function (d) {
837
			if (d.planDate) {
838
				if (!beatDateMap[d.planDate]) beatDateMap[d.planDate] = [];
839
				beatDateMap[d.planDate].push({
840
					name: b.beatName,
841
					color: b.beatColor,
842
					day: d.dayNumber,
843
					status: b.status,
844
					visits: d.visitCount
845
				});
846
			}
847
		});
848
	});
849
 
850
	var today = fmtDate(new Date());
851
	var startOff = (first.getDay() + 6) % 7;
852
	var calStart = new Date(year, month, 1 - startOff);
853
 
854
	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>';
855
	var d = new Date(calStart);
856
	for (var w = 0; w < 6; w++) {
857
		var hasDay = false;
858
		var row = '<tr>';
859
		for (var wd = 0; wd < 7; wd++) {
860
			var ds = fmtDate(d);
861
			var inMonth = d.getMonth() === month;
862
			var sun = d.getDay() === 0;
863
			var hol = !!holidayMap[ds];
864
			var isToday = ds === today;
865
			var beats = beatDateMap[ds] || [];
866
			var isPicked = state.calPickedDates.indexOf(ds) > -1;
867
 
868
			var cls = [];
869
			if (sun) cls.push('blocked');
870
			else if (hol) cls.push('holiday');
871
			if (isToday) cls.push('today');
872
			if (!inMonth) cls.push('blocked');
873
 
874
			var style = inMonth ? '' : 'opacity:0.3;';
875
 
876
			row += '<td class="' + cls.join(' ') + '" data-date="' + ds + '" style="' + style + '">';
877
			row += '<div class="cal-date">' + d.getDate() + '</div>';
878
			if (hol) row += '<div class="cal-holiday-label">' + holidayMap[ds] + '</div>';
879
 
880
			beats.forEach(function (b) {
881
				var chipCls = 'cal-chip';
882
				if (b.status === 'running') chipCls += ' running';
883
				if (b.status === 'completed') chipCls += ' completed';
884
				row += '<span class="' + chipCls + '" style="background:' + b.color + '">' + b.name + ' D' + b.day + (b.status === 'running' ? ' [live]' : '') + ' (' + b.visits + ')</span>';
885
			});
886
 
887
			if (isPicked) {
888
				var idx = state.calPickedDates.indexOf(ds) + 1;
889
				row += '<span class="cal-chip" style="background:#3b82f6;border:1px dashed #93c5fd;">Day ' + idx + '</span>';
890
			}
891
 
892
			row += '</td>';
893
			if (inMonth) hasDay = true;
894
			d.setDate(d.getDate() + 1);
895
		}
896
		row += '</tr>';
897
		if (hasDay) html += row;
898
	}
899
	html += '</table>';
900
	$('#cal-grid-content').html(html);
901
}
902
 
903
function renderBeatCards(beats) {
904
	var html = '';
905
	if (!beats || beats.length === 0) {
906
		html = '<p style="color:#64748b;font-size:12px;padding:20px;">No beats scheduled. Create a beat first.</p>';
907
	}
908
	(beats || []).forEach(function (b) {
909
		var badge = b.status === 'running' ? '<span class="status-badge status-running">Live</span>' :
910
			b.status === 'completed' ? '<span class="status-badge status-completed">Done</span>' :
911
				b.status === 'unscheduled' ? '<span class="status-badge" style="background:#f59e0b;color:#0f172a;">Unscheduled</span>' :
912
					'<span class="status-badge status-scheduled">Scheduled</span>';
913
 
914
		var totalV = 0, totalKm = 0;
915
		b.days.forEach(function (d) {
916
			totalV += d.visitCount || 0;
917
			totalKm += d.totalKm || 0;
918
		});
919
 
920
		html += '<div class="beat-card" data-plangroup="' + b.planGroupId + '">';
921
		html += '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + b.beatColor + ';"></span> ' + b.beatName + ' ' + badge + '</div>';
922
		html += '<div class="beat-meta">' + b.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km</div>';
923
		if (b.status !== 'running') {
924
			var isScheduling = state.calSelectedBeat === b.planGroupId && state.calPickedDates.length > 0;
925
			html += '<div class="beat-actions">';
926
			if (isScheduling) {
927
				html += '<button class="btn-sm btn-confirm-schedule" data-pg="' + b.planGroupId + '" data-days="' + b.days.length + '" style="background:#22c55e;color:#fff;">Confirm (' + state.calPickedDates.length + ' dates)</button>';
928
				html += '<button class="btn-sm btn-cancel-schedule" style="background:#475569;color:#e2e8f0;">Cancel</button>';
929
			} else {
930
				html += '<button class="btn-sm btn-schedule" data-pg="' + b.planGroupId + '" data-days="' + b.days.length + '">Schedule</button>';
931
				html += '<button class="btn-sm btn-repeat" data-pg="' + b.planGroupId + '" data-days="' + b.days.length + '">Repeat</button>';
932
			}
933
			html += '<button class="btn-sm" style="background:#ef4444;color:#fff;" data-pg="' + b.planGroupId + '" onclick="deleteBeat(\'' + b.planGroupId + '\')">Delete</button>';
934
			html += '</div>';
935
			if (isScheduling) {
936
				html += '<div style="font-size:10px;color:#60a5fa;margin-top:4px;">Dates: ' + state.calPickedDates.join(', ') + '</div>';
937
				html += '<div style="font-size:10px;color:#94a3b8;">Click calendar cells to adjust</div>';
938
			}
939
		}
940
		html += '</div>';
941
	});
942
	$('#cal-beat-cards').html(html);
943
}
944
 
945
// Schedule button — suggest dates, show Confirm button in beat card
946
$(document).on('click', '.btn-schedule', function (e) {
947
	e.stopPropagation();
948
	var pg = $(this).data('pg');
949
	var days = parseInt($(this).data('days'));
950
	state.calSelectedBeat = pg;
951
	state.calPickedDates = [];
952
 
953
	$.ajax({
954
		url: context + "/beatPlan/availableSlots", type: "GET", dataType: "json",
955
		data: {authUserId: state.authUserId, month: state.calMonth, daysNeeded: days},
956
		success: function (r) {
957
			var data = r.response || r;
958
			state.calPickedDates = data.suggestedDates || [];
959
			renderCalGrid(state.calData);
960
			renderBeatCards(state.calData.scheduledBeats);
961
			if (state.calPickedDates.length < days) {
962
				alert('Only ' + state.calPickedDates.length + ' slots available this month. Need ' + days + '.');
963
			}
964
		}
965
	});
966
});
967
 
968
// Click calendar cell to toggle picked date
969
$(document).on('click', '#cal-grid-content td:not(.blocked)', function () {
970
	if (!state.calSelectedBeat) return;
971
	var ds = $(this).data('date');
972
	var idx = state.calPickedDates.indexOf(ds);
973
	if (idx > -1) state.calPickedDates.splice(idx, 1);
974
	else state.calPickedDates.push(ds);
975
	state.calPickedDates.sort();
976
	renderCalGrid(state.calData);
977
	renderBeatCards(state.calData.scheduledBeats);
978
});
979
 
980
// Cancel scheduling mode
981
$(document).on('click', '.btn-cancel-schedule', function (e) {
982
	e.stopPropagation();
983
	state.calSelectedBeat = null;
984
	state.calPickedDates = [];
985
	renderCalGrid(state.calData);
986
	renderBeatCards(state.calData.scheduledBeats);
987
});
988
 
989
// Confirm schedule — save to server
990
$(document).on('click', '.btn-confirm-schedule', function (e) {
991
	e.stopPropagation();
992
	var pg = $(this).data('pg');
993
	var daysNeeded = parseInt($(this).data('days'));
994
 
995
	if (state.calPickedDates.length < daysNeeded) {
996
		alert('Need ' + daysNeeded + ' dates but only ' + state.calPickedDates.length + ' selected.');
997
		return;
998
	}
999
 
1000
	var beat = (state.calData.scheduledBeats || []).find(function (b) {
1001
		return b.planGroupId === pg;
1002
	});
1003
	if (!beat) return;
1004
 
1005
	$.ajax({
1006
		url: context + "/beatPlan/scheduleOnCalendar", type: "POST",
1007
		data: {
1008
			planGroupId: pg,
1009
			dates: JSON.stringify(state.calPickedDates.slice(0, daysNeeded)),
1010
			beatName: beat.beatName,
1011
			beatColor: beat.beatColor
1012
		},
1013
		success: function () {
1014
			state.calSelectedBeat = null;
1015
			state.calPickedDates = [];
1016
			loadCalendarData();
1017
		},
1018
		error: function (xhr) {
1019
			alert('Error: ' + (xhr.responseText || xhr.statusText));
1020
		}
1021
	});
1022
});
1023
 
1024
// Repeat
1025
$(document).on('click', '.btn-repeat', function (e) {
1026
	e.stopPropagation();
1027
	var pg = $(this).data('pg');
1028
	var days = parseInt($(this).data('days'));
1029
 
1030
	$.ajax({
1031
		url: context + "/beatPlan/availableSlots", type: "GET", dataType: "json",
1032
		data: {authUserId: state.authUserId, month: state.calMonth, daysNeeded: days},
1033
		success: function (r) {
1034
			var data = r.response || r;
1035
			if ((data.suggestedDates || []).length < days) {
1036
				alert('Not enough slots this month');
1037
				return;
1038
			}
1039
			if (!confirm('Repeat beat on: ' + data.suggestedDates.join(', ') + '?')) return;
1040
 
1041
			$.ajax({
1042
				url: context + "/beatPlan/repeatBeat", type: "POST",
1043
				data: {sourcePlanGroupId: pg, authUserId: state.authUserId, dates: JSON.stringify(data.suggestedDates)},
1044
				success: function () {
1045
					loadCalendarData();
1046
					alert('Beat repeated!');
1047
				}
1048
			});
1049
		}
1050
	});
1051
});
1052
 
1053
// ============ DELETE BEAT ============
1054
function deleteBeat(planGroupId) {
1055
	if (!confirm('Delete this beat? This cannot be undone.')) return;
1056
	$.ajax({
1057
		url: context + "/beatPlan/delete", type: "POST",
1058
		data: {planGroupId: planGroupId},
1059
		success: function () {
1060
			loadCalendarData();
1061
		},
1062
		error: function (xhr) {
1063
			alert('Error: ' + (xhr.responseText || xhr.statusText));
1064
		}
1065
	});
1066
}
1067
 
1068
// ============ HELPERS ============
1069
function curDay() {
1070
	return state.days[state.currentDay - 1];
1071
}
1072
 
1073
function haversine(lat1, lng1, lat2, lng2) {
1074
	var R = 6371, dLat = (lat2 - lat1) * Math.PI / 180, dLng = (lng2 - lng1) * Math.PI / 180;
1075
	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);
1076
	return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1077
}
1078
 
1079
function fmtMins(m) {
1080
	return Math.floor(m / 60) + 'h ' + Math.round(m % 60) + 'm';
1081
}
1082
 
1083
function fmtDate(d) {
1084
	return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
1085
}
1086
 
1087
function fitBounds() {
1088
	if (Object.keys(markers).length > 0) {
1089
		var b = new google.maps.LatLngBounds();
1090
		if (homeMarker) b.extend(homeMarker.getPosition());
1091
		for (var k in markers) b.extend(markers[k].getPosition());
1092
		map.fitBounds(b);
1093
	}
1094
}