Subversion Repositories SmartDukaan

Rev

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