Subversion Repositories SmartDukaan

Rev

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

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