Subversion Repositories SmartDukaan

Rev

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

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