Subversion Repositories SmartDukaan

Rev

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