Subversion Repositories SmartDukaan

Rev

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