Subversion Repositories SmartDukaan

Rev

Rev 36962 | 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,
36962 vikas 20
	calPickedDates: [],
21
	// L4+ operators may schedule a beat on today's date (set from the page).
22
	canScheduleToday: (typeof canScheduleToday !== 'undefined' && !!canScheduleToday)
36621 ranu 23
};
24
 
25
var VISIT_MINS = 30;
26
var AVG_SPEED = 30;
27
var DAY_LIMIT = 540;
28
var ROAD_FACTOR = 1.3;
36644 ranu 29
var isSubmittingBeat = false; // guard against double-submit
36621 ranu 30
var COLORS = ['#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b', '#06b6d4', '#ec4899', '#334155', '#14b8a6', '#dc2626'];
31
 
32
// ============ TOP BAR ============
33
$('#bp-level').on('change', function () {
34
	var level = $(this).val();
35
	$('#bp-auth-user').html('<option value="">Select User</option>');
36
	if (!level) return;
37
 
38
	$.ajax({
39
		url: context + "/beatPlan/getAuthUsers", type: "GET", dataType: "json",
40
		data: {categoryId: $('#bp-category').val(), escalationType: level},
41
		success: function (r) {
42
			var data = r.response || r;
43
			var html = '<option value="">Select User</option>';
36788 ranu 44
			// Show "Name (#authUserId)" so the head can quickly match identities
45
			// (useful when grabbing an id for the Visit-Request Reassign prompt etc).
36621 ranu 46
			data.forEach(function (u) {
36788 ranu 47
				html += '<option value="' + u.id + '">' + u.name + ' (#' + u.id + ')</option>';
36621 ranu 48
			});
49
			$('#bp-auth-user').html(html);
50
		}
51
	});
52
});
53
 
54
$('#bp-load').on('click', function () {
55
	var uid = $('#bp-auth-user').val();
56
	if (!uid) {
57
		alert('Select a user');
58
		return;
59
	}
36785 ranu 60
 
61
	// Hard reset of any previous user's cached state so calendar chips,
62
	// route lines, partner markers, and the route-detail view don't leak.
36621 ranu 63
	state.authUserId = parseInt(uid);
64
	state.categoryId = parseInt($('#bp-category').val());
36785 ranu 65
	state.mode = 'list';
66
	state.days = [];
67
	state.partners = [];
68
	state.currentDay = 1;
69
	state.calMonth = null;
70
	state.calData = null;
71
	state.calSelectedBeat = null;
72
	state.calPickedDates = [];
73
	state.homeLat = null;
74
	state.homeLng = null;
75
	state.homeName = '';
76
	state.activeBaseLocationId = null;
77
	state.baseLocations = [];
78
	state.viewDate = null;
79
	state.originalDayCount = 0;
36787 ranu 80
	state.visitRequests = [];
81
	state.assignVisitRequest = null;
36792 ranu 82
	state.deferredItems = [];
36787 ranu 83
	$('#visit-request-panel').hide();
36792 ranu 84
	// #deferred-items-panel now sits inside its own tab pane (#panel-deferred)
85
	// which controls visibility — no need to hide the inner panel here.
86
	$('#di-list').empty();
87
	$('#di-count').text('');
88
	$('#di-tab-count').text('');
36785 ranu 89
	// Clear the rendered calendar grid + route map / panel so nothing from
90
	// the previous user is briefly visible while the new fetch is in flight.
91
	$('#cal-grid-content').empty();
92
	$('#route-list').empty();
93
	if (typeof clearRoute === 'function') {
94
		try {
95
			clearRoute();
96
		} catch (e) {
97
		}
98
	}
99
	if (typeof markers === 'object' && markers) {
100
		for (var k in markers) {
101
			try {
102
				markers[k].setMap(null);
103
			} catch (e) {
104
			}
105
		}
106
		markers = {};
107
	}
108
	if (typeof homeMarker !== 'undefined' && homeMarker) {
109
		try {
110
			homeMarker.setMap(null);
111
		} catch (e) {
112
		}
113
		homeMarker = null;
114
	}
115
 
36621 ranu 116
	$('#bp-user-label').text($('#bp-auth-user option:selected').text());
117
 
36681 ranu 118
	// Load all saved base locations — default goes in state.home*, the rest is
119
	// exposed via a picker in the route panel so the planner can pick a
120
	// different starting point for this one beat without changing the default.
36621 ranu 121
	$.ajax({
36681 ranu 122
		url: context + "/beatPlan/listBaseLocations", type: "GET", dataType: "json",
36621 ranu 123
		data: {authUserId: uid},
124
		success: function (r) {
125
			var data = r.response || r;
36681 ranu 126
			state.baseLocations = data.locations || [];
127
			var def = state.baseLocations.find(function (l) {
128
				return l.isDefault;
129
			}) || state.baseLocations[0];
130
			if (def) {
131
				state.homeLat = parseFloat(def.latitude);
132
				state.homeLng = parseFloat(def.longitude);
133
				state.homeName = def.locationName;
134
				state.activeBaseLocationId = def.id;
135
			} else {
136
				state.activeBaseLocationId = null;
36632 ranu 137
            }
138
            showBeatList();
36785 ranu 139
			// If the user is currently on the Calendar tab when Load is clicked,
140
			// re-fetch immediately. Otherwise the calendar grid stays blank
141
			// until they click the tab (which would auto-fetch on its own).
142
			if ($('.panel-tab[data-tab="calendar"]').hasClass('active')) {
143
				var now = new Date();
144
				state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
145
				loadCalendarData();
146
			}
36787 ranu 147
			// Pending visit requests for this user's hierarchy. Hidden when empty.
148
			if (typeof loadVisitRequests === 'function') {
149
				loadVisitRequests();
150
			}
36792 ranu 151
			// Deferred items (partners + leads) for this user. Hidden when empty.
152
			if (typeof loadDeferredItems === 'function') {
153
				loadDeferredItems();
154
			}
36632 ranu 155
        }
156
    });
36621 ranu 157
});
158
 
36681 ranu 159
// Called from the picker → swap the start location for the beat being
160
// created/edited. Updates state.home* + day 1's start point, then re-draws.
36698 ranu 161
// Inline typeahead inside the route sidebar — same data source as the
162
// Partners tab (state.partners, fed by /beatPlan/getPartners, which mirrors
163
// the /getPartnerReadonlyInfo pattern used by the Partner Access tab).
164
// User types 2+ chars → suggestions appear → click adds to the current day.
165
function renderPartnerQuickAdd() {
166
	if (!state.partners || state.partners.length === 0) return '';
167
	return ''
168
		+ '<div style="position:relative; margin-bottom:10px;">'
169
		+ '  <input type="text" id="bp-partner-quick" autocomplete="off"'
170
		+ '         placeholder="Search & add a partner..."'
171
		+ '         style="width:100%; padding:8px 10px; border:1px solid #475569; border-radius:6px;'
172
		+ '                background:#0f172a; color:#e2e8f0; font-size:12px; font-family:inherit;">'
173
		+ '  <div id="bp-partner-suggest"'
174
		+ '       style="display:none; position:absolute; left:0; right:0; top:100%; z-index:50;'
175
		+ '              background:#1e293b; border:1px solid #334155; border-radius:6px;'
176
		+ '              margin-top:4px; max-height:260px; overflow-y:auto;'
177
		+ '              box-shadow:0 6px 16px rgba(0,0,0,0.4);"></div>'
178
		+ '</div>';
179
}
180
 
181
function refreshPartnerSuggestions() {
182
	var $input = $('#bp-partner-quick');
183
	if (!$input.length) return;
184
	var q = ($input.val() || '').trim().toLowerCase();
185
	var $sug = $('#bp-partner-suggest');
186
	if (q.length < 2) {
187
		$sug.hide().empty();
188
		return;
189
	}
190
 
191
	// Lookup of fofoId → day number for partners already on the route
192
	var inRoute = {};
193
	(state.days || []).forEach(function (d) {
194
		(d.visits || []).forEach(function (v) {
195
			if (!v.isLead && v.type !== 'lead') inRoute[v.id] = d.dayNumber;
196
		});
197
	});
198
 
199
	var matches = (state.partners || []).filter(function (p) {
200
		var hay = ((p.code || '') + ' '
201
			+ (p.outletName || '') + ' '
202
			+ (p.businessName || '') + ' '
203
			+ (p.city || '')).toLowerCase();
204
		return hay.indexOf(q) !== -1;
205
	}).slice(0, 10);
206
 
207
	if (matches.length === 0) {
208
		$sug.html('<div style="padding:10px; color:#94a3b8; font-size:11px; text-align:center;">No partners found</div>').show();
209
		return;
210
	}
211
 
212
	var html = '';
213
	matches.forEach(function (p) {
214
		var day = inRoute[p.fofoId];
215
		var nameLine = (p.outletName || p.businessName || '');
216
		if (p.city) nameLine += (nameLine ? ' - ' : '') + p.city;
36802 ranu 217
		var inactiveBadge = (p.active === false)
218
			? ' <span style="background:#7c2d12;color:#fed7aa;padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600;">Inactive</span>'
219
			: '';
36698 ranu 220
		if (day) {
221
			html += '<div class="bp-partner-row in-route" style="padding:8px 10px; font-size:11px;'
222
				+ ' color:#94a3b8; border-bottom:1px solid #0f172a; cursor:not-allowed;">'
36802 ranu 223
				+ '<div style="font-weight:600;">' + (p.code || '') + inactiveBadge + '</div>'
36698 ranu 224
				+ '<div>' + nameLine + ' <span style="color:#22c55e;">(in D' + day + ')</span></div>'
225
				+ '</div>';
226
		} else {
227
			html += '<div class="bp-partner-row" data-fofoid="' + p.fofoId + '"'
228
				+ ' style="padding:8px 10px; font-size:12px; color:#e2e8f0;'
229
				+ ' border-bottom:1px solid #0f172a; cursor:pointer;">'
36802 ranu 230
				+ '<div style="font-weight:600;">' + (p.code || '') + inactiveBadge + '</div>'
36698 ranu 231
				+ '<div style="font-size:11px; color:#94a3b8;">' + nameLine + '</div>'
232
				+ '</div>';
233
		}
234
	});
235
	$sug.html(html).show();
236
}
237
 
238
$(document).on('input', '#bp-partner-quick', refreshPartnerSuggestions);
239
 
240
$(document).on('focus', '#bp-partner-quick', function () {
241
	if (($(this).val() || '').trim().length >= 2) refreshPartnerSuggestions();
242
});
243
 
244
// Hide on outside click
245
$(document).on('click', function (e) {
246
	if (!$(e.target).closest('#bp-partner-quick, #bp-partner-suggest').length) {
247
		$('#bp-partner-suggest').hide();
248
	}
249
});
250
 
251
$(document).on('mousedown', '#bp-partner-suggest .bp-partner-row', function (e) {
252
	if ($(this).hasClass('in-route')) return;
253
	e.preventDefault(); // keep input from losing focus before we read fofoid
254
	var fofoId = parseInt($(this).data('fofoid'));
255
	if (!fofoId) return;
256
	if (typeof addVisit === 'function') addVisit(fofoId, 'next');
257
	$('#bp-partner-quick').val('').focus();
258
	$('#bp-partner-suggest').hide().empty();
259
});
260
 
36681 ranu 261
function renderBaseLocationPicker() {
262
	var locs = state.baseLocations || [];
263
	if (locs.length === 0) return '';
264
	var activeId = state.activeBaseLocationId;
265
	if (locs.length === 1) {
266
		var only = locs[0];
267
		return '<div style="font-size:11px;color:#94a3b8;margin-bottom:10px;">'
268
			+ 'Starting from: <strong style="color:#e2e8f0;">' + (only.locationName || 'Home') + '</strong>'
269
			+ (only.isDefault ? ' <span style="color:#22c55e;">(default)</span>' : '')
270
			+ '</div>';
271
	}
272
	var html = '<div style="margin-bottom:10px;">'
273
		+ '<label style="font-size:11px;color:#94a3b8;display:block;margin-bottom:4px;">Start this beat from:</label>'
274
		+ '<select id="bp-base-picker" style="width:100%;padding:6px 8px;border:1px solid #475569;border-radius:4px;background:#0f172a;color:#e2e8f0;font-size:12px;font-family:inherit;">';
275
	locs.forEach(function (l) {
276
		var sel = (l.id === activeId) ? ' selected' : '';
277
		var label = (l.locationName || 'Location ' + l.id) + (l.isDefault ? ' (default)' : '');
278
		html += '<option value="' + l.id + '"' + sel + '>' + label + '</option>';
279
	});
280
	html += '</select></div>';
281
	return html;
282
}
283
 
284
$(document).on('change', '#bp-base-picker', function () {
285
	var id = parseInt($(this).val());
286
	if (!id) return;
287
	applyBaseLocation(id);
288
});
289
 
290
function applyBaseLocation(id) {
291
	var loc = (state.baseLocations || []).find(function (l) {
292
		return l.id === id;
293
	});
294
	if (!loc) return;
295
	state.homeLat = parseFloat(loc.latitude);
296
	state.homeLng = parseFloat(loc.longitude);
297
	state.homeName = loc.locationName;
298
	state.activeBaseLocationId = id;
299
 
300
	// Only day 1 starts from the chosen base; later days start where the
301
	// previous day ended (existing behavior unchanged).
302
	if (state.days && state.days.length > 0) {
303
		state.days[0].startLat = state.homeLat;
304
		state.days[0].startLng = state.homeLng;
305
		state.days[0].startName = state.homeName;
306
		if (typeof recalcDay === 'function') {
307
			var saved = state.currentDay;
308
			state.currentDay = 1;
309
			recalcDay();
310
			state.currentDay = saved;
311
		}
312
	}
313
 
314
	// Move the home marker on the map + recenter so the user actually sees it.
315
	if (typeof google !== 'undefined' && google.maps && map) {
316
		var pos = new google.maps.LatLng(state.homeLat, state.homeLng);
317
		if (homeMarker) {
318
			homeMarker.setPosition(pos);
319
			homeMarker.setTitle('Home: ' + state.homeName);
320
		} else {
321
			homeMarker = new google.maps.Marker({
322
				map: map, position: pos, title: 'Home: ' + state.homeName,
323
				icon: {
324
					url: 'https://maps.google.com/mapfiles/kml/shapes/homegardenbusiness.png',
325
					scaledSize: new google.maps.Size(32, 32)
326
				},
327
				zIndex: 100
328
			});
329
		}
330
		map.panTo(pos);
331
	}
332
 
333
	if (typeof drawRoute === 'function') drawRoute();
334
	if (typeof renderRoutePanel === 'function') renderRoutePanel();
335
	if (typeof updateBottomBar === 'function') updateBottomBar();
336
}
337
 
36632 ranu 338
// ============ BEAT LIST VIEW ============
339
function showBeatList() {
340
    state.mode = 'list';
341
    $('#bottom-bar').hide();
342
 
343
    // Load all beats for this user
344
    var now = new Date();
345
    var month = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
346
 
347
    $.ajax({
348
        url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
349
        data: {authUserId: state.authUserId, month: month},
350
        success: function (r) {
351
            var data = r.response || r;
352
            var beats = data.scheduledBeats || [];
353
 
354
            var html = '<div style="margin-bottom:10px;">';
355
            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>';
356
            html += '</div>';
357
 
358
            if (beats.length === 0) {
359
                html += '<p style="color:#64748b;font-size:12px;text-align:center;padding:20px;">No beats created yet.</p>';
360
            }
361
 
362
            // Deduplicate by beatName
363
            var seen = {};
364
            beats.forEach(function (b) {
365
                var key = b.beatName || b.planGroupId;
366
                if (!seen[key]) {
367
                    seen[key] = {beat: b, count: 0};
368
                }
369
                seen[key].count++;
370
            });
371
 
372
            Object.keys(seen).forEach(function (key) {
373
                var item = seen[key];
374
                var b = item.beat;
375
                var totalV = 0, totalKm = 0;
376
                b.days.forEach(function (d) {
377
                    totalV += d.visitCount || 0;
378
                    totalKm += d.totalKm || 0;
379
                });
380
 
381
                var statusCls = b.status === 'running' ? 'status-running' : b.status === 'completed' ? 'status-completed' : 'status-scheduled';
382
                var statusLabel = b.status === 'running' ? 'Live' : b.status === 'completed' ? 'Done' : b.status === 'unscheduled' ? 'Unscheduled' : 'Scheduled';
383
 
36651 ranu 384
				html += '<div class="beat-card beat-list-item" data-plangroup="' + b.planGroupId + '" style="cursor:pointer; position:relative;">';
36632 ranu 385
                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>';
386
                html += '<div class="beat-meta">' + b.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km';
387
                if (item.count > 1) html += ' | ' + item.count + 'x scheduled';
388
                html += '</div>';
36651 ranu 389
				html += '<button class="beat-edit-btn" data-plangroup="' + b.planGroupId + '" '
390
					+ 'style="position:absolute;top:8px;right:8px;background:#475569;color:#e2e8f0;'
391
					+ 'border:none;border-radius:4px;padding:3px 8px;font-size:10px;cursor:pointer;'
392
					+ 'font-family:inherit;">Edit</button>';
36632 ranu 393
                html += '</div>';
394
            });
395
 
396
            $('#route-list').html(html);
397
        }
398
    });
399
}
400
 
401
// Click on existing beat → view its route on map
402
$(document).on('click', '.beat-list-item', function () {
403
    var pg = $(this).data('plangroup');
404
    viewBeat(pg);
405
});
406
 
36651 ranu 407
// Edit button on a beat card → load in EDIT mode (modifiable)
408
$(document).on('click', '.beat-edit-btn', function (e) {
409
	e.stopPropagation(); // don't also trigger beat-list-item view
410
	var pg = $(this).data('plangroup');
411
	var date = $(this).data('date') || null;
412
	editBeat(String(pg), date ? String(date) : null);
413
});
414
 
