Subversion Repositories SmartDukaan

Rev

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