Subversion Repositories SmartDukaan

Rev

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