Subversion Repositories SmartDukaan

Rev

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