415
// Loads a beat into edit mode — same UI as create but Finish becomes Save.
416
// When planDate is provided: leads scheduled on that date are loaded too and
417
// can be removed (template = partners stays the same across runs).
418
function editBeat(planGroupId, planDate) {
419
	state.mode = 'edit';
420
	state.editingBeatId = planGroupId;
421
	state.editingDate = planDate || null;
422
	state.viewDate = null;
423
	state.savedPlanGroupId = null;
424
	isSubmittingBeat = false;
425
 
426
	$.ajax({
427
		url: context + '/beatPlan/getPartners', type: 'GET', dataType: 'json',
428
		data: {
429
			authUserId: state.authUserId, categoryId: state.categoryId,
430
			startLat: state.homeLat, startLng: state.homeLng
431
		},
432
		success: function (r) {
433
			var pData = r.response || r;
434
			state.partners = pData.partners || [];
435
			var partnerMap = {};
436
			state.partners.forEach(function (p) {
437
				partnerMap[p.fofoId] = p;
438
			});
439
 
440
			$.ajax({
441
				url: context + '/beatPlan/calendar', type: 'GET', dataType: 'json',
442
				data: {authUserId: state.authUserId, month: '2020-01'},
443
				success: function (r2) {
444
					var calData = r2.response || r2;
445
					var beat = (calData.scheduledBeats || []).find(function (b) {
446
						return String(b.planGroupId) === String(planGroupId);
447
					});
448
					if (!beat) {
449
						alert('Beat not found');
450
						return;
451
					}
452
					state.beatTitle = beat.beatName || 'Beat';
453
 
454
					// Pass planDate to getBeatVisits so leads for that date are included
455
					var visitsData = planDate
456
						? {planGroupId: planGroupId, planDate: planDate}
457
						: {planGroupId: planGroupId};
458
 
459
					$.ajax({
460
						url: context + '/beatPlan/getBeatVisits', type: 'GET', dataType: 'json',
461
						data: visitsData,
462
						success: function (r3) {
463
							var visits = (r3.response || r3) || [];
464
							var dayVisitsMap = {};
465
							visits.forEach(function (v) {
466
								if (!dayVisitsMap[v.dayNumber]) dayVisitsMap[v.dayNumber] = [];
467
								dayVisitsMap[v.dayNumber].push(v);
468
							});
469
 
470
							// Collect lead IDs so we can fetch their name/geo in parallel
471
							var leadIdsToFetch = [];
472
							visits.forEach(function (v) {
473
								if (v.visitType === 'lead') leadIdsToFetch.push(v.fofoId);
474
							});
475
 
476
							state.days = [];
477
							var seen = {};
478
							beat.days.forEach(function (d) {
479
								if (seen[d.dayNumber]) return;
480
								seen[d.dayNumber] = true;
481
								var dayVisits = (dayVisitsMap[d.dayNumber] || []).sort(function (a, b) {
482
									return a.sequenceOrder - b.sequenceOrder;
483
								});
484
								var builtVisits = [];
485
								dayVisits.forEach(function (v) {
486
									if (v.visitType === 'lead') {
487
										builtVisits.push({
488
											id: v.fofoId, type: 'lead',
489
											name: 'LEAD #' + v.fofoId, code: 'LEAD',
490
											lat: null, lng: null, isLead: true
491
										});
36811 ranu 492
									} else if (v.visitType === 'office') {
493
										// Office stops are enriched server-side because the partner
494
										// map doesn't cover them. Lat/lng come from logistics.company_office.
495
										builtVisits.push({
496
											id: v.fofoId, type: 'office',
497
											name: (v.code ? v.code + ' - ' : '') + (v.name || 'Office #' + v.fofoId),
498
											code: v.code || 'OFFICE',
499
											lat: v.latitude ? parseFloat(v.latitude) : null,
500
											lng: v.longitude ? parseFloat(v.longitude) : null,
501
											isOffice: true
502
										});
36651 ranu 503
									} else {
504
										var p = partnerMap[v.fofoId];
505
										if (p) builtVisits.push({
506
											id: p.fofoId, type: 'partner',
507
											name: p.code + ' - ' + (p.outletName || p.businessName || ''),
508
											code: p.code,
509
											lat: p.latitude ? parseFloat(p.latitude) : null,
510
											lng: p.longitude ? parseFloat(p.longitude) : null
511
										});
512
									}
513
								});
37060 vikas 514
								// Day 1 starts at the beat's own saved start location when set;
515
								// otherwise fall back to the user's default base. Later days
516
								// start where the prior day ended (handled by the Day Break flow).
517
								var dayStartLat = state.homeLat, dayStartLng = state.homeLng, dayStartName = state.homeName;
518
								if (d.dayNumber === 1) {
519
									var bLat = beat.startLatitude != null ? parseFloat(beat.startLatitude) : NaN;
520
									var bLng = beat.startLongitude != null ? parseFloat(beat.startLongitude) : NaN;
521
									if (!isNaN(bLat) && !isNaN(bLng)) {
522
										dayStartLat = bLat;
523
										dayStartLng = bLng;
524
										dayStartName = beat.startLocationName || state.homeName;
525
									}
526
								}
36651 ranu 527
								state.days.push({
528
									dayNumber: d.dayNumber,
37060 vikas 529
									startLat: dayStartLat, startLng: dayStartLng,
530
									startName: dayStartName,
36651 ranu 531
									visits: builtVisits,
36711 ranu 532
									endAction: d.endAction || (d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME'),
36651 ranu 533
									totalKm: 0, totalMins: 0
534
								});
535
							});
536
							state.currentDay = 1;
537
 
36681 ranu 538
							// Snapshot the originals — used during save to detect day-count
539
							// increases (blocked) and lead removals (popup with cancel/reschedule).
540
							state.originalDayCount = state.days.length;
541
							state.originalLeads = [];
542
							state.days.forEach(function (d) {
543
								d.visits.forEach(function (v) {
544
									if (v.type === 'lead' || v.isLead) {
545
										state.originalLeads.push({
546
											leadId: v.id,
547
											dayNumber: d.dayNumber,
548
											name: v.name || ('Lead #' + v.id)
549
										});
550
									}
551
								});
552
							});
553
 
36651 ranu 554
							// Async enrich each lead with name + geo
555
							var leadPromises = [];
556
							state.days.forEach(function (day) {
557
								day.visits.forEach(function (v) {
558
									if (!v.isLead) return;
559
									leadPromises.push(
560
										$.get(context + '/lead-geo/check/' + v.id).then(function (rr) {
561
											var g = rr.response || rr;
562
											if (g && g.hasApprovedGeo) {
563
												v.lat = g.latitude;
564
												v.lng = g.longitude;
565
											}
566
										}),
567
										$.get(context + '/getLead?leadId=' + v.id).then(function (rr) {
568
											var l = rr.response || rr;
36681 ranu 569
											if (l && l.firstName) {
570
												var nm = 'LEAD - ' + l.firstName + ' ' + (l.lastName || '');
571
												v.name = nm;
572
												// Keep the originalLeads snapshot label in sync so the
573
												// removed-leads popup shows the real name, not 'LEAD #123'.
574
												(state.originalLeads || []).forEach(function (ol) {
575
													if (ol.leadId === v.id) ol.name = nm;
576
												});
577
											}
36651 ranu 578
										})
579
									);
580
								});
581
							});
582
 
583
							$.when.apply($, leadPromises).always(function () {
36681 ranu 584
								// Pre-select whichever saved base location matches the beat's
585
								// existing day-1 start (lat/lng), so the picker reflects current state.
586
								var day1 = state.days[0];
587
								if (day1 && state.baseLocations) {
588
									var match = state.baseLocations.find(function (l) {
589
										return Math.abs(parseFloat(l.latitude) - day1.startLat) < 1e-4
590
											&& Math.abs(parseFloat(l.longitude) - day1.startLng) < 1e-4;
591
									});
592
									if (match) state.activeBaseLocationId = match.id;
593
								}
594
 
36700 ranu 595
								// renderRoutePanel now builds the edit header (back btn, EDITING
596
								// title, date label, base picker, partner search) inline — so we
597
								// just call it without the one-time prepend that used to live here.
36651 ranu 598
								renderRoutePanel();
599
 
600
								initMap();
601
								// Add lead markers (orange) on the map
602
								state.days.forEach(function (day) {
603
									day.visits.forEach(function (v) {
604
										if (v.isLead && v.lat && v.lng) {
605
											var m = new google.maps.Marker({
606
												map: map,
607
												position: {lat: v.lat, lng: v.lng},
608
												title: v.name,
609
												label: {text: 'L', color: '#fff', fontSize: '11px', fontWeight: '700'},
610
												icon: {
611
													path: google.maps.SymbolPath.CIRCLE, scale: 16,
612
													fillColor: '#e67e22', fillOpacity: 0.95,
613
													strokeColor: '#fff', strokeWeight: 2
614
												}
615
											});
616
											markers[v.id] = m;
617
										}
618
									});
619
								});
620
								resetMarkerColors();
621
								drawRoute();
622
								updateBottomBar();
623
								$('#bottom-bar').show();        // make Save bar visible
624
								$('#btn-finish').text('Save Changes').prop('disabled', false);
625
							});
626
						}
627
					});
628
				}
629
			});
630
		}
631
	});
632
}
633
 
36644 ranu 634
function viewBeat(planGroupId, planDate) {
36632 ranu 635
    state.mode = 'view';
36644 ranu 636
	state.viewDate = planDate || null; // when set, show that run's leads
36632 ranu 637
    $('#bottom-bar').hide();
638
 
639
    // Load partners first (for coordinates), then load beat visits
640
    $.ajax({
641
        url: context + "/beatPlan/getPartners", type: "GET", dataType: "json",
642
        data: {
643
            authUserId: state.authUserId,
644
            categoryId: state.categoryId,
645
            startLat: state.homeLat,
646
            startLng: state.homeLng
647
        },
648
        success: function (r) {
649
            var pData = r.response || r;
650
            state.partners = pData.partners || [];
651
 
652
            // Build partner lookup by fofoId
653
            var partnerMap = {};
654
            state.partners.forEach(function (p) {
655
                partnerMap[p.fofoId] = p;
656
            });
657
 
658
            // Load beat visits
659
            $.ajax({
660
                url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
661
                data: {authUserId: state.authUserId, month: '2020-01'},
662
                success: function (r2) {
663
                    var calData = r2.response || r2;
664
                    var beat = (calData.scheduledBeats || []).find(function (b) {
36644 ranu 665
						return String(b.planGroupId) === String(planGroupId);
36632 ranu 666
                    });
667
                    if (!beat) {
668
                        alert('Beat not found');
669
                        return;
670
                    }
671
 
672
                    // Reconstruct state.days from beat data for map drawing
673
                    // We need actual visit fofoIds — fetch from beat_plan records
674
                    $.ajax({
675
                        url: context + "/beatPlan/getBeatVisits", type: "GET", dataType: "json",
36644 ranu 676
						data: planDate
677
							? {planGroupId: planGroupId, planDate: planDate}
678
							: {planGroupId: planGroupId},
36632 ranu 679
                        success: function (r3) {
680
                            var visits = (r3.response || r3) || [];
681
 
682
                            // Group visits by dayNumber
683
                            var dayVisitsMap = {};
684
                            visits.forEach(function (v) {
685
                                if (!dayVisitsMap[v.dayNumber]) dayVisitsMap[v.dayNumber] = [];
686
                                dayVisitsMap[v.dayNumber].push(v);
687
                            });
688
 
36644 ranu 689
							// Build days for route display.
690
							// A beat scheduled on multiple dates has multiple schedule
691
							// rows per day_number — dedupe so each day appears once.
36632 ranu 692
                            state.days = [];
36644 ranu 693
							var seenDayNumbers = {};
36632 ranu 694
                            beat.days.forEach(function (d) {
36644 ranu 695
								if (seenDayNumbers[d.dayNumber]) return; // skip duplicate day
696
								seenDayNumbers[d.dayNumber] = true;
697
 
36632 ranu 698
                                var dayVisits = (dayVisitsMap[d.dayNumber] || []).sort(function (a, b) {
699
                                    return a.sequenceOrder - b.sequenceOrder;
700
                                });
701
                                var startLat = state.homeLat, startLng = state.homeLng, startName = state.homeName;
702
 
703
                                // If not first day, start from previous day's last visit
704
                                if (state.days.length > 0) {
705
                                    var prevDay = state.days[state.days.length - 1];
706
                                    if (prevDay.visits.length > 0 && prevDay.endAction === 'DAYBREAK') {
707
                                        var lastV = prevDay.visits[prevDay.visits.length - 1];
708
                                        startLat = lastV.lat;
709
                                        startLng = lastV.lng;
710
                                        startName = lastV.code || lastV.name;
711
                                    }
712
                                }
713
 
714
                                var builtVisits = [];
715
                                dayVisits.forEach(function (v) {
36642 ranu 716
                                    if (v.visitType === 'lead') {
36644 ranu 717
										builtVisits.push({
36642 ranu 718
                                            id: v.fofoId, type: 'lead',
719
                                            name: 'LEAD #' + v.fofoId,
720
                                            code: 'LEAD',
721
                                            lat: null, lng: null,
722
                                            isLead: true
36632 ranu 723
                                        });
36811 ranu 724
									} else if (v.visitType === 'office') {
725
										builtVisits.push({
726
											id: v.fofoId, type: 'office',
727
											name: (v.code ? v.code + ' - ' : '') + (v.name || 'Office #' + v.fofoId),
728
											code: v.code || 'OFFICE',
729
											lat: v.latitude ? parseFloat(v.latitude) : null,
730
											lng: v.longitude ? parseFloat(v.longitude) : null,
731
											isOffice: true
732
										});
36642 ranu 733
                                    } else {
734
                                        var p = partnerMap[v.fofoId];
735
                                        if (p) {
736
                                            builtVisits.push({
737
                                                id: p.fofoId, type: 'partner',
738
                                                name: p.code + ' - ' + (p.outletName || p.businessName || ''),
739
                                                code: p.code,
740
                                                lat: p.latitude ? parseFloat(p.latitude) : null,
741
                                                lng: p.longitude ? parseFloat(p.longitude) : null
742
                                            });
743
                                        }
36632 ranu 744
                                    }
745
                                });
746
 
747
                                state.days.push({
748
                                    dayNumber: d.dayNumber,
749
                                    startLat: startLat,
750
                                    startLng: startLng,
751
                                    startName: startName,
752
                                    visits: builtVisits,
36711 ranu 753
									// Use the actual end_action stored on this schedule row.
754
									// The dayNumber-based inference is wrong for multi-instance
755
									// beats (same beat scheduled on several dates) — every
756
									// instance would have day N but only one was treated HOME.
757
									endAction: d.endAction || (d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME'),
36632 ranu 758
                                    totalKm: d.totalKm || 0,
759
                                    totalMins: d.totalMins || 0
760
                                });
761
                            });
762
 
763
                            state.currentDay = 1;
764
 
36644 ranu 765
							// Collect all lead data promises across all days
766
							var allLeadPromises = [];
767
							state.days.forEach(function (d) {
768
								d.visits.forEach(function (v) {
769
									if (v.isLead) {
770
										allLeadPromises.push(
771
											$.get(context + '/lead-geo/check/' + v.id).then(function (resp) {
772
												var geoData = resp.response || resp;
773
												if (geoData && geoData.hasApprovedGeo) {
774
													v.lat = geoData.latitude;
775
													v.lng = geoData.longitude;
776
												}
777
											})
778
										);
779
										allLeadPromises.push(
780
											$.get(context + '/getLead?leadId=' + v.id).then(function (resp) {
781
												var lead = resp.response || resp;
782
												if (lead && lead.firstName) {
783
													v.name = 'LEAD - ' + lead.firstName + ' ' + (lead.lastName || '');
784
												}
785
											})
786
										);
787
									}
788
								});
789
							});
36632 ranu 790
 
36644 ranu 791
							// Wait for all lead data, then render
792
							$.when.apply($, allLeadPromises).always(function () {
793
								// Render left panel
794
								state.mode = 'view';
795
								renderRoutePanel();
796
								var dateLabel = state.viewDate
797
									? '<div style="font-size:11px;color:#f59e0b;margin-bottom:8px;">Run date: ' + state.viewDate + ' (showing this day\'s leads)</div>'
798
									: '<div style="font-size:11px;color:#94a3b8;margin-bottom:8px;">Beat template (partners only)</div>';
799
								$('#route-list').prepend('<div style="margin-bottom:8px;"><button class="btn-back-to-list" style="font-size:11px;padding:4px 10px;border:1px solid #475569;border-radius:4px;background:none;color:#94a3b8;cursor:pointer;font-family:inherit;">← Back to list</button></div><div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:4px;">' + beat.beatName + '</div>' + dateLabel);
36632 ranu 800
 
36644 ranu 801
								// Init map with all partners + lead markers
802
								initMap();
803
								state.days.forEach(function (d) {
804
									d.visits.forEach(function (v) {
805
										if (v.isLead && v.lat && v.lng) {
806
											var m = new google.maps.Marker({
807
												map: map,
808
												position: {lat: v.lat, lng: v.lng},
809
												title: v.name,
810
												label: {text: 'L', color: '#fff', fontSize: '11px', fontWeight: '700'},
811
												icon: {
812
													path: google.maps.SymbolPath.CIRCLE,
813
													scale: 16,
814
													fillColor: '#e67e22',
815
													fillOpacity: 0.95,
816
													strokeColor: '#fff',
817
													strokeWeight: 2
818
												}
819
											});
820
											markers[v.id] = m;
821
										}
822
									});
823
								});
824
								resetMarkerColors();
825
								drawRoute();
826
							});
36632 ranu 827
                        }
828
                    });
829
                }
830
            });
831
        }
832
    });
833
}
834
 
835
// Back to list
836
$(document).on('click', '.btn-back-to-list', function () {
837
    showBeatList();
838
    // Clear map
839
    if (map) {
840
        for (var key in markers) {
841
            markers[key].setMap(null);
842
        }
843
        markers = {};
844
        clearRoute();
845
    }
846
});
847
 
848
// New Beat button
849
$(document).on('click', '#btn-new-beat', function () {
850
    var beatTitle = prompt('Enter beat name:', '');
851
    if (!beatTitle || !beatTitle.trim()) return;
852
 
853
    state.beatTitle = beatTitle.trim();
854
    state.mode = 'create';
36644 ranu 855
	state.savedPlanGroupId = null; // reset — this is a fresh beat
856
	state.days = [];               // clear any leftover route data
857
	isSubmittingBeat = false;
36632 ranu 858
 
859
    if (!state.homeLat) {
860
        showHomeModal();
861
    } else {
862
        loadPartners();
863
    }
864
});
865
 
36621 ranu 866
// ============ HOME LOCATION MODAL ============
867
var homeMap, homeMapMarker;
868
 
869
function showHomeModal() {
870
	$('#modal-home').addClass('show');
871
	setTimeout(function () {
872
		if (!homeMap) {
873
			homeMap = new google.maps.Map(document.getElementById('home-map'), {
874
				center: {lat: 28.6, lng: 77.2},
875
				zoom: 6
876
			});
877
			var ac = new google.maps.places.Autocomplete(document.getElementById('home-search'), {componentRestrictions: {country: 'in'}});
878
			homeMapMarker = new google.maps.Marker({map: homeMap, draggable: true});
879
 
880
			ac.addListener('place_changed', function () {
881
				var p = ac.getPlace();
882
				if (!p.geometry) return;
883
				homeMap.setCenter(p.geometry.location);
884
				homeMap.setZoom(14);
885
				homeMapMarker.setPosition(p.geometry.location);
886
				state.homeLat = p.geometry.location.lat();
887
				state.homeLng = p.geometry.location.lng();
888
				state.homeName = p.name || p.formatted_address || '';
889
			});
890
			homeMapMarker.addListener('dragend', function () {
891
				state.homeLat = homeMapMarker.getPosition().lat();
892
				state.homeLng = homeMapMarker.getPosition().lng();
893
			});
894
			homeMap.addListener('click', function (e) {
895
				homeMapMarker.setPosition(e.latLng);
896
				state.homeLat = e.latLng.lat();
897
				state.homeLng = e.latLng.lng();
898
			});
899
		}
900
	}, 200);
901
}
902
 
903
$('#home-cancel').on('click', function () {
904
	$('#modal-home').removeClass('show');
905
});
906
$('#home-confirm').on('click', function () {
907
	if (!state.homeLat) {
908
		alert('Please select a location');
909
		return;
910
	}
911
	$('#modal-home').removeClass('show');
912
	$.ajax({
913
		url: context + "/beatPlan/saveBaseLocation", type: "POST",
914
		data: {
915
			authUserId: state.authUserId,
916
			locationName: state.homeName || 'Home',
917
			latitude: state.homeLat,
918
			longitude: state.homeLng,
919
			address: $('#home-search').val()
920
		}
921
	});
922
	loadPartners();
923
});
924
 
925
// ============ LOAD PARTNERS + INIT MAP ============
926
function loadPartners() {
927
	$.ajax({
928
		url: context + "/beatPlan/getPartners", type: "GET", dataType: "json",
929
		data: {
930
			authUserId: state.authUserId,
931
			categoryId: state.categoryId,
932
			startLat: state.homeLat,
933
			startLng: state.homeLng
934
		},
935
		success: function (r) {
936
			var data = r.response || r;
937
			state.partners = data.partners || [];
938
			initDay1();
939
			initMap();
940
		}
941
	});
942
}
943
 
944
function initDay1() {
945
	state.days = [{
946
		dayNumber: 1, startLat: state.homeLat, startLng: state.homeLng, startName: state.homeName,
947
		visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
948
	}];
949
	state.currentDay = 1;
36651 ranu 950
	if (state.mode === 'create' || state.mode === 'edit') $('#bottom-bar').show();
36621 ranu 951
	renderRoutePanel();
952
}
953
 
954
function initMap() {
955
	var center = {lat: state.homeLat, lng: state.homeLng};
956
	map = new google.maps.Map(document.getElementById('beat-map'), {
957
		center: center, zoom: 9,
958
		styles: [
959
			{elementType: 'geometry', stylers: [{color: '#1d2c4d'}]},
960
			{elementType: 'labels.text.fill', stylers: [{color: '#8ec3b9'}]},
961
			{elementType: 'labels.text.stroke', stylers: [{color: '#1a3646'}]},
962
			{featureType: 'road', elementType: 'geometry', stylers: [{color: '#304a7d'}]},
963
			{featureType: 'water', elementType: 'geometry', stylers: [{color: '#0e1626'}]}
964
		]
965
	});
966
	infoWindow = new google.maps.InfoWindow();
967
 
968
	// Home marker
969
	homeMarker = new google.maps.Marker({
970
		map: map, position: center, title: 'Home: ' + state.homeName,
971
		icon: {
972
			url: 'https://maps.google.com/mapfiles/kml/shapes/homegardenbusiness.png',
973
			scaledSize: new google.maps.Size(32, 32)
974
		},
975
		zIndex: 100
976
	});
977
 
978
	// Partner markers with name labels
979
	for (var key in markers) {
980
		markers[key].setMap(null);
981
	}
982
	markers = {};
983
 
984
	state.partners.forEach(function (p) {
985
		if (!p.latitude || !p.longitude) return;
986
		var lat = parseFloat(p.latitude), lng = parseFloat(p.longitude);
987
		if (isNaN(lat) || isNaN(lng)) return;
988
 
989
		var m = new google.maps.Marker({
990
			map: map, position: {lat: lat, lng: lng},
991
			title: p.code + ' - ' + (p.outletName || p.businessName || ''),
992
			label: {text: p.code || '', color: '#fff', fontSize: '10px', fontWeight: '600'},
993
			icon: {
994
				path: google.maps.SymbolPath.CIRCLE,
995
				scale: 14,
996
				fillColor: '#ef4444',
997
				fillOpacity: 0.9,
998
				strokeColor: '#fff',
999
				strokeWeight: 1
1000
			}
1001
		});
1002
 
1003
		m.addListener('click', function () {
1004
			showMarkerPopup(p, m);
1005
		});
1006
		m.addListener('mouseover', function () {
1007
			showPreviewLine(p);
1008
		});
1009
		m.addListener('mouseout', function () {
1010
			hidePreviewLine();
1011
		});
1012
 
1013
		markers[p.fofoId] = m;
1014
	});
1015
 
1016
	fitBounds();
1017
}
1018
 
1019
// ============ HOVER PREVIEW ============
1020
function showPreviewLine(partner) {
1021
	if (!partner.latitude || !partner.longitude) return;
1022
	var day = curDay();
1023
	var lastPt = day.visits.length > 0
1024
		? {lat: day.visits[day.visits.length - 1].lat, lng: day.visits[day.visits.length - 1].lng}
1025
		: {lat: day.startLat, lng: day.startLng};
1026
	if (!lastPt.lat) return;
1027
 
1028
	var endPt = {lat: parseFloat(partner.latitude), lng: parseFloat(partner.longitude)};
1029
	if (previewLine) previewLine.setMap(null);
1030
	previewLine = new google.maps.Polyline({
1031
		path: [lastPt, endPt],
1032
		geodesic: true,
1033
		strokeColor: '#60a5fa',
1034
		strokeOpacity: 0.5,
1035
		strokeWeight: 2,
1036
		strokePattern: [10, 5],
1037
		icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
1038
		map: map
1039
	});
1040
}
1041
 
1042
function hidePreviewLine() {
1043
	if (previewLine) {
1044
		previewLine.setMap(null);
1045
		previewLine = null;
1046
	}
1047
}
1048
 
1049
// ============ MARKER CLICK POPUP ============
1050
function showMarkerPopup(partner, marker) {
1051
	var day = curDay();
1052
	var alreadyAdded = day.visits.some(function (v) {
1053
		return v.id === partner.fofoId;
1054
	});
1055
	if (alreadyAdded) {
1056
		infoWindow.setContent('<div class="marker-popup"><h4>' + partner.code + '</h4><p>Already in today\'s route</p></div>');
1057
		infoWindow.open(map, marker);
1058
		return;
1059
	}
1060
 
1061
	var lastPt = day.visits.length > 0
1062
		? {lat: day.visits[day.visits.length - 1].lat, lng: day.visits[day.visits.length - 1].lng}
1063
		: {lat: day.startLat, lng: day.startLng};
1064
 
1065
	var dist = 0, travelMins = 0;
1066
	if (lastPt.lat && partner.latitude) {
1067
		dist = haversine(lastPt.lat, lastPt.lng, parseFloat(partner.latitude), parseFloat(partner.longitude)) * ROAD_FACTOR;
1068
		travelMins = (dist / AVG_SPEED) * 60;
1069
	}
1070
 
1071
	var name = partner.outletName || partner.businessName || '';
1072
	var addr = partner.address || '';
1073
 
1074
	var html = '<div class="marker-popup">';
1075
	html += '<h4>' + partner.code + ' - ' + name + '</h4>';
1076
	html += '<div class="popup-meta">' + addr + '</div>';
36711 ranu 1077
	html += '<div class="popup-distance">~' + dist.toFixed(1) + ' km | ~' + fmtMins(travelMins) + ' travel + ' + VISIT_MINS + ' Minutes Discussion Time</div>';
36621 ranu 1078
	html += '<button class="popup-next" onclick="addVisit(' + partner.fofoId + ',\'next\')">Next Visit</button>';
1079
	html += '<button class="popup-last" onclick="addVisit(' + partner.fofoId + ',\'last\')">Last Visit</button>';
1080
	html += '</div>';
1081
 
1082
	infoWindow.setContent(html);
1083
	infoWindow.open(map, marker);
1084
}
1085
 
1086
// ============ ADD VISIT ============
1087
function addVisit(fofoId, action) {
1088
	infoWindow.close();
1089
	var p = state.partners.find(function (x) {
1090
		return x.fofoId === fofoId;
1091
	});
1092
	if (!p) return;
1093
 
1094
	var day = curDay();
1095
	var visit = {
1096
		id: p.fofoId, type: 'partner',
1097
		name: p.code + ' - ' + (p.outletName || p.businessName || ''),
1098
		code: p.code,
1099
		lat: p.latitude ? parseFloat(p.latitude) : null,
1100
		lng: p.longitude ? parseFloat(p.longitude) : null
1101
	};
1102
 
1103
	day.visits.push(visit);
1104
 
37060 vikas 1105
	// Auto-order the day into a nearest-neighbor route. The just-added stop may
1106
	// no longer be last, so recolour + renumber every current-day marker by its
1107
	// new index (resetMarkerColors handles both) rather than labelling this one.
1108
	sortDayByNearestNeighbor(day);
1109
	resetMarkerColors();
36621 ranu 1110
 
1111
	recalcDay();
1112
	drawRoute();
1113
	renderRoutePanel();
1114
 
1115
	if (action === 'last') {
1116
		// Last visit — end day, go home
1117
		day.endAction = 'HOME';
1118
		day.stayLat = state.homeLat;
1119
		day.stayLng = state.homeLng;
1120
		day.stayName = state.homeName;
1121
		drawReturnHome();
1122
		renderRoutePanel();
1123
	} else if (action === 'nextday') {
1124
		// Continue next day — end current day, start new day from here
1125
		day.endAction = 'CONTINUE';
1126
		day.stayLat = visit.lat;
1127
		day.stayLng = visit.lng;
1128
		day.stayName = visit.name;
1129
		startNextDay(visit.lat, visit.lng, visit.name);
1130
	}
1131
	// 'next' — just added, continue on same day
1132
}
1133
 
1134
function drawReturnHome() {
1135
	var day = curDay();
1136
	if (day.visits.length === 0) return;
1137
	var lastVisit = day.visits[day.visits.length - 1];
1138
	if (!lastVisit.lat || !state.homeLat) return;
1139
 
1140
	var line = new google.maps.Polyline({
1141
		path: [{lat: lastVisit.lat, lng: lastVisit.lng}, {lat: state.homeLat, lng: state.homeLng}],
1142
		geodesic: true, strokeColor: '#22c55e', strokeOpacity: 0.6, strokeWeight: 2,
1143
		icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
1144
		map: map
1145
	});
1146
	routeLines.push(line);
1147
 
1148
	// Add return distance to day total
1149
	var retDist = haversine(lastVisit.lat, lastVisit.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
1150
	day.totalKm += retDist;
1151
	day.totalMins += (retDist / AVG_SPEED) * 60;
1152
	updateBottomBar();
1153
}
1154
 
1155
function startNextDay(startLat, startLng, startName) {
1156
	var nextNum = state.days.length + 1;
1157
	state.days.push({
1158
		dayNumber: nextNum, startLat: startLat, startLng: startLng, startName: startName,
1159
		visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
1160
	});
1161
	state.currentDay = nextNum;
1162
	updateBottomBar();
1163
	renderRoutePanel();
1164
	// Reset marker colors for unvisited
1165
	resetMarkerColors();
1166
}
1167
 
1168
function resetMarkerColors() {
1169
	var allVisited = {};
1170
	state.days.forEach(function (d) {
1171
		d.visits.forEach(function (v) {
1172
			allVisited[v.id] = true;
1173
		});
1174
	});
1175
 
1176
	state.partners.forEach(function (p) {
1177
		if (!markers[p.fofoId]) return;
1178
		if (allVisited[p.fofoId]) {
1179
			markers[p.fofoId].setIcon({
1180
				path: google.maps.SymbolPath.CIRCLE,
1181
				scale: 14,
1182
				fillColor: '#22c55e',
1183
				fillOpacity: 0.9,
1184
				strokeColor: '#fff',
1185
				strokeWeight: 1
1186
			});
1187
		} else {
1188
			markers[p.fofoId].setIcon({
1189
				path: google.maps.SymbolPath.CIRCLE,
1190
				scale: 14,
1191
				fillColor: '#ef4444',
1192
				fillOpacity: 0.9,
1193
				strokeColor: '#fff',
1194
				strokeWeight: 1
1195
			});
1196
			markers[p.fofoId].setLabel({text: p.code || '', color: '#fff', fontSize: '10px', fontWeight: '600'});
1197
		}
1198
	});
1199
 
1200
	// Re-label current day visits
1201
	var day = curDay();
1202
	day.visits.forEach(function (v, i) {
1203
		if (markers[v.id]) markers[v.id].setLabel({
1204
			text: '' + (i + 1),
1205
			color: '#fff',
1206
			fontSize: '11px',
1207
			fontWeight: '700'
1208
		});
1209
	});
1210
}
1211
 
1212
// ============ ROUTE DRAWING ============
1213
function clearRoute() {
1214
	routeLines.forEach(function (l) {
1215
		l.setMap(null);
1216
	});
1217
	routeLines = [];
1218
	distLabels.forEach(function (l) {
1219
		l.setMap(null);
1220
	});
1221
	distLabels = [];
1222
}
1223
 
36632 ranu 1224
var DAY_ROUTE_COLORS = ['#3b82f6', '#a855f7', '#06b6d4', '#ec4899', '#f59e0b', '#14b8a6'];
1225
 
36621 ranu 1226
function drawRoute() {
1227
	clearRoute();
1228
 
36632 ranu 1229
    // Draw routes for ALL days, not just current
1230
    state.days.forEach(function (day, dayIdx) {
1231
        var isCurrent = day.dayNumber === state.currentDay;
1232
        var routeColor = DAY_ROUTE_COLORS[dayIdx % DAY_ROUTE_COLORS.length];
1233
        var opacity = isCurrent ? 0.9 : 0.5;
1234
        var weight = isCurrent ? 3 : 2;
36621 ranu 1235
 
36632 ranu 1236
        var pts = [{lat: day.startLat, lng: day.startLng}];
1237
        day.visits.forEach(function (v) {
1238
            if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
1239
        });
1240
 
1241
        // Draw visit route lines for this day
1242
        for (var i = 0; i < pts.length - 1; i++) {
1243
            var line = new google.maps.Polyline({
1244
                path: [pts[i], pts[i + 1]], geodesic: true,
1245
                strokeColor: routeColor, strokeOpacity: opacity, strokeWeight: weight, map: map
1246
            });
1247
            routeLines.push(line);
1248
 
1249
            // Only show distance labels on current day
1250
            if (isCurrent) {
1251
                var d = haversine(pts[i].lat, pts[i].lng, pts[i + 1].lat, pts[i + 1].lng) * ROAD_FACTOR;
1252
                var mid = {lat: (pts[i].lat + pts[i + 1].lat) / 2, lng: (pts[i].lng + pts[i + 1].lng) / 2};
1253
                var lbl = new google.maps.Marker({
1254
                    position: mid, map: map, icon: {path: google.maps.SymbolPath.CIRCLE, scale: 0},
1255
                    label: {text: d.toFixed(1) + 'km', color: '#93c5fd', fontSize: '10px', fontWeight: '600'}
1256
                });
1257
                distLabels.push(lbl);
1258
            }
1259
        }
1260
 
1261
        // Draw return-to-home line (not on DAYBREAK days)
1262
        if (day.visits.length > 0 && day.endAction !== 'DAYBREAK') {
1263
            var lastV = day.visits[day.visits.length - 1];
1264
            if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
1265
                var retLine = new google.maps.Polyline({
1266
                    path: [{lat: lastV.lat, lng: lastV.lng}, {lat: state.homeLat, lng: state.homeLng}],
1267
                    geodesic: true, strokeColor: '#22c55e', strokeOpacity: isCurrent ? 0.4 : 0.2, strokeWeight: 2,
1268
                    icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
1269
                    map: map
1270
                });
1271
                routeLines.push(retLine);
1272
            }
1273
        }
1274
    });
36621 ranu 1275
}
1276
 
1277
function recalcDay() {
1278
	var day = curDay();
1279
	var pts = [{lat: day.startLat, lng: day.startLng}];
1280
	day.visits.forEach(function (v) {
1281
		if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
1282
	});
1283
 
1284
	var km = 0;
1285
	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;
1286
	day.totalKm = km;
1287
	day.totalMins = (km / AVG_SPEED) * 60 + day.visits.length * VISIT_MINS;
1288
	updateBottomBar();
1289
}
1290
 
37060 vikas 1291
// Auto-order a day's stops into a nearest-neighbor route from the day's start
1292
// point (beat start / base for day 1, prior day's end for later days). Reorders
1293
// day.visits in place; sequence = array index downstream. Stops without valid
1294
// coords (e.g. leads with no captured geo) are parked at the end in their
1295
// existing relative order rather than dropped.
1296
function sortDayByNearestNeighbor(day) {
1297
	if (!day || !day.visits || day.visits.length < 2) return;
1298
 
1299
	var routable = [], parked = [];
1300
	day.visits.forEach(function (v) {
1301
		if (typeof v.lat === 'number' && typeof v.lng === 'number' && !isNaN(v.lat) && !isNaN(v.lng)) routable.push(v);
1302
		else parked.push(v);
1303
	});
1304
 
1305
	var haveStart = (typeof day.startLat === 'number' && typeof day.startLng === 'number' && !isNaN(day.startLat) && !isNaN(day.startLng));
1306
	var ordered = [], curLat, curLng;
1307
	if (haveStart) {
1308
		curLat = day.startLat;
1309
		curLng = day.startLng;
1310
	} else if (routable.length > 0) {
1311
		var first = routable.shift();
1312
		ordered.push(first);
1313
		curLat = first.lat;
1314
		curLng = first.lng;
1315
	}
1316
 
1317
	while (routable.length > 0) {
1318
		var bestIdx = 0, bestD = Infinity;
1319
		for (var i = 0; i < routable.length; i++) {
1320
			var d = haversine(curLat, curLng, routable[i].lat, routable[i].lng);
1321
			if (d < bestD) { bestD = d; bestIdx = i; }
1322
		}
1323
		var next = routable.splice(bestIdx, 1)[0];
1324
		ordered.push(next);
1325
		curLat = next.lat;
1326
		curLng = next.lng;
1327
	}
1328
 
1329
	day.visits.length = 0;
1330
	ordered.forEach(function (v) { day.visits.push(v); });
1331
	parked.forEach(function (v) { day.visits.push(v); });
1332
}
1333
 
36621 ranu 1334
function updateBottomBar() {
1335
	var day = curDay();
1336
	$('#bb-day-label').text('Day ' + day.dayNumber);
1337
	$('#bb-stops').text(day.visits.length + ' stops');
1338
	$('#bb-distance').text(day.totalKm.toFixed(1) + ' km');
1339
	$('#bb-time').text(fmtMins(day.totalMins));
1340
 
1341
	var pct = Math.min((day.totalMins / DAY_LIMIT) * 100, 100);
1342
	var col = pct < 78 ? '#22c55e' : pct < 89 ? '#f59e0b' : '#ef4444';
1343
	$('#bb-progress').css({width: pct + '%', background: col});
1344
}
1345
 
1346
// ============ ROUTE PANEL ============
1347
function renderRoutePanel() {
1348
	var html = '';
36632 ranu 1349
    if (state.mode === 'create' && state.beatTitle) {
1350
        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>';
1351
        html += '<div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:10px;">' + state.beatTitle + '</div>';
36681 ranu 1352
		html += renderBaseLocationPicker();
36698 ranu 1353
		html += renderPartnerQuickAdd();
36700 ranu 1354
	} else if (state.mode === 'edit' && state.beatTitle) {
1355
		// Edit header used to be injected via a one-time prepend() in editBeat's
1356
		// success callback — that meant any addVisit-triggered re-render wiped the
1357
		// back button / title / base picker / search box. Building it here keeps
1358
		// them present across every re-render so users can keep adding partners.
1359
		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>';
1360
		html += '<div style="font-size:14px;font-weight:600;color:#f59e0b;margin-bottom:4px;">EDITING: ' + state.beatTitle + '</div>';
1361
		html += state.editingDate
1362
			? '<div style="font-size:11px;color:#f59e0b;margin-bottom:8px;">Editing run on: ' + state.editingDate + ' (leads on this date can be removed)</div>'
1363
			: '<div style="font-size:11px;color:#94a3b8;margin-bottom:8px;">Editing beat template (partners only)</div>';
1364
		html += renderBaseLocationPicker();
1365
		html += renderPartnerQuickAdd();
36632 ranu 1366
    }
36621 ranu 1367
	state.days.forEach(function (day) {
1368
		var isCurrent = day.dayNumber === state.currentDay;
36632 ranu 1369
        var isLastDay = day.dayNumber === state.days.length;
36621 ranu 1370
		html += '<div class="route-day">';
1371
		html += '<div class="route-day-header">';
1372
		html += '<span>Day ' + day.dayNumber + (isCurrent ? ' (current)' : '') + '</span>';
1373
		html += '<span>' + fmtMins(day.totalMins) + '</span>';
1374
		html += '</div>';
1375
 
1376
		// Start point
36632 ranu 1377
        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 1378
 
1379
		// Visits with distance between each
1380
		var prevLat = day.startLat, prevLng = day.startLng;
36632 ranu 1381
        var isLastStop;
36621 ranu 1382
		day.visits.forEach(function (v, i) {
36632 ranu 1383
            isLastStop = (i === day.visits.length - 1);
1384
 
36621 ranu 1385
			var segDist = 0, segTime = 0;
1386
			if (prevLat && prevLng && v.lat && v.lng) {
1387
				segDist = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
1388
				segTime = (segDist / AVG_SPEED) * 60;
1389
			}
1390
			if (segDist > 0) {
36711 ranu 1391
				// Show drive ETA and discussion-time dwell separately so the
1392
				// planner can see where the time goes.
1393
				html += '<div class="route-segment"><span class="seg-line"></span><span class="seg-dist">'
1394
					+ segDist.toFixed(1) + ' km | '
1395
					+ fmtMins(segTime) + ' drive '
1396
					+ '<span style="color:#94a3b8;">+ ' + VISIT_MINS + ' Minutes Discussion Time</span>'
1397
					+ '</span></div>';
36621 ranu 1398
			}
1399
 
36642 ranu 1400
            var isLeadVisit = v.isLead || v.type === 'lead';
36711 ranu 1401
			// Outlet/business name is the primary label; code is the muted subtitle.
1402
			// v.name is stored as "CODE - Outlet Name" — strip the code prefix.
1403
			var outletLabel = v.name || '';
1404
			if (v.code && outletLabel.indexOf(v.code + ' - ') === 0) {
1405
				outletLabel = outletLabel.substring(v.code.length + 3);
1406
			}
1407
			if (!outletLabel) outletLabel = v.code || '';
36642 ranu 1408
            html += '<div class="route-stop route-stop-clickable" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" style="cursor:pointer;' + (isLeadVisit ? 'background:#fff3cd;border-left:3px solid #e67e22;' : '') + '">';
1409
            html += '<span class="stop-num" style="' + (isLeadVisit ? 'background:#e67e22;' : '') + '">' + (i + 1) + '</span>';
36865 ranu 1410
			html += '<div class="stop-info"><div class="stop-name">' + outletLabel + (isLeadVisit ? ' <span style="background:#e67e22;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:3px;margin-left:4px;letter-spacing:0.3px;">LEAD VISIT</span>' : '') + '</div>';
36711 ranu 1411
			html += '<div class="stop-meta">' + (v.code || '') + '</div>';
36632 ranu 1412
 
1413
            // Show "Last Visit" + "Day Break" buttons on last stop of current day
1414
            if (isLastStop && isCurrent && !day.endAction) {
1415
                html += '<div style="margin-top:4px;display:flex;gap:4px;">';
1416
                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>';
1417
                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>';
1418
                html += '</div>';
1419
            }
1420
 
1421
            html += '</div>';
36651 ranu 1422
			if (state.mode === 'create' || state.mode === 'edit') {
36632 ranu 1423
                html += '<span class="stop-remove" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" title="Remove">&times;</span>';
1424
            }
36621 ranu 1425
			html += '</div>';
1426
 
1427
			prevLat = v.lat;
1428
			prevLng = v.lng;
1429
		});
1430
 
36632 ranu 1431
        // Show return-to-home only when day is complete (Last Visit) or still in progress
1432
        if (day.visits.length > 0 && day.endAction !== 'DAYBREAK') {
36621 ranu 1433
			var lastV = day.visits[day.visits.length - 1];
1434
			var retDist = 0, retTime = 0;
1435
			if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
1436
				retDist = haversine(lastV.lat, lastV.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
1437
				retTime = (retDist / AVG_SPEED) * 60;
1438
			}
1439
			if (retDist > 0) {
36711 ranu 1440
				// Return-home leg = travel time only (no discussion at home).
1441
				html += '<div class="route-segment"><span class="seg-line seg-line-home"></span><span class="seg-dist">'
1442
					+ retDist.toFixed(1) + ' km | ' + fmtMins(retTime) + ' drive (return)'
1443
					+ '</span></div>';
36621 ranu 1444
			}
1445
			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>';
1446
		}
1447
 
36632 ranu 1448
        // Day break indicator
1449
        if (day.endAction === 'DAYBREAK' && day.visits.length > 0) {
1450
            html += '<div style="margin-left:12px;padding:4px 8px;font-size:11px;color:#f59e0b;font-weight:600;">-- Day Break (stays near last visit) --</div>';
1451
        }
1452
 
36621 ranu 1453
		html += '<div class="day-summary">' + day.visits.length + ' stops | ' + day.totalKm.toFixed(1) + ' km</div>';
1454
		html += '</div>';
1455
	});
1456
 
1457
	$('#route-list').html(html);
1458
}
1459
 
1460
// Click on stop name in route panel → show popup on map
1461
$(document).on('click', '.route-stop-clickable', function (e) {
1462
	if ($(e.target).hasClass('stop-remove')) return; // let remove handler handle it
1463
	var fofoId = parseInt($(this).data('fofoid'));
1464
	if (!fofoId || !markers[fofoId]) return;
1465
 
1466
	var marker = markers[fofoId];
1467
	var p = state.partners.find(function (x) {
1468
		return x.fofoId === fofoId;
1469
	});
1470
	if (!p) return;
1471
 
1472
	map.panTo(marker.getPosition());
1473
	map.setZoom(12);
1474
 
1475
	var name = p.outletName || p.businessName || '';
1476
	var addr = p.address || '';
1477
	infoWindow.setContent('<div class="marker-popup"><h4>' + p.code + ' - ' + name + '</h4><div class="popup-meta">' + addr + '</div></div>');
1478
	infoWindow.open(map, marker);
1479
});
1480
 
1481
// Remove stop from route panel
1482
$(document).on('click', '.stop-remove', function (e) {
1483
	e.stopPropagation();
1484
	var fofoId = parseInt($(this).data('fofoid'));
1485
	var dayNum = parseInt($(this).data('daynum'));
1486
 
1487
	var day = state.days[dayNum - 1];
1488
	if (!day) return;
1489
 
1490
	// Remove visit from day
1491
	day.visits = day.visits.filter(function (v) {
1492
		return v.id !== fofoId;
1493
	});
1494
 
37060 vikas 1495
	// Re-route the remaining stops (nearest-neighbor) before re-numbering below.
1496
	sortDayByNearestNeighbor(day);
1497
 
36621 ranu 1498
	// Reset marker back to red (unvisited)
1499
	if (markers[fofoId]) {
1500
		var p = state.partners.find(function (x) {
1501
			return x.fofoId === fofoId;
1502
		});
1503
		markers[fofoId].setIcon({
1504
			path: google.maps.SymbolPath.CIRCLE, scale: 14,
1505
			fillColor: '#ef4444', fillOpacity: 0.9, strokeColor: '#fff', strokeWeight: 1
1506
		});
1507
		markers[fofoId].setLabel({text: (p ? p.code : '') || '', color: '#fff', fontSize: '10px', fontWeight: '600'});
1508
	}
1509
 
1510
	// Re-number remaining markers for this day
1511
	day.visits.forEach(function (v, i) {
1512
		if (markers[v.id]) {
1513
			markers[v.id].setLabel({text: '' + (i + 1), color: '#fff', fontSize: '11px', fontWeight: '700'});
1514
		}
1515
	});
1516
 
1517
	recalcDay();
1518
	drawRoute();
1519
	renderRoutePanel();
1520
	updateBottomBar();
1521
});
1522
 
36632 ranu 1523
// ============ LAST VISIT (from sidebar) — marks beat as complete, return home ============
1524
$(document).on('click', '.btn-last-visit', function (e) {
1525
    e.stopPropagation();
1526
    var dayNum = parseInt($(this).data('daynum'));
1527
    var day = state.days[dayNum - 1];
1528
    if (!day || day.visits.length === 0) return;
36621 ranu 1529
 
36632 ranu 1530
    var lastVisit = day.visits[day.visits.length - 1];
1531
    if (!confirm('Mark ' + (lastVisit.code || lastVisit.name) + ' as last visit? Beat will end here and return home.')) return;
36621 ranu 1532
 
36632 ranu 1533
    day.endAction = 'HOME';
1534
    drawRoute();
1535
    renderRoutePanel();
1536
    updateBottomBar();
36621 ranu 1537
});
1538
 
36632 ranu 1539
// ============ DAY BREAK (from sidebar) — end day, next day starts from last visit ============
1540
$(document).on('click', '.btn-day-break', function (e) {
1541
    e.stopPropagation();
1542
    var dayNum = parseInt($(this).data('daynum'));
1543
    var day = state.days[dayNum - 1];
1544
    if (!day || day.visits.length === 0) return;
36621 ranu 1545
 
36632 ranu 1546
    var lastVisit = day.visits[day.visits.length - 1];
1547
    if (!confirm('Day Break at ' + (lastVisit.code || lastVisit.name) + '? Next day will start from this location.')) return;
1548
 
1549
    day.endAction = 'DAYBREAK';
1550
 
1551
    // Next day starts from LAST VISIT location (not home)
1552
    var nextNum = state.days.length + 1;
1553
    state.days.push({
1554
        dayNumber: nextNum,
1555
        startLat: lastVisit.lat, startLng: lastVisit.lng, startName: lastVisit.code || lastVisit.name,
1556
        visits: [], endAction: null,
1557
        stayName: null, stayLat: null, stayLng: null,
1558
        totalKm: 0, totalMins: 0
1559
    });
1560
    state.currentDay = nextNum;
1561
 
1562
    resetMarkerColors();
1563
    drawRoute();
36621 ranu 1564
	renderRoutePanel();
36632 ranu 1565
    updateBottomBar();
36621 ranu 1566
});
1567
 
1568
$('#btn-finish').on('click', function () {
36644 ranu 1569
	// Guard 1: already submitting — ignore duplicate clicks
1570
	if (isSubmittingBeat) {
1571
		return;
1572
	}
1573
	// Guard 2: this beat was already saved — don't re-submit
1574
	if (state.savedPlanGroupId) {
1575
		alert('This beat is already saved.');
1576
		showBeatList();
1577
		return;
1578
	}
36621 ranu 1579
	if (state.days.every(function (d) {
1580
		return d.visits.length === 0;
1581
	})) {
1582
		alert('No visits added');
1583
		return;
1584
	}
1585
 
36681 ranu 1586
	var beatTitle = state.beatTitle || 'Beat';
1587
	var isEdit = state.mode === 'edit' && state.editingBeatId;
36621 ranu 1588
 
36681 ranu 1589
	// EDIT RULE: cannot grow day count. Caught client-side so the modal
1590
	// doesn't even open; server enforces the same rule.
1591
	if (isEdit && state.originalDayCount && state.days.length > state.originalDayCount) {
1592
		alert('Cannot increase the number of days while editing.\n\n'
1593
			+ 'Original: ' + state.originalDayCount + ' day(s)\n'
1594
			+ 'Current:  ' + state.days.length + ' day(s)\n\n'
1595
			+ 'Please create a new beat for additional days.');
1596
		return;
1597
	}
1598
 
36711 ranu 1599
	// Build plan data — recompute every day's totals + per-leg distance/time
1600
	// fresh from the current state. recalcDay() only refreshes the CURRENT day,
1601
	// so older days could carry stale values; doing it here guarantees DB rows
1602
	// match what the user sees in the panel.
36621 ranu 1603
	var daysData = [];
1604
	state.days.forEach(function (day) {
1605
		if (day.visits.length === 0) return;
36711 ranu 1606
 
1607
		// Walk the visits in order, computing each leg from the previous point.
1608
		// Day always starts at (startLat, startLng); for the LAST stop, if
1609
		// endAction === 'HOME' we ALSO add the return-to-home leg into the day total
1610
		// (but not stored per-visit, just rolled into totalDistanceKm/totalTimeMins).
1611
		var prevLat = day.startLat, prevLng = day.startLng;
1612
		var dayKm = 0, dayMins = 0;
1613
		var visitsOut = [];
1614
		day.visits.forEach(function (v) {
1615
			var legKm = null, legMins = null;
1616
			if (prevLat != null && prevLng != null && v.lat != null && v.lng != null) {
1617
				legKm = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
1618
				legMins = Math.round((legKm / AVG_SPEED) * 60);
1619
				dayKm += legKm;
1620
				dayMins += legMins;
1621
			}
1622
			visitsOut.push({
1623
				id: v.id,
1624
				type: v.type || 'partner',
1625
				distanceFromPrevKm: legKm != null ? Number(legKm.toFixed(3)) : null,
1626
				timeFromPrevMins: legMins
1627
			});
1628
			if (v.lat != null && v.lng != null) {
1629
				prevLat = v.lat;
1630
				prevLng = v.lng;
1631
			}
1632
		});
1633
		// Return-home leg if applicable
1634
		if (day.endAction === 'HOME' && day.visits.length > 0
1635
			&& state.homeLat != null && state.homeLng != null) {
1636
			var last = day.visits[day.visits.length - 1];
1637
			if (last.lat != null && last.lng != null) {
1638
				var retKm = haversine(last.lat, last.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
1639
				dayKm += retKm;
1640
				dayMins += (retKm / AVG_SPEED) * 60;
1641
			}
1642
		}
1643
		// Add per-stop dwell time
1644
		dayMins += day.visits.length * VISIT_MINS;
1645
 
36621 ranu 1646
		daysData.push({
1647
			dayNumber: day.dayNumber,
1648
			startLocationName: day.startName,
1649
			startLatitude: day.startLat ? day.startLat.toString() : null,
1650
			startLongitude: day.startLng ? day.startLng.toString() : null,
1651
			endAction: day.endAction,
1652
			stayLocationName: day.stayName,
1653
			stayLatitude: day.stayLat ? day.stayLat.toString() : null,
1654
			stayLongitude: day.stayLng ? day.stayLng.toString() : null,
36711 ranu 1655
			totalDistanceKm: Number(dayKm.toFixed(3)),
1656
			totalTimeMins: Math.round(dayMins),
1657
			visits: visitsOut
36621 ranu 1658
		});
1659
	});
1660
 
36681 ranu 1661
	// EDIT RULE: detect removed leads → show popup with cancel/reschedule per lead
1662
	var removedLeads = [];
1663
	if (isEdit && state.originalLeads && state.originalLeads.length) {
1664
		var currentLeadIds = {};
1665
		state.days.forEach(function (d) {
1666
			d.visits.forEach(function (v) {
1667
				if (v.type === 'lead' || v.isLead) currentLeadIds[v.id] = true;
1668
			});
1669
		});
1670
		removedLeads = state.originalLeads.filter(function (l) {
1671
			return !currentLeadIds[l.leadId];
1672
		});
36651 ranu 1673
	}
36681 ranu 1674
 
1675
	if (removedLeads.length > 0) {
1676
		openRemovedLeadsModal(removedLeads, function (actions) {
1677
			doFinishSubmit(daysData, beatTitle, isEdit, actions);
1678
		});
1679
		return;
1680
	}
1681
 
1682
	doFinishSubmit(daysData, beatTitle, isEdit, null);
1683
});
1684
 
1685
function doFinishSubmit(daysData, beatTitle, isEdit, removedLeadActions) {
1686
	var planPayload = {
1687
		days: daysData,
1688
		dates: daysData.map(function () {
1689
			return null;
1690
		}),
1691
		beatName: beatTitle.trim()
1692
	};
1693
	if (state.mode === 'edit' && state.editingDate) planPayload.planDate = state.editingDate;
1694
	if (removedLeadActions && removedLeadActions.length) {
1695
		planPayload.removedLeadActions = JSON.stringify(removedLeadActions);
1696
	}
36651 ranu 1697
	var planData = JSON.stringify(planPayload);
36621 ranu 1698
 
36644 ranu 1699
	isSubmittingBeat = true;
36651 ranu 1700
	$('#btn-finish').prop('disabled', true).text(isEdit ? 'Updating...' : 'Saving...');
36621 ranu 1701
 
36651 ranu 1702
	var url = isEdit ? '/beatPlan/updateBeat' : '/beatPlan/submitPlan';
1703
	var ajaxData = isEdit
1704
		? {beatId: state.editingBeatId, planData: planData}
1705
		: {authUserId: state.authUserId, planData: planData};
1706
 
36621 ranu 1707
	$.ajax({
36651 ranu 1708
		url: context + url,
36621 ranu 1709
		type: "POST",
36651 ranu 1710
		data: ajaxData,
36621 ranu 1711
		success: function (response) {
1712
			var data = (typeof response === 'string' ? JSON.parse(response) : response);
1713
			var result = data.response || data;
1714
 
1715
			if (result.status) {
1716
				state.savedPlanGroupId = result.planGroupId;
1717
				$('#bottom-bar').hide();
1718
 
36681 ranu 1719
				if (result.duplicate) {
36644 ranu 1720
					alert('This beat already exists.');
36651 ranu 1721
				} else if (isEdit) {
36681 ranu 1722
					var extras = [];
1723
					if (result.leadsCancelled) extras.push(result.leadsCancelled + ' lead(s) cancelled');
1724
					if (result.leadsRescheduled) extras.push(result.leadsRescheduled + ' lead(s) rescheduled');
1725
					alert('Beat "' + beatTitle + '" updated successfully'
1726
						+ (extras.length ? '\n\n' + extras.join('\n') : '') + '.');
1727
				} else {
1728
					alert('Beat "' + beatTitle + '" saved successfully!');
1729
				}
36632 ranu 1730
 
36698 ranu 1731
				// Wipe in-progress beat state for this user — the next "+ New Beat"
1732
				// must start from a clean slate (no leftover days/visits/title/leads).
36651 ranu 1733
				state.editingBeatId = null;
36698 ranu 1734
				state.editingDate = null;
1735
				state.beatTitle = null;
1736
				state.days = [];
1737
				state.currentDay = 1;
1738
				state.originalDayCount = null;
1739
				state.originalLeads = [];
1740
				state.savedPlanGroupId = null;
1741
 
1742
				// Clear map artifacts from the just-saved route so the canvas
1743
				// doesn't show stale polylines/labels behind the beat list.
1744
				routeLines.forEach(function (l) {
1745
					l.setMap(null);
1746
				});
1747
				routeLines = [];
1748
				if (previewLine) {
1749
					previewLine.setMap(null);
1750
					previewLine = null;
1751
				}
1752
				if (typeof resetMarkerColors === 'function') resetMarkerColors();
1753
 
36651 ranu 1754
				state.mode = 'list';
36681 ranu 1755
				showBeatList();
36621 ranu 1756
			} else {
36651 ranu 1757
				$('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
36621 ranu 1758
				alert('Error saving beat plan');
1759
			}
36644 ranu 1760
			isSubmittingBeat = false;
36621 ranu 1761
		},
1762
		error: function (xhr) {
36644 ranu 1763
			isSubmittingBeat = false;
36651 ranu 1764
			$('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
36681 ranu 1765
			// Surface a clean server message if present (e.g., day-increase block, missing beat on reschedule date)
1766
			var msg = xhr.responseText || xhr.statusText;
1767
			try {
1768
				var err = JSON.parse(xhr.responseText);
1769
				if (err && err.response) {
1770
					if (typeof err.response === 'string') msg = err.response;
1771
					else if (err.response.message) msg = err.response.message;
1772
				}
1773
			} catch (_) {
1774
			}
1775
			alert(msg);
36621 ranu 1776
		}
1777
	});
36681 ranu 1778
}
36621 ranu 1779
 
36681 ranu 1780
// ============ REMOVED LEADS POPUP ============
1781
// Shown when an edit removes one or more leads. Per lead, the user picks:
1782
//   - Cancel  → mark CANCELLED
1783
//   - Reschedule + date → move to whichever beat the same user has on that date
1784
// The date input is validated against /beatPlan/userBeatsOnDate so the user
1785
// sees "No beat on this date — pick another" before submit.
1786
function openRemovedLeadsModal(removedLeads, onConfirm) {
1787
	$('#modal-removed-leads').remove(); // clean any prior instance
1788
 
1789
	var rowsHtml = removedLeads.map(function (l) {
1790
		return ''
1791
			+ '<div class="rl-row" data-leadid="' + l.leadId + '" style="border:1px solid #334155;border-radius:6px;padding:8px;margin-bottom:8px;">'
1792
			+ '  <div style="font-weight:600;color:#f1f5f9;margin-bottom:6px;">'
1793
			+ '    ' + (l.name || ('Lead #' + l.leadId)) + ' <span style="color:#64748b;font-weight:400;font-size:11px;">(was on day ' + l.dayNumber + ')</span>'
1794
			+ '  </div>'
1795
			+ '  <label style="display:inline-flex;align-items:center;gap:6px;margin-right:14px;cursor:pointer;font-size:12px;">'
1796
			+ '    <input type="radio" name="rl-act-' + l.leadId + '" value="cancel" checked> Cancel this lead'
1797
			+ '  </label>'
1798
			+ '  <label style="display:inline-flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;">'
1799
			+ '    <input type="radio" name="rl-act-' + l.leadId + '" value="reschedule"> Reschedule to another date'
1800
			+ '  </label>'
1801
			+ '  <div class="rl-date-wrap" style="display:none;margin-top:6px;">'
1802
			+ '    <input type="date" class="rl-date" min="' + fmtDate(new Date()) + '" style="width:auto;display:inline-block;margin-bottom:0;">'
1803
			+ '    <span class="rl-date-status" style="font-size:11px;margin-left:8px;"></span>'
1804
			+ '  </div>'
1805
			+ '</div>';
1806
	}).join('');
1807
 
1808
	var html = ''
1809
		+ '<div class="modal-overlay show" id="modal-removed-leads">'
1810
		+ '  <div class="modal-box" style="min-width:520px;max-width:680px;">'
1811
		+ '    <h3>You removed ' + removedLeads.length + ' lead(s) from this beat</h3>'
1812
		+ '    <p style="font-size:12px;color:#94a3b8;margin-bottom:12px;">'
1813
		+ '      Choose what to do with each lead before saving. Reschedule moves the lead to whichever beat this user has on the selected date.'
1814
		+ '    </p>'
1815
		+ '    <div id="rl-list" style="max-height:55vh;overflow-y:auto;">' + rowsHtml + '</div>'
1816
		+ '    <div class="modal-actions">'
1817
		+ '      <button class="modal-cancel" id="rl-cancel">Cancel (keep editing)</button>'
1818
		+ '      <button class="modal-confirm" id="rl-confirm">Save with these actions</button>'
1819
		+ '    </div>'
1820
		+ '  </div>'
1821
		+ '</div>';
1822
	$('body').append(html);
1823
 
1824
	// Show/hide date picker per row
1825
	$('#modal-removed-leads').on('change', 'input[type="radio"]', function () {
1826
		var $row = $(this).closest('.rl-row');
1827
		var isResched = $row.find('input[type="radio"]:checked').val() === 'reschedule';
1828
		$row.find('.rl-date-wrap').toggle(isResched);
1829
		$row.find('.rl-date-status').text('');
1830
	});
1831
 
1832
	// Validate the date as the user types it — show "no beat on this date" warning
1833
	$('#modal-removed-leads').on('change', '.rl-date', function () {
1834
		var $row = $(this).closest('.rl-row');
1835
		var $status = $row.find('.rl-date-status');
1836
		var d = $(this).val();
1837
		if (!d) {
1838
			$status.text('');
1839
			return;
1840
		}
1841
		$status.text('Checking...').css('color', '#94a3b8');
1842
		$.ajax({
1843
			url: context + '/beatPlan/userBeatsOnDate', type: 'GET', dataType: 'json',
1844
			data: {authUserId: state.authUserId, date: d},
1845
			success: function (r) {
1846
				var dd = r.response || r;
1847
				var beats = dd.beats || [];
1848
				if (beats.length === 0) {
1849
					$status.text('No beat on this date — pick another date, or Cancel this lead instead.').css('color', '#ef4444');
1850
				} else {
1851
					var names = beats.map(function (b) {
1852
						return '"' + b.beatName + '" (day ' + b.dayNumber + ')';
1853
					}).join(', ');
1854
					$status.text('Will attach to: ' + names).css('color', '#22c55e');
1855
				}
1856
			},
1857
			error: function () {
1858
				$status.text('Could not verify date').css('color', '#f59e0b');
1859
			}
1860
		});
1861
	});
1862
 
1863
	$('#modal-removed-leads').on('click', '#rl-cancel', function () {
1864
		$('#modal-removed-leads').remove();
1865
		// abort save — user goes back to editing
1866
	});
1867
 
1868
	$('#modal-removed-leads').on('click', '#rl-confirm', function () {
1869
		var actions = [];
1870
		var problems = [];
1871
		$('#modal-removed-leads .rl-row').each(function () {
1872
			var $row = $(this);
1873
			var leadId = parseInt($row.data('leadid'));
1874
			var mode = $row.find('input[type="radio"]:checked').val();
1875
			if (mode === 'reschedule') {
1876
				var d = $row.find('.rl-date').val();
1877
				if (!d) {
1878
					problems.push('Lead ' + leadId + ': pick a date or switch to Cancel');
1879
					return;
1880
				}
1881
				actions.push({leadId: leadId, action: 'reschedule', toDate: d});
1882
			} else {
1883
				actions.push({leadId: leadId, action: 'cancel'});
1884
			}
1885
		});
1886
		if (problems.length) {
1887
			alert(problems.join('\n'));
1888
			return;
1889
		}
1890
		$('#modal-removed-leads').remove();
1891
		onConfirm(actions);
1892
	});
1893
}
1894
 
36621 ranu 1895
// ============ PANEL TABS ============
1896
$('.panel-tab').on('click', function () {
1897
	var tab = $(this).data('tab');
1898
	$('.panel-tab').removeClass('active');
1899
	$(this).addClass('active');
1900
	$('#panel-route').toggle(tab === 'route');
1901
	$('#panel-calendar').toggle(tab === 'calendar');
36792 ranu 1902
	$('#panel-deferred').toggle(tab === 'deferred');
36621 ranu 1903
	$('#cal-grid-container').toggle(tab === 'calendar');
36698 ranu 1904
	$('#bottom-bar').toggle(tab === 'route' && (state.mode === 'create' || state.mode === 'edit') && state.days && state.days.length > 0);
36621 ranu 1905
 
1906
	if (tab === 'calendar' && !state.calMonth) {
1907
		var now = new Date();
1908
		state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
1909
		loadCalendarData();
1910
	}
36792 ranu 1911
	if (tab === 'deferred' && typeof loadDeferredItems === 'function') {
1912
		loadDeferredItems();
1913
	}
36621 ranu 1914
});
1915
 
36644 ranu 1916
// Click a beat chip on the calendar → view that specific day's route
1917
// (partners + only the leads scheduled for that exact date)
1918
$(document).on('click', '.cal-chip-view', function (e) {
1919
	if ($(e.target).hasClass('cal-chip-remove')) return; // handled separately
1920
	e.stopPropagation();
1921
	var pg = $(this).data('plangroup');
1922
	var date = $(this).data('date');
1923
	if (!pg || !date) return;
36740 ranu 1924
 
1925
	// Deferred-assign mode: clicking a beat chip drops the deferred item into
1926
	// that beat on that date (instead of opening the route).
1927
	if (state.assignDeferred) {
1928
		assignDeferredToBeat(String(pg), String(date));
1929
		return;
1930
	}
1931
 
36787 ranu 1932
	// Visit-request scheduling mode: similar handoff but talks to /visitRequest/{id}/approve-schedule.
1933
	if (state.assignVisitRequest) {
1934
		approveVisitRequestOnBeat(String(pg), String(date));
1935
		return;
1936
	}
1937
 
36644 ranu 1938
	// Switch to Route tab and load that day's run
1939
	$('.panel-tab[data-tab="route"]').click();
1940
	viewBeat(String(pg), String(date));
1941
});
1942
 
36740 ranu 1943
// Drop the deferred item (set on entry via URL params) into the chosen beat+date.
1944
function assignDeferredToBeat(beatId, date) {
1945
	var ad = state.assignDeferred;
1946
	if (!ad) return;
1947
	// Must be a FUTURE beat — strictly after the deferred day (and not in the past).
1948
	if (date <= ad.minDate) {
1949
		alert('Pick a later beat. A deferred ' + (ad.type === 'lead' ? 'lead' : 'partner')
1950
			+ ' can only move to a date after ' + ad.minDate + ' — not ' + date + '.');
1951
		return;
1952
	}
1953
	if (!confirm('Add deferred ' + (ad.type === 'lead' ? 'lead' : 'partner') + ' "' + ad.name + '" into this beat on ' + date + '?')) return;
1954
	$.ajax({
1955
		url: context + '/beatPlan/deferred/assignToBeat',
1956
		type: 'POST', contentType: 'application/json',
1957
		data: JSON.stringify({deferredId: ad.id, beatId: parseInt(beatId), date: date}),
1958
		success: function (r) {
1959
			var d = r.response || r;
36761 ranu 1960
			var label = (ad.type === 'lead' ? 'Lead' : 'Partner') + ' "' + ad.name + '"';
1961
			var msg = label + ' rescheduled to ' + date + '.';
1962
			$('#deferred-assign-banner').html('<span style="color:#bbf7d0;">' + msg + '</span>');
1963
			// Confirm to the head (name included so they don't have to read the banner),
1964
			// then auto-close the parent's iframe modal. The parent's `hidden.bs.modal`
1965
			// handler reloads the deferred list (rescheduled row drops off).
1966
			alert(msg);
36792 ranu 1967
			// Clear the assign-mode so subsequent chip clicks don't try to
1968
			// re-schedule the same (now-resolved) deferred row and trip the
1969
			// "already scheduled" guard. Refresh the in-page deferred panel and
1970
			// switch back to the route tab.
1971
			state.assignDeferred = null;
1972
			if (typeof loadDeferredItems === 'function') loadDeferredItems();
1973
			$('.panel-tab[data-tab="route"]').click();
36761 ranu 1974
			if (window.parent && window.parent !== window) {
1975
				try {
1976
					window.parent.$('#defCalModal').modal('hide');
1977
				} catch (e) {
1978
				}
1979
			}
36740 ranu 1980
		},
1981
		error: function (xhr) {
1982
			var msg = 'Failed';
1983
			try {
1984
				var err = JSON.parse(xhr.responseText);
1985
				if (err && err.response) msg = (typeof err.response === 'string') ? err.response : (err.response.message || msg);
1986
			} catch (_) {
1987
			}
1988
			alert(msg);
1989
		}
1990
	});
1991
}
1992
 
36621 ranu 1993
// ============ CALENDAR ============
1994
$('#cal-prev').on('click', function () {
1995
	navMonth(-1);
1996
});
1997
$('#cal-next').on('click', function () {
1998
	navMonth(1);
1999
});
2000
 
2001
function navMonth(delta) {
2002
	var p = state.calMonth.split('-');
2003
	var d = new Date(parseInt(p[0]), parseInt(p[1]) - 1 + delta, 1);
2004
	state.calMonth = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2);
2005
	loadCalendarData();
2006
}
2007
 
2008
function loadCalendarData() {
2009
	if (!state.authUserId) return;
2010
	$('#cal-month-label').text(state.calMonth);
2011
 
2012
	$.ajax({
2013
		url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
2014
		data: {authUserId: state.authUserId, month: state.calMonth},
2015
		success: function (r) {
2016
			var data = r.response || r;
2017
			state.calData = data;
2018
			renderCalGrid(data);
2019
			renderBeatCards(data.scheduledBeats);
2020
		}
2021
	});
2022
}
2023
 
2024
function renderCalGrid(data) {
2025
	var p = state.calMonth.split('-');
2026
	var year = parseInt(p[0]), month = parseInt(p[1]) - 1;
2027
	var first = new Date(year, month, 1);
2028
	var last = new Date(year, month + 1, 0);
2029
	var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
2030
	$('#cal-month-label').text(months[month] + ' ' + year);
2031
 
2032
	var holidayMap = {};
2033
	(data.holidays || []).forEach(function (h) {
2034
		holidayMap[h.date] = h.occasion;
2035
	});
2036
	var blockedSet = {};
2037
	(data.blockedDates || []).forEach(function (d) {
2038
		blockedSet[d] = true;
2039
	});
2040
 
2041
	var beatDateMap = {};
2042
	(data.scheduledBeats || []).forEach(function (b) {
2043
		(b.days || []).forEach(function (d) {
2044
			if (d.planDate) {
2045
				if (!beatDateMap[d.planDate]) beatDateMap[d.planDate] = [];
2046
				beatDateMap[d.planDate].push({
2047
					name: b.beatName,
2048
					color: b.beatColor,
2049
					day: d.dayNumber,
2050
					status: b.status,
36632 ranu 2051
                    visits: d.visitCount,
2052
                    planGroupId: b.planGroupId
36621 ranu 2053
				});
2054
			}
2055
		});
2056
	});
2057
 
2058
	var today = fmtDate(new Date());
2059
	var startOff = (first.getDay() + 6) % 7;
2060
	var calStart = new Date(year, month, 1 - startOff);
2061
 
2062
	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>';
2063
	var d = new Date(calStart);
2064
	for (var w = 0; w < 6; w++) {
2065
		var hasDay = false;
2066
		var row = '<tr>';
2067
		for (var wd = 0; wd < 7; wd++) {
2068
			var ds = fmtDate(d);
2069
			var inMonth = d.getMonth() === month;
2070
			var sun = d.getDay() === 0;
2071
			var hol = !!holidayMap[ds];
2072
			var isToday = ds === today;
2073
			var beats = beatDateMap[ds] || [];
2074
			var isPicked = state.calPickedDates.indexOf(ds) > -1;
2075
 
36632 ranu 2076
            var isPast = ds < today;
2077
            var hasBeats = beats.length > 0;
2078
 
36621 ranu 2079
			var cls = [];
36632 ranu 2080
            if (hol && !sun) cls.push('holiday', 'blocked');
2081
            else if (isPast) cls.push('blocked', 'past');
2082
            if (sun && !isPast) cls.push('sunday'); // Sundays styled but not blocked
2083
            if (sun && isPast) cls.push('blocked', 'past');
36621 ranu 2084
			if (isToday) cls.push('today');
2085
			if (!inMonth) cls.push('blocked');
2086
 
2087
			var style = inMonth ? '' : 'opacity:0.3;';
36632 ranu 2088
            if (isPast && inMonth) style += 'opacity:0.5;';
36621 ranu 2089
 
36632 ranu 2090
            row += '<td class="' + cls.join(' ') + '" data-date="' + ds + '" data-hasbeat="' + (hasBeats ? '1' : '0') + '" style="' + style + '">';
36621 ranu 2091
			row += '<div class="cal-date">' + d.getDate() + '</div>';
2092
			if (hol) row += '<div class="cal-holiday-label">' + holidayMap[ds] + '</div>';
2093
 
2094
			beats.forEach(function (b) {
36785 ranu 2095
				// The "running"/[live] state only applies to TODAY's chip — future
2096
				// chips of the same beat are still freely editable (drag + ×).
2097
				// Past chips are always locked.
2098
				var isLiveChip = b.status === 'running' && isToday;
2099
				var isLockedChip = isPast || isLiveChip;
2100
 
36621 ranu 2101
				var chipCls = 'cal-chip';
36785 ranu 2102
				if (isLiveChip) chipCls += ' running';
36621 ranu 2103
				if (b.status === 'completed') chipCls += ' completed';
36785 ranu 2104
				var removeBtn = !isLockedChip
36632 ranu 2105
                    ? '<span class="cal-chip-remove" data-date="' + ds + '" data-plangroup="' + (b.planGroupId || '') + '" title="Remove">&times;</span>'
2106
                    : '';
36785 ranu 2107
				var draggable = !isLockedChip ? ' draggable="true"' : '';
36670 ranu 2108
				var cursor = draggable ? 'grab' : 'pointer';
2109
				row += '<span class="' + chipCls + ' cal-chip-view"' + draggable + ' data-plangroup="' + (b.planGroupId || '') + '" data-date="' + ds + '" style="background:' + b.color + ';position:relative;cursor:' + cursor + ';" title="Drag to another date to shuffle">'
36785 ranu 2110
					+ b.name + ' D' + b.day + (isLiveChip ? ' [live]' : '') + ' (' + b.visits + ')'
36632 ranu 2111
                    + removeBtn + '</span>';
36621 ranu 2112
			});
2113
 
2114
			if (isPicked) {
2115
				var idx = state.calPickedDates.indexOf(ds) + 1;
2116
				row += '<span class="cal-chip" style="background:#3b82f6;border:1px dashed #93c5fd;">Day ' + idx + '</span>';
2117
			}
2118
 
2119
			row += '</td>';
2120
			if (inMonth) hasDay = true;
2121
			d.setDate(d.getDate() + 1);
2122
		}
2123
		row += '</tr>';
2124
		if (hasDay) html += row;
2125
	}
2126
	html += '</table>';
2127
	$('#cal-grid-content').html(html);
2128
}
2129
 
36700 ranu 2130
// Lists the scheduled dates for a beat (grouped by day number) inside a beat card.
2131
// Output looks like:  "Schedule: D1 May 27 | D2 May 28 | D3 May 29"
2132
// Unscheduled days show as "—"; if NO real dates exist, returns empty string.
2133
function renderBeatScheduledDates(instances) {
2134
	if (!instances || !instances.length) return '';
2135
 
2136
	// Collect planDates per dayNumber across every instance of this beat
2137
	var byDay = {};
2138
	var today = fmtDate(new Date());
2139
	instances.forEach(function (inst) {
2140
		(inst.days || []).forEach(function (d) {
2141
			if (!d.planDate) return; // skip unscheduled placeholders
2142
			if (!byDay[d.dayNumber]) byDay[d.dayNumber] = [];
2143
			byDay[d.dayNumber].push(d.planDate);
2144
		});
2145
	});
2146
 
2147
	var dayNums = Object.keys(byDay).map(function (n) {
2148
		return parseInt(n, 10);
2149
	}).sort(function (a, b) {
2150
		return a - b;
2151
	});
2152
	if (dayNums.length === 0) {
2153
		return '<div class="beat-meta" style="color:#475569; margin-top:4px;">Not scheduled yet</div>';
2154
	}
2155
 
2156
	var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
2157
 
2158
	function fmt(ds) {
2159
		// ds = "yyyy-mm-dd"
2160
		var p = ds.split('-');
2161
		if (p.length !== 3) return ds;
2162
		return MONTHS[parseInt(p[1], 10) - 1] + ' ' + parseInt(p[2], 10);
2163
	}
2164
 
2165
	var parts = dayNums.map(function (n) {
2166
		// De-dup + sort dates for this day (could repeat if beat scheduled multiple times)
2167
		var dates = (byDay[n] || []).filter(function (v, i, a) {
2168
			return a.indexOf(v) === i;
2169
		}).sort();
2170
		var label = dates.map(function (ds) {
2171
			var color = ds === today ? '#ef4444' : (ds < today ? '#64748b' : '#60a5fa');
2172
			return '<span style="color:' + color + ';">' + fmt(ds) + '</span>';
2173
		}).join(', ');
2174
		return '<span style="color:#94a3b8;">D' + n + '</span> ' + label;
2175
	});
2176
 
2177
	return '<div class="beat-meta" style="margin-top:4px; line-height:1.6;">'
2178
		+ '<span style="color:#64748b;">Schedule:</span> '
2179
		+ parts.join('<span style="color:#334155;"> | </span>')
2180
		+ '</div>';
2181
}
2182
 
36621 ranu 2183
function renderBeatCards(beats) {
2184
	var html = '';
2185
	if (!beats || beats.length === 0) {
2186
		html = '<p style="color:#64748b;font-size:12px;padding:20px;">No beats scheduled. Create a beat first.</p>';
36632 ranu 2187
        $('#cal-beat-cards').html(html);
2188
        return;
36621 ranu 2189
	}
36632 ranu 2190
 
2191
    // Deduplicate by beatName — group beats with same name, show count
2192
    var beatGroups = {};
36621 ranu 2193
	(beats || []).forEach(function (b) {
36632 ranu 2194
        var key = b.beatName || b.planGroupId;
2195
        if (!beatGroups[key]) {
2196
            beatGroups[key] = {
2197
                beatName: b.beatName,
2198
                beatColor: b.beatColor,
2199
                instances: []
2200
            };
2201
        }
2202
        beatGroups[key].instances.push(b);
2203
    });
36621 ranu 2204
 
36632 ranu 2205
    Object.keys(beatGroups).forEach(function (key) {
2206
        var group = beatGroups[key];
2207
        var first = group.instances[0];
2208
        var scheduledCount = group.instances.filter(function (b) {
2209
            return b.status === 'scheduled';
2210
        }).length;
2211
        var unscheduledCount = group.instances.filter(function (b) {
2212
            return b.status === 'unscheduled';
2213
        }).length;
2214
        var totalInstances = group.instances.length;
2215
 
36621 ranu 2216
		var totalV = 0, totalKm = 0;
36632 ranu 2217
        first.days.forEach(function (d) {
2218
            totalV += d.visitCount || 0;
2219
            totalKm += d.totalKm || 0;
2220
        });
36621 ranu 2221
 
36632 ranu 2222
        // Use first unscheduled instance for scheduling, or first overall
2223
        var primaryPg = first.planGroupId;
2224
        var unscheduledInstance = group.instances.find(function (b) {
2225
            return b.status === 'unscheduled';
2226
        });
2227
        if (unscheduledInstance) primaryPg = unscheduledInstance.planGroupId;
2228
 
2229
        var statusText = '';
2230
        if (scheduledCount > 0) statusText += scheduledCount + ' scheduled';
2231
        if (unscheduledCount > 0) statusText += (statusText ? ', ' : '') + unscheduledCount + ' unscheduled';
2232
 
2233
        var isScheduling = state.calSelectedBeat === primaryPg && state.calPickedDates.length > 0;
2234
 
2235
        html += '<div class="beat-card" data-plangroup="' + primaryPg + '" data-beatname="' + (group.beatName || '') + '" draggable="true">';
2236
        html += '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + group.beatColor + ';"></span> ' + group.beatName + '</div>';
36728 vikas 2237
        html += '<div class="beat-meta">' + (first.totalDays || 1) + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km</div>';
36632 ranu 2238
        html += '<div class="beat-meta" style="color:#94a3b8;">' + statusText + ' (' + totalInstances + ' total)</div>';
36700 ranu 2239
		html += renderBeatScheduledDates(group.instances);
36632 ranu 2240
 
2241
        html += '<div class="beat-actions">';
2242
        if (isScheduling) {
36728 vikas 2243
            html += '<button class="btn-sm btn-confirm-schedule" data-pg="' + primaryPg + '" data-days="' + (first.totalDays || 1) + '" style="background:#22c55e;color:#fff;">Confirm (' + state.calPickedDates.length + ' dates)</button>';
36632 ranu 2244
            html += '<button class="btn-sm btn-cancel-schedule" style="background:#475569;color:#e2e8f0;">Cancel</button>';
2245
        } else {
36728 vikas 2246
            html += '<button class="btn-sm btn-schedule" data-pg="' + primaryPg + '" data-days="' + (first.totalDays || 1) + '">Schedule</button>';
36632 ranu 2247
        }
2248
        html += '<button class="btn-sm" style="background:#ef4444;color:#fff;" onclick="deleteBeat(\'' + primaryPg + '\')">Delete</button>';
2249
        html += '</div>';
2250
 
2251
        if (isScheduling) {
2252
            html += '<div style="font-size:10px;color:#60a5fa;margin-top:4px;">Dates: ' + state.calPickedDates.join(', ') + '</div>';
2253
            html += '<div style="font-size:10px;color:#94a3b8;">Click calendar cells to adjust</div>';
2254
        }
2255
 
2256
        html += '<div style="font-size:9px;color:#475569;margin-top:4px;">Drag onto calendar to schedule</div>';
36621 ranu 2257
		html += '</div>';
2258
	});
36632 ranu 2259
 
36621 ranu 2260
	$('#cal-beat-cards').html(html);
2261
}
2262
 
2263
// Schedule button — suggest dates, show Confirm button in beat card
2264
$(document).on('click', '.btn-schedule', function (e) {
2265
	e.stopPropagation();
2266
	var pg = $(this).data('pg');
2267
	var days = parseInt($(this).data('days'));
2268
	state.calSelectedBeat = pg;
2269
	state.calPickedDates = [];
2270
 
2271
	$.ajax({
2272
		url: context + "/beatPlan/availableSlots", type: "GET", dataType: "json",
2273
		data: {authUserId: state.authUserId, month: state.calMonth, daysNeeded: days},
2274
		success: function (r) {
2275
			var data = r.response || r;
2276
			state.calPickedDates = data.suggestedDates || [];
2277
			renderCalGrid(state.calData);
2278
			renderBeatCards(state.calData.scheduledBeats);
2279
			if (state.calPickedDates.length < days) {
2280
				alert('Only ' + state.calPickedDates.length + ' slots available this month. Need ' + days + '.');
2281
			}
2282
		}
2283
	});
2284
});
2285
 
36632 ranu 2286
// Click calendar cell during Schedule flow — auto-fill consecutive days from clicked date
2287
$(document).on('click', '#cal-grid-content td', function () {
36621 ranu 2288
	if (!state.calSelectedBeat) return;
2289
	var ds = $(this).data('date');
36632 ranu 2290
    if (!ds || isDateBlocked(ds)) return;
2291
 
2292
    // Find the beat to get days needed
2293
    var beat = (state.calData.scheduledBeats || []).find(function (b) {
36644 ranu 2294
		return String(b.planGroupId) === String(state.calSelectedBeat);
36632 ranu 2295
    });
36728 vikas 2296
    var daysNeeded = beat ? (beat.totalDays || 1) : 1;
36632 ranu 2297
 
2298
    // Auto-fill consecutive slots from clicked date
2299
    var slots = findConsecutiveSlots(ds, daysNeeded);
2300
    if (slots) {
2301
        state.calPickedDates = slots;
2302
        renderCalGrid(state.calData);
2303
        renderBeatCards(state.calData.scheduledBeats);
2304
    }
36621 ranu 2305
});
2306
 
2307
// Cancel scheduling mode
2308
$(document).on('click', '.btn-cancel-schedule', function (e) {
2309
	e.stopPropagation();
2310
	state.calSelectedBeat = null;
2311
	state.calPickedDates = [];
2312
	renderCalGrid(state.calData);
2313
	renderBeatCards(state.calData.scheduledBeats);
2314
});
2315
 
2316
// Confirm schedule — save to server
2317
$(document).on('click', '.btn-confirm-schedule', function (e) {
2318
	e.stopPropagation();
2319
	var pg = $(this).data('pg');
2320
	var daysNeeded = parseInt($(this).data('days'));
2321
 
2322
	if (state.calPickedDates.length < daysNeeded) {
2323
		alert('Need ' + daysNeeded + ' dates but only ' + state.calPickedDates.length + ' selected.');
2324
		return;
2325
	}
2326
 
2327
	var beat = (state.calData.scheduledBeats || []).find(function (b) {
36644 ranu 2328
		return String(b.planGroupId) === String(pg);
36621 ranu 2329
	});
2330
	if (!beat) return;
2331
 
2332
	$.ajax({
2333
		url: context + "/beatPlan/scheduleOnCalendar", type: "POST",
2334
		data: {
2335
			planGroupId: pg,
2336
			dates: JSON.stringify(state.calPickedDates.slice(0, daysNeeded)),
2337
			beatName: beat.beatName,
2338
			beatColor: beat.beatColor
2339
		},
2340
		success: function () {
2341
			state.calSelectedBeat = null;
2342
			state.calPickedDates = [];
2343
			loadCalendarData();
2344
		},
2345
		error: function (xhr) {
2346
			alert('Error: ' + (xhr.responseText || xhr.statusText));
2347
		}
2348
	});
2349
});
2350
 
36632 ranu 2351
// ============ DRAG & DROP BEAT TO CALENDAR ============
2352
$(document).on('dragstart', '.beat-card', function (e) {
2353
    var pg = $(this).data('plangroup');
2354
    var beatName = $(this).data('beatname');
2355
    e.originalEvent.dataTransfer.setData('text/plain', pg);
2356
    e.originalEvent.dataTransfer.setData('beatname', beatName || '');
2357
    $(this).css('opacity', '0.5');
2358
});
36621 ranu 2359
 
36632 ranu 2360
$(document).on('dragend', '.beat-card', function () {
2361
    $(this).css('opacity', '1');
2362
});
36621 ranu 2363
 
36632 ranu 2364
// Calendar cells accept drops
2365
$(document).on('dragover', '#cal-grid-content td', function (e) {
2366
    var ds = $(this).data('date');
2367
    if (!ds) return;
2368
    if (isDateBlocked(ds)) return; // don't allow drop indicator on blocked dates
2369
    e.preventDefault();
2370
    $(this).css('background', '#1e3a5f');
2371
});
2372
 
2373
$(document).on('dragleave', '#cal-grid-content td', function () {
2374
    $(this).css('background', '');
2375
});
2376
 
2377
$(document).on('drop', '#cal-grid-content td', function (e) {
2378
    e.preventDefault();
2379
    $(this).css('background', '');
2380
 
2381
    var pg = e.originalEvent.dataTransfer.getData('text/plain');
2382
    var dropDate = $(this).data('date');
2383
    if (!pg || !dropDate) return;
2384
 
2385
    if (isDateBlocked(dropDate)) {
2386
        alert('Cannot schedule on this date (past, Sunday, or holiday)');
2387
        return;
2388
    }
2389
 
2390
    var beat = (state.calData.scheduledBeats || []).find(function (b) {
36644 ranu 2391
		return String(b.planGroupId) === String(pg);
36632 ranu 2392
    });
2393
    if (!beat) return;
2394
    var beatName = beat.beatName || 'Beat';
36728 vikas 2395
    var daysNeeded = beat.totalDays || 1;
36632 ranu 2396
 
2397
    // Auto-fill consecutive available days starting from drop date
2398
    var scheduleDates = findConsecutiveSlots(dropDate, daysNeeded);
2399
 
2400
    if (!scheduleDates) return; // alerts already shown inside findConsecutiveSlots
2401
 
2402
    var dateStr = scheduleDates.map(function (d, i) {
2403
        return 'Day ' + (i + 1) + ': ' + d;
2404
    }).join('\n');
2405
    if (!confirm('Schedule "' + beatName + '" (' + daysNeeded + ' days):\n\n' + dateStr + '\n\nConfirm?')) return;
2406
 
2407
    $.ajax({
2408
        url: context + "/beatPlan/repeatBeat", type: "POST",
2409
        data: {
2410
            sourcePlanGroupId: pg,
2411
            authUserId: state.authUserId,
2412
            dates: JSON.stringify(scheduleDates)
2413
        },
2414
        success: function () {
2415
            loadCalendarData();
2416
        },
2417
        error: function (xhr) {
2418
            alert('Error: ' + (xhr.responseText || xhr.statusText));
2419
        }
2420
    });
2421
});
2422
 
2423
// Find N consecutive available days starting from startDate, skipping Sundays/holidays
2424
// Returns array of date strings, or null if can't fit
2425
function findConsecutiveSlots(startDateStr, daysNeeded) {
2426
    var startDate = new Date(startDateStr);
2427
    var startMonth = startDate.getMonth();
2428
    var dates = [];
2429
    var d = new Date(startDate);
2430
    var sundayAsked = false;
2431
    var includeSundays = false;
2432
 
2433
    while (dates.length < daysNeeded) {
2434
        var ds = fmtDate(d);
2435
 
2436
        // Rule 3: must stay within same month
2437
        if (d.getMonth() !== startMonth) {
2438
            alert('Beat has ' + daysNeeded + ' days but only ' + dates.length + ' available days left in this month starting from ' + startDateStr + '. Try an earlier date.');
2439
            return null;
36621 ranu 2440
		}
36632 ranu 2441
 
36962 vikas 2442
        // Past dates — skip. Today is skipped too, unless this is an L4+ operator.
36632 ranu 2443
        var today = fmtDate(new Date());
36962 vikas 2444
        if (ds < today || (ds === today && !state.canScheduleToday)) {
36632 ranu 2445
            d.setDate(d.getDate() + 1);
2446
            continue;
2447
        }
2448
 
2449
        // Holidays — always skip
2450
        if (isHoliday(ds)) {
2451
            d.setDate(d.getDate() + 1);
2452
            continue;
2453
        }
2454
 
2455
        // Sunday — ask once whether to include
2456
        if (isSunday(ds)) {
2457
            if (!sundayAsked) {
2458
                sundayAsked = true;
2459
                includeSundays = confirm('This beat falls on a Sunday (' + ds + '). Include Sundays in the schedule?');
2460
            }
2461
            if (!includeSundays) {
2462
                d.setDate(d.getDate() + 1);
2463
                continue;
2464
            }
2465
        }
2466
 
2467
        // Already occupied — block
2468
        if (getBeatsOnDate(ds).length > 0) {
2469
            alert('Cannot schedule: ' + ds + ' already has a beat scheduled. Consecutive days not available from ' + startDateStr + '.');
2470
            return null;
2471
        }
2472
 
2473
        dates.push(ds);
2474
        d.setDate(d.getDate() + 1);
2475
    }
2476
 
2477
    return dates;
2478
}
2479
 
2480
function isDateBlocked(ds) {
2481
    var today = fmtDate(new Date());
36962 vikas 2482
    if (ds < today) return true; // past
2483
    if (ds === today && !state.canScheduleToday) return true; // today blocked unless L4+
36632 ranu 2484
    // Sundays NOT blocked — handled via confirm dialog instead
2485
    // Only block holidays
2486
    if (state.calData && state.calData.blockedDates) {
2487
        var blocked = state.calData.blockedDates;
2488
        // blockedDates from server includes Sundays+holidays, but we only block holidays now
2489
        var d = new Date(ds);
2490
        if (d.getDay() !== 0) { // not Sunday — check holidays
2491
            if (Array.isArray(blocked) && blocked.indexOf(ds) > -1) return true;
2492
            if (typeof blocked === 'object' && blocked[ds]) return true;
2493
        }
2494
    }
2495
    return false;
2496
}
2497
 
2498
function isSunday(ds) {
2499
    return new Date(ds).getDay() === 0;
2500
}
2501
 
2502
function isHoliday(ds) {
2503
    if (!state.calData || !state.calData.holidays) return false;
2504
    return state.calData.holidays.some(function (h) {
2505
        return h.date === ds;
2506
    });
2507
}
2508
 
2509
function getBeatsOnDate(ds) {
2510
    var result = [];
2511
    if (!state.calData || !state.calData.scheduledBeats) return result;
2512
    state.calData.scheduledBeats.forEach(function (b) {
2513
        (b.days || []).forEach(function (d) {
2514
            if (d.planDate === ds) result.push({planGroupId: b.planGroupId, beatName: b.beatName});
2515
        });
2516
    });
2517
    return result;
2518
}
2519
 
2520
// ============ REMOVE BEAT FROM CALENDAR DATE ============
2521
$(document).on('click', '.cal-chip-remove', function (e) {
2522
    e.stopPropagation();
2523
    var date = $(this).data('date');
2524
    var planGroupId = $(this).data('plangroup');
2525
    if (!planGroupId || !date) return;
2526
 
36670 ranu 2527
	if (!confirm('Unschedule this beat from ' + date + '?\n\n(The beat stays in the list — only this date is cleared.)')) return;
36632 ranu 2528
 
36670 ranu 2529
	// Unschedule ONLY this date — the beat itself is preserved
36632 ranu 2530
    $.ajax({
36670 ranu 2531
		url: context + "/beatPlan/unscheduleDate", type: "POST",
2532
		data: {planGroupId: String(planGroupId), date: date},
36632 ranu 2533
        success: function () {
2534
            loadCalendarData();
2535
        },
2536
        error: function (xhr) {
2537
            alert('Error: ' + (xhr.responseText || xhr.statusText));
2538
        }
36621 ranu 2539
	});
2540
});
2541
 
36670 ranu 2542
// ============ CALENDAR SHUFFLE (drag chip to another date) ============
2543
$(document).on('dragstart', '.cal-chip-view', function (e) {
2544
	var pg = $(this).data('plangroup');
2545
	var date = $(this).data('date');
2546
	if (!pg || !date) {
2547
		e.preventDefault();
2548
		return;
2549
	}
2550
	var payload = JSON.stringify({pg: String(pg), fromDate: String(date)});
2551
	var dt = e.originalEvent.dataTransfer;
2552
	dt.effectAllowed = 'move';
2553
	dt.setData('text/plain', payload);
2554
	$(this).css('opacity', '0.4');
2555
});
2556
 
2557
$(document).on('dragend', '.cal-chip-view', function () {
2558
	$(this).css('opacity', '');
2559
});
2560
 
2561
$(document).on('dragover', '.cal-grid td', function (e) {
2562
	var $td = $(this);
36785 ranu 2563
	// Today is the live/running slot — nothing can be shuffled onto it. The
2564
	// shuffle is only valid between future dates.
2565
	if ($td.hasClass('past') || $td.hasClass('blocked') || $td.hasClass('holiday') || $td.hasClass('sunday') || $td.hasClass('today')) return;
36670 ranu 2566
	e.preventDefault();
2567
	e.originalEvent.dataTransfer.dropEffect = 'move';
2568
	$td.css('box-shadow', 'inset 0 0 0 2px #60a5fa');
2569
});
2570
 
2571
$(document).on('dragleave', '.cal-grid td', function () {
2572
	$(this).css('box-shadow', '');
2573
});
2574
 
2575
$(document).on('drop', '.cal-grid td', function (e) {
2576
	var $td = $(this);
2577
	$td.css('box-shadow', '');
36785 ranu 2578
	if ($td.hasClass('past') || $td.hasClass('blocked') || $td.hasClass('holiday') || $td.hasClass('sunday') || $td.hasClass('today')) return;
36670 ranu 2579
	e.preventDefault();
2580
 
2581
	var raw;
2582
	try {
2583
		raw = e.originalEvent.dataTransfer.getData('text/plain');
2584
	} catch (_) {
2585
		return;
2586
	}
2587
	if (!raw) return;
2588
	var data;
2589
	try {
2590
		data = JSON.parse(raw);
2591
	} catch (_) {
2592
		return;
2593
	}
2594
	if (!data.pg || !data.fromDate) return;
2595
 
2596
	var toDate = $td.data('date');
2597
	if (!toDate || toDate === data.fromDate) return;
2598
 
2599
	$.ajax({
2600
		url: context + "/beatPlan/moveScheduleDate", type: "POST",
2601
		data: {planGroupId: data.pg, fromDate: data.fromDate, toDate: String(toDate)},
2602
		success: function () {
2603
			loadCalendarData();
2604
		},
2605
		error: function (xhr) {
2606
			var msg = 'Move failed';
2607
			try {
2608
				var err = JSON.parse(xhr.responseText);
2609
				if (err && err.response) {
2610
					if (typeof err.response === 'string') msg = err.response;
2611
					else if (err.response.message) msg = err.response.message;
2612
				}
2613
			} catch (_) {
2614
			}
2615
			alert(msg);
2616
		}
2617
	});
2618
});
2619
 
36621 ranu 2620
// ============ DELETE BEAT ============
2621
function deleteBeat(planGroupId) {
2622
	if (!confirm('Delete this beat? This cannot be undone.')) return;
2623
	$.ajax({
2624
		url: context + "/beatPlan/delete", type: "POST",
2625
		data: {planGroupId: planGroupId},
2626
		success: function () {
2627
			loadCalendarData();
2628
		},
2629
		error: function (xhr) {
2630
			alert('Error: ' + (xhr.responseText || xhr.statusText));
2631
		}
2632
	});
2633
}
2634
 
2635
// ============ HELPERS ============
2636
function curDay() {
2637
	return state.days[state.currentDay - 1];
2638
}
2639
 
2640
function haversine(lat1, lng1, lat2, lng2) {
2641
	var R = 6371, dLat = (lat2 - lat1) * Math.PI / 180, dLng = (lng2 - lng1) * Math.PI / 180;
2642
	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);
2643
	return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
2644
}
2645
 
2646
function fmtMins(m) {
2647
	return Math.floor(m / 60) + 'h ' + Math.round(m % 60) + 'm';
2648
}
2649
 
2650
function fmtDate(d) {
2651
	return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
2652
}
2653
 
2654
function fitBounds() {
36761 ranu 2655
	var b = new google.maps.LatLngBounds();
2656
	var any = false;
2657
	if (homeMarker) {
2658
		b.extend(homeMarker.getPosition());
2659
		any = true;
36621 ranu 2660
	}
36761 ranu 2661
 
2662
	// Prefer the current route's stops — the unselected partners on the map can
2663
	// span a much wider area than the actual planned visits, which would zoom
2664
	// the camera all the way out. We want focus on the route the user is viewing.
2665
	try {
2666
		var day = curDay();
2667
		if (day && day.visits && day.visits.length > 0) {
2668
			day.visits.forEach(function (v) {
2669
				if (v.lat && v.lng) {
2670
					b.extend(new google.maps.LatLng(parseFloat(v.lat), parseFloat(v.lng)));
2671
					any = true;
2672
				}
2673
			});
2674
		}
2675
	} catch (e) {
2676
	}
2677
 
2678
	// No route yet (e.g., still picking partners) → fit all rendered markers.
2679
	if (!any) {
2680
		for (var k in markers) {
2681
			b.extend(markers[k].getPosition());
2682
			any = true;
2683
		}
2684
	}
2685
	if (any) map.fitBounds(b);
36621 ranu 2686
}
36650 ranu 2687
 
2688
// ============ AUTO-LOAD FROM URL PARAMS ============
2689
// When opened with ?autoUserId=X[&autoUserName=Y], skip the user-picker
2690
// dropdowns and load that user's beats directly (and jump to Calendar tab).
2691
$(function () {
2692
	// Use a manual parser so we don't depend on URLSearchParams
2693
	function getParam(name) {
2694
		var q = window.location.search.substring(1);
2695
		var pairs = q.split('&');
2696
		for (var i = 0; i < pairs.length; i++) {
2697
			var kv = pairs[i].split('=');
2698
			if (decodeURIComponent(kv[0]) === name) {
2699
				return kv.length > 1 ? decodeURIComponent(kv[1].replace(/\+/g, ' ')) : '';
2700
			}
2701
		}
2702
		return null;
2703
	}
2704
 
2705
	var autoUid = getParam('autoUserId');
2706
	console.log('[BEAT-AUTO] autoUserId =', autoUid);
2707
	if (!autoUid) return;
2708
 
2709
	var autoName = getParam('autoUserName');
36651 ranu 2710
	var editBeatId = getParam('editBeatId');
2711
	var editDate = getParam('editDate');
2712
 
36740 ranu 2713
	// Deferred-assign mode (opened from the Deferred Partners panel): the head
2714
	// picks an upcoming beat on the calendar to drop this deferred item into.
2715
	var assignDeferredId = getParam('assignDeferredId');
2716
	if (assignDeferredId) {
2717
		var d2 = new Date();
2718
		var todayStr = d2.getFullYear() + '-' + ('0' + (d2.getMonth() + 1)).slice(-2) + '-' + ('0' + d2.getDate()).slice(-2);
2719
		var deferDate = getParam('deferDate') || '';
2720
		// A deferral can only move FORWARD — never to the day it was deferred or
2721
		// earlier. The floor is the later of (deferred date, today).
2722
		var floor = (deferDate && deferDate > todayStr) ? deferDate : todayStr;
2723
		state.assignDeferred = {
2724
			id: parseInt(assignDeferredId),
2725
			type: getParam('deferType') || 'visit',
2726
			name: getParam('deferName') || 'item',
2727
			deferDate: deferDate,
2728
			minDate: floor // target date must be strictly AFTER this
2729
		};
2730
	}
2731
 
36650 ranu 2732
	state.authUserId = parseInt(autoUid);
2733
	state.categoryId = parseInt($('#bp-category').val()) || 4;
2734
	state.mode = 'list';
2735
	$('#bp-user-label').text(autoName || ('User #' + autoUid));
2736
 
2737
	$.ajax({
2738
		url: context + '/beatPlan/getBaseLocation', type: 'GET', dataType: 'json',
2739
		data: {authUserId: autoUid},
2740
		success: function (r) {
2741
			console.log('[BEAT-AUTO] getBaseLocation ok');
2742
			var data = r.response || r;
2743
			if (data.locationName) {
2744
				state.homeLat = parseFloat(data.latitude);
2745
				state.homeLng = parseFloat(data.longitude);
2746
				state.homeName = data.locationName;
2747
			}
36651 ranu 2748
			if (editBeatId) {
2749
				console.log('[BEAT-AUTO] entering edit for beat', editBeatId, 'date', editDate);
2750
				editBeat(String(editBeatId), editDate || null);
2751
			} else {
2752
				showBeatList();
2753
				setTimeout(function () {
2754
					$('.panel-tab[data-tab="calendar"]').click();
2755
				}, 100);
36740 ranu 2756
				if (state.assignDeferred) {
2757
					// Banner instructing the head to click an upcoming beat.
2758
					var ad = state.assignDeferred;
2759
					$('#deferred-assign-banner').remove();
2760
					$('body').prepend(
2761
						'<div id="deferred-assign-banner" style="position:sticky;top:0;z-index:2000;'
2762
						+ 'background:#0d9488;color:#fff;padding:8px 16px;font-size:13px;">'
2763
						+ 'Adding deferred ' + (ad.type === 'lead' ? 'lead' : 'partner') + ': '
2764
						+ '<strong>' + ad.name + '</strong> — click an upcoming beat on the calendar to drop it in.'
2765
						+ '</div>');
2766
				}
36651 ranu 2767
			}
36650 ranu 2768
		},
2769
		error: function (xhr) {
2770
			console.error('[BEAT-AUTO] getBaseLocation failed:', xhr.status, xhr.responseText);
2771
		}
2772
	});
2773
});
36787 ranu 2774
 
2775
// ============ VISIT REQUEST PANEL ============
2776
// Lives on the route tab (sidebar) when a sales user is loaded. Fetches
2777
// PENDING requests addressed at this user's hierarchy and shows three actions
2778
// per row: Schedule on a beat, Reassign, Reject. Schedule reuses the calendar
2779
// chip-click flow by setting state.assignVisitRequest = {…} and switching
2780
// the panel to the Calendar tab — clicking a future chip then fires the POST.
2781
function loadVisitRequests() {
2782
	if (!state.authUserId) return;
2783
	$.ajax({
2784
		url: context + '/visitRequest/list',
2785
		type: 'GET', dataType: 'json',
36788 ranu 2786
		// Force a fresh fetch — otherwise the browser HTTP cache can serve the
2787
		// pre-reassign list and the panel won't reflect the new ownership.
2788
		cache: false,
36787 ranu 2789
		data: {assigneeAuthId: state.authUserId, status: 'PENDING'},
2790
		success: function (r) {
2791
			var data = r.response || r;
2792
			var rows = (data && data.rows) || [];
2793
			state.visitRequests = rows;
2794
			renderVisitRequests(rows);
2795
		},
2796
		error: function () {
2797
			$('#visit-request-panel').hide();
2798
		}
2799
	});
2800
}
2801
 
2802
function renderVisitRequests(rows) {
2803
	if (!rows || rows.length === 0) {
2804
		$('#visit-request-panel').hide();
2805
		return;
2806
	}
2807
	$('#vr-count').text('(' + rows.length + ')');
2808
	var html = '';
2809
	rows.forEach(function (r) {
2810
		var leadLine = '<strong style="color:#e2e8f0;">' + (r.leadName || ('Lead #' + r.leadId)) + '</strong>';
2811
		if (r.leadOutlet) leadLine += ' · <span style="color:#cbd5e1;">' + r.leadOutlet + '</span>';
2812
		if (r.leadMobile) leadLine += ' · <span style="color:#94a3b8;">' + r.leadMobile + '</span>';
36800 ranu 2813
        var locParts = [];
2814
        if (r.leadCity) locParts.push(r.leadCity);
2815
        if (r.leadState) locParts.push(r.leadState);
2816
        var locLine = locParts.length
2817
            ? '<div style="color:#94a3b8;font-size:11px;">Location: <span style="color:#cbd5e1;">' + locParts.join(', ') + '</span></div>'
2818
            : '';
36787 ranu 2819
		var nearLine = '';
2820
		if (r.nearestStoreCode) nearLine = '<div style="color:#94a3b8;font-size:11px;">Near: ' + r.nearestStoreCode
2821
			+ (r.nearestStoreOutlet ? ' — ' + r.nearestStoreOutlet : '') + '</div>';
2822
		// Show "Name (#authUserId)" so the head can quickly match identities —
2823
		// especially useful when they need the id for the Reassign prompt.
2824
		var reqByLabel = (r.requestedByName || '-') + (r.requestedByAuthId ? ' (#' + r.requestedByAuthId + ')' : '');
2825
		var assigneeLabel = (r.assigneeName || '-') + (r.assigneeAuthId ? ' (#' + r.assigneeAuthId + ')' : '');
2826
		var reqMeta = '<div style="color:#94a3b8;font-size:11px;">'
2827
			+ 'Assignee: <span style="color:#cbd5e1;">' + assigneeLabel + '</span>'
2828
			+ ' · Requested by ' + reqByLabel
2829
			+ (r.requestedDate ? ' · Preferred: <strong style="color:#fbbf24;">' + r.requestedDate + '</strong>' : '')
2830
			+ (r.communicationType ? ' · ' + r.communicationType : '')
2831
			+ '</div>';
2832
		var safeName = String(r.leadName || '').replace(/"/g, '&quot;');
2833
		html += '<div data-rid="' + r.id + '" style="padding:8px;border:1px solid #334155;border-radius:6px;background:#0f172a;margin-bottom:6px;">'
2834
			+ leadLine
36800 ranu 2835
            + locLine
36787 ranu 2836
			+ nearLine
2837
			+ reqMeta
2838
			+ '<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap;">'
2839
			+ '<button class="vr-schedule" data-rid="' + r.id + '" data-lead="' + r.leadId + '" data-leadname="' + safeName + '" data-assignee="' + r.assigneeAuthId + '" style="font-size:11px;padding:3px 8px;border:none;border-radius:4px;background:#22c55e;color:#fff;cursor:pointer;">Schedule on a beat</button>'
36847 ranu 2840
			+ '<button class="vr-new-beat" data-rid="' + r.id + '" data-leadname="' + safeName + '" data-preferred="' + (r.requestedDate || '') + '" style="font-size:11px;padding:3px 8px;border:none;border-radius:4px;background:#3b82f6;color:#fff;cursor:pointer;" title="Use this when the assignee has no beats yet">+ New beat</button>'
36787 ranu 2841
			+ '<button class="vr-reassign" data-rid="' + r.id + '" style="font-size:11px;padding:3px 8px;border:1px solid #475569;border-radius:4px;background:none;color:#cbd5e1;cursor:pointer;">Reassign</button>'
2842
			+ '<button class="vr-reject" data-rid="' + r.id + '" style="font-size:11px;padding:3px 8px;border:none;border-radius:4px;background:#ef4444;color:#fff;cursor:pointer;">Reject</button>'
2843
			+ '</div>'
2844
			+ '</div>';
2845
	});
2846
	$('#vr-list').html(html);
2847
	$('#visit-request-panel').show();
2848
}
2849
 
2850
$(document).on('click', '#vr-refresh', function () {
2851
	loadVisitRequests();
2852
});
2853
 
2854
// Enter "schedule a visit request" mode — same handoff to the calendar chip
2855
// click that the deferred-assign flow uses.
2856
$(document).on('click', '.vr-schedule', function () {
2857
	var $b = $(this);
2858
	state.assignVisitRequest = {
2859
		id: parseInt($b.data('rid')),
2860
		leadId: parseInt($b.data('lead')),
2861
		leadName: String($b.data('leadname') || ''),
2862
		assigneeAuthId: parseInt($b.data('assignee'))
2863
	};
2864
	alert('Click a future beat-chip on the calendar to schedule "'
2865
		+ (state.assignVisitRequest.leadName || ('Lead #' + state.assignVisitRequest.leadId))
2866
		+ '" onto it.');
2867
	$('.panel-tab[data-tab="calendar"]').click();
2868
});
2869
 
36847 ranu 2870
// "+ New beat" — create a single-day beat with just this lead as the Day-1 stop.
2871
// For the case the screen card was added to handle: assignee has zero beats,
2872
// so "Schedule on a beat" has nothing to land on.
2873
$(document).on('click', '.vr-new-beat', function () {
2874
	var $b = $(this);
2875
	var rid = $b.data('rid');
2876
	var leadName = String($b.data('leadname') || 'this lead');
2877
	var preferred = String($b.data('preferred') || '');
2878
	// Prompt for date — preferred wins as default, otherwise tomorrow.
2879
	var tmrw = new Date();
2880
	tmrw.setDate(tmrw.getDate() + 1);
2881
	var def = preferred || tmrw.toISOString().slice(0, 10);
2882
	var date = prompt('Create a new 1-day beat for "' + leadName + '". Date (YYYY-MM-DD):', def);
2883
	if (!date) return;
2884
	date = date.trim();
2885
	if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
2886
		alert('Date must be YYYY-MM-DD');
2887
		return;
2888
	}
2889
	if (!confirm('Create new beat for "' + leadName + '" on ' + date + '?')) return;
2890
	$.ajax({
2891
		url: context + '/visitRequest/' + rid + '/create-beat-and-schedule',
2892
		type: 'POST', contentType: 'application/json',
2893
		data: JSON.stringify({scheduleDate: date}),
2894
		success: function (resp) {
2895
			var r = resp && resp.response ? resp.response : resp;
2896
			alert('Beat "' + (r.beatName || 'New beat') + '" created on ' + (r.scheduleDate || date) + '.');
2897
			loadVisitRequests();
2898
			// Refresh the beat list so the new beat shows up in the left rail.
2899
			$('#bp-load').click();
2900
		},
2901
		error: function (xhr) {
2902
			var msg = 'Could not create the beat';
2903
			try {
2904
				var e = JSON.parse(xhr.responseText);
2905
				if (e && e.response) msg = (typeof e.response === 'string') ? e.response : (e.response.message || msg);
2906
			} catch (_) {
2907
			}
2908
			alert(msg);
2909
		}
2910
	});
2911
});
2912
 
36787 ranu 2913
$(document).on('click', '.vr-reassign', function () {
2914
	var rid = $(this).data('rid');
2915
	var newId = prompt('New assignee auth user id (must be in your downline):');
2916
	if (!newId) return;
2917
	var newIdInt = parseInt(newId);
2918
	if (!newIdInt) return;
2919
	$.ajax({
2920
		url: context + '/visitRequest/' + rid + '/reassign',
2921
		type: 'POST', contentType: 'application/json',
2922
		data: JSON.stringify({newAssigneeAuthId: newIdInt}),
2923
		success: function () {
36788 ranu 2924
			alert('Reassigned to user #' + newIdInt + '. The request will drop off this panel.');
36787 ranu 2925
			loadVisitRequests();
2926
		},
2927
		error: function (xhr) {
2928
			var msg = 'Reassign failed';
2929
			try {
2930
				var e = JSON.parse(xhr.responseText);
2931
				if (e && e.response) msg = (typeof e.response === 'string') ? e.response : (e.response.message || msg);
2932
			} catch (_) {
2933
			}
2934
			alert(msg);
2935
		}
2936
	});
2937
});
2938
 
2939
$(document).on('click', '.vr-reject', function () {
2940
	var rid = $(this).data('rid');
2941
	var reason = prompt('Reject reason (required):');
2942
	if (!reason || !reason.trim()) return;
2943
	$.ajax({
2944
		url: context + '/visitRequest/' + rid + '/reject',
2945
		type: 'POST', contentType: 'application/json',
2946
		data: JSON.stringify({rejectReason: reason.trim()}),
2947
		success: function () {
36788 ranu 2948
			alert('Request rejected. It will drop off this panel.');
36787 ranu 2949
			loadVisitRequests();
2950
		},
2951
		error: function (xhr) {
2952
			var msg = 'Reject failed';
2953
			try {
2954
				var e = JSON.parse(xhr.responseText);
2955
				if (e && e.response) msg = (typeof e.response === 'string') ? e.response : (e.response.message || msg);
2956
			} catch (_) {
2957
			}
2958
			alert(msg);
2959
		}
2960
	});
2961
});
2962
 
36792 ranu 2963
// ============ DEFERRED ITEMS PANEL (inside beat-plan-window) ============
2964
// Reuses the existing /beatPlan/deferred read (hierarchy-scoped) and filters
2965
// client-side to the currently-loaded sales user. Schedule → switches to the
2966
// Calendar tab in deferred-assign mode and reuses assignDeferredToBeat() on
2967
// chip click (existing flow). Cancel posts /beatPlan/deferred/action with a
2968
// required reason. The standalone Deferred Partners page is untouched.
2969
function loadDeferredItems() {
2970
	if (!state.authUserId) return;
2971
	// Wide window so recent misses + any not-yet-acted older ones surface.
2972
	var now = new Date();
2973
 
2974
	function fmt(d) {
2975
		return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
2976
	}
2977
 
2978
	var start = new Date();
2979
	start.setDate(now.getDate() - 30);
2980
	var end = new Date();
2981
	end.setDate(now.getDate() + 30);
2982
	$.ajax({
2983
		url: context + '/beatPlan/deferred',
2984
		type: 'GET', dataType: 'json', cache: false,
2985
		data: {startDate: fmt(start), endDate: fmt(end)},
2986
		success: function (r) {
2987
			var data = r.response || r;
2988
			var all = (data && data.rows) || [];
2989
			var mine = all.filter(function (row) {
2990
				return row.authUserId === state.authUserId;
2991
			});
2992
			state.deferredItems = mine;
2993
			renderDeferredItems(mine);
2994
		},
2995
		error: function () {
2996
			$('#di-list').html('<p style="color:#d9534f;font-size:12px;text-align:center;padding:20px 10px;">Could not load deferred items.</p>');
2997
		}
2998
	});
2999
}
3000
 
3001
function renderDeferredItems(rows) {
3002
	var n = (rows && rows.length) || 0;
3003
	$('#di-count').text('(' + n + ')');
3004
	// Tab pill counter — keeps the head aware of pending items while on other tabs.
3005
	if (n > 0) {
3006
		$('#di-tab-count').text('(' + n + ')').css('color', '#fca5a5');
3007
	} else {
3008
		$('#di-tab-count').text('').css('color', '');
3009
	}
3010
	if (n === 0) {
3011
		$('#di-list').html('<p style="color:#64748b;font-size:12px;text-align:center;padding:20px 10px;">No deferred items for this user.</p>');
3012
		return;
3013
	}
3014
	var html = '';
3015
	rows.forEach(function (r) {
3016
		var safeName = String(r.name || '').replace(/"/g, '&quot;');
3017
		var typeTag = r.type === 'Lead'
3018
			? '<span style="background:#fbbf24;color:#0f172a;font-size:10px;padding:1px 5px;border-radius:3px;margin-right:6px;">Lead</span>'
3019
			: '<span style="background:#60a5fa;color:#0f172a;font-size:10px;padding:1px 5px;border-radius:3px;margin-right:6px;">Visit</span>';
3020
		var nextLine = r.nextScheduledDate
3021
			? '<div style="color:#22c55e;font-size:11px;">Auto-covered on ' + r.nextScheduledDate + '</div>' : '';
3022
		html += '<div data-did="' + r.id + '" style="padding:8px;border:1px solid #334155;border-radius:6px;background:#0f172a;margin-bottom:6px;">'
3023
			+ typeTag
3024
			+ '<strong style="color:#e2e8f0;">' + (r.name || '#' + r.fofoStoreId) + '</strong>'
3025
			+ '<div style="color:#94a3b8;font-size:11px;margin-top:2px;">'
3026
			+ 'Deferred: <span style="color:#fca5a5;">' + (r.deferredDate || '-') + '</span>'
3027
			+ (r.reason ? ' · ' + r.reason : '')
3028
			+ '</div>'
3029
			+ nextLine
3030
			+ '<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap;">'
3031
			+ '<button class="di-schedule" data-did="' + r.id + '" data-type="' + (r.type === 'Lead' ? 'lead' : 'visit') + '" data-name="' + safeName + '" data-deferdate="' + (r.deferredDate || '') + '" style="font-size:11px;padding:3px 8px;border:none;border-radius:4px;background:#22c55e;color:#fff;cursor:pointer;">Schedule on a beat</button>'
3032
			+ '<button class="di-cancel" data-did="' + r.id + '" data-name="' + safeName + '" style="font-size:11px;padding:3px 8px;border:none;border-radius:4px;background:#ef4444;color:#fff;cursor:pointer;">Cancel</button>'
3033
			+ '</div>'
3034
			+ '</div>';
3035
	});
3036
	$('#di-list').html(html);
3037
}
3038
 
3039
$(document).on('click', '#di-refresh', function () {
3040
	loadDeferredItems();
3041
});
3042
 
3043
// Schedule: set state.assignDeferred (existing pattern) and switch to the
3044
// Calendar tab. The existing .cal-chip-view handler dispatches into
3045
// assignDeferredToBeat() when state.assignDeferred is set.
3046
$(document).on('click', '.di-schedule', function () {
3047
	var $b = $(this);
3048
	var deferDate = String($b.data('deferdate') || '');
3049
	var todayObj = new Date();
3050
	var todayStr = todayObj.getFullYear() + '-' + ('0' + (todayObj.getMonth() + 1)).slice(-2) + '-' + ('0' + todayObj.getDate()).slice(-2);
3051
	// Floor: later of the deferred-date OR today — same guard the iframe flow uses.
3052
	var floor = (deferDate && deferDate > todayStr) ? deferDate : todayStr;
3053
	state.assignDeferred = {
3054
		id: parseInt($b.data('did')),
3055
		type: String($b.data('type') || 'visit'),
3056
		name: String($b.data('name') || 'item'),
3057
		deferDate: deferDate,
3058
		minDate: floor
3059
	};
3060
	alert('Click a future beat-chip on the calendar to drop "' + (state.assignDeferred.name) + '" onto it (after ' + floor + ').');
3061
	$('.panel-tab[data-tab="calendar"]').click();
3062
});
3063
 
3064
$(document).on('click', '.di-cancel', function () {
3065
	var did = $(this).data('did');
3066
	var name = String($(this).data('name') || 'this deferred item');
3067
	var reason = prompt('Cancel reason (required) — why is "' + name + '" being cancelled?');
3068
	if (!reason || !reason.trim()) return;
3069
	$.ajax({
3070
		url: context + '/beatPlan/deferred/action',
3071
		type: 'POST', contentType: 'application/json',
3072
		data: JSON.stringify({deferredId: did, action: 'cancel', reason: reason.trim()}),
3073
		success: function () {
3074
			alert('Cancelled. The row will drop off this panel.');
3075
			loadDeferredItems();
3076
		},
3077
		error: function (xhr) {
3078
			var msg = 'Cancel failed';
3079
			try {
3080
				var e = JSON.parse(xhr.responseText);
3081
				if (e && e.response) msg = (typeof e.response === 'string') ? e.response : (e.response.message || msg);
3082
			} catch (_) {
3083
			}
3084
			alert(msg);
3085
		}
3086
	});
3087
});
3088
 
36787 ranu 3089
function approveVisitRequestOnBeat(pg, date) {
3090
	var vr = state.assignVisitRequest;
3091
	if (!vr) return;
3092
	var today = new Date();
3093
	var todayStr = today.getFullYear() + '-' + ('0' + (today.getMonth() + 1)).slice(-2) + '-' + ('0' + today.getDate()).slice(-2);
3094
	if (date <= todayStr) {
3095
		alert('Pick a FUTURE date (after today).');
3096
		return;
3097
	}
3098
	if (!confirm('Schedule "' + (vr.leadName || ('Lead #' + vr.leadId)) + '" onto this beat on ' + date + '?')) return;
3099
	$.ajax({
3100
		url: context + '/visitRequest/' + vr.id + '/approve-schedule',
3101
		type: 'POST', contentType: 'application/json',
3102
		data: JSON.stringify({beatId: parseInt(pg), scheduleDate: date}),
3103
		success: function () {
3104
			state.assignVisitRequest = null;
3105
			alert('Scheduled. The request has been resolved.');
3106
			$('.panel-tab[data-tab="route"]').click();
3107
			loadVisitRequests();
3108
		},
3109
		error: function (xhr) {
3110
			var msg = 'Schedule failed';
3111
			try {
3112
				var er = JSON.parse(xhr.responseText);
3113
				if (er && er.response) msg = (typeof er.response === 'string') ? er.response : (er.response.message || msg);
3114
			} catch (_) {
3115
			}
3116
			alert(msg);
3117
		}
3118
	});
3119
}