Subversion Repositories SmartDukaan

Rev

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