Subversion Repositories SmartDukaan

Rev

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

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